weir-harness 1.2.3 → 2.1.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.
Files changed (117) hide show
  1. package/cordis.patch.yml +7 -18
  2. package/lib/client.capability-badge.js +2 -0
  3. package/lib/client.capability-manager.js +46 -179
  4. package/lib/client.capability-model.js +428 -3
  5. package/lib/client.file-lease-badge.js +307 -0
  6. package/lib/client.file-lease-tool-model.js +189 -0
  7. package/lib/client.file-lease-tool-view.js +434 -0
  8. package/lib/client.js +124 -36
  9. package/lib/client.mcp-manager.js +563 -0
  10. package/lib/client.settings-page.js +10 -34
  11. package/package.json +2 -2
  12. package/skills/mcp-server-setup/SKILL.md +124 -0
  13. package/src/capabilities/apply-engine.js +18 -4
  14. package/src/capabilities/capability-remote.js +296 -45
  15. package/src/capabilities/initial-selection.js +3 -2
  16. package/src/capabilities/lifecycle-preload.js +69 -11
  17. package/src/capabilities/lifecycle-snapshot.js +127 -39
  18. package/src/capabilities/mcp-admission.js +2 -2
  19. package/src/capabilities/mcp-config.js +344 -0
  20. package/src/capabilities/mcp-credentials.js +180 -0
  21. package/src/capabilities/mcp-diagnostics.js +169 -0
  22. package/src/capabilities/mcp-drain.js +18 -4
  23. package/src/capabilities/mcp-facade.js +3 -3
  24. package/src/capabilities/mcp-gate.js +51 -52
  25. package/src/capabilities/mcp-install-tool.js +276 -0
  26. package/src/capabilities/mcp-manager.js +368 -142
  27. package/src/capabilities/mcp-mount.js +219 -79
  28. package/src/capabilities/mcp-operations.js +359 -0
  29. package/src/capabilities/mcp-process.js +43 -0
  30. package/src/capabilities/mcp-registry.js +74 -60
  31. package/src/capabilities/mcp-runtime.js +103 -0
  32. package/src/capabilities/read-payloads.js +8 -3
  33. package/src/capabilities/skill-applicability.js +185 -0
  34. package/src/capabilities/skill-builtin-migration.js +2 -1
  35. package/src/capabilities/skill-selection-plugin.js +161 -56
  36. package/src/capabilities/store/checked-contract.d.ts +2 -2
  37. package/src/capabilities/store/fs-adapter.js +2 -3
  38. package/src/context-guard/index.js +2 -2
  39. package/src/delegate/supervision-mount.js +5 -5
  40. package/src/delegate/tool.js +2 -10
  41. package/src/file-lease/client.js +1299 -0
  42. package/src/file-lease/index.js +34 -0
  43. package/src/file-lease/lineage.js +207 -0
  44. package/src/file-lease/node-contracts.d.ts +42 -0
  45. package/src/file-lease/plugin.js +1013 -0
  46. package/src/file-lease/singleton.js +155 -0
  47. package/src/file-lease/spawn.js +62 -0
  48. package/src/file-lease/tools.js +847 -0
  49. package/src/file-lease/view.js +144 -0
  50. package/src/file-lease/watch.js +473 -0
  51. package/src/hashline-edit/index.js +76 -42
  52. package/src/hashline-edit/write-tool.js +90 -0
  53. package/src/intent-gate/index.js +29 -12
  54. package/src/intent-gate/matcher.js +36 -2
  55. package/src/lease-broker/entry.js +98 -0
  56. package/src/lease-broker/node-contracts.d.ts +63 -0
  57. package/src/lease-broker/protocol.js +237 -0
  58. package/src/lease-broker/server.js +402 -0
  59. package/src/lease-broker/state.js +482 -0
  60. package/src/lsp/tools.js +137 -27
  61. package/src/process/index.js +4 -2
  62. package/src/settings/index.js +2 -44
  63. package/src/settings/sections.js +2 -24
  64. package/src/shared/audit.js +9 -1
  65. package/src/shared/child-scope.js +2 -0
  66. package/src/todo-driver/index.js +1 -6
  67. package/src/worktree/cards.js +6 -21
  68. package/src/worktree/lane-core.js +1 -19
  69. package/src/worktree/lane-workflows.js +11 -32
  70. package/src/worktree/rules.js +1 -1
  71. package/lib/client.edit-lock-maintenance.js +0 -592
  72. package/lib/client.edit-lock-panel.js +0 -405
  73. package/src/edit-lock/adapter.js +0 -57
  74. package/src/edit-lock/admin-ledger.js +0 -149
  75. package/src/edit-lock/admin-recovery.js +0 -151
  76. package/src/edit-lock/admission.js +0 -82
  77. package/src/edit-lock/binding-attestation.js +0 -29
  78. package/src/edit-lock/binding-guidance.js +0 -72
  79. package/src/edit-lock/binding-machine.js +0 -312
  80. package/src/edit-lock/call-context.js +0 -65
  81. package/src/edit-lock/derived-facts.js +0 -67
  82. package/src/edit-lock/domain.js +0 -165
  83. package/src/edit-lock/domains.js +0 -170
  84. package/src/edit-lock/host.js +0 -149
  85. package/src/edit-lock/incarnations.js +0 -150
  86. package/src/edit-lock/index.js +0 -1303
  87. package/src/edit-lock/inspect.js +0 -219
  88. package/src/edit-lock/lifecycle.js +0 -579
  89. package/src/edit-lock/limits.js +0 -61
  90. package/src/edit-lock/maintenance.js +0 -310
  91. package/src/edit-lock/manager.js +0 -875
  92. package/src/edit-lock/negotiation.js +0 -132
  93. package/src/edit-lock/operation-history.js +0 -214
  94. package/src/edit-lock/peer-client.js +0 -86
  95. package/src/edit-lock/peer-transport.js +0 -76
  96. package/src/edit-lock/peer.js +0 -87
  97. package/src/edit-lock/publisher.js +0 -191
  98. package/src/edit-lock/read-authority.js +0 -101
  99. package/src/edit-lock/recovery.js +0 -139
  100. package/src/edit-lock/remote.js +0 -362
  101. package/src/edit-lock/request-binding.js +0 -42
  102. package/src/edit-lock/request-data.js +0 -40
  103. package/src/edit-lock/reservation.js +0 -301
  104. package/src/edit-lock/reserved-runtime.js +0 -45
  105. package/src/edit-lock/resource-identity.js +0 -130
  106. package/src/edit-lock/runtime.js +0 -145
  107. package/src/edit-lock/settle.js +0 -144
  108. package/src/edit-lock/snapshot.js +0 -175
  109. package/src/edit-lock/stale-sweep.js +0 -120
  110. package/src/edit-lock/state.js +0 -462
  111. package/src/edit-lock/store.js +0 -327
  112. package/src/edit-lock/tool-scope.js +0 -29
  113. package/src/edit-lock/view.js +0 -170
  114. package/src/edit-lock/write-scopes.js +0 -48
  115. package/src/edit-lock/write-tool.js +0 -45
  116. package/src/shared/delegate-edit-lifecycle.js +0 -36
  117. package/src/worktree/authority.js +0 -56
