From 78645fdf9d3f2d42d56af35a09d790fed767f5f2 Mon Sep 17 00:00:00 2001
From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com>
Date: Fri, 8 May 2026 14:53:29 +0000
Subject: [PATCH 1/2] Initial plan
From 86f72d459fc03b804d913cdc2f3b17a530972152 Mon Sep 17 00:00:00 2001
From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com>
Date: Fri, 8 May 2026 15:05:13 +0000
Subject: [PATCH 2/2] feat: add work item process inspector panel
Co-authored-by: CompN3rd <1405794+CompN3rd@users.noreply.github.com>
---
media/webviews/workItemSchemaInspector.js | 866 +++++++++++++++++++
package.json | 6 +
scripts/build-webviews.js | 3 +-
src/api/adoClient.ts | 124 +++
src/commands/schemaInspectorCommands.ts | 41 +
src/extension.ts | 8 +
src/views/webview/workItemSchemaInspector.ts | 179 ++++
src/views/webviewTypes.ts | 45 +
src/views/workItemSchemaInspectorPanel.ts | 253 ++++++
9 files changed, 1524 insertions(+), 1 deletion(-)
create mode 100644 media/webviews/workItemSchemaInspector.js
create mode 100644 src/commands/schemaInspectorCommands.ts
create mode 100644 src/views/webview/workItemSchemaInspector.ts
create mode 100644 src/views/workItemSchemaInspectorPanel.ts
diff --git a/media/webviews/workItemSchemaInspector.js b/media/webviews/workItemSchemaInspector.js
new file mode 100644
index 0000000..23fb3b1
--- /dev/null
+++ b/media/webviews/workItemSchemaInspector.js
@@ -0,0 +1,866 @@
+"use strict";
+(() => {
+ // node_modules/@lit/reactive-element/css-tag.js
+ var t = globalThis;
+ var e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype;
+ var s = /* @__PURE__ */ Symbol();
+ var o = /* @__PURE__ */ new WeakMap();
+ var n = class {
+ constructor(t3, e4, o5) {
+ if (this._$cssResult$ = true, o5 !== s) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
+ this.cssText = t3, this.t = e4;
+ }
+ get styleSheet() {
+ let t3 = this.o;
+ const s4 = this.t;
+ if (e && void 0 === t3) {
+ const e4 = void 0 !== s4 && 1 === s4.length;
+ e4 && (t3 = o.get(s4)), void 0 === t3 && ((this.o = t3 = new CSSStyleSheet()).replaceSync(this.cssText), e4 && o.set(s4, t3));
+ }
+ return t3;
+ }
+ toString() {
+ return this.cssText;
+ }
+ };
+ var r = (t3) => new n("string" == typeof t3 ? t3 : t3 + "", void 0, s);
+ var i = (t3, ...e4) => {
+ const o5 = 1 === t3.length ? t3[0] : e4.reduce((e5, s4, o6) => e5 + ((t4) => {
+ if (true === t4._$cssResult$) return t4.cssText;
+ if ("number" == typeof t4) return t4;
+ throw Error("Value passed to 'css' function must be a 'css' function result: " + t4 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
+ })(s4) + t3[o6 + 1], t3[0]);
+ return new n(o5, t3, s);
+ };
+ var S = (s4, o5) => {
+ if (e) s4.adoptedStyleSheets = o5.map((t3) => t3 instanceof CSSStyleSheet ? t3 : t3.styleSheet);
+ else for (const e4 of o5) {
+ const o6 = document.createElement("style"), n4 = t.litNonce;
+ void 0 !== n4 && o6.setAttribute("nonce", n4), o6.textContent = e4.cssText, s4.appendChild(o6);
+ }
+ };
+ var c = e ? (t3) => t3 : (t3) => t3 instanceof CSSStyleSheet ? ((t4) => {
+ let e4 = "";
+ for (const s4 of t4.cssRules) e4 += s4.cssText;
+ return r(e4);
+ })(t3) : t3;
+
+ // node_modules/@lit/reactive-element/reactive-element.js
+ var { is: i2, defineProperty: e2, getOwnPropertyDescriptor: h, getOwnPropertyNames: r2, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object;
+ var a = globalThis;
+ var c2 = a.trustedTypes;
+ var l = c2 ? c2.emptyScript : "";
+ var p = a.reactiveElementPolyfillSupport;
+ var d = (t3, s4) => t3;
+ var u = { toAttribute(t3, s4) {
+ switch (s4) {
+ case Boolean:
+ t3 = t3 ? l : null;
+ break;
+ case Object:
+ case Array:
+ t3 = null == t3 ? t3 : JSON.stringify(t3);
+ }
+ return t3;
+ }, fromAttribute(t3, s4) {
+ let i5 = t3;
+ switch (s4) {
+ case Boolean:
+ i5 = null !== t3;
+ break;
+ case Number:
+ i5 = null === t3 ? null : Number(t3);
+ break;
+ case Object:
+ case Array:
+ try {
+ i5 = JSON.parse(t3);
+ } catch (t4) {
+ i5 = null;
+ }
+ }
+ return i5;
+ } };
+ var f = (t3, s4) => !i2(t3, s4);
+ var b = { attribute: true, type: String, converter: u, reflect: false, useDefault: false, hasChanged: f };
+ Symbol.metadata ?? (Symbol.metadata = /* @__PURE__ */ Symbol("metadata")), a.litPropertyMetadata ?? (a.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
+ var y = class extends HTMLElement {
+ static addInitializer(t3) {
+ this._$Ei(), (this.l ?? (this.l = [])).push(t3);
+ }
+ static get observedAttributes() {
+ return this.finalize(), this._$Eh && [...this._$Eh.keys()];
+ }
+ static createProperty(t3, s4 = b) {
+ if (s4.state && (s4.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t3) && ((s4 = Object.create(s4)).wrapped = true), this.elementProperties.set(t3, s4), !s4.noAccessor) {
+ const i5 = /* @__PURE__ */ Symbol(), h3 = this.getPropertyDescriptor(t3, i5, s4);
+ void 0 !== h3 && e2(this.prototype, t3, h3);
+ }
+ }
+ static getPropertyDescriptor(t3, s4, i5) {
+ const { get: e4, set: r4 } = h(this.prototype, t3) ?? { get() {
+ return this[s4];
+ }, set(t4) {
+ this[s4] = t4;
+ } };
+ return { get: e4, set(s5) {
+ const h3 = e4?.call(this);
+ r4?.call(this, s5), this.requestUpdate(t3, h3, i5);
+ }, configurable: true, enumerable: true };
+ }
+ static getPropertyOptions(t3) {
+ return this.elementProperties.get(t3) ?? b;
+ }
+ static _$Ei() {
+ if (this.hasOwnProperty(d("elementProperties"))) return;
+ const t3 = n2(this);
+ t3.finalize(), void 0 !== t3.l && (this.l = [...t3.l]), this.elementProperties = new Map(t3.elementProperties);
+ }
+ static finalize() {
+ if (this.hasOwnProperty(d("finalized"))) return;
+ if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d("properties"))) {
+ const t4 = this.properties, s4 = [...r2(t4), ...o2(t4)];
+ for (const i5 of s4) this.createProperty(i5, t4[i5]);
+ }
+ const t3 = this[Symbol.metadata];
+ if (null !== t3) {
+ const s4 = litPropertyMetadata.get(t3);
+ if (void 0 !== s4) for (const [t4, i5] of s4) this.elementProperties.set(t4, i5);
+ }
+ this._$Eh = /* @__PURE__ */ new Map();
+ for (const [t4, s4] of this.elementProperties) {
+ const i5 = this._$Eu(t4, s4);
+ void 0 !== i5 && this._$Eh.set(i5, t4);
+ }
+ this.elementStyles = this.finalizeStyles(this.styles);
+ }
+ static finalizeStyles(s4) {
+ const i5 = [];
+ if (Array.isArray(s4)) {
+ const e4 = new Set(s4.flat(1 / 0).reverse());
+ for (const s5 of e4) i5.unshift(c(s5));
+ } else void 0 !== s4 && i5.push(c(s4));
+ return i5;
+ }
+ static _$Eu(t3, s4) {
+ const i5 = s4.attribute;
+ return false === i5 ? void 0 : "string" == typeof i5 ? i5 : "string" == typeof t3 ? t3.toLowerCase() : void 0;
+ }
+ constructor() {
+ super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
+ }
+ _$Ev() {
+ this._$ES = new Promise((t3) => this.enableUpdating = t3), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t3) => t3(this));
+ }
+ addController(t3) {
+ (this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(t3), void 0 !== this.renderRoot && this.isConnected && t3.hostConnected?.();
+ }
+ removeController(t3) {
+ this._$EO?.delete(t3);
+ }
+ _$E_() {
+ const t3 = /* @__PURE__ */ new Map(), s4 = this.constructor.elementProperties;
+ for (const i5 of s4.keys()) this.hasOwnProperty(i5) && (t3.set(i5, this[i5]), delete this[i5]);
+ t3.size > 0 && (this._$Ep = t3);
+ }
+ createRenderRoot() {
+ const t3 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
+ return S(t3, this.constructor.elementStyles), t3;
+ }
+ connectedCallback() {
+ this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(true), this._$EO?.forEach((t3) => t3.hostConnected?.());
+ }
+ enableUpdating(t3) {
+ }
+ disconnectedCallback() {
+ this._$EO?.forEach((t3) => t3.hostDisconnected?.());
+ }
+ attributeChangedCallback(t3, s4, i5) {
+ this._$AK(t3, i5);
+ }
+ _$ET(t3, s4) {
+ const i5 = this.constructor.elementProperties.get(t3), e4 = this.constructor._$Eu(t3, i5);
+ if (void 0 !== e4 && true === i5.reflect) {
+ const h3 = (void 0 !== i5.converter?.toAttribute ? i5.converter : u).toAttribute(s4, i5.type);
+ this._$Em = t3, null == h3 ? this.removeAttribute(e4) : this.setAttribute(e4, h3), this._$Em = null;
+ }
+ }
+ _$AK(t3, s4) {
+ const i5 = this.constructor, e4 = i5._$Eh.get(t3);
+ if (void 0 !== e4 && this._$Em !== e4) {
+ const t4 = i5.getPropertyOptions(e4), h3 = "function" == typeof t4.converter ? { fromAttribute: t4.converter } : void 0 !== t4.converter?.fromAttribute ? t4.converter : u;
+ this._$Em = e4;
+ const r4 = h3.fromAttribute(s4, t4.type);
+ this[e4] = r4 ?? this._$Ej?.get(e4) ?? r4, this._$Em = null;
+ }
+ }
+ requestUpdate(t3, s4, i5, e4 = false, h3) {
+ if (void 0 !== t3) {
+ const r4 = this.constructor;
+ if (false === e4 && (h3 = this[t3]), i5 ?? (i5 = r4.getPropertyOptions(t3)), !((i5.hasChanged ?? f)(h3, s4) || i5.useDefault && i5.reflect && h3 === this._$Ej?.get(t3) && !this.hasAttribute(r4._$Eu(t3, i5)))) return;
+ this.C(t3, s4, i5);
+ }
+ false === this.isUpdatePending && (this._$ES = this._$EP());
+ }
+ C(t3, s4, { useDefault: i5, reflect: e4, wrapped: h3 }, r4) {
+ i5 && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(t3) && (this._$Ej.set(t3, r4 ?? s4 ?? this[t3]), true !== h3 || void 0 !== r4) || (this._$AL.has(t3) || (this.hasUpdated || i5 || (s4 = void 0), this._$AL.set(t3, s4)), true === e4 && this._$Em !== t3 && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(t3));
+ }
+ async _$EP() {
+ this.isUpdatePending = true;
+ try {
+ await this._$ES;
+ } catch (t4) {
+ Promise.reject(t4);
+ }
+ const t3 = this.scheduleUpdate();
+ return null != t3 && await t3, !this.isUpdatePending;
+ }
+ scheduleUpdate() {
+ return this.performUpdate();
+ }
+ performUpdate() {
+ if (!this.isUpdatePending) return;
+ if (!this.hasUpdated) {
+ if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
+ for (const [t5, s5] of this._$Ep) this[t5] = s5;
+ this._$Ep = void 0;
+ }
+ const t4 = this.constructor.elementProperties;
+ if (t4.size > 0) for (const [s5, i5] of t4) {
+ const { wrapped: t5 } = i5, e4 = this[s5];
+ true !== t5 || this._$AL.has(s5) || void 0 === e4 || this.C(s5, void 0, i5, e4);
+ }
+ }
+ let t3 = false;
+ const s4 = this._$AL;
+ try {
+ t3 = this.shouldUpdate(s4), t3 ? (this.willUpdate(s4), this._$EO?.forEach((t4) => t4.hostUpdate?.()), this.update(s4)) : this._$EM();
+ } catch (s5) {
+ throw t3 = false, this._$EM(), s5;
+ }
+ t3 && this._$AE(s4);
+ }
+ willUpdate(t3) {
+ }
+ _$AE(t3) {
+ this._$EO?.forEach((t4) => t4.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t3)), this.updated(t3);
+ }
+ _$EM() {
+ this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
+ }
+ get updateComplete() {
+ return this.getUpdateComplete();
+ }
+ getUpdateComplete() {
+ return this._$ES;
+ }
+ shouldUpdate(t3) {
+ return true;
+ }
+ update(t3) {
+ this._$Eq && (this._$Eq = this._$Eq.forEach((t4) => this._$ET(t4, this[t4]))), this._$EM();
+ }
+ updated(t3) {
+ }
+ firstUpdated(t3) {
+ }
+ };
+ y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[d("elementProperties")] = /* @__PURE__ */ new Map(), y[d("finalized")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: y }), (a.reactiveElementVersions ?? (a.reactiveElementVersions = [])).push("2.1.2");
+
+ // node_modules/lit-html/lit-html.js
+ var t2 = globalThis;
+ var i3 = (t3) => t3;
+ var s2 = t2.trustedTypes;
+ var e3 = s2 ? s2.createPolicy("lit-html", { createHTML: (t3) => t3 }) : void 0;
+ var h2 = "$lit$";
+ var o3 = `lit$${Math.random().toFixed(9).slice(2)}$`;
+ var n3 = "?" + o3;
+ var r3 = `<${n3}>`;
+ var l2 = document;
+ var c3 = () => l2.createComment("");
+ var a2 = (t3) => null === t3 || "object" != typeof t3 && "function" != typeof t3;
+ var u2 = Array.isArray;
+ var d2 = (t3) => u2(t3) || "function" == typeof t3?.[Symbol.iterator];
+ var f2 = "[ \n\f\r]";
+ var v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g;
+ var _ = /-->/g;
+ var m = />/g;
+ var p2 = RegExp(`>|${f2}(?:([^\\s"'>=/]+)(${f2}*=${f2}*(?:[^
+\f\r"'\`<>=]|("|')|))|$)`, "g");
+ var g = /'/g;
+ var $ = /"/g;
+ var y2 = /^(?:script|style|textarea|title)$/i;
+ var x = (t3) => (i5, ...s4) => ({ _$litType$: t3, strings: i5, values: s4 });
+ var b2 = x(1);
+ var w = x(2);
+ var T = x(3);
+ var E = /* @__PURE__ */ Symbol.for("lit-noChange");
+ var A = /* @__PURE__ */ Symbol.for("lit-nothing");
+ var C = /* @__PURE__ */ new WeakMap();
+ var P = l2.createTreeWalker(l2, 129);
+ function V(t3, i5) {
+ if (!u2(t3) || !t3.hasOwnProperty("raw")) throw Error("invalid template strings array");
+ return void 0 !== e3 ? e3.createHTML(i5) : i5;
+ }
+ var N = (t3, i5) => {
+ const s4 = t3.length - 1, e4 = [];
+ let n4, l3 = 2 === i5 ? "" : 3 === i5 ? "" : "")), e4];
+ };
+ var S2 = class _S {
+ constructor({ strings: t3, _$litType$: i5 }, e4) {
+ let r4;
+ this.parts = [];
+ let l3 = 0, a3 = 0;
+ const u3 = t3.length - 1, d3 = this.parts, [f3, v2] = N(t3, i5);
+ if (this.el = _S.createElement(f3, e4), P.currentNode = this.el.content, 2 === i5 || 3 === i5) {
+ const t4 = this.el.content.firstChild;
+ t4.replaceWith(...t4.childNodes);
+ }
+ for (; null !== (r4 = P.nextNode()) && d3.length < u3; ) {
+ if (1 === r4.nodeType) {
+ if (r4.hasAttributes()) for (const t4 of r4.getAttributeNames()) if (t4.endsWith(h2)) {
+ const i6 = v2[a3++], s4 = r4.getAttribute(t4).split(o3), e5 = /([.?@])?(.*)/.exec(i6);
+ d3.push({ type: 1, index: l3, name: e5[2], strings: s4, ctor: "." === e5[1] ? I : "?" === e5[1] ? L : "@" === e5[1] ? z : H }), r4.removeAttribute(t4);
+ } else t4.startsWith(o3) && (d3.push({ type: 6, index: l3 }), r4.removeAttribute(t4));
+ if (y2.test(r4.tagName)) {
+ const t4 = r4.textContent.split(o3), i6 = t4.length - 1;
+ if (i6 > 0) {
+ r4.textContent = s2 ? s2.emptyScript : "";
+ for (let s4 = 0; s4 < i6; s4++) r4.append(t4[s4], c3()), P.nextNode(), d3.push({ type: 2, index: ++l3 });
+ r4.append(t4[i6], c3());
+ }
+ }
+ } else if (8 === r4.nodeType) if (r4.data === n3) d3.push({ type: 2, index: l3 });
+ else {
+ let t4 = -1;
+ for (; -1 !== (t4 = r4.data.indexOf(o3, t4 + 1)); ) d3.push({ type: 7, index: l3 }), t4 += o3.length - 1;
+ }
+ l3++;
+ }
+ }
+ static createElement(t3, i5) {
+ const s4 = l2.createElement("template");
+ return s4.innerHTML = t3, s4;
+ }
+ };
+ function M(t3, i5, s4 = t3, e4) {
+ if (i5 === E) return i5;
+ let h3 = void 0 !== e4 ? s4._$Co?.[e4] : s4._$Cl;
+ const o5 = a2(i5) ? void 0 : i5._$litDirective$;
+ return h3?.constructor !== o5 && (h3?._$AO?.(false), void 0 === o5 ? h3 = void 0 : (h3 = new o5(t3), h3._$AT(t3, s4, e4)), void 0 !== e4 ? (s4._$Co ?? (s4._$Co = []))[e4] = h3 : s4._$Cl = h3), void 0 !== h3 && (i5 = M(t3, h3._$AS(t3, i5.values), h3, e4)), i5;
+ }
+ var R = class {
+ constructor(t3, i5) {
+ this._$AV = [], this._$AN = void 0, this._$AD = t3, this._$AM = i5;
+ }
+ get parentNode() {
+ return this._$AM.parentNode;
+ }
+ get _$AU() {
+ return this._$AM._$AU;
+ }
+ u(t3) {
+ const { el: { content: i5 }, parts: s4 } = this._$AD, e4 = (t3?.creationScope ?? l2).importNode(i5, true);
+ P.currentNode = e4;
+ let h3 = P.nextNode(), o5 = 0, n4 = 0, r4 = s4[0];
+ for (; void 0 !== r4; ) {
+ if (o5 === r4.index) {
+ let i6;
+ 2 === r4.type ? i6 = new k(h3, h3.nextSibling, this, t3) : 1 === r4.type ? i6 = new r4.ctor(h3, r4.name, r4.strings, this, t3) : 6 === r4.type && (i6 = new Z(h3, this, t3)), this._$AV.push(i6), r4 = s4[++n4];
+ }
+ o5 !== r4?.index && (h3 = P.nextNode(), o5++);
+ }
+ return P.currentNode = l2, e4;
+ }
+ p(t3) {
+ let i5 = 0;
+ for (const s4 of this._$AV) void 0 !== s4 && (void 0 !== s4.strings ? (s4._$AI(t3, s4, i5), i5 += s4.strings.length - 2) : s4._$AI(t3[i5])), i5++;
+ }
+ };
+ var k = class _k {
+ get _$AU() {
+ return this._$AM?._$AU ?? this._$Cv;
+ }
+ constructor(t3, i5, s4, e4) {
+ this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t3, this._$AB = i5, this._$AM = s4, this.options = e4, this._$Cv = e4?.isConnected ?? true;
+ }
+ get parentNode() {
+ let t3 = this._$AA.parentNode;
+ const i5 = this._$AM;
+ return void 0 !== i5 && 11 === t3?.nodeType && (t3 = i5.parentNode), t3;
+ }
+ get startNode() {
+ return this._$AA;
+ }
+ get endNode() {
+ return this._$AB;
+ }
+ _$AI(t3, i5 = this) {
+ t3 = M(this, t3, i5), a2(t3) ? t3 === A || null == t3 || "" === t3 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t3 !== this._$AH && t3 !== E && this._(t3) : void 0 !== t3._$litType$ ? this.$(t3) : void 0 !== t3.nodeType ? this.T(t3) : d2(t3) ? this.k(t3) : this._(t3);
+ }
+ O(t3) {
+ return this._$AA.parentNode.insertBefore(t3, this._$AB);
+ }
+ T(t3) {
+ this._$AH !== t3 && (this._$AR(), this._$AH = this.O(t3));
+ }
+ _(t3) {
+ this._$AH !== A && a2(this._$AH) ? this._$AA.nextSibling.data = t3 : this.T(l2.createTextNode(t3)), this._$AH = t3;
+ }
+ $(t3) {
+ const { values: i5, _$litType$: s4 } = t3, e4 = "number" == typeof s4 ? this._$AC(t3) : (void 0 === s4.el && (s4.el = S2.createElement(V(s4.h, s4.h[0]), this.options)), s4);
+ if (this._$AH?._$AD === e4) this._$AH.p(i5);
+ else {
+ const t4 = new R(e4, this), s5 = t4.u(this.options);
+ t4.p(i5), this.T(s5), this._$AH = t4;
+ }
+ }
+ _$AC(t3) {
+ let i5 = C.get(t3.strings);
+ return void 0 === i5 && C.set(t3.strings, i5 = new S2(t3)), i5;
+ }
+ k(t3) {
+ u2(this._$AH) || (this._$AH = [], this._$AR());
+ const i5 = this._$AH;
+ let s4, e4 = 0;
+ for (const h3 of t3) e4 === i5.length ? i5.push(s4 = new _k(this.O(c3()), this.O(c3()), this, this.options)) : s4 = i5[e4], s4._$AI(h3), e4++;
+ e4 < i5.length && (this._$AR(s4 && s4._$AB.nextSibling, e4), i5.length = e4);
+ }
+ _$AR(t3 = this._$AA.nextSibling, s4) {
+ for (this._$AP?.(false, true, s4); t3 !== this._$AB; ) {
+ const s5 = i3(t3).nextSibling;
+ i3(t3).remove(), t3 = s5;
+ }
+ }
+ setConnected(t3) {
+ void 0 === this._$AM && (this._$Cv = t3, this._$AP?.(t3));
+ }
+ };
+ var H = class {
+ get tagName() {
+ return this.element.tagName;
+ }
+ get _$AU() {
+ return this._$AM._$AU;
+ }
+ constructor(t3, i5, s4, e4, h3) {
+ this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t3, this.name = i5, this._$AM = e4, this.options = h3, s4.length > 2 || "" !== s4[0] || "" !== s4[1] ? (this._$AH = Array(s4.length - 1).fill(new String()), this.strings = s4) : this._$AH = A;
+ }
+ _$AI(t3, i5 = this, s4, e4) {
+ const h3 = this.strings;
+ let o5 = false;
+ if (void 0 === h3) t3 = M(this, t3, i5, 0), o5 = !a2(t3) || t3 !== this._$AH && t3 !== E, o5 && (this._$AH = t3);
+ else {
+ const e5 = t3;
+ let n4, r4;
+ for (t3 = h3[0], n4 = 0; n4 < h3.length - 1; n4++) r4 = M(this, e5[s4 + n4], i5, n4), r4 === E && (r4 = this._$AH[n4]), o5 || (o5 = !a2(r4) || r4 !== this._$AH[n4]), r4 === A ? t3 = A : t3 !== A && (t3 += (r4 ?? "") + h3[n4 + 1]), this._$AH[n4] = r4;
+ }
+ o5 && !e4 && this.j(t3);
+ }
+ j(t3) {
+ t3 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t3 ?? "");
+ }
+ };
+ var I = class extends H {
+ constructor() {
+ super(...arguments), this.type = 3;
+ }
+ j(t3) {
+ this.element[this.name] = t3 === A ? void 0 : t3;
+ }
+ };
+ var L = class extends H {
+ constructor() {
+ super(...arguments), this.type = 4;
+ }
+ j(t3) {
+ this.element.toggleAttribute(this.name, !!t3 && t3 !== A);
+ }
+ };
+ var z = class extends H {
+ constructor(t3, i5, s4, e4, h3) {
+ super(t3, i5, s4, e4, h3), this.type = 5;
+ }
+ _$AI(t3, i5 = this) {
+ if ((t3 = M(this, t3, i5, 0) ?? A) === E) return;
+ const s4 = this._$AH, e4 = t3 === A && s4 !== A || t3.capture !== s4.capture || t3.once !== s4.once || t3.passive !== s4.passive, h3 = t3 !== A && (s4 === A || e4);
+ e4 && this.element.removeEventListener(this.name, this, s4), h3 && this.element.addEventListener(this.name, this, t3), this._$AH = t3;
+ }
+ handleEvent(t3) {
+ "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t3) : this._$AH.handleEvent(t3);
+ }
+ };
+ var Z = class {
+ constructor(t3, i5, s4) {
+ this.element = t3, this.type = 6, this._$AN = void 0, this._$AM = i5, this.options = s4;
+ }
+ get _$AU() {
+ return this._$AM._$AU;
+ }
+ _$AI(t3) {
+ M(this, t3);
+ }
+ };
+ var B = t2.litHtmlPolyfillSupport;
+ B?.(S2, k), (t2.litHtmlVersions ?? (t2.litHtmlVersions = [])).push("3.3.2");
+ var D = (t3, i5, s4) => {
+ const e4 = s4?.renderBefore ?? i5;
+ let h3 = e4._$litPart$;
+ if (void 0 === h3) {
+ const t4 = s4?.renderBefore ?? null;
+ e4._$litPart$ = h3 = new k(i5.insertBefore(c3(), t4), t4, void 0, s4 ?? {});
+ }
+ return h3._$AI(t3), h3;
+ };
+
+ // node_modules/lit-element/lit-element.js
+ var s3 = globalThis;
+ var i4 = class extends y {
+ constructor() {
+ super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
+ }
+ createRenderRoot() {
+ var _a;
+ const t3 = super.createRenderRoot();
+ return (_a = this.renderOptions).renderBefore ?? (_a.renderBefore = t3.firstChild), t3;
+ }
+ update(t3) {
+ const r4 = this.render();
+ this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t3), this._$Do = D(r4, this.renderRoot, this.renderOptions);
+ }
+ connectedCallback() {
+ super.connectedCallback(), this._$Do?.setConnected(true);
+ }
+ disconnectedCallback() {
+ super.disconnectedCallback(), this._$Do?.setConnected(false);
+ }
+ render() {
+ return E;
+ }
+ };
+ i4._$litElement$ = true, i4["finalized"] = true, s3.litElementHydrateSupport?.({ LitElement: i4 });
+ var o4 = s3.litElementPolyfillSupport;
+ o4?.({ LitElement: i4 });
+ (s3.litElementVersions ?? (s3.litElementVersions = [])).push("4.2.2");
+
+ // src/views/webview/commonStyles.ts
+ var baseStyles = i`
+ :host {
+ display: block;
+ min-height: 100vh;
+ color: var(--vscode-foreground);
+ background: var(--vscode-editor-background);
+ font-family: var(--vscode-font-family);
+ font-size: var(--vscode-font-size);
+ }
+
+ .shell {
+ padding: 16px;
+ }
+
+ h1 {
+ margin: 0 0 4px;
+ font-size: 1.3em;
+ font-weight: 600;
+ }
+
+ h2 {
+ font-size: 1em;
+ border-bottom: 1px solid var(--vscode-panel-border);
+ padding-bottom: 4px;
+ margin: 0 0 8px;
+ }
+
+ .section {
+ margin-bottom: 20px;
+ }
+
+ .toolbar {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 12px;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ button,
+ select,
+ textarea,
+ input {
+ font: inherit;
+ }
+
+ .btn {
+ padding: 4px 10px;
+ border-radius: 3px;
+ border: 1px solid var(--vscode-button-border, transparent);
+ cursor: pointer;
+ font-size: 0.85em;
+ }
+
+ .btn-primary {
+ background: var(--vscode-button-background);
+ color: var(--vscode-button-foreground);
+ }
+
+ .btn-primary:hover {
+ background: var(--vscode-button-hoverBackground);
+ }
+
+ .btn-secondary {
+ background: var(--vscode-button-secondaryBackground);
+ color: var(--vscode-button-secondaryForeground);
+ }
+
+ .btn-secondary:hover {
+ background: var(--vscode-button-secondaryHoverBackground);
+ }
+
+ .btn-link {
+ background: transparent;
+ border: none;
+ color: var(--vscode-textLink-foreground);
+ padding: 0;
+ cursor: pointer;
+ text-align: left;
+ }
+
+ .btn-link:hover {
+ color: var(--vscode-textLink-activeForeground);
+ text-decoration: underline;
+ }
+
+ .empty {
+ color: var(--vscode-descriptionForeground);
+ font-style: italic;
+ }
+
+ .meta {
+ color: var(--vscode-descriptionForeground);
+ font-size: 0.9em;
+ }
+
+ .badge {
+ display: inline-block;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.8em;
+ }
+
+ .reply-input {
+ background: var(--vscode-input-background);
+ color: var(--vscode-input-foreground);
+ border: 1px solid var(--vscode-input-border);
+ border-radius: 3px;
+ padding: 6px 8px;
+ resize: vertical;
+ box-sizing: border-box;
+ }
+
+ select {
+ background: var(--vscode-dropdown-background);
+ color: var(--vscode-dropdown-foreground);
+ border: 1px solid var(--vscode-dropdown-border);
+ border-radius: 3px;
+ padding: 3px 22px 3px 6px;
+ }
+
+ .check-state {
+ font-size: 0.8em;
+ min-width: 80px;
+ padding: 2px 6px;
+ border-radius: 3px;
+ text-align: center;
+ border: 1px solid;
+ }
+
+ .check-success { color: var(--vscode-charts-green); border-color: var(--vscode-charts-green); }
+ .check-failure { color: var(--vscode-charts-red); border-color: var(--vscode-charts-red); }
+ .check-pending { color: var(--vscode-charts-yellow); border-color: var(--vscode-charts-yellow); }
+ .check-neutral { color: var(--vscode-descriptionForeground); border-color: var(--vscode-panel-border); }
+
+ @media (max-width: 720px) {
+ .shell {
+ padding: 12px;
+ }
+ }
+`;
+
+ // src/views/webview/vscodeApi.ts
+ var api;
+ function vscode() {
+ if (!api) {
+ api = acquireVsCodeApi();
+ }
+ return api;
+ }
+ function readInitialData() {
+ const element = document.getElementById("adoext-data");
+ if (!element?.textContent) {
+ throw new Error("Missing ADOExt webview data.");
+ }
+ return JSON.parse(element.textContent);
+ }
+ function postMessage(message) {
+ vscode().postMessage(message);
+ }
+
+ // src/views/webview/workItemSchemaInspector.ts
+ var AdoWorkItemSchemaInspectorApp = class extends i4 {
+ constructor() {
+ super(...arguments);
+ this.model = readInitialData();
+ this.filter = "";
+ this.onFilter = (event) => {
+ this.filter = event.target.value ?? "";
+ };
+ }
+ render() {
+ const filtered = this.filteredTypes();
+ const processLabel = this.processLabel();
+ return b2`
+
+
+ ${this.model.warnings.length ? b2`Warnings
${this.model.warnings.map((w2) => b2`- ${w2}
`)}
` : A}
+
+
+
+
+ ${filtered.length}/${this.model.types.length} types
+
+
+ ${filtered.length === 0 ? b2`No matching work item types.
` : b2`${filtered.map((type) => this.renderType(type))}`}
+
+ Tip: Use “Copy Diagnostic Summary” when filing bugs about custom fields, states, or icons.
+ `;
+ }
+ renderType(type) {
+ const style = type.color ? `--type-color:${type.color}` : "";
+ return b2`
+
+ ${type.iconUrl ? b2`
` : A}
+
+ ${type.name}
+ ${type.referenceName ? b2`${type.referenceName} · ` : A}${type.stateCount} states ${type.fieldCount} fields
+
+
+
+
+ States
+ ${type.states.length === 0 ? b2`No state metadata available.
` : b2`| Name | Category |
${type.states.map((state) => this.renderStateRow(state))}
`}
+
+
+ Fields
+ ${type.fields.length === 0 ? b2`No field metadata available.
` : b2`| Reference | Name | |
${type.fields.map((field) => this.renderFieldRow(field))}
`}
+
+
+
+ `;
+ }
+ renderStateRow(state) {
+ const style = state.color ? `--state-color:${state.color}` : "";
+ return b2`
+ | ${state.name} |
+ ${state.category ?? ""} |
+
`;
+ }
+ renderFieldRow(field) {
+ return b2`
+ ${field.referenceName}${field.alwaysRequired ? b2` required` : A} |
+ ${field.name}${field.helpText ? b2` ${field.helpText} ` : A} |
+ |
+
`;
+ }
+ copyField(field) {
+ this.send({ type: "copyFieldReferenceName", referenceName: field.referenceName });
+ }
+ filteredTypes() {
+ const filter = this.filter.trim().toLowerCase();
+ if (!filter) {
+ return this.model.types;
+ }
+ return this.model.types.filter((type) => {
+ if (this.match(filter, type.name) || this.match(filter, type.referenceName ?? "")) {
+ return true;
+ }
+ if (type.states.some((state) => this.match(filter, state.name) || this.match(filter, state.category ?? ""))) {
+ return true;
+ }
+ if (type.fields.some((field) => this.match(filter, field.name) || this.match(filter, field.referenceName))) {
+ return true;
+ }
+ return false;
+ });
+ }
+ match(filter, value) {
+ return value.toLowerCase().includes(filter);
+ }
+ processLabel() {
+ const template = this.model.processTemplate?.templateName?.trim();
+ const version = this.model.processTemplate?.templateVersion?.trim();
+ if (!template && !version) {
+ return void 0;
+ }
+ if (template && version) {
+ return `${template} (${version})`;
+ }
+ return template || version;
+ }
+ send(message) {
+ postMessage(message);
+ }
+ };
+ AdoWorkItemSchemaInspectorApp.properties = {
+ model: { state: true },
+ filter: { state: true }
+ };
+ AdoWorkItemSchemaInspectorApp.styles = [baseStyles, i`
+ .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
+ .title { display: flex; flex-direction: column; gap: 4px; min-width: 280px; }
+ .subtitle { color: var(--vscode-descriptionForeground); }
+ .toolbar { margin-top: 8px; }
+ .search { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
+ .search input { min-width: 260px; padding: 4px 8px; border: 1px solid var(--vscode-input-border); background: var(--vscode-input-background); color: var(--vscode-input-foreground); border-radius: 3px; }
+ details { border: 1px solid var(--vscode-panel-border); border-radius: 4px; margin: 10px 0; background: var(--vscode-sideBar-background); }
+ summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 10px; }
+ summary::-webkit-details-marker { display: none; }
+ .type-icon { width: 18px; height: 18px; object-fit: contain; }
+ .type-name { font-weight: 600; }
+ .type-meta { color: var(--vscode-descriptionForeground); font-size: 0.9em; }
+ .type-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--type-color, transparent); }
+ .pill { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 0.8em; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); }
+ .body { padding: 0 12px 12px; }
+ .grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
+ @media (min-width: 920px) { .grid { grid-template-columns: 1fr 1fr; } }
+ table { width: 100%; border-collapse: collapse; }
+ th, td { text-align: left; border-bottom: 1px solid var(--vscode-panel-border); padding: 6px 6px; vertical-align: top; }
+ th { color: var(--vscode-descriptionForeground); font-weight: 600; font-size: 0.9em; }
+ code { font-family: var(--vscode-editor-font-family); }
+ .state-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--state-color, transparent); display: inline-block; margin-right: 6px; vertical-align: middle; }
+ .warnings { border: 1px solid color-mix(in srgb, var(--vscode-charts-yellow) 50%, transparent); background: color-mix(in srgb, var(--vscode-charts-yellow) 10%, transparent); padding: 10px 12px; border-radius: 4px; margin: 10px 0; }
+ .warnings h2 { margin-bottom: 6px; }
+ .warnings ul { margin: 0; padding-left: 18px; }
+ .copy-btn { white-space: nowrap; }
+ .help { color: var(--vscode-descriptionForeground); font-size: 0.9em; }
+ `];
+ customElements.define("ado-work-item-schema-inspector-app", AdoWorkItemSchemaInspectorApp);
+})();
diff --git a/package.json b/package.json
index 0e0acc9..1ee4cd4 100644
--- a/package.json
+++ b/package.json
@@ -202,6 +202,12 @@
"category": "ADOExt",
"icon": "$(search)"
},
+ {
+ "command": "adoext.openWorkItemSchemaInspector",
+ "title": "Open Work Item Process Inspector",
+ "category": "ADOExt",
+ "icon": "$(beaker)"
+ },
{
"command": "adoext.createWorkItem",
"title": "Create Work Item",
diff --git a/scripts/build-webviews.js b/scripts/build-webviews.js
index 37246d5..0fd966a 100644
--- a/scripts/build-webviews.js
+++ b/scripts/build-webviews.js
@@ -12,6 +12,7 @@ const buildOptions = {
path.join(root, 'src', 'views', 'webview', 'pipelineRunDetails.ts'),
path.join(root, 'src', 'views', 'webview', 'prDetails.ts'),
path.join(root, 'src', 'views', 'webview', 'workItemDetails.ts'),
+ path.join(root, 'src', 'views', 'webview', 'workItemSchemaInspector.ts'),
path.join(root, 'src', 'views', 'webview', 'planning.ts')
],
outdir,
@@ -45,4 +46,4 @@ async function main() {
main().catch(error => {
console.error(error);
process.exit(1);
-});
\ No newline at end of file
+});
diff --git a/src/api/adoClient.ts b/src/api/adoClient.ts
index 814e347..3a5d888 100644
--- a/src/api/adoClient.ts
+++ b/src/api/adoClient.ts
@@ -9,6 +9,7 @@ import { GitVersionType, VersionControlChangeType, GitStatusState, PullRequestAs
import { BuildReason, BuildResult, BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Operation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { normalizeWorkItemTypeName, workItemTypeScopeKey } from '../utils/workItemTypeIcons';
+import { mapWithConcurrencyLimit } from '../utils/async';
import type {
WorkItem,
WorkItemType,
@@ -103,6 +104,40 @@ export const PullRequestReviewVotes = {
approved: 10
} as const satisfies Record;
+export interface WorkItemProcessTemplateInfo {
+ templateName?: string;
+ templateTypeId?: string;
+ templateVersion?: string;
+}
+
+export interface WorkItemSchemaStateInfo {
+ name: string;
+ category?: string;
+ color?: string;
+}
+
+export interface WorkItemSchemaFieldInfo {
+ name: string;
+ referenceName: string;
+ alwaysRequired: boolean;
+ helpText?: string;
+}
+
+export interface WorkItemTypeSchemaInfo {
+ name: string;
+ referenceName?: string;
+ color?: string;
+ iconUrl?: string;
+ states: WorkItemSchemaStateInfo[];
+ fields: WorkItemSchemaFieldInfo[];
+}
+
+export interface WorkItemProcessSchemaInfo {
+ processTemplate?: WorkItemProcessTemplateInfo;
+ types: WorkItemTypeSchemaInfo[];
+ warnings: string[];
+}
+
const WORK_ITEM_QUERY_LIMIT = 200;
const PLANNING_WORK_ITEM_QUERY_LIMIT = 500;
const BUILDS_PER_QUERY = 10;
@@ -591,6 +626,95 @@ export class AdoClient {
return icons;
}
+ /**
+ * Fetch a read-only schema snapshot for work item types/states/fields in the project.
+ *
+ * This is intended for diagnostics and should not require admin permissions beyond
+ * reading work item metadata.
+ */
+ async getWorkItemProcessSchema(
+ project: string,
+ organization?: string
+ ): Promise {
+ const warnings: string[] = [];
+ const coreApi: ICoreApi = await this.getConnectionFor(organization).getCoreApi();
+ const witApi: IWorkItemTrackingApi = await this.getConnectionFor(organization).getWorkItemTrackingApi();
+
+ let processTemplate: WorkItemProcessTemplateInfo | undefined;
+ try {
+ const projectInfo = await coreApi.getProject(project, true, false);
+ const template = projectInfo?.capabilities?.processTemplate;
+ if (template) {
+ processTemplate = {
+ templateName: template.templateName,
+ templateTypeId: template.templateTypeId,
+ templateVersion: template.templateVersion
+ };
+ }
+ } catch (err) {
+ warnings.push(`Failed to fetch project process template: ${this.formatError(err)}`);
+ }
+
+ const typeRefs = await this.getWorkItemTypes(project, organization);
+ const types = await mapWithConcurrencyLimit(typeRefs, 4, async (typeRef) => {
+ const typeName = typeRef.name?.trim();
+ if (!typeName) {
+ return undefined;
+ }
+
+ try {
+ const full = await witApi.getWorkItemType(project, typeName);
+ const states = (full.states ?? [])
+ .map(state => ({
+ name: state.name?.trim() ?? '',
+ category: state.category?.trim() || undefined,
+ color: state.color?.trim() || undefined
+ }))
+ .filter(state => state.name.length > 0);
+
+ const fields = (full.fields ?? full.fieldInstances ?? [])
+ .map(field => ({
+ name: field.name?.trim() ?? '',
+ referenceName: field.referenceName?.trim() ?? '',
+ alwaysRequired: Boolean(field.alwaysRequired),
+ helpText: field.helpText?.trim() || undefined
+ }))
+ .filter(field => field.referenceName.length > 0 && field.name.length > 0);
+
+ return {
+ name: full.name?.trim() ?? typeName,
+ referenceName: full.referenceName?.trim() || undefined,
+ color: full.color?.trim() || undefined,
+ iconUrl: full.icon?.url?.trim() || undefined,
+ states,
+ fields
+ } satisfies WorkItemTypeSchemaInfo;
+ } catch (err) {
+ warnings.push(`Failed to fetch work item type "${typeName}": ${this.formatError(err)}`);
+ return {
+ name: typeName,
+ referenceName: typeRef.referenceName?.trim() || undefined,
+ color: typeRef.color?.trim() || undefined,
+ iconUrl: typeRef.icon?.url?.trim() || undefined,
+ states: [],
+ fields: []
+ } satisfies WorkItemTypeSchemaInfo;
+ }
+ });
+
+ return {
+ processTemplate,
+ types: types
+ .filter((type): type is WorkItemTypeSchemaInfo => Boolean(type))
+ .sort((a, b) => a.name.localeCompare(b.name)),
+ warnings
+ };
+ }
+
+ private formatError(error: unknown): string {
+ return error instanceof Error ? error.message : String(error);
+ }
+
// -------------------------------------------------------------------------
// Pull Requests
// -------------------------------------------------------------------------
diff --git a/src/commands/schemaInspectorCommands.ts b/src/commands/schemaInspectorCommands.ts
new file mode 100644
index 0000000..9af4cb6
--- /dev/null
+++ b/src/commands/schemaInspectorCommands.ts
@@ -0,0 +1,41 @@
+import * as vscode from 'vscode';
+import type { AdoClient } from '../api/adoClient';
+import type { ConfigManager } from '../config/configManager';
+import { resolveProjectScopes, scopeLabel, type ProjectScope } from '../providers/projectScopes';
+import { showInformationMessage } from '../utils/notifications';
+import { WorkItemSchemaInspectorPanel } from '../views/workItemSchemaInspectorPanel';
+
+export async function openWorkItemSchemaInspector(
+ context: vscode.ExtensionContext,
+ client: AdoClient,
+ config: ConfigManager
+): Promise {
+ const scopes = await resolveProjectScopes(client, config);
+ if (scopes.length === 0) {
+ showInformationMessage('Select an organization and project first (ADOExt: Select Organization / Select Project).');
+ return;
+ }
+
+ const scope = await pickScope(scopes);
+ if (!scope) {
+ return;
+ }
+
+ await WorkItemSchemaInspectorPanel.show(context, client, config, scope);
+}
+
+async function pickScope(scopes: ProjectScope[]): Promise {
+ if (scopes.length === 1) {
+ return scopes[0];
+ }
+
+ const choice = await vscode.window.showQuickPick(
+ scopes.map(scope => ({
+ label: scopeLabel(scope),
+ scope
+ })),
+ { placeHolder: 'Select a project scope to inspect' }
+ );
+ return choice?.scope;
+}
+
diff --git a/src/extension.ts b/src/extension.ts
index 6f9128a..cf4bd96 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -59,6 +59,7 @@ import {
saveWorkItemQuery,
savePullRequestQuery
} from './commands/queryCommands';
+import { openWorkItemSchemaInspector } from './commands/schemaInspectorCommands';
import {
cancelPipelineRun,
openPipelineRunInBrowser,
@@ -539,6 +540,13 @@ export async function activate(context: vscode.ExtensionContext): Promise
})
);
+ context.subscriptions.push(
+ vscode.commands.registerCommand('adoext.openWorkItemSchemaInspector', async () => {
+ if (!(await ensureSignedIn())) { return; }
+ await openWorkItemSchemaInspector(context, client, config);
+ })
+ );
+
context.subscriptions.push(
vscode.commands.registerCommand('adoext.createWorkItem', async () => {
if (!(await ensureSignedIn())) { return; }
diff --git a/src/views/webview/workItemSchemaInspector.ts b/src/views/webview/workItemSchemaInspector.ts
new file mode 100644
index 0000000..ce6be2b
--- /dev/null
+++ b/src/views/webview/workItemSchemaInspector.ts
@@ -0,0 +1,179 @@
+import { LitElement, css, html, nothing, type PropertyDeclarations } from 'lit';
+import { baseStyles } from './commonStyles';
+import { postMessage, readInitialData } from './vscodeApi';
+import type {
+ WorkItemSchemaInspectorFieldViewModel,
+ WorkItemSchemaInspectorMessage,
+ WorkItemSchemaInspectorTypeViewModel,
+ WorkItemSchemaInspectorViewModel
+} from '../webviewTypes';
+
+class AdoWorkItemSchemaInspectorApp extends LitElement {
+ static properties: PropertyDeclarations = {
+ model: { state: true },
+ filter: { state: true }
+ };
+
+ static styles = [baseStyles, css`
+ .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
+ .title { display: flex; flex-direction: column; gap: 4px; min-width: 280px; }
+ .subtitle { color: var(--vscode-descriptionForeground); }
+ .toolbar { margin-top: 8px; }
+ .search { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
+ .search input { min-width: 260px; padding: 4px 8px; border: 1px solid var(--vscode-input-border); background: var(--vscode-input-background); color: var(--vscode-input-foreground); border-radius: 3px; }
+ details { border: 1px solid var(--vscode-panel-border); border-radius: 4px; margin: 10px 0; background: var(--vscode-sideBar-background); }
+ summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 10px; }
+ summary::-webkit-details-marker { display: none; }
+ .type-icon { width: 18px; height: 18px; object-fit: contain; }
+ .type-name { font-weight: 600; }
+ .type-meta { color: var(--vscode-descriptionForeground); font-size: 0.9em; }
+ .type-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--type-color, transparent); }
+ .pill { display: inline-block; padding: 1px 6px; border-radius: 10px; font-size: 0.8em; background: var(--vscode-badge-background); color: var(--vscode-badge-foreground); }
+ .body { padding: 0 12px 12px; }
+ .grid { display: grid; grid-template-columns: 1fr; gap: 14px; }
+ @media (min-width: 920px) { .grid { grid-template-columns: 1fr 1fr; } }
+ table { width: 100%; border-collapse: collapse; }
+ th, td { text-align: left; border-bottom: 1px solid var(--vscode-panel-border); padding: 6px 6px; vertical-align: top; }
+ th { color: var(--vscode-descriptionForeground); font-weight: 600; font-size: 0.9em; }
+ code { font-family: var(--vscode-editor-font-family); }
+ .state-color { width: 10px; height: 10px; border-radius: 2px; border: 1px solid var(--vscode-panel-border); background: var(--state-color, transparent); display: inline-block; margin-right: 6px; vertical-align: middle; }
+ .warnings { border: 1px solid color-mix(in srgb, var(--vscode-charts-yellow) 50%, transparent); background: color-mix(in srgb, var(--vscode-charts-yellow) 10%, transparent); padding: 10px 12px; border-radius: 4px; margin: 10px 0; }
+ .warnings h2 { margin-bottom: 6px; }
+ .warnings ul { margin: 0; padding-left: 18px; }
+ .copy-btn { white-space: nowrap; }
+ .help { color: var(--vscode-descriptionForeground); font-size: 0.9em; }
+ `];
+
+ model: WorkItemSchemaInspectorViewModel = readInitialData();
+ filter = '';
+
+ render() {
+ const filtered = this.filteredTypes();
+ const processLabel = this.processLabel();
+ return html`
+
+
+ ${this.model.warnings.length
+ ? html`Warnings
${this.model.warnings.map(w => html`- ${w}
`)}
`
+ : nothing}
+
+
+
+
+ ${filtered.length}/${this.model.types.length} types
+
+
+ ${filtered.length === 0
+ ? html`No matching work item types.
`
+ : html`${filtered.map(type => this.renderType(type))}`}
+
+ Tip: Use “Copy Diagnostic Summary” when filing bugs about custom fields, states, or icons.
+ `;
+ }
+
+ private renderType(type: WorkItemSchemaInspectorTypeViewModel) {
+ const style = type.color ? `--type-color:${type.color}` : '';
+ return html`
+
+ ${type.iconUrl ? html`
` : nothing}
+
+ ${type.name}
+ ${type.referenceName ? html`${type.referenceName} · ` : nothing}${type.stateCount} states ${type.fieldCount} fields
+
+
+
+
+ States
+ ${type.states.length === 0
+ ? html`No state metadata available.
`
+ : html`| Name | Category |
${type.states.map(state => this.renderStateRow(state))}
`}
+
+
+ Fields
+ ${type.fields.length === 0
+ ? html`No field metadata available.
`
+ : html`| Reference | Name | |
${type.fields.map(field => this.renderFieldRow(field))}
`}
+
+
+
+ `;
+ }
+
+ private renderStateRow(state: WorkItemSchemaInspectorTypeViewModel['states'][number]) {
+ const style = state.color ? `--state-color:${state.color}` : '';
+ return html`
+ | ${state.name} |
+ ${state.category ?? ''} |
+
`;
+ }
+
+ private renderFieldRow(field: WorkItemSchemaInspectorFieldViewModel) {
+ return html`
+ ${field.referenceName}${field.alwaysRequired ? html` required` : nothing} |
+ ${field.name}${field.helpText ? html` ${field.helpText} ` : nothing} |
+ |
+
`;
+ }
+
+ private copyField(field: WorkItemSchemaInspectorFieldViewModel): void {
+ this.send({ type: 'copyFieldReferenceName', referenceName: field.referenceName });
+ }
+
+ private onFilter = (event: Event): void => {
+ this.filter = (event.target as HTMLInputElement).value ?? '';
+ };
+
+ private filteredTypes(): WorkItemSchemaInspectorTypeViewModel[] {
+ const filter = this.filter.trim().toLowerCase();
+ if (!filter) {
+ return this.model.types;
+ }
+
+ return this.model.types.filter(type => {
+ if (this.match(filter, type.name) || this.match(filter, type.referenceName ?? '')) {
+ return true;
+ }
+ if (type.states.some(state => this.match(filter, state.name) || this.match(filter, state.category ?? ''))) {
+ return true;
+ }
+ if (type.fields.some(field => this.match(filter, field.name) || this.match(filter, field.referenceName))) {
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private match(filter: string, value: string): boolean {
+ return value.toLowerCase().includes(filter);
+ }
+
+ private processLabel(): string | undefined {
+ const template = this.model.processTemplate?.templateName?.trim();
+ const version = this.model.processTemplate?.templateVersion?.trim();
+ if (!template && !version) {
+ return undefined;
+ }
+ if (template && version) {
+ return `${template} (${version})`;
+ }
+ return template || version;
+ }
+
+ private send(message: WorkItemSchemaInspectorMessage): void {
+ postMessage(message);
+ }
+}
+
+customElements.define('ado-work-item-schema-inspector-app', AdoWorkItemSchemaInspectorApp);
+
diff --git a/src/views/webviewTypes.ts b/src/views/webviewTypes.ts
index c9c46cd..e67cd9a 100644
--- a/src/views/webviewTypes.ts
+++ b/src/views/webviewTypes.ts
@@ -224,3 +224,48 @@ export type PipelineRunDetailsMessage =
| { type: 'rerun' }
| { type: 'cancel' }
| { type: 'openArtifact'; url: string };
+
+export interface WorkItemSchemaInspectorProcessTemplateViewModel {
+ templateName?: string;
+ templateTypeId?: string;
+ templateVersion?: string;
+}
+
+export interface WorkItemSchemaInspectorStateViewModel {
+ name: string;
+ category?: string;
+ color?: string;
+}
+
+export interface WorkItemSchemaInspectorFieldViewModel {
+ name: string;
+ referenceName: string;
+ alwaysRequired: boolean;
+ helpText?: string;
+}
+
+export interface WorkItemSchemaInspectorTypeViewModel {
+ name: string;
+ referenceName?: string;
+ color?: string;
+ iconUrl?: string;
+ stateCount: number;
+ fieldCount: number;
+ states: WorkItemSchemaInspectorStateViewModel[];
+ fields: WorkItemSchemaInspectorFieldViewModel[];
+}
+
+export interface WorkItemSchemaInspectorViewModel {
+ organization: string;
+ project: string;
+ fetchedAt: string;
+ processTemplate?: WorkItemSchemaInspectorProcessTemplateViewModel;
+ warnings: string[];
+ types: WorkItemSchemaInspectorTypeViewModel[];
+}
+
+export type WorkItemSchemaInspectorMessage =
+ | { type: 'refresh' }
+ | { type: 'openProcessSettings' }
+ | { type: 'copyDiagnosticSummary' }
+ | { type: 'copyFieldReferenceName'; referenceName: string };
diff --git a/src/views/workItemSchemaInspectorPanel.ts b/src/views/workItemSchemaInspectorPanel.ts
new file mode 100644
index 0000000..6628af9
--- /dev/null
+++ b/src/views/workItemSchemaInspectorPanel.ts
@@ -0,0 +1,253 @@
+import * as vscode from 'vscode';
+import type { AdoClient, WorkItemProcessSchemaInfo } from '../api/adoClient';
+import type { ConfigManager } from '../config/configManager';
+import { showErrorMessage, showInformationMessage, showWarningMessage } from '../utils/notifications';
+import { buildWebviewDocument, buildMessageDocument, webviewAssetRoots } from './webviewHtml';
+import type { WorkItemSchemaInspectorMessage, WorkItemSchemaInspectorViewModel } from './webviewTypes';
+
+export interface WorkItemSchemaInspectorScope {
+ organization: string;
+ project: string;
+}
+
+export class WorkItemSchemaInspectorPanel {
+ private static _panels = new Map();
+
+ private readonly _panel: vscode.WebviewPanel;
+ private readonly _panelKey: string;
+ private _disposables: vscode.Disposable[] = [];
+
+ static async show(
+ context: vscode.ExtensionContext,
+ client: AdoClient,
+ config: ConfigManager,
+ scope: WorkItemSchemaInspectorScope
+ ): Promise {
+ const key = WorkItemSchemaInspectorPanel.panelKey(scope);
+ const existing = WorkItemSchemaInspectorPanel._panels.get(key);
+ if (existing) {
+ existing._panel.reveal(vscode.ViewColumn.One);
+ await existing._refresh(client, config, scope);
+ return;
+ }
+ new WorkItemSchemaInspectorPanel(context, client, config, scope, key);
+ }
+
+ private static panelKey(scope: WorkItemSchemaInspectorScope): string {
+ return `${scope.organization}\u0000${scope.project}`;
+ }
+
+ private constructor(
+ private readonly _context: vscode.ExtensionContext,
+ private readonly _client: AdoClient,
+ private readonly _config: ConfigManager,
+ private _scope: WorkItemSchemaInspectorScope,
+ panelKey: string
+ ) {
+ this._panelKey = panelKey;
+ this._panel = vscode.window.createWebviewPanel(
+ 'adoext.workItemSchemaInspector',
+ `Work Item Process Inspector: ${_scope.organization}/${_scope.project}`,
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ localResourceRoots: webviewAssetRoots(_context)
+ }
+ );
+
+ this._panel.onDidDispose(() => this._dispose(), null, this._disposables);
+ this._panel.webview.onDidReceiveMessage(
+ async (msg) => this._handleMessage(msg),
+ null,
+ this._disposables
+ );
+
+ WorkItemSchemaInspectorPanel._panels.set(panelKey, this);
+ void this._refresh(this._client, this._config, this._scope);
+ }
+
+ private async _handleMessage(msg: WorkItemSchemaInspectorMessage): Promise {
+ if (msg.type === 'refresh') {
+ await this._refresh(this._client, this._config, this._scope);
+ return;
+ }
+
+ if (msg.type === 'openProcessSettings') {
+ const url = `https://dev.azure.com/${this._scope.organization}/_settings/process`;
+ void vscode.env.openExternal(vscode.Uri.parse(url));
+ return;
+ }
+
+ if (msg.type === 'copyFieldReferenceName') {
+ const ref = (msg.referenceName ?? '').trim();
+ if (!ref) { return; }
+ await vscode.env.clipboard.writeText(ref);
+ showInformationMessage(`Copied field reference name: ${ref}`);
+ return;
+ }
+
+ if (msg.type === 'copyDiagnosticSummary') {
+ const summary = await this._buildDiagnosticSummary();
+ await vscode.env.clipboard.writeText(summary);
+ showInformationMessage('Copied process/schema diagnostic summary to clipboard.');
+ return;
+ }
+ }
+
+ private async _buildDiagnosticSummary(): Promise {
+ const organization = this._scope.organization;
+ const project = this._scope.project;
+ let schema: WorkItemProcessSchemaInfo | undefined;
+ try {
+ schema = await this._client.getWorkItemProcessSchema(project, organization);
+ } catch (err) {
+ return JSON.stringify({
+ organization,
+ project,
+ error: this._formatError(err)
+ }, null, 2);
+ }
+
+ return JSON.stringify({
+ organization,
+ project,
+ processTemplate: schema.processTemplate ?? null,
+ fetchedAt: new Date().toISOString(),
+ typeCount: schema.types.length,
+ warnings: schema.warnings,
+ types: schema.types.map(type => ({
+ name: type.name,
+ referenceName: type.referenceName ?? null,
+ color: type.color ?? null,
+ iconUrl: type.iconUrl ?? null,
+ stateCount: type.states.length,
+ fieldCount: type.fields.length,
+ states: type.states.map(state => ({
+ name: state.name,
+ category: state.category ?? null,
+ color: state.color ?? null
+ })),
+ fields: type.fields.map(field => ({
+ referenceName: field.referenceName,
+ name: field.name,
+ alwaysRequired: field.alwaysRequired,
+ helpText: field.helpText ?? null
+ }))
+ }))
+ }, null, 2);
+ }
+
+ private async _refresh(
+ client: AdoClient,
+ _config: ConfigManager,
+ scope: WorkItemSchemaInspectorScope
+ ): Promise {
+ this._scope = scope;
+ this._panel.title = `Work Item Process Inspector: ${scope.organization}/${scope.project}`;
+ const { organization, project } = scope;
+
+ if (!organization || !project) {
+ this._panel.webview.html = buildMessageDocument(this._panel.webview, 'Select an organization and project first.');
+ return;
+ }
+
+ let schema: WorkItemProcessSchemaInfo;
+ try {
+ schema = await client.getWorkItemProcessSchema(project, organization);
+ } catch (err) {
+ showErrorMessage(`Failed to load work item process schema: ${this._formatError(err)}`);
+ this._panel.webview.html = buildMessageDocument(this._panel.webview, 'Failed to load work item schema. See the ADOExt output for details.');
+ return;
+ }
+
+ if (schema.warnings.length) {
+ showWarningMessage(`Work item schema loaded with warnings (${schema.warnings.length}).`);
+ }
+
+ const model: WorkItemSchemaInspectorViewModel = {
+ organization,
+ project,
+ fetchedAt: new Date().toLocaleString(),
+ processTemplate: schema.processTemplate
+ ? {
+ templateName: schema.processTemplate.templateName,
+ templateTypeId: schema.processTemplate.templateTypeId,
+ templateVersion: schema.processTemplate.templateVersion
+ }
+ : undefined,
+ warnings: schema.warnings,
+ types: schema.types.map(type => ({
+ name: type.name,
+ referenceName: type.referenceName,
+ color: this._sanitizeHexColor(type.color),
+ iconUrl: this._sanitizeIconUrl(type.iconUrl),
+ stateCount: type.states.length,
+ fieldCount: type.fields.length,
+ states: type.states
+ .map(state => ({
+ name: state.name,
+ category: state.category,
+ color: this._sanitizeHexColor(state.color)
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name)),
+ fields: type.fields
+ .map(field => ({
+ name: field.name,
+ referenceName: field.referenceName,
+ alwaysRequired: field.alwaysRequired,
+ helpText: field.helpText
+ }))
+ .sort((a, b) => a.referenceName.localeCompare(b.referenceName))
+ }))
+ };
+
+ this._panel.webview.html = this._buildHtml(model);
+ }
+
+ private _buildHtml(model: WorkItemSchemaInspectorViewModel): string {
+ return buildWebviewDocument(this._context, this._panel.webview, {
+ title: `Work Item Process Inspector`,
+ entry: 'workItemSchemaInspector.js',
+ appTag: 'ado-work-item-schema-inspector-app',
+ data: model
+ });
+ }
+
+ private _sanitizeIconUrl(value: string | undefined): string | undefined {
+ if (!value) {
+ return undefined;
+ }
+ try {
+ const uri = vscode.Uri.parse(value);
+ return uri.scheme === 'https' ? uri.toString(true) : undefined;
+ } catch {
+ return undefined;
+ }
+ }
+
+ private _sanitizeHexColor(value: string | undefined): string | undefined {
+ if (!value) {
+ return undefined;
+ }
+ const raw = value.trim();
+ const withHash = raw.startsWith('#') ? raw : `#${raw}`;
+ if (!/^#[0-9a-fA-F]{6}$/.test(withHash)) {
+ return undefined;
+ }
+ return withHash.toLowerCase();
+ }
+
+ private _formatError(err: unknown): string {
+ return err instanceof Error ? err.message : String(err);
+ }
+
+ private _dispose(): void {
+ WorkItemSchemaInspectorPanel._panels.delete(this._panelKey);
+ for (const disposable of this._disposables) {
+ disposable.dispose();
+ }
+ this._disposables = [];
+ }
+}
+