@gilbertgt/dsh-plan-orchestrator 1.0.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/lib/client.js ADDED
@@ -0,0 +1,1242 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@gilbertgt/dsh-plan-orchestrator",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
29
+ let react = require("react");
30
+ react = __toESM(react, 1);
31
+ let react_jsx_runtime = require("react/jsx-runtime");
32
+ //#region src/client/locales.ts
33
+ const zh = {
34
+ nav: "Plan Mode",
35
+ overview: "總覽",
36
+ roles: "角色模型",
37
+ planning: "規劃",
38
+ execution: "執行",
39
+ review: "審查",
40
+ recovery: "恢復",
41
+ external: "External Issue",
42
+ diagnostics: "進階/診斷",
43
+ loading: "載入中…",
44
+ save: "儲存",
45
+ stop: "停止",
46
+ resume: "安全恢復"
47
+ };
48
+ const en = {
49
+ nav: "Plan Mode",
50
+ overview: "Overview",
51
+ roles: "Roles",
52
+ planning: "Planning",
53
+ execution: "Execution",
54
+ review: "Review",
55
+ recovery: "Recovery",
56
+ external: "External Issue",
57
+ diagnostics: "Diagnostics",
58
+ loading: "Loading…",
59
+ save: "Save",
60
+ stop: "Stop",
61
+ resume: "Resume safely"
62
+ };
63
+ //#endregion
64
+ //#region src/client/styles.ts
65
+ const STYLE = `
66
+ .planx{padding:18px;max-width:1180px;color:inherit}
67
+ .planx h2,.planx h3{margin:.2rem 0 .8rem}
68
+ .planx-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px}
69
+ .planx-tabs button,.planx button{padding:7px 10px;border-radius:8px;border:1px solid color-mix(in srgb,currentColor 18%,transparent);background:transparent;color:inherit;cursor:pointer}
70
+ .planx-tabs button[aria-selected="true"]{font-weight:700;outline:2px solid color-mix(in srgb,currentColor 35%,transparent)}
71
+ .planx-card{border:1px solid color-mix(in srgb,currentColor 15%,transparent);border-radius:12px;padding:14px;margin:10px 0;min-width:0}
72
+ .planx-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:10px}
73
+ .planx-muted{opacity:.72}.planx-error{border-left:3px solid currentColor;padding:8px;margin:8px 0}
74
+ .planx-chip{border-radius:999px!important;padding:4px 9px!important;white-space:nowrap}
75
+ .planx-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.planx-between{justify-content:space-between}
76
+ .planx label{display:grid;gap:4px;margin:8px 0}.planx input,.planx select{background:transparent;color:inherit;border:1px solid color-mix(in srgb,currentColor 20%,transparent);border-radius:7px;padding:7px;min-width:0}
77
+ .planx input[type="checkbox"]{width:auto}.planx pre{white-space:pre-wrap;word-break:break-word;max-height:320px;overflow:auto}
78
+ .planx-overlay{position:fixed;inset:0;background:rgba(0,0,0,.38);display:flex;justify-content:flex-end;z-index:9999}
79
+ .planx-dialog{width:min(720px,92vw);height:100%;overflow:auto;background:Canvas;color:CanvasText;padding:20px;box-shadow:0 0 30px rgba(0,0,0,.25)}
80
+ .planx-kv{display:grid;grid-template-columns:minmax(120px,1fr) 2fr;gap:6px 12px}.planx-kv>span:nth-child(odd){opacity:.72}
81
+ .planx progress{width:100%}
82
+ @media (prefers-color-scheme: dark){.planx-dialog{background:#161616;color:#f2f2f2}}
83
+ `;
84
+ //#endregion
85
+ //#region src/client/rpc-client.ts
86
+ /**
87
+ * Call the Host-owned exact `/api/plan-orchestrator/*` routes. DSH's web
88
+ * carrier authenticates these requests with the browser session cookie before
89
+ * dispatch, so this intentionally uses same-origin Fetch rather than the
90
+ * logical `ctx.connection.rpc` surface (which only exposes call/open in rc.1).
91
+ */
92
+ function createRpc(fetcher = globalThis.fetch.bind(globalThis)) {
93
+ return async (method, body = {}, signal) => {
94
+ const response = await fetcher(`/api/plan-orchestrator/${encodeURIComponent(method)}`, {
95
+ method: "POST",
96
+ credentials: "same-origin",
97
+ headers: { "content-type": "application/json" },
98
+ body: JSON.stringify(body),
99
+ signal
100
+ });
101
+ let value;
102
+ try {
103
+ value = await response.json();
104
+ } catch {
105
+ throw new Error(`Plan Mode RPC returned invalid JSON (${response.status})`);
106
+ }
107
+ if (!response.ok || !value?.ok) throw new Error(value?.error?.message ?? `Plan Mode RPC failed (${response.status})`);
108
+ return value.data;
109
+ };
110
+ }
111
+ //#endregion
112
+ //#region src/client/components/Overview.tsx
113
+ function Overview({ diagnostics, settings }) {
114
+ const caps = [
115
+ ["Native Plan Mode", diagnostics?.nativePlanMode],
116
+ ["SDK parallel backend", diagnostics?.sdkAvailable],
117
+ ["Git", diagnostics?.gitAvailable],
118
+ ["LSP", diagnostics?.lspAvailable],
119
+ ["gh", diagnostics?.ghAvailable],
120
+ ["Settings writable", diagnostics?.settingsWritable]
121
+ ];
122
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
123
+ className: "planx-grid",
124
+ children: [
125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
126
+ className: "planx-card",
127
+ children: [
128
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Compatibility" }),
129
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: ["DSH: ", diagnostics?.dshVersion ?? "detecting"] }),
130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: ["Plugin: ", diagnostics?.pluginVersion ?? "1.0.0"] }),
131
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: "Supported: 0.1.5-rc.1" }),
132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: "Preview: 0.1.5-rc.2" })
133
+ ]
134
+ }),
135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
136
+ className: "planx-card",
137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Capabilities" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
138
+ className: "planx-kv",
139
+ children: caps.flatMap(([name, value]) => [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }, `${name}-n`), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: value === void 0 ? "—" : value ? "Available" : "Unavailable" }, `${name}-v`)])
140
+ })]
141
+ }),
142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
143
+ className: "planx-card",
144
+ children: [
145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Safety" }),
146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: ["Plan Orchestrator: ", settings?.enabled ? "enabled" : "disabled"] }),
147
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: ["Strict Planner read-only: ", settings?.planning?.strictReadOnly ? "on" : "off"] }),
148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: "Ownership / trusted validation: mandatory" }),
149
+ diagnostics?.readOnlyDegraded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
150
+ role: "alert",
151
+ children: ["Degraded: ", diagnostics.readOnlyDegraded]
152
+ })
153
+ ]
154
+ })
155
+ ]
156
+ });
157
+ }
158
+ //#endregion
159
+ //#region src/client/components/Roles.tsx
160
+ const roleNames = [
161
+ "planner",
162
+ "worker",
163
+ "integrator",
164
+ "reviewer"
165
+ ];
166
+ function Roles({ settings, catalog, onRole, rpc }) {
167
+ const [errors, setErrors] = (0, react.useState)({});
168
+ const validate = async (role, route) => {
169
+ if (route.mode !== "fixed" || !route.provider || !route.model) {
170
+ setErrors((e) => ({
171
+ ...e,
172
+ [role]: ""
173
+ }));
174
+ return;
175
+ }
176
+ try {
177
+ await rpc("route-validate", { route });
178
+ setErrors((e) => ({
179
+ ...e,
180
+ [role]: ""
181
+ }));
182
+ } catch (e) {
183
+ setErrors((x) => ({
184
+ ...x,
185
+ [role]: e.message
186
+ }));
187
+ }
188
+ };
189
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
190
+ className: "planx-grid",
191
+ children: roleNames.map((role) => {
192
+ const r = settings?.roles?.[role] ?? {
193
+ mode: "current",
194
+ fallbacks: []
195
+ };
196
+ const providers = (catalog ?? []).map((x) => x.provider);
197
+ const models = (catalog ?? []).find((x) => x.provider.id === r.provider)?.models ?? [];
198
+ const change = (next) => {
199
+ onRole(role, next);
200
+ validate(role, next);
201
+ };
202
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
203
+ className: "planx-card",
204
+ children: [
205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: role[0].toUpperCase() + role.slice(1) }),
206
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Mode ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
207
+ value: r.mode,
208
+ onChange: (e) => change(e.target.value === "current" ? {
209
+ mode: "current",
210
+ fallbacks: r.fallbacks ?? []
211
+ } : {
212
+ ...r,
213
+ mode: "fixed"
214
+ }),
215
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
216
+ value: "current",
217
+ children: "Current Session"
218
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
219
+ value: "fixed",
220
+ children: "Fixed"
221
+ })]
222
+ })] }),
223
+ r.mode === "fixed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Provider ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
225
+ value: r.provider ?? "",
226
+ onChange: (e) => change({
227
+ ...r,
228
+ provider: e.target.value,
229
+ model: ""
230
+ }),
231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
232
+ value: "",
233
+ children: "Select…"
234
+ }), providers.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
235
+ value: p.id,
236
+ children: p.name ?? p.id
237
+ }, p.id))]
238
+ })] }),
239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Model ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
240
+ value: r.model ?? "",
241
+ onChange: (e) => change({
242
+ ...r,
243
+ model: e.target.value
244
+ }),
245
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
246
+ value: "",
247
+ children: "Select…"
248
+ }), models.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
249
+ value: m.id,
250
+ children: m.name ?? m.id
251
+ }, m.id))]
252
+ })] }),
253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Reasoning effort ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
254
+ value: r.reasoningEffort ?? "",
255
+ onBlur: () => void validate(role, r),
256
+ onChange: (e) => onRole(role, {
257
+ ...r,
258
+ reasoningEffort: e.target.value || void 0
259
+ })
260
+ })] }),
261
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Max Tokens ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
262
+ type: "number",
263
+ min: 1,
264
+ value: r.maxTokens ?? "",
265
+ onBlur: () => void validate(role, r),
266
+ onChange: (e) => onRole(role, {
267
+ ...r,
268
+ maxTokens: e.target.value ? Number(e.target.value) : void 0
269
+ })
270
+ })] })
271
+ ] }),
272
+ errors[role] && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
273
+ className: "planx-error",
274
+ role: "alert",
275
+ children: errors[role]
276
+ }),
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
278
+ className: "planx-muted",
279
+ children: "Fallbacks remain host-validated at execution preflight."
280
+ })
281
+ ]
282
+ }, role);
283
+ })
284
+ });
285
+ }
286
+ //#endregion
287
+ //#region src/client/components/Planning.tsx
288
+ function Planning({ settings, patch }) {
289
+ const p = settings.planning;
290
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
291
+ className: "planx-card",
292
+ children: [
293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Planning" }),
294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
295
+ type: "checkbox",
296
+ checked: p.adaptiveResearch,
297
+ onChange: (e) => patch({ adaptiveResearch: e.target.checked })
298
+ }), " Adaptive Research"] }) }),
299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
300
+ type: "checkbox",
301
+ checked: p.strictReadOnly,
302
+ onChange: (e) => patch({ strictReadOnly: e.target.checked })
303
+ }), " Strict Planner read-only"] }) }),
304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Initial read-file budget ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
305
+ type: "number",
306
+ min: 1,
307
+ max: 32,
308
+ value: p.maxInitialReadFiles,
309
+ onChange: (e) => patch({ maxInitialReadFiles: Number(e.target.value) })
310
+ })] }),
311
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Soft input-token budget ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
312
+ type: "number",
313
+ min: 1e3,
314
+ value: p.softInputTokens,
315
+ onChange: (e) => patch({ softInputTokens: Number(e.target.value) })
316
+ })] }),
317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
318
+ type: "checkbox",
319
+ checked: p.progressiveDiscovery,
320
+ onChange: (e) => patch({ progressiveDiscovery: e.target.checked })
321
+ }), " Progressive discovery"] }) }),
322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
323
+ type: "checkbox",
324
+ checked: p.requireExpansionReason,
325
+ onChange: (e) => patch({ requireExpansionReason: e.target.checked })
326
+ }), " Require reason before context expansion"] }) })
327
+ ]
328
+ });
329
+ }
330
+ //#endregion
331
+ //#region src/client/components/Execution.tsx
332
+ function Execution({ settings, patch }) {
333
+ const e = settings.execution;
334
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
335
+ className: "planx-card",
336
+ children: [
337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Execution" }),
338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Parallel mode ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
339
+ value: e.parallelMode,
340
+ onChange: (x) => patch({ parallelMode: x.target.value }),
341
+ children: [
342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
343
+ value: "auto",
344
+ children: "Auto"
345
+ }),
346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
347
+ value: "serial",
348
+ children: "Serial"
349
+ }),
350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
351
+ value: "worktree",
352
+ children: "Worktree when safe"
353
+ })
354
+ ]
355
+ })] }),
356
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Max parallel Workers ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
357
+ type: "number",
358
+ min: 1,
359
+ max: 8,
360
+ value: e.maxParallelWorkers,
361
+ onChange: (x) => patch({ maxParallelWorkers: Number(x.target.value) })
362
+ })] }),
363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["SDK child profile ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
364
+ value: e.sdkProfile,
365
+ onChange: (x) => patch({ sdkProfile: x.target.value })
366
+ })] }),
367
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Mutation role timeout (ms) ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
368
+ type: "number",
369
+ min: 1e3,
370
+ max: 36e5,
371
+ value: e.roleTimeoutMs,
372
+ onChange: (x) => patch({ roleTimeoutMs: Number(x.target.value) })
373
+ })] }),
374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
375
+ type: "checkbox",
376
+ checked: e.keepFailedWorktrees,
377
+ onChange: (x) => patch({ keepFailedWorktrees: x.target.checked })
378
+ }), " Retain failed worktrees for evidence"] }) }),
379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
380
+ className: "planx-muted",
381
+ children: "The timeout is enforced for native/SDK Worker, Integrator and targeted-fix mutation roles. Explicit ownership is always enforced and cannot be disabled."
382
+ })
383
+ ]
384
+ });
385
+ }
386
+ //#endregion
387
+ //#region src/client/components/Review.tsx
388
+ function Review({ settings, patch }) {
389
+ const r = settings.review;
390
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
391
+ className: "planx-card",
392
+ children: [
393
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Review" }),
394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Maximum targeted-fix rounds ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
395
+ type: "number",
396
+ min: 0,
397
+ max: 5,
398
+ value: r.maxReviewRounds,
399
+ onChange: (e) => patch({ maxReviewRounds: Number(e.target.value) })
400
+ })] }),
401
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Reviewer protocol retry ", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
402
+ value: r.protocolRetry,
403
+ onChange: (e) => patch({ protocolRetry: Number(e.target.value) }),
404
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
405
+ value: 0,
406
+ children: "0"
407
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
408
+ value: 1,
409
+ children: "1"
410
+ })]
411
+ })] }),
412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: ["Per-stream output cap (bytes) ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
413
+ type: "number",
414
+ min: 1024,
415
+ max: 16777216,
416
+ value: r.outputCapBytes,
417
+ onChange: (e) => patch({ outputCapBytes: Number(e.target.value) })
418
+ })] }),
419
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
420
+ className: "planx-muted",
421
+ children: "Trusted host validation is mandatory. Worker self-reports are never accepted as proof."
422
+ })
423
+ ]
424
+ });
425
+ }
426
+ //#endregion
427
+ //#region src/client/components/Recovery.tsx
428
+ function Recovery({ rpc, allowSafeResume }) {
429
+ const [runs, setRuns] = (0, react.useState)([]), [error, setError] = (0, react.useState)("");
430
+ const load = () => rpc("run-list", {}).then((r) => {
431
+ setRuns(r);
432
+ setError("");
433
+ }).catch((e) => setError(e.message));
434
+ (0, react.useEffect)(() => {
435
+ load();
436
+ }, []);
437
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
438
+ className: "planx-card",
439
+ children: [
440
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Recovery" }),
441
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
442
+ role: "alert",
443
+ children: error
444
+ }),
445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
446
+ className: "planx-muted",
447
+ children: "Resume is available only when Safe Resume is enabled and an INTERRUPTED run's checkpoint still matches HEAD, ownership and working-tree fingerprint. No reset/clean/stash recovery exists."
448
+ }),
449
+ runs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "No loaded runs." }) : runs.slice(0, 20).map((run) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
450
+ className: "planx-row",
451
+ children: [
452
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: run.runId.slice(0, 8) }),
453
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: run.phase }),
454
+ run.phase === "INTERRUPTED" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
455
+ disabled: !allowSafeResume,
456
+ title: !allowSafeResume ? "Safe Resume is disabled in Plan Mode settings." : void 0,
457
+ onClick: async () => {
458
+ const v = await rpc("run-resume", { runId: run.runId });
459
+ if (!v.ok) setError(v.reason);
460
+ await load();
461
+ },
462
+ children: "Resume safely"
463
+ }),
464
+ [
465
+ "COMPLETE",
466
+ "FAILED",
467
+ "BLOCKED",
468
+ "CANCELLED"
469
+ ].includes(run.phase) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
470
+ onClick: async () => {
471
+ if (confirm("Cleanup this terminal run?")) {
472
+ await rpc("run-cleanup", { runId: run.runId });
473
+ await load();
474
+ }
475
+ },
476
+ children: "Cleanup"
477
+ })
478
+ ]
479
+ }, run.runId))
480
+ ]
481
+ });
482
+ }
483
+ //#endregion
484
+ //#region src/client/components/ExternalIssue.tsx
485
+ function ExternalIssue({ settings, patch }) {
486
+ const x = settings.externalIssue;
487
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
488
+ className: "planx-card",
489
+ children: [
490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "External Issue" }),
491
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
492
+ type: "checkbox",
493
+ checked: x.enabled,
494
+ onChange: (e) => patch({ enabled: e.target.checked })
495
+ }), " Enable /plan-issue"] }) }),
496
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
497
+ type: "checkbox",
498
+ checked: x.publishAfterPass,
499
+ disabled: !x.enabled,
500
+ onChange: (e) => patch({ publishAfterPass: e.target.checked })
501
+ }), " After Reviewer PASS, commit/push/create or reuse PR"] }) }),
502
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
503
+ className: "planx-muted",
504
+ children: "Publication remains fail-closed: trusted revision, repo match, current validation receipts and Reviewer PASS are required. Force push and auto-merge are never used."
505
+ })
506
+ ]
507
+ });
508
+ }
509
+ //#endregion
510
+ //#region src/client/components/Diagnostics.tsx
511
+ function Diagnostics({ diagnostics }) {
512
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
513
+ className: "planx-card",
514
+ children: [
515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Diagnostics" }),
516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(diagnostics ?? {}, null, 2) }),
517
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
518
+ className: "planx-muted",
519
+ children: "Diagnostics intentionally expose capability state and plugin storage location, never credentials or raw validation logs."
520
+ })
521
+ ]
522
+ });
523
+ }
524
+ //#endregion
525
+ //#region src/contract/settings.ts
526
+ const SETTINGS_NAMESPACE = "plan-orchestrator";
527
+ const ROLE_NAMES = [
528
+ "planner",
529
+ "worker",
530
+ "integrator",
531
+ "reviewer"
532
+ ];
533
+ const SETTINGS_KEYS = /* @__PURE__ */ new Set([
534
+ "enabled",
535
+ "roles",
536
+ "planning",
537
+ "execution",
538
+ "review",
539
+ "recovery",
540
+ "externalIssue",
541
+ "workspaceOverrides"
542
+ ]);
543
+ const ROUTE_KEYS = /* @__PURE__ */ new Set([
544
+ "mode",
545
+ "provider",
546
+ "model",
547
+ "reasoningEffort",
548
+ "maxTokens",
549
+ "fallbacks"
550
+ ]);
551
+ const CHOICE_KEYS = /* @__PURE__ */ new Set([
552
+ "provider",
553
+ "model",
554
+ "reasoningEffort",
555
+ "maxTokens"
556
+ ]);
557
+ const PLANNING_KEYS = /* @__PURE__ */ new Set([
558
+ "adaptiveResearch",
559
+ "strictReadOnly",
560
+ "maxInitialReadFiles",
561
+ "softInputTokens",
562
+ "progressiveDiscovery",
563
+ "requireExpansionReason"
564
+ ]);
565
+ const EXECUTION_KEYS = /* @__PURE__ */ new Set([
566
+ "maxParallelWorkers",
567
+ "parallelMode",
568
+ "requireExplicitOwnership",
569
+ "keepFailedWorktrees",
570
+ "sdkProfile",
571
+ "roleTimeoutMs"
572
+ ]);
573
+ const REVIEW_KEYS = /* @__PURE__ */ new Set([
574
+ "maxReviewRounds",
575
+ "protocolRetry",
576
+ "trustedValidation",
577
+ "outputCapBytes"
578
+ ]);
579
+ const RECOVERY_KEYS = /* @__PURE__ */ new Set(["allowSafeResume"]);
580
+ const EXTERNAL_KEYS = /* @__PURE__ */ new Set(["enabled", "publishAfterPass"]);
581
+ const WORKSPACE_KEYS = /* @__PURE__ */ new Set(["roles"]);
582
+ const defaultRoute = () => ({
583
+ mode: "current",
584
+ fallbacks: []
585
+ });
586
+ Object.freeze({
587
+ enabled: true,
588
+ roles: {
589
+ planner: defaultRoute(),
590
+ worker: defaultRoute(),
591
+ integrator: defaultRoute(),
592
+ reviewer: defaultRoute()
593
+ },
594
+ planning: {
595
+ adaptiveResearch: true,
596
+ strictReadOnly: true,
597
+ maxInitialReadFiles: 6,
598
+ softInputTokens: 2e4,
599
+ progressiveDiscovery: true,
600
+ requireExpansionReason: true
601
+ },
602
+ execution: {
603
+ maxParallelWorkers: 3,
604
+ parallelMode: "auto",
605
+ requireExplicitOwnership: true,
606
+ keepFailedWorktrees: true,
607
+ sdkProfile: "sdk",
608
+ roleTimeoutMs: 9e5
609
+ },
610
+ review: {
611
+ maxReviewRounds: 2,
612
+ protocolRetry: 1,
613
+ trustedValidation: true,
614
+ outputCapBytes: 16 * 1024 * 1024
615
+ },
616
+ recovery: { allowSafeResume: true },
617
+ externalIssue: {
618
+ enabled: false,
619
+ publishAfterPass: false
620
+ },
621
+ workspaceOverrides: {}
622
+ });
623
+ function object(value, where) {
624
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${where} must be an object`);
625
+ return value;
626
+ }
627
+ function strictKeys(value, allowed, where) {
628
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
629
+ if (unknown.length > 0) throw new Error(`${where} has unknown field(s): ${unknown.join(", ")}`);
630
+ }
631
+ function bool(value, where) {
632
+ if (typeof value !== "boolean") throw new Error(`${where} must be boolean`);
633
+ return value;
634
+ }
635
+ function integer(value, where, min, max) {
636
+ if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error(`${where} must be an integer from ${min} through ${max}`);
637
+ return value;
638
+ }
639
+ function optionalString(value, where, max = 256) {
640
+ if (value === void 0) return void 0;
641
+ if (typeof value !== "string" || value.length === 0 || value.length > max || value.includes("\0")) throw new Error(`${where} invalid`);
642
+ return value;
643
+ }
644
+ function routeChoice(value, where) {
645
+ const x = object(value, where);
646
+ strictKeys(x, CHOICE_KEYS, where);
647
+ const provider = optionalString(x.provider, `${where}.provider`);
648
+ const model = optionalString(x.model, `${where}.model`);
649
+ if (!provider || !model) throw new Error(`${where} needs provider and model`);
650
+ const reasoningEffort = optionalString(x.reasoningEffort, `${where}.reasoningEffort`, 64);
651
+ const maxTokens = x.maxTokens === void 0 ? void 0 : integer(x.maxTokens, `${where}.maxTokens`, 1, 2e6);
652
+ return {
653
+ provider,
654
+ model,
655
+ ...reasoningEffort ? { reasoningEffort } : {},
656
+ ...maxTokens ? { maxTokens } : {}
657
+ };
658
+ }
659
+ function roleRoute(value, where, workspace) {
660
+ const x = object(value, where);
661
+ strictKeys(x, ROUTE_KEYS, where);
662
+ const allowedModes = workspace ? [
663
+ "inherit",
664
+ "current",
665
+ "fixed"
666
+ ] : ["current", "fixed"];
667
+ if (typeof x.mode !== "string" || !allowedModes.includes(x.mode)) throw new Error(`${where}.mode invalid`);
668
+ const mode = x.mode;
669
+ const provider = optionalString(x.provider, `${where}.provider`);
670
+ const model = optionalString(x.model, `${where}.model`);
671
+ const reasoningEffort = optionalString(x.reasoningEffort, `${where}.reasoningEffort`, 64);
672
+ const maxTokens = x.maxTokens === void 0 ? void 0 : integer(x.maxTokens, `${where}.maxTokens`, 1, 2e6);
673
+ if (mode === "fixed" && (!provider || !model)) throw new Error(`${where} fixed route needs provider and model`);
674
+ if (mode !== "fixed" && (provider !== void 0 || model !== void 0 || reasoningEffort !== void 0 || maxTokens !== void 0)) throw new Error(`${where} ${mode} route cannot carry fixed-route fields`);
675
+ if (!Array.isArray(x.fallbacks) || x.fallbacks.length > 8) throw new Error(`${where}.fallbacks invalid`);
676
+ return {
677
+ mode,
678
+ fallbacks: x.fallbacks.map((item, index) => routeChoice(item, `${where}.fallbacks[${index}]`)),
679
+ ...provider ? { provider } : {},
680
+ ...model ? { model } : {},
681
+ ...reasoningEffort ? { reasoningEffort } : {},
682
+ ...maxTokens ? { maxTokens } : {}
683
+ };
684
+ }
685
+ function section(value, allowed, where) {
686
+ const x = object(value, where);
687
+ strictKeys(x, allowed, where);
688
+ return x;
689
+ }
690
+ function validateSettings(value) {
691
+ const x = object(value, "settings");
692
+ strictKeys(x, SETTINGS_KEYS, "settings");
693
+ const rolesRaw = object(x.roles, "settings.roles");
694
+ strictKeys(rolesRaw, new Set(ROLE_NAMES), "settings.roles");
695
+ const roles = Object.fromEntries(ROLE_NAMES.map((role) => [role, roleRoute(rolesRaw[role], `settings.roles.${role}`, false)]));
696
+ const planning = section(x.planning, PLANNING_KEYS, "settings.planning");
697
+ const execution = section(x.execution, EXECUTION_KEYS, "settings.execution");
698
+ const review = section(x.review, REVIEW_KEYS, "settings.review");
699
+ const recovery = section(x.recovery, RECOVERY_KEYS, "settings.recovery");
700
+ const externalIssue = section(x.externalIssue, EXTERNAL_KEYS, "settings.externalIssue");
701
+ const workspaceRaw = object(x.workspaceOverrides, "settings.workspaceOverrides");
702
+ const workspaceOverrides = {};
703
+ if (Object.keys(workspaceRaw).length > 256) throw new Error("settings.workspaceOverrides has too many entries");
704
+ for (const [workspaceKey, raw] of Object.entries(workspaceRaw)) {
705
+ if (!/^[A-Za-z0-9_-]{16,128}$/.test(workspaceKey)) throw new Error(`invalid workspace override key: ${workspaceKey}`);
706
+ const override = object(raw, `settings.workspaceOverrides.${workspaceKey}`);
707
+ strictKeys(override, WORKSPACE_KEYS, `settings.workspaceOverrides.${workspaceKey}`);
708
+ const result = {};
709
+ if (override.roles !== void 0) {
710
+ const roleObject = object(override.roles, `settings.workspaceOverrides.${workspaceKey}.roles`);
711
+ strictKeys(roleObject, new Set(ROLE_NAMES), `settings.workspaceOverrides.${workspaceKey}.roles`);
712
+ result.roles = {};
713
+ for (const [role, route] of Object.entries(roleObject)) result.roles[role] = roleRoute(route, `settings.workspaceOverrides.${workspaceKey}.roles.${role}`, true);
714
+ }
715
+ workspaceOverrides[workspaceKey] = result;
716
+ }
717
+ if (bool(execution.requireExplicitOwnership, "settings.execution.requireExplicitOwnership") !== true) throw new Error("settings.execution.requireExplicitOwnership is a security invariant and must be true");
718
+ if (bool(review.trustedValidation, "settings.review.trustedValidation") !== true) throw new Error("settings.review.trustedValidation is a security invariant and must be true");
719
+ const parallelMode = execution.parallelMode;
720
+ if (![
721
+ "auto",
722
+ "serial",
723
+ "worktree"
724
+ ].includes(String(parallelMode))) throw new Error("settings.execution.parallelMode invalid");
725
+ return {
726
+ enabled: bool(x.enabled, "settings.enabled"),
727
+ roles,
728
+ planning: {
729
+ adaptiveResearch: bool(planning.adaptiveResearch, "settings.planning.adaptiveResearch"),
730
+ strictReadOnly: bool(planning.strictReadOnly, "settings.planning.strictReadOnly"),
731
+ maxInitialReadFiles: integer(planning.maxInitialReadFiles, "settings.planning.maxInitialReadFiles", 1, 32),
732
+ softInputTokens: integer(planning.softInputTokens, "settings.planning.softInputTokens", 1e3, 2e6),
733
+ progressiveDiscovery: bool(planning.progressiveDiscovery, "settings.planning.progressiveDiscovery"),
734
+ requireExpansionReason: bool(planning.requireExpansionReason, "settings.planning.requireExpansionReason")
735
+ },
736
+ execution: {
737
+ maxParallelWorkers: integer(execution.maxParallelWorkers, "settings.execution.maxParallelWorkers", 1, 8),
738
+ parallelMode,
739
+ requireExplicitOwnership: true,
740
+ keepFailedWorktrees: bool(execution.keepFailedWorktrees, "settings.execution.keepFailedWorktrees"),
741
+ sdkProfile: optionalString(execution.sdkProfile, "settings.execution.sdkProfile", 128),
742
+ roleTimeoutMs: integer(execution.roleTimeoutMs, "settings.execution.roleTimeoutMs", 1e3, 36e5)
743
+ },
744
+ review: {
745
+ maxReviewRounds: integer(review.maxReviewRounds, "settings.review.maxReviewRounds", 0, 5),
746
+ protocolRetry: integer(review.protocolRetry, "settings.review.protocolRetry", 0, 1),
747
+ trustedValidation: true,
748
+ outputCapBytes: integer(review.outputCapBytes, "settings.review.outputCapBytes", 1024, 16 * 1024 * 1024)
749
+ },
750
+ recovery: { allowSafeResume: bool(recovery.allowSafeResume, "settings.recovery.allowSafeResume") },
751
+ externalIssue: {
752
+ enabled: bool(externalIssue.enabled, "settings.externalIssue.enabled"),
753
+ publishAfterPass: bool(externalIssue.publishAfterPass, "settings.externalIssue.publishAfterPass")
754
+ },
755
+ workspaceOverrides
756
+ };
757
+ }
758
+ //#endregion
759
+ //#region src/client/section.tsx
760
+ const tabs = [
761
+ ["overview", "Overview"],
762
+ ["roles", "Roles"],
763
+ ["planning", "Planning"],
764
+ ["execution", "Execution"],
765
+ ["review", "Review"],
766
+ ["recovery", "Recovery"],
767
+ ["external", "ExternalIssue"],
768
+ ["diagnostics", "Diagnostics"]
769
+ ];
770
+ const settingFields = [
771
+ "enabled",
772
+ "roles",
773
+ "planning",
774
+ "execution",
775
+ "review",
776
+ "recovery",
777
+ "externalIssue",
778
+ "workspaceOverrides"
779
+ ];
780
+ function PlanModeSection({ rpc, t, settingsScope }) {
781
+ const [tab, setTab] = (0, react.useState)("overview"), [diag, setDiag] = (0, react.useState)(), [settings, setSettings] = (0, react.useState)(), [catalog, setCatalog] = (0, react.useState)([]), [dirty, setDirty] = (0, react.useState)(false), [error, setError] = (0, react.useState)("");
782
+ const draftRevision = (0, react.useRef)();
783
+ (0, react.useEffect)(() => settingsScope.subscribe(() => {
784
+ const snap = settingsScope.getSnapshot();
785
+ if (!dirty && snap.status === "ready" && snap.value) {
786
+ setSettings(snap.value);
787
+ draftRevision.current = snap.revision;
788
+ }
789
+ }), [settingsScope, dirty]);
790
+ (0, react.useEffect)(() => {
791
+ const snap = settingsScope.getSnapshot();
792
+ if (snap.status === "ready" && snap.value) {
793
+ setSettings(snap.value);
794
+ draftRevision.current = snap.revision;
795
+ }
796
+ Promise.all([rpc("diagnostics"), rpc("model-catalog")]).then(([d, c]) => {
797
+ setDiag(d);
798
+ setCatalog(c);
799
+ }).catch((e) => setError(e.message));
800
+ }, [rpc, settingsScope]);
801
+ const mutate = (path, patch) => {
802
+ setSettings((s) => ({
803
+ ...s,
804
+ [path]: {
805
+ ...s[path],
806
+ ...patch
807
+ }
808
+ }));
809
+ setDirty(true);
810
+ };
811
+ const onRole = (role, value) => {
812
+ setSettings((s) => ({
813
+ ...s,
814
+ roles: {
815
+ ...s.roles,
816
+ [role]: value
817
+ }
818
+ }));
819
+ setDirty(true);
820
+ };
821
+ const save = async () => {
822
+ try {
823
+ const checked = validateSettings(settings);
824
+ const ops = settingFields.map((field) => ({
825
+ op: "set",
826
+ path: [field],
827
+ value: checked[field]
828
+ }));
829
+ const expected = draftRevision.current;
830
+ await settingsScope.mutate(ops, expected);
831
+ const snap = settingsScope.getSnapshot();
832
+ if (snap.status !== "ready" || JSON.stringify(snap.value) !== JSON.stringify(checked)) throw new Error("Settings changed concurrently; your draft was not silently overwritten. Reload the current values and reapply the change.");
833
+ setSettings(snap.value);
834
+ draftRevision.current = snap.revision;
835
+ setDirty(false);
836
+ setError("");
837
+ } catch (e) {
838
+ setError(e.message);
839
+ }
840
+ };
841
+ const tabRefs = (0, react.useRef)([]);
842
+ const key = (event, index) => {
843
+ if (![
844
+ "ArrowLeft",
845
+ "ArrowRight",
846
+ "Home",
847
+ "End"
848
+ ].includes(event.key)) return;
849
+ event.preventDefault();
850
+ let next = index;
851
+ if (event.key === "ArrowLeft") next = (index + tabs.length - 1) % tabs.length;
852
+ if (event.key === "ArrowRight") next = (index + 1) % tabs.length;
853
+ if (event.key === "Home") next = 0;
854
+ if (event.key === "End") next = tabs.length - 1;
855
+ setTab(tabs[next][0]);
856
+ tabRefs.current[next]?.focus();
857
+ };
858
+ if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
859
+ className: "planx",
860
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t?.("loading") ?? "Loading…" }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
861
+ role: "alert",
862
+ children: error
863
+ })]
864
+ });
865
+ let content;
866
+ if (tab === "overview") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Overview, {
867
+ diagnostics: diag,
868
+ settings
869
+ });
870
+ else if (tab === "roles") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Roles, {
871
+ settings,
872
+ catalog,
873
+ onRole,
874
+ rpc
875
+ });
876
+ else if (tab === "planning") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Planning, {
877
+ settings,
878
+ patch: (p) => mutate("planning", p)
879
+ });
880
+ else if (tab === "execution") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Execution, {
881
+ settings,
882
+ patch: (p) => mutate("execution", p)
883
+ });
884
+ else if (tab === "review") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Review, {
885
+ settings,
886
+ patch: (p) => mutate("review", p)
887
+ });
888
+ else if (tab === "recovery") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Recovery, {
889
+ rpc,
890
+ allowSafeResume: settings.recovery.allowSafeResume
891
+ });
892
+ else if (tab === "external") content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExternalIssue, {
893
+ settings,
894
+ patch: (p) => mutate("externalIssue", p)
895
+ });
896
+ else content = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Diagnostics, { diagnostics: diag });
897
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
898
+ className: "planx",
899
+ children: [
900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
901
+ className: "planx-row planx-between",
902
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "Plan Mode" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
903
+ className: "planx-row",
904
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
905
+ type: "checkbox",
906
+ checked: settings.enabled,
907
+ onChange: (e) => {
908
+ setSettings((s) => ({
909
+ ...s,
910
+ enabled: e.target.checked
911
+ }));
912
+ setDirty(true);
913
+ }
914
+ }), " Enabled"] }) }), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
915
+ onClick: save,
916
+ children: t?.("save") ?? "Save"
917
+ })]
918
+ })]
919
+ }),
920
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
921
+ role: "alert",
922
+ className: "planx-error",
923
+ children: error
924
+ }),
925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
926
+ className: "planx-tabs",
927
+ role: "tablist",
928
+ "aria-label": "Plan Mode settings",
929
+ children: tabs.map(([id, label], index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
930
+ ref: (el) => {
931
+ tabRefs.current[index] = el;
932
+ },
933
+ role: "tab",
934
+ "aria-selected": tab === id,
935
+ tabIndex: tab === id ? 0 : -1,
936
+ onKeyDown: (e) => key(e, index),
937
+ onClick: () => setTab(id),
938
+ children: t?.(id) ?? label
939
+ }, id))
940
+ }),
941
+ content
942
+ ]
943
+ });
944
+ }
945
+ //#endregion
946
+ //#region src/client/run-chip.tsx
947
+ function RunChip({ rpc, sessionId, overlay }) {
948
+ const [run, setRun] = (0, react.useState)();
949
+ (0, react.useEffect)(() => {
950
+ let live = true;
951
+ const load = () => rpc("run-list", { sessionId }).then((rows) => {
952
+ if (live) setRun(rows[0]);
953
+ }).catch(() => {});
954
+ load();
955
+ const timer = setInterval(load, 2500);
956
+ return () => {
957
+ live = false;
958
+ clearInterval(timer);
959
+ };
960
+ }, [rpc, sessionId]);
961
+ if (!run) return null;
962
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
963
+ className: "planx-chip",
964
+ "aria-label": `Plan Run ${run.phase}`,
965
+ onClick: () => overlay.open(run.runId),
966
+ children: [
967
+ "Plan Run · ",
968
+ run.tasksDone ?? 0,
969
+ "/",
970
+ run.tasksTotal ?? 0,
971
+ " · ",
972
+ run.phase
973
+ ]
974
+ });
975
+ }
976
+ //#endregion
977
+ //#region src/client/run-overlay.tsx
978
+ var RunOverlayService = class {
979
+ #runId;
980
+ #listeners = /* @__PURE__ */ new Set();
981
+ #returnFocus = null;
982
+ get runId() {
983
+ return this.#runId;
984
+ }
985
+ open(runId) {
986
+ if (typeof document !== "undefined" && document.activeElement instanceof HTMLElement) this.#returnFocus = document.activeElement;
987
+ this.#runId = runId;
988
+ this.#emit();
989
+ }
990
+ close() {
991
+ if (this.#runId === void 0) return;
992
+ this.#runId = void 0;
993
+ this.#emit();
994
+ const target = this.#returnFocus;
995
+ this.#returnFocus = null;
996
+ if (target?.isConnected) queueMicrotask(() => target.focus());
997
+ }
998
+ subscribe(listener) {
999
+ this.#listeners.add(listener);
1000
+ return () => this.#listeners.delete(listener);
1001
+ }
1002
+ #emit() {
1003
+ for (const listener of this.#listeners) listener();
1004
+ }
1005
+ };
1006
+ function RunOverlay({ service, rpc, t }) {
1007
+ const [runId, setRunId] = (0, react.useState)(service.runId);
1008
+ const [run, setRun] = (0, react.useState)();
1009
+ const [error, setError] = (0, react.useState)("");
1010
+ const dialogRef = (0, react.useRef)(null);
1011
+ const titleId = "planx-run-dialog-title";
1012
+ (0, react.useEffect)(() => service.subscribe(() => setRunId(service.runId)), [service]);
1013
+ (0, react.useEffect)(() => {
1014
+ if (!runId) {
1015
+ setRun(void 0);
1016
+ return;
1017
+ }
1018
+ let live = true;
1019
+ const load = async () => {
1020
+ try {
1021
+ const value = await rpc("run-detail", { runId });
1022
+ if (live) {
1023
+ setRun(value);
1024
+ setError("");
1025
+ }
1026
+ } catch (e) {
1027
+ if (live) setError(e.message);
1028
+ }
1029
+ };
1030
+ load();
1031
+ const timer = setInterval(load, 2500);
1032
+ return () => {
1033
+ live = false;
1034
+ clearInterval(timer);
1035
+ };
1036
+ }, [runId, rpc]);
1037
+ (0, react.useEffect)(() => {
1038
+ if (!runId) return;
1039
+ dialogRef.current?.focus();
1040
+ const onKeyDown = (event) => {
1041
+ if (event.key === "Escape") {
1042
+ event.preventDefault();
1043
+ service.close();
1044
+ }
1045
+ };
1046
+ document.addEventListener("keydown", onKeyDown);
1047
+ return () => document.removeEventListener("keydown", onKeyDown);
1048
+ }, [runId, service]);
1049
+ if (!runId) return null;
1050
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1051
+ className: "planx-overlay",
1052
+ role: "presentation",
1053
+ onMouseDown: (event) => {
1054
+ if (event.target === event.currentTarget) service.close();
1055
+ },
1056
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1057
+ ref: dialogRef,
1058
+ role: "dialog",
1059
+ "aria-modal": "true",
1060
+ "aria-labelledby": titleId,
1061
+ className: "planx-dialog",
1062
+ tabIndex: -1,
1063
+ children: [
1064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1065
+ className: "planx-row planx-between",
1066
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("b", {
1067
+ id: titleId,
1068
+ children: ["Plan Run ", runId]
1069
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1070
+ "aria-label": "Close",
1071
+ onClick: () => service.close(),
1072
+ children: "×"
1073
+ })]
1074
+ }),
1075
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1076
+ role: "alert",
1077
+ className: "planx-error",
1078
+ children: error
1079
+ }),
1080
+ !run ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t?.("loading") ?? "Loading…" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1081
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: run.phase }), run.message ? ` · ${run.message}` : ""] }),
1082
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("progress", {
1083
+ "aria-label": "Plan task progress",
1084
+ max: Math.max(1, run.tasksTotal ?? 1),
1085
+ value: run.tasksDone ?? 0
1086
+ }),
1087
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
1088
+ run.tasksDone ?? 0,
1089
+ "/",
1090
+ run.tasksTotal ?? 0,
1091
+ " tasks · review round ",
1092
+ run.reviewRound ?? 0
1093
+ ] }),
1094
+ run.activeTaskIds?.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: ["Active: ", run.activeTaskIds.join(", ")] }),
1095
+ run.review && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1096
+ className: "planx-card",
1097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "Review" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(run.review, null, 2) })]
1098
+ }),
1099
+ Array.isArray(run.receipts) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1100
+ className: "planx-card",
1101
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "Validation receipts" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(run.receipts, null, 2) })]
1102
+ }),
1103
+ run.usage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1104
+ className: "planx-card",
1105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "Usage" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(run.usage, null, 2) })]
1106
+ }),
1107
+ Array.isArray(run.worktrees) && run.worktrees.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1108
+ className: "planx-card",
1109
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: "Worktrees" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: JSON.stringify(run.worktrees.map((w) => ({
1110
+ taskId: w.taskId,
1111
+ status: w.status,
1112
+ baseHead: w.baseHead
1113
+ })), null, 2) })]
1114
+ }),
1115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1116
+ className: "planx-row",
1117
+ children: [
1118
+ ![
1119
+ "COMPLETE",
1120
+ "FAILED",
1121
+ "BLOCKED",
1122
+ "CANCELLED"
1123
+ ].includes(run.phase) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1124
+ onClick: () => rpc("run-cancel", { runId }).then(() => setRun((r) => ({
1125
+ ...r,
1126
+ phase: "CANCELLED"
1127
+ }))).catch((e) => setError(e.message)),
1128
+ children: t?.("stop") ?? "Stop"
1129
+ }),
1130
+ run.phase === "INTERRUPTED" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1131
+ onClick: async () => {
1132
+ try {
1133
+ const result = await rpc("run-resume", { runId });
1134
+ if (!result?.ok) setError(result?.reason ?? "Resume blocked");
1135
+ } catch (e) {
1136
+ setError(e.message);
1137
+ }
1138
+ },
1139
+ children: t?.("resume") ?? "Resume safely"
1140
+ }),
1141
+ [
1142
+ "COMPLETE",
1143
+ "FAILED",
1144
+ "BLOCKED",
1145
+ "CANCELLED"
1146
+ ].includes(run.phase) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1147
+ onClick: async () => {
1148
+ if (confirm("Remove Plan Orchestrator run artifacts and owned worktrees?")) try {
1149
+ await rpc("run-cleanup", { runId });
1150
+ service.close();
1151
+ } catch (e) {
1152
+ setError(e.message);
1153
+ }
1154
+ },
1155
+ children: "Cleanup"
1156
+ })
1157
+ ]
1158
+ })
1159
+ ] })
1160
+ ]
1161
+ })
1162
+ });
1163
+ }
1164
+ //#endregion
1165
+ //#region src/client/index.tsx
1166
+ const inject = [
1167
+ "slots",
1168
+ "locale",
1169
+ "connection",
1170
+ "settingsScope",
1171
+ "sessions"
1172
+ ];
1173
+ function apply(ctx) {
1174
+ const NS = "plan-orchestrator";
1175
+ ctx.effect(() => ctx.locale.register(NS, {
1176
+ zh,
1177
+ en
1178
+ }), "plan-orchestrator: locale");
1179
+ ctx.effect(() => {
1180
+ const style = document.createElement("style");
1181
+ style.dataset.plugin = "dsh-plan-orchestrator";
1182
+ style.textContent = STYLE;
1183
+ document.head.append(style);
1184
+ return () => style.remove();
1185
+ }, "plan-orchestrator: style");
1186
+ const t = ctx.locale.bind(NS);
1187
+ const rpc = createRpc();
1188
+ const settingsScope = ctx.settingsScope.bind({
1189
+ namespace: SETTINGS_NAMESPACE,
1190
+ decode(value) {
1191
+ try {
1192
+ return validateSettings(value);
1193
+ } catch {
1194
+ return;
1195
+ }
1196
+ }
1197
+ });
1198
+ const overlay = new RunOverlayService();
1199
+ ctx.effect(() => () => overlay.close(), "plan-orchestrator: overlay service");
1200
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1201
+ name: "settings.section",
1202
+ id: "plan-orchestrator",
1203
+ order: 20,
1204
+ label: () => t("nav"),
1205
+ locale: NS,
1206
+ inject: () => ({
1207
+ rpc,
1208
+ t,
1209
+ settingsScope
1210
+ })
1211
+ }, PlanModeSection));
1212
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1213
+ name: "conversation.input.right",
1214
+ id: "plan-orchestrator-run",
1215
+ order: 20,
1216
+ locale: NS,
1217
+ inject: (sessionId) => ({
1218
+ rpc,
1219
+ sessionId,
1220
+ overlay
1221
+ })
1222
+ }, RunChip));
1223
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1224
+ name: "shell.overlay",
1225
+ id: "plan-orchestrator-run-overlay",
1226
+ order: 21,
1227
+ locale: NS,
1228
+ inject: () => ({
1229
+ service: overlay,
1230
+ rpc,
1231
+ t
1232
+ })
1233
+ }, RunOverlay));
1234
+ }
1235
+ //#endregion
1236
+ exports.apply = apply;
1237
+ exports.inject = inject;
1238
+ return module.exports;
1239
+ }
1240
+ });
1241
+
1242
+ //# sourceMappingURL=client.js.map