@@ -0,0 +1,307 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "weir-harness",
3
+ chunk: "client.file-lease-badge.js",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ // ---- Edit-lock Badge (conversation composer bar) ----
11
+ // The ONLY user-visible surface of the edit-lock mechanism (design D13).
12
+ // It is read-only by construction, not by convention:
13
+ //
14
+ // - the badge has exactly three states — idle, holding N, unprotected
15
+ // — and each one resolves by itself (turn end, expiry, negotiation).
16
+ // None of them asks the user for anything. The badge names the
17
+ // surface, and appends a fact only when there is one: how many files
18
+ // this session holds, or that protection is off. An idle badge is
19
+ // just its name — never a status word standing in for "nothing";
20
+ // - the panel speaks only when there is something to say. With no
21
+ // claim and no degraded protection it is ONE line; each group's
22
+ // heading appears only above a non-empty list, and the protection
23
+ // notice only when protection is degraded. There are no headings over
24
+ // empty lists and no disclaimer about what cannot be clicked: a
25
+ // surface with no controls needs no sentence saying so;
26
+ // - it contains NO button besides its own toggle, NO command input, NO
27
+ // confirmation step and no clickable row. Expand/collapse is the one
28
+ // interaction in this chunk; every ownership change belongs to the
29
+ // model's tools. A control here would reintroduce the "some state
30
+ // needs a human" that the rebuild exists to delete;
31
+ // - it never writes to the conversation and never notifies. The read
32
+ // goes to the one read-only endpoint through the composition root's
33
+ // `fetchView` prop; nothing else is called.
34
+ //
35
+ // Everything shown arrives pre-shaped by the server-side view
36
+ // (`src/file-lease/view.js`): short path names, holder labels, and how
37
+ // long a retention has left. This chunk renders that and derives only
38
+ // the presentation state — it never parses prose and never invents a
39
+ // fact.
40
+ const STATE_NONE = "none";
41
+ const STATE_HOLDING = "holding";
42
+ const STATE_DEGRADED = "degraded";
43
+ /** Status-dot colour per state; only theme-defined tokens. */
44
+ const dotColor = {
45
+ none: "var(--dsw-alias-label-tertiary)",
46
+ holding: "var(--dsw-alias-state-business-primary)",
47
+ degraded: "var(--dsw-alias-state-warn-primary)"
48
+ };
49
+ const rootStyle = { position: "relative", display: "inline-flex" };
50
+ const badgeStyle = {
51
+ display: "inline-flex",
52
+ alignItems: "center",
53
+ gap: "6px",
54
+ background: "none",
55
+ border: "1px solid var(--dsw-alias-border-l2)",
56
+ borderRadius: "var(--dsw-radius-sm)",
57
+ cursor: "pointer",
58
+ padding: "3px 8px",
59
+ fontSize: "12px",
60
+ lineHeight: "16px",
61
+ color: "var(--dsw-alias-label-secondary)"
62
+ };
63
+ // Popover surface: the theme's documented overlay token (light and dark).
64
+ const panelStyle = {
65
+ position: "absolute",
66
+ bottom: "calc(100% + 6px)",
67
+ right: 0,
68
+ zIndex: 20,
69
+ width: "min(420px, 80vw)",
70
+ maxHeight: "50vh",
71
+ overflow: "auto",
72
+ background: "var(--dsw-alias-bg-overlay)",
73
+ border: "1px solid var(--dsw-alias-border-l2)",
74
+ borderRadius: "var(--dsw-radius-md, 8px)",
75
+ boxShadow: "0 6px 24px var(--dsw-alias-border-l2)",
76
+ padding: "10px 12px",
77
+ color: "var(--dsw-alias-label-primary)",
78
+ fontSize: "12px",
79
+ lineHeight: "18px"
80
+ };
81
+ const mutedStyle = { color: "var(--dsw-alias-label-secondary)" };
82
+ /** A group heading: bold, and spaced only when something precedes it. */
83
+ const headStyle = { fontWeight: 600 };
84
+ // The degraded notice: amber (the same token the dot uses) at the panel's
85
+ // own weight — a status line, not an alarm.
86
+ const noticeStyle = { color: "var(--dsw-alias-state-warn-primary)" };
87
+ const rowStyle = { display: "flex", flexWrap: "wrap", alignItems: "baseline", gap: "8px", padding: "3px 0", borderTop: "1px solid var(--dsw-alias-border-l2)" };
88
+ const nameStyle = { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--dsw-font-markdown-code-block-font-family, ui-monospace, monospace)" };
89
+ const dot = (state) => react_jsx_runtime.jsx("span", { "aria-hidden": true, style: { display: "inline-block", width: "7px", height: "7px", borderRadius: "50%", background: dotColor[state], flex: "none" } });
90
+ /**
91
+ * The badge state of a view. Anything unreadable is `degraded`: an
92
+ * unreadable surface must never be presented as "nothing held", which
93
+ * reads like a fact the panel cannot actually vouch for.
94
+ * @param {any} view the server-shaped view
95
+ * @returns {"none" | "holding" | "degraded"}
96
+ */
97
+ function stateOf(view) {
98
+ if (!view || typeof view !== "object") return STATE_DEGRADED;
99
+ if (view.arbiter !== "ok") return STATE_DEGRADED;
100
+ const mine = Array.isArray(view.mine) ? view.mine : [];
101
+ return mine.length > 0 ? STATE_HOLDING : STATE_NONE;
102
+ }
103
+ /** The claims of one section, tolerating a partial/degraded payload. */
104
+ function rowsOf(view, key) {
105
+ const rows = view && Array.isArray(view[key]) ? view[key] : [];
106
+ return rows.filter((row) => row && typeof row === "object" && typeof row.path === "string" && row.path !== "");
107
+ }
108
+ /** Remaining retention in whole minutes, or null when there is none. */
109
+ function retainedMinutes(holdUntil, at) {
110
+ if (typeof holdUntil !== "number" || !Number.isFinite(holdUntil)) return null;
111
+ const minutes = Math.max(0, Math.ceil((holdUntil - at) / 60000));
112
+ return minutes > 0 ? minutes : null;
113
+ }
114
+ function FileLeaseBadge(props) {
115
+ const t = props.t;
116
+ const sessionId = props.sessionId;
117
+ const [open, setOpen] = react.useState(false);
118
+ // `any`: the payload is the server-shaped view, narrowed on read.
119
+ const [view, setView] = react.useState(/** @type {any} */ (null));
120
+ // The one read: on mount so the badge is right before it is ever
121
+ // expanded, and again each time the panel opens (a snapshot, never a
122
+ // poll — ownership has no state to keep in sync).
123
+ const read = () => {
124
+ if (typeof props.fetchView !== "function") {
125
+ setView({ schemaVersion: 1, arbiter: "unavailable", mine: [], others: [] });
126
+ return Promise.resolve();
127
+ }
128
+ return Promise.resolve(props.fetchView()).then(
129
+ (next) => { if (next && typeof next === "object") setView(next); },
130
+ // A failed read is the degraded state: the panel cannot vouch
131
+ // for anything, so it says so instead of showing an empty list.
132
+ () => { setView({ schemaVersion: 1, arbiter: "unavailable", mine: [], others: [] }); }
133
+ );
134
+ };
135
+ react.useEffect(() => {
136
+ if (!sessionId) {
137
+ setView(null);
138
+ setOpen(false);
139
+ return undefined;
140
+ }
141
+ read();
142
+ return undefined;
143
+ }, [sessionId]);
144
+ // Close on Escape and on any pointerdown outside this entry's wrapper:
145
+ // the same dismissal the other composer surfaces use, and the only way
146
+ // out of the panel that needs no action model behind it.
147
+ react.useEffect(() => {
148
+ if (!open || typeof document === "undefined") return undefined;
149
+ const onPointerDown = (event) => {
150
+ if (event && typeof event.target?.closest === "function" && event.target.closest("[data-weir-file-lease-root]")) return;
151
+ setOpen(false);
152
+ };
153
+ const onKeyDown = (event) => {
154
+ if (event?.key === "Escape") setOpen(false);
155
+ };
156
+ document.addEventListener("pointerdown", onPointerDown);
157
+ document.addEventListener("keydown", onKeyDown);
158
+ return () => {
159
+ document.removeEventListener("pointerdown", onPointerDown);
160
+ document.removeEventListener("keydown", onKeyDown);
161
+ };
162
+ }, [open]);
163
+ if (!sessionId) return null;
164
+ // Before the first read settles the entry shows the idle state:
165
+ // ownership does not survive the process (D10), so "nothing held" is
166
+ // the honest answer for a session that has not been read yet.
167
+ const state = view === null ? STATE_NONE : stateOf(view);
168
+ const mine = rowsOf(view, "mine");
169
+ const others = rowsOf(view, "others");
170
+ // The badge suffix: a fact when there is one, nothing when there is
171
+ // not. An idle badge never says "not holding" — it simply is idle.
172
+ const suffix = state === STATE_HOLDING
173
+ ? t("fileLeaseState_holding").replace("{n}", String(mine.length))
174
+ : state === STATE_DEGRADED ? t("fileLeaseState_degraded") : "";
175
+ const arbiter = view && typeof view.arbiter === "string" ? view.arbiter : "unavailable";
176
+ const at = Date.now();
177
+ const toggle = () => {
178
+ const next = !open;
179
+ setOpen(next);
180
+ if (next) read();
181
+ };
182
+ const row = (entry, key, trailing) => react_jsx_runtime.jsxs("div", {
183
+ style: rowStyle,
184
+ "data-weir-file-lease-row": key,
185
+ children: [
186
+ react_jsx_runtime.jsx("span", { style: nameStyle, title: entry.path, children: entry.path }),
187
+ ...trailing
188
+ ]
189
+ }, `${key}:${entry.path}`);
190
+ const mineRows = mine.map((entry) => {
191
+ const trailing = [];
192
+ const minutes = retainedMinutes(entry.holdUntil, at);
193
+ if (minutes !== null) {
194
+ trailing.push(react_jsx_runtime.jsx("span", { style: mutedStyle, "data-weir-file-lease-retained": entry.path, children: t("fileLeaseRetainedFor").replace("{n}", String(minutes)) }, "retained"));
195
+ }
196
+ return row(entry, "mine", trailing);
197
+ });
198
+ const otherRows = others.map((entry) => {
199
+ const label = entry.holder && typeof entry.holder.label === "string" ? entry.holder.label : "";
200
+ return row(entry, "others", [react_jsx_runtime.jsx("span", { style: mutedStyle, "data-weir-file-lease-holder": entry.path, children: label }, "holder")]);
201
+ });
202
+ // The panel is assembled as blocks so that spacing, and the existence
203
+ // of a heading, both follow from what is actually there.
204
+ const blocks = [];
205
+ if (state === STATE_DEGRADED) {
206
+ blocks.push(react_jsx_runtime.jsx("div", {
207
+ style: noticeStyle,
208
+ "data-weir-file-lease-arbiter": arbiter,
209
+ children: t("fileLeaseDegradedHint")
210
+ }, "notice"));
211
+ }
212
+ const head = (key, text) => react_jsx_runtime.jsx("div", {
213
+ style: blocks.length === 0 ? headStyle : { ...headStyle, marginTop: "10px" },
214
+ children: text
215
+ }, `${key}-head`);
216
+ if (mineRows.length > 0) {
217
+ blocks.push(head("mine", t("fileLeaseMine")));
218
+ blocks.push(react_jsx_runtime.jsx("div", { "data-weir-file-lease-section": "mine", children: mineRows }, "mine"));
219
+ }
220
+ if (otherRows.length > 0) {
221
+ blocks.push(head("others", t("fileLeaseOthers")));
222
+ blocks.push(react_jsx_runtime.jsx("div", { "data-weir-file-lease-section": "others", children: otherRows }, "others"));
223
+ }
224
+ // Nothing held anywhere: one plain line, and (when protection is
225
+ // degraded) one notice above it — never a section skeleton.
226
+ if (mineRows.length === 0 && otherRows.length === 0) {
227
+ blocks.push(react_jsx_runtime.jsx("div", {
228
+ style: blocks.length === 0 ? mutedStyle : { ...mutedStyle, marginTop: "6px" },
229
+ "data-weir-file-lease-empty": "",
230
+ children: t("fileLeaseEmpty")
231
+ }, "empty"));
232
+ }
233
+ return react_jsx_runtime.jsxs("span", {
234
+ style: rootStyle,
235
+ "data-weir-file-lease-root": "",
236
+ children: [
237
+ react_jsx_runtime.jsxs("button", {
238
+ type: "button",
239
+ style: badgeStyle,
240
+ onClick: toggle,
241
+ "aria-expanded": open,
242
+ "data-weir-file-lease": "",
243
+ "data-weir-file-lease-state": state,
244
+ title: t("fileLeaseTitle"),
245
+ children: [
246
+ dot(state),
247
+ react_jsx_runtime.jsx("span", { children: t("fileLeaseLabel") }),
248
+ suffix === "" ? null : react_jsx_runtime.jsx("span", { "data-weir-file-lease-summary": "", children: suffix })
249
+ ]
250
+ }),
251
+ open ? react_jsx_runtime.jsx("div", {
252
+ role: "dialog",
253
+ "aria-label": t("fileLeasePanelTitle"),
254
+ style: panelStyle,
255
+ "data-weir-file-lease-panel": "",
256
+ onKeyDown: (event) => { if (event?.key === "Escape") setOpen(false); },
257
+ children: blocks
258
+ }) : null
259
+ ]
260
+ });
261
+ }
262
+ // Locale data face (pure data, NO ctx calls — a chunk factory re-executes
263
+ // under HMR, so a chunk-local register would throw on the duplicate
264
+ // (namespace, locale) pair): the entry registers these under
265
+ // "settings.weir.file-lease" inside a ctx.effect when this chunk arrives
266
+ // (lib/client.js registerChunkDictionaries). Pinned by test/client.test.js's
267
+ // dictionary union pin.
268
+ //
269
+ // Copy rules: plain user-facing words only — no internal vocabulary, no
270
+ // "read-only" disclaimers, no status word for the idle state. Badge
271
+ // suffixes carry their own leading separator so the badge reads as one
272
+ // string in both locales.
273
+ const dictionaries = {
274
+ en: {
275
+ fileLeaseLabel: "Edit Lock",
276
+ fileLeaseTitle: "Files in use",
277
+ fileLeasePanelTitle: "Files in use",
278
+ fileLeaseState_holding: " · {n}",
279
+ fileLeaseState_degraded: " · unprotected",
280
+ fileLeaseMine: "You are editing",
281
+ fileLeaseOthers: "Another session is editing",
282
+ fileLeaseEmpty: "No files in use.",
283
+ fileLeaseDegradedHint: "Protection is temporarily unavailable; editing continues.",
284
+ fileLeaseRetainedFor: "{n} min left"
285
+ },
286
+ zh: {
287
+ fileLeaseLabel: "编辑锁",
288
+ fileLeaseTitle: "文件占用",
289
+ fileLeasePanelTitle: "文件占用",
290
+ fileLeaseState_holding: " · {n}",
291
+ fileLeaseState_degraded: " · 未加锁",
292
+ fileLeaseMine: "你正在编辑",
293
+ fileLeaseOthers: "其他会话正在编辑",
294
+ fileLeaseEmpty: "暂无文件占用。",
295
+ fileLeaseDegradedHint: "保护暂不可用,编辑不受影响。",
296
+ fileLeaseRetainedFor: "保留 {n} 分钟"
297
+ }
298
+ };
299
+
300
+ exports.dictionaries = dictionaries;
301
+ exports.FileLeaseBadge = FileLeaseBadge;
302
+ exports.stateOf = stateOf;
303
+ exports.rowsOf = rowsOf;
304
+ exports.retainedMinutes = retainedMinutes;
305
+ return module.exports;
306
+ }
307
+ });
@@ -0,0 +1,189 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "weir-harness",
3
+ chunk: "client.file-lease-tool-model.js",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ // Zero-dependency view-model chunk for the edit-lock conversation cards
9
+ // (openspec edit-lock-tool-cards, design D2): the factory never calls
10
+ // require. Every helper is pure and defensive — persisted metadata is
11
+ // replayed from session logs that may predate this card (no meta at all)
12
+ // or carry a shape this build does not know, and either case must narrow
13
+ // to null so the view renders its argument/raw-output fallback instead of
14
+ // throwing.
15
+ const LEASE_META_VERSION = 1;
16
+ // The closed state set per tool. Mirrors LEASE_META_STATES in
17
+ // src/file-lease/tools.js (the producer); a state outside it is malformed.
18
+ const LEASE_TOOL_STATES = {
19
+ edit_lock_acquire: ["claimed", "degraded"],
20
+ edit_lock_release: ["released", "degraded"],
21
+ edit_lock_hold: ["held", "degraded"],
22
+ edit_lock_status: ["status", "degraded"],
23
+ edit_lock_try_steal: ["pending", "free", "self", "degraded"],
24
+ edit_lock_reply: ["transferred", "declined", "degraded"],
25
+ edit_lock_watch: ["watching", "free", "self", "released", "reset", "unsupported", "cancelled", "degraded"]
26
+ };
27
+ const LEASE_TOOLS = Object.keys(LEASE_TOOL_STATES);
28
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ const isText = (value) => typeof value === "string" && value !== "";
30
+ const isInstant = (value) => typeof value === "number" && Number.isFinite(value);
31
+ const textList = (value) => (Array.isArray(value) ? value.filter(isText) : []);
32
+ const holderOf = (value) => (isRecord(value) && isText(value.label) && isText(value.session) ? { label: value.label, session: value.session } : null);
33
+ /**
34
+ * Narrow a persisted `block.meta` to the card's view of one outcome, or
35
+ * null when it is absent, of another version, names another tool, or
36
+ * carries a state outside that tool's closed set.
37
+ */
38
+ function narrowLeaseMeta(meta) {
39
+ const raw = isRecord(meta) ? meta.editLock : undefined;
40
+ if (!isRecord(raw) || raw.v !== LEASE_META_VERSION) return null;
41
+ const tool = raw.tool;
42
+ if (typeof tool !== "string" || !Object.prototype.hasOwnProperty.call(LEASE_TOOL_STATES, tool)) return null;
43
+ const state = raw.state;
44
+ if (typeof state !== "string" || !LEASE_TOOL_STATES[tool].includes(state)) return null;
45
+ const rows = (value) => (Array.isArray(value) ? value.filter((row) => isRecord(row) && isText(row.path)) : []);
46
+ return {
47
+ tool,
48
+ state,
49
+ code: isText(raw.code) ? raw.code : null,
50
+ path: isText(raw.path) ? raw.path : null,
51
+ paths: textList(raw.paths),
52
+ released: textList(raw.released),
53
+ notOwned: textList(raw.notOwned),
54
+ held: rows(raw.held).map((row) => ({ path: row.path, holdUntil: isInstant(row.holdUntil) ? row.holdUntil : null })),
55
+ mine: rows(raw.mine).map((row) => ({ path: row.path, until: isInstant(row.until) ? row.until : null, retained: row.retained === true })),
56
+ others: rows(raw.others).map((row) => ({
57
+ path: row.path,
58
+ label: isText(row.label) ? row.label : null,
59
+ session: isText(row.session) ? row.session : null,
60
+ until: isInstant(row.until) ? row.until : null,
61
+ lapsed: row.lapsed === true
62
+ })),
63
+ requestId: isText(raw.requestId) ? raw.requestId : null,
64
+ decision: raw.decision === "release" || raw.decision === "keep" ? raw.decision : null,
65
+ holder: holderOf(raw.holder),
66
+ expiresAt: isInstant(raw.expiresAt) ? raw.expiresAt : null,
67
+ at: isInstant(raw.at) ? raw.at : null
68
+ };
69
+ }
70
+ /**
71
+ * The tone of one outcome (design D3): success, wait, danger, warn or
72
+ * neutral. The view maps a tone to a theme colour. The fallback states
73
+ * `running`, `failed`, `stopped` and `raw` share the same scale.
74
+ */
75
+ function colorOf(tool, state) {
76
+ if (state === "claimed" || state === "held" || state === "transferred") return "success";
77
+ if (state === "released") return tool === "edit_lock_release" ? "success" : "neutral";
78
+ if (state === "watching" || state === "pending" || state === "running") return "wait";
79
+ if (state === "failed") return "danger";
80
+ if (state === "degraded" || state === "unsupported" || state === "reset" || state === "cancelled") return "warn";
81
+ return "neutral";
82
+ }
83
+ /**
84
+ * The dictionary key (without namespace) of one state's badge label.
85
+ * `released` reads differently on the watch card (a release that landed
86
+ * while the subscription was set up) than on the release card.
87
+ */
88
+ function stateKey(tool, state) {
89
+ if (tool === "edit_lock_watch" && state === "released") return "leaseToolState_watch_released";
90
+ return `leaseToolState_${state}`;
91
+ }
92
+ /** The raw argument JSON of a start or result block, when present. */
93
+ function argsRawOf(block) {
94
+ if (typeof block?.argsRaw === "string") return block.argsRaw;
95
+ if (typeof block?.call?.argsRaw === "string") return block.call.argsRaw;
96
+ if (isRecord(block?.args)) return block.args;
97
+ if (isRecord(block?.call?.args)) return block.call.args;
98
+ return null;
99
+ }
100
+ /**
101
+ * The paths a call asked about, from its arguments: `paths` for the batch
102
+ * tools, `file_path` for the single-path ones. Unparseable or partial
103
+ * arguments (a call still streaming) yield an empty list, never a throw.
104
+ */
105
+ function argsPaths(tool, argsRaw) {
106
+ let args = argsRaw;
107
+ if (typeof argsRaw === "string") {
108
+ try {
109
+ args = JSON.parse(argsRaw);
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+ if (!isRecord(args)) return [];
115
+ if (Array.isArray(args.paths)) return textList(args.paths);
116
+ if (isText(args.file_path)) return [args.file_path];
117
+ return [];
118
+ }
119
+ /** The request id a reply call answered, from its arguments. */
120
+ function argsRequestId(argsRaw) {
121
+ let args = argsRaw;
122
+ if (typeof argsRaw === "string") {
123
+ try {
124
+ args = JSON.parse(argsRaw);
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ return isRecord(args) && isText(args.request_id) ? args.request_id : null;
130
+ }
131
+ /** Joined text of a result block's content parts (the model-facing text). */
132
+ function resultText(block) {
133
+ if (!Array.isArray(block?.content)) return "";
134
+ return block.content.map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : "")).filter((value) => value !== "").join("\n");
135
+ }
136
+ /** The failure text of an errored block: its content, else its error message. */
137
+ function errorText(block) {
138
+ const text = resultText(block);
139
+ if (text !== "") return text;
140
+ if (isText(block?.error?.message)) return block.error.message;
141
+ return "";
142
+ }
143
+ /**
144
+ * The card state when no structured outcome exists: a call still
145
+ * preparing or running, one that threw (a refusal or failure), one the
146
+ * user interrupted, or a completed result recorded before the cards
147
+ * existed (`raw`).
148
+ */
149
+ function fallbackState(phase, block) {
150
+ if (phase === "preparing" || phase === "start") return "running";
151
+ if (block?.isError) return block?.error?.code === "interrupted" ? "stopped" : "failed";
152
+ if (phase === "result" || Array.isArray(block?.content)) return "raw";
153
+ return "running";
154
+ }
155
+ /** A path as the session sees it: relative to the session cwd when inside it. */
156
+ function shortPath(path, cwd) {
157
+ if (typeof path !== "string") return "";
158
+ if (typeof cwd === "string" && cwd !== "") {
159
+ const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`;
160
+ if (path.startsWith(prefix)) return path.slice(prefix.length);
161
+ }
162
+ return path;
163
+ }
164
+ /** The paths one outcome is about, in display order (first = primary). */
165
+ function outcomePaths(meta) {
166
+ if (meta === null) return [];
167
+ if (meta.path !== null) return [meta.path];
168
+ if (meta.tool === "edit_lock_release" && meta.state === "released") return [...meta.released, ...meta.notOwned];
169
+ if (meta.tool === "edit_lock_hold" && meta.state === "held") return meta.held.map((row) => row.path);
170
+ if (meta.tool === "edit_lock_status" && meta.state === "status") return [...meta.mine.map((row) => row.path), ...meta.others.map((row) => row.path)];
171
+ return meta.paths;
172
+ }
173
+ exports.LEASE_META_VERSION = LEASE_META_VERSION;
174
+ exports.LEASE_TOOL_STATES = LEASE_TOOL_STATES;
175
+ exports.LEASE_TOOLS = LEASE_TOOLS;
176
+ exports.narrowLeaseMeta = narrowLeaseMeta;
177
+ exports.colorOf = colorOf;
178
+ exports.stateKey = stateKey;
179
+ exports.argsRawOf = argsRawOf;
180
+ exports.argsPaths = argsPaths;
181
+ exports.argsRequestId = argsRequestId;
182
+ exports.resultText = resultText;
183
+ exports.errorText = errorText;
184
+ exports.fallbackState = fallbackState;
185
+ exports.shortPath = shortPath;
186
+ exports.outcomePaths = outcomePaths;
187
+ return module.exports;
188
+ }
189
+ });