opendoor-plugin 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // Generated from src/pi-web-plugin.ts. Do not edit directly.
1
+ // Generated from src/browser/pi-web-plugin.ts. Do not edit directly.
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __decorateClass = (decorators, target, key, kind) => {
@@ -562,6 +562,21 @@ o4?.({ LitElement: i4 });
562
562
  (s3.litElementVersions ??= []).push("4.2.2");
563
563
 
564
564
  // src/doorstop-contract.ts
565
+ var ITEM_STATE_KEYS = [
566
+ "normative",
567
+ "non-normative",
568
+ "inactive",
569
+ "reviewed",
570
+ "unreviewed",
571
+ "suspect-link",
572
+ "no-child-links",
573
+ "no-links",
574
+ "unknown-link",
575
+ "missing-reference"
576
+ ];
577
+ function isItemStateKey(value) {
578
+ return ITEM_STATE_KEYS.includes(value);
579
+ }
565
580
  function formatUnknownError(error) {
566
581
  return error instanceof Error ? error.message : String(error);
567
582
  }
@@ -1303,10 +1318,12 @@ var OPENDOOR_SETTINGS_PATH = ".pi-web/opendoor.json";
1303
1318
  var OPENDOOR_SETTINGS_VERSION = 1;
1304
1319
  var DEFAULT_PUBLISH_TARGET = "./public";
1305
1320
  var MAX_EXCLUDED_DIRECTORIES = 16;
1321
+ var MAX_SHOWN_ATTRIBUTES = 16;
1306
1322
  var MAX_EXCLUDED_DIRECTORY_LENGTH = 64;
1307
1323
  var DEFAULT_OPENDOOR_SETTINGS = Object.freeze({
1308
1324
  publishTarget: DEFAULT_PUBLISH_TARGET,
1309
1325
  excludedDirectories: Object.freeze([]),
1326
+ showAdditionalAttribute: Object.freeze([]),
1310
1327
  commitAfterReview: false
1311
1328
  });
1312
1329
  var MISSING_FILE_ERROR = "Path does not exist";
@@ -1410,6 +1427,19 @@ function parseOpendoorSettings(value) {
1410
1427
  excludedDirectories = parseExcludedDirectories(rawExcluded, diagnostics);
1411
1428
  }
1412
1429
  }
1430
+ let showAdditionalAttribute = [];
1431
+ const rawShown = value["showAdditionalAttribute"];
1432
+ if (rawShown !== void 0) {
1433
+ if (!Array.isArray(rawShown)) {
1434
+ diagnostics.push({
1435
+ severity: "warning",
1436
+ path: OPENDOOR_SETTINGS_PATH,
1437
+ message: `${OPENDOOR_SETTINGS_PATH} "showAdditionalAttribute" must be an array; using none`
1438
+ });
1439
+ } else {
1440
+ showAdditionalAttribute = parseShowAdditionalAttribute(rawShown, diagnostics);
1441
+ }
1442
+ }
1413
1443
  let commitAfterReview = false;
1414
1444
  const rawCommit = value["commitAfterReview"];
1415
1445
  if (rawCommit !== void 0) {
@@ -1423,7 +1453,7 @@ function parseOpendoorSettings(value) {
1423
1453
  commitAfterReview = rawCommit;
1424
1454
  }
1425
1455
  }
1426
- return { settings: { publishTarget, excludedDirectories, commitAfterReview }, diagnostics };
1456
+ return { settings: { publishTarget, excludedDirectories, showAdditionalAttribute, commitAfterReview }, diagnostics };
1427
1457
  }
1428
1458
  function parseExcludedDirectories(entries, diagnostics) {
1429
1459
  const seen = /* @__PURE__ */ new Set();
@@ -1452,6 +1482,33 @@ function parseExcludedDirectories(entries, diagnostics) {
1452
1482
  }
1453
1483
  return result;
1454
1484
  }
