opendoor-plugin 0.2.0 → 0.5.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,68 @@ 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 typed invalidation (browser API v4,
6566
+ * unchanged on v5): `invalidation` carries
6567
+ * the reason (`"manual"` Refresh action / `"mutation"` a workspace file
6568
+ * changed / `"agent-activity"` agent work finished) and the affected
6569
+ * resources. The rescan stays REASON-AGNOSTIC deliberately: every reason
6570
+ * may have changed item files, the load job re-reads everything anyway
6571
+ * (no partial invalidation is possible — the index is one unit), and the
6572
+ * controller's re-entrant guard coalesces storms (an invalidate while a
6573
+ * load is running JOINS that load; only an invalidate after it lands
6574
+ * starts a fresh one), so `"agent-activity"` bursts cannot stack loads.
6575
+ * The payload is accepted (so the contribution can forward it) but not
6576
+ * consulted. */
6577
+ invalidate(context, invalidation) {
6578
+ void invalidation;
6378
6579
  return this.for(context).invalidate();
6379
6580
  }
6581
+ /** Record the chat file-open index for one workspace from a LANDED result
6582
+ * (plan Phase C step C3). Called from renderDoorstopPanel on every render
6583
+ * that has a result, so the index always mirrors the last landed
6584
+ * discovery; never throws and triggers nothing (the hook must be free of
6585
+ * side effects — recording a plain index from a landed result is one). */
6586
+ recordFileOpenIndex(context, result) {
6587
+ const key = workspaceContextKey(context);
6588
+ const existing = this.fileOpenEntries.get(key);
6589
+ if (existing !== void 0 && existing.result === result) {
6590
+ this.fileOpenEntries.delete(key);
6591
+ this.fileOpenEntries.set(key, existing);
6592
+ return;
6593
+ }
6594
+ const entries = /* @__PURE__ */ new Map();
6595
+ for (const item of result.index.items) {
6596
+ entries.set(item.path, { uid: item.uid, documentPrefix: item.documentPrefix });
6597
+ }
6598
+ this.fileOpenEntries.delete(key);
6599
+ this.fileOpenEntries.set(key, { result, entries });
6600
+ this.boundFileOpenEntries();
6601
+ }
6602
+ /** The panel's chat file-open hook (plan Phase C step C3): does the last
6603
+ * landed discovery of this workspace know `path` as an item file? Returns
6604
+ * the panel's navigation query (`{ file: path }` — the key
6605
+ * applyNavigationQuery resolves to the item's uid and document
6606
+ * selection), or undefined when the panel does not own the path (the
6607
+ * host then asks the next panel, or serves the Files fallback). Before
6608
+ * the first load lands there is no index yet → undefined (the chat link
6609
+ * falls back to Files; the panel still deep-links once opened and
6610
+ * re-rendered). */
6611
+ fileOpenQuery(context, path) {
6612
+ const entries = this.fileOpenEntries.get(workspaceContextKey(context));
6613
+ if (entries === void 0) return void 0;
6614
+ return entries.entries.has(path) ? { [DOORSTOP_NAV_FILE]: path } : void 0;
6615
+ }
6616
+ /** Keep the file-open index at the same LRU bound as the controller map
6617
+ * (strictly oldest-first, mirroring evictOldest). */
6618
+ boundFileOpenEntries() {
6619
+ while (this.fileOpenEntries.size > DOORSTOP_WORKSPACE_STATE_LIMIT) {
6620
+ const key = this.fileOpenEntries.keys().next().value;
6621
+ if (key === void 0) break;
6622
+ this.fileOpenEntries.delete(key);
6623
+ }
6624
+ }
6380
6625
  evictOldest() {
6381
6626
  if (this.workspaces.size < DOORSTOP_WORKSPACE_STATE_LIMIT) return;
6382
6627
  const key = this.workspaces.keys().next().value;
@@ -6384,6 +6629,7 @@ var DoorstopWorkspaceRegistry = class {
6384
6629
  const controller = this.workspaces.get(key);
6385
6630
  if (controller !== void 0) controller.release();
6386
6631
  this.workspaces.delete(key);
6632
+ this.fileOpenEntries.delete(key);
6387
6633
  }
6388
6634
  };
6389
6635
  var DoorstopPanelHost = class {
@@ -6395,127 +6641,94 @@ function workspaceContextKey(context) {
6395
6641
  return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
6396
6642
  }
6397
6643
 
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";
6644
+ // src/doorstop-session-metadata.ts
6645
+ var OPENDOOR_SESSION_METADATA_NAMESPACE = "opendoor";
6646
+ var DOORSTOP_SESSION_METADATA_SCHEMA = 1;
6647
+ var DOORSTOP_SESSION_METADATA_MAX_UIDS = 8;
6648
+ var DOORSTOP_SESSION_METADATA_MAX_UID_LENGTH = 32;
6649
+ var DOORSTOP_SESSION_METADATA_OPS = /* @__PURE__ */ new Set(["review"]);
6650
+ var DOORSTOP_SESSION_METADATA_UID_PATTERN = /^[\w.-]+$/u;
6651
+ function isMetadataUid(value) {
6652
+ return typeof value === "string" && value !== "" && value.length <= DOORSTOP_SESSION_METADATA_MAX_UID_LENGTH && DOORSTOP_SESSION_METADATA_UID_PATTERN.test(value);
6653
+ }
6654
+ function parseDoorstopSessionMetadata(value) {
6655
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
6656
+ const record = value;
6657
+ if (record["schema"] !== DOORSTOP_SESSION_METADATA_SCHEMA) return void 0;
6658
+ const op = record["op"];
6659
+ if (typeof op !== "string" || !DOORSTOP_SESSION_METADATA_OPS.has(op)) {
6660
+ return void 0;
6428
6661
  }
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";
6662
+ const uidsValue = record["uids"];
6663
+ if (!Array.isArray(uidsValue) || uidsValue.length === 0 || uidsValue.length > DOORSTOP_SESSION_METADATA_MAX_UIDS) {
6664
+ return void 0;
6665
+ }
6666
+ const uids = [];
6667
+ for (const uid of uidsValue) {
6668
+ if (!isMetadataUid(uid)) return void 0;
6669
+ uids.push(uid);
6438
6670
  }
6671
+ return {
6672
+ schema: DOORSTOP_SESSION_METADATA_SCHEMA,
6673
+ op,
6674
+ uids
6675
+ };
6439
6676
  }
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;
6677
+
6678
+ // src/browser/doorstop-session-labels.ts
6679
+ var DOORSTOP_PANEL_LOCAL_ID = "workspace.doorstop";
6680
+ var DOORSTOP_SESSION_LABEL_ID = "doorstop.review";
6681
+ function createDoorstopSessionLabel(runtimePluginId) {
6682
+ const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID}`;
6683
+ return {
6684
+ id: DOORSTOP_SESSION_LABEL_ID,
6685
+ order: 10,
6686
+ items: (context) => sessionLabelItems(context, panelId)
6687
+ };
6450
6688
  }
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;
6689
+ function sessionLabelItems(context, panelId) {
6690
+ const value = context.session.metadata?.[OPENDOOR_SESSION_METADATA_NAMESPACE];
6691
+ const metadata = parseDoorstopSessionMetadata(value);
6692
+ if (metadata === void 0) return [];
6693
+ const firstUid = metadata.uids[0];
6694
+ if (firstUid === void 0) return [];
6695
+ const rest = metadata.uids.length - 1;
6696
+ const text = rest === 0 ? firstUid : `${firstUid} +${String(rest)}`;
6697
+ const target = doorstopReopenDestination(context.machine.id, context.workspace, panelId, firstUid);
6698
+ return [
6699
+ {
6700
+ type: "render",
6701
+ render: () => b2`<a
6702
+ class="doorstop-session-label"
6703
+ href="#"
6704
+ title=${`Open ${metadata.uids.join(", ")} in Requirements`}
6705
+ @click=${(event) => {
6706
+ event.preventDefault();
6707
+ event.stopPropagation();
6708
+ void context.navigate(target);
6709
+ }}
6710
+ >${text}</a>`
6460
6711
  }
6461
- return true;
6462
- });
6712
+ ];
6713
+ }
6714
+ function doorstopReopenDestination(machineId, workspace, panelId, uid) {
6715
+ return {
6716
+ machineId,
6717
+ projectId: workspace.projectId,
6718
+ workspaceId: workspace.id,
6719
+ view: "workspace",
6720
+ tool: panelId,
6721
+ query: { [DOORSTOP_NAV_UID]: uid }
6722
+ };
6463
6723
  }
6724
+
6725
+ // src/browser/view-model/core.ts
6464
6726
  function shortFingerprint(fingerprint) {
6465
6727
  return fingerprint === null ? "none" : `${fingerprint.slice(0, 8)}\u2026`;
6466
6728
  }
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
6729
  function doorstopPublishTarget(result) {
6494
6730
  return result?.settings?.publishTarget ?? DEFAULT_OPENDOOR_SETTINGS.publishTarget;
6495
6731
  }
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
6732
  function lastRunHasMessage(lastRun) {
6520
6733
  return lastRun.stdout !== "" || lastRun.stderr !== "" || (lastRun.errorMessage ?? "") !== "" || lastRun.commit !== void 0;
6521
6734
  }
@@ -6591,7 +6804,6 @@ function doorstopPublishCommand(result, target = doorstopPublishTarget(result))
6591
6804
  const argument = PUBLISH_TARGET_SAFE_TOKEN.test(target) ? target : quoteShellArgument(target);
6592
6805
  return `doorstop publish all ${argument}`;
6593
6806
  }
6594
- var isValidTargetUid = isValidDoorstopUid;
6595
6807
  function documentConfigFor(index, item) {
6596
6808
  return index.byPrefix.get(item.documentPrefix) ?? {
6597
6809
  directoryPath: "",
@@ -6603,29 +6815,15 @@ function documentConfigFor(index, item) {
6603
6815
  extra: {}
6604
6816
  };
6605
6817
  }
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
6818
  function jsonishText(value) {
6625
6819
  if (value === void 0) return "undefined";
6626
6820
  const json2 = JSON.stringify(value);
6627
6821
  return json2 === void 0 ? String(value) : json2;
6628
6822
  }
6823
+ var DOORSTOP_MARKDOWN_ATTRIBUTES = Object.freeze(["rationale"]);
6824
+ function isMarkdownAttribute(key, value) {
6825
+ return typeof value === "string" && DOORSTOP_MARKDOWN_ATTRIBUTES.includes(key.toLowerCase());
6826
+ }
6629
6827
  function diffValueText(value) {
6630
6828
  return value === void 0 ? "\u2014" : jsonishText(value);
6631
6829
  }
@@ -6637,68 +6835,179 @@ function referenceListText(references) {
6637
6835
  return parts.join(" ");
6638
6836
  }).join(", ");
6639
6837
  }
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
6838
 
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
- }
6839
+ // src/browser/view-model/item-list.ts
6840
+ var EMPTY_WORKSPACE_MESSAGE = "This workspace has no Doorstop documents \u2014 run `doorstop create REQ ./reqs` in the workspace to start a requirements tree.";
6841
+ var ALL_DOCUMENTS_LABEL = "All";
6842
+ var STATE_CHIP_LABELS = {
6843
+ normative: "normative",
6844
+ "non-normative": "non-normative",
6845
+ inactive: "inactive",
6846
+ reviewed: "reviewed",
6847
+ unreviewed: "unreviewed",
6848
+ "suspect-link": "suspect link",
6849
+ "no-child-links": "no child links",
6850
+ "no-links": "no links",
6851
+ "unknown-link": "unknown link",
6852
+ "missing-reference": "missing reference"
6666
6853
  };
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);
6854
+ function stateChipKind(key) {
6855
+ switch (key) {
6856
+ case "normative":
6857
+ case "non-normative":
6858
+ case "reviewed":
6859
+ return "muted";
6860
+ case "unreviewed":
6861
+ case "no-child-links":
6862
+ case "no-links":
6863
+ return "warning";
6864
+ case "inactive":
6865
+ case "suspect-link":
6866
+ case "unknown-link":
6867
+ case "missing-reference":
6868
+ return "danger";
6869
+ }
6870
+ }
6871
+ function dotTitle(dot) {
6872
+ switch (dot) {
6873
+ case "ok":
6874
+ return "all items reviewed, no suspect links";
6875
+ case "unreviewed":
6876
+ return "has unreviewed changes";
6877
+ case "suspect":
6878
+ return "has suspect links";
6879
+ }
6880
+ }
6881
+ function documentStateDots(document2, items) {
6882
+ const own = items.filter((item) => item.documentPrefix === document2.prefix);
6883
+ const dots = [];
6884
+ if (own.length === 0) return dots;
6885
+ const hasSuspect = own.some((item) => item.stateKeys.includes("suspect-link"));
6886
+ const hasUnreviewed = own.some((item) => item.stateKeys.includes("unreviewed"));
6887
+ if (!hasSuspect && !hasUnreviewed) dots.push("ok");
6888
+ if (hasUnreviewed) dots.push("unreviewed");
6889
+ if (hasSuspect) dots.push("suspect");
6890
+ return dots;
6891
+ }
6892
+ function filteredItems(index, documentPrefix, stateFilter, search) {
6893
+ const needle = search.trim().toLowerCase();
6894
+ return index.items.filter((item) => {
6895
+ if (documentPrefix !== void 0 && documentPrefix !== "" && item.documentPrefix !== documentPrefix) {
6896
+ return false;
6681
6897
  }
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)));
6898
+ if (stateFilter !== void 0 && !item.stateKeys.includes(stateFilter)) return false;
6899
+ if (needle !== "" && !`${item.uid} ${item.header ?? ""} ${item.text}`.toLowerCase().includes(needle)) {
6900
+ return false;
6687
6901
  }
6688
- return E;
6902
+ return true;
6903
+ });
6904
+ }
6905
+ function doorstopShowAdditionalAttribute(result) {
6906
+ return result?.settings?.showAdditionalAttribute ?? DEFAULT_OPENDOOR_SETTINGS.showAdditionalAttribute;
6907
+ }
6908
+ function itemExcerpt(item) {
6909
+ const header = item.header;
6910
+ if (header !== void 0 && header !== "") return header;
6911
+ const firstLine = (item.text.split("\n")[0] ?? "").trim();
6912
+ if (firstLine === "") return "\u2014";
6913
+ return firstLine.length > 80 ? `${firstLine.slice(0, 80)}\u2026` : firstLine;
6914
+ }
6915
+ function itemTextRow(item) {
6916
+ return item.text.trim() === "" ? void 0 : item.text;
6917
+ }
6918
+ function shownAttributeRows(item, keys) {
6919
+ const rows = [];
6920
+ for (const key of keys) {
6921
+ if (!Object.prototype.hasOwnProperty.call(item.attributes, key)) continue;
6922
+ rows.push({ key, text: jsonishText(item.attributes[key]) });
6689
6923
  }
6690
- });
6924
+ return rows;
6925
+ }
6926
+
6927
+ // src/browser/view-model/palette.ts
6928
+ function doorstopCommitAfterReview(result) {
6929
+ return result?.settings?.commitAfterReview ?? DEFAULT_OPENDOOR_SETTINGS.commitAfterReview;
6930
+ }
6931
+ var isValidTargetUid = isValidDoorstopUid;
6932
+ function suspectParentItems(item, index) {
6933
+ const suspects = [];
6934
+ for (const link of item.links) {
6935
+ if (link.fingerprint === null) continue;
6936
+ const target = index.byUid.get(link.uid);
6937
+ if (target === void 0) continue;
6938
+ if (link.fingerprint !== computeItemStamp(target, documentConfigFor(index, target), false)) {
6939
+ suspects.push(target);
6940
+ }
6941
+ }
6942
+ return suspects;
6943
+ }
6944
+ function firstChildDocumentPrefix(index, item) {
6945
+ for (const document2 of index.documents) {
6946
+ if (document2.parentPrefix === item.documentPrefix) return document2.prefix;
6947
+ }
6948
+ return void 0;
6949
+ }
6950
+
6951
+ // src/browser/view-model/status-bar.ts
6952
+ function commitOutcomeText(op, outcome) {
6953
+ const gitRun = op === "git-stage" || op === "git-commit" || op === "git-unstage";
6954
+ switch (outcome.status) {
6955
+ case "staged":
6956
+ return `staged ${String(outcome.staged ?? 0)} paths`;
6957
+ case "unstaged":
6958
+ return `unstaged ${String(outcome.unstaged ?? 0)} paths`;
6959
+ case "committed":
6960
+ return gitRun ? `committed ${outcome.sha ?? "<unknown sha>"}` : `commit: ${outcome.sha ?? "<unknown sha>"}`;
6961
+ case "clean":
6962
+ if (!gitRun) return "commit: clean (already committed)";
6963
+ if (op === "git-stage") return "clean \u2014 nothing to stage";
6964
+ if (op === "git-unstage") return "clean \u2014 nothing to unstage";
6965
+ return "clean \u2014 nothing staged";
6966
+ case "skipped":
6967
+ return gitRun ? "skipped" : "commit: skipped (not a git repository | review failed | deadline)";
6968
+ case "failed":
6969
+ return gitRun ? `failed \u2014 ${outcome.stderr ?? "git step errored"}` : `commit: failed \u2014 ${outcome.stderr ?? "git step errored"}`;
6970
+ }
6971
+ }
6972
+
6973
+ // src/browser/view-model/findings.ts
6974
+ var FINDINGS_EMPTY_MESSAGE = "No findings \u2014 the tree is clean.";
6975
+ var FINDINGS_EMPTY_HINT = "Findings are plugin-local; run `doorstop` validation in the terminal for the authoritative check.";
6976
+ var FINDINGS_PLUGIN_LOCAL_NOTE = "plugin-local findings \u2014 `doorstop` validation in the terminal is authoritative";
6977
+ var FINDING_SEVERITY_RANK = {
6978
+ error: 0,
6979
+ warning: 1,
6980
+ info: 2
6981
+ };
6982
+ function findingsViewRows(index) {
6983
+ const rows = [...index.findings, ...index.diagnostics];
6984
+ return rows.sort((a3, b3) => FINDING_SEVERITY_RANK[a3.severity] - FINDING_SEVERITY_RANK[b3.severity]);
6985
+ }
6986
+ function findingsViewCounts(rows) {
6987
+ const counts = { errors: 0, warnings: 0, info: 0 };
6988
+ for (const row of rows) {
6989
+ if (row.severity === "error") counts.errors += 1;
6990
+ else if (row.severity === "warning") counts.warnings += 1;
6991
+ else counts.info += 1;
6992
+ }
6993
+ return counts;
6994
+ }
6995
+ function findingsCountText(counts) {
6996
+ const errors = counts.errors === 1 ? "1 error" : `${String(counts.errors)} errors`;
6997
+ const warnings = counts.warnings === 1 ? "1 warning" : `${String(counts.warnings)} warnings`;
6998
+ return `${errors} \xB7 ${warnings} \xB7 ${String(counts.info)} info`;
6999
+ }
6691
7000
 
6692
7001
  // node_modules/lit-html/directive-helpers.js
6693
- var { I: t4 } = j;
6694
- var i6 = (o8) => o8;
7002
+ var { I: t3 } = j;
7003
+ var i5 = (o8) => o8;
6695
7004
  var r4 = (o8) => void 0 === o8.strings;
6696
7005
  var s4 = () => document.createComment("");
6697
7006
  var v2 = (o8, n7, e8) => {
6698
7007
  const l3 = o8._$AA.parentNode, d3 = void 0 === n7 ? o8._$AB : n7._$AA;
6699
7008
  if (void 0 === e8) {
6700
7009
  const i8 = l3.insertBefore(s4(), d3), n8 = l3.insertBefore(s4(), d3);
6701
- e8 = new t4(i8, n8, o8, o8.options);
7010
+ e8 = new t3(i8, n8, o8, o8.options);
6702
7011
  } else {
6703
7012
  const t5 = e8._$AB.nextSibling, n8 = e8._$AM, c6 = n8 !== o8;
6704
7013
  if (c6) {
@@ -6708,8 +7017,8 @@ var v2 = (o8, n7, e8) => {
6708
7017
  if (t5 !== d3 || c6) {
6709
7018
  let o9 = e8._$AA;
6710
7019
  for (; o9 !== t5; ) {
6711
- const t6 = i6(o9).nextSibling;
6712
- i6(l3).insertBefore(o9, d3), o9 = t6;
7020
+ const t6 = i5(o9).nextSibling;
7021
+ i5(l3).insertBefore(o9, d3), o9 = t6;
6713
7022
  }
6714
7023
  }
6715
7024
  }
@@ -6723,18 +7032,25 @@ var h3 = (o8) => {
6723
7032
  o8._$AR(), o8._$AA.remove();
6724
7033
  };
6725
7034
 
6726
- // node_modules/lit-html/directives/keyed.js
6727
- var i7 = e4(class extends i5 {
6728
- constructor() {
6729
- super(...arguments), this.key = A;
7035
+ // node_modules/lit-html/directive.js
7036
+ var t4 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 };
7037
+ var e4 = (t5) => (...e8) => ({ _$litDirective$: t5, values: e8 });
7038
+ var i6 = class {
7039
+ constructor(t5) {
6730
7040
  }
6731
- render(r8, t5) {
6732
- return this.key = r8, t5;
7041
+ get _$AU() {
7042
+ return this._$AM._$AU;
6733
7043
  }
6734
- update(r8, [t5, e8]) {
6735
- return t5 !== this.key && (p3(r8), this.key = t5), e8;
7044
+ _$AT(t5, e8, i8) {
7045
+ this._$Ct = t5, this._$AM = e8, this._$Ci = i8;
6736
7046
  }
6737
- });
7047
+ _$AS(t5, e8) {
7048
+ return this.update(t5, e8);
7049
+ }
7050
+ update(t5, e8) {
7051
+ return this.render(...e8);
7052
+ }
7053
+ };
6738
7054
 
6739
7055
  // node_modules/lit-html/async-directive.js
6740
7056
  var s5 = (i8, t5) => {
@@ -6768,9 +7084,9 @@ function n4(i8, t5 = false, e8 = 0) {
6768
7084
  else s5(this, i8);
6769
7085
  }
6770
7086
  var c4 = (i8) => {
6771
- i8.type == t3.CHILD && (i8._$AP ??= n4, i8._$AQ ??= h4);
7087
+ i8.type == t4.CHILD && (i8._$AP ??= n4, i8._$AQ ??= h4);
6772
7088
  };
6773
- var f3 = class extends i5 {
7089
+ var f3 = class extends i6 {
6774
7090
  constructor() {
6775
7091
  super(...arguments), this._$AN = void 0;
6776
7092
  }
@@ -6794,7 +7110,7 @@ var f3 = class extends i5 {
6794
7110
  };
6795
7111
 
6796
7112
  // node_modules/lit-html/directives/ref.js
6797
- var e6 = () => new h5();
7113
+ var e5 = () => new h5();
6798
7114
  var h5 = class {
6799
7115
  };
6800
7116
  var o6 = /* @__PURE__ */ new WeakMap();
@@ -6824,59 +7140,6 @@ var n5 = e4(class extends f3 {
6824
7140
  }
6825
7141
  });
6826
7142
 
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
7143
  // node_modules/@lit/reactive-element/decorators/property.js
6881
7144
  var o7 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f };
6882
7145
  var r6 = (t5 = o7, e8, r8) => {
@@ -6912,8 +7175,8 @@ function r7(r8) {
6912
7175
  return n6({ ...r8, state: true, attribute: false });
6913
7176
  }
6914
7177
 
6915
- // src/doorstop-panel-styles.ts
6916
- var panelStyles = i`
7178
+ // src/browser/styles/base.ts
7179
+ var baseStyles = i`
6917
7180
  :host {
6918
7181
  flex: 1 1 auto;
6919
7182
  min-height: 0;
@@ -6922,7 +7185,7 @@ var panelStyles = i`
6922
7185
  overflow: hidden;
6923
7186
  color: var(--pi-text);
6924
7187
  background: var(--pi-bg);
6925
- font: 13px system-ui, sans-serif;
7188
+ font: 14px system-ui, sans-serif;
6926
7189
  }
