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.
- package/cordis.patch.yml +7 -18
- package/lib/client.capability-badge.js +2 -0
- package/lib/client.capability-manager.js +46 -179
- package/lib/client.capability-model.js +428 -3
- package/lib/client.file-lease-badge.js +307 -0
- package/lib/client.file-lease-tool-model.js +189 -0
- package/lib/client.file-lease-tool-view.js +434 -0
- package/lib/client.js +124 -36
- package/lib/client.mcp-manager.js +563 -0
- package/lib/client.settings-page.js +10 -34
- package/package.json +2 -2
- package/skills/mcp-server-setup/SKILL.md +124 -0
- package/src/capabilities/apply-engine.js +18 -4
- package/src/capabilities/capability-remote.js +296 -45
- package/src/capabilities/initial-selection.js +3 -2
- package/src/capabilities/lifecycle-preload.js +69 -11
- package/src/capabilities/lifecycle-snapshot.js +127 -39
- package/src/capabilities/mcp-admission.js +2 -2
- package/src/capabilities/mcp-config.js +344 -0
- package/src/capabilities/mcp-credentials.js +180 -0
- package/src/capabilities/mcp-diagnostics.js +169 -0
- package/src/capabilities/mcp-drain.js +18 -4
- package/src/capabilities/mcp-facade.js +3 -3
- package/src/capabilities/mcp-gate.js +51 -52
- package/src/capabilities/mcp-install-tool.js +276 -0
- package/src/capabilities/mcp-manager.js +368 -142
- package/src/capabilities/mcp-mount.js +219 -79
- package/src/capabilities/mcp-operations.js +359 -0
- package/src/capabilities/mcp-process.js +43 -0
- package/src/capabilities/mcp-registry.js +74 -60
- package/src/capabilities/mcp-runtime.js +103 -0
- package/src/capabilities/read-payloads.js +8 -3
- package/src/capabilities/skill-applicability.js +185 -0
- package/src/capabilities/skill-builtin-migration.js +2 -1
- package/src/capabilities/skill-selection-plugin.js +161 -56
- package/src/capabilities/store/checked-contract.d.ts +2 -2
- package/src/capabilities/store/fs-adapter.js +2 -3
- package/src/context-guard/index.js +2 -2
- package/src/delegate/supervision-mount.js +5 -5
- package/src/delegate/tool.js +2 -10
- package/src/file-lease/client.js +1299 -0
- package/src/file-lease/index.js +34 -0
- package/src/file-lease/lineage.js +207 -0
- package/src/file-lease/node-contracts.d.ts +42 -0
- package/src/file-lease/plugin.js +1013 -0
- package/src/file-lease/singleton.js +155 -0
- package/src/file-lease/spawn.js +62 -0
- package/src/file-lease/tools.js +847 -0
- package/src/file-lease/view.js +144 -0
- package/src/file-lease/watch.js +473 -0
- package/src/hashline-edit/index.js +76 -42
- package/src/hashline-edit/write-tool.js +90 -0
- package/src/intent-gate/index.js +29 -12
- package/src/intent-gate/matcher.js +36 -2
- package/src/lease-broker/entry.js +98 -0
- package/src/lease-broker/node-contracts.d.ts +63 -0
- package/src/lease-broker/protocol.js +237 -0
- package/src/lease-broker/server.js +402 -0
- package/src/lease-broker/state.js +482 -0
- package/src/lsp/tools.js +137 -27
- package/src/process/index.js +4 -2
- package/src/settings/index.js +2 -44
- package/src/settings/sections.js +2 -24
- package/src/shared/audit.js +9 -1
- package/src/shared/child-scope.js +2 -0
- package/src/todo-driver/index.js +1 -6
- package/src/worktree/cards.js +6 -21
- package/src/worktree/lane-core.js +1 -19
- package/src/worktree/lane-workflows.js +11 -32
- package/src/worktree/rules.js +1 -1
- package/lib/client.edit-lock-maintenance.js +0 -592
- package/lib/client.edit-lock-panel.js +0 -405
- package/src/edit-lock/adapter.js +0 -57
- package/src/edit-lock/admin-ledger.js +0 -149
- package/src/edit-lock/admin-recovery.js +0 -151
- package/src/edit-lock/admission.js +0 -82
- package/src/edit-lock/binding-attestation.js +0 -29
- package/src/edit-lock/binding-guidance.js +0 -72
- package/src/edit-lock/binding-machine.js +0 -312
- package/src/edit-lock/call-context.js +0 -65
- package/src/edit-lock/derived-facts.js +0 -67
- package/src/edit-lock/domain.js +0 -165
- package/src/edit-lock/domains.js +0 -170
- package/src/edit-lock/host.js +0 -149
- package/src/edit-lock/incarnations.js +0 -150
- package/src/edit-lock/index.js +0 -1303
- package/src/edit-lock/inspect.js +0 -219
- package/src/edit-lock/lifecycle.js +0 -579
- package/src/edit-lock/limits.js +0 -61
- package/src/edit-lock/maintenance.js +0 -310
- package/src/edit-lock/manager.js +0 -875
- package/src/edit-lock/negotiation.js +0 -132
- package/src/edit-lock/operation-history.js +0 -214
- package/src/edit-lock/peer-client.js +0 -86
- package/src/edit-lock/peer-transport.js +0 -76
- package/src/edit-lock/peer.js +0 -87
- package/src/edit-lock/publisher.js +0 -191
- package/src/edit-lock/read-authority.js +0 -101
- package/src/edit-lock/recovery.js +0 -139
- package/src/edit-lock/remote.js +0 -362
- package/src/edit-lock/request-binding.js +0 -42
- package/src/edit-lock/request-data.js +0 -40
- package/src/edit-lock/reservation.js +0 -301
- package/src/edit-lock/reserved-runtime.js +0 -45
- package/src/edit-lock/resource-identity.js +0 -130
- package/src/edit-lock/runtime.js +0 -145
- package/src/edit-lock/settle.js +0 -144
- package/src/edit-lock/snapshot.js +0 -175
- package/src/edit-lock/stale-sweep.js +0 -120
- package/src/edit-lock/state.js +0 -462
- package/src/edit-lock/store.js +0 -327
- package/src/edit-lock/tool-scope.js +0 -29
- package/src/edit-lock/view.js +0 -170
- package/src/edit-lock/write-scopes.js +0 -48
- package/src/edit-lock/write-tool.js +0 -45
- package/src/shared/delegate-edit-lifecycle.js +0 -36
- 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
|
+
});
|