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 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
- let modelPicker = require("weir-model-picker");
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(modelPicker.ModelPickerField, {
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
- const inspect = (root) => {
233
- setExpanded(root);
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":"c29ae07483a296ef0eac6ec5dcb06fda99236a4a5d7a9f52789220362aea1b0d","client.chain-model.js":"81cdd04626900a63c38ccb3d9c4d117686c6f8a5da2ce9131b7aaf2f089dea77","client.disabled-categories-editor.js":"d4606a3172b47b25caf76fd64c8420a0079cd5f008bc67a9c71b3f146724e393","client.edit-lock-maintenance.js":"3a9e94fff33bb17cb60c51c7c317060c881486113209642bf7dc6f5c8f59eb51","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.notify-permissions.js":"c1c103ce5976a4032b62e12b3b3ecb907a574159bf89512539c68c4886ea37a7","client.notify-web.js":"b2ccf831524be4e94c532324cb5495793d4424e3b78d6014d2a3f3d20e9c5871","client.robash-editor.js":"cd6a283b72cb02598e32ca4ce03a1b8de5f328ecdd1f5f920377524f3bdcbe1c","client.robash-model.js":"356ee7a679223bcd351720b2bc9e7300e390d97a60d1c8d75998706b4644923b","client.settings-page.js":"03b228e12bb191a10e34d3af9fef0acc9d1093cfc2bf0e1a35bc59b57c0af72f","client.worktree-model.js":"761a0f6316a3f3af3f924a7784d61ca4bdbfd3c6b63c81c5681bf3a801280c26","client.worktree-view.js":"c8399f67041061a357e6f11bfed1e40340494d75216b4c10ce4b940a763f0824"}
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
+ });