6927
7190
 
6928
7191
  button {
@@ -6948,7 +7211,7 @@ var panelStyles = i`
6948
7211
  border-radius: 6px;
6949
7212
  background: var(--pi-surface);
6950
7213
  color: var(--pi-text);
6951
- font-size: 12px;
7214
+ font-size: 13px;
6952
7215
  padding: 4px 6px;
6953
7216
  }
6954
7217
 
@@ -6957,13 +7220,16 @@ var panelStyles = i`
6957
7220
  border-radius: 5px;
6958
7221
  background: var(--pi-bg);
6959
7222
  padding: 1px 4px;
6960
- font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
7223
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
6961
7224
  }
6962
7225
 
6963
7226
  .doorstop-muted {
6964
7227
  color: var(--pi-muted);
6965
7228
  }
7229
+ `;
6966
7230
 
7231
+ // src/browser/styles/project-actions.ts
7232
+ var projectActionsStyles = i`
6967
7233
  /* --- project actions (region 1) --- */
6968
7234
  .doorstop-project-actions {
6969
7235
  flex: 0 0 auto;
@@ -7002,7 +7268,7 @@ var panelStyles = i`
7002
7268
  .doorstop-doc-chip {
7003
7269
  border-radius: 999px;
7004
7270
  padding: 2px 9px;
7005
- font-size: 12px;
7271
+ font-size: 13px;
7006
7272
  }
7007
7273
 
7008
7274
  .doorstop-doc-chip.is-selected {
@@ -7013,12 +7279,12 @@ var panelStyles = i`
7013
7279
 
7014
7280
  .doorstop-doc-arrow {
7015
7281
  color: var(--pi-muted);
7016
- font-size: 11px;
7282
+ font-size: 12px;
7017
7283
  }
7018
7284
 
7019
7285
  .doorstop-doc-count {
7020
7286
  color: var(--pi-muted);
7021
- font-size: 11px;
7287
+ font-size: 12px;
7022
7288
  }
7023
7289
 
7024
7290
  .doorstop-dot {
@@ -7053,7 +7319,7 @@ var panelStyles = i`
7053
7319
  border-radius: 999px;
7054
7320
  color: var(--pi-warning);
7055
7321
  padding: 1px 8px;
7056
- font-size: 12px;
7322
+ font-size: 13px;
7057
7323
  }
7058
7324
 
7059
7325
  /* --- project-scoped git actions (plan-add-git-actions Phase D step 17) --- */
@@ -7076,7 +7342,7 @@ var panelStyles = i`
7076
7342
  background: transparent;
7077
7343
  color: var(--pi-muted);
7078
7344
  padding: 2px 9px;
7079
- font-size: 12px;
7345
+ font-size: 13px;
7080
7346
  }
7081
7347
 
7082
7348
  .doorstop-git-status-text {
@@ -7132,7 +7398,7 @@ var panelStyles = i`
7132
7398
  background: transparent;
7133
7399
  color: var(--pi-muted);
7134
7400
  padding: 3px 10px;
7135
- font-size: 12px;
7401
+ font-size: 13px;
7136
7402
  }
7137
7403
 
7138
7404
  .doorstop-view-tab + .doorstop-view-tab {
@@ -7143,7 +7409,10 @@ var panelStyles = i`
7143
7409
  background: var(--pi-selection-bg);
7144
7410
  color: var(--pi-accent);
7145
7411
  }
7412
+ `;
7146
7413
 
7414
+ // src/browser/styles/item-list.ts
7415
+ var itemListStyles = i`
7147
7416
  /* --- viewer (regions 2 + 3; the filters row and split stack) --- */
7148
7417
  .doorstop-viewer {
7149
7418
  flex: 1 1 auto;
@@ -7260,7 +7529,7 @@ var panelStyles = i`
7260
7529
  .doorstop-item-row {
7261
7530
  display: grid;
7262
7531
  grid-template-columns: max-content max-content minmax(0, 1fr) max-content;
7263
- gap: 8px;
7532
+ gap: 2px 8px;
7264
7533
  align-items: baseline;
7265
7534
  width: 100%;
7266
7535
  border: 0;
@@ -7277,7 +7546,7 @@ var panelStyles = i`
7277
7546
 
7278
7547
  .doorstop-item-level {
7279
7548
  color: var(--pi-muted);
7280
- font-size: 11px;
7549
+ font-size: 12px;
7281
7550
  }
7282
7551
 
7283
7552
  .doorstop-item-summary {
@@ -7288,6 +7557,49 @@ var panelStyles = i`
7288
7557
  color: var(--pi-text-secondary);
7289
7558
  }
7290
7559
 
7560
+ /* A fifth column exists only when showAdditionalAttribute configures
7561
+ at least one name; otherwise the default rows keep the original
7562
+ 4-column track (and its single 8px summary→chips gap). */
7563
+ .doorstop-items.has-item-attrs .doorstop-item-row {
7564
+ grid-template-columns: max-content max-content minmax(0, 1fr) minmax(0, max-content) max-content;
7565
+ }
7566
+
7567
+ /* The configured key: value attribute pairs (settings field
7568
+ showAdditionalAttribute). The column collapses to zero when the row
7569
+ renders nothing here (no configured attribute the item has). The
7570
+ max-width bounds a pathologically long value so it cannot starve
7571
+ the summary's fr track before the ellipsis kicks in. */
7572
+ .doorstop-item-attrs {
7573
+ min-width: 0;
7574
+ max-width: 40ch;
7575
+ overflow: hidden;
7576
+ text-overflow: ellipsis;
7577
+ white-space: nowrap;
7578
+ color: var(--pi-muted);
7579
+ font-size: 12px;
7580
+ }
7581
+
7582
+ /* The item body ("text" configured in showAdditionalAttribute) on its own
7583
+ indented second line below level/uid/summary/chips. Full-width implicit
7584
+ grid row; 5ch inline indent; clamped to 2 lines with ellipsis — the full
7585
+ body lives in the detail pane. The span only exists when rendered, so no
7586
+ container modifier class is needed. */
7587
+ .doorstop-item-text {
7588
+ grid-column: 1 / -1;
7589
+ align-self: start;
7590
+ padding-inline-start: 5ch;
7591
+ min-width: 0;
7592
+ color: var(--pi-muted);
7593
+ /* Body-like content: reads one tier above the 12px metadata. */
7594
+ font-size: 13px;
7595
+ white-space: pre-wrap;
7596
+ overflow-wrap: anywhere;
7597
+ display: -webkit-box;
7598
+ -webkit-box-orient: vertical;
7599
+ -webkit-line-clamp: 2;
7600
+ overflow: hidden;
7601
+ }
7602
+
7291
7603
  .doorstop-item-chips {
7292
7604
  display: flex;
7293
7605
  flex-wrap: wrap;
@@ -7301,8 +7613,14 @@ var panelStyles = i`
7301
7613
  keyboard path is the region-4 Stage/Unstage buttons. The same class
7302
7614
  hosts both directions: the plus (git add) icon and the unstage
7303
7615
  minus (git reset) icon, chosen by {@link itemUnstageable} /
7304
- {@link itemStageable} precedence. */
7305
- .doorstop-item-add {
7616
+ {@link itemStageable} precedence.
7617
+
7618
+ The per-row "insert UID into the agent prompt" arrow shares the
7619
+ pointer-only <span> contract and this visual box; it keeps a
7620
+ distinct class because the +/reset icon and the arrow stay
7621
+ independently evolvable. */
7622
+ .doorstop-item-add,
7623
+ .doorstop-item-insert {
7306
7624
  display: inline-flex;
7307
7625
  align-items: center;
7308
7626
  align-self: center;
@@ -7313,7 +7631,8 @@ var panelStyles = i`
7313
7631
  cursor: pointer;
7314
7632
  }
7315
7633
 
7316
- .doorstop-item-add:hover {
7634
+ .doorstop-item-add:hover,
7635
+ .doorstop-item-insert:hover {
7317
7636
  color: var(--pi-text);
7318
7637
  }
7319
7638
 
@@ -7321,6 +7640,7 @@ var panelStyles = i`
7321
7640
  .doorstop-chip {
7322
7641
  border-radius: 999px;
7323
7642
  padding: 0 7px;
7643
+ /* Strict parity: pi-web keeps compact chips at 11px. */
7324
7644
  font-size: 11px;
7325
7645
  white-space: nowrap;
7326
7646
  }
@@ -7344,16 +7664,132 @@ var panelStyles = i`
7344
7664
  border: 1px solid var(--pi-success-border);
7345
7665
  color: var(--pi-success);
7346
7666
  }
7347
-
7348
- /* --- detail pane (region 3) --- */
7349
- .doorstop-detail {
7350
- padding: 12px;
7351
- display: grid;
7352
- gap: 10px;
7353
- align-content: start;
7354
- }
7355
-
7356
- .doorstop-detail-head {
7667
+ `;
7668
+ var findingsStyles = i`
7669
+ /* --- findings view (spec §7.2) --- */
7670
+ .doorstop-findings-view {
7671
+ flex: 1 1 auto;
7672
+ min-height: 0;
7673
+ overflow: auto;
7674
+ padding: 10px 12px;
7675
+ display: grid;
7676
+ gap: 8px;
7677
+ align-content: start;
7678
+ }
7679
+
7680
+ .doorstop-findings-head {
7681
+ display: flex;
7682
+ flex-wrap: wrap;
7683
+ align-items: baseline;
7684
+ gap: 6px 10px;
7685
+ }
7686
+
7687
+ .doorstop-findings-counts {
7688
+ font-size: 13px;
7689
+ font-weight: 600;
7690
+ }
7691
+
7692
+ .doorstop-findings-note {
7693
+ font-size: 12px;
7694
+ }
7695
+
7696
+ .doorstop-findings-list {
7697
+ display: grid;
7698
+ gap: 3px;
7699
+ }
7700
+
7701
+ .doorstop-finding-row {
7702
+ display: flex;
7703
+ flex-wrap: wrap;
7704
+ align-items: baseline;
7705
+ gap: 6px;
7706
+ border-radius: 6px;
7707
+ padding: 5px 8px;
7708
+ }
7709
+
7710
+ .doorstop-finding-row.doorstop-error {
7711
+ border: 1px solid var(--pi-danger);
7712
+ background: color-mix(in srgb, var(--pi-danger) 9%, transparent);
7713
+ }
7714
+
7715
+ .doorstop-finding-row.doorstop-warning {
7716
+ border: 1px solid var(--pi-warning-border);
7717
+ background: color-mix(in srgb, var(--pi-warning) 9%, transparent);
7718
+ }
7719
+
7720
+ .doorstop-finding-row.doorstop-info {
7721
+ border: 1px solid var(--pi-border-muted);
7722
+ color: var(--pi-muted);
7723
+ }
7724
+
7725
+ .doorstop-finding-row .doorstop-severity {
7726
+ background: var(--pi-border-muted);
7727
+ color: var(--pi-muted);
7728
+ }
7729
+
7730
+ .doorstop-finding-row.doorstop-error .doorstop-severity {
7731
+ background: var(--pi-danger);
7732
+ color: var(--pi-bg);
7733
+ }
7734
+
7735
+ .doorstop-finding-row.doorstop-warning .doorstop-severity {
7736
+ background: var(--pi-warning);
7737
+ color: var(--pi-bg);
7738
+ }
7739
+
7740
+ .doorstop-finding-uid {
7741
+ border: 0;
7742
+ border-radius: 5px;
7743
+ background: transparent;
7744
+ color: inherit;
7745
+ padding: 0;
7746
+ font-weight: 600;
7747
+ }
7748
+
7749
+ .doorstop-finding-uid:hover {
7750
+ text-decoration: underline;
7751
+ cursor: pointer;
7752
+ }
7753
+
7754
+ .doorstop-finding-message {
7755
+ min-width: 0;
7756
+ overflow-wrap: anywhere;
7757
+ }
7758
+
7759
+ /* --- empty states --- */
7760
+ .doorstop-empty {
7761
+ margin: 10px 12px;
7762
+ border: 1px dashed var(--pi-border-muted);
7763
+ border-radius: 8px;
7764
+ color: var(--pi-muted);
7765
+ padding: 12px;
7766
+ overflow: auto;
7767
+ }
7768
+
7769
+ .doorstop-empty p {
7770
+ margin: 0;
7771
+ }
7772
+
7773
+ .doorstop-empty button {
7774
+ margin-top: 10px;
7775
+ }
7776
+
7777
+ .doorstop-empty .doorstop-muted {
7778
+ margin-top: 6px;
7779
+ }
7780
+ `;
7781
+
7782
+ // src/browser/styles/detail-pane.ts
7783
+ var detailPaneStyles = i`
7784
+ /* --- detail pane (region 3) --- */
7785
+ .doorstop-detail {
7786
+ padding: 12px;
7787
+ display: grid;
7788
+ gap: 10px;
7789
+ align-content: start;
7790
+ }
7791
+
7792
+ .doorstop-detail-head {
7357
7793
  display: flex;
7358
7794
  flex-wrap: wrap;
7359
7795
  align-items: baseline;
@@ -7362,14 +7798,14 @@ var panelStyles = i`
7362
7798
 
7363
7799
  .doorstop-detail-head h3 {
7364
7800
  margin: 0;
7365
- font-size: 15px;
7801
+ font-size: 16px;
7366
7802
  min-width: 0;
7367
7803
  overflow-wrap: anywhere;
7368
7804
  }
7369
7805
 
7370
7806
  .doorstop-level {
7371
7807
  color: var(--pi-muted);
7372
- font-size: 12px;
7808
+ font-size: 13px;
7373
7809
  }
7374
7810
 
7375
7811
  .doorstop-flags {
@@ -7383,7 +7819,7 @@ var panelStyles = i`
7383
7819
  border-radius: 999px;
7384
7820
  color: var(--pi-muted);
7385
7821
  padding: 0 7px;
7386
- font-size: 11px;
7822
+ font-size: 12px;
7387
7823
  }
7388
7824
 
7389
7825
  .doorstop-flag.is-inactive {
@@ -7406,7 +7842,7 @@ var panelStyles = i`
7406
7842
 
7407
7843
  .doorstop-changes-summary {
7408
7844
  color: var(--pi-muted);
7409
- font-size: 11px;
7845
+ font-size: 12px;
7410
7846
  text-transform: uppercase;
7411
7847
  letter-spacing: 0.03em;
7412
7848
  cursor: pointer;
@@ -7419,12 +7855,12 @@ var panelStyles = i`
7419
7855
 
7420
7856
  .doorstop-changes-notice {
7421
7857
  margin: 6px 0 0;
7422
- font-size: 12px;
7858
+ font-size: 13px;
7423
7859
  }
7424
7860
 
7425
7861
  .doorstop-changes-source {
7426
7862
  margin: 6px 0 4px;
7427
- font-size: 11px;
7863
+ font-size: 12px;
7428
7864
  }
7429
7865
 
7430
7866
  .doorstop-diff-lines {
@@ -7494,7 +7930,7 @@ var panelStyles = i`
7494
7930
 
7495
7931
  .doorstop-field-name {
7496
7932
  color: var(--pi-muted);
7497
- font-size: 11px;
7933
+ font-size: 12px;
7498
7934
  text-transform: uppercase;
7499
7935
  letter-spacing: 0.03em;
7500
7936
  }
@@ -7502,7 +7938,7 @@ var panelStyles = i`
7502
7938
  .doorstop-field-chip {
7503
7939
  border-radius: 5px;
7504
7940
  border: 1px solid var(--pi-border-muted);
7505
- font-size: 12px;
7941
+ font-size: 13px;
7506
7942
  padding: 1px 6px;
7507
7943
  overflow-wrap: anywhere;
7508
7944
  }
@@ -7525,7 +7961,7 @@ var panelStyles = i`
7525
7961
 
7526
7962
  .doorstop-section-title {
7527
7963
  color: var(--pi-muted);
7528
- font-size: 11px;
7964
+ font-size: 12px;
7529
7965
  text-transform: uppercase;
7530
7966
  letter-spacing: 0.03em;
7531
7967
  margin: 0;
@@ -7546,6 +7982,107 @@ var panelStyles = i`
7546
7982
  font-style: italic;
7547
7983
  }
7548
7984
 
7985
+ /* Plan §2.1: the item body is host-rendered Markdown. The host renderer
7986
+ * mounts its nodes in THIS shadow root (the host's
7987
+ * pi-web-content-rendering-boundary element deliberately has no shadow
7988
+ * root of its own — createRenderRoot returns the element), so the prose
7989
+ * rules below reach them. The plain-text pre-wrap on .doorstop-text is
7990
+ * not appropriate inside Markdown blocks; the boundary's own source
7991
+ * fallback sets its own pre-wrap inline.
7992
+ *
7993
+ * This element does NOT inherit host typography: the boundary only
7994
+ * avoids adding a shadow root, it does not import host theme CSS into
7995
+ * ours, so every rule the document needs is restated here. */
7996
+ .doorstop-text-markdown {
7997
+ white-space: normal;
7998
+ }
7999
+
8000
+ /* Edge trimming must reach the BLOCKS, not the wrappers: the boundary
8001
+ * element (display: contents) and its .content-rendering-body wrapper
8002
+ * sit between this element and the rendered blocks, so a direct-child
8003
+ * (> :first-child) selector only ever matched the boundary itself. No
8004
+ * matching :last-child rule: the body keeps the source-fallback span
8005
+ * AFTER the content wrapper, so the last block is never a last-child;
8006
+ * its trailing 8px margin is invisible inside the bordered box. */
8007
+ .doorstop-text-markdown :is(p, ul, ol, pre, blockquote, .table-scroll, h1, h2, h3, h4, h5, h6):first-child {
8008
+ margin-top: 0;
8009
+ }
8010
+
8011
+ .doorstop-text-markdown :is(p, ul, ol, blockquote, .table-scroll) {
8012
+ margin: 0 0 8px;
8013
+ }
8014
+
8015
+ .doorstop-text-markdown :is(ul, ol) {
8016
+ padding-left: 20px;
8017
+ }
8018
+
8019
+ .doorstop-text-markdown :is(h1, h2, h3, h4, h5, h6) {
8020
+ margin: 12px 0 6px;
8021
+ font-size: 14px;
8022
+ font-weight: 600;
8023
+ }
8024
+
8025
+ .doorstop-text-markdown code {
8026
+ border: 1px solid var(--pi-border-muted);
8027
+ border-radius: 4px;
8028
+ background: var(--pi-bg);
8029
+ padding: 0 3px;
8030
+ font-size: 12px;
8031
+ }
8032
+
8033
+ .doorstop-text-markdown pre {
8034
+ border: 1px solid var(--pi-border-muted);
8035
+ border-radius: 6px;
8036
+ padding: 8px;
8037
+ overflow-x: auto;
8038
+ }
8039
+
8040
+ .doorstop-text-markdown pre code {
8041
+ border: 0;
8042
+ background: transparent;
8043
+ padding: 0;
8044
+ }
8045
+
8046
+ .doorstop-text-markdown blockquote {
8047
+ border-left: 3px solid var(--pi-border-muted);
8048
+ padding-left: 8px;
8049
+ color: var(--pi-muted);
8050
+ }
8051
+
8052
+ /* The host wraps every table in a div.table-scroll role=region
8053
+ * tabindex=0 and styles it ONLY from host theme CSS
8054
+ * (pi-web base/formattedText.css), which does not reach this
8055
+ * unregistered shadow root. Without these two rules a wide table is
8056
+ * clipped by the pane and its focusable region is unusable, so the
8057
+ * panel restates the host's rules (pi-web 1.202611.1).
8058
+ *
8059
+ * overflow-wrap: normal undoes the anywhere inherited from
8060
+ * .doorstop-text: harmless (host-identical) in prose, but inside table
8061
+ * cells it would let every column collapse to min-content instead of
8062
+ * making the table overflow into its scroll region. */
8063
+ .doorstop-text-markdown .table-scroll {
8064
+ max-width: 100%;
8065
+ overflow-x: auto;
8066
+ overflow-y: hidden;
8067
+ overflow-wrap: normal;
8068
+ }
8069
+
8070
+ .doorstop-text-markdown .table-scroll:focus-visible {
8071
+ outline: 1px solid var(--pi-accent);
8072
+ outline-offset: 2px;
8073
+ }
8074
+
8075
+ .doorstop-text-markdown table {
8076
+ border-collapse: collapse;
8077
+ margin: 0;
8078
+ }
8079
+
8080
+ .doorstop-text-markdown :is(th, td) {
8081
+ border: 1px solid var(--pi-border-muted);
8082
+ padding: 2px 6px;
8083
+ text-align: left;
8084
+ }
8085
+
7549
8086
  .doorstop-links {
7550
8087
  display: grid;
7551
8088
  gap: 4px;
@@ -7586,7 +8123,7 @@ var panelStyles = i`
7586
8123
 
7587
8124
  .doorstop-fingerprint {
7588
8125
  color: var(--pi-muted);
7589
- font-size: 11px;
8126
+ font-size: 12px;
7590
8127
  /* The link row wraps, which could push the trash can onto a second
7591
8128
  line on narrow panes. Let the fingerprint shrink and ellipsize
7592
8129
  instead (min-width: 0 lifts the flex min-content floor). */
@@ -7611,7 +8148,7 @@ var panelStyles = i`
7611
8148
 
7612
8149
  .doorstop-attribute dt {
7613
8150
  color: var(--pi-muted);
7614
- font-size: 12px;
8151
+ font-size: 13px;
7615
8152
  }
7616
8153
 
7617
8154
  .doorstop-attribute dd {
@@ -7620,6 +8157,28 @@ var panelStyles = i`
7620
8157
  overflow-wrap: anywhere;
7621
8158
  }
7622
8159
 
8160
+ /* Plan plan-attribute-markdown D5: an allowlisted prose attribute
8161
+ * (initially "rationale") is host-rendered Markdown, so its row uses a
8162
+ * STACKED variant — the label above a full-width block — instead of
8163
+ * the two-column key|value grid sized for single-line <code> values.
8164
+ * The dt keeps the existing small muted label rule above.
8165
+ *
8166
+ * Nothing here restates the §2.1 prose rules: the rendered container
8167
+ * carries the shared .doorstop-text-markdown class, so block
8168
+ * margins, code/pre, blockquote and the 9bc9cc9 .table-scroll fixes
8169
+ * apply automatically. */
8170
+ .doorstop-attribute.doorstop-attribute-markdown {
8171
+ grid-template-columns: minmax(0, 1fr);
8172
+ gap: 3px;
8173
+ }
8174
+
8175
+ /* The escaped-text fallback (no host renderer) is prose too: keep its
8176
+ * line breaks. This only reaches the fallback — the markdown container
8177
+ * inside resets white-space to normal (.doorstop-text-markdown). */
8178
+ .doorstop-attribute-markdown dd {
8179
+ white-space: pre-wrap;
8180
+ }
8181
+
7623
8182
  .doorstop-findings {
7624
8183
  display: grid;
7625
8184
  gap: 4px;
@@ -7652,10 +8211,14 @@ var panelStyles = i`
7652
8211
 
7653
8212
  .doorstop-finding-severity {
7654
8213
  flex: 0 0 auto;
8214
+ /* Strict parity: pi-web keeps compact chips at 11px. */
7655
8215
  font-size: 11px;
7656
8216
  font-weight: 600;
7657
8217
  }
8218
+ `;
7658
8219
 
8220
+ // src/browser/styles/item-action-palette.ts
8221
+ var itemActionPaletteStyles = i`
7659
8222
  /* --- item action palette (region 4), sized to the prompt footer --- */
7660
8223
  .doorstop-action-palette {
7661
8224
  flex: 0 0 auto;
@@ -7701,7 +8264,7 @@ var panelStyles = i`
7701
8264
 
7702
8265
  .doorstop-op-error {
7703
8266
  color: var(--pi-danger);
7704
- font-size: 12px;
8267
+ font-size: 13px;
7705
8268
  }
7706
8269
 
7707
8270
  .doorstop-menu {
@@ -7734,174 +8297,70 @@ var panelStyles = i`
7734
8297
  .doorstop-menu-item:hover:not(:disabled) {
7735
8298
  background: var(--pi-selection-bg);
7736
8299
  }
8300
+ `;
7737
8301
 
7738
- /* --- findings view (spec §7.2) --- */
7739
- .doorstop-findings-view {
7740
- flex: 1 1 auto;
7741
- min-height: 0;
7742
- overflow: auto;
7743
- padding: 10px 12px;
7744
- display: grid;
7745
- gap: 8px;
7746
- align-content: start;
8302
+ // src/browser/styles/status-bar.ts
8303
+ var statusBarStyles = i`
8304
+ /* --- status bar (region 5), sized to the host center status bar --- */
8305
+ .doorstop-status-bar {
8306
+ flex: 0 0 auto;
8307
+ display: flex;
8308
+ flex-direction: column;
8309
+ min-width: 0;
8310
+ /* Cap the whole bar at 38% of the panel (the pre-improvement cap),
8311
+ not 38vh: on a short viewport with a tall side panel a viewport
8312
+ unit can dwarf the panel. The cap cannot live on the body div
8313
+ (.doorstop-last-run) — its containing block (this bar) is
8314
+ content-sized, so a percentage max-height there would be treated
8315
+ as none — so the bar caps itself and the body shrinks (and
8316
+ scrolls) below it. */
8317
+ max-height: 38%;
8318
+ border-top: 1px solid var(--pi-border);
8319
+ background: var(--pi-bg);
8320
+ color: var(--pi-muted);
8321
+ font: 13px system-ui, sans-serif;
7747
8322
  }
7748
8323
 
7749
- .doorstop-findings-head {
8324
+ .doorstop-status-bar-row {
8325
+ flex: 0 0 auto; /* the status row never collapses under the body cap */
7750
8326
  display: flex;
7751
- flex-wrap: wrap;
7752
- align-items: baseline;
7753
- gap: 6px 10px;
8327
+ align-items: center;
8328
+ gap: 8px;
8329
+ min-width: 0;
8330
+ padding: 7px 12px;
8331
+ overflow: hidden;
8332
+ white-space: nowrap;
7754
8333
  }
7755
8334
 
7756
- .doorstop-findings-counts {
7757
- font-size: 12px;
7758
- font-weight: 600;
8335
+ /* The status bar's expanded output body — no card chrome. Shrinks
8336
+ (and scrolls) when the bar hits its 38% cap; content-sized
8337
+ otherwise. */
8338
+ .doorstop-last-run {
8339
+ flex: 1 1 auto;
8340
+ min-height: 0;
8341
+ overflow: auto;
8342
+ padding: 8px 12px 10px;
8343
+ border-top: 1px solid var(--pi-border-muted);
7759
8344
  }
7760
8345
 
7761
- .doorstop-findings-note {
8346
+ .doorstop-last-run-commit {
8347
+ margin: 0 0 6px;
8348
+ font-size: 13px;
8349
+ overflow-wrap: anywhere;
8350
+ }
8351
+
8352
+ .doorstop-last-run-status {
8353
+ flex: 0 0 auto; /* the badge label keeps its full width */
8354
+ border-radius: 999px;
8355
+ padding: 0 7px;
8356
+ /* Strict parity: pi-web keeps compact chips at 11px. */
7762
8357
  font-size: 11px;
8358
+ white-space: nowrap;
7763
8359
  }
7764
8360
 
7765
- .doorstop-findings-list {
7766
- display: grid;
7767
- gap: 3px;
7768
- }
7769
-
7770
- .doorstop-finding-row {
7771
- display: flex;
7772
- flex-wrap: wrap;
7773
- align-items: baseline;
7774
- gap: 6px;
7775
- border-radius: 6px;
7776
- padding: 5px 8px;
7777
- }
7778
-
7779
- .doorstop-finding-row.doorstop-error {
7780
- border: 1px solid var(--pi-danger);
7781
- background: color-mix(in srgb, var(--pi-danger) 9%, transparent);
7782
- }
7783
-
7784
- .doorstop-finding-row.doorstop-warning {
7785
- border: 1px solid var(--pi-warning-border);
7786
- background: color-mix(in srgb, var(--pi-warning) 9%, transparent);
7787
- }
7788
-
7789
- .doorstop-finding-row.doorstop-info {
7790
- border: 1px solid var(--pi-border-muted);
7791
- color: var(--pi-muted);
7792
- }
7793
-
7794
- .doorstop-finding-row .doorstop-severity {
7795
- background: var(--pi-border-muted);
7796
- color: var(--pi-muted);
7797
- }
7798
-
7799
- .doorstop-finding-row.doorstop-error .doorstop-severity {
7800
- background: var(--pi-danger);
7801
- color: var(--pi-bg);
7802
- }
7803
-
7804
- .doorstop-finding-row.doorstop-warning .doorstop-severity {
7805
- background: var(--pi-warning);
7806
- color: var(--pi-bg);
7807
- }
7808
-
7809
- .doorstop-finding-uid {
7810
- border: 0;
7811
- border-radius: 5px;
7812
- background: transparent;
7813
- color: inherit;
7814
- padding: 0;
7815
- font-weight: 600;
7816
- }
7817
-
7818
- .doorstop-finding-uid:hover {
7819
- text-decoration: underline;
7820
- cursor: pointer;
7821
- }
7822
-
7823
- .doorstop-finding-message {
7824
- min-width: 0;
7825
- overflow-wrap: anywhere;
7826
- }
7827
-
7828
- /* --- empty states --- */
7829
- .doorstop-empty {
7830
- margin: 10px 12px;
7831
- border: 1px dashed var(--pi-border-muted);
7832
- border-radius: 8px;
7833
- color: var(--pi-muted);
7834
- padding: 12px;
7835
- overflow: auto;
7836
- }
7837
-
7838
- .doorstop-empty p {
7839
- margin: 0;
7840
- }
7841
-
7842
- .doorstop-empty .doorstop-muted {
7843
- margin-top: 6px;
7844
- }
7845
-
7846
- /* --- status bar (region 5), sized to the host center status bar --- */
7847
- .doorstop-status-bar {
7848
- flex: 0 0 auto;
7849
- display: flex;
7850
- flex-direction: column;
7851
- min-width: 0;
7852
- /* Cap the whole bar at 38% of the panel (the pre-improvement cap),
7853
- not 38vh: on a short viewport with a tall side panel a viewport
7854
- unit can dwarf the panel. The cap cannot live on the body div
7855
- (.doorstop-last-run) — its containing block (this bar) is
7856
- content-sized, so a percentage max-height there would be treated
7857
- as none — so the bar caps itself and the body shrinks (and
7858
- scrolls) below it. */
7859
- max-height: 38%;
7860
- border-top: 1px solid var(--pi-border);
7861
- background: var(--pi-bg);
7862
- color: var(--pi-muted);
7863
- font: 12px system-ui, sans-serif;
7864
- }
7865
-
7866
- .doorstop-status-bar-row {
7867
- flex: 0 0 auto; /* the status row never collapses under the body cap */
7868
- display: flex;
7869
- align-items: center;
7870
- gap: 8px;
7871
- min-width: 0;
7872
- padding: 7px 12px;
7873
- overflow: hidden;
7874
- white-space: nowrap;
7875
- }
7876
-
7877
- /* The status bar's expanded output body — no card chrome. Shrinks
7878
- (and scrolls) when the bar hits its 38% cap; content-sized
7879
- otherwise. */
7880
- .doorstop-last-run {
7881
- flex: 1 1 auto;
7882
- min-height: 0;
7883
- overflow: auto;
7884
- padding: 8px 12px 10px;
7885
- border-top: 1px solid var(--pi-border-muted);
7886
- }
7887
-
7888
- .doorstop-last-run-commit {
7889
- margin: 0 0 6px;
7890
- font-size: 12px;
7891
- overflow-wrap: anywhere;
7892
- }
7893
-
7894
- .doorstop-last-run-status {
7895
- flex: 0 0 auto; /* the badge label keeps its full width */
7896
- border-radius: 999px;
7897
- padding: 0 7px;
7898
- font-size: 11px;
7899
- white-space: nowrap;
7900
- }
7901
-
7902
- .doorstop-last-run-status.is-ok {
7903
- border: 1px solid var(--pi-success-border);
7904
- color: var(--pi-success);
8361
+ .doorstop-last-run-status.is-ok {
8362
+ border: 1px solid var(--pi-success-border);
8363
+ color: var(--pi-success);
7905
8364
  }
7906
8365
 
7907
8366
  .doorstop-last-run-status.is-failed,
@@ -7927,12 +8386,12 @@ var panelStyles = i`
7927
8386
  text-overflow: ellipsis;
7928
8387
  white-space: nowrap;
7929
8388
  color: var(--pi-muted);
7930
- font-size: 12px;
8389
+ font-size: 13px;
7931
8390
  }
7932
8391
 
7933
8392
  .doorstop-last-run-dismiss {
7934
8393
  flex: 0 0 auto;
7935
- /* Buttons do not inherit font; match the bar's 12px system-ui. */
8394
+ /* Buttons do not inherit font; match the bar's 13px system-ui. */
7936
8395
  font: inherit;
7937
8396
  }
7938
8397
 
@@ -7942,7 +8401,7 @@ var panelStyles = i`
7942
8401
  background: var(--pi-surface);
7943
8402
  color: var(--pi-text);
7944
8403
  padding: 6px 8px;
7945
- font: 11px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8404
+ font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
7946
8405
  white-space: pre-wrap;
7947
8406
  word-break: break-word;
7948
8407
  overflow-wrap: anywhere;
@@ -7954,21 +8413,297 @@ var panelStyles = i`
7954
8413
  .doorstop-last-run-notice {
7955
8414
  margin: 4px 0 0;
7956
8415
  color: var(--pi-muted);
7957
- font-size: 11px;
8416
+ font-size: 12px;
7958
8417
  }
7959
8418
  `;
7960
8419
 
7961
- // src/doorstop-panel-icons.ts
8420
+ // src/browser/doorstop-panel-styles.ts
8421
+ var panelStyles = i`${baseStyles}${projectActionsStyles}${itemListStyles}${detailPaneStyles}${itemActionPaletteStyles}${findingsStyles}${statusBarStyles}`;
8422
+
8423
+ // node_modules/lit-html/directives/class-map.js
8424
+ var e7 = e4(class extends i6 {
8425
+ constructor(t5) {
8426
+ 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.");
8427
+ }
8428
+ render(t5) {
8429
+ return " " + Object.keys(t5).filter((s6) => t5[s6]).join(" ") + " ";
8430
+ }
8431
+ update(s6, [i8]) {
8432
+ if (void 0 === this.st) {
8433
+ this.st = /* @__PURE__ */ new Set(), void 0 !== s6.strings && (this.nt = new Set(s6.strings.join(" ").split(/\s/).filter((t5) => "" !== t5)));
8434
+ for (const t5 in i8) i8[t5] && !this.nt?.has(t5) && this.st.add(t5);
8435
+ return this.render(i8);
8436
+ }
8437
+ const r8 = s6.element.classList;
8438
+ for (const t5 of this.st) t5 in i8 || (r8.remove(t5), this.st.delete(t5));
8439
+ for (const t5 in i8) {
8440
+ const s7 = !!i8[t5];
8441
+ s7 === this.st.has(t5) || this.nt?.has(t5) || (s7 ? (r8.add(t5), this.st.add(t5)) : (r8.remove(t5), this.st.delete(t5)));
8442
+ }
8443
+ return E;
8444
+ }
8445
+ });
8446
+
8447
+ // src/browser/doorstop-panel-icons.ts
7962
8448
  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>`;
7963
8449
  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>`;
7964
8450
  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>`;
7965
8451
  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>`;
7966
- 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>`;
7967
- 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>`;
8452
+ 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>`;
8453
+ 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>`;
7968
8454
  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>`;
8455
+ 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>`;
7969
8456
  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>`;
7970
8457
 
7971
- // src/doorstop-prompts.ts
8458
+ // src/browser/sections/project-actions.ts
8459
+ function renderProjectActions(host) {
8460
+ const publishTarget = doorstopPublishTarget(host.result);
8461
+ const projectPath = host.context?.workspace.path ?? "";
8462
+ const projectLabel = projectPath === "" ? "Doorstop" : projectPath.split("/").filter(Boolean).pop() ?? projectPath;
8463
+ return b2`
8464
+ <section class="doorstop-project-actions">
8465
+ <strong class="doorstop-title" title=${projectPath === "" ? A : projectPath}>${doorstopIconSvg}${projectLabel}</strong>
8466
+ ${renderViewToggle(host)}
8467
+ <div class="doorstop-toolbar-actions">
8468
+ ${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}
8469
+ ${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}
8470
+ <button type="button" class="doorstop-refresh" title="Re-read the workspace" @click=${() => onRefreshClick(host)}>${refreshIconSvg}Refresh</button>
8471
+ <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>
8472
+ <button type="button" class="doorstop-publish" title=${`Publish the tree to ${publishTarget}`} ?disabled=${host.runInProgress !== void 0} @click=${() => onPublishClick(host)}>${publishIconSvg}Publish HTML</button>
8473
+ ${renderGitActions(host)}
8474
+ </div>
8475
+ </section>
8476
+ `;
8477
+ }
8478
+ function renderViewToggle(host) {
8479
+ return b2`
8480
+ <div class="doorstop-view-toggle" role="tablist" aria-label="Requirements panel view">
8481
+ <button
8482
+ type="button"
8483
+ role="tab"
8484
+ class=${e7({
8485
+ "doorstop-view-tab": true,
8486
+ "doorstop-view-items": true,
8487
+ "is-selected": host.view === "items"
8488
+ })}
8489
+ aria-selected=${host.view === "items" ? "true" : "false"}
8490
+ title="Item list and detail"
8491
+ @click=${() => {
8492
+ host.view = "items";
8493
+ }}
8494
+ >Items</button>
8495
+ <button
8496
+ type="button"
8497
+ role="tab"
8498
+ class=${e7({
8499
+ "doorstop-view-tab": true,
8500
+ "doorstop-view-findings": true,
8501
+ "is-selected": host.view === "findings"
8502
+ })}
8503
+ aria-selected=${host.view === "findings" ? "true" : "false"}
8504
+ title="Validation findings"
8505
+ @click=${() => {
8506
+ host.view = "findings";
8507
+ }}
8508
+ >Findings</button>
8509
+ </div>
8510
+ `;
8511
+ }
8512
+ function renderGitActions(host) {
8513
+ if (!host.backendActive()) return A;
8514
+ const hasDocuments = (host.result?.index.documents.length ?? 0) > 0;
8515
+ return b2`
8516
+ ${renderGitStatus(host)}
8517
+ <button
8518
+ type="button"
8519
+ class="doorstop-git-stage"
8520
+ title="Stage all Doorstop-managed files (requirements, documents, configs)"
8521
+ ?disabled=${host.runInProgress !== void 0 || !hasDocuments}
8522
+ @click=${() => onGitStageClick(host)}
8523
+ >${gitStageIconSvg}Stage all</button>
8524
+ <div class="doorstop-git-commit">
8525
+ <input
8526
+ type="text"
8527
+ class="doorstop-git-commit-input"
8528
+ placeholder="Commit message"
8529
+ aria-label="Git commit message"
8530
+ ?disabled=${host.runInProgress !== void 0}
8531
+ ${n5(host.gitCommitInputRef)}
8532
+ @input=${(event) => onGitCommitInput(host, event)}
8533
+ @keydown=${(event) => onGitCommitKeydown(host, event)}
8534
+ />
8535
+ <button
8536
+ type="button"
8537
+ class="doorstop-git-commit-button"
8538
+ title="Commit the staged index with this message"
8539
+ ?disabled=${host.runInProgress !== void 0 || host.gitCommitMessage.trim() === ""}
8540
+ @click=${() => onGitCommitClick(host)}
8541
+ >${gitCommitIconSvg}Commit</button>
8542
+ ${host.gitActionError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${host.gitActionError}</span>`}
8543
+ </div>
8544
+ `;
8545
+ }
8546
+ function renderGitStatus(host) {
8547
+ const view = host.gitStatusView;
8548
+ if (view === void 0) return A;
8549
+ const refresh = () => {
8550
+ void host.controller?.requestGitStatus();
8551
+ };
8552
+ switch (view.state) {
8553
+ case "loading":
8554
+ return b2`<button
8555
+ type="button"
8556
+ class="doorstop-git-status"
8557
+ title="Refreshing git status…"
8558
+ aria-busy=${host.gitStatusInFlight}
8559
+ @click=${refresh}
8560
+ ><span class="doorstop-git-status-text">⎇ …</span></button>`;
8561
+ case "no-git":
8562
+ 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>`;
8563
+ case "error":
8564
+ return b2`<button
8565
+ type="button"
8566
+ class="doorstop-git-status doorstop-git-status-error"
8567
+ title=${`Git status unavailable \u2014 ${view.errorMessage ?? "request failed"} \u2014 click to retry`}
8568
+ aria-busy=${host.gitStatusInFlight}
8569
+ @click=${refresh}
8570
+ ><span class="doorstop-git-status-text">git status error — retry</span></button>`;
8571
+ case "ready":
8572
+ 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>`;
8573
+ }
8574
+ }
8575
+ function onRefreshClick(host) {
8576
+ void host.controller?.invalidate();
8577
+ }
8578
+ function onValidateClick(host) {
8579
+ host.runDoorstop("validate", "Doorstop: validate", { op: "validate" }, "doorstop", true);
8580
+ }
8581
+ function onPublishClick(host) {
8582
+ const target = doorstopPublishTarget(host.result);
8583
+ if (typeof window.confirm === "function") {
8584
+ if (!window.confirm(`Publish the Doorstop tree as HTML to ${target} in the workspace terminal?`)) return;
8585
+ host.confirmSkipped = false;
8586
+ } else {
8587
+ host.confirmSkipped = true;
8588
+ }
8589
+ host.runDoorstop(
8590
+ "publish",
8591
+ "Doorstop: publish",
8592
+ { op: "publish", target },
8593
+ doorstopPublishCommand(host.result, target),
8594
+ false
8595
+ );
8596
+ }
8597
+ function onGitStageClick(host) {
8598
+ const controller = host.controller;
8599
+ if (controller === void 0) return;
8600
+ if (controller.runInProgress !== void 0) return;
8601
+ const result = host.result;
8602
+ if (result === void 0) {
8603
+ host.gitActionError = "The workspace is not loaded \u2014 refresh first";
8604
+ return;
8605
+ }
8606
+ const paths = doorstopPaths(result);
8607
+ if (paths.length === 0) {
8608
+ host.gitActionError = "Nothing to stage \u2014 the workspace has no Doorstop-managed files";
8609
+ return;
8610
+ }
8611
+ host.gitActionError = void 0;
8612
+ void controller.runGitStage(paths);
8613
+ }
8614
+ function onGitCommitInput(host, event) {
8615
+ host.gitCommitMessage = event.target.value;
8616
+ }
8617
+ function onGitCommitClick(host) {
8618
+ gitCommitSubmit(host);
8619
+ }
8620
+ function onGitCommitKeydown(host, event) {
8621
+ const input = host.gitCommitInputRef.value;
8622
+ if (input === void 0) return;
8623
+ if (event.key === "Enter") {
8624
+ event.preventDefault();
8625
+ gitCommitSubmit(host);
8626
+ } else if (event.key === "Escape") {
8627
+ input.value = "";
8628
+ host.gitCommitMessage = "";
8629
+ host.gitActionError = void 0;
8630
+ }
8631
+ }
8632
+ function gitCommitSubmit(host) {
8633
+ const controller = host.controller;
8634
+ if (controller === void 0) return;
8635
+ if (controller.runInProgress !== void 0) return;
8636
+ const input = host.gitCommitInputRef.value;
8637
+ const message = input?.value.trim() ?? "";
8638
+ if (message === "") {
8639
+ host.gitActionError = "Enter a commit message";
8640
+ input?.focus();
8641
+ return;
8642
+ }
8643
+ host.gitActionError = void 0;
8644
+ void controller.runGitCommit(message).then(() => {
8645
+ const lastRun = controller.lastRun;
8646
+ if (lastRun?.op === "git-commit" && lastRun.status === "ok" && input !== void 0) {
8647
+ input.value = "";
8648
+ host.gitCommitMessage = "";
8649
+ }
8650
+ });
8651
+ }
8652
+
8653
+ // node_modules/lit-html/directives/repeat.js
8654
+ var u4 = (e8, s6, t5) => {
8655
+ const r8 = /* @__PURE__ */ new Map();
8656
+ for (let l3 = s6; l3 <= t5; l3++) r8.set(e8[l3], l3);
8657
+ return r8;
8658
+ };
8659
+ var c5 = e4(class extends i6 {
8660
+ constructor(e8) {
8661
+ if (super(e8), e8.type !== t4.CHILD) throw Error("repeat() can only be used in text expressions");
8662
+ }
8663
+ dt(e8, s6, t5) {
8664
+ let r8;
8665
+ void 0 === t5 ? t5 = s6 : void 0 !== s6 && (r8 = s6);
8666
+ const l3 = [], o8 = [];
8667
+ let i8 = 0;
8668
+ for (const s7 of e8) l3[i8] = r8 ? r8(s7, i8) : i8, o8[i8] = t5(s7, i8), i8++;
8669
+ return { values: o8, keys: l3 };
8670
+ }
8671
+ render(e8, s6, t5) {
8672
+ return this.dt(e8, s6, t5).values;
8673
+ }
8674
+ update(s6, [t5, r8, c6]) {
8675
+ const d3 = M2(s6), { values: p4, keys: a3 } = this.dt(t5, r8, c6);
8676
+ if (!Array.isArray(d3)) return this.ut = a3, p4;
8677
+ const h6 = this.ut ??= [], v3 = [];
8678
+ let m3, y3, x2 = 0, j2 = d3.length - 1, k2 = 0, w2 = p4.length - 1;
8679
+ for (; x2 <= j2 && k2 <= w2; ) if (null === d3[x2]) x2++;
8680
+ else if (null === d3[j2]) j2--;
8681
+ else if (h6[x2] === a3[k2]) v3[k2] = u3(d3[x2], p4[k2]), x2++, k2++;
8682
+ else if (h6[j2] === a3[w2]) v3[w2] = u3(d3[j2], p4[w2]), j2--, w2--;
8683
+ else if (h6[x2] === a3[w2]) v3[w2] = u3(d3[x2], p4[w2]), v2(s6, v3[w2 + 1], d3[x2]), x2++, w2--;
8684
+ else if (h6[j2] === a3[k2]) v3[k2] = u3(d3[j2], p4[k2]), v2(s6, d3[x2], d3[j2]), j2--, k2++;
8685
+ else if (void 0 === m3 && (m3 = u4(a3, k2, w2), y3 = u4(h6, x2, j2)), m3.has(h6[x2])) if (m3.has(h6[j2])) {
8686
+ const e8 = y3.get(a3[k2]), t6 = void 0 !== e8 ? d3[e8] : null;
8687
+ if (null === t6) {
8688
+ const e9 = v2(s6, d3[x2]);
8689
+ u3(e9, p4[k2]), v3[k2] = e9;
8690
+ } else v3[k2] = u3(t6, p4[k2]), v2(s6, d3[x2], t6), d3[e8] = null;
8691
+ k2++;
8692
+ } else h3(d3[j2]), j2--;
8693
+ else h3(d3[x2]), x2++;
8694
+ for (; k2 <= w2; ) {
8695
+ const e8 = v2(s6, v3[w2 + 1]);
8696
+ u3(e8, p4[k2]), v3[k2++] = e8;
8697
+ }
8698
+ for (; x2 <= j2; ) {
8699
+ const e8 = d3[x2++];
8700
+ null !== e8 && h3(e8);
8701
+ }
8702
+ return this.ut = a3, p3(s6, v3), E;
8703
+ }
8704
+ });
8705
+
8706
+ // src/browser/doorstop-prompts.ts
7972
8707
  function andJoin(values) {
7973
8708
  if (values.length === 0) return "";
7974
8709
  if (values.length === 1) return values[0] ?? "";
@@ -8013,28 +8748,788 @@ function draftChildRequirementPrompt(parent, childPrefix) {
8013
8748
  "draftChildRequirementPrompt requires a non-empty child document prefix: `doorstop add` names its document by prefix"
8014
8749
  );
8015
8750
  }
8016
- 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>\`.`;
8751
+ 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>\`.`;
8752
+ }
8753
+ function referencePaths(item) {
8754
+ const paths = item.references?.map((reference) => reference.path) ?? [];
8755
+ return item.ref === "" ? paths : [item.ref, ...paths];
8756
+ }
8757
+ function reviewReadinessPrompt(item, children = []) {
8758
+ const parents = andJoin(item.links.map((link) => link.uid));
8759
+ const references = referencePaths(item);
8760
+ const childLinks = andJoin(
8761
+ children.map((child) => `${child.uid} (\`${child.path}\`)`)
8762
+ );
8763
+ const facts = [
8764
+ item.text.trim() === "" ? "its text is empty (that alone blocks review)" : "its text is filled in",
8765
+ item.links.length === 0 ? "it declares no parent links" : `its recorded parent links (${parents}) are not suspect or missing a fingerprint`,
8766
+ children.length === 0 ? "its child links are left to the `doorstop` run below" : `its child links are declared (${childLinks})`,
8767
+ references.length === 0 ? "it has no file references to verify" : `its file references exist (${references.map((path) => `\`${path}\``).join(", ")})`
8768
+ ];
8769
+ 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.`;
8770
+ }
8771
+ function composeReviewCommand(item) {
8772
+ return `/opendoor-review ${item.uid}
8773
+ Review ${item.uid} (\`${item.path}\`) against its child links and references; run \`doorstop\` validation and, when it is ready, \`doorstop review ${item.uid}\`.`;
8774
+ }
8775
+ function setupProjectPrompt() {
8776
+ 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.`;
8777
+ }
8778
+
8779
+ // node_modules/lit-html/directives/keyed.js
8780
+ var i7 = e4(class extends i6 {
8781
+ constructor() {
8782
+ super(...arguments), this.key = A;
8783
+ }
8784
+ render(r8, t5) {
8785
+ return this.key = r8, t5;
8786
+ }
8787
+ update(r8, [t5, e8]) {
8788
+ return t5 !== this.key && (p3(r8), this.key = t5), e8;
8789
+ }
8790
+ });
8791
+
8792
+ // src/browser/sections/detail-pane.ts
8793
+ function renderDetail(host, result) {
8794
+ const selectedUid = host.selectedUid;
8795
+ if (selectedUid === void 0) {
8796
+ return b2`<p class="doorstop-muted doorstop-standalone">Select an item in the list to inspect its details.</p>`;
8797
+ }
8798
+ const item = result.index.byUid.get(selectedUid);
8799
+ if (item === void 0) {
8800
+ return b2`<p class="doorstop-muted doorstop-standalone">This item is no longer available — refresh the panel.</p>`;
8801
+ }
8802
+ const children = result.index.childrenByUid.get(item.uid) ?? [];
8803
+ const findings = result.index.findings.filter((finding) => finding.uid === item.uid);
8804
+ return b2`
8805
+ <section class="doorstop-detail" aria-label="Item details">
8806
+ <div class="doorstop-detail-head">
8807
+ <code class="doorstop-detail-uid">${item.uid}</code>
8808
+ <span class="doorstop-level">level ${item.level}</span>
8809
+ ${item.header === void 0 ? A : b2`<h3>${item.header}</h3>`}
8810
+ </div>
8811
+ ${renderFlags(host, item)}
8812
+ <div class="doorstop-state-chip-row">
8813
+ ${item.stateKeys.map((key) => host.renderStateChip(key))}
8814
+ </div>
8815
+ <h4 class="doorstop-section-title">Text</h4>
8816
+ ${renderItemText(host, item)}
8817
+ ${renderChangesSinceReview(host, item)}
8818
+ <h4 class="doorstop-section-title">Parent links</h4>
8819
+ ${renderLinksOut(host, item, result.index)}
8820
+ <h4 class="doorstop-section-title">Child links</h4>
8821
+ ${renderLinksIn(host, item, result.index, children)}
8822
+ <h4 class="doorstop-section-title">References</h4>
8823
+ ${renderReferences(host, item, result.index)}
8824
+ ${Object.entries(item.attributes).length === 0 ? A : b2`
8825
+ <h4 class="doorstop-section-title">Extended attributes</h4>
8826
+ <dl class="doorstop-attributes">
8827
+ ${Object.entries(item.attributes).map(([key, value]) => renderAttribute(host, key, value))}
8828
+ </dl>
8829
+ `}
8830
+ <h4 class="doorstop-section-title">Findings</h4>
8831
+ ${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">
8832
+ ${findings.map((finding) => {
8833
+ const kind = finding.severity === "error" ? "doorstop-error" : finding.severity === "warning" ? "doorstop-warning" : "doorstop-info";
8834
+ return b2`<div class=${`doorstop-finding ${kind}`}><span class="doorstop-finding-severity">${finding.severity}</span><span>${finding.message}</span></div>`;
8835
+ })}
8836
+ </div>`}
8837
+ </section>
8838
+ `;
8839
+ }
8840
+ function renderItemText(host, item) {
8841
+ if (item.text === "") {
8842
+ return b2`<p class="doorstop-text doorstop-text-empty">empty</p>`;
8843
+ }
8844
+ const renderer = host.markdownRenderer;
8845
+ const machineId = host.context?.machine.id;
8846
+ if (renderer === void 0 || machineId === void 0) {
8847
+ return b2`<p class="doorstop-text">${item.text}</p>`;
8848
+ }
8849
+ return b2`<div class="doorstop-text doorstop-text-markdown">${renderer.render({
8850
+ machineId,
8851
+ text: item.text,
8852
+ allowManualPreview: true
8853
+ })}</div>`;
8854
+ }
8855
+ function renderAttribute(host, key, value) {
8856
+ if (!isMarkdownAttribute(key, value)) {
8857
+ return b2`<div class="doorstop-attribute"><dt>${key}</dt><dd><code>${jsonishText(value)}</code></dd></div>`;
8858
+ }
8859
+ const text = value.trim();
8860
+ const renderer = host.markdownRenderer;
8861
+ const machineId = host.context?.machine.id;
8862
+ const body = text === "" ? b2`<p class="doorstop-text doorstop-text-empty">empty</p>` : renderer === void 0 || machineId === void 0 ? text : b2`<div class="doorstop-text-markdown">${renderer.render({ machineId, text, allowManualPreview: true })}</div>`;
8863
+ return b2`<div class="doorstop-attribute doorstop-attribute-markdown"><dt>${key}</dt><dd>${body}</dd></div>`;
8864
+ }
8865
+ function renderFlags(host, item) {
8866
+ return b2`
8867
+ <div class="doorstop-flags" aria-label="Item flags">
8868
+ <span class=${e7({ "doorstop-flag": true, "is-inactive": !item.active })}>
8869
+ ${item.active ? "active" : "inactive"}
8870
+ </span>
8871
+ <span class="doorstop-flag">${item.normative ? "normative" : "non-normative"}</span>
8872
+ <span class="doorstop-flag">${item.derived ? "derived" : "non-derived"}</span>
8873
+ </div>
8874
+ `;
8875
+ }
8876
+ function renderLinksOut(host, item, index) {
8877
+ if (item.links.length === 0) return b2`<p class="doorstop-muted">No parent links.</p>`;
8878
+ return b2`
8879
+ <div class="doorstop-links" aria-label="Parent links">
8880
+ ${item.links.map((link) => renderLinkOut(host, item, link, index))}
8881
+ </div>
8882
+ `;
8883
+ }
8884
+ function renderLinkOut(host, item, link, index) {
8885
+ const remove = b2`
8886
+ <span
8887
+ class="doorstop-link-remove"
8888
+ aria-hidden="true"
8889
+ title=${`doorstop unlink ${item.uid} ${link.uid}`}
8890
+ @click=${(event) => {
8891
+ event.stopPropagation();
8892
+ unlinkLink(host, item, link.uid);
8893
+ }}
8894
+ >${trashIconSvg}</span>
8895
+ `;
8896
+ const target = index.byUid.get(link.uid);
8897
+ if (target === void 0) {
8898
+ return b2`
8899
+ <div class="doorstop-link-row">
8900
+ <code>${link.uid}</code>
8901
+ <span class="doorstop-chip doorstop-chip-danger">unknown</span>
8902
+ <span class="doorstop-fingerprint">not in the index — ${link.fingerprint === null ? "no recorded fingerprint" : `recorded ${shortFingerprint(link.fingerprint)}`}</span>
8903
+ ${remove}
8904
+ </div>
8905
+ `;
8906
+ }
8907
+ const current = computeItemStamp(target, documentConfigFor(index, target), false);
8908
+ const suspect = link.fingerprint !== null && link.fingerprint !== current;
8909
+ return b2`
8910
+ <button
8911
+ type="button"
8912
+ class="doorstop-link-row"
8913
+ data-uid=${link.uid}
8914
+ title=${`Open ${link.uid}`}
8915
+ @click=${() => {
8916
+ host.controller?.selectUid(link.uid);
8917
+ }}
8918
+ >
8919
+ <code>${link.uid}</code>
8920
+ <span class=${suspect ? "doorstop-chip doorstop-chip-danger" : "doorstop-chip doorstop-chip-ok"}>${suspect ? "suspect" : "ok"}</span>
8921
+ <span class="doorstop-fingerprint">recorded ${shortFingerprint(link.fingerprint)} · current ${shortFingerprint(current)}</span>
8922
+ ${remove}
8923
+ </button>
8924
+ `;
8925
+ }
8926
+ function renderLinksIn(host, item, index, children) {
8927
+ if (children.length === 0) {
8928
+ return b2`<p class="doorstop-muted">No child links.</p>`;
8929
+ }
8930
+ return b2`
8931
+ <div class="doorstop-links" aria-label="Child links">
8932
+ ${children.map(
8933
+ (child) => b2`<button type="button" class="doorstop-link-row" data-uid=${child.uid} title=${`Open ${child.uid}`} @click=${() => {
8934
+ host.controller?.selectUid(child.uid);
8935
+ }}>
8936
+ <code>${child.uid}</code>
8937
+ <span class="doorstop-muted">${child.level}</span>
8938
+ </button>`
8939
+ )}
8940
+ </div>
8941
+ `;
8942
+ }
8943
+ function renderReferences(host, item, index) {
8944
+ const paths = [];
8945
+ if (item.ref !== "") paths.push(item.ref);
8946
+ if (item.references !== void 0) {
8947
+ for (const reference of item.references) paths.push(reference.path);
8948
+ }
8949
+ if (paths.length === 0) return b2`<p class="doorstop-muted">No file references.</p>`;
8950
+ return b2`
8951
+ <div class="doorstop-links" aria-label="File references">
8952
+ ${paths.map((path) => {
8953
+ const found = index.knownFilePaths.has(path);
8954
+ return b2`
8955
+ <div class="doorstop-link-row">
8956
+ <code>${path}</code>
8957
+ ${found ? A : b2`<span class="doorstop-chip doorstop-chip-danger">not found</span>`}
8958
+ </div>
8959
+ `;
8960
+ })}
8961
+ </div>
8962
+ `;
8963
+ }
8964
+ function renderChangesSinceReview(host, item) {
8965
+ if (!item.stateKeys.includes("unreviewed") || item.reviewed === null) return A;
8966
+ if (!host.backendActive()) return A;
8967
+ const view = host.controller?.baselineViewFor(item);
8968
+ return i7(
8969
+ item.uid,
8970
+ b2`
8971
+ <details class="doorstop-changes" @toggle=${(event) => onChangesToggle(host, event)}>
8972
+ <summary class="doorstop-changes-summary">Changes since review</summary>
8973
+ ${view === void 0 || view.state === "loading" ? b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>` : renderBaselineView(host, view)}
8974
+ </details>
8975
+ `
8976
+ );
8977
+ }
8978
+ function renderBaselineView(host, view) {
8979
+ switch (view.state) {
8980
+ case "loading":
8981
+ return b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>`;
8982
+ case "no-git":
8983
+ return b2`<p class="doorstop-muted doorstop-changes-notice">No git history — previous version unavailable</p>`;
8984
+ case "no-match":
8985
+ return b2`<p class="doorstop-muted doorstop-changes-notice">Could not locate the reviewed version (history may have been rewritten)</p>`;
8986
+ case "error":
8987
+ return b2`<p class="doorstop-muted doorstop-changes-notice">Baseline unavailable — ${view.errorMessage ?? "request failed"}</p>`;
8988
+ case "ready": {
8989
+ const diff = view.diff;
8990
+ if (diff === void 0) return b2`<p class="doorstop-muted doorstop-changes-notice">No changes found.</p>`;
8991
+ return renderBaselineDiff(host, view, diff);
8992
+ }
8993
+ }
8994
+ }
8995
+ function renderBaselineDiff(host, view, diff) {
8996
+ return b2`
8997
+ ${view.source === void 0 ? A : b2`<p class="doorstop-muted doorstop-changes-source">matched via ${view.source === "review-commit" ? "review commit" : "history walk"}</p>`}
8998
+ ${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">
8999
+ ${diff.text.map((line) => renderDiffLine(host, line))}
9000
+ </div>`}
9001
+ ${diff.ref === void 0 ? A : renderFieldChange(host, "ref", diff.ref.before, diff.ref.after)}
9002
+ ${diff.references === void 0 ? A : renderFieldChange(
9003
+ host,
9004
+ "references",
9005
+ referenceListText(diff.references.before),
9006
+ referenceListText(diff.references.after)
9007
+ )}
9008
+ ${diff.linksAdded.length === 0 && diff.linksRemoved.length === 0 ? A : b2`<div class="doorstop-field-change">
9009
+ <span class="doorstop-field-name">links</span>
9010
+ ${diff.linksAdded.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-after">+ ${uid}</code>`)}
9011
+ ${diff.linksRemoved.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-before">− ${uid}</code>`)}
9012
+ </div>`}
9013
+ ${diff.extended.map((change) => renderFieldChange(host, change.name, change.before, change.after))}
9014
+ `;
9015
+ }
9016
+ function renderDiffLine(host, line) {
9017
+ const mark = line.kind === "added" ? "+" : line.kind === "removed" ? "\u2212" : "";
9018
+ return b2`
9019
+ <div class=${`doorstop-diff-line is-${line.kind}`} role="listitem">
9020
+ <span class="doorstop-diff-mark">${mark}</span>
9021
+ <span class="doorstop-diff-text">${line.text === "" ? "\xA0" : line.text}</span>
9022
+ </div>
9023
+ `;
9024
+ }
9025
+ function renderFieldChange(host, label, before, after) {
9026
+ return b2`
9027
+ <div class="doorstop-field-change">
9028
+ <span class="doorstop-field-name">${label}</span>
9029
+ <code class="doorstop-field-chip doorstop-field-chip-before">${diffValueText(before)}</code>
9030
+ <span class="doorstop-field-arrow">→</span>
9031
+ <code class="doorstop-field-chip doorstop-field-chip-after">${diffValueText(after)}</code>
9032
+ </div>
9033
+ `;
9034
+ }
9035
+ function onChangesToggle(host, event) {
9036
+ const details = event.currentTarget;
9037
+ if (!(details instanceof HTMLDetailsElement) || !details.open) return;
9038
+ const item = host.selectedItem();
9039
+ if (item === void 0) return;
9040
+ void host.controller?.requestBaseline(item);
9041
+ }
9042
+ function unlinkLink(host, item, target) {
9043
+ const controller = host.controller;
9044
+ if (controller === void 0 || controller.runInProgress !== void 0) return;
9045
+ host.runDoorstop(
9046
+ "unlink",
9047
+ `Doorstop: unlink ${item.uid}`,
9048
+ { op: "unlink", uid: item.uid, target },
9049
+ `doorstop unlink ${item.uid} ${target}`,
9050
+ false
9051
+ );
9052
+ }
9053
+
9054
+ // src/browser/sections/item-list.ts
9055
+ var DEFAULT_STATE_KEY = "normative";
9056
+ function renderListFilters(host, result) {
9057
+ return b2`
9058
+ <section class="doorstop-list-filters">
9059
+ <div class="doorstop-docs" role="list" aria-label="Doorstop documents">
9060
+ ${renderDocumentChip(host, void 0, result)}
9061
+ ${result.index.documents.map((document2) => renderDocumentChip(host, document2, result))}
9062
+ </div>
9063
+ <select class="doorstop-state-filter" aria-label="Filter by state" @change=${(event) => onStateFilterChange(host, event)}>
9064
+ <option value="" .selected=${host.stateFilter === void 0}>All states</option>
9065
+ ${Object.entries(STATE_CHIP_LABELS).map(
9066
+ ([key, label]) => b2`<option value=${key} .selected=${host.stateFilter === key}>${label}</option>`
9067
+ )}
9068
+ </select>
9069
+ <input
9070
+ class="doorstop-search"
9071
+ type="search"
9072
+ aria-label="Search items"
9073
+ placeholder="Search UID or text"
9074
+ .value=${host.search}
9075
+ @input=${(event) => onSearchInput(host, event)}
9076
+ />
9077
+ </section>
9078
+ `;
9079
+ }
9080
+ function renderDocumentChip(host, document2, result) {
9081
+ const prefix = document2?.prefix ?? ALL_DOCUMENTS_LABEL;
9082
+ const count = document2 === void 0 ? result.index.items.length : result.index.items.filter((item) => item.documentPrefix === document2.prefix).length;
9083
+ const dots = document2 === void 0 ? [] : documentStateDots(document2, result.index.items);
9084
+ const selected = document2 === void 0 ? host.selectedDocumentPrefix === void 0 || host.selectedDocumentPrefix === "" : host.selectedDocumentPrefix === document2.prefix;
9085
+ return b2`
9086
+ <button
9087
+ type="button"
9088
+ role="listitem"
9089
+ class=${e7({ "doorstop-doc-chip": true, "is-selected": selected })}
9090
+ data-prefix=${document2?.prefix ?? ""}
9091
+ @click=${() => {
9092
+ host.controller?.selectDocument(document2?.prefix ?? "");
9093
+ }}
9094
+ >
9095
+ ${document2?.parentPrefix === void 0 ? A : b2`<span class="doorstop-doc-arrow">← ${document2.parentPrefix}</span>`}
9096
+ <span class="doorstop-doc-prefix">${prefix}</span>
9097
+ <span class="doorstop-doc-count">${String(count)}</span>
9098
+ ${dots.map((dot) => b2`<span class=${`doorstop-dot doorstop-dot-${dot}`} title=${dotTitle(dot)}></span>`)}
9099
+ </button>
9100
+ `;
9101
+ }
9102
+ function renderViewer(host) {
9103
+ const result = host.result;
9104
+ if (result === void 0) {
9105
+ return b2`<p class="doorstop-muted doorstop-standalone">${host.loading ? "Loading workspace\u2026" : "Run Refresh to scan for Doorstop documents."}</p>`;
9106
+ }
9107
+ if (host.view === "findings") {
9108
+ return renderFindingsView(host, result);
9109
+ }
9110
+ return b2`
9111
+ ${renderListFilters(host, result)}
9112
+ ${result.index.documents.length === 0 ? b2`
9113
+ ${renderDiagnostics(host, result)}
9114
+ <section class="doorstop-empty">
9115
+ <p>${EMPTY_WORKSPACE_MESSAGE}</p>
9116
+ ${host.context === void 0 ? A : b2`
9117
+ <button
9118
+ type="button"
9119
+ class="doorstop-setup"
9120
+ @click=${() => {
9121
+ host.insertPrompt(setupProjectPrompt());
9122
+ }}
9123
+ >Ask agent to set up the project</button>
9124
+ `}
9125
+ </section>
9126
+ ` : b2`
9127
+ <section class="doorstop-split">
9128
+ <section class="doorstop-list">
9129
+ ${renderDiagnostics(host, result)}
9130
+ ${renderItemList(host, result)}
9131
+ </section>
9132
+ <section class="doorstop-detail-pane">${renderDetail(host, result)}</section>
9133
+ </section>
9134
+ `}
9135
+ `;
9136
+ }
9137
+ function renderItemList(host, result) {
9138
+ if (result.index.items.length === 0) {
9139
+ return b2`<p class="doorstop-muted doorstop-standalone">No Doorstop items found (item files may be binary or truncated — see the diagnostics above).</p>`;
9140
+ }
9141
+ const items = filteredItems(result.index, host.selectedDocumentPrefix, host.stateFilter, host.search);
9142
+ if (items.length === 0) {
9143
+ return b2`<p class="doorstop-muted doorstop-standalone">No items match the current document, state, or search filters.</p>`;
9144
+ }
9145
+ const gitFiles = host.readyGitFiles();
9146
+ const attributeKeys = doorstopShowAdditionalAttribute(result);
9147
+ return b2`
9148
+ <div
9149
+ class=${e7({ "doorstop-items": true, "has-item-attrs": attributeKeys.length > 0 })}
9150
+ role="list"
9151
+ aria-label="Doorstop items"
9152
+ >
9153
+ ${c5(items, (item) => item.uid, (item) => renderItemRow(host, item, gitFiles, attributeKeys))}
9154
+ </div>
9155
+ `;
9156
+ }
9157
+ function renderItemRow(host, item, gitFiles, attributeKeys) {
9158
+ const selected = host.selectedUid === item.uid;
9159
+ const gitState = itemGitState(gitFiles?.get(item.path));
9160
+ const shownAttributes = shownAttributeRows(item, attributeKeys);
9161
+ const textBody = attributeKeys.includes("text") ? itemTextRow(item) : void 0;
9162
+ const stageable = itemStageable(gitFiles?.get(item.path));
9163
+ const unstageable = itemUnstageable(gitFiles?.get(item.path));
9164
+ return b2`
9165
+ <button
9166
+ type="button"
9167
+ role="listitem"
9168
+ class=${e7({ "doorstop-item-row": true, "is-selected": selected })}
9169
+ data-uid=${item.uid}
9170
+ @click=${() => {
9171
+ host.controller?.selectUid(item.uid);
9172
+ }}
9173
+ >
9174
+ <span class="doorstop-item-level">${item.level}</span>
9175
+ <code class="doorstop-item-uid">${item.uid}</code>
9176
+ <span class="doorstop-item-summary">${itemExcerpt(item)}</span>
9177
+ ${shownAttributes.length === 0 ? A : b2`<span class="doorstop-item-attrs">${shownAttributes.map((row) => `${row.key}: ${row.text}`).join(" \xB7 ")}</span>`}
9178
+ <span class="doorstop-item-chips">
9179
+ ${item.stateKeys.filter((key) => key !== DEFAULT_STATE_KEY).map((key) => renderStateChip(key))}
9180
+ ${gitState === "clean" ? A : b2`<span class=${`doorstop-chip doorstop-chip-${gitChipKind(gitState)}`}>${GIT_CHIP_LABELS[gitState]}</span>`}
9181
+ ${unstageable && host.runInProgress === void 0 ? b2`<span
9182
+ class="doorstop-item-add"
9183
+ aria-hidden="true"
9184
+ title=${`git reset ${item.path}`}
9185
+ @click=${(event) => {
9186
+ event.stopPropagation();
9187
+ host.unstageItem(item);
9188
+ }}
9189
+ >${gitUnstageIconSvg}</span>` : stageable && host.runInProgress === void 0 ? b2`<span
9190
+ class="doorstop-item-add"
9191
+ aria-hidden="true"
9192
+ title=${`git add ${item.path}`}
9193
+ @click=${(event) => {
9194
+ event.stopPropagation();
9195
+ host.stageItem(item);
9196
+ }}
9197
+ >${gitStageIconSvg}</span>` : A}
9198
+ ${host.context === void 0 ? A : b2`<span
9199
+ class="doorstop-item-insert"
9200
+ aria-hidden="true"
9201
+ title=${`Insert ${item.uid} into the prompt`}
9202
+ @click=${(event) => {
9203
+ event.stopPropagation();
9204
+ host.insertPrompt(item.uid);
9205
+ }}
9206
+ >${insertPromptIconSvg}</span>`}
9207
+ </span>
9208
+ ${textBody === void 0 ? A : b2`<span class="doorstop-item-text">${textBody}</span>`}
9209
+ </button>
9210
+ `;
9211
+ }
9212
+ function renderStateChip(key) {
9213
+ const kind = stateChipKind(key);
9214
+ return b2`<span class=${`doorstop-chip doorstop-chip-${kind}`}>${STATE_CHIP_LABELS[key]}</span>`;
9215
+ }
9216
+ function renderDiagnostics(host, result) {
9217
+ if (result.index.diagnostics.length === 0) return A;
9218
+ return b2`
9219
+ <section class="doorstop-diagnostics" aria-label="Workspace diagnostics">
9220
+ ${result.index.diagnostics.map((diagnostic) => {
9221
+ const kind = diagnostic.severity === "error" ? "doorstop-error" : "doorstop-warning";
9222
+ return b2`
9223
+ <div class=${`doorstop-diagnostic ${kind}`}>
9224
+ <span class="doorstop-severity">${diagnostic.severity}</span>
9225
+ <span class="doorstop-diagnostic-copy">
9226
+ ${diagnostic.path === void 0 ? A : b2`<code>${diagnostic.path}</code>`}
9227
+ ${diagnostic.message}
9228
+ </span>
9229
+ </div>
9230
+ `;
9231
+ })}
9232
+ </section>
9233
+ `;
9234
+ }
9235
+ function renderFindingsView(host, result) {
9236
+ const rows = findingsViewRows(result.index);
9237
+ const counts = findingsViewCounts(rows);
9238
+ return b2`
9239
+ <section class="doorstop-findings-view" aria-label="Doorstop findings">
9240
+ <header class="doorstop-findings-head">
9241
+ <span class="doorstop-findings-counts" role="status" aria-label="Finding counts">${findingsCountText(counts)}</span>
9242
+ <span class="doorstop-findings-note doorstop-muted">${FINDINGS_PLUGIN_LOCAL_NOTE}</span>
9243
+ </header>
9244
+ ${rows.length === 0 ? b2`
9245
+ <section class="doorstop-empty">
9246
+ <p>${FINDINGS_EMPTY_MESSAGE}</p>
9247
+ <p class="doorstop-muted">${FINDINGS_EMPTY_HINT}</p>
9248
+ </section>
9249
+ ` : b2`<div class="doorstop-findings-list" role="list" aria-label="Validation findings">
9250
+ ${rows.map((row) => renderFindingRow(host, row, result.index))}
9251
+ </div>`}
9252
+ </section>
9253
+ `;
9254
+ }
9255
+ function renderFindingRow(host, row, index) {
9256
+ const kind = row.severity === "error" ? "doorstop-error" : row.severity === "warning" ? "doorstop-warning" : "doorstop-info";
9257
+ const uid = row.uid;
9258
+ const navigable = uid !== void 0 && index.byUid.has(uid);
9259
+ return b2`
9260
+ <div class=${`doorstop-finding-row ${kind}`} role="listitem">
9261
+ <span class="doorstop-severity">${row.severity}</span>
9262
+ ${uid === void 0 ? A : navigable ? b2`<button type="button" class="doorstop-finding-uid" data-uid=${uid} title=${`Show ${uid} in the item list`} @click=${() => {
9263
+ selectFindingTarget(host, uid);
9264
+ }}><code>${uid}</code></button>` : b2`<code class="doorstop-finding-uid">${uid}</code>`}
9265
+ ${row.path === void 0 ? A : b2`<code class="doorstop-finding-path">${row.path}</code>`}
9266
+ <span class="doorstop-finding-message">${row.message}</span>
9267
+ </div>
9268
+ `;
9269
+ }
9270
+ function selectFindingTarget(host, uid) {
9271
+ const controller = host.controller;
9272
+ if (controller === void 0) return;
9273
+ controller.selectDocument("");
9274
+ controller.setStateFilter(void 0);
9275
+ controller.setSearch("");
9276
+ controller.selectUid(uid);
9277
+ host.view = "items";
9278
+ }
9279
+ function onStateFilterChange(host, event) {
9280
+ const value = event.target.value;
9281
+ host.controller?.setStateFilter(value === "" ? void 0 : value);
9282
+ }
9283
+ function onSearchInput(host, event) {
9284
+ host.controller?.setSearch(event.target.value);
9285
+ }
9286
+
9287
+ // src/browser/sections/item-action-palette.ts
9288
+ function renderItemActionPalette(host) {
9289
+ if (host.view !== "items") return A;
9290
+ const result = host.result;
9291
+ if (result === void 0 || result.index.documents.length === 0) return A;
9292
+ const item = host.selectedItem();
9293
+ if (item === void 0) {
9294
+ return b2`
9295
+ <section class="doorstop-action-palette" aria-label="Item actions">
9296
+ <span class="doorstop-muted doorstop-palette-placeholder">Select an item…</span>
9297
+ </section>
9298
+ `;
9299
+ }
9300
+ const suspects = suspectParentItems(item, result.index);
9301
+ return b2`
9302
+ <section class="doorstop-action-palette" aria-label="Item actions">
9303
+ ${renderActionRow(host, item, result.index, suspects)}
9304
+ </section>
9305
+ `;
9306
+ }
9307
+ function renderActionRow(host, item, index, suspects) {
9308
+ const reviewed = item.stateKeys.includes("reviewed");
9309
+ const suspectUids = suspects.length > 0 ? suspects.map((parent) => parent.uid) : [];
9310
+ const gitStatus = host.readyGitStatus();
9311
+ const gitFile = gitStatus === void 0 ? void 0 : gitStatusFileFor(gitStatus, item.path);
9312
+ const stageable = itemStageable(gitFile);
9313
+ const unstageable = itemUnstageable(gitFile);
9314
+ return b2`
9315
+ <button
9316
+ type="button"
9317
+ class="doorstop-review"
9318
+ ?disabled=${reviewed || host.runInProgress !== void 0}
9319
+ title=${reviewed ? `${item.uid} is already reviewed against its current fingerprint` : `Mark ${item.uid} reviewed`}
9320
+ @click=${() => {
9321
+ reviewItem(host, item);
9322
+ }}
9323
+ >Approve</button>
9324
+ <button
9325
+ type="button"
9326
+ class="doorstop-clear"
9327
+ ?disabled=${suspects.length === 0 || host.runInProgress !== void 0}
9328
+ title=${suspects.length === 0 ? `No suspect links to clear` : `Re-record the parent fingerprints of ${item.uid}`}
9329
+ @click=${() => {
9330
+ clearSuspects(host, item, suspectUids);
9331
+ }}
9332
+ >Clear suspect links</button>
9333
+ ${gitStatus === void 0 ? A : b2`<button
9334
+ type="button"
9335
+ class="doorstop-item-stage"
9336
+ ?disabled=${host.runInProgress !== void 0 || !stageable}
9337
+ title=${stageable ? `Stage ${item.path}` : `${item.uid} has no unstaged changes \u2014 commit next`}
9338
+ @click=${() => {
9339
+ host.stageItem(item);
9340
+ }}
9341
+ >${gitStageIconSvg}Stage</button>
9342
+ <button
9343
+ type="button"
9344
+ class="doorstop-item-unstage"
9345
+ ?disabled=${host.runInProgress !== void 0 || !unstageable}
9346
+ title=${unstageable ? `Unstage ${item.path} (git reset)` : `${item.uid} has nothing staged`}
9347
+ @click=${() => {
9348
+ host.unstageItem(item);
9349
+ }}
9350
+ >${gitUnstageIconSvg}Unstage</button>`}
9351
+ <div class="doorstop-op">
9352
+ <input
9353
+ type="text"
9354
+ class="doorstop-target-input"
9355
+ data-op="link"
9356
+ placeholder="parent UID"
9357
+ ${n5(host.linkInputRef)}
9358
+ @keydown=${(event) => onTargetKeydown(host, event)}
9359
+ />
9360
+ <button type="button" class="doorstop-link" title=${`doorstop link ${item.uid} <target>`} ?disabled=${host.runInProgress !== void 0} @click=${() => {
9361
+ runTargetOp(host, "link", host.linkInputRef, item);
9362
+ }}>Link</button>
9363
+ </div>
9364
+ ${host.targetError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${host.targetError}</span>`}
9365
+ ${renderAskMenu(host, item, index, suspects)}
9366
+ `;
9367
+ }
9368
+ function renderAskMenu(host, item, index, suspects) {
9369
+ const children = index.childrenByUid.get(item.uid) ?? [];
9370
+ const childPrefix = firstChildDocumentPrefix(index, item);
9371
+ return b2`
9372
+ <div class="doorstop-menu">
9373
+ <button
9374
+ type="button"
9375
+ class="doorstop-menu-toggle"
9376
+ aria-expanded=${host.askMenuOpen ? "true" : "false"}
9377
+ @click=${() => onAskMenuToggle(host)}
9378
+ >Ask agent</button>
9379
+ ${host.askMenuOpen ? b2`
9380
+ <div class="doorstop-menu-items" role="menu" aria-label="Ask the agent about ${item.uid}">
9381
+ <button type="button" role="menuitem" class="doorstop-menu-item doorstop-explain" @click=${() => {
9382
+ host.insertPrompt(explainItemPrompt(item));
9383
+ }}>Explain</button>
9384
+ <button
9385
+ type="button"
9386
+ role="menuitem"
9387
+ class="doorstop-menu-item doorstop-fix-suspects"
9388
+ ?disabled=${suspects.length === 0}
9389
+ title=${suspects.length === 0 ? "No suspect links \u2014 nothing to fix" : `Fix the suspect links of ${item.uid}`}
9390
+ @click=${() => {
9391
+ host.insertPrompt(fixSuspectLinksPrompt(item, suspects));
9392
+ }}
9393
+ >Fix suspect links</button>
9394
+ <button
9395
+ type="button"
9396
+ role="menuitem"
9397
+ class="doorstop-menu-item doorstop-draft-child"
9398
+ ?disabled=${childPrefix === void 0}
9399
+ title=${childPrefix === void 0 ? "No child document to add to" : `Draft a child item in the ${childPrefix} document`}
9400
+ @click=${() => {
9401
+ if (childPrefix !== void 0) host.insertPrompt(draftChildRequirementPrompt(item, childPrefix));
9402
+ }}
9403
+ >Draft child requirement</button>
9404
+ <button type="button" role="menuitem" class="doorstop-menu-item doorstop-review-readiness" @click=${() => {
9405
+ host.insertPrompt(reviewReadinessPrompt(item, children));
9406
+ }}>Review readiness</button>
9407
+ <button
9408
+ type="button"
9409
+ role="menuitem"
9410
+ class="doorstop-menu-item doorstop-start-review-session"
9411
+ title=${`Start an independent review session for ${item.uid} (inserts /opendoor-review)`}
9412
+ @click=${() => {
9413
+ insertReviewCommand(host, item);
9414
+ }}
9415
+ >Start review session</button>
9416
+ </div>
9417
+ ` : A}
9418
+ </div>
9419
+ `;
9420
+ }
9421
+ function insertReviewCommand(host, item) {
9422
+ if (host.context === void 0) return;
9423
+ host.insertPrompt(composeReviewCommand(item));
9424
+ host.controller?.commitAskReview(item.uid);
9425
+ }
9426
+ function reviewItem(host, item) {
9427
+ const commit = doorstopCommitAfterReview(host.result);
9428
+ host.runDoorstop(
9429
+ "review",
9430
+ `Doorstop: review ${item.uid}`,
9431
+ commit ? { op: "review", uid: item.uid, commit: true } : { op: "review", uid: item.uid },
9432
+ `doorstop review ${item.uid}`,
9433
+ false
9434
+ );
9435
+ }
9436
+ function clearSuspects(host, item, suspectUids) {
9437
+ if (suspectUids.length === 0) return;
9438
+ host.runDoorstop(
9439
+ "clear",
9440
+ "Doorstop: clear suspect links",
9441
+ { op: "clear", uid: item.uid, parents: suspectUids },
9442
+ `doorstop clear ${item.uid} ${suspectUids.join(" ")}`,
9443
+ false
9444
+ );
9445
+ }
9446
+ function runTargetOp(host, op, inputRef, item) {
9447
+ const input = inputRef.value;
9448
+ const target = input?.value.trim() ?? "";
9449
+ if (target === "") {
9450
+ host.targetError = `Enter a link target UID (e.g. ${item.documentPrefix}0001)`;
9451
+ input?.focus();
9452
+ return;
9453
+ }
9454
+ if (!isValidTargetUid(target)) {
9455
+ host.targetError = `Invalid link target "${target}" \u2014 expected a Doorstop UID like ${item.documentPrefix}0001 (no spaces, quotes, or special characters)`;
9456
+ input?.focus();
9457
+ return;
9458
+ }
9459
+ host.targetError = void 0;
9460
+ if (input !== void 0) input.value = "";
9461
+ host.runDoorstop(
9462
+ op,
9463
+ `Doorstop: ${op} ${item.uid}`,
9464
+ { op, uid: item.uid, target },
9465
+ `doorstop ${op} ${item.uid} ${target}`,
9466
+ false
9467
+ );
9468
+ }
9469
+ function onTargetKeydown(host, event) {
9470
+ const input = event.target;
9471
+ if (input.getAttribute("data-op") !== "link") return;
9472
+ if (event.key === "Enter") {
9473
+ event.preventDefault();
9474
+ const item = host.selectedItem();
9475
+ if (item !== void 0) runTargetOp(host, "link", host.linkInputRef, item);
9476
+ } else if (event.key === "Escape") {
9477
+ input.value = "";
9478
+ }
9479
+ }
9480
+ function onAskMenuToggle(host) {
9481
+ host.askMenuOpen = !host.askMenuOpen;
9482
+ }
9483
+
9484
+ // src/browser/sections/status-bar.ts
9485
+ function renderStatusBar(host) {
9486
+ const lastRun = host.lastRun;
9487
+ if (lastRun === void 0) return A;
9488
+ const statusLabel = lastRun.status === "killed" ? "killed (timeout)" : lastRun.status;
9489
+ const detailParts = [lastRun.title, `${String(lastRun.durationMs)} ms`];
9490
+ if (lastRun.exitCode !== null && (lastRun.status === "failed" || lastRun.status === "killed")) {
9491
+ detailParts.push(`exit ${String(lastRun.exitCode)}`);
9492
+ }
9493
+ if (lastRun.signal !== null) detailParts.push(lastRun.signal);
9494
+ return b2`
9495
+ <section class="doorstop-status-bar" aria-label="Last run">
9496
+ <div class="doorstop-status-bar-row">
9497
+ <span class=${`doorstop-last-run-status is-${lastRun.status}`}>${statusLabel}</span>
9498
+ <span class="doorstop-last-run-meta">${detailParts.join(" \xB7 ")}</span>
9499
+ <button
9500
+ type="button"
9501
+ class="doorstop-last-run-dismiss"
9502
+ title="Dismiss the last run output"
9503
+ @click=${() => onDismissRun(host)}
9504
+ >Dismiss</button>
9505
+ </div>
9506
+ ${host.statusExpanded ? renderStatusBarBody(host, lastRun) : A}
9507
+ </section>
9508
+ `;
8017
9509
  }
8018
- function referencePaths(item) {
8019
- const paths = item.references?.map((reference) => reference.path) ?? [];
8020
- return item.ref === "" ? paths : [item.ref, ...paths];
9510
+ function renderStatusBarBody(host, lastRun) {
9511
+ return b2`
9512
+ <div class="doorstop-last-run">
9513
+ ${lastRun.commit === void 0 ? A : b2`<p class="doorstop-last-run-commit doorstop-muted">${commitOutcomeText(lastRun.op, lastRun.commit)}</p>`}
9514
+ ${lastRun.status === "error" ? b2`<pre class="doorstop-last-run-pre">${lastRun.errorMessage ?? ""}</pre>` : A}
9515
+ ${lastRun.stdout === "" ? A : b2`
9516
+ <pre class="doorstop-last-run-pre">${lastRun.stdout}</pre>
9517
+ ${lastRun.stdoutTruncated ? b2`<p class="doorstop-last-run-notice">stdout truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
9518
+ `}
9519
+ ${lastRun.stderr === "" ? A : b2`
9520
+ <pre class="doorstop-last-run-pre">${lastRun.stderr}</pre>
9521
+ ${lastRun.stderrTruncated ? b2`<p class="doorstop-last-run-notice">stderr truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
9522
+ `}
9523
+ ${lastRun.stdout === "" && lastRun.stderr === "" && lastRun.status !== "error" ? b2`<p class="doorstop-last-run-notice">No output captured.</p>` : A}
9524
+ </div>
9525
+ `;
8021
9526
  }
8022
- function reviewReadinessPrompt(item, children = []) {
8023
- const parents = andJoin(item.links.map((link) => link.uid));
8024
- const references = referencePaths(item);
8025
- const childLinks = andJoin(
8026
- children.map((child) => `${child.uid} (\`${child.path}\`)`)
8027
- );
8028
- const facts = [
8029
- item.text.trim() === "" ? "its text is empty (that alone blocks review)" : "its text is filled in",
8030
- item.links.length === 0 ? "it declares no parent links" : `its recorded parent links (${parents}) are not suspect or missing a fingerprint`,
8031
- children.length === 0 ? "its child links are left to the `doorstop` run below" : `its child links are declared (${childLinks})`,
8032
- references.length === 0 ? "it has no file references to verify" : `its file references exist (${references.map((path) => `\`${path}\``).join(", ")})`
8033
- ];
8034
- 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.`;
9527
+ function onDismissRun(host) {
9528
+ host.statusExpanded = false;
9529
+ host.controller?.dismissRun();
8035
9530
  }
8036
9531
 
8037
- // src/doorstop-panel-element.ts
9532
+ // src/browser/doorstop-panel-element.ts
8038
9533
  var bodyElementTag = "pi-web-opendoor-panel-body";
8039
9534
  function defineDoorstopPanelElements() {
8040
9535
  defineDoorstopPanelBodyElement();
@@ -8054,90 +9549,10 @@ function defineDoorstopPanelBodyElement() {
8054
9549
  this.confirmSkipped = false;
8055
9550
  this.view = "items";
8056
9551
  this.statusExpanded = false;
8057
- this.unlinkInputRef = e6();
8058
- this.linkInputRef = e6();
8059
- this.gitCommitInputRef = e6();
8060
- // --- handlers ------------------------------------------------------------------
8061
- this.onDismissRun = () => {
8062
- this.statusExpanded = false;
8063
- this.controller?.dismissRun();
8064
- };
8065
- this.onRefreshClick = () => {
8066
- void this.controller?.invalidate();
8067
- };
8068
- this.onValidateClick = () => {
8069
- this.runDoorstop("validate", "Doorstop: validate", { op: "validate" }, "doorstop", true);
8070
- };
8071
- this.onPublishClick = () => {
8072
- const target = doorstopPublishTarget(this.result);
8073
- if (typeof window.confirm === "function") {
8074
- if (!window.confirm(`Publish the Doorstop tree as HTML to ${target} in the workspace terminal?`)) return;
8075
- this.confirmSkipped = false;
8076
- } else {
8077
- this.confirmSkipped = true;
8078
- }
8079
- this.runDoorstop(
8080
- "publish",
8081
- "Doorstop: publish",
8082
- { op: "publish", target },
8083
- doorstopPublishCommand(this.result, target),
8084
- false
8085
- );
8086
- };
8087
- this.onGitCommitInput = (event) => {
8088
- this.gitCommitMessage = event.target.value;
8089
- };
8090
- this.onGitStageClick = () => {
8091
- const controller = this.controller;
8092
- if (controller === void 0) return;
8093
- if (controller.runInProgress !== void 0) return;
8094
- const result = this.result;
8095
- if (result === void 0) {
8096
- this.gitActionError = "The workspace is not loaded \u2014 refresh first";
8097
- return;
8098
- }
8099
- const paths = doorstopPaths(result);
8100
- if (paths.length === 0) {
8101
- this.gitActionError = "Nothing to stage \u2014 the workspace has no Doorstop-managed files";
8102
- return;
8103
- }
8104
- this.gitActionError = void 0;
8105
- void controller.runGitStage(paths);
8106
- };
8107
- this.onGitCommitClick = () => {
8108
- this.gitCommitSubmit();
8109
- };
8110
- this.onGitCommitKeydown = (event) => {
8111
- const input = this.gitCommitInputRef.value;
8112
- if (input === void 0) return;
8113
- if (event.key === "Enter") {
8114
- event.preventDefault();
8115
- this.gitCommitSubmit();
8116
- } else if (event.key === "Escape") {
8117
- input.value = "";
8118
- this.gitCommitMessage = "";
8119
- this.gitActionError = void 0;
8120
- }
8121
- };
8122
- this.onStateFilterChange = (event) => {
8123
- const value = event.target.value;
8124
- this.controller?.setStateFilter(value === "" ? void 0 : value);
8125
- };
8126
- this.onSearchInput = (event) => {
8127
- this.controller?.setSearch(event.target.value);
8128
- };
8129
- this.onTargetKeydown = (event) => {
8130
- const input = event.target;
8131
- const op = input.getAttribute("data-op");
8132
- if (op !== "unlink" && op !== "link") return;
8133
- if (event.key === "Enter") {
8134
- event.preventDefault();
8135
- const item = this.selectedItem();
8136
- if (item !== void 0) this.runTargetOp(op, op === "unlink" ? this.unlinkInputRef : this.linkInputRef, item);
8137
- } else if (event.key === "Escape") {
8138
- input.value = "";
8139
- }
8140
- };
9552
+ /** section-API: the palette's free-text Link target input. */
9553
+ this.linkInputRef = e5();
9554
+ /** section-API: the toolbar's commit-message input. */
9555
+ this.gitCommitInputRef = e5();
8141
9556
  this.onDocumentClick = (event) => {
8142
9557
  if (!this.askMenuOpen) return;
8143
9558
  const menuRoot = this.shadowRoot?.querySelector(".doorstop-menu");
@@ -8156,16 +9571,6 @@ function defineDoorstopPanelBodyElement() {
8156
9571
  this.askMenuOpen = false;
8157
9572
  }
8158
9573
  };
8159
- this.onAskMenuToggle = () => {
8160
- this.askMenuOpen = !this.askMenuOpen;
8161
- };
8162
- this.onChangesToggle = (event) => {
8163
- const details = event.currentTarget;
8164
- if (!(details instanceof HTMLDetailsElement) || !details.open) return;
8165
- const item = this.selectedItem();
8166
- if (item === void 0) return;
8167
- void this.controller?.requestBaseline(item);
8168
- };
8169
9574
  }
8170
9575
  static {
8171
9576
  this.styles = panelStyles;
@@ -8211,9 +9616,8 @@ function defineDoorstopPanelBodyElement() {
8211
9616
  * expand handler alone would strand the section on "Loading baseline…"
8212
9617
  * with no fetch in flight. `requestBaseline` installs its "loading"
8213
9618
  * view synchronously, so this cannot loop. (A selection switch needs
8214
- * no help here — {@link renderChangesSinceReview} keys the node by UID,
8215
- * so Lit recreates it closed and the user's expand fires a real
8216
- * `toggle`.)
9619
+ * no help here — the section keys the node by UID, so Lit recreates it
9620
+ * closed and the user's expand fires a real `toggle`.)
8217
9621
  */
8218
9622
  updated() {
8219
9623
  this.ensureGitStatus();
@@ -8241,318 +9645,29 @@ function defineDoorstopPanelBodyElement() {
8241
9645
  }
8242
9646
  render() {
8243
9647
  return b2`
8244
- ${this.renderProjectActions()}
9648
+ ${renderProjectActions(this)}
8245
9649
  ${this.error === void 0 ? A : b2`<div class="doorstop-error" role="alert">${this.error}</div>`}
8246
- <section class="doorstop-viewer">${this.renderViewer()}</section>
8247
- ${this.renderItemActionPalette()}
8248
- ${this.renderStatusBar()}
8249
- `;
8250
- }
8251
- // --- project actions (region 1) -----------------------------------------------
8252
- renderProjectActions() {
8253
- const publishTarget = doorstopPublishTarget(this.result);
8254
- const projectPath = this.context?.workspace.path ?? "";
8255
- const projectLabel = projectPath === "" ? "Doorstop" : projectPath.split("/").filter(Boolean).pop() ?? projectPath;
8256
- return b2`
8257
- <section class="doorstop-project-actions">
8258
- <strong class="doorstop-title" title=${projectPath === "" ? A : projectPath}>${doorstopIconSvg}${projectLabel}</strong>
8259
- ${this.renderViewToggle()}
8260
- <div class="doorstop-toolbar-actions">
8261
- ${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}
8262
- ${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}
8263
- <button type="button" class="doorstop-refresh" title="Re-read the workspace" @click=${this.onRefreshClick}>${refreshIconSvg}Refresh</button>
8264
- <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>
8265
- <button type="button" class="doorstop-publish" title=${`Publish the tree to ${publishTarget}`} ?disabled=${this.runInProgress !== void 0} @click=${this.onPublishClick}>${publishIconSvg}Publish HTML</button>
8266
- ${this.renderGitActions()}
8267
- </div>
8268
- </section>
8269
- `;
8270
- }
8271
- /**
8272
- * The commit INPUT is disabled while a run is in flight (like the
8273
- * buttons) so a typed-but-never-submitted message cannot be lost when
8274
- * the run's invalidate lands mid-typing.
8275
- */
8276
- renderGitActions() {
8277
- if (!this.backendActive()) return A;
8278
- const hasDocuments = (this.result?.index.documents.length ?? 0) > 0;
8279
- return b2`
8280
- ${this.renderGitStatus()}
8281
- <button
8282
- type="button"
8283
- class="doorstop-git-stage"
8284
- title="Stage all Doorstop-managed files (requirements, documents, configs)"
8285
- ?disabled=${this.runInProgress !== void 0 || !hasDocuments}
8286
- @click=${this.onGitStageClick}
8287
- >${gitStageIconSvg}Stage all</button>
8288
- <div class="doorstop-git-commit">
8289
- <input
8290
- type="text"
8291
- class="doorstop-git-commit-input"
8292
- placeholder="Commit message"
8293
- aria-label="Git commit message"
8294
- ?disabled=${this.runInProgress !== void 0}
8295
- ${n5(this.gitCommitInputRef)}
8296
- @input=${this.onGitCommitInput}
8297
- @keydown=${this.onGitCommitKeydown}
8298
- />
8299
- <button
8300
- type="button"
8301
- class="doorstop-git-commit-button"
8302
- title="Commit the staged index with this message"
8303
- ?disabled=${this.runInProgress !== void 0 || this.gitCommitMessage.trim() === ""}
8304
- @click=${this.onGitCommitClick}
8305
- >${gitCommitIconSvg}Commit</button>
8306
- ${this.gitActionError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${this.gitActionError}</span>`}
8307
- </div>
8308
- `;
8309
- }
8310
- renderGitStatus() {
8311
- const view = this.gitStatusView;
8312
- if (view === void 0) return A;
8313
- const refresh = () => {
8314
- void this.controller?.requestGitStatus();
8315
- };
8316
- switch (view.state) {
8317
- case "loading":
8318
- return b2`<button
8319
- type="button"
8320
- class="doorstop-git-status"
8321
- title="Refreshing git status…"
8322
- aria-busy=${this.gitStatusInFlight}
8323
- @click=${refresh}
8324
- ><span class="doorstop-git-status-text">⎇ …</span></button>`;
8325
- case "no-git":
8326
- 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>`;
8327
- case "error":
8328
- return b2`<button
8329
- type="button"
8330
- class="doorstop-git-status doorstop-git-status-error"
8331
- title=${`Git status unavailable \u2014 ${view.errorMessage ?? "request failed"} \u2014 click to retry`}
8332
- aria-busy=${this.gitStatusInFlight}
8333
- @click=${refresh}
8334
- ><span class="doorstop-git-status-text">git status error — retry</span></button>`;
8335
- case "ready":
8336
- 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>`;
8337
- }
8338
- }
8339
- // --- list filters (above the item list) ---------------------------------------
8340
- renderListFilters(result) {
8341
- return b2`
8342
- <section class="doorstop-list-filters">
8343
- <div class="doorstop-docs" role="list" aria-label="Doorstop documents">
8344
- ${this.renderDocumentChip(void 0, result)}
8345
- ${result.index.documents.map((document2) => this.renderDocumentChip(document2, result))}
8346
- </div>
8347
- <select class="doorstop-state-filter" aria-label="Filter by state" @change=${this.onStateFilterChange}>
8348
- <option value="" .selected=${this.stateFilter === void 0}>All states</option>
8349
- ${Object.entries(STATE_CHIP_LABELS).map(
8350
- ([key, label]) => b2`<option value=${key} .selected=${this.stateFilter === key}>${label}</option>`
8351
- )}
8352
- </select>
8353
- <input
8354
- class="doorstop-search"
8355
- type="search"
8356
- aria-label="Search items"
8357
- placeholder="Search UID or text"
8358
- .value=${this.search}
8359
- @input=${this.onSearchInput}
8360
- />
8361
- </section>
8362
- `;
8363
- }
8364
- /**
8365
- * Expansion is CONTENT-DRIVEN (see `willUpdate`); there is no manual
8366
- * expand/collapse toggle.
8367
- */
8368
- renderStatusBar() {
8369
- const lastRun = this.lastRun;
8370
- if (lastRun === void 0) return A;
8371
- const statusLabel = lastRun.status === "killed" ? "killed (timeout)" : lastRun.status;
8372
- const detailParts = [lastRun.title, `${String(lastRun.durationMs)} ms`];
8373
- if (lastRun.exitCode !== null && (lastRun.status === "failed" || lastRun.status === "killed")) {
8374
- detailParts.push(`exit ${String(lastRun.exitCode)}`);
8375
- }
8376
- if (lastRun.signal !== null) detailParts.push(lastRun.signal);
8377
- return b2`
8378
- <section class="doorstop-status-bar" aria-label="Last run">
8379
- <div class="doorstop-status-bar-row">
8380
- <span class=${`doorstop-last-run-status is-${lastRun.status}`}>${statusLabel}</span>
8381
- <span class="doorstop-last-run-meta">${detailParts.join(" \xB7 ")}</span>
8382
- <button
8383
- type="button"
8384
- class="doorstop-last-run-dismiss"
8385
- title="Dismiss the last run output"
8386
- @click=${this.onDismissRun}
8387
- >Dismiss</button>
8388
- </div>
8389
- ${this.statusExpanded ? this.renderStatusBarBody(lastRun) : A}
8390
- </section>
8391
- `;
8392
- }
8393
- renderStatusBarBody(lastRun) {
8394
- return b2`
8395
- <div class="doorstop-last-run">
8396
- ${lastRun.commit === void 0 ? A : b2`<p class="doorstop-last-run-commit doorstop-muted">${commitOutcomeText(lastRun.op, lastRun.commit)}</p>`}
8397
- ${lastRun.status === "error" ? b2`<pre class="doorstop-last-run-pre">${lastRun.errorMessage ?? ""}</pre>` : A}
8398
- ${lastRun.stdout === "" ? A : b2`
8399
- <pre class="doorstop-last-run-pre">${lastRun.stdout}</pre>
8400
- ${lastRun.stdoutTruncated ? b2`<p class="doorstop-last-run-notice">stdout truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
8401
- `}
8402
- ${lastRun.stderr === "" ? A : b2`
8403
- <pre class="doorstop-last-run-pre">${lastRun.stderr}</pre>
8404
- ${lastRun.stderrTruncated ? b2`<p class="doorstop-last-run-notice">stderr truncated by the host stream limit (2 MiB) — output not fully captured</p>` : A}
8405
- `}
8406
- ${lastRun.stdout === "" && lastRun.stderr === "" && lastRun.status !== "error" ? b2`<p class="doorstop-last-run-notice">No output captured.</p>` : A}
8407
- </div>
8408
- `;
8409
- }
8410
- renderViewToggle() {
8411
- return b2`
8412
- <div class="doorstop-view-toggle" role="tablist" aria-label="Requirements panel view">
8413
- <button
8414
- type="button"
8415
- role="tab"
8416
- class=${e5({
8417
- "doorstop-view-tab": true,
8418
- "doorstop-view-items": true,
8419
- "is-selected": this.view === "items"
8420
- })}
8421
- aria-selected=${this.view === "items" ? "true" : "false"}
8422
- title="Item list and detail"
8423
- @click=${() => {
8424
- this.view = "items";
8425
- }}
8426
- >Items</button>
8427
- <button
8428
- type="button"
8429
- role="tab"
8430
- class=${e5({
8431
- "doorstop-view-tab": true,
8432
- "doorstop-view-findings": true,
8433
- "is-selected": this.view === "findings"
8434
- })}
8435
- aria-selected=${this.view === "findings" ? "true" : "false"}
8436
- title="Validation findings"
8437
- @click=${() => {
8438
- this.view = "findings";
8439
- }}
8440
- >Findings</button>
8441
- </div>
8442
- `;
8443
- }
8444
- renderDocumentChip(document2, result) {
8445
- const prefix = document2?.prefix ?? ALL_DOCUMENTS_LABEL;
8446
- const count = document2 === void 0 ? result.index.items.length : result.index.items.filter((item) => item.documentPrefix === document2.prefix).length;
8447
- const dots = document2 === void 0 ? [] : documentStateDots(document2, result.index.items);
8448
- const selected = document2 === void 0 ? this.selectedDocumentPrefix === void 0 || this.selectedDocumentPrefix === "" : this.selectedDocumentPrefix === document2.prefix;
8449
- return b2`
8450
- <button
8451
- type="button"
8452
- role="listitem"
8453
- class=${e5({ "doorstop-doc-chip": true, "is-selected": selected })}
8454
- data-prefix=${document2?.prefix ?? ""}
8455
- @click=${() => {
8456
- this.controller?.selectDocument(document2?.prefix ?? "");
8457
- }}
8458
- >
8459
- ${document2?.parentPrefix === void 0 ? A : b2`<span class="doorstop-doc-arrow">← ${document2.parentPrefix}</span>`}
8460
- <span class="doorstop-doc-prefix">${prefix}</span>
8461
- <span class="doorstop-doc-count">${String(count)}</span>
8462
- ${dots.map((dot) => b2`<span class=${`doorstop-dot doorstop-dot-${dot}`} title=${dotTitle(dot)}></span>`)}
8463
- </button>
8464
- `;
8465
- }
8466
- // --- item list ------------------------------------------------------------------
8467
- renderViewer() {
8468
- const result = this.result;
8469
- if (result === void 0) {
8470
- return b2`<p class="doorstop-muted doorstop-standalone">${this.loading ? "Loading workspace\u2026" : "Run Refresh to scan for Doorstop documents."}</p>`;
8471
- }
8472
- if (this.view === "findings") {
8473
- return this.renderFindingsView(result);
8474
- }
8475
- return b2`
8476
- ${this.renderListFilters(result)}
8477
- ${result.index.documents.length === 0 ? b2`
8478
- ${this.renderDiagnostics(result)}
8479
- <section class="doorstop-empty"><p>${EMPTY_WORKSPACE_MESSAGE}</p></section>
8480
- ` : b2`
8481
- <section class="doorstop-split">
8482
- <section class="doorstop-list">
8483
- ${this.renderDiagnostics(result)}
8484
- ${this.renderItemList(result)}
8485
- </section>
8486
- <section class="doorstop-detail-pane">${this.renderDetail(result)}</section>
8487
- </section>
8488
- `}
8489
- `;
8490
- }
8491
- renderFindingsView(result) {
8492
- const rows = findingsViewRows(result.index);
8493
- const counts = findingsViewCounts(rows);
8494
- return b2`
8495
- <section class="doorstop-findings-view" aria-label="Doorstop findings">
8496
- <header class="doorstop-findings-head">
8497
- <span class="doorstop-findings-counts" role="status" aria-label="Finding counts">${findingsCountText(counts)}</span>
8498
- <span class="doorstop-findings-note doorstop-muted">${FINDINGS_PLUGIN_LOCAL_NOTE}</span>
8499
- </header>
8500
- ${rows.length === 0 ? b2`
8501
- <section class="doorstop-empty">
8502
- <p>${FINDINGS_EMPTY_MESSAGE}</p>
8503
- <p class="doorstop-muted">${FINDINGS_EMPTY_HINT}</p>
8504
- </section>
8505
- ` : b2`<div class="doorstop-findings-list" role="list" aria-label="Validation findings">
8506
- ${rows.map((row) => this.renderFindingRow(row, result.index))}
8507
- </div>`}
8508
- </section>
8509
- `;
8510
- }
8511
- renderFindingRow(row, index) {
8512
- const kind = row.severity === "error" ? "doorstop-error" : row.severity === "warning" ? "doorstop-warning" : "doorstop-info";
8513
- const uid = row.uid;
8514
- const navigable = uid !== void 0 && index.byUid.has(uid);
8515
- return b2`
8516
- <div class=${`doorstop-finding-row ${kind}`} role="listitem">
8517
- <span class="doorstop-severity">${row.severity}</span>
8518
- ${uid === void 0 ? A : navigable ? b2`<button type="button" class="doorstop-finding-uid" data-uid=${uid} title=${`Show ${uid} in the item list`} @click=${() => {
8519
- this.selectFindingTarget(uid);
8520
- }}><code>${uid}</code></button>` : b2`<code class="doorstop-finding-uid">${uid}</code>`}
8521
- ${row.path === void 0 ? A : b2`<code class="doorstop-finding-path">${row.path}</code>`}
8522
- <span class="doorstop-finding-message">${row.message}</span>
8523
- </div>
9650
+ <section class="doorstop-viewer">${renderViewer(this)}</section>
9651
+ ${renderItemActionPalette(this)}
9652
+ ${renderStatusBar(this)}
8524
9653
  `;
8525
9654
  }
8526
- selectFindingTarget(uid) {
8527
- const controller = this.controller;
8528
- if (controller === void 0) return;
8529
- controller.selectDocument("");
8530
- controller.setStateFilter(void 0);
8531
- controller.setSearch("");
8532
- controller.selectUid(uid);
8533
- this.view = "items";
8534
- }
8535
- renderDiagnostics(result) {
8536
- if (result.index.diagnostics.length === 0) return A;
8537
- return b2`
8538
- <section class="doorstop-diagnostics" aria-label="Workspace diagnostics">
8539
- ${result.index.diagnostics.map((diagnostic) => {
8540
- const kind = diagnostic.severity === "error" ? "doorstop-error" : "doorstop-warning";
8541
- return b2`
8542
- <div class=${`doorstop-diagnostic ${kind}`}>
8543
- <span class="doorstop-severity">${diagnostic.severity}</span>
8544
- <span class="doorstop-diagnostic-copy">
8545
- ${diagnostic.path === void 0 ? A : b2`<code>${diagnostic.path}</code>`}
8546
- ${diagnostic.message}
8547
- </span>
8548
- </div>
8549
- `;
8550
- })}
8551
- </section>
8552
- `;
9655
+ /** section-API: the baseline fetch and the review→commit pipeline live
9656
+ * server-side, so this gates the sections that need them. The peer
9657
+ * (`context.peer.request`) is the exact revision-paired path to this
9658
+ * package's own active server entry — unchanged by the v5 migration
9659
+ * (the v5 scoped `backend` is not consulted here), so `peer.request`
9660
+ * presence stays the paired-backend gate (a legal openChannel-only
9661
+ * peer is NOT paired for these request-shaped sections). The opendoor
9662
+ * provider ownership check is kept for the ownership-based
9663
+ * information display (behavior-preserving Phase A; Phase C revisits
9664
+ * it). */
9665
+ backendActive() {
9666
+ const context = this.context;
9667
+ return context?.peer?.request !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID;
8553
9668
  }
8554
- /** The status response usable for PER-ITEM git UI: a paired install, a
8555
- * ready view, and an UNTRUNCATED `files` list. `undefined` when
9669
+ /** section-API: the status response usable for PER-ITEM git UI: a paired
9670
+ * install, a ready view, and an UNTRUNCATED `files` list. `undefined` when
8556
9671
  * unpaired / loading / no-git / error / truncated, so the item list and
8557
9672
  * action row render no chips, no per-row add, and no svg inside the row
8558
9673
  * (the XSS assertion's structural dependency). The strip reads
@@ -8565,448 +9680,24 @@ function defineDoorstopPanelBodyElement() {
8565
9680
  if (gitStatusFilesTruncated(view.response)) return void 0;
8566
9681
  return view.response;
8567
9682
  }
8568
- /** The path→git-file map for one list render; `undefined` when per-item
8569
- * git UI is unavailable (see {@link readyGitStatus}). */
9683
+ /** section-API: the path→git-file map for one list render; `undefined`
9684
+ * when per-item git UI is unavailable (see {@link readyGitStatus}). */
8570
9685
  readyGitFiles() {
8571
9686
  const response = this.readyGitStatus();
8572
9687
  return response === void 0 ? void 0 : gitStatusFilesByPath(response);
8573
9688
  }
8574
- renderItemList(result) {
8575
- if (result.index.items.length === 0) {
8576
- return b2`<p class="doorstop-muted doorstop-standalone">No Doorstop items found (item files may be binary or truncated — see the diagnostics above).</p>`;
8577
- }
8578
- const items = filteredItems(result.index, this.selectedDocumentPrefix, this.stateFilter, this.search);
8579
- if (items.length === 0) {
8580
- return b2`<p class="doorstop-muted doorstop-standalone">No items match the current document, state, or search filters.</p>`;
8581
- }
8582
- const gitFiles = this.readyGitFiles();
8583
- return b2`
8584
- <div class="doorstop-items" role="list" aria-label="Doorstop items">
8585
- ${c5(items, (item) => item.uid, (item) => this.renderItemRow(item, gitFiles))}
8586
- </div>
8587
- `;
8588
- }
8589
- renderItemRow(item, gitFiles) {
8590
- const selected = this.selectedUid === item.uid;
8591
- const gitState = itemGitState(gitFiles?.get(item.path));
8592
- const stageable = itemStageable(gitFiles?.get(item.path));
8593
- const unstageable = itemUnstageable(gitFiles?.get(item.path));
8594
- return b2`
8595
- <button
8596
- type="button"
8597
- role="listitem"
8598
- class=${e5({ "doorstop-item-row": true, "is-selected": selected })}
8599
- data-uid=${item.uid}
8600
- @click=${() => {
8601
- this.controller?.selectUid(item.uid);
8602
- }}
8603
- >
8604
- <span class="doorstop-item-level">${item.level}</span>
8605
- <code class="doorstop-item-uid">${item.uid}</code>
8606
- <span class="doorstop-item-summary">${itemExcerpt(item)}</span>
8607
- <span class="doorstop-item-chips">
8608
- ${item.stateKeys.map((key) => this.renderStateChip(key))}
8609
- ${gitState === "clean" ? A : b2`<span class=${`doorstop-chip doorstop-chip-${gitChipKind(gitState)}`}>${GIT_CHIP_LABELS[gitState]}</span>`}
8610
- ${unstageable && this.runInProgress === void 0 ? b2`<span
8611
- class="doorstop-item-add"
8612
- aria-hidden="true"
8613
- title=${`git reset ${item.path}`}
8614
- @click=${(event) => {
8615
- event.stopPropagation();
8616
- this.unstageItem(item);
8617
- }}
8618
- >${gitUnstageIconSvg}</span>` : stageable && this.runInProgress === void 0 ? b2`<span
8619
- class="doorstop-item-add"
8620
- aria-hidden="true"
8621
- title=${`git add ${item.path}`}
8622
- @click=${(event) => {
8623
- event.stopPropagation();
8624
- this.stageItem(item);
8625
- }}
8626
- >${gitStageIconSvg}</span>` : A}
8627
- </span>
8628
- </button>
8629
- `;
8630
- }
8631
- renderStateChip(key) {
8632
- const kind = stateChipKind(key);
8633
- return b2`<span class=${`doorstop-chip doorstop-chip-${kind}`}>${STATE_CHIP_LABELS[key]}</span>`;
8634
- }
8635
- /** The baseline fetch and the review→commit pipeline live server-side,
8636
- * so this gates the sections that need them. */
8637
- backendActive() {
8638
- const context = this.context;
8639
- return context?.backend !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID && context.workspace.provider?.capabilities.request !== false;
8640
- }
8641
- /**
8642
- * The `<details>` is wrapped in `keyed(item.uid, …)` so a selection
8643
- * switch RE-CREATES the node (fresh, collapsed, with a real `toggle` on
8644
- * expand) instead of Lit reusing the old item's open node — a reused
8645
- * node fires no `toggle`, which would strand the new item on
8646
- * "Loading baseline…". {@link updated} covers the remaining reuse
8647
- * hole (same item re-rendered after its cache key changed).
8648
- */
8649
- renderChangesSinceReview(item) {
8650
- if (!item.stateKeys.includes("unreviewed") || item.reviewed === null) return A;
8651
- if (!this.backendActive()) return A;
8652
- const view = this.controller?.baselineViewFor(item);
8653
- return i7(
8654
- item.uid,
8655
- b2`
8656
- <details class="doorstop-changes" @toggle=${this.onChangesToggle}>
8657
- <summary class="doorstop-changes-summary">Changes since review</summary>
8658
- ${view === void 0 || view.state === "loading" ? b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>` : this.renderBaselineView(view)}
8659
- </details>
8660
- `
8661
- );
8662
- }
8663
- renderBaselineView(view) {
8664
- switch (view.state) {
8665
- case "loading":
8666
- return b2`<p class="doorstop-muted doorstop-changes-notice">Loading baseline…</p>`;
8667
- case "no-git":
8668
- return b2`<p class="doorstop-muted doorstop-changes-notice">No git history — previous version unavailable</p>`;
8669
- case "no-match":
8670
- return b2`<p class="doorstop-muted doorstop-changes-notice">Could not locate the reviewed version (history may have been rewritten)</p>`;
8671
- case "error":
8672
- return b2`<p class="doorstop-muted doorstop-changes-notice">Baseline unavailable — ${view.errorMessage ?? "request failed"}</p>`;
8673
- case "ready": {
8674
- const diff = view.diff;
8675
- if (diff === void 0) return b2`<p class="doorstop-muted doorstop-changes-notice">No changes found.</p>`;
8676
- return this.renderBaselineDiff(view, diff);
8677
- }
8678
- }
8679
- }
8680
- renderBaselineDiff(view, diff) {
8681
- return b2`
8682
- ${view.source === void 0 ? A : b2`<p class="doorstop-muted doorstop-changes-source">matched via ${view.source === "review-commit" ? "review commit" : "history walk"}</p>`}
8683
- ${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">
8684
- ${diff.text.map((line) => this.renderDiffLine(line))}
8685
- </div>`}
8686
- ${diff.ref === void 0 ? A : this.renderFieldChange("ref", diff.ref.before, diff.ref.after)}
8687
- ${diff.references === void 0 ? A : this.renderFieldChange(
8688
- "references",
8689
- referenceListText(diff.references.before),
8690
- referenceListText(diff.references.after)
8691
- )}
8692
- ${diff.linksAdded.length === 0 && diff.linksRemoved.length === 0 ? A : b2`<div class="doorstop-field-change">
8693
- <span class="doorstop-field-name">links</span>
8694
- ${diff.linksAdded.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-after">+ ${uid}</code>`)}
8695
- ${diff.linksRemoved.map((uid) => b2`<code class="doorstop-field-chip doorstop-field-chip-before">− ${uid}</code>`)}
8696
- </div>`}
8697
- ${diff.extended.map((change) => this.renderFieldChange(change.name, change.before, change.after))}
8698
- `;
8699
- }
8700
- renderDiffLine(line) {
8701
- const mark = line.kind === "added" ? "+" : line.kind === "removed" ? "\u2212" : "";
8702
- return b2`
8703
- <div class=${`doorstop-diff-line is-${line.kind}`} role="listitem">
8704
- <span class="doorstop-diff-mark">${mark}</span>
8705
- <span class="doorstop-diff-text">${line.text === "" ? "\xA0" : line.text}</span>
8706
- </div>
8707
- `;
8708
- }
8709
- renderFieldChange(label, before, after) {
8710
- return b2`
8711
- <div class="doorstop-field-change">
8712
- <span class="doorstop-field-name">${label}</span>
8713
- <code class="doorstop-field-chip doorstop-field-chip-before">${diffValueText(before)}</code>
8714
- <span class="doorstop-field-arrow">→</span>
8715
- <code class="doorstop-field-chip doorstop-field-chip-after">${diffValueText(after)}</code>
8716
- </div>
8717
- `;
8718
- }
8719
- // --- detail pane ------------------------------------------------------------------
8720
- // v1 deliberately renders the item text as escaped text — no markdown,
8721
- // injection-safe by construction.
8722
- renderDetail(result) {
8723
- const selectedUid = this.selectedUid;
8724
- if (selectedUid === void 0) {
8725
- return b2`<p class="doorstop-muted doorstop-standalone">Select an item in the list to inspect its details.</p>`;
8726
- }
8727
- const item = result.index.byUid.get(selectedUid);
8728
- if (item === void 0) {
8729
- return b2`<p class="doorstop-muted doorstop-standalone">This item is no longer available — refresh the panel.</p>`;
8730
- }
8731
- const children = result.index.childrenByUid.get(item.uid) ?? [];
8732
- const findings = result.index.findings.filter((finding) => finding.uid === item.uid);
8733
- return b2`
8734
- <section class="doorstop-detail" aria-label="Item details">
8735
- <div class="doorstop-detail-head">
8736
- <code class="doorstop-detail-uid">${item.uid}</code>
8737
- <span class="doorstop-level">level ${item.level}</span>
8738
- ${item.header === void 0 ? A : b2`<h3>${item.header}</h3>`}
8739
- </div>
8740
- ${this.renderFlags(item)}
8741
- <div class="doorstop-state-chip-row">
8742
- ${item.stateKeys.map((key) => this.renderStateChip(key))}
8743
- </div>
8744
- <h4 class="doorstop-section-title">Text</h4>
8745
- <p class=${item.text === "" ? "doorstop-text doorstop-text-empty" : "doorstop-text"}>${item.text === "" ? "empty" : item.text}</p>
8746
- ${this.renderChangesSinceReview(item)}
8747
- <h4 class="doorstop-section-title">Parent links</h4>
8748
- ${this.renderLinksOut(item, result.index)}
8749
- <h4 class="doorstop-section-title">Child links</h4>
8750
- ${this.renderLinksIn(item, result.index, children)}
8751
- <h4 class="doorstop-section-title">References</h4>
8752
- ${this.renderReferences(item, result.index)}
8753
- ${Object.entries(item.attributes).length === 0 ? A : b2`
8754
- <h4 class="doorstop-section-title">Extended attributes</h4>
8755
- <dl class="doorstop-attributes">
8756
- ${Object.entries(item.attributes).map(
8757
- ([key, value]) => b2`<div class="doorstop-attribute"><dt>${key}</dt><dd><code>${jsonishText(value)}</code></dd></div>`
8758
- )}
8759
- </dl>
8760
- `}
8761
- <h4 class="doorstop-section-title">Findings</h4>
8762
- ${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">
8763
- ${findings.map((finding) => {
8764
- const kind = finding.severity === "error" ? "doorstop-error" : finding.severity === "warning" ? "doorstop-warning" : "doorstop-info";
8765
- return b2`<div class=${`doorstop-finding ${kind}`}><span class="doorstop-finding-severity">${finding.severity}</span><span>${finding.message}</span></div>`;
8766
- })}
8767
- </div>`}
8768
- </section>
8769
- `;
8770
- }
8771
- // --- item action palette (region 4) --------------------------------------------
8772
- renderItemActionPalette() {
8773
- if (this.view !== "items") return A;
8774
- const result = this.result;
8775
- if (result === void 0 || result.index.documents.length === 0) return A;
8776
- const item = this.selectedItem();
8777
- if (item === void 0) {
8778
- return b2`
8779
- <section class="doorstop-action-palette" aria-label="Item actions">
8780
- <span class="doorstop-muted doorstop-palette-placeholder">Select an item…</span>
8781
- </section>
8782
- `;
8783
- }
8784
- const suspects = suspectParentItems(item, result.index);
8785
- return b2`
8786
- <section class="doorstop-action-palette" aria-label="Item actions">
8787
- ${this.renderActionRow(item, result.index, suspects)}
8788
- </section>
8789
- `;
8790
- }
8791
- renderFlags(item) {
8792
- return b2`
8793
- <div class="doorstop-flags" aria-label="Item flags">
8794
- <span class=${e5({ "doorstop-flag": true, "is-inactive": !item.active })}>
8795
- ${item.active ? "active" : "inactive"}
8796
- </span>
8797
- <span class="doorstop-flag">${item.normative ? "normative" : "non-normative"}</span>
8798
- <span class="doorstop-flag">${item.derived ? "derived" : "non-derived"}</span>
8799
- </div>
8800
- `;
8801
- }
8802
- /** `computeItemStamp(parent, config, false)` is exactly the
8803
- * link-record stamp the state chain compares against. */
8804
- renderLinksOut(item, index) {
8805
- if (item.links.length === 0) return b2`<p class="doorstop-muted">No parent links.</p>`;
8806
- return b2`
8807
- <div class="doorstop-links" aria-label="Parent links">
8808
- ${item.links.map((link) => this.renderLinkOut(item, link, index))}
8809
- </div>
8810
- `;
8811
- }
8812
- renderLinkOut(item, link, index) {
8813
- const remove = b2`
8814
- <span
8815
- class="doorstop-link-remove"
8816
- aria-hidden="true"
8817
- title=${`doorstop unlink ${item.uid} ${link.uid}`}
8818
- @click=${(event) => {
8819
- event.stopPropagation();
8820
- this.unlinkLink(item, link.uid);
8821
- }}
8822
- >${trashIconSvg}</span>
8823
- `;
8824
- const target = index.byUid.get(link.uid);
8825
- if (target === void 0) {
8826
- return b2`
8827
- <div class="doorstop-link-row">
8828
- <code>${link.uid}</code>
8829
- <span class="doorstop-chip doorstop-chip-danger">unknown</span>
8830
- <span class="doorstop-fingerprint">not in the index — ${link.fingerprint === null ? "no recorded fingerprint" : `recorded ${shortFingerprint(link.fingerprint)}`}</span>
8831
- ${remove}
8832
- </div>
8833
- `;
8834
- }
8835
- const current = computeItemStamp(target, documentConfigFor(index, target), false);
8836
- const suspect = link.fingerprint !== null && link.fingerprint !== current;
8837
- return b2`
8838
- <button
8839
- type="button"
8840
- class="doorstop-link-row"
8841
- data-uid=${link.uid}
8842
- title=${`Open ${link.uid}`}
8843
- @click=${() => {
8844
- this.controller?.selectUid(link.uid);
8845
- }}
8846
- >
8847
- <code>${link.uid}</code>
8848
- <span class=${suspect ? "doorstop-chip doorstop-chip-danger" : "doorstop-chip doorstop-chip-ok"}>${suspect ? "suspect" : "ok"}</span>
8849
- <span class="doorstop-fingerprint">recorded ${shortFingerprint(link.fingerprint)} · current ${shortFingerprint(current)}</span>
8850
- ${remove}
8851
- </button>
8852
- `;
8853
- }
8854
- renderLinksIn(item, index, children) {
8855
- if (children.length === 0) {
8856
- return b2`<p class="doorstop-muted">No child links.</p>`;
8857
- }
8858
- return b2`
8859
- <div class="doorstop-links" aria-label="Child links">
8860
- ${children.map(
8861
- (child) => b2`<button type="button" class="doorstop-link-row" data-uid=${child.uid} title=${`Open ${child.uid}`} @click=${() => {
8862
- this.controller?.selectUid(child.uid);
8863
- }}>
8864
- <code>${child.uid}</code>
8865
- <span class="doorstop-muted">${child.level}</span>
8866
- </button>`
8867
- )}
8868
- </div>
8869
- `;
8870
- }
8871
- renderReferences(item, index) {
8872
- const paths = [];
8873
- if (item.ref !== "") paths.push(item.ref);
8874
- if (item.references !== void 0) {
8875
- for (const reference of item.references) paths.push(reference.path);
8876
- }
8877
- if (paths.length === 0) return b2`<p class="doorstop-muted">No file references.</p>`;
8878
- return b2`
8879
- <div class="doorstop-links" aria-label="File references">
8880
- ${paths.map((path) => {
8881
- const found = index.knownFilePaths.has(path);
8882
- return b2`
8883
- <div class="doorstop-link-row">
8884
- <code>${path}</code>
8885
- ${found ? A : b2`<span class="doorstop-chip doorstop-chip-danger">not found</span>`}
8886
- </div>
8887
- `;
8888
- })}
8889
- </div>
8890
- `;
8891
- }
8892
- // --- action row ------------------------------------------------------------------
8893
- renderActionRow(item, index, suspects) {
8894
- const reviewed = item.stateKeys.includes("reviewed");
8895
- const suspectUids = suspects.length > 0 ? suspects.map((parent) => parent.uid) : [];
8896
- const gitStatus = this.readyGitStatus();
8897
- const gitFile = gitStatus === void 0 ? void 0 : gitStatusFileFor(gitStatus, item.path);
8898
- const stageable = itemStageable(gitFile);
8899
- const unstageable = itemUnstageable(gitFile);
8900
- return b2`
8901
- <button
8902
- type="button"
8903
- class="doorstop-review"
8904
- ?disabled=${reviewed || this.runInProgress !== void 0}
8905
- title=${reviewed ? `${item.uid} is already reviewed against its current fingerprint` : `Mark ${item.uid} reviewed`}
8906
- @click=${() => {
8907
- this.reviewItem(item);
8908
- }}
8909
- >Approve</button>
8910
- <button
8911
- type="button"
8912
- class="doorstop-clear"
8913
- ?disabled=${suspects.length === 0 || this.runInProgress !== void 0}
8914
- title=${suspects.length === 0 ? `No suspect links to clear` : `Re-record the parent fingerprints of ${item.uid}`}
8915
- @click=${() => {
8916
- this.clearSuspects(item, suspectUids);
8917
- }}
8918
- >Clear suspect links</button>
8919
- ${gitStatus === void 0 ? A : b2`<button
8920
- type="button"
8921
- class="doorstop-item-stage"
8922
- ?disabled=${this.runInProgress !== void 0 || !stageable}
8923
- title=${stageable ? `Stage ${item.path}` : `${item.uid} has no unstaged changes \u2014 commit next`}
8924
- @click=${() => {
8925
- this.stageItem(item);
8926
- }}
8927
- >${gitStageIconSvg}Stage</button>
8928
- <button
8929
- type="button"
8930
- class="doorstop-item-unstage"
8931
- ?disabled=${this.runInProgress !== void 0 || !unstageable}
8932
- title=${unstageable ? `Unstage ${item.path} (git reset)` : `${item.uid} has nothing staged`}
8933
- @click=${() => {
8934
- this.unstageItem(item);
8935
- }}
8936
- >${gitUnstageIconSvg}Unstage</button>`}
8937
- <div class="doorstop-op">
8938
- <input
8939
- type="text"
8940
- class="doorstop-target-input"
8941
- data-op="link"
8942
- placeholder="parent UID"
8943
- ${n5(this.linkInputRef)}
8944
- @keydown=${this.onTargetKeydown}
8945
- />
8946
- <button type="button" class="doorstop-link" title=${`doorstop link ${item.uid} <target>`} ?disabled=${this.runInProgress !== void 0} @click=${() => {
8947
- this.runTargetOp("link", this.linkInputRef, item);
8948
- }}>Link</button>
8949
- </div>
8950
- ${this.targetError === void 0 ? A : b2`<span class="doorstop-op-error" role="alert">${this.targetError}</span>`}
8951
- ${this.renderAskMenu(item, index, suspects)}
8952
- `;
8953
- }
8954
- renderAskMenu(item, index, suspects) {
8955
- const children = index.childrenByUid.get(item.uid) ?? [];
8956
- const childPrefix = firstChildDocumentPrefix(index, item);
8957
- return b2`
8958
- <div class="doorstop-menu">
8959
- <button
8960
- type="button"
8961
- class="doorstop-menu-toggle"
8962
- aria-expanded=${this.askMenuOpen ? "true" : "false"}
8963
- @click=${this.onAskMenuToggle}
8964
- >Ask agent</button>
8965
- ${this.askMenuOpen ? b2`
8966
- <div class="doorstop-menu-items" role="menu" aria-label="Ask the agent about ${item.uid}">
8967
- <button type="button" role="menuitem" class="doorstop-menu-item doorstop-explain" @click=${() => {
8968
- this.insertPrompt(explainItemPrompt(item));
8969
- }}>Explain</button>
8970
- <button
8971
- type="button"
8972
- role="menuitem"
8973
- class="doorstop-menu-item doorstop-fix-suspects"
8974
- ?disabled=${suspects.length === 0}
8975
- title=${suspects.length === 0 ? "No suspect links \u2014 nothing to fix" : `Fix the suspect links of ${item.uid}`}
8976
- @click=${() => {
8977
- this.insertPrompt(fixSuspectLinksPrompt(item, suspects));
8978
- }}
8979
- >Fix suspect links</button>
8980
- <button
8981
- type="button"
8982
- role="menuitem"
8983
- class="doorstop-menu-item doorstop-draft-child"
8984
- ?disabled=${childPrefix === void 0}
8985
- title=${childPrefix === void 0 ? "No child document to add to" : `Draft a child item in the ${childPrefix} document`}
8986
- @click=${() => {
8987
- if (childPrefix !== void 0) this.insertPrompt(draftChildRequirementPrompt(item, childPrefix));
8988
- }}
8989
- >Draft child requirement</button>
8990
- <button type="button" role="menuitem" class="doorstop-menu-item doorstop-review-readiness" @click=${() => {
8991
- this.insertPrompt(reviewReadinessPrompt(item, children));
8992
- }}>Review readiness</button>
8993
- </div>
8994
- ` : A}
8995
- </div>
8996
- `;
8997
- }
8998
- /** Stage one item's own file — the same `doorstop.git-stage` operation
8999
- * with `[item.path]`; the shared run dispatch narrates the uid. Shared
9000
- * by the row's `git add` span and the palette's Stage button. */
9689
+ /** section-API: stage one item's own file — the same `doorstop.git-stage`
9690
+ * operation with `[item.path]`; the shared run dispatch narrates the uid.
9691
+ * Shared by the row's `git add` span and the palette's Stage button. */
9001
9692
  stageItem(item) {
9002
9693
  const controller = this.controller;
9003
9694
  if (controller === void 0 || controller.runInProgress !== void 0) return;
9004
9695
  void controller.runGitStage([item.path], `Git: stage ${item.uid}`);
9005
9696
  }
9006
- /** Unstage one item's own file — the `doorstop.git-unstage` inverse with
9007
- * `[item.path]`; the shared run dispatch narrates the uid. Shared by
9008
- * the row's minus span and the palette's Unstage button. The guard
9009
- * mirrors {@link stageItem}: a run already in flight must never be
9697
+ /** section-API: unstage one item's own file — the `doorstop.git-unstage`
9698
+ * inverse with `[item.path]`; the shared run dispatch narrates the uid.
9699
+ * Shared by the row's minus span and the palette's Unstage button. The
9700
+ * guard mirrors {@link stageItem}: a run already in flight must never be
9010
9701
  * overlapped (the disabled states cover pointer clicks; this covers
9011
9702
  * every call path). */
9012
9703
  unstageItem(item) {
@@ -9014,103 +9705,7 @@ function defineDoorstopPanelBodyElement() {
9014
9705
  if (controller === void 0 || controller.runInProgress !== void 0) return;
9015
9706
  void controller.runGitUnstage([item.path], `Git: unstage ${item.uid}`);
9016
9707
  }
9017
- /** Unlink one parent from one item — the same `unlink` run the palette's
9018
- * free-text Unlink button dispatches, but with the target sourced from
9019
- * the index (`link.uid`) instead of an input. The guard mirrors
9020
- * {@link stageItem}: a run already in flight must never be overlapped.
9021
- * There is no `targetError` interaction: an index-sourced UID cannot
9022
- * fail the free-text validation, and `parseDoorstopRunRequest`
9023
- * re-validates server-side. */
9024
- unlinkLink(item, target) {
9025
- const controller = this.controller;
9026
- if (controller === void 0 || controller.runInProgress !== void 0) return;
9027
- this.runDoorstop(
9028
- "unlink",
9029
- `Doorstop: unlink ${item.uid}`,
9030
- { op: "unlink", uid: item.uid, target },
9031
- `doorstop unlink ${item.uid} ${target}`,
9032
- false
9033
- );
9034
- }
9035
- /**
9036
- * The browser must never send an empty commit message: the server
9037
- * rejects it, and git with an empty `-m` would hang the exec until the
9038
- * deadline. The input is cleared only on SUCCESS so a `failed` commit
9039
- * (hook stderr, missing identity) keeps the message for a corrected
9040
- * retry.
9041
- */
9042
- gitCommitSubmit() {
9043
- const controller = this.controller;
9044
- if (controller === void 0) return;
9045
- if (controller.runInProgress !== void 0) return;
9046
- const input = this.gitCommitInputRef.value;
9047
- const message = input?.value.trim() ?? "";
9048
- if (message === "") {
9049
- this.gitActionError = "Enter a commit message";
9050
- input?.focus();
9051
- return;
9052
- }
9053
- this.gitActionError = void 0;
9054
- void controller.runGitCommit(message).then(() => {
9055
- const lastRun = controller.lastRun;
9056
- if (lastRun?.op === "git-commit" && lastRun.status === "ok" && input !== void 0) {
9057
- input.value = "";
9058
- this.gitCommitMessage = "";
9059
- }
9060
- });
9061
- }
9062
- reviewItem(item) {
9063
- const commit = doorstopCommitAfterReview(this.result);
9064
- this.runDoorstop(
9065
- "review",
9066
- `Doorstop: review ${item.uid}`,
9067
- commit ? { op: "review", uid: item.uid, commit: true } : { op: "review", uid: item.uid },
9068
- `doorstop review ${item.uid}`,
9069
- false
9070
- );
9071
- }
9072
- clearSuspects(item, suspectUids) {
9073
- if (suspectUids.length === 0) return;
9074
- this.runDoorstop(
9075
- "clear",
9076
- "Doorstop: clear suspect links",
9077
- { op: "clear", uid: item.uid, parents: suspectUids },
9078
- `doorstop clear ${item.uid} ${suspectUids.join(" ")}`,
9079
- false
9080
- );
9081
- }
9082
- editItem(item) {
9083
- this.runDoorstop(
9084
- "edit",
9085
- `Doorstop: edit ${item.uid}`,
9086
- { op: "edit", uid: item.uid },
9087
- `doorstop edit ${item.uid}`,
9088
- false
9089
- );
9090
- }
9091
- runTargetOp(op, inputRef, item) {
9092
- const input = inputRef.value;
9093
- const target = input?.value.trim() ?? "";
9094
- if (target === "") {
9095
- this.targetError = `Enter a ${op} target UID (e.g. ${item.documentPrefix}0001)`;
9096
- input?.focus();
9097
- return;
9098
- }
9099
- if (!isValidTargetUid(target)) {
9100
- this.targetError = `Invalid ${op} target "${target}" \u2014 expected a Doorstop UID like ${item.documentPrefix}0001 (no spaces, quotes, or special characters)`;
9101
- input?.focus();
9102
- return;
9103
- }
9104
- this.targetError = void 0;
9105
- if (input !== void 0) input.value = "";
9106
- this.runDoorstop(
9107
- op,
9108
- `Doorstop: ${op} ${item.uid}`,
9109
- { op, uid: item.uid, target },
9110
- `doorstop ${op} ${item.uid} ${target}`,
9111
- false
9112
- );
9113
- }
9708
+ /** section-API: insert an Ask-agent prompt and close the menu. */
9114
9709
  insertPrompt(text) {
9115
9710
  const context = this.context;
9116
9711
  if (context === void 0) return;
@@ -9121,9 +9716,9 @@ function defineDoorstopPanelBodyElement() {
9121
9716
  /**
9122
9717
  * The single dispatch surface of every panel action, with TWO paths:
9123
9718
  *
9124
- * - BACKEND path when the workspace is owned by the opendoor provider
9125
- * with an active backend: the STRUCTURED `input` goes through
9126
- * `context.backend.request("doorstop.run", …)` — argv is built
9719
+ * - PEER path when the workspace is owned by the opendoor provider
9720
+ * with an active peer: the STRUCTURED `input` goes through
9721
+ * `context.peer.request("doorstop.run", …)` — argv is built
9127
9722
  * server-side, so the browser never shell-quotes on this path. A
9128
9723
  * rejected request commits `status: "error"` and does NOT
9129
9724
  * invalidate (the run wrote nothing to the workspace).
@@ -9139,8 +9734,8 @@ function defineDoorstopPanelBodyElement() {
9139
9734
  const controller = this.controller;
9140
9735
  if (context === void 0 || controller === void 0) return;
9141
9736
  if (controller.runInProgress !== void 0) return;
9142
- if (context.backend !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID && context.workspace.provider?.capabilities.request !== false) {
9143
- void this.runDoorstopBackend(op, title, input, context.backend);
9737
+ if (context.peer?.request !== void 0 && context.workspace.provider?.pluginId === OPENDOOR_PLUGIN_ID) {
9738
+ void this.runDoorstopBackend(op, title, input, context.peer);
9144
9739
  return;
9145
9740
  }
9146
9741
  const terminal = context.terminal;
@@ -9154,13 +9749,15 @@ function defineDoorstopPanelBodyElement() {
9154
9749
  }).catch(() => {
9155
9750
  });
9156
9751
  }
9157
- async runDoorstopBackend(op, title, input, backend) {
9752
+ async runDoorstopBackend(op, title, input, peer) {
9158
9753
  const controller = this.controller;
9159
9754
  if (controller === void 0) return;
9755
+ const request = peer.request;
9756
+ if (request === void 0) return;
9160
9757
  controller.beginRun(title);
9161
9758
  const startedAt = Date.now();
9162
9759
  try {
9163
- const response = await backend.request(DOORSTOP_RUN_OPERATION, input);
9760
+ const response = await request(DOORSTOP_RUN_OPERATION, input);
9164
9761
  const parsed = parseDoorstopRunResponse(response);
9165
9762
  const status = parsed.signal !== null ? "killed" : parsed.exitCode === 0 ? "ok" : "failed";
9166
9763
  controller.commitRun({
@@ -9200,12 +9797,21 @@ function defineDoorstopPanelBodyElement() {
9200
9797
  controller.endRun();
9201
9798
  }
9202
9799
  }
9800
+ /** section-API: the currently selected item, resolved against the
9801
+ * loaded index. */
9203
9802
  selectedItem() {
9204
9803
  const result = this.result;
9205
9804
  const uid = this.selectedUid;
9206
9805
  if (result === void 0 || uid === void 0) return void 0;
9207
9806
  return result.index.byUid.get(uid);
9208
9807
  }
9808
+ /** section-API: the shared state chip (item rows + detail head). The
9809
+ * renderer is a free function in `sections/item-list.ts`; this delegate
9810
+ * lets detail-pane reach it through the host, keeping the runtime import
9811
+ * graph one-way (coordinator → sections). */
9812
+ renderStateChip(key) {
9813
+ return renderStateChip(key);
9814
+ }
9209
9815
  }
9210
9816
  __decorateClass([
9211
9817
  n6({ attribute: false })
@@ -9213,6 +9819,9 @@ function defineDoorstopPanelBodyElement() {
9213
9819
  __decorateClass([
9214
9820
  n6({ attribute: false })
9215
9821
  ], DoorstopPanelBodyElement.prototype, "context", 2);
9822
+ __decorateClass([
9823
+ n6({ attribute: false })
9824
+ ], DoorstopPanelBodyElement.prototype, "markdownRenderer", 2);
9216
9825
  __decorateClass([
9217
9826
  n6({ attribute: false })
9218
9827
  ], DoorstopPanelBodyElement.prototype, "result", 2);
@@ -9285,18 +9894,19 @@ function defineCustomElementOnce(tag, define) {
9285
9894
  define();
9286
9895
  }
9287
9896
 
9288
- // src/doorstop-contributions.ts
9289
- var DOORSTOP_PANEL_LOCAL_ID = "workspace.doorstop";
9897
+ // src/browser/doorstop-contributions.ts
9898
+ var DOORSTOP_PANEL_LOCAL_ID2 = "workspace.doorstop";
9290
9899
  var DOORSTOP_LABEL_STATE_LIMIT = 8;
9291
- function createOpendoorBrowserContributions(runtimePluginId, _html, _svg) {
9292
- const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID}`;
9900
+ function createOpendoorBrowserContributions(runtimePluginId, _html, _svg, markdown) {
9901
+ const panelId = `${runtimePluginId}:${DOORSTOP_PANEL_LOCAL_ID2}`;
9293
9902
  const registry = new DoorstopWorkspaceRegistry();
9294
9903
  const labelCache = new DoorstopLabelCache();
9295
9904
  defineDoorstopPanelElements();
9296
9905
  return {
9297
9906
  actions: createDoorstopActions(panelId),
9298
- workspacePanels: [createDoorstopPanel(registry)],
9299
- workspaceLabels: [createDoorstopLabel(labelCache)]
9907
+ workspacePanels: [createDoorstopPanel(registry, markdown)],
9908
+ workspaceLabels: [createDoorstopLabel(labelCache)],
9909
+ sessionLabels: [createDoorstopSessionLabel(runtimePluginId)]
9300
9910
  };
9301
9911
  }
9302
9912
  function createDoorstopActions(panelId) {
@@ -9315,7 +9925,7 @@ function createDoorstopActions(panelId) {
9315
9925
  // Enabled unconditionally: the Requirements panel is useful for every
9316
9926
  // workspace (paired or unpaired — it needs no owned provider).
9317
9927
  run: (context) => {
9318
- context.selectMainView(panelId);
9928
+ context.selectWorkspaceTool(panelId);
9319
9929
  }
9320
9930
  },
9321
9931
  {
@@ -9334,9 +9944,9 @@ function createDoorstopActions(panelId) {
9334
9944
  }
9335
9945
  ];
9336
9946
  }
9337
- function createDoorstopPanel(registry) {
9947
+ function createDoorstopPanel(registry, markdown) {
9338
9948
  return {
9339
- id: DOORSTOP_PANEL_LOCAL_ID,
9949
+ id: DOORSTOP_PANEL_LOCAL_ID2,
9340
9950
  title: "Requirements",
9341
9951
  // Door/archive icon (currentColor): a filing cabinet with the doorstop
9342
9952
  // checklist mark — the panel title icon, created with this module's own
@@ -9348,22 +9958,58 @@ function createDoorstopPanel(registry) {
9348
9958
  <path d="m9 12 2 2 4-4"></path>
9349
9959
  </svg>
9350
9960
  `,
9351
- order: 60,
9961
+ // Plan Phase C step C3 follow-up — order 5: the host's chat-file-link
9962
+ // dispatch (resolveWorkspaceFileOpen) opens the FIRST ACCEPTING panel,
9963
+ // iterating panels by `order` ascending, and the bundled Files panel is
9964
+ // order 10 and answers `{ file: path }` for EVERY path unconditionally —
9965
+ // at order 60 this panel's `fileOpenQuery` was never consulted at all.
9966
+ // Sitting below 10 is the only host-supported way to claim doorstop item
9967
+ // files for the Requirements panel; the accepted consequence is that the
9968
+ // Requirements tab precedes Files in the workspace tab bar.
9969
+ order: 5,
9352
9970
  // The panel is always visible (paired or unpaired): when the workspace
9353
9971
  // is opendoor-owned the run actions use the backend, otherwise the
9354
9972
  // terminal fallback keeps every action working; the discovery-based
9355
9973
  // empty state carries the "no documents" story (feature spec §7.1).
9356
9974
  visible: () => true,
9357
- onInvalidate: (context) => registry.invalidate(context),
9358
- render: (context) => renderDoorstopPanel(registry, context)
9975
+ // Plan Phase C step C1 — typed invalidation (browser API v4, unchanged
9976
+ // on v5): declare the fixed workspace resources whose automatic invalidations this panel
9977
+ // receives. With `workspace.files` declared, the host calls onInvalidate
9978
+ // not only for the manual Refresh action (refreshWorkspacePanels) but
9979
+ // also when workspace files change (`reason: "mutation"` — e.g. the
9980
+ // review-commit pipeline's commits landing, or any other plugin's file
9981
+ // writes) and when agent work finishes in the workspace (`reason:
9982
+ // "agent-activity"` — an agent editing doorstop documents refreshes the
9983
+ // panel without a manual Refresh). The reason is forwarded to the
9984
+ // registry, which rescans unconditionally (see
9985
+ // DoorstopWorkspaceRegistry.invalidate).
9986
+ invalidationResources: ["workspace.files"],
9987
+ onInvalidate: (context, invalidation) => registry.invalidate(context, invalidation),
9988
+ // Plan Phase C step C3 — chat file links: when the host resolves a
9989
+ // Markdown link to a workspace-relative path, this hook answers with the
9990
+ // panel's navigation query IF the panel owns the path (its last landed
9991
+ // discovery indexed it as an item file), so the chat opens the item IN
9992
+ // the Requirements panel instead of the Files fallback. Synchronous and
9993
+ // side-effect-free: it answers purely from the registry's recorded index
9994
+ // (never loads, never renders); unknown paths and pre-first-load panels
9995
+ // return undefined (the host asks the next panel / serves Files).
9996
+ fileOpenQuery: (context, path) => registry.fileOpenQuery(context, path),
9997
+ render: (context) => renderDoorstopPanel(registry, context, markdown)
9359
9998
  };
9360
9999
  }
9361
- function renderDoorstopPanel(registry, context) {
10000
+ function renderDoorstopPanel(registry, context, markdown) {
9362
10001
  const controller = registry.for(context);
10002
+ if (context.navigation !== void 0) {
10003
+ controller.applyNavigationQuery(readPanelNavigationQuery(context.navigation));
10004
+ }
10005
+ if (controller.result !== void 0) {
10006
+ registry.recordFileOpenIndex(context, controller.result);
10007
+ }
9363
10008
  return b2`
9364
10009
  <pi-web-opendoor-panel-body
9365
10010
  .controller=${controller}
9366
10011
  .context=${context}
10012
+ .markdownRenderer=${markdown.renderer}
9367
10013
  .result=${controller.result}
9368
10014
  .loading=${controller.loading}
9369
10015
  .stale=${controller.stale}
@@ -9458,13 +10104,36 @@ function doorstopLabelKey(context) {
9458
10104
  return JSON.stringify([context.machine.id, context.workspace.projectId, context.workspace.id]);
9459
10105
  }
9460
10106
 
9461
- // src/pi-web-plugin.ts
10107
+ // src/browser/doorstop-markdown.ts
10108
+ var DOORSTOP_MARKDOWN_CAPABILITY = {
10109
+ pluginId: "pi-web",
10110
+ id: "markdown-rendering",
10111
+ version: 1,
10112
+ parse(value) {
10113
+ if (typeof value !== "object" || value === null || !("render" in value) || typeof value.render !== "function") {
10114
+ throw new Error("Markdown rendering capability v1 is unavailable");
10115
+ }
10116
+ return value;
10117
+ }
10118
+ };
10119
+ function createDoorstopMarkdownHolder() {
10120
+ return { renderer: void 0 };
10121
+ }
10122
+
10123
+ // src/browser/pi-web-plugin.ts
9462
10124
  var plugin = {
9463
- apiVersion: 2,
10125
+ apiVersion: 5,
9464
10126
  name: "Opendoor",
9465
- activate: ({ runtimePluginId, html, svg: svg2 }) => ({
9466
- contributions: createOpendoorBrowserContributions(runtimePluginId, html, svg2)
9467
- })
10127
+ requires: [DOORSTOP_MARKDOWN_CAPABILITY],
10128
+ activate: ({ runtimePluginId, html, svg }) => {
10129
+ const markdown = createDoorstopMarkdownHolder();
10130
+ return {
10131
+ contributions: createOpendoorBrowserContributions(runtimePluginId, html, svg, markdown),
10132
+ start: ({ capabilities }) => {
10133
+ markdown.renderer = capabilities.resolve(DOORSTOP_MARKDOWN_CAPABILITY);
10134
+ }
10135
+ };
10136
+ }
9468
10137
  };
9469
10138
  var pi_web_plugin_default = plugin;
9470
10139
  export {
@@ -9484,7 +10153,6 @@ lit-html/lit-html.js:
9484
10153
  lit-element/lit-element.js:
9485
10154
  lit-html/directive.js:
9486
10155
  lit-html/async-directive.js:
9487
- lit-html/directives/repeat.js:
9488
10156
  @lit/reactive-element/decorators/custom-element.js:
9489
10157
  @lit/reactive-element/decorators/property.js:
9490
10158
  @lit/reactive-element/decorators/state.js:
@@ -9494,6 +10162,7 @@ lit-html/directives/repeat.js:
9494
10162
  @lit/reactive-element/decorators/query-all.js:
9495
10163
  @lit/reactive-element/decorators/query-async.js:
9496
10164
  @lit/reactive-element/decorators/query-assigned-nodes.js:
10165
+ lit-html/directives/repeat.js:
9497
10166
  (**
9498
10167
  * @license
9499
10168
  * Copyright 2017 Google LLC
@@ -9507,13 +10176,6 @@ lit-html/is-server.js:
9507
10176
  * SPDX-License-Identifier: BSD-3-Clause
9508
10177
  *)
9509
10178
 
9510
- lit-html/directives/class-map.js:
9511
- (**
9512
- * @license
9513
- * Copyright 2018 Google LLC
9514
- * SPDX-License-Identifier: BSD-3-Clause
9515
- *)
9516
-
9517
10179
  lit-html/directive-helpers.js:
9518
10180
  lit-html/directives/ref.js:
9519
10181
  (**
@@ -9522,11 +10184,18 @@ lit-html/directives/ref.js:
9522
10184
  * SPDX-License-Identifier: BSD-3-Clause
9523
10185
  *)
9524
10186
 
9525
- lit-html/directives/keyed.js:
9526
10187
  @lit/reactive-element/decorators/query-assigned-elements.js:
10188
+ lit-html/directives/keyed.js:
9527
10189
  (**
9528
10190
  * @license
9529
10191
  * Copyright 2021 Google LLC
9530
10192
  * SPDX-License-Identifier: BSD-3-Clause
9531
10193
  *)
10194
+
10195
+ lit-html/directives/class-map.js:
10196
+ (**
10197
+ * @license
10198
+ * Copyright 2018 Google LLC
10199
+ * SPDX-License-Identifier: BSD-3-Clause
10200
+ *)
9532
10201
  */