1485
+ function parseShowAdditionalAttribute(entries, diagnostics) {
1486
+ const seen = /* @__PURE__ */ new Set();
1487
+ const result = [];
1488
+ for (const entry of entries) {
1489
+ const name = typeof entry === "string" ? entry.trim() : "";
1490
+ if (name === "") {
1491
+ diagnostics.push({
1492
+ severity: "warning",
1493
+ path: OPENDOOR_SETTINGS_PATH,
1494
+ message: `${OPENDOOR_SETTINGS_PATH} "showAdditionalAttribute" entry "${truncateEcho(String(entry))}" is not a non-empty string and was dropped`
1495
+ });
1496
+ continue;
1497
+ }
1498
+ if (seen.has(name)) continue;
1499
+ if (result.length >= MAX_SHOWN_ATTRIBUTES) {
1500
+ diagnostics.push({
1501
+ severity: "warning",
1502
+ path: OPENDOOR_SETTINGS_PATH,
1503
+ message: `${OPENDOOR_SETTINGS_PATH} "showAdditionalAttribute" has more than ${String(MAX_SHOWN_ATTRIBUTES)} entries; the rest were ignored`
1504
+ });
1505
+ break;
1506
+ }
1507
+ seen.add(name);
1508
+ result.push(name);
1509
+ }
1510
+ return result;
1511
+ }
1455
1512
  function normalizeRelativePath(value) {
1456
1513
  const collapsed = value.replace(/\/{2,}/g, "/");
1457
1514
  return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
@@ -5728,7 +5785,23 @@ function serializeReferences(references) {
5728
5785
  return JSON.stringify(references === void 0 ? [] : references);
5729
5786
  }
5730
5787
 
5731
- // src/doorstop-panel-controller.ts
5788
+ // src/browser/doorstop-panel-navigation.ts
5789
+ var DOORSTOP_NAV_UID = "uid";
5790
+ var DOORSTOP_NAV_FILE = "file";
5791
+ var DOORSTOP_NAV_DOC = "doc";
5792
+ var DOORSTOP_NAV_FILTER = "filter";
5793
+ var DOORSTOP_NAV_SEARCH = "search";
5794
+ function readPanelNavigationQuery(navigation) {
5795
+ const output = {};
5796
+ if (navigation === void 0) return output;
5797
+ for (const [key, value] of Object.entries(navigation.query)) {
5798
+ const first = Array.isArray(value) ? value[0] : value;
5799
+ if (typeof first === "string" && first !== "") output[key] = first;
5800
+ }
5801
+ return output;
5802
+ }
5803
+
5804
+ // src/browser/doorstop-panel-controller.ts
5732
5805
  var DoorstopWorkspaceController = class {
5733
5806
  constructor(host, context, loadJob) {
5734
5807
  this.loading = false;
@@ -5828,25 +5901,99 @@ var DoorstopWorkspaceController = class {
5828
5901
  this.loadRequest = request;
5829
5902
  return request;
5830
5903
  }
5831
- /** Select an item UID for the detail pane. */
5904
+ /** Select an item UID for the detail pane. Plan Phase C step C2 — a
5905
+ * deliberate selection change also writes the `uid` deep-link key, so
5906
+ * the selection is bookmarkable/back-navigable. */
5832
5907
  selectUid(uid) {
5833
5908
  this.selectedUid = uid;
5909
+ this.setNavigation(DOORSTOP_NAV_UID, uid);
5910
+ this.setNavigation(DOORSTOP_NAV_FILE, void 0, { replace: true });
5834
5911
  this.requestUpdate();
5835
5912
  }
5836
- /** Select a document by prefix for the document-detail pane. */
5913
+ /** Select a document by prefix for the document-detail pane. C2 — writes
5914
+ * the `doc` deep-link key. */
5837
5915
  selectDocument(prefix) {
5838
5916
  this.selectedDocumentPrefix = prefix;
5917
+ this.setNavigation(DOORSTOP_NAV_DOC, prefix);
5918
+ this.setNavigation(DOORSTOP_NAV_FILE, void 0, { replace: true });
5839
5919
  this.requestUpdate();
5840
5920
  }
5841
5921
  setStateFilter(filter) {
5842
5922
  this.stateFilter = filter;
5843
5923
  if (this.result !== void 0) this.selectedUid = selectionIn(this.result, filter, this.selectedUid);
5924
+ this.setNavigation(DOORSTOP_NAV_FILTER, filter);
5844
5925
  this.requestUpdate();
5845
5926
  }
5846
5927
  setSearch(search) {
5847
5928
  this.search = search;
5929
+ this.setNavigation(DOORSTOP_NAV_SEARCH, search === "" ? void 0 : search, { replace: true });
5848
5930
  this.requestUpdate();
5849
5931
  }
5932
+ /** Apply the panel's deep-link query (URL → state). Plan Phase C step C2:
5933
+ * `renderDoorstopPanel` calls this on EVERY render, idempotently — values
5934
+ * are compared first, so re-applying the same query is a no-op, and the
5935
+ * query wins exactly when it differs from the controller state (back/
5936
+ * forward navigation re-rendering the panel with an older URL; a user
5937
+ * interaction re-rendering with the query its own setter just wrote).
5938
+ *
5939
+ * READ-ONLY for the navigation: this method never writes keys back (the
5940
+ * setters own writes — writing here would loop render → set → render).
5941
+ *
5942
+ * Validation: an unknown `filter` value is ignored (the current filter
5943
+ * stands — `?filter=bogus` must not crash the panel), and `uid`/`doc`
5944
+ * values that no longer exist in a LOADED result are dropped by the
5945
+ * same selectionIn reconciliation a landed load applies; before the first
5946
+ * load lands they apply blindly (the landing load reconciles). The C3
5947
+ * `file` key (chat file link) resolves only against a loaded result. */
5948
+ applyNavigationQuery(query) {
5949
+ let changed = false;
5950
+ const filterValue = query[DOORSTOP_NAV_FILTER];
5951
+ if (filterValue === void 0) {
5952
+ if (this.stateFilter !== void 0) {
5953
+ this.stateFilter = void 0;
5954
+ changed = true;
5955
+ }
5956
+ } else if (isItemStateKey(filterValue) && this.stateFilter !== filterValue) {
5957
+ this.stateFilter = filterValue;
5958
+ changed = true;
5959
+ }
5960
+ const filePath = query[DOORSTOP_NAV_FILE];
5961
+ const fileItem = filePath !== void 0 && this.result !== void 0 ? this.result.index.items.find((candidate) => candidate.path === filePath) : void 0;
5962
+ const uid = fileItem !== void 0 ? fileItem.uid : query[DOORSTOP_NAV_UID];
5963
+ if (this.selectedUid !== uid) {
5964
+ this.selectedUid = uid;
5965
+ changed = true;
5966
+ }
5967
+ const documentPrefix = fileItem !== void 0 ? fileItem.documentPrefix : query[DOORSTOP_NAV_DOC];
5968
+ if (this.selectedDocumentPrefix !== documentPrefix) {
5969
+ this.selectedDocumentPrefix = documentPrefix;
5970
+ changed = true;
5971
+ }
5972
+ const search = query[DOORSTOP_NAV_SEARCH] ?? "";
5973
+ if (this.search !== search) {
5974
+ this.search = search;
5975
+ changed = true;
5976
+ }
5977
+ if (this.result !== void 0) {
5978
+ const keptUid = selectionIn(this.result, this.stateFilter, this.selectedUid);
5979
+ if (keptUid !== this.selectedUid) {
5980
+ this.selectedUid = keptUid;
5981
+ changed = true;
5982
+ }
5983
+ const keptDocument = documentSelectionIn(this.result, this.selectedDocumentPrefix);
5984
+ if (keptDocument !== this.selectedDocumentPrefix) {
5985
+ this.selectedDocumentPrefix = keptDocument;
5986
+ changed = true;
5987
+ }
5988
+ }
5989
+ if (changed) this.requestUpdate();
5990
+ }
5991
+ /** Write one deep-link key on the CURRENT context's navigation, when the
5992
+ * host supplies one. `undefined` clears the key (the selection is gone);
5993
+ * absent navigation (older hosts, tests without one) is a silent no-op. */
5994
+ setNavigation(key, value, options) {
5995
+ this.context.navigation?.set(key, value, options);
5996
+ }
5850
5997
  /** Begin a doorstop run: record the op title (element disables action
5851
5998
  * buttons) and notify. */
5852
5999
  beginRun(title) {
@@ -5871,6 +6018,37 @@ var DoorstopWorkspaceController = class {
5871
6018
  this.lastRun = void 0;
5872
6019
  this.requestUpdate();
5873
6020
  }
6021
+ /**
6022
+ * Commit the "Start review session" composer-insert as the last run (relay
6023
+ * `opendoor-dispatch-redesign` design §3). The insert happens in the
6024
+ * browser, so the record carries no process data: `status: "ok"` with an
6025
+ * empty stdout/stderr and NO `commit` field — which is exactly what makes
6026
+ * the status bar render its quiet ok badge, skip the commit-narration line
6027
+ * and the error pre, and stay collapsed (`lastRunHasMessage` is false, so
6028
+ * the bar does not auto-expand). The write goes through the single
6029
+ * {@link commitRun} point, so it survives `invalidate()`/`load()` and is
6030
+ * cleared only by `dismissRun()` or the next commit — every existing
6031
+ * run-outcome semantic is inherited unchanged.
6032
+ *
6033
+ * `at` is REQUIRED by {@link DoorstopLastRunView} (the run-start epoch ms
6034
+ * every `commitRun` caller sets); `durationMs` is 0 because no run was
6035
+ * measured — the insert is synchronous.
6036
+ */
6037
+ commitAskReview(uid) {
6038
+ this.commitRun({
6039
+ op: "ask-review",
6040
+ title: `Opendoor: review dispatch ${uid}`,
6041
+ status: "ok",
6042
+ exitCode: null,
6043
+ signal: null,
6044
+ stdout: "",
6045
+ stderr: "",
6046
+ stdoutTruncated: false,
6047
+ stderrTruncated: false,
6048
+ durationMs: 0,
6049
+ at: Date.now()
6050
+ });
6051
+ }
5874
6052
  /** Run the Stage action (plan Phase C step 12): stage every Doorstop-managed
5875
6053
  * path of the loaded index (the element hands the {@link doorstopPaths}
5876
6054
  * list in), or — for a per-item stage — exactly one item's path with
@@ -5951,12 +6129,12 @@ var DoorstopWorkspaceController = class {
5951
6129
  this.beginRun(title);
5952
6130
  const startedAt = Date.now();
5953
6131
  try {
5954
- const backend = this.context.backend;
5955
- if (backend === void 0) {
6132
+ const peer = this.context.peer;
6133
+ if (peer?.request === void 0) {
5956
6134
  this.commitGitError(op, title, startedAt, "Git actions need the paired opendoor backend");
5957
6135
  return;
5958
6136
  }
5959
- const response = await backend.request(backendOperation, input);
6137
+ const response = await peer.request(backendOperation, input);
5960
6138
  const parsed = parseResponse(response);
5961
6139
  this.commitRun({
5962
6140
  op,
@@ -6029,7 +6207,7 @@ var DoorstopWorkspaceController = class {
6029
6207
  * retries instead of showing a stale failure forever). A fetch already in
6030
6208
  * flight for the same UID is joined (never stacked). Otherwise the fetch
6031
6209
  * runs through
6032
- * `backend.request(DOORSTOP_BASELINE_OPERATION, { uid, path })`, parses the
6210
+ * `peer.request(DOORSTOP_BASELINE_OPERATION, { uid, path })`, parses the
6033
6211
  * response with the contract validator, and stamp-walks the returned blobs
6034
6212
  * newest-first (`parseDoorstopItem` + the document's config via
6035
6213
  * `computeItemStamp(item, config, true)`) until one matches
@@ -6071,12 +6249,12 @@ var DoorstopWorkspaceController = class {
6071
6249
  this.baselineVersion += 1;
6072
6250
  this.requestUpdate();
6073
6251
  try {
6074
- const backend = this.context.backend;
6075
- if (backend === void 0) {
6252
+ const peer = this.context.peer;
6253
+ if (peer?.request === void 0) {
6076
6254
  this.commitBaseline(uid, { key, view: { state: "no-git" } });
6077
6255
  return;
6078
6256
  }
6079
- const response = await backend.request(DOORSTOP_BASELINE_OPERATION, { uid, path: item.path });
6257
+ const response = await peer.request(DOORSTOP_BASELINE_OPERATION, { uid, path: item.path });
6080
6258
  if (!this.host.isConnected) return;
6081
6259
  const parsed = parseDoorstopBaselineResponse(response);
6082
6260
  if (!parsed.git) {
@@ -6145,12 +6323,12 @@ var DoorstopWorkspaceController = class {
6145
6323
  this.gitStatusInFlight = true;
6146
6324
  this.requestUpdate();
6147
6325
  try {
6148
- const backend = this.context.backend;
6149
- if (backend === void 0) {
6326
+ const peer = this.context.peer;
6327
+ if (peer?.request === void 0) {
6150
6328
  this.gitStatusView = { state: "no-git" };
6151
6329
  return;
6152
6330
  }
6153
- const response = await backend.request(DOORSTOP_GIT_STATUS_OPERATION, {});
6331
+ const response = await peer.request(DOORSTOP_GIT_STATUS_OPERATION, {});
6154
6332
  if (!this.host.isConnected || this.gitStatusView?.state !== "loading") return;
6155
6333
  const parsed = parseDoorstopGitStatusResponse(response);
6156
6334
  this.gitStatusView = parsed.git ? { state: "ready", response: parsed } : { state: "no-git" };
@@ -6219,7 +6397,7 @@ function doorstopPaths(result) {
6219
6397
  return paths;
6220
6398
  }
6221
6399
 
6222
- // src/doorstop-panel.ts
6400
+ // src/browser/doorstop-panel.ts
6223
6401
  var DOORSTOP_WORKSPACE_STATE_LIMIT = 8;
6224
6402
  var MAX_ITEM_DIGITS = 24;
6225
6403
  async function loadDoorstopWorkspace(files) {
@@ -6352,6 +6530,15 @@ function escapeRegExp(value) {
6352
6530
  var DoorstopWorkspaceRegistry = class {
6353
6531
  constructor() {
6354
6532
  this.workspaces = /* @__PURE__ */ new Map();
6533
+ /** Plan Phase C step C3 — the chat file-open index: per-workspace
6534
+ * item-file path → { uid, documentPrefix } (plus the landed result it was
6535
+ * built from, kept by identity), recorded from every LANDED
6536
+ * load result (renderDoorstopPanel records it whenever the controller
6537
+ * has a result), so the SYNCHRONOUS `fileOpenQuery` hook can answer from
6538
+ * the last landed discovery without running one. Same LRU bound and the
6539
+ * same workspace keys as the controller map; an evicted workspace drops
6540
+ * its file-open entries with its controller. */
6541
+ this.fileOpenEntries = /* @__PURE__ */ new Map();
6355
6542
  }
6356
6543
  /** Get-or-create the controller for a workspace (the render entry point). */
6357
6544
  for(context) {
@@ -6373,10 +6560,67 @@ var DoorstopWorkspaceRegistry = class {
6373
6560
  return controller;
6374
6561
  }
6375
6562
  /** Panel invalidation: re-run discovery + load unconditionally for the
6376
- * connected workspace (paired or unpaired: the load job is browser-side, so there is no owned-workspace gate). */
6377
- invalidate(context) {
6563
+ * connected workspace (paired or unpaired: the load job is browser-side, so there is no owned-workspace gate).
6564
+ *
6565
+ * Plan Phase C step C1 — the v4 typed invalidation: `invalidation` carries
6566
+ * the reason (`"manual"` Refresh action / `"mutation"` a workspace file
6567
+ * changed / `"agent-activity"` agent work finished) and the affected
6568
+ * resources. The rescan stays REASON-AGNOSTIC deliberately: every reason
6569
+ * may have changed item files, the load job re-reads everything anyway
6570
+ * (no partial invalidation is possible — the index is one unit), and the
6571
+ * controller's re-entrant guard coalesces storms (an invalidate while a
6572
+ * load is running JOINS that load; only an invalidate after it lands
6573
+ * starts a fresh one), so `"agent-activity"` bursts cannot stack loads.
6574
+ * The payload is accepted (so the contribution can forward it) but not
6575
+ * consulted. */
6576
+ invalidate(context, invalidation) {
6577
+ void invalidation;
6378
6578
  return this.for(context).invalidate();
6379
6579
  }
6580
+ /** Record the chat file-open index for one workspace from a LANDED result
6581
+ * (plan Phase C step C3). Called from renderDoorstopPanel on every render
6582
+ * that has a result, so the index always mirrors the last landed
6583
+ * discovery; never throws and triggers nothing (the hook must be free of
6584
+ * side effects — recording a plain index from a landed result is one). */
6585
+ recordFileOpenIndex(context, result) {
6586
+ const key = workspaceContextKey(context);
6587
+ const existing = this.fileOpenEntries.get(key);
6588
+ if (existing !== void 0 && existing.result === result) {
6589
+ this.fileOpenEntries.delete(key);
6590
+ this.fileOpenEntries.set(key, existing);
6591
+ return;
6592
+ }
6593
+ const entries = /* @__PURE__ */ new Map();
6594
+ for (const item of result.index.items) {
6595
+ entries.set(item.path, { uid: item.uid, documentPrefix: item.documentPrefix });
6596
+ }
6597
+ this.fileOpenEntries.delete(key);
6598
+ this.fileOpenEntries.set(key, { result, entries });
6599
+ this.boundFileOpenEntries();
6600
+ }
6601
+ /** The panel's chat file-open hook (plan Phase C step C3): does the last
6602
+ * landed discovery of this workspace know `path` as an item file? Returns
6603
+ * the panel's navigation query (`{ file: path }` — the key
6604
+ * applyNavigationQuery resolves to the item's uid and document
6605
+ * selection), or undefined when the panel does not own the path (the
6606
+ * host then asks the next panel, or serves the Files fallback). Before
6607
+ * the first load lands there is no index yet → undefined (the chat link
6608
+ * falls back to Files; the panel still deep-links once opened and
6609
+ * re-rendered). */
6610
+ fileOpenQuery(context, path) {
6611
+ const entries = this.fileOpenEntries.get(workspaceContextKey(context));
6612
+ if (entries === void 0) return void 0;
6613
+ return entries.entries.has(path) ? { [DOORSTOP_NAV_FILE]: path } : void 0;
6614
+ }
6615
+ /** Keep the file-open index at the same LRU bound as the controller map
6616
+ * (strictly oldest-first, mirroring evictOldest). */
6617
+ boundFileOpenEntries() {
6618
+ while (this.fileOpenEntries.size > DOORSTOP_WORKSPACE_STATE_LIMIT) {
6619
+ const key = this.fileOpenEntries.keys().next().value;
6620
+ if (key === void 0) break;
6621
+ this.fileOpenEntries.delete(key);
6622
+ }
6623
+ }
6380
6624
  evictOldest() {
6381
6625
  if (this.workspaces.size < DOORSTOP_WORKSPACE_STATE_LIMIT) return;
6382
6626
  const key = this.workspaces.keys().next().value;
@@ -6384,6 +6628,7 @@ var DoorstopWorkspaceRegistry = class {
6384
6628
  const controller = this.workspaces.get(key);
6385
6629
  if (controller !== void 0) controller.release();
6386
6630
  this.workspaces.delete(key);
6631
+ this.fileOpenEntries.delete(key);
6387
6632
  }
6388
6633
  };
6389
6634
  var DoorstopPanelHost = class {
@@ -6395,127 +6640,94 @@ function workspaceContextKey(context) {
6395
6640
  return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
6396
6641
  }
6397
6642
 
6398
- // src/doorstop-panel-view-model.ts
6399
- var EMPTY_WORKSPACE_MESSAGE = "This workspace has no Doorstop documents \u2014 run `doorstop create REQ ./reqs` in the workspace to start a requirements tree.";
6400
- var ALL_DOCUMENTS_LABEL = "All";
6401
- var STATE_CHIP_LABELS = {
6402
- normative: "normative",
6403
- "non-normative": "non-normative",
6404
- inactive: "inactive",
6405
- reviewed: "reviewed",
6406
- unreviewed: "unreviewed",
6407
- "suspect-link": "suspect link",
6408
- "no-child-links": "no child links",
6409
- "no-links": "no links",
6410
- "unknown-link": "unknown link",
6411
- "missing-reference": "missing reference"
6412
- };
6413
- function stateChipKind(key) {
6414
- switch (key) {
6415
- case "normative":
6416
- case "non-normative":
6417
- case "reviewed":
6418
- return "muted";
6419
- case "unreviewed":
6420
- case "no-child-links":
6421
- case "no-links":
6422
- return "warning";
6423
- case "inactive":
6424
- case "suspect-link":
6425
- case "unknown-link":
6426
- case "missing-reference":
6427
- return "danger";
6643
+ // src/doorstop-session-metadata.ts
6644
+ var OPENDOOR_SESSION_METADATA_NAMESPACE = "opendoor";
6645
+ var DOORSTOP_SESSION_METADATA_SCHEMA = 1;
6646
+ var DOORSTOP_SESSION_METADATA_MAX_UIDS = 8;
6647
+ var DOORSTOP_SESSION_METADATA_MAX_UID_LENGTH = 32;
6648
+ var DOORSTOP_SESSION_METADATA_OPS = /* @__PURE__ */ new Set(["review"]);
6649
+ var DOORSTOP_SESSION_METADATA_UID_PATTERN = /^[\w.-]+$/u;
6650
+ function isMetadataUid(value) {
6651
+ return typeof value === "string" && value !== "" && value.length <= DOORSTOP_SESSION_METADATA_MAX_UID_LENGTH && DOORSTOP_SESSION_METADATA_UID_PATTERN.test(value);
6652
+ }
6653
+ function parseDoorstopSessionMetadata(value) {
6654
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
6655
+ const record = value;
6656
+ if (record["schema"] !== DOORSTOP_SESSION_METADATA_SCHEMA) return void 0;
6657
+ const op = record["op"];
6658
+ if (typeof op !== "string" || !DOORSTOP_SESSION_METADATA_OPS.has(op)) {
6659
+ return void 0;
6428
6660
  }
6429
- }
6430
- function dotTitle(dot) {
6431
- switch (dot) {
6432
- case "ok":
6433
- return "all items reviewed, no suspect links";
6434
- case "unreviewed":
6435
- return "has unreviewed changes";
6436
- case "suspect":
6437
- return "has suspect links";
6661
+ const uidsValue = record["uids"];
6662
+ if (!Array.isArray(uidsValue) || uidsValue.length === 0 || uidsValue.length > DOORSTOP_SESSION_METADATA_MAX_UIDS) {
6663
+ return void 0;
6664
+ }
6665
+ const uids = [];
6666
+ for (const uid of uidsValue) {
6667
+ if (!isMetadataUid(uid)) return void 0;
6668
+ uids.push(uid);
6438
6669
  }
6670
+ return {
6671
+ schema: DOORSTOP_SESSION_METADATA_SCHEMA,
6672
+ op,
6673
+ uids
6674
+ };
6439
6675
  }
6440
- function documentStateDots(document2, items) {
6441
- const own = items.filter((item) => item.documentPrefix === document2.prefix);
6442
- const dots = [];
6443
- if (own.length === 0) return dots;
6444
- const hasSuspect = own.some((item) => item.stateKeys.includes("suspect-link"));
6445
- const hasUnreviewed = own.some((item) => item.stateKeys.includes("unreviewed"));
6446
- if (!hasSuspect && !hasUnreviewed) dots.push("ok");
6447
- if (hasUnreviewed) dots.push("unreviewed");
6448
- if (hasSuspect) dots.push("suspect");
6449
- return dots;
6676
+
6677
+ // src/browser/doorstop-session-labels.ts
6678
+ var DOORSTOP_PANEL_LOCAL_ID = "workspace.doorstop";
6679
+ var DOORSTOP_SESSION_LABEL_ID = "doorstop.review";
6680
+ function createDoorstopSessionLabel(runtimePluginId) {
6681
+ const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID}`;
6682
+ return {
6683
+ id: DOORSTOP_SESSION_LABEL_ID,
6684
+ order: 10,
6685
+ items: (context) => sessionLabelItems(context, panelId)
6686
+ };
6450
6687
  }
6451
- function filteredItems(index, documentPrefix, stateFilter, search) {
6452
- const needle = search.trim().toLowerCase();
6453
- return index.items.filter((item) => {
6454
- if (documentPrefix !== void 0 && documentPrefix !== "" && item.documentPrefix !== documentPrefix) {
6455
- return false;
6456
- }
6457
- if (stateFilter !== void 0 && !item.stateKeys.includes(stateFilter)) return false;
6458
- if (needle !== "" && !`${item.uid} ${item.header ?? ""} ${item.text}`.toLowerCase().includes(needle)) {
6459
- return false;
6688
+ function sessionLabelItems(context, panelId) {
6689
+ const value = context.session.metadata?.[OPENDOOR_SESSION_METADATA_NAMESPACE];
6690
+ const metadata = parseDoorstopSessionMetadata(value);
6691
+ if (metadata === void 0) return [];
6692
+ const firstUid = metadata.uids[0];
6693
+ if (firstUid === void 0) return [];
6694
+ const rest = metadata.uids.length - 1;
6695
+ const text = rest === 0 ? firstUid : `${firstUid} +${String(rest)}`;
6696
+ const target = doorstopReopenDestination(context.machine.id, context.workspace, panelId, firstUid);
6697
+ return [
6698
+ {
6699
+ type: "render",
6700
+ render: () => b2`<a
6701
+ class="doorstop-session-label"
6702
+ href="#"
6703
+ title=${`Open ${metadata.uids.join(", ")} in Requirements`}
6704
+ @click=${(event) => {
6705
+ event.preventDefault();
6706
+ event.stopPropagation();
6707
+ void context.navigate(target);
6708
+ }}
6709
+ >${text}</a>`
6460
6710
  }
6461
- return true;
6462
- });
6711
+ ];
6712
+ }
6713
+ function doorstopReopenDestination(machineId, workspace, panelId, uid) {
6714
+ return {
6715
+ machineId,
6716
+ projectId: workspace.projectId,
6717
+ workspaceId: workspace.id,
6718
+ view: "workspace",
6719
+ tool: panelId,
6720
+ query: { [DOORSTOP_NAV_UID]: uid }
6721
+ };
6463
6722
  }
6723
+
6724
+ // src/browser/view-model/core.ts
6464
6725
  function shortFingerprint(fingerprint) {
6465
6726
  return fingerprint === null ? "none" : `${fingerprint.slice(0, 8)}\u2026`;
6466
6727
  }
6467
- var FINDINGS_EMPTY_MESSAGE = "No findings \u2014 the tree is clean.";
6468
- var FINDINGS_EMPTY_HINT = "Findings are plugin-local; run `doorstop` validation in the terminal for the authoritative check.";
6469
- var FINDINGS_PLUGIN_LOCAL_NOTE = "plugin-local findings \u2014 `doorstop` validation in the terminal is authoritative";
6470
- var FINDING_SEVERITY_RANK = {
6471
- error: 0,
6472
- warning: 1,
6473
- info: 2
6474
- };
6475
- function findingsViewRows(index) {
6476
- const rows = [...index.findings, ...index.diagnostics];
6477
- return rows.sort((a3, b3) => FINDING_SEVERITY_RANK[a3.severity] - FINDING_SEVERITY_RANK[b3.severity]);
6478
- }
6479
- function findingsViewCounts(rows) {
6480
- const counts = { errors: 0, warnings: 0, info: 0 };
6481
- for (const row of rows) {
6482
- if (row.severity === "error") counts.errors += 1;
6483
- else if (row.severity === "warning") counts.warnings += 1;
6484
- else counts.info += 1;
6485
- }
6486
- return counts;
6487
- }
6488
- function findingsCountText(counts) {
6489
- const errors = counts.errors === 1 ? "1 error" : `${String(counts.errors)} errors`;
6490
- const warnings = counts.warnings === 1 ? "1 warning" : `${String(counts.warnings)} warnings`;
6491
- return `${errors} \xB7 ${warnings} \xB7 ${String(counts.info)} info`;
6492
- }
6493
6728
  function doorstopPublishTarget(result) {
6494
6729
  return result?.settings?.publishTarget ?? DEFAULT_OPENDOOR_SETTINGS.publishTarget;
6495
6730
  }
6496
- function doorstopCommitAfterReview(result) {
6497
- return result?.settings?.commitAfterReview ?? DEFAULT_OPENDOOR_SETTINGS.commitAfterReview;
6498
- }
6499
- function commitOutcomeText(op, outcome) {
6500
- const gitRun = op === "git-stage" || op === "git-commit" || op === "git-unstage";
6501
- switch (outcome.status) {
6502
- case "staged":
6503
- return `staged ${String(outcome.staged ?? 0)} paths`;
6504
- case "unstaged":
6505
- return `unstaged ${String(outcome.unstaged ?? 0)} paths`;
6506
- case "committed":
6507
- return gitRun ? `committed ${outcome.sha ?? "<unknown sha>"}` : `commit: ${outcome.sha ?? "<unknown sha>"}`;
6508
- case "clean":
6509
- if (!gitRun) return "commit: clean (already committed)";
6510
- if (op === "git-stage") return "clean \u2014 nothing to stage";
6511
- if (op === "git-unstage") return "clean \u2014 nothing to unstage";
6512
- return "clean \u2014 nothing staged";
6513
- case "skipped":
6514
- return gitRun ? "skipped" : "commit: skipped (not a git repository | review failed | deadline)";
6515
- case "failed":
6516
- return gitRun ? `failed \u2014 ${outcome.stderr ?? "git step errored"}` : `commit: failed \u2014 ${outcome.stderr ?? "git step errored"}`;
6517
- }
6518
- }
6519
6731
  function lastRunHasMessage(lastRun) {
6520
6732
  return lastRun.stdout !== "" || lastRun.stderr !== "" || (lastRun.errorMessage ?? "") !== "" || lastRun.commit !== void 0;
6521
6733
  }
@@ -6591,7 +6803,6 @@ function doorstopPublishCommand(result, target = doorstopPublishTarget(result))
6591
6803
  const argument = PUBLISH_TARGET_SAFE_TOKEN.test(target) ? target : quoteShellArgument(target);
6592
6804
  return `doorstop publish all ${argument}`;
6593
6805
  }
6594
- var isValidTargetUid = isValidDoorstopUid;
6595
6806
  function documentConfigFor(index, item) {
6596
6807
  return index.byPrefix.get(item.documentPrefix) ?? {
6597
6808
  directoryPath: "",
@@ -6603,24 +6814,6 @@ function documentConfigFor(index, item) {
6603
6814
  extra: {}
6604
6815
  };
6605
6816
  }
6606
- function suspectParentItems(item, index) {
6607
- const suspects = [];
6608
- for (const link of item.links) {
6609
- if (link.fingerprint === null) continue;
6610
- const target = index.byUid.get(link.uid);
6611
- if (target === void 0) continue;
6612
- if (link.fingerprint !== computeItemStamp(target, documentConfigFor(index, target), false)) {
6613
- suspects.push(target);
6614
- }
6615
- }
6616
- return suspects;
6617
- }
6618
- function firstChildDocumentPrefix(index, item) {
6619
- for (const document2 of index.documents) {
6620
- if (document2.parentPrefix === item.documentPrefix) return document2.prefix;
6621
- }
6622
- return void 0;
6623
- }
6624
6817
  function jsonishText(value) {
6625
6818
  if (value === void 0) return "undefined";
6626
6819
  const json2 = JSON.stringify(value);
@@ -6637,68 +6830,179 @@ function referenceListText(references) {
6637
6830
  return parts.join(" ");
6638
6831
  }).join(", ");
6639
6832
  }
6640
- function itemExcerpt(item) {
6641
- const header = item.header;
6642
- if (header !== void 0 && header !== "") return header;
6643
- const firstLine = (item.text.split("\n")[0] ?? "").trim();
6644
- if (firstLine === "") return "\u2014";
6645
- return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
6646
- }
6647
6833
 
6648
- // node_modules/lit-html/directive.js
6649
- var t3 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };
6650
- var e4 = (t5) => (...e8) => ({ _$litDirective$: t5, values: e8 });
6651
- var i5 = class {
6652
- constructor(t5) {
6653
- }
6654
- get _$AU() {
6655
- return this._$AM._$AU;
6656
- }
6657
- _$AT(t5, e8, i8) {
6658
- this._$Ct = t5, this._$AM = e8, this._$Ci = i8;
6659
- }
6660
- _$AS(t5, e8) {
6661
- return this.update(t5, e8);
6662
- }
6663
- update(t5, e8) {
6664
- return this.render(...e8);
6665
- }
6834
+ // src/browser/view-model/item-list.ts
6835
+ var EMPTY_WORKSPACE_MESSAGE = "This workspace has no Doorstop documents \u2014 run `doorstop create REQ ./reqs` in the workspace to start a requirements tree.";
6836
+ var ALL_DOCUMENTS_LABEL = "All";
6837
+ var STATE_CHIP_LABELS = {
6838
+ normative: "normative",
6839
+ "non-normative": "non-normative",
6840
+ inactive: "inactive",
6841
+ reviewed: "reviewed",
6842
+ unreviewed: "unreviewed",
6843
+ "suspect-link": "suspect link",
6844
+ "no-child-links": "no child links",
6845
+ "no-links": "no links",
6846
+ "unknown-link": "unknown link",
6847
+ "missing-reference": "missing reference"
6666
6848
  };
6667
-
6668
- // node_modules/lit-html/directives/class-map.js
6669
- var e5 = e4(class extends i5 {
6670
- constructor(t5) {
6671
- if (super(t5), t5.type !== t3.ATTRIBUTE || "class" !== t5.name || t5.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.");
6672
- }
6673
- render(t5) {
6674
- return " " + Object.keys(t5).filter((s6) => t5[s6]).join(" ") + " ";
6675
- }
6676
- update(s6, [i8]) {
6677
- if (void 0 === this.st) {
6678
- this.st = /* @__PURE__ */ new Set(), void 0 !== s6.strings && (this.nt = new Set(s6.strings.join(" ").split(/\s/).filter((t5) => "" !== t5)));
6679
- for (const t5 in i8) i8[t5] && !this.nt?.has(t5) && this.st.add(t5);
6680
- return this.render(i8);
6849
+ function stateChipKind(key) {
6850
+ switch (key) {
6851
+ case "normative":
6852
+ case "non-normative":
6853
+ case "reviewed":
6854
+ return "muted";
6855
+ case "unreviewed":
6856
+ case "no-child-links":
6857
+ case "no-links":
6858
+ return "warning";
6859
+ case "inactive":
6860
+ case "suspect-link":
6861
+ case "unknown-link":
6862
+ case "missing-reference":
6863
+ return "danger";
6864
+ }
6865
+ }
6866
+ function dotTitle(dot) {
6867
+ switch (dot) {
6868
+ case "ok":
6869
+ return "all items reviewed, no suspect links";
6870
+ case "unreviewed":
6871
+ return "has unreviewed changes";
6872
+ case "suspect":
6873
+ return "has suspect links";
6874
+ }
6875
+ }
6876
+ function documentStateDots(document2, items) {
6877
+ const own = items.filter((item) => item.documentPrefix === document2.prefix);
6878
+ const dots = [];
6879
+ if (own.length === 0) return dots;
6880
+ const hasSuspect = own.some((item) => item.stateKeys.includes("suspect-link"));
6881
+ const hasUnreviewed = own.some((item) => item.stateKeys.includes("unreviewed"));
6882
+ if (!hasSuspect && !hasUnreviewed) dots.push("ok");
6883
+ if (hasUnreviewed) dots.push("unreviewed");
6884
+ if (hasSuspect) dots.push("suspect");
6885
+ return dots;
6886
+ }
6887
+ function filteredItems(index, documentPrefix, stateFilter, search) {
6888
+ const needle = search.trim().toLowerCase();
6889
+ return index.items.filter((item) => {
6890
+ if (documentPrefix !== void 0 && documentPrefix !== "" && item.documentPrefix !== documentPrefix) {
6891
+ return false;
6681
6892
  }
6682
- const r8 = s6.element.classList;
6683
- for (const t5 of this.st) t5 in i8 || (r8.remove(t5), this.st.delete(t5));
6684
- for (const t5 in i8) {
6685
- const s7 = !!i8[t5];
6686
- s7 === this.st.has(t5) || this.nt?.has(t5) || (s7 ? (r8.add(t5), this.st.add(t5)) : (r8.remove(t5), this.st.delete(t5)));
6893
+ if (stateFilter !== void 0 && !item.stateKeys.includes(stateFilter)) return false;
6894
+ if (needle !== "" && !`${item.uid} ${item.header ?? ""} ${item.text}`.toLowerCase().includes(needle)) {
6895
+ return false;
6687
6896
  }
6688
- return E;
6897
+ return true;
6898
+ });
6899
+ }
6900
+ function doorstopShowAdditionalAttribute(result) {
6901
+ return result?.settings?.showAdditionalAttribute ?? DEFAULT_OPENDOOR_SETTINGS.showAdditionalAttribute;
6902
+ }
6903
+ function itemExcerpt(item) {
6904
+ const header = item.header;
6905
+ if (header !== void 0 && header !== "") return header;
6906
+ const firstLine = (item.text.split("\n")[0] ?? "").trim();
6907
+ if (firstLine === "") return "\u2014";
6908
+ return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
6909
+ }
6910
+ function itemTextRow(item) {
6911
+ return item.text.trim() === "" ? void 0 : item.text;
6912
+ }
6913
+ function shownAttributeRows(item, keys) {
6914
+ const rows = [];
6915
+ for (const key of keys) {
6916
+ if (!Object.prototype.hasOwnProperty.call(item.attributes, key)) continue;
6917
+ rows.push({ key, text: jsonishText(item.attributes[key]) });
6689
6918
  }
6690
- });
6919
+ return rows;
6920
+ }
6921
+
6922
+ // src/browser/view-model/palette.ts
6923
+ function doorstopCommitAfterReview(result) {
6924
+ return result?.settings?.commitAfterReview ?? DEFAULT_OPENDOOR_SETTINGS.commitAfterReview;
6925
+ }
6926
+ var isValidTargetUid = isValidDoorstopUid;
6927
+ function suspectParentItems(item, index) {
6928
+ const suspects = [];
6929
+ for (const link of item.links) {
6930
+ if (link.fingerprint === null) continue;
6931
+ const target = index.byUid.get(link.uid);
6932
+ if (target === void 0) continue;
6933
+ if (link.fingerprint !== computeItemStamp(target, documentConfigFor(index, target), false)) {
6934
+ suspects.push(target);
6935
+ }
6936
+ }
6937
+ return suspects;
6938
+ }
6939
+ function firstChildDocumentPrefix(index, item) {
6940
+ for (const document2 of index.documents) {
6941
+ if (document2.parentPrefix === item.documentPrefix) return document2.prefix;
6942
+ }
6943
+ return void 0;
6944
+ }
6945
+
6946
+ // src/browser/view-model/status-bar.ts
6947
+ function commitOutcomeText(op, outcome) {
6948
+ const gitRun = op === "git-stage" || op === "git-commit" || op === "git-unstage";
6949
+ switch (outcome.status) {
6950
+ case "staged":
6951
+ return `staged ${String(outcome.staged ?? 0)} paths`;
6952
+ case "unstaged":
6953
+ return `unstaged ${String(outcome.unstaged ?? 0)} paths`;
6954
+ case "committed":
6955
+ return gitRun ? `committed ${outcome.sha ?? "<unknown sha>"}` : `commit: ${outcome.sha ?? "<unknown sha>"}`;
6956
+ case "clean":
6957
+ if (!gitRun) return "commit: clean (already committed)";
6958
+ if (op === "git-stage") return "clean \u2014 nothing to stage";
6959
+ if (op === "git-unstage") return "clean \u2014 nothing to unstage";
6960
+ return "clean \u2014 nothing staged";
6961
+ case "skipped":
6962
+ return gitRun ? "skipped" : "commit: skipped (not a git repository | review failed | deadline)";
6963
+ case "failed":
6964
+ return gitRun ? `failed \u2014 ${outcome.stderr ?? "git step errored"}` : `commit: failed \u2014 ${outcome.stderr ?? "git step errored"}`;
6965
+ }
6966
+ }
6967
+
6968
+ // src/browser/view-model/findings.ts
6969
+ var FINDINGS_EMPTY_MESSAGE = "No findings \u2014 the tree is clean.";
6970
+ var FINDINGS_EMPTY_HINT = "Findings are plugin-local; run `doorstop` validation in the terminal for the authoritative check.";
6971
+ var FINDINGS_PLUGIN_LOCAL_NOTE = "plugin-local findings \u2014 `doorstop` validation in the terminal is authoritative";
6972
+ var FINDING_SEVERITY_RANK = {
6973
+ error: 0,
6974
+ warning: 1,
6975
+ info: 2
6976
+ };
6977
+ function findingsViewRows(index) {
6978
+ const rows = [...index.findings, ...index.diagnostics];
6979
+ return rows.sort((a3, b3) => FINDING_SEVERITY_RANK[a3.severity] - FINDING_SEVERITY_RANK[b3.severity]);
6980
+ }
6981
+ function findingsViewCounts(rows) {
6982
+ const counts = { errors: 0, warnings: 0, info: 0 };
6983
+ for (const row of rows) {
6984
+ if (row.severity === "error") counts.errors += 1;
6985
+ else if (row.severity === "warning") counts.warnings += 1;
6986
+ else counts.info += 1;
6987
+ }
6988
+ return counts;
6989
+ }
6990
+ function findingsCountText(counts) {
6991
+ const errors = counts.errors === 1 ? "1 error" : `${String(counts.errors)} errors`;
6992
+ const warnings = counts.warnings === 1 ? "1 warning" : `${String(counts.warnings)} warnings`;
6993
+ return `${errors} \xB7 ${warnings} \xB7 ${String(counts.info)} info`;
6994
+ }
6691
6995
 
6692
6996
  // node_modules/lit-html/directive-helpers.js
6693
- var { I: t4 } = j;
6694
- var i6 = (o8) => o8;
6997
+ var { I: t3 } = j;
6998
+ var i5 = (o8) => o8;
6695
6999
  var r4 = (o8) => void 0 === o8.strings;
6696
7000
  var s4 = () => document.createComment("");
6697
7001
  var v2 = (o8, n7, e8) => {
6698
7002
  const l3 = o8._$AA.parentNode, d3 = void 0 === n7 ? o8._$AB : n7._$AA;
6699
7003
  if (void 0 === e8) {
6700
7004
  const i8 = l3.insertBefore(s4(), d3), n8 = l3.insertBefore(s4(), d3);
6701
- e8 = new t4(i8, n8, o8, o8.options);
7005
+ e8 = new t3(i8, n8, o8, o8.options);
6702
7006
  } else {
6703
7007
  const t5 = e8._$AB.nextSibling, n8 = e8._$AM, c6 = n8 !== o8;
6704
7008
  if (c6) {
@@ -6708,8 +7012,8 @@ var v2 = (o8, n7, e8) => {
6708
7012
  if (t5 !== d3 || c6) {
6709
7013
  let o9 = e8._$AA;
6710
7014
  for (; o9 !== t5; ) {
6711
- const t6 = i6(o9).nextSibling;
6712
- i6(l3).insertBefore(o9, d3), o9 = t6;
7015
+ const t6 = i5(o9).nextSibling;
7016
+ i5(l3).insertBefore(o9, d3), o9 = t6;
6713
7017
  }
6714
7018
  }
6715
7019
  }
@@ -6723,18 +7027,25 @@ var h3 = (o8) => {
6723
7027
  o8._$AR(), o8._$AA.remove();
6724
7028
  };
6725
7029
 
6726
- // node_modules/lit-html/directives/keyed.js
6727
- var i7 = e4(class extends i5 {
6728
- constructor() {
6729
- super(...arguments), this.key = A;
7030
+ // node_modules/lit-html/directive.js
7031
+ var t4 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };
7032
+ var e4 = (t5) => (...e8) => ({ _$litDirective$: t5, values: e8 });
7033
+ var i6 = class {
7034
+ constructor(t5) {
6730
7035
  }
6731
- render(r8, t5) {
6732
- return this.key = r8, t5;
7036
+ get _$AU() {
7037
+ return this._$AM._$AU;
6733
7038
  }
6734
- update(r8, [t5, e8]) {
6735
- return t5 !== this.key && (p3(r8), this.key = t5), e8;
7039
+ _$AT(t5, e8, i8) {
7040
+ this._$Ct = t5, this._$AM = e8, this._$Ci = i8;
6736
7041
  }
6737
- });
7042
+ _$AS(t5, e8) {
7043
+ return this.update(t5, e8);
7044
+ }
7045
+ update(t5, e8) {
7046
+ return this.render(...e8);
7047
+ }
7048
+ };
6738
7049
 
6739
7050
  // node_modules/lit-html/async-directive.js
6740
7051
  var s5 = (i8, t5) => {
@@ -6768,9 +7079,9 @@ function n4(i8, t5 = false, e8 = 0) {
6768
7079
  else s5(this, i8);
6769
7080
  }
6770
7081
  var c4 = (i8) => {
6771
- i8.type == t3.CHILD && (i8._$AP ??= n4, i8._$AQ ??= h4);
7082
+ i8.type == t4.CHILD && (i8._$AP ??= n4, i8._$AQ ??= h4);
6772
7083
  };
6773
- var f3 = class extends i5 {
7084
+ var f3 = class extends i6 {
6774
7085
  constructor() {
6775
7086
  super(...arguments), this._$AN = void 0;
6776
7087
  }
@@ -6794,7 +7105,7 @@ var f3 = class extends i5 {
6794
7105
  };
6795
7106
 
6796
7107
  // node_modules/lit-html/directives/ref.js
6797
- var e6 = () => new h5();
7108
+ var e5 = () => new h5();
6798
7109
  var h5 = class {
6799
7110
  };
6800
7111
  var o6 = /* @__PURE__ */ new WeakMap();
@@ -6824,59 +7135,6 @@ var n5 = e4(class extends f3 {
6824
7135
  }
6825
7136
  });
6826
7137
 
6827
- // node_modules/lit-html/directives/repeat.js
6828
- var u4 = (e8, s6, t5) => {
6829
- const r8 = /* @__PURE__ */ new Map();
6830
- for (let l3 = s6; l3 <= t5; l3++) r8.set(e8[l3], l3);
6831
- return r8;
6832
- };
6833
- var c5 = e4(class extends i5 {
6834
- constructor(e8) {
6835
- if (super(e8), e8.type !== t3.CHILD) throw Error("repeat() can only be used in text expressions");
6836
- }
6837
- dt(e8, s6, t5) {
6838
- let r8;
6839
- void 0 === t5 ? t5 = s6 : void 0 !== s6 && (r8 = s6);
6840
- const l3 = [], o8 = [];
6841
- let i8 = 0;
6842
- for (const s7 of e8) l3[i8] = r8 ? r8(s7, i8) : i8, o8[i8] = t5(s7, i8), i8++;
6843
- return { values: o8, keys: l3 };
6844
- }
6845
- render(e8, s6, t5) {
6846
- return this.dt(e8, s6, t5).values;
6847
- }
6848
- update(s6, [t5, r8, c6]) {
6849
- const d3 = M2(s6), { values: p4, keys: a3 } = this.dt(t5, r8, c6);
6850
- if (!Array.isArray(d3)) return this.ut = a3, p4;
6851
- const h6 = this.ut ??= [], v3 = [];
6852
- let m3, y3, x2 = 0, j2 = d3.length - 1, k2 = 0, w2 = p4.length - 1;
6853
- for (; x2 <= j2 && k2 <= w2; ) if (null === d3[x2]) x2++;
6854
- else if (null === d3[j2]) j2--;
6855
- else if (h6[x2] === a3[k2]) v3[k2] = u3(d3[x2], p4[k2]), x2++, k2++;
6856
- else if (h6[j2] === a3[w2]) v3[w2] = u3(d3[j2], p4[w2]), j2--, w2--;
6857
- else if (h6[x2] === a3[w2]) v3[w2] = u3(d3[x2], p4[w2]), v2(s6, v3[w2 + 1], d3[x2]), x2++, w2--;
6858
- else if (h6[j2] === a3[k2]) v3[k2] = u3(d3[j2], p4[k2]), v2(s6, d3[x2], d3[j2]), j2--, k2++;
6859
- else if (void 0 === m3 && (m3 = u4(a3, k2, w2), y3 = u4(h6, x2, j2)), m3.has(h6[x2])) if (m3.has(h6[j2])) {
6860
- const e8 = y3.get(a3[k2]), t6 = void 0 !== e8 ? d3[e8] : null;
6861
- if (null === t6) {
6862
- const e9 = v2(s6, d3[x2]);
6863
- u3(e9, p4[k2]), v3[k2] = e9;
6864
- } else v3[k2] = u3(t6, p4[k2]), v2(s6, d3[x2], t6), d3[e8] = null;
6865
- k2++;
6866
- } else h3(d3[j2]), j2--;
6867
- else h3(d3[x2]), x2++;
6868
- for (; k2 <= w2; ) {
6869
- const e8 = v2(s6, v3[w2 + 1]);
6870
- u3(e8, p4[k2]), v3[k2++] = e8;
6871
- }
6872
- for (; x2 <= j2; ) {
6873
- const e8 = d3[x2++];
6874
- null !== e8 && h3(e8);
6875
- }
6876
- return this.ut = a3, p3(s6, v3), E;
6877
- }
6878
- });
6879
-
6880
7138
  // node_modules/@lit/reactive-element/decorators/property.js
6881
7139
  var o7 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };
6882
7140
  var r6 = (t5 = o7, e8, r8) => {
@@ -6912,8 +7170,8 @@ function r7(r8) {
6912
7170
  return n6({ ...r8, state: true, attribute: false });
6913
7171
  }
6914
7172
 
6915
- // src/doorstop-panel-styles.ts
6916
- var panelStyles = i`
7173
+ // src/browser/styles/base.ts
7174
+ var baseStyles = i`
6917
7175
  :host {
6918
7176
  flex: 1 1 auto;
6919
7177
  min-height: 0;
@@ -6922,7 +7180,7 @@ var panelStyles = i`
6922
7180
  overflow: hidden;
6923
7181
  color: var(--pi-text);
6924
7182
  background: var(--pi-bg);
6925
- font: 13px system-ui, sans-serif;
7183
+ font: 14px system-ui, sans-serif;
6926
7184
  }
6927
7185
 
6928
7186
  button {
@@ -6948,7 +7206,7 @@ var panelStyles = i`
6948
7206
  border-radius: 6px;
6949
7207
  background: var(--pi-surface);
6950
7208
  color: var(--pi-text);
6951
- font-size: 12px;
7209
+ font-size: 13px;
6952
7210
  padding: 4px 6px;
6953
7211
  }
6954
7212
 
@@ -6957,13 +7215,16 @@ var panelStyles = i`
6957
7215
  border-radius: 5px;
6958
7216
  background: var(--pi-bg);
6959
7217
  padding: 1px 4px;
6960
- font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
7218
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
6961
7219
  }
6962
7220
 
6963
7221
  .doorstop-muted {
6964
7222
  color: var(--pi-muted);
6965
7223
  }
7224
+ `;
6966
7225
 
7226
+ // src/browser/styles/project-actions.ts
7227
+ var projectActionsStyles = i`
6967
7228
  /* --- project actions (region 1) --- */
6968
7229
  .doorstop-project-actions {
6969
7230
  flex: 0 0 auto;
@@ -7002,7 +7263,7 @@ var panelStyles = i`
7002
7263
  .doorstop-doc-chip {
7003
7264
  border-radius: 999px;
7004
7265
  padding: 2px 9px;
7005
- font-size: 12px;
7266
+ font-size: 13px;
7006
7267
  }
7007
7268
 
7008
7269
  .doorstop-doc-chip.is-selected {
@@ -7013,12 +7274,12 @@ var panelStyles = i`
7013
7274
 
7014
7275
  .doorstop-doc-arrow {
7015
7276
  color: var(--pi-muted);
7016
- font-size: 11px;
7277
+ font-size: 12px;
7017
7278
  }
7018
7279
 
7019
7280
  .doorstop-doc-count {
7020
7281
  color: var(--pi-muted);
7021
- font-size: 11px;
7282
+ font-size: 12px;
7022
7283
  }
7023
7284
 
7024
7285
  .doorstop-dot {
@@ -7053,7 +7314,7 @@ var panelStyles = i`
7053
7314
  border-radius: 999px;
7054
7315
  color: var(--pi-warning);
7055
7316
  padding: 1px 8px;
7056
- font-size: 12px;
7317
+ font-size: 13px;
7057
7318
  }
7058
7319
 
7059
7320
  /* --- project-scoped git actions (plan-add-git-actions Phase D step 17) --- */
@@ -7076,7 +7337,7 @@ var panelStyles = i`
7076
7337
  background: transparent;
7077
7338
  color: var(--pi-muted);
7078
7339
  padding: 2px 9px;
7079
- font-size: 12px;
7340
+ font-size: 13px;
7080
7341
  }
7081
7342
 
7082
7343
  .doorstop-git-status-text {
@@ -7132,7 +7393,7 @@ var panelStyles = i`
7132
7393
  background: transparent;
7133
7394
  color: var(--pi-muted);
7134
7395
  padding: 3px 10px;
7135
- font-size: 12px;
7396
+ font-size: 13px;
7136
7397
  }
7137
7398
 
7138
7399
  .doorstop-view-tab + .doorstop-view-tab {
@@ -7143,7 +7404,10 @@ var panelStyles = i`
7143
7404
  background: var(--pi-selection-bg);
7144
7405
  color: var(--pi-accent);
7145
7406
  }
7407
+ `;
7146
7408
 
7409
+ // src/browser/styles/item-list.ts
7410
+ var itemListStyles = i`
7147
7411
  /* --- viewer (regions 2 + 3; the filters row and split stack) --- */
7148
7412
  .doorstop-viewer {
7149
7413
  flex: 1 1 auto;
@@ -7260,7 +7524,7 @@ var panelStyles = i`
7260
7524
  .doorstop-item-row {
7261
7525
  display: grid;
7262
7526
  grid-template-columns: max-content max-content minmax(0, 1fr) max-content;
7263
- gap: 8px;
7527
+ gap: 2px 8px;
7264
7528
  align-items: baseline;
7265
7529
  width: 100%;
7266
7530
  border: 0;
@@ -7277,7 +7541,7 @@ var panelStyles = i`
7277
7541
 
7278
7542
  .doorstop-item-level {
7279
7543
  color: var(--pi-muted);
7280
- font-size: 11px;
7544
+ font-size: 12px;
7281
7545
  }
7282
7546
 
7283
7547
  .doorstop-item-summary {
@@ -7288,6 +7552,49 @@ var panelStyles = i`
7288
7552
  color: var(--pi-text-secondary);
7289
7553
  }
7290
7554
 
7555
+ /* A fifth column exists only when showAdditionalAttribute configures
7556
+ at least one name; otherwise the default rows keep the original
7557
+ 4-column track (and its single 8px summary→chips gap). */
7558
+ .doorstop-items.has-item-attrs .doorstop-item-row {
7559
+ grid-template-columns: max-content max-content minmax(0, 1fr) minmax(0, max-content) max-content;
7560
+ }
7561
+
7562
+ /* The configured key: value attribute pairs (settings field
7563
+ showAdditionalAttribute). The column collapses to zero when the row
7564
+ renders nothing here (no configured attribute the item has). The
7565
+ max-width bounds a pathologically long value so it cannot starve
7566
+ the summary's fr track before the ellipsis kicks in. */
7567
+ .doorstop-item-attrs {
7568
+ min-width: 0;
7569
+ max-width: 40ch;
7570
+ overflow: hidden;
7571
+ text-overflow: ellipsis;
7572
+ white-space: nowrap;
7573
+ color: var(--pi-muted);
7574
+ font-size: 12px;
7575
+ }
7576
+
7577
+ /* The item body ("text" configured in showAdditionalAttribute) on its own
7578
+ indented second line below level/uid/summary/chips. Full-width implicit
7579
+ grid row; 5ch inline indent; clamped to 2 lines with ellipsis — the full
7580
+ body lives in the detail pane. The span only exists when rendered, so no
7581
+ container modifier class is needed. */
7582
+ .doorstop-item-text {
7583
+ grid-column: 1 / -1;
7584
+ align-self: start;
7585
+ padding-inline-start: 5ch;
7586
+ min-width: 0;
7587
+ color: var(--pi-muted);
7588
+ /* Body-like content: reads one tier above the 12px metadata. */
7589
+ font-size: 13px;
7590
+ white-space: pre-wrap;
7591
+ overflow-wrap: anywhere;
7592
+ display: -webkit-box;
7593
+ -webkit-box-orient: vertical;
7594
+ -webkit-line-clamp: 2;
7595
+ overflow: hidden;
7596
+ }
7597
+
7291
7598
  .doorstop-item-chips {
7292
7599
  display: flex;
7293
7600
  flex-wrap: wrap;
@@ -7301,8 +7608,14 @@ var panelStyles = i`
7301
7608
  keyboard path is the region-4 Stage/Unstage buttons. The same class
7302
7609
  hosts both directions: the plus (git add) icon and the unstage
7303
7610
  minus (git reset) icon, chosen by {@link itemUnstageable} /
7304
- {@link itemStageable} precedence. */
7305
- .doorstop-item-add {
7611
+ {@link itemStageable} precedence.
7612
+
7613
+ The per-row "insert UID into the agent prompt" arrow shares the
7614
+ pointer-only <span> contract and this visual box; it keeps a
7615
+ distinct class because the +/reset icon and the arrow stay
7616
+ independently evolvable. */
7617
+ .doorstop-item-add,
7618
+ .doorstop-item-insert {
7306
7619
  display: inline-flex;
7307
7620
  align-items: center;
7308
7621
  align-self: center;
@@ -7313,7 +7626,8 @@ var panelStyles = i`
7313
7626
  cursor: pointer;
7314
7627
  }
7315
7628
 
7316
- .doorstop-item-add:hover {
7629
+ .doorstop-item-add:hover,
7630
+ .doorstop-item-insert:hover {
7317
7631
  color: var(--pi-text);
7318
7632
  }
7319
7633
 
@@ -7321,6 +7635,7 @@ var panelStyles = i`
7321
7635
  .doorstop-chip {
7322
7636
  border-radius: 999px;
7323
7637
  padding: 0 7px;
7638
+ /* Strict parity: pi-web keeps compact chips at 11px. */
7324
7639
  font-size: 11px;
7325
7640
  white-space: nowrap;
7326
7641
  }
@@ -7344,10 +7659,126 @@ var panelStyles = i`
7344
7659
  border: 1px solid var(--pi-success-border);
7345
7660
  color: var(--pi-success);
7346
7661
  }
7347
-
7348
- /* --- detail pane (region 3) --- */
7349
- .doorstop-detail {
7350
- padding: 12px;
7662
+ `;
7663
+ var findingsStyles = i`
7664
+ /* --- findings view (spec §7.2) --- */
7665
+ .doorstop-findings-view {
7666
+ flex: 1 1 auto;
7667
+ min-height: 0;
7668
+ overflow: auto;
7669
+ padding: 10px 12px;
7670
+ display: grid;
7671
+ gap: 8px;
7672
+ align-content: start;
7673
+ }
7674
+
7675
+ .doorstop-findings-head {
7676
+ display: flex;
7677
+ flex-wrap: wrap;
7678
+ align-items: baseline;
7679
+ gap: 6px 10px;
7680
+ }
7681
+
7682
+ .doorstop-findings-counts {
7683
+ font-size: 13px;
7684
+ font-weight: 600;
7685
+ }
7686
+
7687
+ .doorstop-findings-note {
7688
+ font-size: 12px;
7689
+ }
7690
+
7691
+ .doorstop-findings-list {
7692
+ display: grid;
7693
+ gap: 3px;
7694
+ }
7695
+
7696
+ .doorstop-finding-row {
7697
+ display: flex;
7698
+ flex-wrap: wrap;
7699
+ align-items: baseline;
7700
+ gap: 6px;
7701
+ border-radius: 6px;
7702
+ padding: 5px 8px;
7703
+ }
7704
+
7705
+ .doorstop-finding-row.doorstop-error {
7706
+ border: 1px solid var(--pi-danger);
7707
+ background: color-mix(in srgb, var(--pi-danger) 9%, transparent);
7708
+ }
7709
+
7710
+ .doorstop-finding-row.doorstop-warning {
7711
+ border: 1px solid var(--pi-warning-border);
7712
+ background: color-mix(in srgb, var(--pi-warning) 9%, transparent);
7713
+ }
7714
+
7715
+ .doorstop-finding-row.doorstop-info {
7716
+ border: 1px solid var(--pi-border-muted);
7717
+ color: var(--pi-muted);
7718
+ }
7719
+
7720
+ .doorstop-finding-row .doorstop-severity {
7721
+ background: var(--pi-border-muted);
7722
+ color: var(--pi-muted);
7723
+ }
7724
+
7725
+ .doorstop-finding-row.doorstop-error .doorstop-severity {
7726
+ background: var(--pi-danger);
7727
+ color: var(--pi-bg);
7728
+ }
7729
+
7730
+ .doorstop-finding-row.doorstop-warning .doorstop-severity {
7731
+ background: var(--pi-warning);
7732
+ color: var(--pi-bg);
7733
+ }
7734
+
7735
+ .doorstop-finding-uid {
7736
+ border: 0;
7737
+ border-radius: 5px;
7738
+ background: transparent;
7739
+ color: inherit;
7740
+ padding: 0;
7741
+ font-weight: 600;
7742
+ }
7743
+
7744
+ .doorstop-finding-uid:hover {
7745
+ text-decoration: underline;
7746
+ cursor: pointer;
7747
+ }
7748
+
7749
+ .doorstop-finding-message {
7750
+ min-width: 0;
7751
+ overflow-wrap: anywhere;
7752
+ }
7753
+
7754
+ /* --- empty states --- */
7755
+ .doorstop-empty {
7756
+ margin: 10px 12px;
7757
+ border: 1px dashed var(--pi-border-muted);
7758
+ border-radius: 8px;
7759
+ color: var(--pi-muted);
7760
+ padding: 12px;
7761
+ overflow: auto;
7762
+ }
7763
+
7764
+ .doorstop-empty p {
7765
+ margin: 0;
7766
+ }
7767
+
7768
+ .doorstop-empty button {
7769
+ margin-top: 10px;
7770
+ }
7771
+
7772
+ .doorstop-empty .doorstop-muted {
7773
+ margin-top: 6px;
7774
+ }
7775
+ `;
7776
+
7777
+ // src/browser/styles/detail-pane.ts
7778
+ var detailPaneStyles = i`
7779
+ /* --- detail pane (region 3) --- */
7780
+ .doorstop-detail {
7781
+ padding: 12px;
7351
7782
  display: grid;
7352
7783
  gap: 10px;
7353
7784
  align-content: start;
@@ -7362,14 +7793,14 @@ var panelStyles = i`
7362
7793
 
7363
7794
  .doorstop-detail-head h3 {
7364
7795
  margin: 0;
7365
- font-size: 15px;
7796
+ font-size: 16px;
7366
7797
  min-width: 0;
7367
7798
  overflow-wrap: anywhere;
7368
7799
  }
7369
7800
 
7370
7801
  .doorstop-level {
7371
7802
  color: var(--pi-muted);
7372
- font-size: 12px;
7803
+ font-size: 13px;
7373
7804
  }
7374
7805
 
7375
7806
  .doorstop-flags {
@@ -7383,7 +7814,7 @@ var panelStyles = i`
7383
7814
  border-radius: 999px;
7384
7815
  color: var(--pi-muted);
7385
7816
  padding: 0 7px;
7386
- font-size: 11px;
7817
+ font-size: 12px;
7387
7818
  }
7388
7819
 
7389
7820
  .doorstop-flag.is-inactive {
@@ -7406,7 +7837,7 @@ var panelStyles = i`
7406
7837
 
7407
7838
  .doorstop-changes-summary {
7408
7839
  color: var(--pi-muted);
7409
- font-size: 11px;
7840
+ font-size: 12px;
7410
7841
  text-transform: uppercase;
7411
7842
  letter-spacing: 0.03em;
7412
7843
  cursor: pointer;
@@ -7419,12 +7850,12 @@ var panelStyles = i`
7419
7850
 
7420
7851
  .doorstop-changes-notice {
7421
7852
  margin: 6px 0 0;
7422
- font-size: 12px;
7853
+ font-size: 13px;
7423
7854
  }
7424
7855
 
7425
7856
  .doorstop-changes-source {
7426
7857
  margin: 6px 0 4px;
7427
- font-size: 11px;
7858
+ font-size: 12px;
7428
7859
  }
7429
7860
 
7430
7861
  .doorstop-diff-lines {
@@ -7494,7 +7925,7 @@ var panelStyles = i`
7494
7925
 
7495
7926
  .doorstop-field-name {
7496
7927
  color: var(--pi-muted);
7497
- font-size: 11px;
7928
+ font-size: 12px;
7498
7929
  text-transform: uppercase;
7499
7930
  letter-spacing: 0.03em;
7500
7931
  }
@@ -7502,7 +7933,7 @@ var panelStyles = i`
7502
7933
  .doorstop-field-chip {
7503
7934
  border-radius: 5px;
7504
7935
  border: 1px solid var(--pi-border-muted);
7505
- font-size: 12px;
7936
+ font-size: 13px;
7506
7937
  padding: 1px 6px;
7507
7938
  overflow-wrap: anywhere;
7508
7939
  }
@@ -7525,7 +7956,7 @@ var panelStyles = i`
7525
7956
 
7526
7957
  .doorstop-section-title {
7527
7958
  color: var(--pi-muted);
7528
- font-size: 11px;
7959
+ font-size: 12px;
7529
7960
  text-transform: uppercase;
7530
7961
  letter-spacing: 0.03em;
7531
7962
  margin: 0;
@@ -7568,9 +7999,32 @@ var panelStyles = i`
7568
7999
  background: var(--pi-selection-bg);
7569
8000
  }
7570
8001
 
8002
+ /* The per-link trash can (pointer-only, see {@link renderLinkOut}):
8003
+ right-aligned in the flex row, mirroring .doorstop-item-add. */
8004
+ .doorstop-link-remove {
8005
+ display: inline-flex;
8006
+ align-items: center;
8007
+ align-self: center;
8008
+ flex: 0 0 auto;
8009
+ margin-left: auto;
8010
+ color: var(--pi-muted);
8011
+ cursor: pointer;
8012
+ }
8013
+
8014
+ .doorstop-link-remove:hover {
8015
+ color: var(--pi-text);
8016
+ }
8017
+
7571
8018
  .doorstop-fingerprint {
7572
8019
  color: var(--pi-muted);
7573
- font-size: 11px;
8020
+ font-size: 12px;
8021
+ /* The link row wraps, which could push the trash can onto a second
8022
+ line on narrow panes. Let the fingerprint shrink and ellipsize
8023
+ instead (min-width: 0 lifts the flex min-content floor). */
8024
+ min-width: 0;
8025
+ overflow: hidden;
8026
+ text-overflow: ellipsis;
8027
+ white-space: nowrap;
7574
8028
  }
7575
8029
 
7576
8030
  .doorstop-attributes {
@@ -7588,7 +8042,7 @@ var panelStyles = i`
7588
8042
 
7589
8043
  .doorstop-attribute dt {
7590
8044
  color: var(--pi-muted);
7591
- font-size: 12px;
8045
+ font-size: 13px;
7592
8046
  }
7593
8047
 
7594
8048
  .doorstop-attribute dd {
@@ -7629,10 +8083,14 @@ var panelStyles = i`
7629
8083
 
7630
8084
  .doorstop-finding-severity {
7631
8085
  flex: 0 0 auto;
8086
+ /* Strict parity: pi-web keeps compact chips at 11px. */
7632
8087
  font-size: 11px;
7633
8088
  font-weight: 600;
7634
8089
  }
8090
+ `;
7635
8091
 
8092
+ // src/browser/styles/item-action-palette.ts
8093
+ var itemActionPaletteStyles = i`
7636
8094
  /* --- item action palette (region 4), sized to the prompt footer --- */
7637
8095
  .doorstop-action-palette {
7638
8096
  flex: 0 0 auto;
@@ -7678,7 +8136,7 @@ var panelStyles = i`
7678
8136
 
7679
8137
  .doorstop-op-error {
7680
8138
  color: var(--pi-danger);
7681
- font-size: 12px;
8139
+ font-size: 13px;
7682
8140
  }
7683
8141
 
7684
8142
  .doorstop-menu {
@@ -7688,7 +8146,7 @@ var panelStyles = i`
7688
8146
  .doorstop-menu-items {
7689
8147
  position: absolute;
7690
8148
  right: 0;
7691
- top: calc(100% + 4px);
8149
+ bottom: calc(100% + 4px);
7692
8150
  z-index: 3;
7693
8151
  min-width: 180px;
7694
8152
  display: grid;
@@ -7711,115 +8169,10 @@ var panelStyles = i`
7711
8169
  .doorstop-menu-item:hover:not(:disabled) {
7712
8170
  background: var(--pi-selection-bg);
7713
8171
  }
8172
+ `;
7714
8173
 
7715
- /* --- findings view (spec §7.2) --- */
7716
- .doorstop-findings-view {
7717
- flex: 1 1 auto;
7718
- min-height: 0;
7719
- overflow: auto;
7720
- padding: 10px 12px;
7721
- display: grid;
7722
- gap: 8px;
7723
- align-content: start;
7724
- }
7725
-
7726
- .doorstop-findings-head {
7727
- display: flex;
7728
- flex-wrap: wrap;
7729
- align-items: baseline;
7730
- gap: 6px 10px;
7731
- }
7732
-
7733
- .doorstop-findings-counts {
7734
- font-size: 12px;
7735
- font-weight: 600;
7736
- }
7737
-
7738
- .doorstop-findings-note {
7739
- font-size: 11px;
7740
- }
7741
-
7742
- .doorstop-findings-list {
7743
- display: grid;
7744
- gap: 3px;
7745
- }
7746
-
7747
- .doorstop-finding-row {
7748
- display: flex;
7749
- flex-wrap: wrap;
7750
- align-items: baseline;
7751
- gap: 6px;
7752
- border-radius: 6px;
7753
- padding: 5px 8px;
7754
- }
7755
-
7756
- .doorstop-finding-row.doorstop-error {
7757
- border: 1px solid var(--pi-danger);
7758
- background: color-mix(in srgb, var(--pi-danger) 9%, transparent);
7759
- }
7760
-
7761
- .doorstop-finding-row.doorstop-warning {
7762
- border: 1px solid var(--pi-warning-border);
7763
- background: color-mix(in srgb, var(--pi-warning) 9%, transparent);
7764
- }
7765
-
7766
- .doorstop-finding-row.doorstop-info {
7767
- border: 1px solid var(--pi-border-muted);
7768
- color: var(--pi-muted);
7769
- }
7770
-
7771
- .doorstop-finding-row .doorstop-severity {
7772
- background: var(--pi-border-muted);
7773
- color: var(--pi-muted);
7774
- }
7775
-
7776
- .doorstop-finding-row.doorstop-error .doorstop-severity {
7777
- background: var(--pi-danger);
7778
- color: var(--pi-bg);
7779
- }
7780
-
7781
- .doorstop-finding-row.doorstop-warning .doorstop-severity {
7782
- background: var(--pi-warning);
7783
- color: var(--pi-bg);
7784
- }
7785
-
7786
- .doorstop-finding-uid {
7787
- border: 0;
7788
- border-radius: 5px;
7789
- background: transparent;
7790
- color: inherit;
7791
- padding: 0;
7792
- font-weight: 600;
7793
- }
7794
-
7795
- .doorstop-finding-uid:hover {
7796
- text-decoration: underline;
7797
- cursor: pointer;
7798
- }
7799
-
7800
- .doorstop-finding-message {
7801
- min-width: 0;
7802
- overflow-wrap: anywhere;
7803
- }
7804
-
7805
- /* --- empty states --- */
7806
- .doorstop-empty {
7807
- margin: 10px 12px;
7808
- border: 1px dashed var(--pi-border-muted);
7809
- border-radius: 8px;
7810
- color: var(--pi-muted);
7811
- padding: 12px;
7812
- overflow: auto;
7813
- }
7814
-
7815
- .doorstop-empty p {
7816
- margin: 0;
7817
- }
7818
-
7819
- .doorstop-empty .doorstop-muted {
7820
- margin-top: 6px;
7821
- }
7822
-
8174
+ // src/browser/styles/status-bar.ts
8175
+ var statusBarStyles = i`
7823
8176
  /* --- status bar (region 5), sized to the host center status bar --- */
7824
8177
  .doorstop-status-bar {
7825
8178
  flex: 0 0 auto;
@@ -7837,7 +8190,7 @@ var panelStyles = i`
7837
8190
  border-top: 1px solid var(--pi-border);
7838
8191
  background: var(--pi-bg);
7839
8192
  color: var(--pi-muted);
7840
- font: 12px system-ui, sans-serif;
8193
+ font: 13px system-ui, sans-serif;
7841
8194
  }
7842
8195
 
7843
8196
  .doorstop-status-bar-row {
@@ -7864,7 +8217,7 @@ var panelStyles = i`
7864
8217
 
7865
8218
  .doorstop-last-run-commit {
7866
8219
  margin: 0 0 6px;
7867
- font-size: 12px;
8220
+ font-size: 13px;
7868
8221
  overflow-wrap: anywhere;
7869
8222
  }
7870
8223
 
@@ -7872,6 +8225,7 @@ var panelStyles = i`
7872
8225
  flex: 0 0 auto; /* the badge label keeps its full width */
7873
8226
  border-radius: 999px;
7874
8227
  padding: 0 7px;
8228
+ /* Strict parity: pi-web keeps compact chips at 11px. */
7875
8229
  font-size: 11px;
7876
8230
  white-space: nowrap;
7877
8231
  }
@@ -7904,12 +8258,12 @@ var panelStyles = i`
7904
8258
  text-overflow: ellipsis;
7905
8259
  white-space: nowrap;
7906
8260
  color: var(--pi-muted);
7907
- font-size: 12px;
8261
+ font-size: 13px;
7908
8262
  }
7909
8263
 
7910
8264
  .doorstop-last-run-dismiss {
7911
8265
  flex: 0 0 auto;
7912
- /* Buttons do not inherit font; match the bar's 12px system-ui. */
8266
+ /* Buttons do not inherit font; match the bar's 13px system-ui. */
7913
8267
  font: inherit;
7914
8268
  }
7915
8269
 
@@ -7919,7 +8273,7 @@ var panelStyles = i`
7919
8273
  background: var(--pi-surface);
7920
8274
  color: var(--pi-text);
7921
8275
  padding: 6px 8px;
7922
- font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8276
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
7923
8277
  white-space: pre-wrap;
7924
8278
  word-break: break-word;
7925
8279
  overflow-wrap: anywhere;
@@ -7931,20 +8285,297 @@ var panelStyles = i`
7931
8285
  .doorstop-last-run-notice {
7932
8286
  margin: 4px 0 0;
7933
8287
  color: var(--pi-muted);
7934
- font-size: 11px;
8288
+ font-size: 12px;
7935
8289
  }
7936
8290
  `;
7937
8291
 
7938
- // src/doorstop-panel-icons.ts
8292
+ // src/browser/doorstop-panel-styles.ts
8293
+ var panelStyles = i`${baseStyles}${projectActionsStyles}${itemListStyles}${detailPaneStyles}${itemActionPaletteStyles}${findingsStyles}${statusBarStyles}`;
8294
+
8295
+ // node_modules/lit-html/directives/class-map.js
8296
+ var e7 = e4(class extends i6 {
8297
+ constructor(t5) {
8298
+ if (super(t5), t5.type !== t4.ATTRIBUTE || "class" !== t5.name || t5.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.");
8299
+ }
8300
+ render(t5) {
8301
+ return " " + Object.keys(t5).filter((s6) => t5[s6]).join(" ") + " ";
8302
+ }
8303
+ update(s6, [i8]) {
8304
+ if (void 0 === this.st) {
8305
+ this.st = /* @__PURE__ */ new Set(), void 0 !== s6.strings && (this.nt = new Set(s6.strings.join(" ").split(/\s/).filter((t5) => "" !== t5)));
8306
+ for (const t5 in i8) i8[t5] && !this.nt?.has(t5) && this.st.add(t5);
8307
+ return this.render(i8);
8308
+ }
8309
+ const r8 = s6.element.classList;
8310
+ for (const t5 of this.st) t5 in i8 || (r8.remove(t5), this.st.delete(t5));
8311
+ for (const t5 in i8) {
8312
+ const s7 = !!i8[t5];
8313
+ s7 === this.st.has(t5) || this.nt?.has(t5) || (s7 ? (r8.add(t5), this.st.add(t5)) : (r8.remove(t5), this.st.delete(t5)));
8314
+ }
8315
+ return E;
8316
+ }
8317
+ });
8318
+
8319
+ // src/browser/doorstop-panel-icons.ts
7939
8320
  var doorstopIconSvg = w`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect width="8" height="4" x="8" y="3" rx="1"/><path d="m9 12 2 2 4-4"/></svg>`;
7940
8321
  var refreshIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>`;
7941
8322
  var validateIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>`;
7942
8323
  var publishIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>`;
7943
- var gitStageIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>`;
7944
- var gitUnstageIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/></svg>`;
8324
+ var gitStageIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 12h12"/><path d="M12 6v12"/></svg>`;
8325
+ var gitUnstageIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 12h12"/></svg>`;
7945
8326
  var gitCommitIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`;
8327
+ var insertPromptIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>`;
8328
+ var trashIconSvg = w`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>`;
8329
+
8330
+ // src/browser/sections/project-actions.ts
8331
+ function renderProjectActions(host) {
8332
+ const publishTarget = doorstopPublishTarget(host.result);
8333
+ const projectPath = host.context?.workspace.path ?? "";
8334
+ const projectLabel = projectPath === "" ? "Doorstop" : projectPath.split("/").filter(Boolean).pop() ?? projectPath;
8335
+ return b2`
8336
+ <section class="doorstop-project-actions">
8337
+ <strong class="doorstop-title" title=${projectPath === "" ? A : projectPath}>${doorstopIconSvg}${projectLabel}</strong>
8338
+ ${renderViewToggle(host)}
8339
+ <div class="doorstop-toolbar-actions">
8340
+ ${host.stale ? b2`<button type="button" class="doorstop-stale" title="Doorstop ran or files changed behind the panel — click to rescan" @click=${() => onRefreshClick(host)}>stale — refresh</button>` : A}
8341
+ ${host.confirmSkipped ? b2`<span class="doorstop-muted doorstop-confirm-skipped" title="No confirmation dialog is available in this environment — publishing proceeded without one">confirmation skipped — publishing</span>` : A}
8342
+ <button type="button" class="doorstop-refresh" title="Re-read the workspace" @click=${() => onRefreshClick(host)}>${refreshIconSvg}Refresh</button>
8343
+ <button type="button" class="doorstop-validate" title="Run \`doorstop\` in the workspace (terminal when unpaired)" ?disabled=${host.runInProgress !== void 0} @click=${() => onValidateClick(host)}>${validateIconSvg}Run validation</button>
8344
+ <button type="button" class="doorstop-publish" title=${`Publish the tree to ${publishTarget}`} ?disabled=${host.runInProgress !== void 0} @click=${() => onPublishClick(host)}>${publishIconSvg}Publish HTML</button>
8345
+ ${renderGitActions(host)}
8346
+ </div>
8347
+ </section>
8348
+ `;
8349
+ }
8350
+ function renderViewToggle(host) {
8351
+ return b2`
8352
+ <div class="doorstop-view-toggle" role="tablist" aria-label="Requirements panel view">
8353
+ <button
8354
+ type="button"
8355
+ role="tab"
8356
+ class=${e7({
8357
+ "doorstop-view-tab": true,
8358
+ "doorstop-view-items": true,
8359
+ "is-selected": host.view === "items"
8360
+ })}
8361
+ aria-selected=${host.view === "items" ? "true" : "false"}
8362
+ title="Item list and detail"
8363
+ @click=${() => {
8364
+ host.view = "items";
8365
+ }}
8366
+ >Items</button>
8367
+ <button
8368
+ type="button"
8369
+ role="tab"
8370
+ class=${e7({
8371
+ "doorstop-view-tab": true,
8372
+ "doorstop-view-findings": true,
8373
+ "is-selected": host.view === "findings"
8374
+ })}
8375
+ aria-selected=${host.view === "findings" ? "true" : "false"}
8376
+ title="Validation findings"
8377
+ @click=${() => {
8378
+ host.view = "findings";
8379
+ }}
8380
+ >Findings</button>
8381
+ </div>
8382
+ `;
8383
+ }
8384
+ function renderGitActions(host) {
8385
+ if (!host.backendActive()) return A;
8386
+ const hasDocuments = (host.result?.index.documents.length ?? 0) > 0;
8387
+ return b2`
8388
+ ${renderGitStatus(host)}
8389
+ <button
8390
+ type="button"
8391
+ class="doorstop-git-stage"
8392
+ title="Stage all Doorstop-managed files (requirements, documents, configs)"
8393
+ ?disabled=${host.runInProgress !== void 0 || !hasDocuments}
8394
+ @click=${() => onGitStageClick(host)}
8395
+ >${gitStageIconSvg}Stage all</button>
8396
+ <div class="doorstop-git-commit">
8397
+ <input
8398
+ type="text"
8399
+ class="doorstop-git-commit-input"
8400
+ placeholder="Commit message"
8401
+ aria-label="Git commit message"
8402
+ ?disabled=${host.runInProgress !== void 0}
8403
+ ${n5(host.gitCommitInputRef)}
8404
+ @input=${(event) => onGitCommitInput(host, event)}
8405
+ @keydown=${(event) => onGitCommitKeydown(host, event)}
8406
+ />
8407
+ <button
8408
+ type="button"
8409
+ class="doorstop-git-commit-button"
8410
+ title="Commit the staged index with this message"
8411
+ ?disabled=${host.runInProgress !== void 0 || host.gitCommitMessage.trim() === ""}
8412
+ @click=${() => onGitCommitClick(host)}
8413
+ >${gitCommitIconSvg}Commit</button>
8414
+ ${host.gitActionError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${host.gitActionError}</span>`}
8415
+ </div>
8416
+ `;
8417
+ }
8418
+ function renderGitStatus(host) {
8419
+ const view = host.gitStatusView;
8420
+ if (view === void 0) return A;
8421
+ const refresh = () => {
8422
+ void host.controller?.requestGitStatus();
8423
+ };
8424
+ switch (view.state) {
8425
+ case "loading":
8426
+ return b2`<button
8427
+ type="button"
8428
+ class="doorstop-git-status"
8429
+ title="Refreshing git status…"
8430
+ aria-busy=${host.gitStatusInFlight}
8431
+ @click=${refresh}
8432
+ ><span class="doorstop-git-status-text">⎇ …</span></button>`;
8433
+ case "no-git":
8434
+ return b2`<button type="button" class="doorstop-git-status" title="Not a git repository — click to re-check" aria-busy=${host.gitStatusInFlight} @click=${refresh}><span class="doorstop-git-status-text">no git</span></button>`;
8435
+ case "error":
8436
+ return b2`<button
8437
+ type="button"
8438
+ class="doorstop-git-status doorstop-git-status-error"
8439
+ title=${`Git status unavailable \u2014 ${view.errorMessage ?? "request failed"} \u2014 click to retry`}
8440
+ aria-busy=${host.gitStatusInFlight}
8441
+ @click=${refresh}
8442
+ ><span class="doorstop-git-status-text">git status error — retry</span></button>`;
8443
+ case "ready":
8444
+ return b2`<button type="button" class="doorstop-git-status" title="Git status — click to refresh" aria-busy=${host.gitStatusInFlight} @click=${refresh}><span class="doorstop-git-status-text">${gitStatusText(view.response)}</span></button>`;
8445
+ }
8446
+ }
8447
+ function onRefreshClick(host) {
8448
+ void host.controller?.invalidate();
8449
+ }
8450
+ function onValidateClick(host) {
8451
+ host.runDoorstop("validate", "Doorstop: validate", { op: "validate" }, "doorstop", true);
8452
+ }
8453
+ function onPublishClick(host) {
8454
+ const target = doorstopPublishTarget(host.result);
8455
+ if (typeof window.confirm === "function") {
8456
+ if (!window.confirm(`Publish the Doorstop tree as HTML to ${target} in the workspace terminal?`)) return;
8457
+ host.confirmSkipped = false;
8458
+ } else {
8459
+ host.confirmSkipped = true;
8460
+ }
8461
+ host.runDoorstop(
8462
+ "publish",
8463
+ "Doorstop: publish",
8464
+ { op: "publish", target },
8465
+ doorstopPublishCommand(host.result, target),
8466
+ false
8467
+ );
8468
+ }
8469
+ function onGitStageClick(host) {
8470
+ const controller = host.controller;
8471
+ if (controller === void 0) return;
8472
+ if (controller.runInProgress !== void 0) return;
8473
+ const result = host.result;
8474
+ if (result === void 0) {
8475
+ host.gitActionError = "The workspace is not loaded \u2014 refresh first";
8476
+ return;
8477
+ }
8478
+ const paths = doorstopPaths(result);
8479
+ if (paths.length === 0) {
8480
+ host.gitActionError = "Nothing to stage \u2014 the workspace has no Doorstop-managed files";
8481
+ return;
8482
+ }
8483
+ host.gitActionError = void 0;
8484
+ void controller.runGitStage(paths);
8485
+ }
8486
+ function onGitCommitInput(host, event) {
8487
+ host.gitCommitMessage = event.target.value;
8488
+ }
8489
+ function onGitCommitClick(host) {
8490
+ gitCommitSubmit(host);
8491
+ }
8492
+ function onGitCommitKeydown(host, event) {
8493
+ const input = host.gitCommitInputRef.value;
8494
+ if (input === void 0) return;
8495
+ if (event.key === "Enter") {
8496
+ event.preventDefault();
8497
+ gitCommitSubmit(host);
8498
+ } else if (event.key === "Escape") {
8499
+ input.value = "";
8500
+ host.gitCommitMessage = "";
8501
+ host.gitActionError = void 0;
8502
+ }
8503
+ }
8504
+ function gitCommitSubmit(host) {
8505
+ const controller = host.controller;
8506
+ if (controller === void 0) return;
8507
+ if (controller.runInProgress !== void 0) return;
8508
+ const input = host.gitCommitInputRef.value;
8509
+ const message = input?.value.trim() ?? "";
8510
+ if (message === "") {
8511
+ host.gitActionError = "Enter a commit message";
8512
+ input?.focus();
8513
+ return;
8514
+ }
8515
+ host.gitActionError = void 0;
8516
+ void controller.runGitCommit(message).then(() => {
8517
+ const lastRun = controller.lastRun;
8518
+ if (lastRun?.op === "git-commit" && lastRun.status === "ok" && input !== void 0) {
8519
+ input.value = "";
8520
+ host.gitCommitMessage = "";
8521
+ }
8522
+ });
8523
+ }
7946
8524
 
7947
- // src/doorstop-prompts.ts
8525
+ // node_modules/lit-html/directives/repeat.js
8526
+ var u4 = (e8, s6, t5) => {
8527
+ const r8 = /* @__PURE__ */ new Map();
8528
+ for (let l3 = s6; l3 <= t5; l3++) r8.set(e8[l3], l3);
8529
+ return r8;
8530
+ };
8531
+ var c5 = e4(class extends i6 {
8532
+ constructor(e8) {
8533
+ if (super(e8), e8.type !== t4.CHILD) throw Error("repeat() can only be used in text expressions");
8534
+ }
8535
+ dt(e8, s6, t5) {
8536
+ let r8;
8537
+ void 0 === t5 ? t5 = s6 : void 0 !== s6 && (r8 = s6);
8538
+ const l3 = [], o8 = [];
8539
+ let i8 = 0;
8540
+ for (const s7 of e8) l3[i8] = r8 ? r8(s7, i8) : i8, o8[i8] = t5(s7, i8), i8++;
8541
+ return { values: o8, keys: l3 };
8542
+ }
8543
+ render(e8, s6, t5) {
8544
+ return this.dt(e8, s6, t5).values;
8545
+ }
8546
+ update(s6, [t5, r8, c6]) {
8547
+ const d3 = M2(s6), { values: p4, keys: a3 } = this.dt(t5, r8, c6);
8548
+ if (!Array.isArray(d3)) return this.ut = a3, p4;
8549
+ const h6 = this.ut ??= [], v3 = [];
8550
+ let m3, y3, x2 = 0, j2 = d3.length - 1, k2 = 0, w2 = p4.length - 1;
8551
+ for (; x2 <= j2 && k2 <= w2; ) if (null === d3[x2]) x2++;
8552
+ else if (null === d3[j2]) j2--;
8553
+ else if (h6[x2] === a3[k2]) v3[k2] = u3(d3[x2], p4[k2]), x2++, k2++;
8554
+ else if (h6[j2] === a3[w2]) v3[w2] = u3(d3[j2], p4[w2]), j2--, w2--;
8555
+ else if (h6[x2] === a3[w2]) v3[w2] = u3(d3[x2], p4[w2]), v2(s6, v3[w2 + 1], d3[x2]), x2++, w2--;
8556
+ else if (h6[j2] === a3[k2]) v3[k2] = u3(d3[j2], p4[k2]), v2(s6, d3[x2], d3[j2]), j2--, k2++;
8557
+ else if (void 0 === m3 && (m3 = u4(a3, k2, w2), y3 = u4(h6, x2, j2)), m3.has(h6[x2])) if (m3.has(h6[j2])) {
8558
+ const e8 = y3.get(a3[k2]), t6 = void 0 !== e8 ? d3[e8] : null;
8559
+ if (null === t6) {
8560
+ const e9 = v2(s6, d3[x2]);
8561
+ u3(e9, p4[k2]), v3[k2] = e9;
8562
+ } else v3[k2] = u3(t6, p4[k2]), v2(s6, d3[x2], t6), d3[e8] = null;
8563
+ k2++;
8564
+ } else h3(d3[j2]), j2--;
8565
+ else h3(d3[x2]), x2++;
8566
+ for (; k2 <= w2; ) {
8567
+ const e8 = v2(s6, v3[w2 + 1]);
8568
+ u3(e8, p4[k2]), v3[k2++] = e8;
8569
+ }
8570
+ for (; x2 <= j2; ) {
8571
+ const e8 = d3[x2++];
8572
+ null !== e8 && h3(e8);
8573
+ }
8574
+ return this.ut = a3, p3(s6, v3), E;
8575
+ }
8576
+ });
8577
+
8578
+ // src/browser/doorstop-prompts.ts
7948
8579
  function andJoin(values) {
7949
8580
  if (values.length === 0) return "";
7950
8581
  if (values.length === 1) return values[0] ?? "";
@@ -7975,42 +8606,779 @@ function fixSuspectLinksPrompt(item, parents) {
7975
8606
  `fixSuspectLinksPrompt parents must be links of ${item.uid}: ${andJoin(unlinked.map((parent) => parent.uid))} ${unlinked.length === 1 ? "is" : "are"} not among ${item.uid}'s links`
7976
8607
  );
7977
8608
  }
7978
- const parentItems = andJoin(
7979
- parents.map((parent) => `${parent.uid} (\`${parent.path}\`)`)
8609
+ const parentItems = andJoin(
8610
+ parents.map((parent) => `${parent.uid} (\`${parent.path}\`)`)
8611
+ );
8612
+ if (parents.length === 1) {
8613
+ return `${item.uid} has a suspect link to ${parents[0]?.uid}: the recorded fingerprint no longer matches because that parent changed after ${item.uid} was last reviewed. Read \`${item.path}\` and ${parentItems}, compare the two texts, and propose updated text for ${item.uid} that still traces correctly to the changed parent. Then run \`doorstop clear ${item.uid}\` in the workspace root to re-record the parent's current fingerprint, and run \`doorstop\` to confirm the suspect link is gone.`;
8614
+ }
8615
+ return `${item.uid} has suspect links to ${andJoin(parents.map((parent) => parent.uid))}: the recorded fingerprints no longer match because those parents changed after ${item.uid} was last reviewed. Read \`${item.path}\` and the changed parents (${parentItems}), compare the texts, and propose updated text for ${item.uid} that still traces correctly to every changed parent. Then run \`doorstop clear ${item.uid}\` in the workspace root to re-record the parents' current fingerprints, and run \`doorstop\` to confirm no suspect link remains.`;
8616
+ }
8617
+ function draftChildRequirementPrompt(parent, childPrefix) {
8618
+ if (childPrefix.trim() === "") {
8619
+ throw new TypeError(
8620
+ "draftChildRequirementPrompt requires a non-empty child document prefix: `doorstop add` names its document by prefix"
8621
+ );
8622
+ }
8623
+ return `Draft a new ${childPrefix} requirement that traces to ${parent.uid} (\`${parent.path}\`). Run \`doorstop add ${childPrefix}\` in the workspace root and note the UID it prints for the new item. Link the new item to its parent with \`doorstop link <new-uid> ${parent.uid}\`, substituting the printed UID for <new-uid>. Then fill in the new item's text so it elaborates ${parent.uid} at ${childPrefix} level, referencing any relevant files (prefer \`doorstop edit <new-uid>\` when available, otherwise edit the item file the add command created \u2014 doorstop normalizes formatting on the next command run). Finally run \`doorstop\` in the workspace root and confirm the new item introduces no WARNING or ERROR before judging whether it is ready for \`doorstop review <new-uid>\`.`;
8624
+ }
8625
+ function referencePaths(item) {
8626
+ const paths = item.references?.map((reference) => reference.path) ?? [];
8627
+ return item.ref === "" ? paths : [item.ref, ...paths];
8628
+ }
8629
+ function reviewReadinessPrompt(item, children = []) {
8630
+ const parents = andJoin(item.links.map((link) => link.uid));
8631
+ const references = referencePaths(item);
8632
+ const childLinks = andJoin(
8633
+ children.map((child) => `${child.uid} (\`${child.path}\`)`)
8634
+ );
8635
+ const facts = [
8636
+ item.text.trim() === "" ? "its text is empty (that alone blocks review)" : "its text is filled in",
8637
+ item.links.length === 0 ? "it declares no parent links" : `its recorded parent links (${parents}) are not suspect or missing a fingerprint`,
8638
+ children.length === 0 ? "its child links are left to the `doorstop` run below" : `its child links are declared (${childLinks})`,
8639
+ references.length === 0 ? "it has no file references to verify" : `its file references exist (${references.map((path) => `\`${path}\``).join(", ")})`
8640
+ ];
8641
+ return `Check whether ${item.uid} is ready for \`doorstop review ${item.uid}\`. Read \`${item.path}\` and verify that ${facts.join("; ")}. Then run \`doorstop\` in the workspace root and weigh any WARNING or ERROR it reports about ${item.uid} (for example a requirement that should link to ${item.uid} but does not). Report a verdict, and if ${item.uid} is not ready, the exact fixes to make first.`;
8642
+ }
8643
+ function composeReviewCommand(item) {
8644
+ return `/opendoor-review ${item.uid}
8645
+ Review ${item.uid} (\`${item.path}\`) against its child links and references; run \`doorstop\` validation and, when it is ready, \`doorstop review ${item.uid}\`.`;
8646
+ }
8647
+ function setupProjectPrompt() {
8648
+ return `Set up a Doorstop requirements project in this workspace. Use the \`ask_user\` tool to ask the user two questions before creating anything: what the first requirements document should be called (default: REQ) and whether item text should be shown in the opendoor panel (default: no). Run \`git init\` if the workspace is not a git repository. Then run \`doorstop create <chosen UID prefix> ./<prefix-derived folder: REQ to reqs, URD to urds>\` in the workspace root and \`doorstop\` to confirm it validates. If \`.pi-web/opendoor.json\` does not exist, create it with {"version": 1} plus "showAdditionalAttribute": ["text"] when the user opted into showing text; leave publish.target, discovery.excludedDirectories, and commitAfterReview at their defaults. Commit the created \`.doorstop.yml\`, requirements tree, and settings file.`;
8649
+ }
8650
+
8651
+ // node_modules/lit-html/directives/keyed.js
8652
+ var i7 = e4(class extends i6 {
8653
+ constructor() {
8654
+ super(...arguments), this.key = A;
8655
+ }
8656
+ render(r8, t5) {
8657
+ return this.key = r8, t5;
8658
+ }
8659
+ update(r8, [t5, e8]) {
8660
+ return t5 !== this.key && (p3(r8), this.key = t5), e8;
8661
+ }
8662
+ });
8663
+
8664
+ // src/browser/sections/detail-pane.ts
8665
+ function renderDetail(host, result) {
8666
+ const selectedUid = host.selectedUid;
8667
+ if (selectedUid === void 0) {
8668
+ return b2`<p class="doorstop-muted doorstop-standalone">Select an item in the list to inspect its details.</p>`;
8669
+ }
8670
+ const item = result.index.byUid.get(selectedUid);
8671
+ if (item === void 0) {
8672
+ return b2`<p class="doorstop-muted doorstop-standalone">This item is no longer available — refresh the panel.</p>`;
8673
+ }
8674
+ const children = result.index.childrenByUid.get(item.uid) ?? [];
8675
+ const findings = result.index.findings.filter((finding) => finding.uid === item.uid);
8676
+ return b2`
8677
+ <section class="doorstop-detail" aria-label="Item details">
8678
+ <div class="doorstop-detail-head">
8679
+ <code class="doorstop-detail-uid">${item.uid}</code>
8680
+ <span class="doorstop-level">level ${item.level}</span>
8681
+ ${item.header === void 0 ? A : b2`<h3>${item.header}</h3>`}
8682
+ </div>
8683
+ ${renderFlags(host, item)}
8684
+ <div class="doorstop-state-chip-row">
8685
+ ${item.stateKeys.map((key) => host.renderStateChip(key))}
8686
+ </div>
8687
+ <h4 class="doorstop-section-title">Text</h4>
8688
+ <p class=${item.text === "" ? "doorstop-text doorstop-text-empty" : "doorstop-text"}>${item.text === "" ? "empty" : item.text}</p>
8689
+ ${renderChangesSinceReview(host, item)}
8690
+ <h4 class="doorstop-section-title">Parent links</h4>
8691
+ ${renderLinksOut(host, item, result.index)}
8692
+ <h4 class="doorstop-section-title">Child links</h4>
8693
+ ${renderLinksIn(host, item, result.index, children)}
8694
+ <h4 class="doorstop-section-title">References</h4>
8695
+ ${renderReferences(host, item, result.index)}
8696
+ ${Object.entries(item.attributes).length === 0 ? A : b2`
8697
+ <h4 class="doorstop-section-title">Extended attributes</h4>
8698
+ <dl class="doorstop-attributes">
8699
+ ${Object.entries(item.attributes).map(
8700
+ ([key, value]) => b2`<div class="doorstop-attribute"><dt>${key}</dt><dd><code>${jsonishText(value)}</code></dd></div>`
8701
+ )}
8702
+ </dl>
8703
+ `}
8704
+ <h4 class="doorstop-section-title">Findings</h4>
8705
+ ${findings.length === 0 ? b2`<p class="doorstop-muted">No local findings for this item.</p>` : b2`<div class="doorstop-findings" aria-label="Findings for this item">
8706
+ ${findings.map((finding) => {
8707
+ const kind = finding.severity === "error" ? "doorstop-error" : finding.severity === "warning" ? "doorstop-warning" : "doorstop-info";
8708
+ return b2`<div class=${`doorstop-finding ${kind}`}><span class="doorstop-finding-severity">${finding.severity}</span><span>${finding.message}</span></div>`;
8709
+ })}
8710
+ </div>`}
8711
+ </section>
8712
+ `;
8713
+ }
8714
+ function renderFlags(host, item) {
8715
+ return b2`
8716
+ <div class="doorstop-flags" aria-label="Item flags">
8717
+ <span class=${e7({ "doorstop-flag": true, "is-inactive": !item.active })}>
8718
+ ${item.active ? "active" : "inactive"}
8719
+ </span>
8720
+ <span class="doorstop-flag">${item.normative ? "normative" : "non-normative"}</span>
8721
+ <span class="doorstop-flag">${item.derived ? "derived" : "non-derived"}</span>
8722
+ </div>
8723
+ `;
8724
+ }
8725
+ function renderLinksOut(host, item, index) {
8726
+ if (item.links.length === 0) return b2`<p class="doorstop-muted">No parent links.</p>`;
8727
+ return b2`
8728
+ <div class="doorstop-links" aria-label="Parent links">
8729
+ ${item.links.map((link) => renderLinkOut(host, item, link, index))}
8730
+ </div>
8731
+ `;
8732
+ }
8733
+ function renderLinkOut(host, item, link, index) {
8734
+ const remove = b2`
8735
+ <span
8736
+ class="doorstop-link-remove"
8737
+ aria-hidden="true"
8738
+ title=${`doorstop unlink ${item.uid} ${link.uid}`}
8739
+ @click=${(event) => {
8740
+ event.stopPropagation();
8741
+ unlinkLink(host, item, link.uid);
8742
+ }}
8743
+ >${trashIconSvg}</span>
8744
+ `;
8745
+ const target = index.byUid.get(link.uid);
8746
+ if (target === void 0) {
8747
+ return b2`
8748
+ <div class="doorstop-link-row">
8749
+ <code>${link.uid}</code>
8750
+ <span class="doorstop-chip doorstop-chip-danger">unknown</span>
8751
+ <span class="doorstop-fingerprint">not in the index — ${link.fingerprint === null ? "no recorded fingerprint" : `recorded ${shortFingerprint(link.fingerprint)}`}</span>
8752
+ ${remove}
8753
+ </div>
8754
+ `;
8755
+ }
8756
+ const current = computeItemStamp(target, documentConfigFor(index, target), false);
8757
+ const suspect = link.fingerprint !== null && link.fingerprint !== current;
8758
+ return b2`
8759
+ <button
8760
+ type="button"
8761
+ class="doorstop-link-row"
8762
+ data-uid=${link.uid}
8763
+ title=${`Open ${link.uid}`}
8764
+ @click=${() => {
8765
+ host.controller?.selectUid(link.uid);
8766
+ }}
8767
+ >
8768
+ <code>${link.uid}</code>
8769
+ <span class=${suspect ? "doorstop-chip doorstop-chip-danger" : "doorstop-chip doorstop-chip-ok"}>${suspect ? "suspect" : "ok"}</span>
8770
+ <span class="doorstop-fingerprint">recorded ${shortFingerprint(link.fingerprint)} · current ${shortFingerprint(current)}</span>
8771
+ ${remove}
8772
+ </button>
8773
+ `;
8774
+ }
8775
+ function renderLinksIn(host, item, index, children) {
8776
+ if (children.length === 0) {
8777
+ return b2`<p class="doorstop-muted">No child links.</p>`;
8778
+ }
8779
+ return b2`
8780
+ <div class="doorstop-links" aria-label="Child links">
8781
+ ${children.map(
8782
+ (child) => b2`<button type="button" class="doorstop-link-row" data-uid=${child.uid} title=${`Open ${child.uid}`} @click=${() => {
8783
+ host.controller?.selectUid(child.uid);
8784
+ }}>
8785
+ <code>${child.uid}</code>
8786
+ <span class="doorstop-muted">${child.level}</span>
8787
+ </button>`
8788
+ )}
8789
+ </div>
8790
+ `;
8791
+ }
8792
+ function renderReferences(host, item, index) {
8793
+ const paths = [];
8794
+ if (item.ref !== "") paths.push(item.ref);
8795
+ if (item.references !== void 0) {
8796
+ for (const reference of item.references) paths.push(reference.path);
8797
+ }
8798
+ if (paths.length === 0) return b2`<p class="doorstop-muted">No file references.</p>`;
8799
+ return b2`
8800
+ <div class="doorstop-links" aria-label="File references">
8801
+ ${paths.map((path) => {
8802
+ const found = index.knownFilePaths.has(path);
8803
+ return b2`
8804
+ <div class="doorstop-link-row">
8805
+ <code>${path}</code>
8806
+ ${found ? A : b2`<span class="doorstop-chip doorstop-chip-danger">not found</span>`}
8807
+ </div>
8808
+ `;
8809
+ })}
8810
+ </div>
8811
+ `;
8812
+ }
8813
+ function renderChangesSinceReview(host, item) {
8814
+ if (!item.stateKeys.includes("unreviewed") || item.reviewed === null) return A;
8815
+ if (!host.backendActive()) return A;
8816
+ const view = host.controller?.baselineViewFor(item);
8817
+ return i7(
8818
+ item.uid,
8819
+ b2`
8820
+ <details class="doorstop-changes" @toggle=${(event) => onChangesToggle(host, event)}>
8821
+ <summary class="doorstop-changes-summary">Changes since review</summary>
8822
+ ${view === void 0 || view.state === "loading" ? b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>` : renderBaselineView(host, view)}
8823
+ </details>
8824
+ `
8825
+ );
8826
+ }
8827
+ function renderBaselineView(host, view) {
8828
+ switch (view.state) {
8829
+ case "loading":
8830
+ return b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>`;
8831
+ case "no-git":
8832
+ return b2`<p class="doorstop-muted doorstop-changes-notice">No git history — previous version unavailable</p>`;
8833
+ case "no-match":
8834
+ return b2`<p class="doorstop-muted doorstop-changes-notice">Could not locate the reviewed version (history may have been rewritten)</p>`;
8835
+ case "error":
8836
+ return b2`<p class="doorstop-muted doorstop-changes-notice">Baseline unavailable — ${view.errorMessage ?? "request failed"}</p>`;
8837
+ case "ready": {
8838
+ const diff = view.diff;
8839
+ if (diff === void 0) return b2`<p class="doorstop-muted doorstop-changes-notice">No changes found.</p>`;
8840
+ return renderBaselineDiff(host, view, diff);
8841
+ }
8842
+ }
8843
+ }
8844
+ function renderBaselineDiff(host, view, diff) {
8845
+ return b2`
8846
+ ${view.source === void 0 ? A : b2`<p class="doorstop-muted doorstop-changes-source">matched via ${view.source === "review-commit" ? "review commit" : "history walk"}</p>`}
8847
+ ${diff.text === void 0 ? b2`<p class="doorstop-muted doorstop-changes-notice">Text changed — too large to render a line diff.</p>` : b2`<div class="doorstop-diff-lines" role="list" aria-label="Text changes since review">
8848
+ ${diff.text.map((line) => renderDiffLine(host, line))}
8849
+ </div>`}
8850
+ ${diff.ref === void 0 ? A : renderFieldChange(host, "ref", diff.ref.before, diff.ref.after)}
8851
+ ${diff.references === void 0 ? A : renderFieldChange(
8852
+ host,
8853
+ "references",
8854
+ referenceListText(diff.references.before),
8855
+ referenceListText(diff.references.after)
8856
+ )}
8857
+ ${diff.linksAdded.length === 0 && diff.linksRemoved.length === 0 ? A : b2`<div class="doorstop-field-change">
8858
+ <span class="doorstop-field-name">links</span>
8859
+ ${diff.linksAdded.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-after">+ ${uid}</code>`)}
8860
+ ${diff.linksRemoved.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-before">− ${uid}</code>`)}
8861
+ </div>`}
8862
+ ${diff.extended.map((change) => renderFieldChange(host, change.name, change.before, change.after))}
8863
+ `;
8864
+ }
8865
+ function renderDiffLine(host, line) {
8866
+ const mark = line.kind === "added" ? "+" : line.kind === "removed" ? "\u2212" : "";
8867
+ return b2`
8868
+ <div class=${`doorstop-diff-line is-${line.kind}`} role="listitem">
8869
+ <span class="doorstop-diff-mark">${mark}</span>
8870
+ <span class="doorstop-diff-text">${line.text === "" ? "\xA0" : line.text}</span>
8871
+ </div>
8872
+ `;
8873
+ }
8874
+ function renderFieldChange(host, label, before, after) {
8875
+ return b2`
8876
+ <div class="doorstop-field-change">
8877
+ <span class="doorstop-field-name">${label}</span>
8878
+ <code class="doorstop-field-chip doorstop-field-chip-before">${diffValueText(before)}</code>
8879
+ <span class="doorstop-field-arrow">→</span>
8880
+ <code class="doorstop-field-chip doorstop-field-chip-after">${diffValueText(after)}</code>
8881
+ </div>
8882
+ `;
8883
+ }
8884
+ function onChangesToggle(host, event) {
8885
+ const details = event.currentTarget;
8886
+ if (!(details instanceof HTMLDetailsElement) || !details.open) return;
8887
+ const item = host.selectedItem();
8888
+ if (item === void 0) return;
8889
+ void host.controller?.requestBaseline(item);
8890
+ }
8891
+ function unlinkLink(host, item, target) {
8892
+ const controller = host.controller;
8893
+ if (controller === void 0 || controller.runInProgress !== void 0) return;
8894
+ host.runDoorstop(
8895
+ "unlink",
8896
+ `Doorstop: unlink ${item.uid}`,
8897
+ { op: "unlink", uid: item.uid, target },
8898
+ `doorstop unlink ${item.uid} ${target}`,
8899
+ false
8900
+ );
8901
+ }
8902
+
8903
+ // src/browser/sections/item-list.ts
8904
+ var DEFAULT_STATE_KEY = "normative";
8905
+ function renderListFilters(host, result) {
8906
+ return b2`
8907
+ <section class="doorstop-list-filters">
8908
+ <div class="doorstop-docs" role="list" aria-label="Doorstop documents">
8909
+ ${renderDocumentChip(host, void 0, result)}
8910
+ ${result.index.documents.map((document2) => renderDocumentChip(host, document2, result))}
8911
+ </div>
8912
+ <select class="doorstop-state-filter" aria-label="Filter by state" @change=${(event) => onStateFilterChange(host, event)}>
8913
+ <option value="" .selected=${host.stateFilter === void 0}>All states</option>
8914
+ ${Object.entries(STATE_CHIP_LABELS).map(
8915
+ ([key, label]) => b2`<option value=${key} .selected=${host.stateFilter === key}>${label}</option>`
8916
+ )}
8917
+ </select>
8918
+ <input
8919
+ class="doorstop-search"
8920
+ type="search"
8921
+ aria-label="Search items"
8922
+ placeholder="Search UID or text"
8923
+ .value=${host.search}
8924
+ @input=${(event) => onSearchInput(host, event)}
8925
+ />
8926
+ </section>
8927
+ `;
8928
+ }
8929
+ function renderDocumentChip(host, document2, result) {
8930
+ const prefix = document2?.prefix ?? ALL_DOCUMENTS_LABEL;
8931
+ const count = document2 === void 0 ? result.index.items.length : result.index.items.filter((item) => item.documentPrefix === document2.prefix).length;
8932
+ const dots = document2 === void 0 ? [] : documentStateDots(document2, result.index.items);
8933
+ const selected = document2 === void 0 ? host.selectedDocumentPrefix === void 0 || host.selectedDocumentPrefix === "" : host.selectedDocumentPrefix === document2.prefix;
8934
+ return b2`
8935
+ <button
8936
+ type="button"
8937
+ role="listitem"
8938
+ class=${e7({ "doorstop-doc-chip": true, "is-selected": selected })}
8939
+ data-prefix=${document2?.prefix ?? ""}
8940
+ @click=${() => {
8941
+ host.controller?.selectDocument(document2?.prefix ?? "");
8942
+ }}
8943
+ >
8944
+ ${document2?.parentPrefix === void 0 ? A : b2`<span class="doorstop-doc-arrow">← ${document2.parentPrefix}</span>`}
8945
+ <span class="doorstop-doc-prefix">${prefix}</span>
8946
+ <span class="doorstop-doc-count">${String(count)}</span>
8947
+ ${dots.map((dot) => b2`<span class=${`doorstop-dot doorstop-dot-${dot}`} title=${dotTitle(dot)}></span>`)}
8948
+ </button>
8949
+ `;
8950
+ }
8951
+ function renderViewer(host) {
8952
+ const result = host.result;
8953
+ if (result === void 0) {
8954
+ return b2`<p class="doorstop-muted doorstop-standalone">${host.loading ? "Loading workspace\u2026" : "Run Refresh to scan for Doorstop documents."}</p>`;
8955
+ }
8956
+ if (host.view === "findings") {
8957
+ return renderFindingsView(host, result);
8958
+ }
8959
+ return b2`
8960
+ ${renderListFilters(host, result)}
8961
+ ${result.index.documents.length === 0 ? b2`
8962
+ ${renderDiagnostics(host, result)}
8963
+ <section class="doorstop-empty">
8964
+ <p>${EMPTY_WORKSPACE_MESSAGE}</p>
8965
+ ${host.context === void 0 ? A : b2`
8966
+ <button
8967
+ type="button"
8968
+ class="doorstop-setup"
8969
+ @click=${() => {
8970
+ host.insertPrompt(setupProjectPrompt());
8971
+ }}
8972
+ >Ask agent to set up the project</button>
8973
+ `}
8974
+ </section>
8975
+ ` : b2`
8976
+ <section class="doorstop-split">
8977
+ <section class="doorstop-list">
8978
+ ${renderDiagnostics(host, result)}
8979
+ ${renderItemList(host, result)}
8980
+ </section>
8981
+ <section class="doorstop-detail-pane">${renderDetail(host, result)}</section>
8982
+ </section>
8983
+ `}
8984
+ `;
8985
+ }
8986
+ function renderItemList(host, result) {
8987
+ if (result.index.items.length === 0) {
8988
+ return b2`<p class="doorstop-muted doorstop-standalone">No Doorstop items found (item files may be binary or truncated — see the diagnostics above).</p>`;
8989
+ }
8990
+ const items = filteredItems(result.index, host.selectedDocumentPrefix, host.stateFilter, host.search);
8991
+ if (items.length === 0) {
8992
+ return b2`<p class="doorstop-muted doorstop-standalone">No items match the current document, state, or search filters.</p>`;
8993
+ }
8994
+ const gitFiles = host.readyGitFiles();
8995
+ const attributeKeys = doorstopShowAdditionalAttribute(result);
8996
+ return b2`
8997
+ <div
8998
+ class=${e7({ "doorstop-items": true, "has-item-attrs": attributeKeys.length > 0 })}
8999
+ role="list"
9000
+ aria-label="Doorstop items"
9001
+ >
9002
+ ${c5(items, (item) => item.uid, (item) => renderItemRow(host, item, gitFiles, attributeKeys))}
9003
+ </div>
9004
+ `;
9005
+ }
9006
+ function renderItemRow(host, item, gitFiles, attributeKeys) {
9007
+ const selected = host.selectedUid === item.uid;
9008
+ const gitState = itemGitState(gitFiles?.get(item.path));
9009
+ const shownAttributes = shownAttributeRows(item, attributeKeys);
9010
+ const textBody = attributeKeys.includes("text") ? itemTextRow(item) : void 0;
9011
+ const stageable = itemStageable(gitFiles?.get(item.path));
9012
+ const unstageable = itemUnstageable(gitFiles?.get(item.path));
9013
+ return b2`
9014
+ <button
9015
+ type="button"
9016
+ role="listitem"
9017
+ class=${e7({ "doorstop-item-row": true, "is-selected": selected })}
9018
+ data-uid=${item.uid}
9019
+ @click=${() => {
9020
+ host.controller?.selectUid(item.uid);
9021
+ }}
9022
+ >
9023
+ <span class="doorstop-item-level">${item.level}</span>
9024
+ <code class="doorstop-item-uid">${item.uid}</code>
9025
+ <span class="doorstop-item-summary">${itemExcerpt(item)}</span>
9026
+ ${shownAttributes.length === 0 ? A : b2`<span class="doorstop-item-attrs">${shownAttributes.map((row) => `${row.key}: ${row.text}`).join(" \xB7 ")}</span>`}
9027
+ <span class="doorstop-item-chips">
9028
+ ${item.stateKeys.filter((key) => key !== DEFAULT_STATE_KEY).map((key) => renderStateChip(key))}
9029
+ ${gitState === "clean" ? A : b2`<span class=${`doorstop-chip doorstop-chip-${gitChipKind(gitState)}`}>${GIT_CHIP_LABELS[gitState]}</span>`}
9030
+ ${unstageable && host.runInProgress === void 0 ? b2`<span
9031
+ class="doorstop-item-add"
9032
+ aria-hidden="true"
9033
+ title=${`git reset ${item.path}`}
9034
+ @click=${(event) => {
9035
+ event.stopPropagation();
9036
+ host.unstageItem(item);
9037
+ }}
9038
+ >${gitUnstageIconSvg}</span>` : stageable && host.runInProgress === void 0 ? b2`<span
9039
+ class="doorstop-item-add"
9040
+ aria-hidden="true"
9041
+ title=${`git add ${item.path}`}
9042
+ @click=${(event) => {
9043
+ event.stopPropagation();
9044
+ host.stageItem(item);
9045
+ }}
9046
+ >${gitStageIconSvg}</span>` : A}
9047
+ ${host.context === void 0 ? A : b2`<span
9048
+ class="doorstop-item-insert"
9049
+ aria-hidden="true"
9050
+ title=${`Insert ${item.uid} into the prompt`}
9051
+ @click=${(event) => {
9052
+ event.stopPropagation();
9053
+ host.insertPrompt(item.uid);
9054
+ }}
9055
+ >${insertPromptIconSvg}</span>`}
9056
+ </span>
9057
+ ${textBody === void 0 ? A : b2`<span class="doorstop-item-text">${textBody}</span>`}
9058
+ </button>
9059
+ `;
9060
+ }
9061
+ function renderStateChip(key) {
9062
+ const kind = stateChipKind(key);
9063
+ return b2`<span class=${`doorstop-chip doorstop-chip-${kind}`}>${STATE_CHIP_LABELS[key]}</span>`;
9064
+ }
9065
+ function renderDiagnostics(host, result) {
9066
+ if (result.index.diagnostics.length === 0) return A;
9067
+ return b2`
9068
+ <section class="doorstop-diagnostics" aria-label="Workspace diagnostics">
9069
+ ${result.index.diagnostics.map((diagnostic) => {
9070
+ const kind = diagnostic.severity === "error" ? "doorstop-error" : "doorstop-warning";
9071
+ return b2`
9072
+ <div class=${`doorstop-diagnostic ${kind}`}>
9073
+ <span class="doorstop-severity">${diagnostic.severity}</span>
9074
+ <span class="doorstop-diagnostic-copy">
9075
+ ${diagnostic.path === void 0 ? A : b2`<code>${diagnostic.path}</code>`}
9076
+ ${diagnostic.message}
9077
+ </span>
9078
+ </div>
9079
+ `;
9080
+ })}
9081
+ </section>
9082
+ `;
9083
+ }
9084
+ function renderFindingsView(host, result) {
9085
+ const rows = findingsViewRows(result.index);
9086
+ const counts = findingsViewCounts(rows);
9087
+ return b2`
9088
+ <section class="doorstop-findings-view" aria-label="Doorstop findings">
9089
+ <header class="doorstop-findings-head">
9090
+ <span class="doorstop-findings-counts" role="status" aria-label="Finding counts">${findingsCountText(counts)}</span>
9091
+ <span class="doorstop-findings-note doorstop-muted">${FINDINGS_PLUGIN_LOCAL_NOTE}</span>
9092
+ </header>
9093
+ ${rows.length === 0 ? b2`
9094
+ <section class="doorstop-empty">
9095
+ <p>${FINDINGS_EMPTY_MESSAGE}</p>
9096
+ <p class="doorstop-muted">${FINDINGS_EMPTY_HINT}</p>
9097
+ </section>
9098
+ ` : b2`<div class="doorstop-findings-list" role="list" aria-label="Validation findings">
9099
+ ${rows.map((row) => renderFindingRow(host, row, result.index))}
9100
+ </div>`}
9101
+ </section>
9102
+ `;
9103
+ }
9104
+ function renderFindingRow(host, row, index) {
9105
+ const kind = row.severity === "error" ? "doorstop-error" : row.severity === "warning" ? "doorstop-warning" : "doorstop-info";
9106
+ const uid = row.uid;
9107
+ const navigable = uid !== void 0 && index.byUid.has(uid);
9108
+ return b2`
9109
+ <div class=${`doorstop-finding-row ${kind}`} role="listitem">
9110
+ <span class="doorstop-severity">${row.severity}</span>
9111
+ ${uid === void 0 ? A : navigable ? b2`<button type="button" class="doorstop-finding-uid" data-uid=${uid} title=${`Show ${uid} in the item list`} @click=${() => {
9112
+ selectFindingTarget(host, uid);
9113
+ }}><code>${uid}</code></button>` : b2`<code class="doorstop-finding-uid">${uid}</code>`}
9114
+ ${row.path === void 0 ? A : b2`<code class="doorstop-finding-path">${row.path}</code>`}
9115
+ <span class="doorstop-finding-message">${row.message}</span>
9116
+ </div>
9117
+ `;
9118
+ }
9119
+ function selectFindingTarget(host, uid) {
9120
+ const controller = host.controller;
9121
+ if (controller === void 0) return;
9122
+ controller.selectDocument("");
9123
+ controller.setStateFilter(void 0);
9124
+ controller.setSearch("");
9125
+ controller.selectUid(uid);
9126
+ host.view = "items";
9127
+ }
9128
+ function onStateFilterChange(host, event) {
9129
+ const value = event.target.value;
9130
+ host.controller?.setStateFilter(value === "" ? void 0 : value);
9131
+ }
9132
+ function onSearchInput(host, event) {
9133
+ host.controller?.setSearch(event.target.value);
9134
+ }
9135
+
9136
+ // src/browser/sections/item-action-palette.ts
9137
+ function renderItemActionPalette(host) {
9138
+ if (host.view !== "items") return A;
9139
+ const result = host.result;
9140
+ if (result === void 0 || result.index.documents.length === 0) return A;
9141
+ const item = host.selectedItem();
9142
+ if (item === void 0) {
9143
+ return b2`
9144
+ <section class="doorstop-action-palette" aria-label="Item actions">
9145
+ <span class="doorstop-muted doorstop-palette-placeholder">Select an item…</span>
9146
+ </section>
9147
+ `;
9148
+ }
9149
+ const suspects = suspectParentItems(item, result.index);
9150
+ return b2`
9151
+ <section class="doorstop-action-palette" aria-label="Item actions">
9152
+ ${renderActionRow(host, item, result.index, suspects)}
9153
+ </section>
9154
+ `;
9155
+ }
9156
+ function renderActionRow(host, item, index, suspects) {
9157
+ const reviewed = item.stateKeys.includes("reviewed");
9158
+ const suspectUids = suspects.length > 0 ? suspects.map((parent) => parent.uid) : [];
9159
+ const gitStatus = host.readyGitStatus();
9160
+ const gitFile = gitStatus === void 0 ? void 0 : gitStatusFileFor(gitStatus, item.path);
9161
+ const stageable = itemStageable(gitFile);
9162
+ const unstageable = itemUnstageable(gitFile);
9163
+ return b2`
9164
+ <button
9165
+ type="button"
9166
+ class="doorstop-review"
9167
+ ?disabled=${reviewed || host.runInProgress !== void 0}
9168
+ title=${reviewed ? `${item.uid} is already reviewed against its current fingerprint` : `Mark ${item.uid} reviewed`}
9169
+ @click=${() => {
9170
+ reviewItem(host, item);
9171
+ }}
9172
+ >Approve</button>
9173
+ <button
9174
+ type="button"
9175
+ class="doorstop-clear"
9176
+ ?disabled=${suspects.length === 0 || host.runInProgress !== void 0}
9177
+ title=${suspects.length === 0 ? `No suspect links to clear` : `Re-record the parent fingerprints of ${item.uid}`}
9178
+ @click=${() => {
9179
+ clearSuspects(host, item, suspectUids);
9180
+ }}
9181
+ >Clear suspect links</button>
9182
+ ${gitStatus === void 0 ? A : b2`<button
9183
+ type="button"
9184
+ class="doorstop-item-stage"
9185
+ ?disabled=${host.runInProgress !== void 0 || !stageable}
9186
+ title=${stageable ? `Stage ${item.path}` : `${item.uid} has no unstaged changes \u2014 commit next`}
9187
+ @click=${() => {
9188
+ host.stageItem(item);
9189
+ }}
9190
+ >${gitStageIconSvg}Stage</button>
9191
+ <button
9192
+ type="button"
9193
+ class="doorstop-item-unstage"
9194
+ ?disabled=${host.runInProgress !== void 0 || !unstageable}
9195
+ title=${unstageable ? `Unstage ${item.path} (git reset)` : `${item.uid} has nothing staged`}
9196
+ @click=${() => {
9197
+ host.unstageItem(item);
9198
+ }}
9199
+ >${gitUnstageIconSvg}Unstage</button>`}
9200
+ <div class="doorstop-op">
9201
+ <input
9202
+ type="text"
9203
+ class="doorstop-target-input"
9204
+ data-op="link"
9205
+ placeholder="parent UID"
9206
+ ${n5(host.linkInputRef)}
9207
+ @keydown=${(event) => onTargetKeydown(host, event)}
9208
+ />
9209
+ <button type="button" class="doorstop-link" title=${`doorstop link ${item.uid} <target>`} ?disabled=${host.runInProgress !== void 0} @click=${() => {
9210
+ runTargetOp(host, "link", host.linkInputRef, item);
9211
+ }}>Link</button>
9212
+ </div>
9213
+ ${host.targetError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${host.targetError}</span>`}
9214
+ ${renderAskMenu(host, item, index, suspects)}
9215
+ `;
9216
+ }
9217
+ function renderAskMenu(host, item, index, suspects) {
9218
+ const children = index.childrenByUid.get(item.uid) ?? [];
9219
+ const childPrefix = firstChildDocumentPrefix(index, item);
9220
+ return b2`
9221
+ <div class="doorstop-menu">
9222
+ <button
9223
+ type="button"
9224
+ class="doorstop-menu-toggle"
9225
+ aria-expanded=${host.askMenuOpen ? "true" : "false"}
9226
+ @click=${() => onAskMenuToggle(host)}
9227
+ >Ask agent</button>
9228
+ ${host.askMenuOpen ? b2`
9229
+ <div class="doorstop-menu-items" role="menu" aria-label="Ask the agent about ${item.uid}">
9230
+ <button type="button" role="menuitem" class="doorstop-menu-item doorstop-explain" @click=${() => {
9231
+ host.insertPrompt(explainItemPrompt(item));
9232
+ }}>Explain</button>
9233
+ <button
9234
+ type="button"
9235
+ role="menuitem"
9236
+ class="doorstop-menu-item doorstop-fix-suspects"
9237
+ ?disabled=${suspects.length === 0}
9238
+ title=${suspects.length === 0 ? "No suspect links \u2014 nothing to fix" : `Fix the suspect links of ${item.uid}`}
9239
+ @click=${() => {
9240
+ host.insertPrompt(fixSuspectLinksPrompt(item, suspects));
9241
+ }}
9242
+ >Fix suspect links</button>
9243
+ <button
9244
+ type="button"
9245
+ role="menuitem"
9246
+ class="doorstop-menu-item doorstop-draft-child"
9247
+ ?disabled=${childPrefix === void 0}
9248
+ title=${childPrefix === void 0 ? "No child document to add to" : `Draft a child item in the ${childPrefix} document`}
9249
+ @click=${() => {
9250
+ if (childPrefix !== void 0) host.insertPrompt(draftChildRequirementPrompt(item, childPrefix));
9251
+ }}
9252
+ >Draft child requirement</button>
9253
+ <button type="button" role="menuitem" class="doorstop-menu-item doorstop-review-readiness" @click=${() => {
9254
+ host.insertPrompt(reviewReadinessPrompt(item, children));
9255
+ }}>Review readiness</button>
9256
+ <button
9257
+ type="button"
9258
+ role="menuitem"
9259
+ class="doorstop-menu-item doorstop-start-review-session"
9260
+ title=${`Start an independent review session for ${item.uid} (inserts /opendoor-review)`}
9261
+ @click=${() => {
9262
+ insertReviewCommand(host, item);
9263
+ }}
9264
+ >Start review session</button>
9265
+ </div>
9266
+ ` : A}
9267
+ </div>
9268
+ `;
9269
+ }
9270
+ function insertReviewCommand(host, item) {
9271
+ if (host.context === void 0) return;
9272
+ host.insertPrompt(composeReviewCommand(item));
9273
+ host.controller?.commitAskReview(item.uid);
9274
+ }
9275
+ function reviewItem(host, item) {
9276
+ const commit = doorstopCommitAfterReview(host.result);
9277
+ host.runDoorstop(
9278
+ "review",
9279
+ `Doorstop: review ${item.uid}`,
9280
+ commit ? { op: "review", uid: item.uid, commit: true } : { op: "review", uid: item.uid },
9281
+ `doorstop review ${item.uid}`,
9282
+ false
9283
+ );
9284
+ }
9285
+ function clearSuspects(host, item, suspectUids) {
9286
+ if (suspectUids.length === 0) return;
9287
+ host.runDoorstop(
9288
+ "clear",
9289
+ "Doorstop: clear suspect links",
9290
+ { op: "clear", uid: item.uid, parents: suspectUids },
9291
+ `doorstop clear ${item.uid} ${suspectUids.join(" ")}`,
9292
+ false
9293
+ );
9294
+ }
9295
+ function runTargetOp(host, op, inputRef, item) {
9296
+ const input = inputRef.value;
9297
+ const target = input?.value.trim() ?? "";
9298
+ if (target === "") {
9299
+ host.targetError = `Enter a link target UID (e.g. ${item.documentPrefix}0001)`;
9300
+ input?.focus();
9301
+ return;
9302
+ }
9303
+ if (!isValidTargetUid(target)) {
9304
+ host.targetError = `Invalid link target "${target}" \u2014 expected a Doorstop UID like ${item.documentPrefix}0001 (no spaces, quotes, or special characters)`;
9305
+ input?.focus();
9306
+ return;
9307
+ }
9308
+ host.targetError = void 0;
9309
+ if (input !== void 0) input.value = "";
9310
+ host.runDoorstop(
9311
+ op,
9312
+ `Doorstop: ${op} ${item.uid}`,
9313
+ { op, uid: item.uid, target },
9314
+ `doorstop ${op} ${item.uid} ${target}`,
9315
+ false
7980
9316
  );
7981
- if (parents.length === 1) {
7982
- return `${item.uid} has a suspect link to ${parents[0]?.uid}: the recorded fingerprint no longer matches because that parent changed after ${item.uid} was last reviewed. Read \`${item.path}\` and ${parentItems}, compare the two texts, and propose updated text for ${item.uid} that still traces correctly to the changed parent. Then run \`doorstop clear ${item.uid}\` in the workspace root to re-record the parent's current fingerprint, and run \`doorstop\` to confirm the suspect link is gone.`;
9317
+ }
9318
+ function onTargetKeydown(host, event) {
9319
+ const input = event.target;
9320
+ if (input.getAttribute("data-op") !== "link") return;
9321
+ if (event.key === "Enter") {
9322
+ event.preventDefault();
9323
+ const item = host.selectedItem();
9324
+ if (item !== void 0) runTargetOp(host, "link", host.linkInputRef, item);
9325
+ } else if (event.key === "Escape") {
9326
+ input.value = "";
7983
9327
  }
7984
- return `${item.uid} has suspect links to ${andJoin(parents.map((parent) => parent.uid))}: the recorded fingerprints no longer match because those parents changed after ${item.uid} was last reviewed. Read \`${item.path}\` and the changed parents (${parentItems}), compare the texts, and propose updated text for ${item.uid} that still traces correctly to every changed parent. Then run \`doorstop clear ${item.uid}\` in the workspace root to re-record the parents' current fingerprints, and run \`doorstop\` to confirm no suspect link remains.`;
7985
9328
  }
7986
- function draftChildRequirementPrompt(parent, childPrefix) {
7987
- if (childPrefix.trim() === "") {
7988
- throw new TypeError(
7989
- "draftChildRequirementPrompt requires a non-empty child document prefix: `doorstop add` names its document by prefix"
7990
- );
9329
+ function onAskMenuToggle(host) {
9330
+ host.askMenuOpen = !host.askMenuOpen;
9331
+ }
9332
+
9333
+ // src/browser/sections/status-bar.ts
9334
+ function renderStatusBar(host) {
9335
+ const lastRun = host.lastRun;
9336
+ if (lastRun === void 0) return A;
9337
+ const statusLabel = lastRun.status === "killed" ? "killed (timeout)" : lastRun.status;
9338
+ const detailParts = [lastRun.title, `${String(lastRun.durationMs)} ms`];
9339
+ if (lastRun.exitCode !== null && (lastRun.status === "failed" || lastRun.status === "killed")) {
9340
+ detailParts.push(`exit ${String(lastRun.exitCode)}`);
7991
9341
  }
7992
- return `Draft a new ${childPrefix} requirement that traces to ${parent.uid} (\`${parent.path}\`). Run \`doorstop add ${childPrefix}\` in the workspace root and note the UID it prints for the new item. Link the new item to its parent with \`doorstop link <new-uid> ${parent.uid}\`, substituting the printed UID for <new-uid>. Then fill in the new item's text so it elaborates ${parent.uid} at ${childPrefix} level, referencing any relevant files (prefer \`doorstop edit <new-uid>\` when available, otherwise edit the item file the add command created \u2014 doorstop normalizes formatting on the next command run). Finally run \`doorstop\` in the workspace root and confirm the new item introduces no WARNING or ERROR before judging whether it is ready for \`doorstop review <new-uid>\`.`;
9342
+ if (lastRun.signal !== null) detailParts.push(lastRun.signal);
9343
+ return b2`
9344
+ <section class="doorstop-status-bar" aria-label="Last run">
9345
+ <div class="doorstop-status-bar-row">
9346
+ <span class=${`doorstop-last-run-status is-${lastRun.status}`}>${statusLabel}</span>
9347
+ <span class="doorstop-last-run-meta">${detailParts.join(" \xB7 ")}</span>
9348
+ <button
9349
+ type="button"
9350
+ class="doorstop-last-run-dismiss"
9351
+ title="Dismiss the last run output"
9352
+ @click=${() => onDismissRun(host)}
9353
+ >Dismiss</button>
9354
+ </div>
9355
+ ${host.statusExpanded ? renderStatusBarBody(host, lastRun) : A}
9356
+ </section>
9357
+ `;
7993
9358
  }
7994
- function referencePaths(item) {
7995
- const paths = item.references?.map((reference) => reference.path) ?? [];
7996
- return item.ref === "" ? paths : [item.ref, ...paths];
9359
+ function renderStatusBarBody(host, lastRun) {
9360
+ return b2`
9361
+ <div class="doorstop-last-run">
9362
+ ${lastRun.commit === void 0 ? A : b2`<p class="doorstop-last-run-commit doorstop-muted">${commitOutcomeText(lastRun.op, lastRun.commit)}</p>`}
9363
+ ${lastRun.status === "error" ? b2`<pre class="doorstop-last-run-pre">${lastRun.errorMessage ?? ""}</pre>` : A}
9364
+ ${lastRun.stdout === "" ? A : b2`
9365
+ <pre class="doorstop-last-run-pre">${lastRun.stdout}</pre>
9366
+ ${lastRun.stdoutTruncated ? b2`<p class="doorstop-last-run-notice">stdout truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
9367
+ `}
9368
+ ${lastRun.stderr === "" ? A : b2`
9369
+ <pre class="doorstop-last-run-pre">${lastRun.stderr}</pre>
9370
+ ${lastRun.stderrTruncated ? b2`<p class="doorstop-last-run-notice">stderr truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
9371
+ `}
9372
+ ${lastRun.stdout === "" && lastRun.stderr === "" && lastRun.status !== "error" ? b2`<p class="doorstop-last-run-notice">No output captured.</p>` : A}
9373
+ </div>
9374
+ `;
7997
9375
  }
7998
- function reviewReadinessPrompt(item, children = []) {
7999
- const parents = andJoin(item.links.map((link) => link.uid));
8000
- const references = referencePaths(item);
8001
- const childLinks = andJoin(
8002
- children.map((child) => `${child.uid} (\`${child.path}\`)`)
8003
- );
8004
- const facts = [
8005
- item.text.trim() === "" ? "its text is empty (that alone blocks review)" : "its text is filled in",
8006
- item.links.length === 0 ? "it declares no parent links" : `its recorded parent links (${parents}) are not suspect or missing a fingerprint`,
8007
- children.length === 0 ? "its child links are left to the `doorstop` run below" : `its child links are declared (${childLinks})`,
8008
- references.length === 0 ? "it has no file references to verify" : `its file references exist (${references.map((path) => `\`${path}\``).join(", ")})`
8009
- ];
8010
- return `Check whether ${item.uid} is ready for \`doorstop review ${item.uid}\`. Read \`${item.path}\` and verify that ${facts.join("; ")}. Then run \`doorstop\` in the workspace root and weigh any WARNING or ERROR it reports about ${item.uid} (for example a requirement that should link to ${item.uid} but does not). Report a verdict, and if ${item.uid} is not ready, the exact fixes to make first.`;
9376
+ function onDismissRun(host) {
9377
+ host.statusExpanded = false;
9378
+ host.controller?.dismissRun();
8011
9379
  }
8012
9380
 
8013
- // src/doorstop-panel-element.ts
9381
+ // src/browser/doorstop-panel-element.ts
8014
9382
  var bodyElementTag = "pi-web-opendoor-panel-body";
8015
9383
  function defineDoorstopPanelElements() {
8016
9384
  defineDoorstopPanelBodyElement();
@@ -8030,90 +9398,10 @@ function defineDoorstopPanelBodyElement() {
8030
9398
  this.confirmSkipped = false;
8031
9399
  this.view = "items";
8032
9400
  this.statusExpanded = false;
8033
- this.unlinkInputRef = e6();
8034
- this.linkInputRef = e6();
8035
- this.gitCommitInputRef = e6();
8036
- // --- handlers ------------------------------------------------------------------
8037
- this.onDismissRun = () => {
8038
- this.statusExpanded = false;
8039
- this.controller?.dismissRun();
8040
- };
8041
- this.onRefreshClick = () => {
8042
- void this.controller?.invalidate();
8043
- };
8044
- this.onValidateClick = () => {
8045
- this.runDoorstop("validate", "Doorstop: validate", { op: "validate" }, "doorstop", true);
8046
- };
8047
- this.onPublishClick = () => {
8048
- const target = doorstopPublishTarget(this.result);
8049
- if (typeof window.confirm === "function") {
8050
- if (!window.confirm(`Publish the Doorstop tree as HTML to ${target} in the workspace terminal?`)) return;
8051
- this.confirmSkipped = false;
8052
- } else {
8053
- this.confirmSkipped = true;
8054
- }
8055
- this.runDoorstop(
8056
- "publish",
8057
- "Doorstop: publish",
8058
- { op: "publish", target },
8059
- doorstopPublishCommand(this.result, target),
8060
- false
8061
- );
8062
- };
8063
- this.onGitCommitInput = (event) => {
8064
- this.gitCommitMessage = event.target.value;
8065
- };
8066
- this.onGitStageClick = () => {
8067
- const controller = this.controller;
8068
- if (controller === void 0) return;
8069
- if (controller.runInProgress !== void 0) return;
8070
- const result = this.result;
8071
- if (result === void 0) {
8072
- this.gitActionError = "The workspace is not loaded \u2014 refresh first";
8073
- return;
8074
- }
8075
- const paths = doorstopPaths(result);
8076
- if (paths.length === 0) {
8077
- this.gitActionError = "Nothing to stage \u2014 the workspace has no Doorstop-managed files";
8078
- return;
8079
- }
8080
- this.gitActionError = void 0;
8081
- void controller.runGitStage(paths);
8082
- };
8083
- this.onGitCommitClick = () => {
8084
- this.gitCommitSubmit();
8085
- };
8086
- this.onGitCommitKeydown = (event) => {
8087
- const input = this.gitCommitInputRef.value;
8088
- if (input === void 0) return;
8089
- if (event.key === "Enter") {
8090
- event.preventDefault();
8091
- this.gitCommitSubmit();
8092
- } else if (event.key === "Escape") {
8093
- input.value = "";
8094
- this.gitCommitMessage = "";
8095
- this.gitActionError = void 0;
8096
- }
8097
- };
8098
- this.onStateFilterChange = (event) => {
8099
- const value = event.target.value;
8100
- this.controller?.setStateFilter(value === "" ? void 0 : value);
8101
- };
8102
- this.onSearchInput = (event) => {
8103
- this.controller?.setSearch(event.target.value);
8104
- };
8105
- this.onTargetKeydown = (event) => {
8106
- const input = event.target;
8107
- const op = input.getAttribute("data-op");
8108
- if (op !== "unlink" && op !== "link") return;
8109
- if (event.key === "Enter") {
8110
- event.preventDefault();
8111
- const item = this.selectedItem();
8112
- if (item !== void 0) this.runTargetOp(op, op === "unlink" ? this.unlinkInputRef : this.linkInputRef, item);
8113
- } else if (event.key === "Escape") {
8114
- input.value = "";
8115
- }
8116
- };
9401
+ /** section-API: the palette's free-text Link target input. */
9402
+ this.linkInputRef = e5();
9403
+ /** section-API: the toolbar's commit-message input. */
9404
+ this.gitCommitInputRef = e5();
8117
9405
  this.onDocumentClick = (event) => {
8118
9406
  if (!this.askMenuOpen) return;
8119
9407
  const menuRoot = this.shadowRoot?.querySelector(".doorstop-menu");
@@ -8132,16 +9420,6 @@ function defineDoorstopPanelBodyElement() {
8132
9420
  this.askMenuOpen = false;
8133
9421
  }
8134
9422
  };
8135
- this.onAskMenuToggle = () => {
8136
- this.askMenuOpen = !this.askMenuOpen;
8137
- };
8138
- this.onChangesToggle = (event) => {
8139
- const details = event.currentTarget;
8140
- if (!(details instanceof HTMLDetailsElement) || !details.open) return;
8141
- const item = this.selectedItem();
8142
- if (item === void 0) return;
8143
- void this.controller?.requestBaseline(item);
8144
- };
8145
9423
  }
8146
9424
  static {
8147
9425
  this.styles = panelStyles;
@@ -8187,9 +9465,8 @@ function defineDoorstopPanelBodyElement() {
8187
9465
  * expand handler alone would strand the section on "Loading baseline…"
8188
9466
  * with no fetch in flight. `requestBaseline` installs its "loading"
8189
9467
  * view synchronously, so this cannot loop. (A selection switch needs
8190
- * no help here — {@link renderChangesSinceReview} keys the node by UID,
8191
- * so Lit recreates it closed and the user's expand fires a real
8192
- * `toggle`.)
9468
+ * no help here — the section keys the node by UID, so Lit recreates it
9469
+ * closed and the user's expand fires a real `toggle`.)
8193
9470
  */
8194
9471
  updated() {
8195
9472
  this.ensureGitStatus();
@@ -8217,318 +9494,28 @@ function defineDoorstopPanelBodyElement() {
8217
9494
  }
8218
9495
  render() {
8219
9496
  return b2`
8220
- ${this.renderProjectActions()}
9497
+ ${renderProjectActions(this)}
8221
9498
  ${this.error === void 0 ? A : b2`<div class="doorstop-error" role="alert">${this.error}</div>`}
8222
- <section class="doorstop-viewer">${this.renderViewer()}</section>
8223
- ${this.renderItemActionPalette()}
8224
- ${this.renderStatusBar()}
8225
- `;
8226
- }
8227
- // --- project actions (region 1) -----------------------------------------------
8228
- renderProjectActions() {
8229
- const publishTarget = doorstopPublishTarget(this.result);
8230
- const projectPath = this.context?.workspace.path ?? "";
8231
- const projectLabel = projectPath === "" ? "Doorstop" : projectPath.split("/").filter(Boolean).pop() ?? projectPath;
8232
- return b2`
8233
- <section class="doorstop-project-actions">
8234
- <strong class="doorstop-title" title=${projectPath === "" ? A : projectPath}>${doorstopIconSvg}${projectLabel}</strong>
8235
- ${this.renderViewToggle()}
8236
- <div class="doorstop-toolbar-actions">
8237
- ${this.stale ? b2`<button type="button" class="doorstop-stale" title="Doorstop ran or files changed behind the panel — click to rescan" @click=${this.onRefreshClick}>stale — refresh</button>` : A}
8238
- ${this.confirmSkipped ? b2`<span class="doorstop-muted doorstop-confirm-skipped" title="No confirmation dialog is available in this environment — publishing proceeded without one">confirmation skipped — publishing</span>` : A}
8239
- <button type="button" class="doorstop-refresh" title="Re-read the workspace" @click=${this.onRefreshClick}>${refreshIconSvg}Refresh</button>
8240
- <button type="button" class="doorstop-validate" title="Run \`doorstop\` in the workspace (terminal when unpaired)" ?disabled=${this.runInProgress !== void 0} @click=${this.onValidateClick}>${validateIconSvg}Run validation</button>
8241
- <button type="button" class="doorstop-publish" title=${`Publish the tree to ${publishTarget}`} ?disabled=${this.runInProgress !== void 0} @click=${this.onPublishClick}>${publishIconSvg}Publish HTML</button>
8242
- ${this.renderGitActions()}
8243
- </div>
8244
- </section>
9499
+ <section class="doorstop-viewer">${renderViewer(this)}</section>
9500
+ ${renderItemActionPalette(this)}
9501
+ ${renderStatusBar(this)}
8245
9502
  `;
8246
9503
  }
8247
- /**
8248
- * The commit INPUT is disabled while a run is in flight (like the
8249
- * buttons) so a typed-but-never-submitted message cannot be lost when
8250
- * the run's invalidate lands mid-typing.
8251
- */
8252
- renderGitActions() {
8253
- if (!this.backendActive()) return A;
8254
- const hasDocuments = (this.result?.index.documents.length ?? 0) > 0;
8255
- return b2`
8256
- ${this.renderGitStatus()}
8257
- <button
8258
- type="button"
8259
- class="doorstop-git-stage"
8260
- title="Stage all Doorstop-managed files (requirements, documents, configs)"
8261
- ?disabled=${this.runInProgress !== void 0 || !hasDocuments}
8262
- @click=${this.onGitStageClick}
8263
- >${gitStageIconSvg}Stage all</button>
8264
- <div class="doorstop-git-commit">
8265
- <input
8266
- type="text"
8267
- class="doorstop-git-commit-input"
8268
- placeholder="Commit message"
8269
- aria-label="Git commit message"
8270
- ?disabled=${this.runInProgress !== void 0}
8271
- ${n5(this.gitCommitInputRef)}
8272
- @input=${this.onGitCommitInput}
8273
- @keydown=${this.onGitCommitKeydown}
8274
- />
8275
- <button
8276
- type="button"
8277
- class="doorstop-git-commit-button"
8278
- title="Commit the staged index with this message"
8279
- ?disabled=${this.runInProgress !== void 0 || this.gitCommitMessage.trim() === ""}
8280
- @click=${this.onGitCommitClick}
8281
- >${gitCommitIconSvg}Commit</button>
8282
- ${this.gitActionError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${this.gitActionError}</span>`}
8283
- </div>
8284
- `;
8285
- }
8286
- renderGitStatus() {
8287
- const view = this.gitStatusView;
8288
- if (view === void 0) return A;
8289
- const refresh = () => {
8290
- void this.controller?.requestGitStatus();
8291
- };
8292
- switch (view.state) {
8293
- case "loading":
8294
- return b2`<button
8295
- type="button"
8296
- class="doorstop-git-status"
8297
- title="Refreshing git status…"
8298
- aria-busy=${this.gitStatusInFlight}
8299
- @click=${refresh}
8300
- ><span class="doorstop-git-status-text">⎇ …</span></button>`;
8301
- case "no-git":
8302
- return b2`<button type="button" class="doorstop-git-status" title="Not a git repository — click to re-check" aria-busy=${this.gitStatusInFlight} @click=${refresh}><span class="doorstop-git-status-text">no git</span></button>`;
8303
- case "error":
8304
- return b2`<button
8305
- type="button"
8306
- class="doorstop-git-status doorstop-git-status-error"
8307
- title=${`Git status unavailable \u2014 ${view.errorMessage ?? "request failed"} \u2014 click to retry`}
8308
- aria-busy=${this.gitStatusInFlight}
8309
- @click=${refresh}
8310
- ><span class="doorstop-git-status-text">git status error — retry</span></button>`;
8311
- case "ready":
8312
- return b2`<button type="button" class="doorstop-git-status" title="Git status — click to refresh" aria-busy=${this.gitStatusInFlight} @click=${refresh}><span class="doorstop-git-status-text">${gitStatusText(view.response)}</span></button>`;
8313
- }
8314
- }
8315
- // --- list filters (above the item list) ---------------------------------------
8316
- renderListFilters(result) {
8317
- return b2`
8318
- <section class="doorstop-list-filters">
8319
- <div class="doorstop-docs" role="list" aria-label="Doorstop documents">
8320
- ${this.renderDocumentChip(void 0, result)}
8321
- ${result.index.documents.map((document2) => this.renderDocumentChip(document2, result))}
8322
- </div>
8323
- <select class="doorstop-state-filter" aria-label="Filter by state" @change=${this.onStateFilterChange}>
8324
- <option value="" .selected=${this.stateFilter === void 0}>All states</option>
8325
- ${Object.entries(STATE_CHIP_LABELS).map(
8326
- ([key, label]) => b2`<option value=${key} .selected=${this.stateFilter === key}>${label}</option>`
8327
- )}
8328
- </select>
8329
- <input
8330
- class="doorstop-search"
8331
- type="search"
8332
- aria-label="Search items"
8333
- placeholder="Search UID or text"
8334
- .value=${this.search}
8335
- @input=${this.onSearchInput}
8336
- />
8337
- </section>
8338
- `;
8339
- }
8340
- /**
8341
- * Expansion is CONTENT-DRIVEN (see `willUpdate`); there is no manual
8342
- * expand/collapse toggle.
8343
- */
8344
- renderStatusBar() {
8345
- const lastRun = this.lastRun;
8346
- if (lastRun === void 0) return A;
8347
- const statusLabel = lastRun.status === "killed" ? "killed (timeout)" : lastRun.status;
8348
- const detailParts = [lastRun.title, `${String(lastRun.durationMs)} ms`];
8349
- if (lastRun.exitCode !== null && (lastRun.status === "failed" || lastRun.status === "killed")) {
8350
- detailParts.push(`exit ${String(lastRun.exitCode)}`);
8351
- }
8352
- if (lastRun.signal !== null) detailParts.push(lastRun.signal);
8353
- return b2`
8354
- <section class="doorstop-status-bar" aria-label="Last run">
8355
- <div class="doorstop-status-bar-row">
8356
- <span class=${`doorstop-last-run-status is-${lastRun.status}`}>${statusLabel}</span>
8357
- <span class="doorstop-last-run-meta">${detailParts.join(" \xB7 ")}</span>
8358
- <button
8359
- type="button"
8360
- class="doorstop-last-run-dismiss"
8361
- title="Dismiss the last run output"
8362
- @click=${this.onDismissRun}
8363
- >Dismiss</button>
8364
- </div>
8365
- ${this.statusExpanded ? this.renderStatusBarBody(lastRun) : A}
8366
- </section>
8367
- `;
8368
- }
8369
- renderStatusBarBody(lastRun) {
8370
- return b2`
8371
- <div class="doorstop-last-run">
8372
- ${lastRun.commit === void 0 ? A : b2`<p class="doorstop-last-run-commit doorstop-muted">${commitOutcomeText(lastRun.op, lastRun.commit)}</p>`}
8373
- ${lastRun.status === "error" ? b2`<pre class="doorstop-last-run-pre">${lastRun.errorMessage ?? ""}</pre>` : A}
8374
- ${lastRun.stdout === "" ? A : b2`
8375
- <pre class="doorstop-last-run-pre">${lastRun.stdout}</pre>
8376
- ${lastRun.stdoutTruncated ? b2`<p class="doorstop-last-run-notice">stdout truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
8377
- `}
8378
- ${lastRun.stderr === "" ? A : b2`
8379
- <pre class="doorstop-last-run-pre">${lastRun.stderr}</pre>
8380
- ${lastRun.stderrTruncated ? b2`<p class="doorstop-last-run-notice">stderr truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
8381
- `}
8382
- ${lastRun.stdout === "" && lastRun.stderr === "" && lastRun.status !== "error" ? b2`<p class="doorstop-last-run-notice">No output captured.</p>` : A}
8383
- </div>
8384
- `;
8385
- }
8386
- renderViewToggle() {
8387
- return b2`
8388
- <div class="doorstop-view-toggle" role="tablist" aria-label="Requirements panel view">
8389
- <button
8390
- type="button"
8391
- role="tab"
8392
- class=${e5({
8393
- "doorstop-view-tab": true,
8394
- "doorstop-view-items": true,
8395
- "is-selected": this.view === "items"
8396
- })}
8397
- aria-selected=${this.view === "items" ? "true" : "false"}
8398
- title="Item list and detail"
8399
- @click=${() => {
8400
- this.view = "items";
8401
- }}
8402
- >Items</button>
8403
- <button
8404
- type="button"
8405
- role="tab"
8406
- class=${e5({
8407
- "doorstop-view-tab": true,
8408
- "doorstop-view-findings": true,
8409
- "is-selected": this.view === "findings"
8410
- })}
8411
- aria-selected=${this.view === "findings" ? "true" : "false"}
8412
- title="Validation findings"
8413
- @click=${() => {
8414
- this.view = "findings";
8415
- }}
8416
- >Findings</button>
8417
- </div>
8418
- `;
8419
- }
8420
- renderDocumentChip(document2, result) {
8421
- const prefix = document2?.prefix ?? ALL_DOCUMENTS_LABEL;
8422
- const count = document2 === void 0 ? result.index.items.length : result.index.items.filter((item) => item.documentPrefix === document2.prefix).length;
8423
- const dots = document2 === void 0 ? [] : documentStateDots(document2, result.index.items);
8424
- const selected = document2 === void 0 ? this.selectedDocumentPrefix === void 0 || this.selectedDocumentPrefix === "" : this.selectedDocumentPrefix === document2.prefix;
8425
- return b2`
8426
- <button
8427
- type="button"
8428
- role="listitem"
8429
- class=${e5({ "doorstop-doc-chip": true, "is-selected": selected })}
8430
- data-prefix=${document2?.prefix ?? ""}
8431
- @click=${() => {
8432
- this.controller?.selectDocument(document2?.prefix ?? "");
8433
- }}
8434
- >
8435
- ${document2?.parentPrefix === void 0 ? A : b2`<span class="doorstop-doc-arrow">← ${document2.parentPrefix}</span>`}
8436
- <span class="doorstop-doc-prefix">${prefix}</span>
8437
- <span class="doorstop-doc-count">${String(count)}</span>
8438
- ${dots.map((dot) => b2`<span class=${`doorstop-dot doorstop-dot-${dot}`} title=${dotTitle(dot)}></span>`)}
8439
- </button>
8440
- `;
8441
- }
8442
- // --- item list ------------------------------------------------------------------
8443
- renderViewer() {
8444
- const result = this.result;
8445
- if (result === void 0) {
8446
- return b2`<p class="doorstop-muted doorstop-standalone">${this.loading ? "Loading workspace\u2026" : "Run Refresh to scan for Doorstop documents."}</p>`;
8447
- }
8448
- if (this.view === "findings") {
8449
- return this.renderFindingsView(result);
8450
- }
8451
- return b2`
8452
- ${this.renderListFilters(result)}
8453
- ${result.index.documents.length === 0 ? b2`
8454
- ${this.renderDiagnostics(result)}
8455
- <section class="doorstop-empty"><p>${EMPTY_WORKSPACE_MESSAGE}</p></section>
8456
- ` : b2`
8457
- <section class="doorstop-split">
8458
- <section class="doorstop-list">
8459
- ${this.renderDiagnostics(result)}
8460
- ${this.renderItemList(result)}
8461
- </section>
8462
- <section class="doorstop-detail-pane">${this.renderDetail(result)}</section>
8463
- </section>
8464
- `}
8465
- `;
8466
- }
8467
- renderFindingsView(result) {
8468
- const rows = findingsViewRows(result.index);
8469
- const counts = findingsViewCounts(rows);
8470
- return b2`
8471
- <section class="doorstop-findings-view" aria-label="Doorstop findings">
8472
- <header class="doorstop-findings-head">
8473
- <span class="doorstop-findings-counts" role="status" aria-label="Finding counts">${findingsCountText(counts)}</span>
8474
- <span class="doorstop-findings-note doorstop-muted">${FINDINGS_PLUGIN_LOCAL_NOTE}</span>
8475
- </header>
8476
- ${rows.length === 0 ? b2`
8477
- <section class="doorstop-empty">
8478
- <p>${FINDINGS_EMPTY_MESSAGE}</p>
8479
- <p class="doorstop-muted">${FINDINGS_EMPTY_HINT}</p>
8480
- </section>
8481
- ` : b2`<div class="doorstop-findings-list" role="list" aria-label="Validation findings">
8482
- ${rows.map((row) => this.renderFindingRow(row, result.index))}
8483
- </div>`}
8484
- </section>
8485
- `;
8486
- }
8487
- renderFindingRow(row, index) {
8488
- const kind = row.severity === "error" ? "doorstop-error" : row.severity === "warning" ? "doorstop-warning" : "doorstop-info";
8489
- const uid = row.uid;
8490
- const navigable = uid !== void 0 && index.byUid.has(uid);
8491
- return b2`
8492
- <div class=${`doorstop-finding-row ${kind}`} role="listitem">
8493
- <span class="doorstop-severity">${row.severity}</span>
8494
- ${uid === void 0 ? A : navigable ? b2`<button type="button" class="doorstop-finding-uid" data-uid=${uid} title=${`Show ${uid} in the item list`} @click=${() => {
8495
- this.selectFindingTarget(uid);
8496
- }}><code>${uid}</code></button>` : b2`<code class="doorstop-finding-uid">${uid}</code>`}
8497
- ${row.path === void 0 ? A : b2`<code class="doorstop-finding-path">${row.path}</code>`}
8498
- <span class="doorstop-finding-message">${row.message}</span>
8499
- </div>
8500
- `;
8501
- }
8502
- selectFindingTarget(uid) {
8503
- const controller = this.controller;
8504
- if (controller === void 0) return;
8505
- controller.selectDocument("");
8506
- controller.setStateFilter(void 0);
8507
- controller.setSearch("");
8508
- controller.selectUid(uid);
8509
- this.view = "items";
8510
- }
8511
- renderDiagnostics(result) {
8512
- if (result.index.diagnostics.length === 0) return A;
8513
- return b2`
8514
- <section class="doorstop-diagnostics" aria-label="Workspace diagnostics">
8515
- ${result.index.diagnostics.map((diagnostic) => {
8516
- const kind = diagnostic.severity === "error" ? "doorstop-error" : "doorstop-warning";
8517
- return b2`
8518
- <div class=${`doorstop-diagnostic ${kind}`}>
8519
- <span class="doorstop-severity">${diagnostic.severity}</span>
8520
- <span class="doorstop-diagnostic-copy">
8521
- ${diagnostic.path === void 0 ? A : b2`<code>${diagnostic.path}</code>`}
8522
- ${diagnostic.message}
8523
- </span>
8524
- </div>
8525
- `;
8526
- })}
8527
- </section>
8528
- `;
9504
+ /** section-API: the baseline fetch and the review→commit pipeline live
9505
+ * server-side, so this gates the sections that need them. The v4 peer
9506
+ * is the exact revision-paired path to this package's own active server
9507
+ * entry; v4 also removed the provider `capabilities.request` field, so
9508
+ * `peer.request` presence is the paired-backend gate (a legal
9509
+ * openChannel-only peer is NOT paired for these request-shaped
9510
+ * sections). The opendoor provider ownership check is kept for the
9511
+ * ownership-based information display (behavior-preserving Phase A;
9512
+ * Phase C revisits it). */
9513
+ backendActive() {
9514
+ const context = this.context;
9515
+ return context?.peer?.request !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID;
8529
9516
  }
8530
- /** The status response usable for PER-ITEM git UI: a paired install, a
8531
- * ready view, and an UNTRUNCATED `files` list. `undefined` when
9517
+ /** section-API: the status response usable for PER-ITEM git UI: a paired
9518
+ * install, a ready view, and an UNTRUNCATED `files` list. `undefined` when
8532
9519
  * unpaired / loading / no-git / error / truncated, so the item list and
8533
9520
  * action row render no chips, no per-row add, and no svg inside the row
8534
9521
  * (the XSS assertion's structural dependency). The strip reads
@@ -8541,457 +9528,24 @@ function defineDoorstopPanelBodyElement() {
8541
9528
  if (gitStatusFilesTruncated(view.response)) return void 0;
8542
9529
  return view.response;
8543
9530
  }
8544
- /** The path→git-file map for one list render; `undefined` when per-item
8545
- * git UI is unavailable (see {@link readyGitStatus}). */
9531
+ /** section-API: the path→git-file map for one list render; `undefined`
9532
+ * when per-item git UI is unavailable (see {@link readyGitStatus}). */
8546
9533
  readyGitFiles() {
8547
9534
  const response = this.readyGitStatus();
8548
9535
  return response === void 0 ? void 0 : gitStatusFilesByPath(response);
8549
9536
  }
8550
- renderItemList(result) {
8551
- if (result.index.items.length === 0) {
8552
- return b2`<p class="doorstop-muted doorstop-standalone">No Doorstop items found (item files may be binary or truncated — see the diagnostics above).</p>`;
8553
- }
8554
- const items = filteredItems(result.index, this.selectedDocumentPrefix, this.stateFilter, this.search);
8555
- if (items.length === 0) {
8556
- return b2`<p class="doorstop-muted doorstop-standalone">No items match the current document, state, or search filters.</p>`;
8557
- }
8558
- const gitFiles = this.readyGitFiles();
8559
- return b2`
8560
- <div class="doorstop-items" role="list" aria-label="Doorstop items">
8561
- ${c5(items, (item) => item.uid, (item) => this.renderItemRow(item, gitFiles))}
8562
- </div>
8563
- `;
8564
- }
8565
- renderItemRow(item, gitFiles) {
8566
- const selected = this.selectedUid === item.uid;
8567
- const gitState = itemGitState(gitFiles?.get(item.path));
8568
- const stageable = itemStageable(gitFiles?.get(item.path));
8569
- const unstageable = itemUnstageable(gitFiles?.get(item.path));
8570
- return b2`
8571
- <button
8572
- type="button"
8573
- role="listitem"
8574
- class=${e5({ "doorstop-item-row": true, "is-selected": selected })}
8575
- data-uid=${item.uid}
8576
- @click=${() => {
8577
- this.controller?.selectUid(item.uid);
8578
- }}
8579
- >
8580
- <span class="doorstop-item-level">${item.level}</span>
8581
- <code class="doorstop-item-uid">${item.uid}</code>
8582
- <span class="doorstop-item-summary">${itemExcerpt(item)}</span>
8583
- <span class="doorstop-item-chips">
8584
- ${item.stateKeys.map((key) => this.renderStateChip(key))}
8585
- ${gitState === "clean" ? A : b2`<span class=${`doorstop-chip doorstop-chip-${gitChipKind(gitState)}`}>${GIT_CHIP_LABELS[gitState]}</span>`}
8586
- ${unstageable && this.runInProgress === void 0 ? b2`<span
8587
- class="doorstop-item-add"
8588
- aria-hidden="true"
8589
- title=${`git reset ${item.path}`}
8590
- @click=${(event) => {
8591
- event.stopPropagation();
8592
- this.unstageItem(item);
8593
- }}
8594
- >${gitUnstageIconSvg}</span>` : stageable && this.runInProgress === void 0 ? b2`<span
8595
- class="doorstop-item-add"
8596
- aria-hidden="true"
8597
- title=${`git add ${item.path}`}
8598
- @click=${(event) => {
8599
- event.stopPropagation();
8600
- this.stageItem(item);
8601
- }}
8602
- >${gitStageIconSvg}</span>` : A}
8603
- </span>
8604
- </button>
8605
- `;
8606
- }
8607
- renderStateChip(key) {
8608
- const kind = stateChipKind(key);
8609
- return b2`<span class=${`doorstop-chip doorstop-chip-${kind}`}>${STATE_CHIP_LABELS[key]}</span>`;
8610
- }
8611
- /** The baseline fetch and the review→commit pipeline live server-side,
8612
- * so this gates the sections that need them. */
8613
- backendActive() {
8614
- const context = this.context;
8615
- return context?.backend !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID && context.workspace.provider?.capabilities.request !== false;
8616
- }
8617
- /**
8618
- * The `<details>` is wrapped in `keyed(item.uid, …)` so a selection
8619
- * switch RE-CREATES the node (fresh, collapsed, with a real `toggle` on
8620
- * expand) instead of Lit reusing the old item's open node — a reused
8621
- * node fires no `toggle`, which would strand the new item on
8622
- * "Loading baseline…". {@link updated} covers the remaining reuse
8623
- * hole (same item re-rendered after its cache key changed).
8624
- */
8625
- renderChangesSinceReview(item) {
8626
- if (!item.stateKeys.includes("unreviewed") || item.reviewed === null) return A;
8627
- if (!this.backendActive()) return A;
8628
- const view = this.controller?.baselineViewFor(item);
8629
- return i7(
8630
- item.uid,
8631
- b2`
8632
- <details class="doorstop-changes" @toggle=${this.onChangesToggle}>
8633
- <summary class="doorstop-changes-summary">Changes since review</summary>
8634
- ${view === void 0 || view.state === "loading" ? b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>` : this.renderBaselineView(view)}
8635
- </details>
8636
- `
8637
- );
8638
- }
8639
- renderBaselineView(view) {
8640
- switch (view.state) {
8641
- case "loading":
8642
- return b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>`;
8643
- case "no-git":
8644
- return b2`<p class="doorstop-muted doorstop-changes-notice">No git history — previous version unavailable</p>`;
8645
- case "no-match":
8646
- return b2`<p class="doorstop-muted doorstop-changes-notice">Could not locate the reviewed version (history may have been rewritten)</p>`;
8647
- case "error":
8648
- return b2`<p class="doorstop-muted doorstop-changes-notice">Baseline unavailable — ${view.errorMessage ?? "request failed"}</p>`;
8649
- case "ready": {
8650
- const diff = view.diff;
8651
- if (diff === void 0) return b2`<p class="doorstop-muted doorstop-changes-notice">No changes found.</p>`;
8652
- return this.renderBaselineDiff(view, diff);
8653
- }
8654
- }
8655
- }
8656
- renderBaselineDiff(view, diff) {
8657
- return b2`
8658
- ${view.source === void 0 ? A : b2`<p class="doorstop-muted doorstop-changes-source">matched via ${view.source === "review-commit" ? "review commit" : "history walk"}</p>`}
8659
- ${diff.text === void 0 ? b2`<p class="doorstop-muted doorstop-changes-notice">Text changed — too large to render a line diff.</p>` : b2`<div class="doorstop-diff-lines" role="list" aria-label="Text changes since review">
8660
- ${diff.text.map((line) => this.renderDiffLine(line))}
8661
- </div>`}
8662
- ${diff.ref === void 0 ? A : this.renderFieldChange("ref", diff.ref.before, diff.ref.after)}
8663
- ${diff.references === void 0 ? A : this.renderFieldChange(
8664
- "references",
8665
- referenceListText(diff.references.before),
8666
- referenceListText(diff.references.after)
8667
- )}
8668
- ${diff.linksAdded.length === 0 && diff.linksRemoved.length === 0 ? A : b2`<div class="doorstop-field-change">
8669
- <span class="doorstop-field-name">links</span>
8670
- ${diff.linksAdded.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-after">+ ${uid}</code>`)}
8671
- ${diff.linksRemoved.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-before">− ${uid}</code>`)}
8672
- </div>`}
8673
- ${diff.extended.map((change) => this.renderFieldChange(change.name, change.before, change.after))}
8674
- `;
8675
- }
8676
- renderDiffLine(line) {
8677
- const mark = line.kind === "added" ? "+" : line.kind === "removed" ? "\u2212" : "";
8678
- return b2`
8679
- <div class=${`doorstop-diff-line is-${line.kind}`} role="listitem">
8680
- <span class="doorstop-diff-mark">${mark}</span>
8681
- <span class="doorstop-diff-text">${line.text === "" ? "\xA0" : line.text}</span>
8682
- </div>
8683
- `;
8684
- }
8685
- renderFieldChange(label, before, after) {
8686
- return b2`
8687
- <div class="doorstop-field-change">
8688
- <span class="doorstop-field-name">${label}</span>
8689
- <code class="doorstop-field-chip doorstop-field-chip-before">${diffValueText(before)}</code>
8690
- <span class="doorstop-field-arrow">→</span>
8691
- <code class="doorstop-field-chip doorstop-field-chip-after">${diffValueText(after)}</code>
8692
- </div>
8693
- `;
8694
- }
8695
- // --- detail pane ------------------------------------------------------------------
8696
- // v1 deliberately renders the item text as escaped text — no markdown,
8697
- // injection-safe by construction.
8698
- renderDetail(result) {
8699
- const selectedUid = this.selectedUid;
8700
- if (selectedUid === void 0) {
8701
- return b2`<p class="doorstop-muted doorstop-standalone">Select an item in the list to inspect its details.</p>`;
8702
- }
8703
- const item = result.index.byUid.get(selectedUid);
8704
- if (item === void 0) {
8705
- return b2`<p class="doorstop-muted doorstop-standalone">This item is no longer available — refresh the panel.</p>`;
8706
- }
8707
- const children = result.index.childrenByUid.get(item.uid) ?? [];
8708
- const findings = result.index.findings.filter((finding) => finding.uid === item.uid);
8709
- return b2`
8710
- <section class="doorstop-detail" aria-label="Item details">
8711
- <div class="doorstop-detail-head">
8712
- <code class="doorstop-detail-uid">${item.uid}</code>
8713
- <span class="doorstop-level">level ${item.level}</span>
8714
- ${item.header === void 0 ? A : b2`<h3>${item.header}</h3>`}
8715
- </div>
8716
- ${this.renderFlags(item)}
8717
- <div class="doorstop-state-chip-row">
8718
- ${item.stateKeys.map((key) => this.renderStateChip(key))}
8719
- </div>
8720
- ${this.renderChangesSinceReview(item)}
8721
- <h4 class="doorstop-section-title">Text</h4>
8722
- <p class=${item.text === "" ? "doorstop-text doorstop-text-empty" : "doorstop-text"}>${item.text === "" ? "empty" : item.text}</p>
8723
- <h4 class="doorstop-section-title">Parent links</h4>
8724
- ${this.renderLinksOut(item, result.index)}
8725
- <h4 class="doorstop-section-title">Child links</h4>
8726
- ${this.renderLinksIn(item, result.index, children)}
8727
- <h4 class="doorstop-section-title">References</h4>
8728
- ${this.renderReferences(item, result.index)}
8729
- ${Object.entries(item.attributes).length === 0 ? A : b2`
8730
- <h4 class="doorstop-section-title">Extended attributes</h4>
8731
- <dl class="doorstop-attributes">
8732
- ${Object.entries(item.attributes).map(
8733
- ([key, value]) => b2`<div class="doorstop-attribute"><dt>${key}</dt><dd><code>${jsonishText(value)}</code></dd></div>`
8734
- )}
8735
- </dl>
8736
- `}
8737
- <h4 class="doorstop-section-title">Findings</h4>
8738
- ${findings.length === 0 ? b2`<p class="doorstop-muted">No local findings for this item.</p>` : b2`<div class="doorstop-findings" aria-label="Findings for this item">
8739
- ${findings.map((finding) => {
8740
- const kind = finding.severity === "error" ? "doorstop-error" : finding.severity === "warning" ? "doorstop-warning" : "doorstop-info";
8741
- return b2`<div class=${`doorstop-finding ${kind}`}><span class="doorstop-finding-severity">${finding.severity}</span><span>${finding.message}</span></div>`;
8742
- })}
8743
- </div>`}
8744
- </section>
8745
- `;
8746
- }
8747
- // --- item action palette (region 4) --------------------------------------------
8748
- renderItemActionPalette() {
8749
- if (this.view !== "items") return A;
8750
- const result = this.result;
8751
- if (result === void 0 || result.index.documents.length === 0) return A;
8752
- const item = this.selectedItem();
8753
- if (item === void 0) {
8754
- return b2`
8755
- <section class="doorstop-action-palette" aria-label="Item actions">
8756
- <span class="doorstop-muted doorstop-palette-placeholder">Select an item…</span>
8757
- </section>
8758
- `;
8759
- }
8760
- const suspects = suspectParentItems(item, result.index);
8761
- return b2`
8762
- <section class="doorstop-action-palette" aria-label="Item actions">
8763
- ${this.renderActionRow(item, result.index, suspects)}
8764
- </section>
8765
- `;
8766
- }
8767
- renderFlags(item) {
8768
- return b2`
8769
- <div class="doorstop-flags" aria-label="Item flags">
8770
- <span class=${e5({ "doorstop-flag": true, "is-inactive": !item.active })}>
8771
- ${item.active ? "active" : "inactive"}
8772
- </span>
8773
- <span class="doorstop-flag">${item.normative ? "normative" : "non-normative"}</span>
8774
- <span class="doorstop-flag">${item.derived ? "derived" : "non-derived"}</span>
8775
- </div>
8776
- `;
8777
- }
8778
- /** `computeItemStamp(parent, config, false)` is exactly the
8779
- * link-record stamp the state chain compares against. */
8780
- renderLinksOut(item, index) {
8781
- if (item.links.length === 0) return b2`<p class="doorstop-muted">No parent links.</p>`;
8782
- return b2`
8783
- <div class="doorstop-links" aria-label="Parent links">
8784
- ${item.links.map((link) => this.renderLinkOut(link, index))}
8785
- </div>
8786
- `;
8787
- }
8788
- renderLinkOut(link, index) {
8789
- const target = index.byUid.get(link.uid);
8790
- if (target === void 0) {
8791
- return b2`
8792
- <div class="doorstop-link-row">
8793
- <code>${link.uid}</code>
8794
- <span class="doorstop-chip doorstop-chip-danger">unknown</span>
8795
- <span class="doorstop-fingerprint">not in the index — ${link.fingerprint === null ? "no recorded fingerprint" : `recorded ${shortFingerprint(link.fingerprint)}`}</span>
8796
- </div>
8797
- `;
8798
- }
8799
- const current = computeItemStamp(target, documentConfigFor(index, target), false);
8800
- const suspect = link.fingerprint !== null && link.fingerprint !== current;
8801
- return b2`
8802
- <button
8803
- type="button"
8804
- class="doorstop-link-row"
8805
- data-uid=${link.uid}
8806
- title=${`Open ${link.uid}`}
8807
- @click=${() => {
8808
- this.controller?.selectUid(link.uid);
8809
- }}
8810
- >
8811
- <code>${link.uid}</code>
8812
- <span class=${suspect ? "doorstop-chip doorstop-chip-danger" : "doorstop-chip doorstop-chip-ok"}>${suspect ? "suspect" : "ok"}</span>
8813
- <span class="doorstop-fingerprint">recorded ${shortFingerprint(link.fingerprint)} · current ${shortFingerprint(current)}</span>
8814
- </button>
8815
- `;
8816
- }
8817
- renderLinksIn(item, index, children) {
8818
- if (children.length === 0) {
8819
- return b2`<p class="doorstop-muted">No child links.</p>`;
8820
- }
8821
- return b2`
8822
- <div class="doorstop-links" aria-label="Child links">
8823
- ${children.map(
8824
- (child) => b2`<button type="button" class="doorstop-link-row" data-uid=${child.uid} title=${`Open ${child.uid}`} @click=${() => {
8825
- this.controller?.selectUid(child.uid);
8826
- }}>
8827
- <code>${child.uid}</code>
8828
- <span class="doorstop-muted">${child.level}</span>
8829
- </button>`
8830
- )}
8831
- </div>
8832
- `;
8833
- }
8834
- renderReferences(item, index) {
8835
- const paths = [];
8836
- if (item.ref !== "") paths.push(item.ref);
8837
- if (item.references !== void 0) {
8838
- for (const reference of item.references) paths.push(reference.path);
8839
- }
8840
- if (paths.length === 0) return b2`<p class="doorstop-muted">No file references.</p>`;
8841
- return b2`
8842
- <div class="doorstop-links" aria-label="File references">
8843
- ${paths.map((path) => {
8844
- const found = index.knownFilePaths.has(path);
8845
- return b2`
8846
- <div class="doorstop-link-row">
8847
- <code>${path}</code>
8848
- ${found ? A : b2`<span class="doorstop-chip doorstop-chip-danger">not found</span>`}
8849
- </div>
8850
- `;
8851
- })}
8852
- </div>
8853
- `;
8854
- }
8855
- // --- action row ------------------------------------------------------------------
8856
- renderActionRow(item, index, suspects) {
8857
- const reviewed = item.stateKeys.includes("reviewed");
8858
- const suspectUids = suspects.length > 0 ? suspects.map((parent) => parent.uid) : [];
8859
- const gitStatus = this.readyGitStatus();
8860
- const gitFile = gitStatus === void 0 ? void 0 : gitStatusFileFor(gitStatus, item.path);
8861
- const stageable = itemStageable(gitFile);
8862
- const unstageable = itemUnstageable(gitFile);
8863
- return b2`
8864
- <button
8865
- type="button"
8866
- class="doorstop-review"
8867
- ?disabled=${reviewed || this.runInProgress !== void 0}
8868
- title=${reviewed ? `${item.uid} is already reviewed against its current fingerprint` : `Mark ${item.uid} reviewed`}
8869
- @click=${() => {
8870
- this.reviewItem(item);
8871
- }}
8872
- >Review</button>
8873
- <button
8874
- type="button"
8875
- class="doorstop-clear"
8876
- ?disabled=${suspects.length === 0 || this.runInProgress !== void 0}
8877
- title=${suspects.length === 0 ? `No suspect links to clear` : `Re-record the parent fingerprints of ${item.uid}`}
8878
- @click=${() => {
8879
- this.clearSuspects(item, suspectUids);
8880
- }}
8881
- >Clear suspect links</button>
8882
- <button
8883
- type="button"
8884
- class="doorstop-edit"
8885
- ?disabled=${this.runInProgress !== void 0}
8886
- title=${`Open ${item.uid} in the editor`}
8887
- @click=${() => {
8888
- this.editItem(item);
8889
- }}
8890
- >Edit</button>
8891
- ${gitStatus === void 0 ? A : b2`<button
8892
- type="button"
8893
- class="doorstop-item-stage"
8894
- ?disabled=${this.runInProgress !== void 0 || !stageable}
8895
- title=${stageable ? `Stage ${item.path}` : `${item.uid} has no unstaged changes \u2014 commit next`}
8896
- @click=${() => {
8897
- this.stageItem(item);
8898
- }}
8899
- >${gitStageIconSvg}Stage</button>
8900
- <button
8901
- type="button"
8902
- class="doorstop-item-unstage"
8903
- ?disabled=${this.runInProgress !== void 0 || !unstageable}
8904
- title=${unstageable ? `Unstage ${item.path} (git reset)` : `${item.uid} has nothing staged`}
8905
- @click=${() => {
8906
- this.unstageItem(item);
8907
- }}
8908
- >${gitUnstageIconSvg}Unstage</button>`}
8909
- <div class="doorstop-op">
8910
- <input
8911
- type="text"
8912
- class="doorstop-target-input"
8913
- data-op="unlink"
8914
- placeholder="parent UID"
8915
- ${n5(this.unlinkInputRef)}
8916
- @keydown=${this.onTargetKeydown}
8917
- />
8918
- <button type="button" class="doorstop-unlink" title=${`doorstop unlink ${item.uid} <target>`} ?disabled=${this.runInProgress !== void 0} @click=${() => {
8919
- this.runTargetOp("unlink", this.unlinkInputRef, item);
8920
- }}>Unlink</button>
8921
- </div>
8922
- <div class="doorstop-op">
8923
- <input
8924
- type="text"
8925
- class="doorstop-target-input"
8926
- data-op="link"
8927
- placeholder="parent UID"
8928
- ${n5(this.linkInputRef)}
8929
- @keydown=${this.onTargetKeydown}
8930
- />
8931
- <button type="button" class="doorstop-link" title=${`doorstop link ${item.uid} <target>`} ?disabled=${this.runInProgress !== void 0} @click=${() => {
8932
- this.runTargetOp("link", this.linkInputRef, item);
8933
- }}>Link</button>
8934
- </div>
8935
- ${this.targetError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${this.targetError}</span>`}
8936
- ${this.renderAskMenu(item, index, suspects)}
8937
- `;
8938
- }
8939
- renderAskMenu(item, index, suspects) {
8940
- const children = index.childrenByUid.get(item.uid) ?? [];
8941
- const childPrefix = firstChildDocumentPrefix(index, item);
8942
- return b2`
8943
- <div class="doorstop-menu">
8944
- <button
8945
- type="button"
8946
- class="doorstop-menu-toggle"
8947
- aria-expanded=${this.askMenuOpen ? "true" : "false"}
8948
- @click=${this.onAskMenuToggle}
8949
- >Ask agent</button>
8950
- ${this.askMenuOpen ? b2`
8951
- <div class="doorstop-menu-items" role="menu" aria-label="Ask the agent about ${item.uid}">
8952
- <button type="button" role="menuitem" class="doorstop-menu-item doorstop-explain" @click=${() => {
8953
- this.insertPrompt(explainItemPrompt(item));
8954
- }}>Explain</button>
8955
- <button
8956
- type="button"
8957
- role="menuitem"
8958
- class="doorstop-menu-item doorstop-fix-suspects"
8959
- ?disabled=${suspects.length === 0}
8960
- title=${suspects.length === 0 ? "No suspect links \u2014 nothing to fix" : `Fix the suspect links of ${item.uid}`}
8961
- @click=${() => {
8962
- this.insertPrompt(fixSuspectLinksPrompt(item, suspects));
8963
- }}
8964
- >Fix suspect links</button>
8965
- <button
8966
- type="button"
8967
- role="menuitem"
8968
- class="doorstop-menu-item doorstop-draft-child"
8969
- ?disabled=${childPrefix === void 0}
8970
- title=${childPrefix === void 0 ? "No child document to add to" : `Draft a child item in the ${childPrefix} document`}
8971
- @click=${() => {
8972
- if (childPrefix !== void 0) this.insertPrompt(draftChildRequirementPrompt(item, childPrefix));
8973
- }}
8974
- >Draft child requirement</button>
8975
- <button type="button" role="menuitem" class="doorstop-menu-item doorstop-review-readiness" @click=${() => {
8976
- this.insertPrompt(reviewReadinessPrompt(item, children));
8977
- }}>Review readiness</button>
8978
- </div>
8979
- ` : A}
8980
- </div>
8981
- `;
8982
- }
8983
- /** Stage one item's own file — the same `doorstop.git-stage` operation
8984
- * with `[item.path]`; the shared run dispatch narrates the uid. Shared
8985
- * by the row's `git add` span and the palette's Stage button. */
9537
+ /** section-API: stage one item's own file — the same `doorstop.git-stage`
9538
+ * operation with `[item.path]`; the shared run dispatch narrates the uid.
9539
+ * Shared by the row's `git add` span and the palette's Stage button. */
8986
9540
  stageItem(item) {
8987
9541
  const controller = this.controller;
8988
9542
  if (controller === void 0 || controller.runInProgress !== void 0) return;
8989
9543
  void controller.runGitStage([item.path], `Git: stage ${item.uid}`);
8990
9544
  }
8991
- /** Unstage one item's own file — the `doorstop.git-unstage` inverse with
8992
- * `[item.path]`; the shared run dispatch narrates the uid. Shared by
8993
- * the row's minus span and the palette's Unstage button. The guard
8994
- * mirrors {@link stageItem}: a run already in flight must never be
9545
+ /** section-API: unstage one item's own file — the `doorstop.git-unstage`
9546
+ * inverse with `[item.path]`; the shared run dispatch narrates the uid.
9547
+ * Shared by the row's minus span and the palette's Unstage button. The
9548
+ * guard mirrors {@link stageItem}: a run already in flight must never be
8995
9549
  * overlapped (the disabled states cover pointer clicks; this covers
8996
9550
  * every call path). */
8997
9551
  unstageItem(item) {
@@ -8999,85 +9553,7 @@ function defineDoorstopPanelBodyElement() {
8999
9553
  if (controller === void 0 || controller.runInProgress !== void 0) return;
9000
9554
  void controller.runGitUnstage([item.path], `Git: unstage ${item.uid}`);
9001
9555
  }
9002
- /**
9003
- * The browser must never send an empty commit message: the server
9004
- * rejects it, and git with an empty `-m` would hang the exec until the
9005
- * deadline. The input is cleared only on SUCCESS so a `failed` commit
9006
- * (hook stderr, missing identity) keeps the message for a corrected
9007
- * retry.
9008
- */
9009
- gitCommitSubmit() {
9010
- const controller = this.controller;
9011
- if (controller === void 0) return;
9012
- if (controller.runInProgress !== void 0) return;
9013
- const input = this.gitCommitInputRef.value;
9014
- const message = input?.value.trim() ?? "";
9015
- if (message === "") {
9016
- this.gitActionError = "Enter a commit message";
9017
- input?.focus();
9018
- return;
9019
- }
9020
- this.gitActionError = void 0;
9021
- void controller.runGitCommit(message).then(() => {
9022
- const lastRun = controller.lastRun;
9023
- if (lastRun?.op === "git-commit" && lastRun.status === "ok" && input !== void 0) {
9024
- input.value = "";
9025
- this.gitCommitMessage = "";
9026
- }
9027
- });
9028
- }
9029
- reviewItem(item) {
9030
- const commit = doorstopCommitAfterReview(this.result);
9031
- this.runDoorstop(
9032
- "review",
9033
- `Doorstop: review ${item.uid}`,
9034
- commit ? { op: "review", uid: item.uid, commit: true } : { op: "review", uid: item.uid },
9035
- `doorstop review ${item.uid}`,
9036
- false
9037
- );
9038
- }
9039
- clearSuspects(item, suspectUids) {
9040
- if (suspectUids.length === 0) return;
9041
- this.runDoorstop(
9042
- "clear",
9043
- "Doorstop: clear suspect links",
9044
- { op: "clear", uid: item.uid, parents: suspectUids },
9045
- `doorstop clear ${item.uid} ${suspectUids.join(" ")}`,
9046
- false
9047
- );
9048
- }
9049
- editItem(item) {
9050
- this.runDoorstop(
9051
- "edit",
9052
- `Doorstop: edit ${item.uid}`,
9053
- { op: "edit", uid: item.uid },
9054
- `doorstop edit ${item.uid}`,
9055
- false
9056
- );
9057
- }
9058
- runTargetOp(op, inputRef, item) {
9059
- const input = inputRef.value;
9060
- const target = input?.value.trim() ?? "";
9061
- if (target === "") {
9062
- this.targetError = `Enter a ${op} target UID (e.g. ${item.documentPrefix}0001)`;
9063
- input?.focus();
9064
- return;
9065
- }
9066
- if (!isValidTargetUid(target)) {
9067
- this.targetError = `Invalid ${op} target "${target}" \u2014 expected a Doorstop UID like ${item.documentPrefix}0001 (no spaces, quotes, or special characters)`;
9068
- input?.focus();
9069
- return;
9070
- }
9071
- this.targetError = void 0;
9072
- if (input !== void 0) input.value = "";
9073
- this.runDoorstop(
9074
- op,
9075
- `Doorstop: ${op} ${item.uid}`,
9076
- { op, uid: item.uid, target },
9077
- `doorstop ${op} ${item.uid} ${target}`,
9078
- false
9079
- );
9080
- }
9556
+ /** section-API: insert an Ask-agent prompt and close the menu. */
9081
9557
  insertPrompt(text) {
9082
9558
  const context = this.context;
9083
9559
  if (context === void 0) return;
@@ -9088,9 +9564,9 @@ function defineDoorstopPanelBodyElement() {
9088
9564
  /**
9089
9565
  * The single dispatch surface of every panel action, with TWO paths:
9090
9566
  *
9091
- * - BACKEND path when the workspace is owned by the opendoor provider
9092
- * with an active backend: the STRUCTURED `input` goes through
9093
- * `context.backend.request("doorstop.run", …)` — argv is built
9567
+ * - PEER path when the workspace is owned by the opendoor provider
9568
+ * with an active peer: the STRUCTURED `input` goes through
9569
+ * `context.peer.request("doorstop.run", …)` — argv is built
9094
9570
  * server-side, so the browser never shell-quotes on this path. A
9095
9571
  * rejected request commits `status: "error"` and does NOT
9096
9572
  * invalidate (the run wrote nothing to the workspace).
@@ -9106,8 +9582,8 @@ function defineDoorstopPanelBodyElement() {
9106
9582
  const controller = this.controller;
9107
9583
  if (context === void 0 || controller === void 0) return;
9108
9584
  if (controller.runInProgress !== void 0) return;
9109
- if (context.backend !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID && context.workspace.provider?.capabilities.request !== false) {
9110
- void this.runDoorstopBackend(op, title, input, context.backend);
9585
+ if (context.peer?.request !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID) {
9586
+ void this.runDoorstopBackend(op, title, input, context.peer);
9111
9587
  return;
9112
9588
  }
9113
9589
  const terminal = context.terminal;
@@ -9121,13 +9597,15 @@ function defineDoorstopPanelBodyElement() {
9121
9597
  }).catch(() => {
9122
9598
  });
9123
9599
  }
9124
- async runDoorstopBackend(op, title, input, backend) {
9600
+ async runDoorstopBackend(op, title, input, peer) {
9125
9601
  const controller = this.controller;
9126
9602
  if (controller === void 0) return;
9603
+ const request = peer.request;
9604
+ if (request === void 0) return;
9127
9605
  controller.beginRun(title);
9128
9606
  const startedAt = Date.now();
9129
9607
  try {
9130
- const response = await backend.request(DOORSTOP_RUN_OPERATION, input);
9608
+ const response = await request(DOORSTOP_RUN_OPERATION, input);
9131
9609
  const parsed = parseDoorstopRunResponse(response);
9132
9610
  const status = parsed.signal !== null ? "killed" : parsed.exitCode === 0 ? "ok" : "failed";
9133
9611
  controller.commitRun({
@@ -9167,12 +9645,21 @@ function defineDoorstopPanelBodyElement() {
9167
9645
  controller.endRun();
9168
9646
  }
9169
9647
  }
9648
+ /** section-API: the currently selected item, resolved against the
9649
+ * loaded index. */
9170
9650
  selectedItem() {
9171
9651
  const result = this.result;
9172
9652
  const uid = this.selectedUid;
9173
9653
  if (result === void 0 || uid === void 0) return void 0;
9174
9654
  return result.index.byUid.get(uid);
9175
9655
  }
9656
+ /** section-API: the shared state chip (item rows + detail head). The
9657
+ * renderer is a free function in `sections/item-list.ts`; this delegate
9658
+ * lets detail-pane reach it through the host, keeping the runtime import
9659
+ * graph one-way (coordinator → sections). */
9660
+ renderStateChip(key) {
9661
+ return renderStateChip(key);
9662
+ }
9176
9663
  }
9177
9664
  __decorateClass([
9178
9665
  n6({ attribute: false })
@@ -9252,18 +9739,19 @@ function defineCustomElementOnce(tag, define) {
9252
9739
  define();
9253
9740
  }
9254
9741
 
9255
- // src/doorstop-contributions.ts
9256
- var DOORSTOP_PANEL_LOCAL_ID = "workspace.doorstop";
9742
+ // src/browser/doorstop-contributions.ts
9743
+ var DOORSTOP_PANEL_LOCAL_ID2 = "workspace.doorstop";
9257
9744
  var DOORSTOP_LABEL_STATE_LIMIT = 8;
9258
9745
  function createOpendoorBrowserContributions(runtimePluginId, _html, _svg) {
9259
- const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID}`;
9746
+ const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID2}`;
9260
9747
  const registry = new DoorstopWorkspaceRegistry();
9261
9748
  const labelCache = new DoorstopLabelCache();
9262
9749
  defineDoorstopPanelElements();
9263
9750
  return {
9264
9751
  actions: createDoorstopActions(panelId),
9265
9752
  workspacePanels: [createDoorstopPanel(registry)],
9266
- workspaceLabels: [createDoorstopLabel(labelCache)]
9753
+ workspaceLabels: [createDoorstopLabel(labelCache)],
9754
+ sessionLabels: [createDoorstopSessionLabel(runtimePluginId)]
9267
9755
  };
9268
9756
  }
9269
9757
  function createDoorstopActions(panelId) {
@@ -9282,7 +9770,7 @@ function createDoorstopActions(panelId) {
9282
9770
  // Enabled unconditionally: the Requirements panel is useful for every
9283
9771
  // workspace (paired or unpaired — it needs no owned provider).
9284
9772
  run: (context) => {
9285
- context.selectMainView(panelId);
9773
+ context.selectWorkspaceTool(panelId);
9286
9774
  }
9287
9775
  },
9288
9776
  {
@@ -9303,7 +9791,7 @@ function createDoorstopActions(panelId) {
9303
9791
  }
9304
9792
  function createDoorstopPanel(registry) {
9305
9793
  return {
9306
- id: DOORSTOP_PANEL_LOCAL_ID,
9794
+ id: DOORSTOP_PANEL_LOCAL_ID2,
9307
9795
  title: "Requirements",
9308
9796
  // Door/archive icon (currentColor): a filing cabinet with the doorstop
9309
9797
  // checklist mark — the panel title icon, created with this module's own
@@ -9315,18 +9803,53 @@ function createDoorstopPanel(registry) {
9315
9803
  <path d="m9 12 2 2 4-4"></path>
9316
9804
  </svg>
9317
9805
  `,
9318
- order: 60,
9806
+ // Plan Phase C step C3 follow-up — order 5: the host's chat-file-link
9807
+ // dispatch (resolveWorkspaceFileOpen) opens the FIRST ACCEPTING panel,
9808
+ // iterating panels by `order` ascending, and the bundled Files panel is
9809
+ // order 10 and answers `{ file: path }` for EVERY path unconditionally —
9810
+ // at order 60 this panel's `fileOpenQuery` was never consulted at all.
9811
+ // Sitting below 10 is the only host-supported way to claim doorstop item
9812
+ // files for the Requirements panel; the accepted consequence is that the
9813
+ // Requirements tab precedes Files in the workspace tab bar.
9814
+ order: 5,
9319
9815
  // The panel is always visible (paired or unpaired): when the workspace
9320
9816
  // is opendoor-owned the run actions use the backend, otherwise the
9321
9817
  // terminal fallback keeps every action working; the discovery-based
9322
9818
  // empty state carries the "no documents" story (feature spec §7.1).
9323
9819
  visible: () => true,
9324
- onInvalidate: (context) => registry.invalidate(context),
9820
+ // Plan Phase C step C1 — typed invalidation (browser API v4): declare the
9821
+ // fixed workspace resources whose automatic invalidations this panel
9822
+ // receives. With `workspace.files` declared, the host calls onInvalidate
9823
+ // not only for the manual Refresh action (refreshWorkspacePanels) but
9824
+ // also when workspace files change (`reason: "mutation"` — e.g. the
9825
+ // review-commit pipeline's commits landing, or any other plugin's file
9826
+ // writes) and when agent work finishes in the workspace (`reason:
9827
+ // "agent-activity"` — an agent editing doorstop documents refreshes the
9828
+ // panel without a manual Refresh). The reason is forwarded to the
9829
+ // registry, which rescans unconditionally (see
9830
+ // DoorstopWorkspaceRegistry.invalidate).
9831
+ invalidationResources: ["workspace.files"],
9832
+ onInvalidate: (context, invalidation) => registry.invalidate(context, invalidation),
9833
+ // Plan Phase C step C3 — chat file links: when the host resolves a
9834
+ // Markdown link to a workspace-relative path, this hook answers with the
9835
+ // panel's navigation query IF the panel owns the path (its last landed
9836
+ // discovery indexed it as an item file), so the chat opens the item IN
9837
+ // the Requirements panel instead of the Files fallback. Synchronous and
9838
+ // side-effect-free: it answers purely from the registry's recorded index
9839
+ // (never loads, never renders); unknown paths and pre-first-load panels
9840
+ // return undefined (the host asks the next panel / serves Files).
9841
+ fileOpenQuery: (context, path) => registry.fileOpenQuery(context, path),
9325
9842
  render: (context) => renderDoorstopPanel(registry, context)
9326
9843
  };
9327
9844
  }
9328
9845
  function renderDoorstopPanel(registry, context) {
9329
9846
  const controller = registry.for(context);
9847
+ if (context.navigation !== void 0) {
9848
+ controller.applyNavigationQuery(readPanelNavigationQuery(context.navigation));
9849
+ }
9850
+ if (controller.result !== void 0) {
9851
+ registry.recordFileOpenIndex(context, controller.result);
9852
+ }
9330
9853
  return b2`
9331
9854
  <pi-web-opendoor-panel-body
9332
9855
  .controller=${controller}
@@ -9425,12 +9948,12 @@ function doorstopLabelKey(context) {
9425
9948
  return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
9426
9949
  }
9427
9950
 
9428
- // src/pi-web-plugin.ts
9951
+ // src/browser/pi-web-plugin.ts
9429
9952
  var plugin = {
9430
- apiVersion: 2,
9953
+ apiVersion: 4,
9431
9954
  name: "Opendoor",
9432
- activate: ({ runtimePluginId, html, svg: svg2 }) => ({
9433
- contributions: createOpendoorBrowserContributions(runtimePluginId, html, svg2)
9955
+ activate: ({ runtimePluginId, html, svg }) => ({
9956
+ contributions: createOpendoorBrowserContributions(runtimePluginId, html, svg)
9434
9957
  })
9435
9958
  };
9436
9959
  var pi_web_plugin_default = plugin;
@@ -9451,7 +9974,6 @@ lit-html/lit-html.js:
9451
9974
  lit-element/lit-element.js:
9452
9975
  lit-html/directive.js:
9453
9976
  lit-html/async-directive.js:
9454
- lit-html/directives/repeat.js:
9455
9977
  @lit/reactive-element/decorators/custom-element.js:
9456
9978
  @lit/reactive-element/decorators/property.js:
9457
9979
  @lit/reactive-element/decorators/state.js:
@@ -9461,6 +9983,7 @@ lit-html/directives/repeat.js:
9461
9983
  @lit/reactive-element/decorators/query-all.js:
9462
9984
  @lit/reactive-element/decorators/query-async.js:
9463
9985
  @lit/reactive-element/decorators/query-assigned-nodes.js:
9986
+ lit-html/directives/repeat.js:
9464
9987
  (**
9465
9988
  * @license
9466
9989
  * Copyright 2017 Google LLC
@@ -9474,13 +9997,6 @@ lit-html/is-server.js:
9474
9997
  * SPDX-License-Identifier: BSD-3-Clause
9475
9998
  *)
9476
9999
 
9477
- lit-html/directives/class-map.js:
9478
- (**
9479
- * @license
9480
- * Copyright 2018 Google LLC
9481
- * SPDX-License-Identifier: BSD-3-Clause
9482
- *)
9483
-
9484
10000
  lit-html/directive-helpers.js:
9485
10001
  lit-html/directives/ref.js:
9486
10002
  (**
@@ -9489,11 +10005,18 @@ lit-html/directives/ref.js:
9489
10005
  * SPDX-License-Identifier: BSD-3-Clause
9490
10006
  *)
9491
10007
 
9492
- lit-html/directives/keyed.js:
9493
10008
  @lit/reactive-element/decorators/query-assigned-elements.js:
10009
+ lit-html/directives/keyed.js:
9494
10010
  (**
9495
10011
  * @license
9496
10012
  * Copyright 2021 Google LLC
9497
10013
  * SPDX-License-Identifier: BSD-3-Clause
9498
10014
  *)
10015
+
10016
+ lit-html/directives/class-map.js:
10017
+ (**
10018
+ * @license
10019
+ * Copyright 2018 Google LLC
10020
+ * SPDX-License-Identifier: BSD-3-Clause
10021
+ *)
9499
10022
  */