weir-harness 1.1.1 → 1.1.3
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 +0 -5
- package/lib/client.chain-editor.js +7 -2
- package/lib/client.edit-lock-maintenance.js +190 -3
- package/lib/client.js +10 -2
- package/lib/client.model-picker.js +232 -0
- package/lib/client.settings-page.js +57 -50
- package/package.json +1 -1
- package/src/capabilities/lifecycle-snapshot.js +2 -0
- package/src/capabilities/store/checked-contract.d.ts +1 -0
- package/src/capabilities/store/fs-adapter.js +33 -3
- package/src/capabilities/store/liveness.js +37 -3
- package/src/capabilities/store/store.js +7 -2
- package/src/edit-lock/admin-recovery.js +8 -3
- package/src/edit-lock/admission.js +13 -3
- package/src/edit-lock/index.js +12 -2
- package/src/edit-lock/negotiation.js +2 -1
- package/src/edit-lock/operation-history.js +2 -2
- package/src/edit-lock/read-authority.js +5 -2
- package/src/edit-lock/remote.js +27 -11
- package/src/edit-lock/reservation.js +4 -0
- package/src/edit-lock/resource-identity.js +38 -15
- package/src/edit-lock/runtime.js +3 -0
- package/src/edit-lock/stale-sweep.js +18 -1
- package/src/edit-lock/store.js +15 -5
package/cordis.patch.yml
CHANGED
|
@@ -90,11 +90,6 @@
|
|
|
90
90
|
# dsh.client manifest).
|
|
91
91
|
- id: ui-weir-settings
|
|
92
92
|
name: 'weir-harness'
|
|
93
|
-
# Isolated reusable model picker client module (own package + own client
|
|
94
|
-
# export): the settings page requires it and renders it inside an error
|
|
95
|
-
# boundary, so a picker failure can never blank the page.
|
|
96
|
-
- id: ui-weir-model-picker
|
|
97
|
-
name: 'weir-model-picker'
|
|
98
93
|
- id: preset-weir
|
|
99
94
|
name: '@deepseek-ai/dsh-agent-preset'
|
|
100
95
|
config:
|
|
@@ -8,7 +8,11 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
10
|
let primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
11
|
-
|
|
11
|
+
// The model picker is a sibling package-local chunk supplied by the
|
|
12
|
+
// composition root as the `modelPicker` prop (same-package sync require
|
|
13
|
+
// is impossible here); absent means its chunk is still in flight or its
|
|
14
|
+
// load failed, which degrades each rung to a hint instead of breaking the
|
|
15
|
+
// editor's other work.
|
|
12
16
|
// Model-chains editor with the lane list as a PROP: the category lanes
|
|
13
17
|
// are the default (no row props → the pre-generalization category
|
|
14
18
|
// behaviour, byte-identical); the curated-agent lanes pass their own
|
|
@@ -49,6 +53,7 @@ window.__ModuleLoader__.load({
|
|
|
49
53
|
/** Visual editor for one model-chains map: pick models per lane, JSON synthesized on save. */
|
|
50
54
|
function ChainEditorField(props) {
|
|
51
55
|
const model = props.model;
|
|
56
|
+
const picker = props.modelPicker;
|
|
52
57
|
// Lane parameterization: which lanes render, which settings field
|
|
53
58
|
// the synthesized JSON is written to, and which dictionary stems
|
|
54
59
|
// label the rows — the defaults are exactly the category treatment.
|
|
@@ -113,7 +118,7 @@ window.__ModuleLoader__.load({
|
|
|
113
118
|
react_jsx_runtime.jsx("span", { style: chainDescStyle, children: props.t(`${rowLabelPrefix}${row}_desc`) })
|
|
114
119
|
] }),
|
|
115
120
|
...rungs.map((rung, index) => react_jsx_runtime.jsxs("div", { style: chainRungStyle, key: index, children: [
|
|
116
|
-
react_jsx_runtime.jsx(
|
|
121
|
+
picker === void 0 || picker === null ? react_jsx_runtime.jsx("span", { style: hintStyle, children: props.t("pickerUnavailable") }) : react_jsx_runtime.jsx(picker.ModelPickerField, {
|
|
117
122
|
value: rung,
|
|
118
123
|
onChange: (selection) => updateRung(row, index, selection),
|
|
119
124
|
getSession: () => props.getSession(),
|
|
@@ -154,6 +154,101 @@ window.__ModuleLoader__.load({
|
|
|
154
154
|
}
|
|
155
155
|
return candidates;
|
|
156
156
|
}
|
|
157
|
+
/** Recovery-UX zone copy (D1/D2). The entry dictionaries (client.js) are
|
|
158
|
+
* owned by the entry chunk, so the new zone copy lives HERE as
|
|
159
|
+
* template-layer English per the text discipline; file paths, session ids
|
|
160
|
+
* and statuses stay dynamic instance content. */
|
|
161
|
+
const RECOVERY_UX = Object.freeze({
|
|
162
|
+
blockedHeader: "Blocked files — one-click force release",
|
|
163
|
+
blockedHint: "Opening the panel ran the read-only inspection automatically. Each row settles the holder's unresolved publication fence (when present) and releases the lock in one operator-confirmed step.",
|
|
164
|
+
unmetHeader: "Blocked files — recovery preconditions not met",
|
|
165
|
+
forceRelease: "Force release",
|
|
166
|
+
holder: "holder",
|
|
167
|
+
stateFence: "publication fence",
|
|
168
|
+
stateInterrupted: "interrupted",
|
|
169
|
+
stateActive: "active",
|
|
170
|
+
reasonNotInterrupted: "The holding session is not durably interrupted; the online administrative recovery can only override an interrupted historical owner. Stop the session first, or ask it to release the file.",
|
|
171
|
+
reasonPrepared: "The owner still has a prepared operation; recover to stable unknown history before an administrative override.",
|
|
172
|
+
reasonPublishing: "The owner still has an operation in the publishing phase; only unknown outcomes can be overridden online.",
|
|
173
|
+
reasonDisposition: "An administrative recovery is already recorded for this owner; admission for its operations is no longer blocked.",
|
|
174
|
+
reasonNoUnresolved: "The owner has no unresolved operations; an ordinary unlock may be enough.",
|
|
175
|
+
reasonUnknownSession: "The owner session is not recorded in this authority image.",
|
|
176
|
+
});
|
|
177
|
+
/** The first failing precondition of an ineligible blocking owner, in the
|
|
178
|
+
* manager's refusal order (revoked/disposition → owner unknown → not
|
|
179
|
+
* interrupted → prepared → publishing → no unresolved operations).
|
|
180
|
+
* @param {string} owner @param {any} snapshot @returns {string} */
|
|
181
|
+
function unmetReason(owner, snapshot) {
|
|
182
|
+
const unresolved = Array.isArray(snapshot?.unresolved) ? snapshot.unresolved : [];
|
|
183
|
+
const ownerOps = unresolved.filter((op) => op?.key?.sessionId === owner);
|
|
184
|
+
const prepared = (Array.isArray(snapshot?.prepared) ? snapshot.prepared : []).filter((op) => op?.key?.sessionId === owner);
|
|
185
|
+
const session = (Array.isArray(snapshot?.sessions) ? snapshot.sessions : []).find((row) => row?.sessionId === owner);
|
|
186
|
+
if (ownerOps.some((op) => op?.admissionBlocked === false)) return RECOVERY_UX.reasonDisposition;
|
|
187
|
+
if (!session) return RECOVERY_UX.reasonUnknownSession;
|
|
188
|
+
if (session.interrupted !== true) return RECOVERY_UX.reasonNotInterrupted;
|
|
189
|
+
if (prepared.length > 0) return RECOVERY_UX.reasonPrepared;
|
|
190
|
+
if (ownerOps.some((op) => op?.phase === "publishing")) return RECOVERY_UX.reasonPublishing;
|
|
191
|
+
return RECOVERY_UX.reasonNoUnresolved;
|
|
192
|
+
}
|
|
193
|
+
/** Recovery-UX diagnosis (D1/D2): the three-zone classification the panel
|
|
194
|
+
* renders from one inspection. Eligibility is recoveryCandidates — the
|
|
195
|
+
* SAME scope and confirmation algorithm — and every ineligible blocking
|
|
196
|
+
* owner carries the named failing precondition. Blocking artifacts are
|
|
197
|
+
* retained locks plus still-blocking (admissionBlocked) unresolved
|
|
198
|
+
* publications; file-scoped fences become rows, wider fences name their
|
|
199
|
+
* scope path in the unmet zone. Pure: no fetch, no state, no clock.
|
|
200
|
+
* @param {string} root @param {any} snapshot */
|
|
201
|
+
function recoveryDiagnosis(root, snapshot) {
|
|
202
|
+
const candidates = recoveryCandidates(root, snapshot);
|
|
203
|
+
const candidateByOwner = new Map(candidates.map((candidate) => [candidate.owner, candidate]));
|
|
204
|
+
const sessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : [];
|
|
205
|
+
const interruptedByOwner = new Map(sessions.map((session) => [session.sessionId, session?.interrupted === true]));
|
|
206
|
+
const retained = Array.isArray(snapshot?.retainedLocks) ? snapshot.retainedLocks : [];
|
|
207
|
+
/** @type {Map<string, {resourceId: string, owner: string, lockStatus: string|null, fence: boolean, interrupted: boolean}>} */
|
|
208
|
+
const rowsByResource = new Map();
|
|
209
|
+
const blockedOwners = new Set();
|
|
210
|
+
for (const lock of retained) {
|
|
211
|
+
if (typeof lock?.resourceId !== "string" || typeof lock?.owner !== "string") continue;
|
|
212
|
+
blockedOwners.add(lock.owner);
|
|
213
|
+
const row = rowsByResource.get(lock.resourceId) ?? { resourceId: lock.resourceId, owner: lock.owner, lockStatus: null, fence: false, interrupted: interruptedByOwner.get(lock.owner) ?? false };
|
|
214
|
+
row.lockStatus = row.lockStatus ?? (typeof lock.status === "string" ? lock.status : null);
|
|
215
|
+
rowsByResource.set(lock.resourceId, row);
|
|
216
|
+
}
|
|
217
|
+
/** @type {Map<string, {owner: string, scopePaths: string[]}>} */
|
|
218
|
+
const fencedOwners = new Map();
|
|
219
|
+
for (const op of Array.isArray(snapshot?.unresolved) ? snapshot.unresolved : []) {
|
|
220
|
+
if (op?.admissionBlocked !== true || typeof op?.key?.sessionId !== "string") continue;
|
|
221
|
+
blockedOwners.add(op.key.sessionId);
|
|
222
|
+
const scope = op?.scope ?? null;
|
|
223
|
+
if (scope?.kind === "file" && typeof scope.path === "string") {
|
|
224
|
+
const existing = rowsByResource.get(scope.path);
|
|
225
|
+
if (existing) {
|
|
226
|
+
// Same owner locks AND fences the file (the incident shape): one row.
|
|
227
|
+
if (existing.owner === op.key.sessionId) existing.fence = true;
|
|
228
|
+
else {
|
|
229
|
+
const entry = fencedOwners.get(op.key.sessionId) ?? { owner: op.key.sessionId, scopePaths: /** @type {string[]} */ ([]) };
|
|
230
|
+
if (!entry.scopePaths.includes(scope.path)) entry.scopePaths.push(scope.path);
|
|
231
|
+
fencedOwners.set(op.key.sessionId, entry);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
rowsByResource.set(scope.path, { resourceId: scope.path, owner: op.key.sessionId, lockStatus: null, fence: true, interrupted: interruptedByOwner.get(op.key.sessionId) ?? false });
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
const entry = fencedOwners.get(op.key.sessionId) ?? { owner: op.key.sessionId, scopePaths: /** @type {string[]} */ ([]) };
|
|
238
|
+
if (typeof scope?.path === "string" && !entry.scopePaths.includes(scope.path)) entry.scopePaths.push(scope.path);
|
|
239
|
+
fencedOwners.set(op.key.sessionId, entry);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const rows = [...rowsByResource.values()];
|
|
243
|
+
const blocked = rows.filter((row) => candidateByOwner.has(row.owner));
|
|
244
|
+
blocked.sort((a, b) => a.resourceId.localeCompare(b.resourceId));
|
|
245
|
+
const unmet = [];
|
|
246
|
+
for (const owner of [...blockedOwners].sort()) {
|
|
247
|
+
if (candidateByOwner.has(owner)) continue;
|
|
248
|
+
unmet.push({ owner, rows: rows.filter((row) => row.owner === owner), scopePaths: fencedOwners.get(owner)?.scopePaths ?? [], reason: unmetReason(owner, snapshot) });
|
|
249
|
+
}
|
|
250
|
+
return { candidates, blocked, unmet };
|
|
251
|
+
}
|
|
157
252
|
/** Error boundary isolating the maintenance panel from the settings page. */
|
|
158
253
|
class EditLockMaintenanceBoundary extends react.Component {
|
|
159
254
|
constructor(props) {
|
|
@@ -220,6 +315,11 @@ window.__ModuleLoader__.load({
|
|
|
220
315
|
post("api/weir-edit-lock/maintenance/status")
|
|
221
316
|
.then((payload) => {
|
|
222
317
|
setView(payload?.ok ? { status: "ready", value: payload.value } : { status: "error", message: payload?.error?.message ?? "unknown" });
|
|
318
|
+
// Recovery UX D1: opening the panel auto-runs the read-only
|
|
319
|
+
// inspection for every server-derived domain root, so the
|
|
320
|
+
// blocked/unmet zones render without any manual step. The
|
|
321
|
+
// inspection itself stays read-only (the same endpoint).
|
|
322
|
+
if (payload?.ok && Array.isArray(payload.value?.domains)) payload.value.domains.forEach((domain) => inspect(domain.root, false));
|
|
223
323
|
})
|
|
224
324
|
.catch((error) => setView({ status: "error", message: String(error?.message ?? error) }));
|
|
225
325
|
};
|
|
@@ -229,8 +329,9 @@ window.__ModuleLoader__.load({
|
|
|
229
329
|
setExpanded(null);
|
|
230
330
|
if (next) load();
|
|
231
331
|
};
|
|
232
|
-
|
|
233
|
-
|
|
332
|
+
/** @param {string} root @param {boolean} [expand] manual clicks expand the per-domain detail; the automatic pass does not */
|
|
333
|
+
const inspect = (root, expand = true) => {
|
|
334
|
+
if (expand) setExpanded(root);
|
|
234
335
|
setInspections((previous) => ({ ...previous, [root]: { status: "loading" } }));
|
|
235
336
|
post("api/weir-edit-lock/maintenance/inspect", { root })
|
|
236
337
|
.then((payload) => {
|
|
@@ -279,6 +380,58 @@ window.__ModuleLoader__.load({
|
|
|
279
380
|
}) })
|
|
280
381
|
], key: recoveryKey(root, candidate.owner) });
|
|
281
382
|
};
|
|
383
|
+
/** Row state fragment: lock status / publication fence + session liveness. */
|
|
384
|
+
const holderStateText = (row) => {
|
|
385
|
+
const parts = [];
|
|
386
|
+
if (row.lockStatus) parts.push(row.lockStatus);
|
|
387
|
+
if (row.fence) parts.push(RECOVERY_UX.stateFence);
|
|
388
|
+
parts.push(row.interrupted ? RECOVERY_UX.stateInterrupted : RECOVERY_UX.stateActive);
|
|
389
|
+
return parts.join(" · ");
|
|
390
|
+
};
|
|
391
|
+
/** Blocked-zone group for one eligible owner: one row per blocked file
|
|
392
|
+
* (file, holder, holder state, one primary force-release action per
|
|
393
|
+
* row) plus the FULL scope, risk and computed digest the operator
|
|
394
|
+
* accepts with the click — the same confirmation algorithm as the
|
|
395
|
+
* offline path, unchanged. */
|
|
396
|
+
const blockedZoneCard = (root, candidate, rows) => {
|
|
397
|
+
const state = recoveries[recoveryKey(root, candidate.owner)];
|
|
398
|
+
return react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px", padding: "8px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "var(--dsw-radius-md)" }, children: [
|
|
399
|
+
...rows.map((row) => react_jsx_runtime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
|
|
400
|
+
react_jsx_runtime.jsxs("span", { style: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 }, children: [
|
|
401
|
+
react_jsx_runtime.jsx("span", { style: monoStyle, children: row.resourceId }),
|
|
402
|
+
react_jsx_runtime.jsx("span", { style: chainDescStyle, children: `${RECOVERY_UX.holder} ${row.owner} · ${holderStateText(row)}` })
|
|
403
|
+
] }),
|
|
404
|
+
react_jsx_runtime.jsx("span", { style: { flex: 1 } }),
|
|
405
|
+
react_jsx_runtime.jsx("button", {
|
|
406
|
+
type: "button",
|
|
407
|
+
style: chainButtonStyle,
|
|
408
|
+
disabled: state?.status === "busy",
|
|
409
|
+
onClick: () => recover(root, candidate),
|
|
410
|
+
children: state?.status === "busy" ? t("editLockMaintRecoverBusy") : RECOVERY_UX.forceRelease
|
|
411
|
+
})
|
|
412
|
+
], key: row.resourceId })),
|
|
413
|
+
react_jsx_runtime.jsx("div", { style: chainDescStyle, children: `${t("editLockMaintRecoverRoot")}: ${candidate.scope.root}` }),
|
|
414
|
+
react_jsx_runtime.jsx("div", { style: chainDescStyle, children: `${t("editLockMaintRecoverOwner")}: ${candidate.owner}` }),
|
|
415
|
+
react_jsx_runtime.jsx("div", { style: chainDescStyle, children: `${t("editLockMaintRecoverRevision")}: ${candidate.scope.expectedRevision}` }),
|
|
416
|
+
react_jsx_runtime.jsxs("div", { style: chainDescStyle, children: [
|
|
417
|
+
react_jsx_runtime.jsx("div", { children: `${t("editLockMaintRecoverOperations")}:` }),
|
|
418
|
+
...candidate.scope.operationIds.map((id, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: id }, index))
|
|
419
|
+
] }),
|
|
420
|
+
react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverRisk")}: ${LATE_WRITER_RISK}` }),
|
|
421
|
+
react_jsx_runtime.jsx("div", { style: monoStyle, children: `${t("editLockMaintRecoverDigest")}: ${candidate.confirmation}` }),
|
|
422
|
+
state?.status === "error"
|
|
423
|
+
? react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverFailed")} ${state.message}` })
|
|
424
|
+
: null,
|
|
425
|
+
], key: recoveryKey(root, candidate.owner) });
|
|
426
|
+
};
|
|
427
|
+
/** Unmet-zone row for one ineligible blocking owner: the blocked files
|
|
428
|
+
* and the NAMED failing precondition, never a silent missing action. */
|
|
429
|
+
const unmetZoneRow = (root, entry) => react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px", padding: "8px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "var(--dsw-radius-md)" }, children: [
|
|
430
|
+
react_jsx_runtime.jsx("div", { style: chainDescStyle, children: `${t("editLockMaintRecoverOwner")}: ${entry.owner}` }),
|
|
431
|
+
...entry.rows.map((row, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: `${row.resourceId} · ${RECOVERY_UX.holder} ${row.owner} · ${holderStateText(row)}` }, index)),
|
|
432
|
+
...entry.scopePaths.map((path, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: path }, index)),
|
|
433
|
+
react_jsx_runtime.jsx("div", { style: dangerStyle, children: entry.reason })
|
|
434
|
+
], key: `${root}\n${entry.owner}\nunmet` });
|
|
282
435
|
const snapshotSection = (/** @type {string} */ root, /** @type {any} */ snapshot) => react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [
|
|
283
436
|
react_jsx_runtime.jsx("div", { style: chainDescStyle, children: t("editLockMaintCounts")
|
|
284
437
|
.replace("{sessions}", String(snapshot.counts.sessions))
|
|
@@ -348,6 +501,27 @@ window.__ModuleLoader__.load({
|
|
|
348
501
|
}
|
|
349
502
|
const value = view.value;
|
|
350
503
|
const stateKey = STATE_KEYS[value.state] ?? "Unknown";
|
|
504
|
+
// Recovery UX D1: aggregate the zones from every auto-inspected root.
|
|
505
|
+
// Blocked rows (with a force-release action) render first, then the
|
|
506
|
+
// owners whose preconditions fail with the named reason; everything
|
|
507
|
+
// else is the normal zone (banner, warnings, per-domain detail).
|
|
508
|
+
const zoneGroups = new Map();
|
|
509
|
+
const zoneUnmet = [];
|
|
510
|
+
for (const root of Object.keys(inspections)) {
|
|
511
|
+
const inspection = inspections[root];
|
|
512
|
+
if (inspection?.status !== "ready" || inspection.value?.presence !== "valid") continue;
|
|
513
|
+
const diagnosis = recoveryDiagnosis(root, inspection.value.snapshot);
|
|
514
|
+
for (const row of diagnosis.blocked) {
|
|
515
|
+
const candidate = diagnosis.candidates.find((entry) => entry.owner === row.owner);
|
|
516
|
+
const key = recoveryKey(root, row.owner);
|
|
517
|
+
const group = zoneGroups.get(key) ?? { root, candidate, rows: [] };
|
|
518
|
+
group.rows.push(row);
|
|
519
|
+
zoneGroups.set(key, group);
|
|
520
|
+
}
|
|
521
|
+
for (const entry of diagnosis.unmet) zoneUnmet.push({ root, entry });
|
|
522
|
+
}
|
|
523
|
+
const blockedGroups = [...zoneGroups.values()];
|
|
524
|
+
zoneUnmet.sort((a, b) => a.entry.owner.localeCompare(b.entry.owner));
|
|
351
525
|
return react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "10px" }, children: [
|
|
352
526
|
react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [
|
|
353
527
|
react_jsx_runtime.jsx("span", { style: chainCategoryStyle, children: t(`editLockMaintState${stateKey}`) }),
|
|
@@ -360,6 +534,19 @@ window.__ModuleLoader__.load({
|
|
|
360
534
|
react_jsx_runtime.jsx("span", { style: warnStyle, children: t("editLockMaintWarnKeepHistory") }),
|
|
361
535
|
react_jsx_runtime.jsx("span", { style: warnStyle, children: t("editLockMaintWarnUnlock") })
|
|
362
536
|
] }),
|
|
537
|
+
blockedGroups.length > 0
|
|
538
|
+
? react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px" }, children: [
|
|
539
|
+
react_jsx_runtime.jsx("span", { style: chainCategoryStyle, children: RECOVERY_UX.blockedHeader }),
|
|
540
|
+
react_jsx_runtime.jsx("span", { style: chainDescStyle, children: RECOVERY_UX.blockedHint }),
|
|
541
|
+
...blockedGroups.map((group) => blockedZoneCard(group.root, group.candidate, group.rows))
|
|
542
|
+
] })
|
|
543
|
+
: null,
|
|
544
|
+
zoneUnmet.length > 0
|
|
545
|
+
? react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px" }, children: [
|
|
546
|
+
react_jsx_runtime.jsx("span", { style: chainCategoryStyle, children: RECOVERY_UX.unmetHeader }),
|
|
547
|
+
...zoneUnmet.map(({ root, entry }) => unmetZoneRow(root, entry))
|
|
548
|
+
] })
|
|
549
|
+
: null,
|
|
363
550
|
value.blocked.length > 0
|
|
364
551
|
? react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px" }, children: [
|
|
365
552
|
react_jsx_runtime.jsx("span", { style: chainCategoryStyle, children: t("editLockMaintBlocked") }),
|
|
@@ -391,7 +578,7 @@ window.__ModuleLoader__.load({
|
|
|
391
578
|
exports.EditLockMaintenanceBoundary = EditLockMaintenanceBoundary;
|
|
392
579
|
exports.EditLockMaintenanceField = EditLockMaintenanceField;
|
|
393
580
|
// Exported for the chunk test's 对拍 against the server-side digest.
|
|
394
|
-
exports.editLockRecovery = Object.freeze({ canonicalJson, sha256Hex, recoveryConfirmation, recoveryCandidates, recoveryKey, LATE_WRITER_RISK });
|
|
581
|
+
exports.editLockRecovery = Object.freeze({ canonicalJson, sha256Hex, recoveryConfirmation, recoveryCandidates, recoveryDiagnosis, unmetReason, recoveryKey, LATE_WRITER_RISK, RECOVERY_UX });
|
|
395
582
|
return module.exports;
|
|
396
583
|
}
|
|
397
584
|
});
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Weir client chunks: {"client.blackboard-model.js":"66fd9fc5438477d6d7ff156631f810a647ce352612a16051e987e8cd449bc4c9","client.blackboard-panel.js":"f961e0242dad7bf311829a25e265bf091925711b2fb9c19d2141997fbdf792b5","client.capability-badge.js":"4e5805596a9ad34eac03cbd62c956f8b79789527c72279d6a4078f5c9d0eabab","client.capability-manager.js":"c36efbad8bcdb1e6dfee52368b89485127f953e9d04ae7921032740439fffcdd","client.capability-model.js":"34af92420fe4f182ba9bf962dfe6986e8f54c79b147e5981eec3bbfc06cdbc21","client.capability-presets.js":"66aeb7ecde8a73165a3f010bed0dc56b8a8be70812c1e3b56c324f7be095adab","client.chain-editor.js":"
|
|
1
|
+
// Weir client chunks: {"client.blackboard-model.js":"66fd9fc5438477d6d7ff156631f810a647ce352612a16051e987e8cd449bc4c9","client.blackboard-panel.js":"f961e0242dad7bf311829a25e265bf091925711b2fb9c19d2141997fbdf792b5","client.capability-badge.js":"4e5805596a9ad34eac03cbd62c956f8b79789527c72279d6a4078f5c9d0eabab","client.capability-manager.js":"c36efbad8bcdb1e6dfee52368b89485127f953e9d04ae7921032740439fffcdd","client.capability-model.js":"34af92420fe4f182ba9bf962dfe6986e8f54c79b147e5981eec3bbfc06cdbc21","client.capability-presets.js":"66aeb7ecde8a73165a3f010bed0dc56b8a8be70812c1e3b56c324f7be095adab","client.chain-editor.js":"1e9a33bc68cabc43bc27b91ef83eab5028a33256596c147e15722d684687765b","client.chain-model.js":"81cdd04626900a63c38ccb3d9c4d117686c6f8a5da2ce9131b7aaf2f089dea77","client.disabled-categories-editor.js":"d4606a3172b47b25caf76fd64c8420a0079cd5f008bc67a9c71b3f146724e393","client.edit-lock-maintenance.js":"0559d47df0e8e180eaadfd40b15bcfbf6409ec265902514db37366eb270f8e12","client.edit-lock-panel.js":"deb1d536353c51405b3d1b7b49e4847cb593547a0dfa468a5f39346ad25f745d","client.hash-edit-model.js":"99df87cc3cc261abd160b83124b5ce726d0708f3c87eba9d58ad9e5a1f41bae9","client.hash-edit-view.js":"fc0f4dabddab9a48527553ad4232b3715e46e6863aab75e02ae2dcd6687265c2","client.lsp-model.js":"6b5bead2900289427a1ad723b9c7bea85af4c5547029e486c620ff94ece24775","client.lsp-panel.js":"39211da7658e0810abc0984b8bcbbfb4ee6a6299fedfd21649ded1ab39d1f291","client.lsp-toggle.js":"4d960984ccf1045c617be6d282665513a9cf636779563a276973b55036a59fb6","client.model-picker.js":"57f96826e00b37ae6d6474a6750f31b4e128e5fdb37e302875a41cdb2e18f14a","client.notify-permissions.js":"c1c103ce5976a4032b62e12b3b3ecb907a574159bf89512539c68c4886ea37a7","client.notify-web.js":"b2ccf831524be4e94c532324cb5495793d4424e3b78d6014d2a3f3d20e9c5871","client.robash-editor.js":"cd6a283b72cb02598e32ca4ce03a1b8de5f328ecdd1f5f920377524f3bdcbe1c","client.robash-model.js":"356ee7a679223bcd351720b2bc9e7300e390d97a60d1c8d75998706b4644923b","client.settings-page.js":"7520be405d2e67904ed5344aeb09f18ddfc14f209243a86249179cc5930ba46c","client.worktree-model.js":"761a0f6316a3f3af3f924a7784d61ca4bdbfd3c6b63c81c5681bf3a801280c26","client.worktree-view.js":"c8399f67041061a357e6f11bfed1e40340494d75216b4c10ce4b940a763f0824"}
|
|
2
2
|
window.__ModuleLoader__.load({
|
|
3
3
|
id: "weir-harness",
|
|
4
4
|
factory: (require) => {
|
|
@@ -54,6 +54,7 @@ window.__ModuleLoader__.load({
|
|
|
54
54
|
catalogFailed: "Provider catalog unavailable; enter provider and model manually.",
|
|
55
55
|
providerEmpty: "Select a provider",
|
|
56
56
|
modelEmpty: "Select a model",
|
|
57
|
+
pickerUnavailable: "Model picker unavailable — reload the settings page to retry.",
|
|
57
58
|
groupIntent: "Intent",
|
|
58
59
|
groupDelegate: "Delegation",
|
|
59
60
|
groupTodo: "Continuation",
|
|
@@ -748,6 +749,7 @@ window.__ModuleLoader__.load({
|
|
|
748
749
|
catalogFailed: "Provider 目录不可用;可手动填写 provider 与模型。",
|
|
749
750
|
providerEmpty: "选择 provider",
|
|
750
751
|
modelEmpty: "选择模型",
|
|
752
|
+
pickerUnavailable: "模型选择器不可用——请重新加载设置页后重试。",
|
|
751
753
|
groupIntent: "意图分类",
|
|
752
754
|
groupDelegate: "委派与模型链",
|
|
753
755
|
groupTodo: "续推",
|
|
@@ -2096,6 +2098,11 @@ window.__ModuleLoader__.load({
|
|
|
2096
2098
|
getSession: () => ctx.remote.session
|
|
2097
2099
|
};
|
|
2098
2100
|
let arrival = null;
|
|
2101
|
+
// The picker chunk has its own arrival state on purpose: a picker load
|
|
2102
|
+
// failure must degrade only the rows that show a picker, never the card
|
|
2103
|
+
// around them, so it is deliberately NOT a member of the settings-chunk
|
|
2104
|
+
// Promise.all below.
|
|
2105
|
+
const loadModelPickerChunk = lazyChunks(() => require.async("./client.model-picker.js"));
|
|
2099
2106
|
const ensureSettingsChunks = () => {
|
|
2100
2107
|
if (arrival === null) {
|
|
2101
2108
|
arrival = Promise.all([
|
|
@@ -2146,8 +2153,9 @@ window.__ModuleLoader__.load({
|
|
|
2146
2153
|
}
|
|
2147
2154
|
function SettingsCardWrapper(props) {
|
|
2148
2155
|
const cardArrival = useChunkArrival(ensureSettingsChunks);
|
|
2156
|
+
const pickerArrival = useChunkArrival(loadModelPickerChunk);
|
|
2149
2157
|
if (cardArrival?.chunks) {
|
|
2150
|
-
return react_jsx_runtime.jsx(cardArrival.chunks.settingsPage.WeirCard, { ...props, editors: cardArrival.chunks.editors });
|
|
2158
|
+
return react_jsx_runtime.jsx(cardArrival.chunks.settingsPage.WeirCard, { ...props, editors: cardArrival.chunks.editors, modelPicker: pickerArrival?.chunks });
|
|
2151
2159
|
}
|
|
2152
2160
|
if (cardArrival?.error) {
|
|
2153
2161
|
return react_jsx_runtime.jsx("div", { style: settingsLoadFailedStyle, children: `${props.t("loadFailed")} ${describeChunkError(cardArrival.error)}` });
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "weir-harness",
|
|
3
|
+
chunk: "client.model-picker.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
|
+
// Reusable composer-style model picker: one trigger showing
|
|
11
|
+
// "model · effort", expanding into a two-level menu (provider-grouped
|
|
12
|
+
// model list / reasoning-effort list). Self-contained catalog loading
|
|
13
|
+
// (lazy + backoff retries + manual retry) over the caller-provided
|
|
14
|
+
// session RPC accessor, and a React error boundary so a picker failure
|
|
15
|
+
// renders the caller's fallback instead of blanking the page.
|
|
16
|
+
//
|
|
17
|
+
// Package-local chunk (client-module-chunking): the picker was a
|
|
18
|
+
// separate `weir-model-picker` package until it turned out to be
|
|
19
|
+
// undeliverable — a bundle patch can only mount packages the profile can
|
|
20
|
+
// resolve, that package was private and never published, so every
|
|
21
|
+
// registry install lost it and the row failed to import. It is now one
|
|
22
|
+
// more chunk of this package, pulled by the composition root through
|
|
23
|
+
// `require.async` with its own arrival state, so a picker failure still
|
|
24
|
+
// degrades only the surfaces that show a picker.
|
|
25
|
+
const chevron = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")";
|
|
26
|
+
const triggerStyle = {
|
|
27
|
+
borderRadius: "var(--dsw-radius-sm)",
|
|
28
|
+
maxWidth: "280px",
|
|
29
|
+
height: "28px",
|
|
30
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
31
|
+
whiteSpace: "nowrap",
|
|
32
|
+
cursor: "pointer",
|
|
33
|
+
appearance: "none",
|
|
34
|
+
backgroundColor: "transparent",
|
|
35
|
+
backgroundImage: chevron,
|
|
36
|
+
backgroundPosition: "right 4px center",
|
|
37
|
+
backgroundRepeat: "no-repeat",
|
|
38
|
+
backgroundSize: "12px 12px",
|
|
39
|
+
border: "none",
|
|
40
|
+
outline: "none",
|
|
41
|
+
padding: "0 20px 0 8px",
|
|
42
|
+
fontSize: "13px",
|
|
43
|
+
fontWeight: 500,
|
|
44
|
+
lineHeight: "20px",
|
|
45
|
+
textAlign: "left",
|
|
46
|
+
overflow: "hidden",
|
|
47
|
+
textOverflow: "ellipsis"
|
|
48
|
+
};
|
|
49
|
+
const menuBaseStyle = {
|
|
50
|
+
position: "absolute",
|
|
51
|
+
top: "calc(100% + 4px)",
|
|
52
|
+
zIndex: 1000,
|
|
53
|
+
minWidth: "260px",
|
|
54
|
+
maxHeight: "320px",
|
|
55
|
+
overflowY: "auto",
|
|
56
|
+
background: "var(--dsw-specific-selector)",
|
|
57
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
58
|
+
borderRadius: "var(--dsw-radius-md)",
|
|
59
|
+
boxShadow: "var(--dsw-elevation-soft)",
|
|
60
|
+
padding: "4px"
|
|
61
|
+
};
|
|
62
|
+
const menuRowStyle = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", width: "100%", padding: "8px 10px", borderRadius: "var(--dsw-radius-sm)", background: "none", border: "none", cursor: "pointer", fontSize: "13px", lineHeight: "18px", textAlign: "left", color: "var(--dsw-alias-label-primary)" };
|
|
63
|
+
const menuHeaderStyle = { fontSize: "11px", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--dsw-alias-label-secondary)", padding: "8px 10px 2px" };
|
|
64
|
+
function ModelPickerField(props) {
|
|
65
|
+
const { value, onChange, getSession, t, disabled = false } = props;
|
|
66
|
+
const [open, setOpen] = react.useState(false);
|
|
67
|
+
const [pane, setPane] = react.useState("root");
|
|
68
|
+
const [align, setAlign] = react.useState("left");
|
|
69
|
+
const triggerRef = react.useRef(null);
|
|
70
|
+
const [catalog, setCatalog] = react.useState({ status: "idle", groups: [] });
|
|
71
|
+
const inflightRef = react.useRef(null);
|
|
72
|
+
const retriesRef = react.useRef(0);
|
|
73
|
+
const timerRef = react.useRef(void 0);
|
|
74
|
+
const rootRef = react.useRef(null);
|
|
75
|
+
const loadCatalog = react.useCallback(() => {
|
|
76
|
+
if (catalog.status === "ready" || inflightRef.current !== null) return;
|
|
77
|
+
setCatalog((current) => ({ ...current, status: "loading" }));
|
|
78
|
+
const session = getSession();
|
|
79
|
+
if (session === undefined || session === null) {
|
|
80
|
+
setCatalog({ status: "error", groups: [] });
|
|
81
|
+
scheduleRetry();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
inflightRef.current = session.modelCatalog().then((response) => {
|
|
85
|
+
inflightRef.current = null;
|
|
86
|
+
if (response.ok) {
|
|
87
|
+
setCatalog({ status: "ready", groups: response.value.groups ?? [] });
|
|
88
|
+
retriesRef.current = 0;
|
|
89
|
+
} else {
|
|
90
|
+
setCatalog({ status: "error", groups: [] });
|
|
91
|
+
scheduleRetry();
|
|
92
|
+
}
|
|
93
|
+
}).catch(() => {
|
|
94
|
+
inflightRef.current = null;
|
|
95
|
+
setCatalog({ status: "error", groups: [] });
|
|
96
|
+
scheduleRetry();
|
|
97
|
+
});
|
|
98
|
+
}, [catalog.status, getSession]);
|
|
99
|
+
const scheduleRetry = react.useCallback(() => {
|
|
100
|
+
if (timerRef.current !== void 0 || retriesRef.current >= 4) return;
|
|
101
|
+
const delay = 2000 * 2 ** retriesRef.current;
|
|
102
|
+
retriesRef.current += 1;
|
|
103
|
+
timerRef.current = setTimeout(() => {
|
|
104
|
+
timerRef.current = void 0;
|
|
105
|
+
setCatalog((current) => current.status === "error" ? { ...current, status: "idle" } : current);
|
|
106
|
+
}, delay);
|
|
107
|
+
}, []);
|
|
108
|
+
const retry = react.useCallback(() => {
|
|
109
|
+
if (timerRef.current !== void 0) {
|
|
110
|
+
clearTimeout(timerRef.current);
|
|
111
|
+
timerRef.current = void 0;
|
|
112
|
+
}
|
|
113
|
+
retriesRef.current = 0;
|
|
114
|
+
setCatalog({ status: "idle", groups: [] });
|
|
115
|
+
}, []);
|
|
116
|
+
// lazy load on mount and after manual retry
|
|
117
|
+
react.useEffect(() => {
|
|
118
|
+
loadCatalog();
|
|
119
|
+
}, [loadCatalog]);
|
|
120
|
+
react.useEffect(() => () => {
|
|
121
|
+
if (timerRef.current !== void 0) clearTimeout(timerRef.current);
|
|
122
|
+
}, []);
|
|
123
|
+
// open/close handling
|
|
124
|
+
react.useEffect(() => {
|
|
125
|
+
if (!open) return;
|
|
126
|
+
const onDown = (event) => {
|
|
127
|
+
if (rootRef.current?.contains(event.target) === true) return;
|
|
128
|
+
setOpen(false);
|
|
129
|
+
setPane("root");
|
|
130
|
+
};
|
|
131
|
+
const onKey = (event) => {
|
|
132
|
+
if (event.key === "Escape") {
|
|
133
|
+
setOpen(false);
|
|
134
|
+
setPane("root");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener("mousedown", onDown);
|
|
138
|
+
document.addEventListener("keydown", onKey);
|
|
139
|
+
return () => {
|
|
140
|
+
document.removeEventListener("mousedown", onDown);
|
|
141
|
+
document.removeEventListener("keydown", onKey);
|
|
142
|
+
};
|
|
143
|
+
}, [open]);
|
|
144
|
+
const groups = catalog.status === "ready" ? catalog.groups.filter((group) => group.models?.length > 0) : [];
|
|
145
|
+
const models = groups.flatMap((group) => group.models.map((model) => ({ group, model })));
|
|
146
|
+
const provider = (value?.provider ?? "").trim();
|
|
147
|
+
const model = (value?.model ?? "").trim();
|
|
148
|
+
const effort = (value?.reasoningEffort ?? "").trim();
|
|
149
|
+
const current = models.find((entry) => entry.group.id === provider && entry.model.id === model);
|
|
150
|
+
const reasoning = current?.model.reasoning;
|
|
151
|
+
const effortChoices = reasoning === undefined ? [] : [
|
|
152
|
+
...(reasoning.defaultEffort === undefined ? [{ id: "", label: t("effortProviderDefault") }] : []),
|
|
153
|
+
...(reasoning.efforts ?? []).map((entry) => ({ id: entry.id, label: entry.name }))
|
|
154
|
+
];
|
|
155
|
+
const modelLabel = current?.model.name ?? (provider || model ? `${provider}/${model}` : t("modelEmpty"));
|
|
156
|
+
const effortLabel = reasoning === undefined ? "" : effortChoices.find((choice) => choice.id === effort)?.label ?? (effort || t("effortProviderDefault"));
|
|
157
|
+
const menuRow = (item) => react_jsx_runtime.jsxs("button", { type: "button", style: menuRowStyle, onClick: () => item.onSelect(), children: [
|
|
158
|
+
react_jsx_runtime.jsx("span", { children: item.label }),
|
|
159
|
+
item.checked ? react_jsx_runtime.jsx("span", { style: { opacity: 0.9 }, children: "✓" }) : item.chevron ? react_jsx_runtime.jsx("span", { style: { opacity: 0.6 }, children: "›" }) : null
|
|
160
|
+
] });
|
|
161
|
+
return react_jsx_runtime.jsxs("div", { ref: rootRef, style: { position: "relative" }, children: [
|
|
162
|
+
react_jsx_runtime.jsx("button", {
|
|
163
|
+
ref: triggerRef,
|
|
164
|
+
type: "button",
|
|
165
|
+
style: triggerStyle,
|
|
166
|
+
disabled,
|
|
167
|
+
onClick: () => {
|
|
168
|
+
if (catalog.status === "error") retry();
|
|
169
|
+
// Viewport-aware alignment: grow right when the trigger
|
|
170
|
+
// sits near the left (e.g. inside an editor panel),
|
|
171
|
+
// grow left when it sits near the right edge (settings
|
|
172
|
+
// rows) — otherwise the menu overflows its container.
|
|
173
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
174
|
+
if (rect) setAlign(rect.left + 272 < (globalThis.innerWidth ?? 100000) ? "left" : "right");
|
|
175
|
+
setOpen(!open);
|
|
176
|
+
setPane("root");
|
|
177
|
+
},
|
|
178
|
+
children: catalog.status === "ready" ? `${modelLabel}${effortLabel ? ` · ${effortLabel}` : ""}` : catalog.status === "error" ? (provider || model ? modelLabel : t("catalogFailed")) : t("catalogLoading")
|
|
179
|
+
}),
|
|
180
|
+
open ? react_jsx_runtime.jsxs("div", { role: "menu", style: { ...menuBaseStyle, ...(align === "left" ? { left: 0 } : { right: 0 }) }, children: pane === "root" ? [
|
|
181
|
+
menuRow({ label: t("pickerModel"), chevron: true, onSelect: () => setPane("model") }),
|
|
182
|
+
menuRow({ label: t("pickerEffort"), chevron: true, onSelect: () => setPane("effort") })
|
|
183
|
+
] : pane === "model" ? groups.flatMap((group) => [
|
|
184
|
+
react_jsx_runtime.jsx("div", { style: menuHeaderStyle, children: group.id, key: `h-${group.id}` }),
|
|
185
|
+
...group.models.map((entry) => menuRow({
|
|
186
|
+
label: entry.name ?? entry.id,
|
|
187
|
+
checked: provider === group.id && model === entry.id,
|
|
188
|
+
onSelect: () => {
|
|
189
|
+
onChange({
|
|
190
|
+
provider: group.id,
|
|
191
|
+
model: entry.id,
|
|
192
|
+
reasoningEffort: entry.reasoning?.defaultEffort ?? ""
|
|
193
|
+
});
|
|
194
|
+
setOpen(false);
|
|
195
|
+
setPane("root");
|
|
196
|
+
}
|
|
197
|
+
}))
|
|
198
|
+
]) : effortChoices.map((choice) => menuRow({
|
|
199
|
+
label: choice.label,
|
|
200
|
+
checked: choice.id === "" ? !effort : effort === choice.id,
|
|
201
|
+
onSelect: () => {
|
|
202
|
+
onChange({
|
|
203
|
+
provider,
|
|
204
|
+
model,
|
|
205
|
+
reasoningEffort: choice.id
|
|
206
|
+
});
|
|
207
|
+
setOpen(false);
|
|
208
|
+
setPane("root");
|
|
209
|
+
}
|
|
210
|
+
})) }) : null
|
|
211
|
+
] });
|
|
212
|
+
}
|
|
213
|
+
var ModelPickerBoundary = class extends react.Component {
|
|
214
|
+
constructor(props) {
|
|
215
|
+
super(props);
|
|
216
|
+
this.state = { failed: false };
|
|
217
|
+
}
|
|
218
|
+
static getDerivedStateFromError() {
|
|
219
|
+
return { failed: true };
|
|
220
|
+
}
|
|
221
|
+
componentDidCatch(error) {
|
|
222
|
+
console.error("weir-model-picker contained:", error);
|
|
223
|
+
}
|
|
224
|
+
render() {
|
|
225
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
exports.ModelPickerField = ModelPickerField;
|
|
229
|
+
exports.ModelPickerBoundary = ModelPickerBoundary;
|
|
230
|
+
return module.exports;
|
|
231
|
+
}
|
|
232
|
+
});
|