codedeck 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +49 -34
  2. package/dist/cli/commands/claims.js +3 -2
  3. package/dist/cli/commands/claims.js.map +1 -1
  4. package/dist/cli/commands/doctor.js +8 -40
  5. package/dist/cli/commands/doctor.js.map +1 -1
  6. package/dist/cli/commands/logs.js +2 -2
  7. package/dist/cli/commands/logs.js.map +1 -1
  8. package/dist/cli/commands/open.js +8 -9
  9. package/dist/cli/commands/open.js.map +1 -1
  10. package/dist/cli/commands/ps.js +6 -6
  11. package/dist/cli/commands/ps.js.map +1 -1
  12. package/dist/cli/commands/review.js +36 -58
  13. package/dist/cli/commands/review.js.map +1 -1
  14. package/dist/cli/commands/run.js +27 -27
  15. package/dist/cli/commands/run.js.map +1 -1
  16. package/dist/cli/commands/setup.js +96 -314
  17. package/dist/cli/commands/setup.js.map +1 -1
  18. package/dist/cli/commands/show.js +2 -1
  19. package/dist/cli/commands/show.js.map +1 -1
  20. package/dist/cli/commands/ui.js +61 -0
  21. package/dist/cli/commands/ui.js.map +1 -0
  22. package/dist/cli/commands/usage.js +65 -42
  23. package/dist/cli/commands/usage.js.map +1 -1
  24. package/dist/cli/commands/wait.js +3 -2
  25. package/dist/cli/commands/wait.js.map +1 -1
  26. package/dist/cli/index.js +55 -39
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/web-launch.js +126 -0
  29. package/dist/cli/web-launch.js.map +1 -0
  30. package/dist/config/config.js +0 -70
  31. package/dist/config/config.js.map +1 -1
  32. package/dist/config/paths.js +1 -0
  33. package/dist/config/paths.js.map +1 -1
  34. package/dist/config/setup.js +248 -0
  35. package/dist/config/setup.js.map +1 -0
  36. package/dist/config/web-host.js +31 -0
  37. package/dist/config/web-host.js.map +1 -0
  38. package/dist/config/web-port.js +24 -0
  39. package/dist/config/web-port.js.map +1 -0
  40. package/dist/core/claude-transcript.js +87 -0
  41. package/dist/core/claude-transcript.js.map +1 -1
  42. package/dist/core/errors.js +17 -0
  43. package/dist/core/errors.js.map +1 -1
  44. package/dist/core/models.js +4 -1
  45. package/dist/core/models.js.map +1 -1
  46. package/dist/core/pricing.js +73 -50
  47. package/dist/core/pricing.js.map +1 -1
  48. package/dist/core/roles.js +6 -5
  49. package/dist/core/roles.js.map +1 -1
  50. package/dist/core/run-skills.js +114 -0
  51. package/dist/core/run-skills.js.map +1 -0
  52. package/dist/core/run-usage.js +4 -1
  53. package/dist/core/run-usage.js.map +1 -1
  54. package/dist/core/session.js +6 -16
  55. package/dist/core/session.js.map +1 -1
  56. package/dist/core/usage-query.js +41 -0
  57. package/dist/core/usage-query.js.map +1 -0
  58. package/dist/daemon/build-id.js +30 -0
  59. package/dist/daemon/build-id.js.map +1 -0
  60. package/dist/daemon/daemon.js +1116 -767
  61. package/dist/daemon/daemon.js.map +1 -1
  62. package/dist/daemon/instance-lock.js +48 -0
  63. package/dist/daemon/instance-lock.js.map +1 -0
  64. package/dist/daemon/web-supervisor.js +251 -0
  65. package/dist/daemon/web-supervisor.js.map +1 -0
  66. package/dist/drivers/claude/driver.js +1 -0
  67. package/dist/drivers/claude/driver.js.map +1 -1
  68. package/dist/drivers/codex/driver.js +14 -3
  69. package/dist/drivers/codex/driver.js.map +1 -1
  70. package/dist/drivers/codex/host-skills.js +57 -0
  71. package/dist/drivers/codex/host-skills.js.map +1 -0
  72. package/dist/drivers/opencode/driver.js +6 -2
  73. package/dist/drivers/opencode/driver.js.map +1 -1
  74. package/dist/drivers/session-runtime.js +2 -0
  75. package/dist/drivers/session-runtime.js.map +1 -1
  76. package/dist/drivers/terminal.js +15 -0
  77. package/dist/drivers/terminal.js.map +1 -1
  78. package/dist/git/diff.js +29 -4
  79. package/dist/git/diff.js.map +1 -1
  80. package/dist/open/contract.js.map +1 -1
  81. package/dist/open/pty.js +330 -5
  82. package/dist/open/pty.js.map +1 -1
  83. package/dist/plugin/agents/general.md +33 -6
  84. package/dist/plugin/hooks/pane-ticker.ts +102 -0
  85. package/dist/plugin/hooks/register.tsx +33 -8
  86. package/dist/plugin/mods/agents/pane.ts +259 -136
  87. package/dist/plugin/mods/agents/types.ts +19 -3
  88. package/dist/plugin/statusline.sh +22 -34
  89. package/dist/store/database.js +5 -1
  90. package/dist/store/database.js.map +1 -1
  91. package/dist/store/sessions.js +55 -12
  92. package/dist/store/sessions.js.map +1 -1
  93. package/dist/utils/process.js +2 -16
  94. package/dist/utils/process.js.map +1 -1
  95. package/dist/web/brand.js +59 -0
  96. package/dist/web/brand.js.map +1 -0
  97. package/dist/web/child.js +67 -0
  98. package/dist/web/child.js.map +1 -0
  99. package/dist/web/home-page.js +48 -0
  100. package/dist/web/home-page.js.map +1 -0
  101. package/dist/web/review-page.js +12 -5
  102. package/dist/web/review-page.js.map +1 -1
  103. package/dist/web/security.js +170 -0
  104. package/dist/web/security.js.map +1 -0
  105. package/dist/web/server.js +198 -0
  106. package/dist/web/server.js.map +1 -0
  107. package/dist/web/setup-icons.js +37 -0
  108. package/dist/web/setup-icons.js.map +1 -0
  109. package/dist/web/setup-page.js +1795 -0
  110. package/dist/web/setup-page.js.map +1 -0
  111. package/dist/web/setup-routes.js +475 -0
  112. package/dist/web/setup-routes.js.map +1 -0
  113. package/dist/web/usage-page.js +609 -0
  114. package/dist/web/usage-page.js.map +1 -0
  115. package/dist/web/usage-routes.js +88 -0
  116. package/dist/web/usage-routes.js.map +1 -0
  117. package/dist/web/web-token.js +56 -0
  118. package/dist/web/web-token.js.map +1 -0
  119. package/package.json +1 -1
  120. package/dist/plugin/prompts/_partials/commit.md +0 -145
  121. package/dist/plugin/prompts/_partials/pr-writer.md +0 -186
@@ -0,0 +1,1795 @@
1
+ import { REASONING_EFFORTS } from "../core/driver.js";
2
+ import { ROLES } from "../core/roles.js";
3
+ import { AGENT_IDS } from "../core/session.js";
4
+ import { CATALOG_DISCOVERY_TIMEOUT_MS } from "../core/models.js";
5
+ import { ORCHESTRATOR_PRESETS } from "../config/orchestrator-mode.js";
6
+ import { BRAND_CSS, LOGO_FAVICON_HREF, renderTopBar } from "./brand.js";
7
+ import { renderSetupSprite } from "./setup-icons.js";
8
+ export const SETUP_SESSION_EXPIRED_MESSAGE = "This CodeDeck session has expired. Reload the page. If it still fails, restart the command and open its new URL.";
9
+ export function buildSetupSelection(values, currentBindings, options) {
10
+ const agents = {};
11
+ for (const role of options.roles) {
12
+ const selected = values.roles[role];
13
+ if (!selected || selected.skip)
14
+ continue;
15
+ const separator = selected.binding.indexOf(":");
16
+ const harness = separator < 0 ? "" : selected.binding.slice(0, separator).trim();
17
+ const model = separator < 0 ? "" : selected.binding.slice(separator + 1).trim();
18
+ if (!harness || !model)
19
+ throw new Error(`Enter a harness:model value for ${role}.`);
20
+ const previous = currentBindings[role];
21
+ const sameBinding = previous?.harness === harness && previous.model === model;
22
+ let effort;
23
+ const effortValue = selected.effort.trim();
24
+ if (harness === "opencode") {
25
+ if (sameBinding)
26
+ effort = previous?.effort;
27
+ }
28
+ else if (effortValue === "" || effortValue === "keep") {
29
+ if (sameBinding)
30
+ effort = previous?.effort;
31
+ }
32
+ else if (options.efforts.includes(effortValue)) {
33
+ effort = effortValue;
34
+ }
35
+ else {
36
+ throw new Error(`Choose a supported effort value for ${role}.`);
37
+ }
38
+ agents[role] = {
39
+ harness: harness,
40
+ model,
41
+ ...(effort === undefined ? {} : { effort: effort }),
42
+ };
43
+ }
44
+ let orchestrator;
45
+ if (values.orchestrator !== "skip") {
46
+ const preset = options.presets[values.orchestrator];
47
+ if (preset) {
48
+ orchestrator = { ...preset };
49
+ }
50
+ else if (values.orchestrator === "custom") {
51
+ if (!["none", "read", "free"].includes(values.investigate) ||
52
+ !["none", "trivial", "small"].includes(values.selfWork) ||
53
+ !["dispatch", "read", "edit"].includes(values.tools)) {
54
+ throw new Error("Choose valid custom orchestrator parameters.");
55
+ }
56
+ const parallelismValue = values.parallelism.trim();
57
+ const parallelism = parallelismValue === "" ? undefined : Number(parallelismValue);
58
+ if (parallelism !== undefined && (!Number.isFinite(parallelism) || parallelism <= 0)) {
59
+ throw new Error("parallelism must be a positive finite number.");
60
+ }
61
+ orchestrator = {
62
+ investigate: values.investigate,
63
+ selfWork: values.selfWork,
64
+ tools: values.tools,
65
+ ...(parallelism === undefined ? {} : { parallelism }),
66
+ };
67
+ }
68
+ else {
69
+ throw new Error("Choose an orchestrator preset or custom mode.");
70
+ }
71
+ }
72
+ let sandbox;
73
+ if (values.sandbox === "workspace-write" || values.sandbox === "danger-full-access") {
74
+ sandbox = values.sandbox;
75
+ }
76
+ else if (values.sandbox !== "skip") {
77
+ throw new Error("Choose a supported sandbox value.");
78
+ }
79
+ let autocompact;
80
+ if (values.autocompact === "on")
81
+ autocompact = { enabled: true };
82
+ else if (values.autocompact === "off")
83
+ autocompact = { enabled: false };
84
+ else if (values.autocompact !== "skip")
85
+ throw new Error("Choose on or off for autocompact.");
86
+ return {
87
+ agents,
88
+ ...(orchestrator === undefined ? {} : { orchestrator }),
89
+ ...(sandbox === undefined ? {} : { sandbox }),
90
+ ...(autocompact === undefined ? {} : { autocompact }),
91
+ };
92
+ }
93
+ export const SETUP_POLICY_AXES = [
94
+ {
95
+ key: "investigate",
96
+ label: "Reads the code",
97
+ options: [
98
+ { value: "none", label: "Never", icon: "eye-off", summary: "never reads the code" },
99
+ { value: "read", label: "Read only", icon: "eye", summary: "reads code to plan" },
100
+ { value: "free", label: "Freely", icon: "telescope", summary: "explores the code freely" },
101
+ ],
102
+ },
103
+ {
104
+ key: "selfWork",
105
+ label: "Does work itself",
106
+ options: [
107
+ { value: "none", label: "Never", icon: "hand", summary: "hands off every task" },
108
+ { value: "trivial", label: "Trivial fixes", icon: "wrench", summary: "fixes trivial things itself" },
109
+ { value: "small", label: "Small tasks", icon: "hammer", summary: "takes small tasks itself" },
110
+ ],
111
+ },
112
+ {
113
+ key: "tools",
114
+ label: "Tools",
115
+ options: [
116
+ { value: "dispatch", label: "Dispatch only", icon: "split", summary: "only has dispatch tools" },
117
+ { value: "read", label: "Read", icon: "book-open", summary: "has read-only tools" },
118
+ { value: "edit", label: "Edit", icon: "pencil", summary: "can edit files" },
119
+ ],
120
+ },
121
+ ];
122
+ // The orchestrator leads the roster because it is the role the others work for.
123
+ export const SETUP_ROLE_ORDER = [
124
+ "orchestrator",
125
+ ...ROLES.filter((role) => role !== "orchestrator"),
126
+ ];
127
+ const ROLE_COPY = {
128
+ orchestrator: "Coordinates workers and tracks their state.",
129
+ general: "Does the work directly in the workspace.",
130
+ reviewer: "Inspects changes with evidence. Edits nothing.",
131
+ auditor: "Fans out over large scopes, then proves findings.",
132
+ };
133
+ export function createSetupPageController(options) {
134
+ const state = { refreshing: false, loading: true };
135
+ const roles = options.roles;
136
+ const efforts = options.efforts ?? [];
137
+ const harnesses = options.harnesses ?? [];
138
+ const presets = options.presets ?? {};
139
+ const presetNames = Object.keys(presets);
140
+ const columns = [...presetNames, "custom"];
141
+ const axes = options.axes ?? [];
142
+ const ui = {
143
+ drawer: false,
144
+ pickerRole: undefined,
145
+ pickerQuery: "",
146
+ pickerIndex: 0,
147
+ confirmOff: {},
148
+ reviewOk: false,
149
+ bound: false,
150
+ rendered: false,
151
+ catalogDone: false,
152
+ };
153
+ let draft = { agents: {}, custom: false, parallelism: "" };
154
+ let refreshInFlight;
155
+ let actionInFlight = false;
156
+ let actionInFlightLabel = "";
157
+ let toastTimer;
158
+ const htmlCache = new Map();
159
+ const swapFlip = new Map();
160
+ const element = (id) => options.document?.getElementById(id) ?? null;
161
+ function escapeHtml(value) {
162
+ return String(value).replace(/[&<>"']/g, (character) => ({
163
+ "&": "&amp;",
164
+ "<": "&lt;",
165
+ ">": "&gt;",
166
+ '"': "&quot;",
167
+ "'": "&#39;",
168
+ })[character] ?? character);
169
+ }
170
+ function capitalize(value) {
171
+ return value.charAt(0).toUpperCase() + value.slice(1);
172
+ }
173
+ function icon(name, className = "") {
174
+ return `<svg class="ico${className ? ` ${className}` : ""}" aria-hidden="true" focusable="false"><use href="#i-${name}"></use></svg>`;
175
+ }
176
+ function chip(harness) {
177
+ return `<span class="hchip" data-h="${escapeHtml(harness)}"><span class="hmark"><svg aria-hidden="true" focusable="false"><use href="#hi-${escapeHtml(harness)}"></use></svg></span><span class="hname">${escapeHtml(harness)}</span></span>`;
178
+ }
179
+ function setHtml(id, html) {
180
+ const target = element(id);
181
+ if (!target || htmlCache.get(id) === html)
182
+ return false;
183
+ htmlCache.set(id, html);
184
+ target.innerHTML = html;
185
+ return true;
186
+ }
187
+ function setClass(id, className) {
188
+ const target = element(id);
189
+ if (target && target.className !== className)
190
+ target.className = className;
191
+ }
192
+ function setAttr(id, name, value) {
193
+ element(id)?.setAttribute?.(name, value);
194
+ }
195
+ function setDisabled(id, disabled) {
196
+ const target = element(id);
197
+ if (target && target.disabled !== disabled)
198
+ target.disabled = disabled;
199
+ }
200
+ // Swapping between two identical keyframe names restarts the entrance
201
+ // animation every time the text changes, without touching layout.
202
+ function swapText(id, base, text) {
203
+ const target = element(id);
204
+ if (!target)
205
+ return;
206
+ if (target.textContent === text) {
207
+ if ((target.className ?? "").replace(/ swap-[ab]$/, "") !== base)
208
+ target.className = base;
209
+ return;
210
+ }
211
+ target.textContent = text;
212
+ // The first ready render replaces skeleton text, so it fades in too.
213
+ if (state.loading || !state.target) {
214
+ target.className = base;
215
+ return;
216
+ }
217
+ const flip = !swapFlip.get(id);
218
+ swapFlip.set(id, flip);
219
+ target.className = `${base} ${flip ? "swap-a" : "swap-b"}`;
220
+ }
221
+ function cloneDraft(value) {
222
+ return JSON.parse(JSON.stringify(value));
223
+ }
224
+ function presetFor(policy) {
225
+ if (!policy)
226
+ return undefined;
227
+ return presetNames.find((name) => presets[name]?.investigate === policy.investigate &&
228
+ presets[name]?.selfWork === policy.selfWork &&
229
+ presets[name]?.tools === policy.tools);
230
+ }
231
+ function samePolicy(left, right) {
232
+ if (!left || !right)
233
+ return left === right;
234
+ return left.investigate === right.investigate && left.selfWork === right.selfWork && left.tools === right.tools;
235
+ }
236
+ function baselinePolicy() {
237
+ const preset = presets.balanced ?? presets[presetNames[0] ?? ""];
238
+ return {
239
+ investigate: preset?.investigate ?? axes[0]?.options[0]?.value ?? "none",
240
+ selfWork: preset?.selfWork ?? axes[1]?.options[0]?.value ?? "none",
241
+ tools: preset?.tools ?? axes[2]?.options[0]?.value ?? "dispatch",
242
+ };
243
+ }
244
+ function savedDraft() {
245
+ const target = state.target;
246
+ const agents = {};
247
+ for (const role of roles) {
248
+ const binding = target?.bindings?.[role];
249
+ if (binding) {
250
+ agents[role] = {
251
+ harness: binding.harness,
252
+ model: binding.model,
253
+ effort: target?.efforts?.[role] ?? binding.effort ?? "",
254
+ };
255
+ }
256
+ }
257
+ const mode = target?.orchestrator;
258
+ const policy = mode ? { investigate: mode.investigate, selfWork: mode.selfWork, tools: mode.tools } : undefined;
259
+ return {
260
+ agents,
261
+ ...(policy ? { policy } : {}),
262
+ custom: Boolean(policy) && !presetFor(policy),
263
+ parallelism: mode?.parallelism === undefined ? "" : String(mode.parallelism),
264
+ ...(target?.sandbox === undefined ? {} : { sandbox: target.sandbox }),
265
+ ...(target?.autocompact === undefined ? {} : { autocompact: target.autocompact.enabled !== false }),
266
+ };
267
+ }
268
+ function policyMode(value = draft) {
269
+ if (!value.policy)
270
+ return "none";
271
+ if (value.custom)
272
+ return "custom";
273
+ return presetFor(value.policy) ?? "custom";
274
+ }
275
+ function effortOf(binding) {
276
+ return !binding || binding.harness === "opencode" ? "" : binding.effort;
277
+ }
278
+ function modelChanged(role, saved) {
279
+ const current = draft.agents[role];
280
+ const previous = saved.agents[role];
281
+ return Boolean(current) && (!previous || previous.harness !== current?.harness || previous.model !== current?.model);
282
+ }
283
+ function effortChanged(role, saved) {
284
+ const current = draft.agents[role];
285
+ const previous = saved.agents[role];
286
+ return Boolean(current && previous) && effortOf(current) !== effortOf(previous);
287
+ }
288
+ function roleChanged(role, saved) {
289
+ return modelChanged(role, saved) || effortChanged(role, saved);
290
+ }
291
+ function policyChanged(saved) {
292
+ return Boolean(draft.policy) && !samePolicy(draft.policy, saved.policy);
293
+ }
294
+ function parallelismChanged(saved) {
295
+ return Boolean(draft.policy) && draft.parallelism.trim() !== saved.parallelism;
296
+ }
297
+ function sandboxChanged(saved) {
298
+ return draft.sandbox !== undefined && draft.sandbox !== saved.sandbox;
299
+ }
300
+ function autocompactChanged(saved) {
301
+ return draft.autocompact !== undefined && draft.autocompact !== saved.autocompact;
302
+ }
303
+ function changeNames(saved = savedDraft()) {
304
+ return [
305
+ ...roles.filter((role) => roleChanged(role, saved)).map((role) => capitalize(role)),
306
+ ...(policyChanged(saved) ? ["Orchestrator policy"] : []),
307
+ ...(parallelismChanged(saved) ? ["Parallel workers"] : []),
308
+ ...(sandboxChanged(saved) ? ["Sandbox"] : []),
309
+ ...(autocompactChanged(saved) ? ["Autocompact"] : []),
310
+ ];
311
+ }
312
+ function harnessEntry(harness) {
313
+ return state.catalog?.models?.find((entry) => entry.agent === harness);
314
+ }
315
+ function catalogModels(harness) {
316
+ const seen = new Set();
317
+ return (harnessEntry(harness)?.providers ?? []).flatMap((provider) => provider.models).filter((model) => {
318
+ if (seen.has(model.id))
319
+ return false;
320
+ seen.add(model.id);
321
+ return true;
322
+ });
323
+ }
324
+ function catalogModel(harness, model) {
325
+ return catalogModels(harness).find((candidate) => candidate.id === model || candidate.aliases?.includes(model));
326
+ }
327
+ function supportedEfforts(harness, model) {
328
+ const listed = catalogModel(harness, model)?.reasoningEfforts?.filter((effort) => efforts.includes(effort));
329
+ return listed && listed.length > 0 ? listed : undefined;
330
+ }
331
+ // The catalog name reads better than the id, but the id is what the harness
332
+ // receives, so it stays visible next to the name.
333
+ function modelText(harness, model, titleClass, idClass) {
334
+ const name = catalogModel(harness, model)?.name;
335
+ if (!name || name === model)
336
+ return `<span class="${titleClass} mono">${escapeHtml(model)}</span>`;
337
+ return `<span class="${titleClass}">${escapeHtml(name)}</span><span class="${idClass} mono">${escapeHtml(model)}</span>`;
338
+ }
339
+ function isOffCatalog(harness, model) {
340
+ const entry = harnessEntry(harness);
341
+ return Boolean(entry?.available) && !catalogModel(harness, model);
342
+ }
343
+ function offCatalogRoles(saved = savedDraft()) {
344
+ return roles.filter((role) => {
345
+ const current = draft.agents[role];
346
+ return Boolean(current) && modelChanged(role, saved) && isOffCatalog(current.harness, current.model);
347
+ });
348
+ }
349
+ function readForm() {
350
+ const saved = savedDraft();
351
+ const roleValues = {};
352
+ for (const role of roles) {
353
+ const current = draft.agents[role];
354
+ roleValues[role] = {
355
+ skip: !roleChanged(role, saved),
356
+ binding: current ? `${current.harness}:${current.model}` : "",
357
+ effort: current?.effort || "keep",
358
+ };
359
+ }
360
+ const policy = draft.policy;
361
+ let orchestrator = "skip";
362
+ if (policy && (policyChanged(saved) || parallelismChanged(saved))) {
363
+ const preset = !draft.custom && draft.parallelism.trim() === "" ? presetFor(policy) : undefined;
364
+ orchestrator = preset ?? "custom";
365
+ }
366
+ return {
367
+ roles: roleValues,
368
+ orchestrator,
369
+ investigate: policy?.investigate ?? "none",
370
+ selfWork: policy?.selfWork ?? "none",
371
+ tools: policy?.tools ?? "dispatch",
372
+ parallelism: draft.parallelism,
373
+ sandbox: sandboxChanged(saved) ? draft.sandbox ?? "skip" : "skip",
374
+ autocompact: autocompactChanged(saved) ? (draft.autocompact ? "on" : "off") : "skip",
375
+ };
376
+ }
377
+ function formatAge(ms) {
378
+ const minutes = Math.floor(ms / 60000);
379
+ if (minutes < 1)
380
+ return "just now";
381
+ if (minutes < 60)
382
+ return `${minutes} min ago`;
383
+ const hours = Math.floor(minutes / 60);
384
+ if (hours < 24)
385
+ return `${hours} h ago`;
386
+ return `${Math.floor(hours / 24)} d ago`;
387
+ }
388
+ function refreshErrorText(error) {
389
+ const timeout = error.match(/timed out after (\d+) ?ms/);
390
+ if (timeout)
391
+ return `Refresh timed out after ${Math.round(Number(timeout[1]) / 1000)} s.`;
392
+ return `Refresh failed: ${error.replace(/\.$/, "")}.`;
393
+ }
394
+ function catalogText() {
395
+ if (state.refreshing)
396
+ return "Refreshing catalog";
397
+ const catalog = state.catalog;
398
+ const error = state.discoveryError ? refreshErrorText(state.discoveryError) : "";
399
+ if (!catalog) {
400
+ if (error)
401
+ return `Catalog unavailable. ${error}`;
402
+ return state.loading ? "Loading catalog" : "Catalog not loaded";
403
+ }
404
+ const age = typeof catalog.ageMs === "number" ? formatAge(catalog.ageMs) : "";
405
+ if (error)
406
+ return age ? `${error} Showing models from ${age}.` : error;
407
+ if (catalog.status === "fresh")
408
+ return `Catalog updated ${age}`.trim();
409
+ if (catalog.status === "offline")
410
+ return `Catalog offline, cached ${age}`.trim();
411
+ return "Catalog unavailable";
412
+ }
413
+ function formatPreviewPath(path) {
414
+ const parts = path.split("/").slice(1).map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
415
+ const labels = {
416
+ agents: "agents",
417
+ harness: "harness",
418
+ model: "model",
419
+ effort: "effort",
420
+ orchestrator: "orchestrator",
421
+ investigate: "investigation",
422
+ selfWork: "self work",
423
+ tools: "tools",
424
+ parallelism: "parallel workers",
425
+ defaultSandbox: "sandbox",
426
+ autocompact: "autocompact",
427
+ enabled: "enabled",
428
+ cap: "cap",
429
+ };
430
+ if (parts[0] === "agents" && parts[1]) {
431
+ return [capitalize(parts[1]), ...parts.slice(2).map((part) => labels[part] ?? part)].join(" ");
432
+ }
433
+ return capitalize(parts.map((part) => labels[part] ?? part).join(" ") || "configuration");
434
+ }
435
+ function formatPreviewValue(value) {
436
+ if (value === null || value === undefined)
437
+ return "none";
438
+ if (typeof value === "boolean")
439
+ return value ? "on" : "off";
440
+ if (Array.isArray(value))
441
+ return value.map((item) => formatPreviewValue(item)).join(", ") || "none";
442
+ if (typeof value === "object") {
443
+ return Object.entries(value)
444
+ .map(([key, item]) => `${key}: ${formatPreviewValue(item)}`)
445
+ .join(", ") || "none";
446
+ }
447
+ return String(value);
448
+ }
449
+ function sortKeys(value) {
450
+ if (Array.isArray(value))
451
+ return value.map((item) => sortKeys(item));
452
+ if (typeof value === "object" && value !== null) {
453
+ const record = value;
454
+ return Object.fromEntries(Object.keys(record).sort((a, b) => a.localeCompare(b)).map((key) => [key, sortKeys(record[key])]));
455
+ }
456
+ return value;
457
+ }
458
+ // Only the keys Setup writes, so the diff shows what saving changes and
459
+ // never echoes unrelated or legacy keys from the file.
460
+ function managedLines(source) {
461
+ const managed = {};
462
+ for (const key of ["agents", "orchestrator", "defaultSandbox", "autocompact"]) {
463
+ if (source[key] !== undefined)
464
+ managed[key] = sortKeys(source[key]);
465
+ }
466
+ return JSON.stringify(managed, null, 2).split("\n");
467
+ }
468
+ function diffLines(before, after) {
469
+ const rows = before.length;
470
+ const cols = after.length;
471
+ const lengths = Array.from({ length: rows + 1 }, () => new Array(cols + 1).fill(0));
472
+ for (let i = rows - 1; i >= 0; i--) {
473
+ for (let j = cols - 1; j >= 0; j--) {
474
+ lengths[i][j] = before[i] === after[j]
475
+ ? lengths[i + 1][j + 1] + 1
476
+ : Math.max(lengths[i + 1][j], lengths[i][j + 1]);
477
+ }
478
+ }
479
+ const output = [];
480
+ let i = 0;
481
+ let j = 0;
482
+ while (i < rows && j < cols) {
483
+ if (before[i] === after[j]) {
484
+ output.push({ kind: " ", text: before[i], old: i + 1, next: j + 1 });
485
+ i++;
486
+ j++;
487
+ }
488
+ else if (lengths[i + 1][j] >= lengths[i][j + 1]) {
489
+ output.push({ kind: "-", text: before[i], old: i + 1 });
490
+ i++;
491
+ }
492
+ else {
493
+ output.push({ kind: "+", text: after[j], next: j + 1 });
494
+ j++;
495
+ }
496
+ }
497
+ while (i < rows)
498
+ output.push({ kind: "-", text: before[i], old: ++i });
499
+ while (j < cols)
500
+ output.push({ kind: "+", text: after[j], next: ++j });
501
+ return output;
502
+ }
503
+ function renderDiff(proposal) {
504
+ const target = state.target;
505
+ const before = managedLines({
506
+ agents: target?.bindings,
507
+ orchestrator: target?.orchestrator,
508
+ defaultSandbox: target?.sandbox,
509
+ autocompact: target?.autocompact,
510
+ });
511
+ const lines = diffLines(before, managedLines(proposal));
512
+ const added = lines.filter((line) => line.kind === "+").length;
513
+ const removed = lines.filter((line) => line.kind === "-").length;
514
+ if (added + removed === 0)
515
+ return "";
516
+ const near = lines.map((_, index) => lines.slice(Math.max(0, index - 2), index + 3).some((line) => line.kind !== " "));
517
+ const rows = [];
518
+ let hidden = 0;
519
+ const flush = () => {
520
+ if (hidden > 0)
521
+ rows.push(`<div class="fold">${hidden} unchanged ${hidden === 1 ? "line" : "lines"}</div>`);
522
+ hidden = 0;
523
+ };
524
+ lines.forEach((line, index) => {
525
+ if (line.kind === " " && !near[index]) {
526
+ hidden++;
527
+ return;
528
+ }
529
+ flush();
530
+ const text = escapeHtml(line.text)
531
+ .replace(/^(\s*)(&quot;.*?&quot;)(:)/, '$1<span class="k">$2</span>$3')
532
+ .replace(/(:\s)(&quot;.*?&quot;)/, '$1<span class="s">$2</span>');
533
+ const kind = line.kind === "+" ? " plus" : line.kind === "-" ? " minus" : "";
534
+ rows.push(`<div class="dl${kind}"><span class="no">${line.old ?? ""}</span><span class="no">${line.next ?? ""}</span><span class="mk">${line.kind === " " ? "" : line.kind}</span><span class="tx">${text}</span></div>`);
535
+ });
536
+ flush();
537
+ const path = target?.config?.path ?? "config.json";
538
+ const file = path.split("/").pop() || path;
539
+ return `<section class="dr-section"><h3 class="dr-label"><span>${escapeHtml(file)}</span><span class="counts"><span class="add">+${added}</span><span class="del">-${removed}</span></span></h3><div class="diff">${rows.join("")}</div><p class="dr-note">Only the keys Setup manages are shown. The rest of the file stays as it is.</p></section>`;
540
+ }
541
+ function renderPreview(envelope, fallbackError) {
542
+ const body = typeof envelope === "object" && envelope !== null
543
+ ? envelope
544
+ : {};
545
+ const result = typeof body.resultado === "object" && body.resultado !== null
546
+ ? body.resultado
547
+ : {};
548
+ const resultStatus = typeof result.status === "string" ? result.status : "error";
549
+ const statusLabels = {
550
+ "dry-run": "Not saved yet",
551
+ applied: "Saved",
552
+ unchanged: "Nothing to save",
553
+ aborted: "Cancelled",
554
+ error: "Error",
555
+ };
556
+ const statusClass = ["dry-run", "applied", "unchanged", "aborted", "error"].includes(resultStatus)
557
+ ? resultStatus
558
+ : "error";
559
+ const message = fallbackError
560
+ ?? (typeof result.message === "string"
561
+ ? result.message
562
+ : typeof body.error === "string"
563
+ ? body.error
564
+ : "");
565
+ const displayMessage = resultStatus === "dry-run" && !fallbackError
566
+ ? "Check the changes below, then save."
567
+ : message;
568
+ const changes = Array.isArray(body.mudancas) ? body.mudancas.filter((item) => {
569
+ if (typeof item !== "object" || item === null)
570
+ return false;
571
+ const path = typeof item.path === "string"
572
+ ? item.path
573
+ : "";
574
+ const parts = path.split("/").slice(1).map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
575
+ return !parts.includes("profiles") && !parts.includes("activeProfile");
576
+ }) : [];
577
+ const changeRows = changes.map((change) => {
578
+ const path = typeof change.path === "string" ? change.path : "";
579
+ const beforePresent = change.beforePresent !== false;
580
+ const afterPresent = change.afterPresent !== false;
581
+ const before = beforePresent ? `<span class="before">${escapeHtml(formatPreviewValue(change.before))}</span>` : '<span class="marker">Added</span>';
582
+ const after = afterPresent ? `<span class="after">${escapeHtml(formatPreviewValue(change.after))}</span>` : '<span class="marker">Removed</span>';
583
+ return `<div class="ch"><span class="ch-name">${escapeHtml(formatPreviewPath(path))}</span><span class="ch-vals">${before}<span class="ch-to">to</span>${after}</span></div>`;
584
+ }).join("");
585
+ function check(label, status, messageText, ok) {
586
+ return `<div class="check ${ok ? "ok" : "warn"}">${icon(ok ? "check" : "triangle-alert")}<span><strong>${escapeHtml(label)}</strong> ${escapeHtml(status.replaceAll("-", " "))}${messageText ? `<span class="check-msg">${escapeHtml(messageText)}</span>` : ""}</span></div>`;
587
+ }
588
+ function validationRow(label, value) {
589
+ if (typeof value !== "object" || value === null)
590
+ return "";
591
+ const validation = value;
592
+ const status = typeof validation.status === "string" ? validation.status : "not checked";
593
+ const messageText = typeof validation.message === "string" ? validation.message.trim() : "";
594
+ return check(label, status, messageText, ["ok", "accepted", "fresh", "not-needed"].includes(status));
595
+ }
596
+ const validations = typeof body.validacoes === "object" && body.validacoes !== null
597
+ ? body.validacoes
598
+ : {};
599
+ const bindingRows = Array.isArray(validations.bindings) ? validations.bindings.map((item) => {
600
+ if (typeof item !== "object" || item === null)
601
+ return "";
602
+ const binding = item;
603
+ const role = typeof binding.role === "string" ? binding.role : "binding";
604
+ const status = typeof binding.status === "string" ? binding.status : "not checked";
605
+ const identity = [binding.harness, binding.model].filter((part) => typeof part === "string").join(":");
606
+ const messageText = typeof binding.message === "string" ? binding.message.trim() : "";
607
+ return check(`${capitalize(role)}${identity ? ` ${identity}` : ""}`, status, messageText, status === "accepted");
608
+ }).join("") : "";
609
+ const validationRows = [
610
+ validationRow("Config", validations.config),
611
+ validationRow("Catalog", validations.catalogo),
612
+ bindingRows,
613
+ ].filter(Boolean).join("");
614
+ const proposal = typeof body.proposta === "object" && body.proposta !== null
615
+ ? renderDiff(body.proposta)
616
+ : "";
617
+ const rawResponse = escapeHtml(JSON.stringify(envelope, null, 2) ?? String(envelope));
618
+ const statusIcon = statusClass === "applied" || statusClass === "unchanged"
619
+ ? "check"
620
+ : statusClass === "dry-run" ? "eye" : "triangle-alert";
621
+ return `<section class="dr-section result-status status-${statusClass}" role="status">${icon(statusIcon)}<span><strong>${escapeHtml(statusLabels[resultStatus] ?? "Request failed")}</strong>${displayMessage ? `<span class="result-msg">${escapeHtml(displayMessage)}</span>` : ""}</span></section>`
622
+ + `<section class="dr-section"><h3 class="dr-label">${changes.length} ${changes.length === 1 ? "change" : "changes"}</h3><div class="ch-list">${changeRows || '<p class="empty">No changes</p>'}</div></section>`
623
+ + proposal
624
+ + `<section class="dr-section"><h3 class="dr-label">Checks</h3><div class="checks">${validationRows || '<p class="empty">No validation details</p>'}</div></section>`
625
+ + `<details class="raw-response"><summary>Raw response</summary><pre>${rawResponse}</pre></details>`;
626
+ }
627
+ function renderReview() {
628
+ if (actionInFlight && actionInFlightLabel === "Checking changes") {
629
+ return '<div class="dr-loading" role="status"><span>Checking your changes</span><span class="sk"></span><span class="sk short"></span><span class="sk"></span></div>';
630
+ }
631
+ if (state.envelope !== undefined) {
632
+ if (ui.reviewOk && !state.error) {
633
+ const body = state.envelope;
634
+ return renderPreview({ ...body, resultado: { status: "dry-run" } });
635
+ }
636
+ return renderPreview(state.envelope, state.error);
637
+ }
638
+ if (state.error) {
639
+ return `<section class="dr-section result-status status-error" role="status">${icon("triangle-alert")}<span><strong>Error</strong><span class="result-msg">${escapeHtml(state.error)}</span></span></section>`;
640
+ }
641
+ return "";
642
+ }
643
+ function bindingHtml(role) {
644
+ if (state.loading || !state.target)
645
+ return `<span class="model-sk"><span class="sr-only">Loading</span></span>${icon("chevron-down", "chev")}`;
646
+ const current = draft.agents[role];
647
+ if (!current)
648
+ return `<span class="model-body"><span class="model-empty">Choose a model</span></span>${icon("chevron-down", "chev")}`;
649
+ const off = modelChanged(role, savedDraft()) && isOffCatalog(current.harness, current.model);
650
+ return `<span class="model-body">${chip(current.harness)}<span class="model-text">${modelText(current.harness, current.model, "model-title", "model-id")}</span>${off ? '<span class="tag warn">not in catalog</span>' : ""}</span>${icon("chevron-down", "chev")}`;
651
+ }
652
+ function roleStateHtml(role, saved) {
653
+ if (state.loading || !state.target)
654
+ return "";
655
+ const current = draft.agents[role];
656
+ const previous = saved.agents[role];
657
+ if (!current)
658
+ return '<span class="state-note">Not set</span>';
659
+ if (!roleChanged(role, saved))
660
+ return `<span class="state-note">${icon("check")}Saved</span>`;
661
+ const was = !previous
662
+ ? "not set"
663
+ : modelChanged(role, saved)
664
+ ? catalogModel(previous.harness, previous.model)?.name || previous.model
665
+ : `${previous.effort || "default"} effort`;
666
+ return `<span class="was">was <span class="was-value">${escapeHtml(was)}</span></span><button type="button" class="link-btn" data-act="revert" data-role="${role}">${icon("undo-2")}Undo</button>`;
667
+ }
668
+ function pickerEntries() {
669
+ const role = ui.pickerRole;
670
+ const query = ui.pickerQuery.trim().toLowerCase();
671
+ const current = role ? draft.agents[role] : undefined;
672
+ const saved = role ? savedDraft().agents[role] : undefined;
673
+ const groups = [];
674
+ for (const harness of harnesses) {
675
+ const entry = harnessEntry(harness);
676
+ if (!entry || !entry.available) {
677
+ if (query && !harness.includes(query))
678
+ continue;
679
+ const note = !state.catalog
680
+ ? "Catalog not loaded"
681
+ : !entry
682
+ ? "Not in the catalog"
683
+ : entry.error ? `Unavailable: ${entry.error}` : "Unavailable. Run codedeck doctor.";
684
+ groups.push({ harness, note, off: true, items: [] });
685
+ continue;
686
+ }
687
+ const models = catalogModels(harness);
688
+ const matching = models.filter((model) => !query || [`${harness}:${model.id}`, model.name ?? "", ...(model.aliases ?? [])]
689
+ .some((value) => value.toLowerCase().includes(query)));
690
+ if (query && matching.length === 0)
691
+ continue;
692
+ groups.push({
693
+ harness,
694
+ note: models.length === 0
695
+ ? "No models listed"
696
+ : harness === "opencode"
697
+ ? `${models.length} models, sets its own effort`
698
+ : `${models.length} ${models.length === 1 ? "model" : "models"}`,
699
+ off: false,
700
+ items: matching.map((model) => ({
701
+ harness,
702
+ model: model.id,
703
+ saved: saved?.harness === harness && saved.model === model.id,
704
+ selected: current?.harness === harness && current.model === model.id,
705
+ custom: false,
706
+ })),
707
+ });
708
+ }
709
+ const typed = ui.pickerQuery.trim().match(/^([a-z]+):(.+)$/);
710
+ if (typed && harnesses.includes(typed[1]) && !catalogModel(typed[1], typed[2].trim())) {
711
+ groups.push({
712
+ harness: typed[1],
713
+ note: "",
714
+ off: false,
715
+ items: [{ harness: typed[1], model: typed[2].trim(), saved: false, selected: false, custom: true }],
716
+ });
717
+ }
718
+ return { groups, items: groups.flatMap((group) => group.items) };
719
+ }
720
+ function pickerHtml() {
721
+ const { groups, items } = pickerEntries();
722
+ if (items.length === 0 && groups.every((group) => group.off)) {
723
+ const offGroups = groups.map((group) => `<div class="pk-group"><div class="pk-head off">${chip(group.harness)}<span class="pk-note">${escapeHtml(group.note)}</span></div></div>`).join("");
724
+ return `${offGroups}<p class="pk-empty">No model matches. Type harness:model to use one outside the catalog.</p>`;
725
+ }
726
+ let index = 0;
727
+ return groups.map((group) => {
728
+ if (group.items[0]?.custom) {
729
+ const item = group.items[0];
730
+ const id = index++;
731
+ return `<div class="pk-group"><button type="button" id="picker-opt-${id}" class="pk-item pk-custom${id === ui.pickerIndex ? " kbd" : ""}" role="option" aria-selected="false" data-act="pick" data-harness="${escapeHtml(item.harness)}" data-model="${escapeHtml(item.model)}"><span>Use <span class="mono">${escapeHtml(`${item.harness}:${item.model}`)}</span></span><span class="tag warn">not in catalog</span></button></div>`;
732
+ }
733
+ const head = `<div class="pk-head${group.off ? " off" : ""}">${chip(group.harness)}<span class="pk-note">${escapeHtml(group.note)}</span></div>`;
734
+ const options = group.items.map((item) => {
735
+ const id = index++;
736
+ return `<button type="button" id="picker-opt-${id}" class="pk-item${item.selected ? " sel" : ""}${id === ui.pickerIndex ? " kbd" : ""}" role="option" aria-selected="${item.selected}" data-act="pick" data-harness="${escapeHtml(item.harness)}" data-model="${escapeHtml(item.model)}">${modelText(item.harness, item.model, "pk-title", "pk-id")}${item.saved ? '<span class="tag">saved</span>' : ""}${item.selected ? icon("check", "pk-check") : ""}</button>`;
737
+ }).join("");
738
+ return `<div class="pk-group">${head}${options}</div>`;
739
+ }).join("");
740
+ }
741
+ function policyCaption(mode) {
742
+ if (mode === "none") {
743
+ return `Not set. The orchestrator runs as ${capitalize(presetNames[0] ?? "dispatcher")} until you choose.`;
744
+ }
745
+ const policy = draft.policy ?? baselinePolicy();
746
+ const parts = axes.map((axis) => axis.options.find((option) => option.value === policy[axis.key])?.summary ?? "");
747
+ const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}` : parts.join("");
748
+ return `${capitalize(mode)}: ${list}.`;
749
+ }
750
+ function fingerprintPoints(policy) {
751
+ return axes.map((axis, row) => {
752
+ const level = Math.max(0, axis.options.findIndex((option) => option.value === policy[axis.key]));
753
+ return [3 + level * 6, 3 + row * 6];
754
+ });
755
+ }
756
+ function update() {
757
+ const saved = savedDraft();
758
+ const ready = !state.loading && Boolean(state.target);
759
+ const busy = actionInFlight || state.refreshing;
760
+ const settled = (element("setup-root")?.className ?? "").includes("settled");
761
+ setClass("setup-root", `setup-root${ready ? " ready" : ""}${state.loading ? " loading" : ""}${settled ? " settled" : ""}`);
762
+ const configPath = element("setup-config-path");
763
+ if (configPath && state.target?.config?.path)
764
+ configPath.textContent = state.target.config.path;
765
+ swapText("catalog-status", `catalog-text${state.discoveryError ? " warn" : ""}`, catalogText());
766
+ setClass("setup-refresh", `icon-btn${state.refreshing ? " spinning" : ""}`);
767
+ setClass("setup-catalog", `catalog${state.refreshing ? " refreshing" : ui.catalogDone ? " done" : ""}`);
768
+ setDisabled("setup-refresh", state.refreshing);
769
+ const loadError = !state.target && !state.loading ? state.error ?? "Could not load setup state." : "";
770
+ const errorBanner = element("setup-error");
771
+ if (errorBanner) {
772
+ errorBanner.hidden = !loadError;
773
+ errorBanner.textContent = loadError;
774
+ }
775
+ for (const role of roles) {
776
+ const current = draft.agents[role];
777
+ const changed = ready && roleChanged(role, saved);
778
+ setClass(`role-row-${role}`, `r-row${changed ? " changed" : ""}${ready && !current ? " unbound" : ""}`);
779
+ setHtml(`binding-${role}`, bindingHtml(role));
780
+ setDisabled(`binding-${role}`, !ready);
781
+ setAttr(`binding-${role}`, "aria-expanded", String(ui.pickerRole === role));
782
+ const noEffort = current?.harness === "opencode";
783
+ const supported = current ? supportedEfforts(current.harness, current.model) : undefined;
784
+ const level = current && !noEffort ? efforts.indexOf(current.effort) + 1 : 0;
785
+ const effortMark = ready && (effortChanged(role, saved) || (modelChanged(role, saved) && effortOf(current) !== effortOf(saved.agents[role])));
786
+ setClass(`effort-${role}`, `meter lvl-${level}${effortMark ? " changed" : ""}${noEffort ? " none" : ""}${ready && !current ? " unbound" : ""}`);
787
+ for (const effort of efforts) {
788
+ setDisabled(`effort-${role}-${effort}`, !ready || !current || noEffort || Boolean(supported && !supported.includes(effort)));
789
+ setAttr(`effort-${role}-${effort}`, "aria-checked", String(current?.effort === effort));
790
+ }
791
+ swapText(`effort-label-${role}`, "meter-label", !ready || !current || noEffort ? "" : current.effort || "default");
792
+ swapText(`effort-note-${role}`, "meter-note", !ready ? "" : !current ? "No model yet" : noEffort ? "opencode sets its own effort" : "");
793
+ setHtml(`role-state-${role}`, roleStateHtml(role, saved));
794
+ }
795
+ const mode = policyMode();
796
+ const column = mode === "none" ? "none" : String(Math.max(0, columns.indexOf(mode)));
797
+ setClass("policy-section", `sec${ready && (policyChanged(saved) || parallelismChanged(saved)) ? " changed" : ""}`);
798
+ setClass("policy-table", `cmp col-${column}`);
799
+ for (const name of columns) {
800
+ setAttr(`policy-col-${name}`, "aria-pressed", String(mode === name));
801
+ setDisabled(`policy-col-${name}`, !ready);
802
+ }
803
+ const customPolicy = draft.policy ?? baselinePolicy();
804
+ for (const axis of axes) {
805
+ const index = Math.max(0, axis.options.findIndex((option) => option.value === customPolicy[axis.key]));
806
+ setClass(`policy-pick-${axis.key}`, `cmp-picks at-${index}`);
807
+ for (const option of axis.options) {
808
+ setAttr(`policy-pick-${axis.key}-${option.value}`, "aria-checked", String(option.value === customPolicy[axis.key]));
809
+ setDisabled(`policy-pick-${axis.key}-${option.value}`, !ready);
810
+ }
811
+ swapText(`policy-custom-label-${axis.key}`, "cmp-custom-label", axis.options[index]?.label ?? "");
812
+ }
813
+ const points = fingerprintPoints(customPolicy);
814
+ setAttr("policy-fp-line", "d", points.map(([x, y], index) => `${index === 0 ? "M" : "L"}${x} ${y}`).join(" "));
815
+ points.forEach(([x], index) => setAttr(`policy-fp-dot-${index}`, "cx", String(x)));
816
+ swapText("policy-caption", "policy-caption", ready ? policyCaption(mode) : "Loading policy");
817
+ setDisabled("orchestrator-parallelism", !ready || !draft.policy);
818
+ setAttr("orchestrator-parallelism", "placeholder", draft.policy ? "No limit" : "Not set");
819
+ const sandboxAt = draft.sandbox === "workspace-write" ? "0" : draft.sandbox === "danger-full-access" ? "1" : "none";
820
+ setClass("sandbox-row", `rt-row${ready && sandboxChanged(saved) ? " changed" : ""}`);
821
+ setClass("sandbox-control", `seg-ctl at-${sandboxAt}`);
822
+ for (const value of ["workspace-write", "danger-full-access"]) {
823
+ setAttr(`sandbox-${value}`, "aria-checked", String(draft.sandbox === value));
824
+ setDisabled(`sandbox-${value}`, !ready);
825
+ }
826
+ const sandboxCopy = !ready
827
+ ? ""
828
+ : draft.sandbox === "danger-full-access"
829
+ ? "No sandbox for Codex workers. They can reach the network and anything your user can."
830
+ : draft.sandbox === "workspace-write"
831
+ ? "Codex workers write only inside their workspace, with no network."
832
+ : "Not set. Codex runs with workspace write.";
833
+ swapText("sandbox-copy", `rt-copy${draft.sandbox === "danger-full-access" ? " warn" : ""}`, sandboxCopy);
834
+ setClass("autocompact-row", `rt-row${ready && autocompactChanged(saved) ? " changed" : ""}`);
835
+ setClass("setup-autocompact", `switch${draft.autocompact === undefined ? " unset" : ""}`);
836
+ setAttr("setup-autocompact", "aria-checked", String(draft.autocompact === true));
837
+ setDisabled("setup-autocompact", !ready);
838
+ const autocompactCopy = !ready
839
+ ? ""
840
+ : draft.autocompact === true
841
+ ? "Claude and OpenCode sessions compact their context before it fills up."
842
+ : draft.autocompact === false
843
+ ? "Off. CodeDeck leaves context compaction to each harness."
844
+ : "Not set, so it stays off. CodeDeck leaves compaction to each harness.";
845
+ swapText("autocompact-copy", "rt-copy", autocompactCopy);
846
+ const names = ready ? changeNames(saved) : [];
847
+ const barError = state.error && !ui.drawer && state.target ? state.error : "";
848
+ setClass("setup-bar", `savebar${names.length > 0 ? " dirty" : ""}${barError ? " has-error" : ""}`);
849
+ const status = barError
850
+ || (state.loading
851
+ ? "Loading setup"
852
+ : !state.target
853
+ ? "Setup not loaded"
854
+ : actionInFlight
855
+ ? actionInFlightLabel
856
+ : names.length === 0
857
+ ? "All changes saved"
858
+ : `${names.length} unsaved ${names.length === 1 ? "change" : "changes"}`);
859
+ swapText("setup-action-status", "sb-status", status);
860
+ swapText("setup-change-list", "sb-list", barError ? "" : names.join(", "));
861
+ setDisabled("setup-discard", names.length === 0 || busy || !ready);
862
+ setDisabled("setup-review", names.length === 0 || busy || !ready);
863
+ setClass("setup-scrim", `scrim${ui.drawer ? " open" : ""}`);
864
+ setClass("setup-drawer", `drawer${ui.drawer ? " open" : ""}`);
865
+ setAttr("setup-drawer", "aria-hidden", String(!ui.drawer));
866
+ const drawerPath = element("setup-drawer-path");
867
+ if (drawerPath && state.target?.config?.path)
868
+ drawerPath.textContent = state.target.config.path;
869
+ setHtml("setup-result", renderReview());
870
+ const offRoles = offCatalogRoles(saved);
871
+ const confirmHtml = offRoles.map((role) => {
872
+ const current = draft.agents[role];
873
+ return `<label class="confirm"><input type="checkbox" data-act="confirm-off" data-role="${role}"><span>Save <span class="mono">${escapeHtml(current.model)}</span> for ${escapeHtml(role)} even though the ${escapeHtml(current.harness)} catalog does not list it.</span></label>`;
874
+ }).join("");
875
+ if (setHtml("setup-confirmations", confirmHtml))
876
+ ui.confirmOff = {};
877
+ const pending = offRoles.filter((role) => !ui.confirmOff[role]);
878
+ setDisabled("setup-apply", actionInFlight || !ui.reviewOk || pending.length > 0);
879
+ setClass("setup-picker", `picker${ui.pickerRole ? " open" : ""}`);
880
+ setAttr("setup-picker", "aria-hidden", String(!ui.pickerRole));
881
+ if (ui.pickerRole) {
882
+ setHtml("setup-picker-list", pickerHtml());
883
+ setAttr("setup-picker-search", "aria-activedescendant", `picker-opt-${ui.pickerIndex}`);
884
+ }
885
+ if (ready)
886
+ ui.rendered = true;
887
+ options.onChange?.(state);
888
+ }
889
+ function placePicker() {
890
+ const role = ui.pickerRole;
891
+ const picker = element("setup-picker");
892
+ const rect = role ? element(`binding-${role}`)?.getBoundingClientRect?.() : undefined;
893
+ if (!picker?.style || !rect)
894
+ return;
895
+ const view = globalThis;
896
+ const width = Math.min(Math.max(rect.width, 380), (view.innerWidth ?? 1024) - 32);
897
+ const left = Math.min(Math.max(16, rect.left), (view.innerWidth ?? 1024) - width - 16);
898
+ const below = (view.innerHeight ?? 768) - rect.bottom - 22;
899
+ const above = rect.top - 22;
900
+ const up = below < 260 && above > below;
901
+ const height = Math.min(420, up ? above : below);
902
+ picker.style.left = `${left}px`;
903
+ picker.style.width = `${width}px`;
904
+ picker.style.maxHeight = `${Math.max(160, height)}px`;
905
+ picker.style.top = up ? `${rect.top - 6 - Math.max(160, height)}px` : `${rect.bottom + 6}px`;
906
+ picker.style.transformOrigin = up ? "bottom left" : "top left";
907
+ }
908
+ function openPicker(role) {
909
+ if (!state.target || state.loading)
910
+ return;
911
+ ui.pickerRole = role;
912
+ ui.pickerQuery = "";
913
+ const search = element("setup-picker-search");
914
+ if (search)
915
+ search.value = "";
916
+ const selected = pickerEntries().items.findIndex((item) => item.selected);
917
+ ui.pickerIndex = Math.max(0, selected);
918
+ placePicker();
919
+ update();
920
+ search?.focus?.();
921
+ element(`picker-opt-${ui.pickerIndex}`)?.scrollIntoView?.({ block: "nearest" });
922
+ }
923
+ function closePicker(restoreFocus = true) {
924
+ const role = ui.pickerRole;
925
+ if (!role)
926
+ return;
927
+ ui.pickerRole = undefined;
928
+ update();
929
+ if (restoreFocus)
930
+ element(`binding-${role}`)?.focus?.();
931
+ }
932
+ function setPickerQuery(value) {
933
+ ui.pickerQuery = value;
934
+ ui.pickerIndex = 0;
935
+ update();
936
+ const list = element("setup-picker-list");
937
+ if (list)
938
+ list.scrollTop = 0;
939
+ }
940
+ function movePicker(step) {
941
+ const count = pickerEntries().items.length;
942
+ if (count === 0)
943
+ return;
944
+ ui.pickerIndex = (ui.pickerIndex + step + count) % count;
945
+ update();
946
+ element(`picker-opt-${ui.pickerIndex}`)?.scrollIntoView?.({ block: "nearest" });
947
+ }
948
+ function selectBinding(role, harness, model) {
949
+ const previous = draft.agents[role];
950
+ const saved = savedDraft().agents[role];
951
+ let effort = previous?.effort ?? "";
952
+ if (saved && saved.harness === harness && saved.model === model) {
953
+ effort = saved.effort;
954
+ }
955
+ else {
956
+ const supported = supportedEfforts(harness, model);
957
+ if (effort && supported && !supported.includes(effort)) {
958
+ const wanted = efforts.indexOf(effort);
959
+ effort = [...supported].reverse().find((level) => efforts.indexOf(level) <= wanted) ?? supported[0] ?? "";
960
+ }
961
+ }
962
+ draft.agents[role] = { harness, model, effort };
963
+ delete ui.confirmOff[role];
964
+ if (ui.pickerRole === role)
965
+ closePicker(true);
966
+ else
967
+ update();
968
+ }
969
+ function handleKey(event) {
970
+ const key = event?.key;
971
+ if (ui.pickerRole) {
972
+ if (key === "ArrowDown" || key === "ArrowUp") {
973
+ event?.preventDefault?.();
974
+ movePicker(key === "ArrowDown" ? 1 : -1);
975
+ }
976
+ else if (key === "Enter") {
977
+ event?.preventDefault?.();
978
+ const item = pickerEntries().items[ui.pickerIndex];
979
+ if (item)
980
+ selectBinding(ui.pickerRole, item.harness, item.model);
981
+ }
982
+ else if (key === "Escape") {
983
+ event?.preventDefault?.();
984
+ closePicker(true);
985
+ }
986
+ return;
987
+ }
988
+ if (key === "Escape" && ui.drawer)
989
+ closeReview();
990
+ }
991
+ function setEffort(role, effort) {
992
+ const current = draft.agents[role];
993
+ if (!current || !efforts.includes(effort))
994
+ return;
995
+ current.effort = effort;
996
+ update();
997
+ }
998
+ function revertRole(role) {
999
+ const saved = savedDraft().agents[role];
1000
+ if (saved)
1001
+ draft.agents[role] = saved;
1002
+ else
1003
+ delete draft.agents[role];
1004
+ delete ui.confirmOff[role];
1005
+ update();
1006
+ }
1007
+ function choosePreset(name) {
1008
+ const preset = presets[name];
1009
+ if (!preset)
1010
+ return;
1011
+ draft.policy = { investigate: preset.investigate, selfWork: preset.selfWork, tools: preset.tools };
1012
+ draft.custom = false;
1013
+ update();
1014
+ }
1015
+ function chooseCustom() {
1016
+ draft.policy = draft.policy ?? baselinePolicy();
1017
+ draft.custom = true;
1018
+ update();
1019
+ }
1020
+ function setPolicyValue(key, value) {
1021
+ const axis = axes.find((candidate) => candidate.key === key);
1022
+ if (!axis?.options.some((option) => option.value === value))
1023
+ return;
1024
+ draft.policy = { ...(draft.policy ?? baselinePolicy()), [key]: value };
1025
+ draft.custom = true;
1026
+ update();
1027
+ }
1028
+ function syncParallelism() {
1029
+ const input = element("orchestrator-parallelism");
1030
+ if (input)
1031
+ input.value = draft.parallelism;
1032
+ }
1033
+ function setParallelism(value) {
1034
+ draft.parallelism = value;
1035
+ if (state.error)
1036
+ state.error = undefined;
1037
+ update();
1038
+ }
1039
+ function setSandbox(value) {
1040
+ if (value !== "workspace-write" && value !== "danger-full-access")
1041
+ return;
1042
+ draft.sandbox = value;
1043
+ update();
1044
+ }
1045
+ function setAutocompact(value) {
1046
+ draft.autocompact = value ?? !draft.autocompact;
1047
+ update();
1048
+ }
1049
+ function resetDraft() {
1050
+ draft = cloneDraft(savedDraft());
1051
+ ui.confirmOff = {};
1052
+ syncParallelism();
1053
+ }
1054
+ function discard() {
1055
+ resetDraft();
1056
+ state.error = undefined;
1057
+ update();
1058
+ }
1059
+ function confirmOffCatalog(role, checked) {
1060
+ ui.confirmOff[role] = checked;
1061
+ update();
1062
+ }
1063
+ async function openReview() {
1064
+ if (changeNames().length === 0 || actionInFlight)
1065
+ return;
1066
+ try {
1067
+ buildSelection();
1068
+ }
1069
+ catch (error) {
1070
+ state.error = error instanceof Error ? error.message : String(error);
1071
+ update();
1072
+ return;
1073
+ }
1074
+ closePicker(false);
1075
+ ui.drawer = true;
1076
+ ui.reviewOk = false;
1077
+ state.error = undefined;
1078
+ state.envelope = undefined;
1079
+ update();
1080
+ element("setup-drawer-close")?.focus?.();
1081
+ return await dryRun();
1082
+ }
1083
+ function closeReview() {
1084
+ if (!ui.drawer)
1085
+ return;
1086
+ ui.drawer = false;
1087
+ if (state.error && state.envelope !== undefined)
1088
+ state.error = undefined;
1089
+ update();
1090
+ element("setup-review")?.focus?.();
1091
+ }
1092
+ function showToast(text) {
1093
+ const toast = element("setup-toast");
1094
+ if (!toast)
1095
+ return;
1096
+ toast.innerHTML = `${icon("check")}<span>${escapeHtml(text)}</span>`;
1097
+ toast.className = "toast show";
1098
+ clearTimeout(toastTimer);
1099
+ toastTimer = setTimeout(() => { toast.className = "toast"; }, 2800);
1100
+ }
1101
+ function onClick(event) {
1102
+ const target = event?.target;
1103
+ const actor = target?.closest?.("[data-act]") ?? null;
1104
+ const act = actor?.getAttribute("data-act");
1105
+ if (ui.pickerRole && !target?.closest?.("#setup-picker") && act !== "open-picker")
1106
+ closePicker(false);
1107
+ if (!actor || !act)
1108
+ return;
1109
+ const role = actor.getAttribute("data-role");
1110
+ const value = actor.getAttribute("data-value") ?? "";
1111
+ if (act === "open-picker" && role) {
1112
+ if (ui.pickerRole === role)
1113
+ closePicker(true);
1114
+ else
1115
+ openPicker(role);
1116
+ }
1117
+ else if (act === "pick" && ui.pickerRole) {
1118
+ selectBinding(ui.pickerRole, actor.getAttribute("data-harness") ?? "", actor.getAttribute("data-model") ?? "");
1119
+ }
1120
+ else if (act === "effort" && role)
1121
+ setEffort(role, value);
1122
+ else if (act === "revert" && role)
1123
+ revertRole(role);
1124
+ else if (act === "preset")
1125
+ choosePreset(value);
1126
+ else if (act === "custom")
1127
+ chooseCustom();
1128
+ else if (act === "policy-value")
1129
+ setPolicyValue(actor.getAttribute("data-key") ?? "", value);
1130
+ else if (act === "sandbox")
1131
+ setSandbox(value);
1132
+ else if (act === "autocompact")
1133
+ setAutocompact();
1134
+ else if (act === "discard")
1135
+ discard();
1136
+ else if (act === "review")
1137
+ void openReview();
1138
+ else if (act === "close-review")
1139
+ closeReview();
1140
+ else if (act === "apply")
1141
+ void apply();
1142
+ else if (act === "refresh")
1143
+ void refreshCatalog();
1144
+ else if (act === "confirm-off" && role)
1145
+ confirmOffCatalog(role, Boolean(actor.checked));
1146
+ }
1147
+ function bindEvents() {
1148
+ if (ui.bound)
1149
+ return;
1150
+ ui.bound = true;
1151
+ const doc = options.document;
1152
+ doc?.addEventListener?.("click", onClick);
1153
+ doc?.addEventListener?.("keydown", handleKey);
1154
+ doc?.addEventListener?.("scroll", () => { if (ui.pickerRole)
1155
+ placePicker(); }, true);
1156
+ globalThis
1157
+ .addEventListener?.("resize", () => { if (ui.pickerRole)
1158
+ placePicker(); });
1159
+ element("setup-picker-search")?.addEventListener?.("input", () => setPickerQuery(element("setup-picker-search")?.value ?? ""));
1160
+ element("orchestrator-parallelism")?.addEventListener?.("input", () => setParallelism(element("orchestrator-parallelism")?.value ?? ""));
1161
+ }
1162
+ async function loadJson(path, init) {
1163
+ const response = await options.fetcher(path, init);
1164
+ const payload = await response.json();
1165
+ return {
1166
+ response,
1167
+ payload: typeof payload === "object" && payload !== null
1168
+ ? payload
1169
+ : {},
1170
+ };
1171
+ }
1172
+ async function loadState() {
1173
+ const { response, payload } = await loadJson("/api/setup/state");
1174
+ if (!response.ok) {
1175
+ state.error = typeof payload.error === "string" ? payload.error : "Could not load setup state.";
1176
+ return false;
1177
+ }
1178
+ state.target = payload;
1179
+ resetDraft();
1180
+ return true;
1181
+ }
1182
+ async function start() {
1183
+ bindEvents();
1184
+ update();
1185
+ try {
1186
+ if (await loadState()) {
1187
+ const catalog = await loadJson("/api/setup/catalog");
1188
+ if (catalog.response.ok) {
1189
+ state.catalog = catalog.payload;
1190
+ state.discoveryError = typeof catalog.payload.discoveryError === "string" ? catalog.payload.discoveryError : undefined;
1191
+ }
1192
+ else {
1193
+ state.discoveryError = typeof catalog.payload.error === "string" ? catalog.payload.error : "Could not load model catalog.";
1194
+ }
1195
+ }
1196
+ }
1197
+ catch (error) {
1198
+ state.error = error instanceof Error ? error.message : String(error);
1199
+ }
1200
+ state.loading = false;
1201
+ update();
1202
+ // Class changes made by the first render should land without sliding.
1203
+ setTimeout(() => setClass("setup-root", `${element("setup-root")?.className ?? "setup-root"} settled`), 60);
1204
+ return state;
1205
+ }
1206
+ function refreshCatalog() {
1207
+ if (refreshInFlight)
1208
+ return refreshInFlight;
1209
+ state.error = undefined;
1210
+ state.discoveryError = undefined;
1211
+ state.refreshing = true;
1212
+ update();
1213
+ refreshInFlight = (async () => {
1214
+ try {
1215
+ const { response, payload } = await loadJson("/api/setup/catalog/refresh", { method: "POST" });
1216
+ if (response.status === 403) {
1217
+ state.error = options.expiredMessage;
1218
+ return { ok: false, status: 403, payload };
1219
+ }
1220
+ if (!response.ok) {
1221
+ state.discoveryError = typeof payload.error === "string" ? payload.error : "Catalog refresh failed.";
1222
+ return { ok: false, status: response.status, payload };
1223
+ }
1224
+ if (payload.status === "unavailable") {
1225
+ state.discoveryError = typeof payload.discoveryError === "string" ? payload.discoveryError : "Catalog unavailable.";
1226
+ return { ok: true, status: response.status, payload };
1227
+ }
1228
+ state.catalog = payload;
1229
+ state.discoveryError = typeof payload.discoveryError === "string" ? payload.discoveryError : undefined;
1230
+ return { ok: true, status: response.status, payload };
1231
+ }
1232
+ catch (error) {
1233
+ state.discoveryError = error instanceof Error ? error.message : String(error);
1234
+ return { ok: false, status: 500, payload: { error: state.discoveryError } };
1235
+ }
1236
+ finally {
1237
+ state.refreshing = false;
1238
+ refreshInFlight = undefined;
1239
+ // Let the progress bar finish its sweep before it fades out.
1240
+ ui.catalogDone = true;
1241
+ update();
1242
+ setTimeout(() => {
1243
+ ui.catalogDone = false;
1244
+ update();
1245
+ }, 450);
1246
+ }
1247
+ })();
1248
+ return refreshInFlight;
1249
+ }
1250
+ function buildSelection(values = readForm()) {
1251
+ if (!state.target)
1252
+ throw new Error("Setup state is not loaded.");
1253
+ return options.buildSelection(values, state.target.bindings);
1254
+ }
1255
+ // The dry-run route never accepts an off-catalog model, since only apply
1256
+ // takes offCatalogConfirmed. A 422 whose only failures are models the user
1257
+ // can confirm in the drawer is still a reviewable plan.
1258
+ function onlyOffCatalogFailures(body) {
1259
+ const validations = typeof body.validacoes === "object" && body.validacoes !== null
1260
+ ? body.validacoes
1261
+ : {};
1262
+ const entries = Array.isArray(validations.bindings) ? validations.bindings : [];
1263
+ const failed = entries.filter((entry) => entry.status !== "accepted");
1264
+ const confirmable = offCatalogRoles();
1265
+ return failed.length > 0 && failed.every((entry) => (entry.status === "unknown-model" || entry.status === "unverified") &&
1266
+ confirmable.includes(entry.role));
1267
+ }
1268
+ async function postAction(path, selection) {
1269
+ const isDryRun = path.endsWith("/dry-run");
1270
+ state.error = undefined;
1271
+ actionInFlight = true;
1272
+ actionInFlightLabel = isDryRun ? "Checking changes" : "Saving";
1273
+ if (isDryRun) {
1274
+ state.envelope = undefined;
1275
+ ui.reviewOk = false;
1276
+ }
1277
+ update();
1278
+ try {
1279
+ const response = await options.fetcher(path, {
1280
+ method: "POST",
1281
+ headers: { "content-type": "application/json" },
1282
+ body: JSON.stringify(selection),
1283
+ });
1284
+ const payload = await response.json();
1285
+ const body = typeof payload === "object" && payload !== null ? payload : {};
1286
+ const result = typeof body.resultado === "object" && body.resultado !== null
1287
+ ? body.resultado
1288
+ : {};
1289
+ if (response.status === 403) {
1290
+ state.error = options.expiredMessage;
1291
+ }
1292
+ else if (!response.ok) {
1293
+ state.error = typeof result.message === "string"
1294
+ ? result.message
1295
+ : typeof body.error === "string"
1296
+ ? body.error
1297
+ : "Setup request failed.";
1298
+ }
1299
+ if (isDryRun) {
1300
+ const confirmable = response.status === 422 && onlyOffCatalogFailures(body);
1301
+ if (confirmable)
1302
+ state.error = undefined;
1303
+ ui.reviewOk = (response.ok && result.status === "dry-run") || confirmable;
1304
+ }
1305
+ state.envelope = payload;
1306
+ return { ok: response.ok, status: response.status, payload };
1307
+ }
1308
+ catch (error) {
1309
+ state.error = error instanceof Error ? error.message : String(error);
1310
+ return { ok: false, status: 500, payload: { error: state.error } };
1311
+ }
1312
+ finally {
1313
+ actionInFlight = false;
1314
+ update();
1315
+ }
1316
+ }
1317
+ async function dryRun(values = readForm()) {
1318
+ try {
1319
+ return await postAction("/api/setup/dry-run", buildSelection(values));
1320
+ }
1321
+ catch (error) {
1322
+ state.error = error instanceof Error ? error.message : String(error);
1323
+ update();
1324
+ }
1325
+ }
1326
+ async function apply(values = readForm()) {
1327
+ let selection;
1328
+ try {
1329
+ selection = buildSelection(values);
1330
+ }
1331
+ catch (error) {
1332
+ state.error = error instanceof Error ? error.message : String(error);
1333
+ update();
1334
+ return;
1335
+ }
1336
+ const confirmed = {};
1337
+ for (const role of roles) {
1338
+ const binding = selection.agents[role];
1339
+ const previous = state.target?.bindings[role];
1340
+ if (!binding || (previous?.harness === binding.harness && previous.model === binding.model))
1341
+ continue;
1342
+ if (!isOffCatalog(binding.harness, binding.model))
1343
+ continue;
1344
+ if (!ui.confirmOff[role]) {
1345
+ state.error = `Confirm the off-catalog model for ${role} before saving.`;
1346
+ update();
1347
+ return;
1348
+ }
1349
+ confirmed[role] = true;
1350
+ }
1351
+ if (Object.keys(confirmed).length > 0)
1352
+ selection.offCatalogConfirmed = confirmed;
1353
+ const result = await postAction("/api/setup/apply", selection);
1354
+ const body = typeof result.payload === "object" && result.payload !== null ? result.payload : {};
1355
+ const status = typeof body.resultado === "object" && body.resultado !== null
1356
+ ? body.resultado.status
1357
+ : undefined;
1358
+ if (result.ok && status === "applied") {
1359
+ ui.drawer = false;
1360
+ ui.reviewOk = false;
1361
+ showToast(`Saved to ${state.target?.config?.path ?? "config.json"}`);
1362
+ try {
1363
+ await loadState();
1364
+ }
1365
+ catch (error) {
1366
+ state.error = error instanceof Error ? error.message : String(error);
1367
+ }
1368
+ update();
1369
+ element("setup-review")?.focus?.();
1370
+ }
1371
+ return result;
1372
+ }
1373
+ return {
1374
+ state,
1375
+ get draft() {
1376
+ return draft;
1377
+ },
1378
+ start,
1379
+ refreshCatalog,
1380
+ buildSelection,
1381
+ readForm,
1382
+ dryRun,
1383
+ apply,
1384
+ changeNames: () => changeNames(),
1385
+ openPicker,
1386
+ closePicker,
1387
+ setPickerQuery,
1388
+ handleKey,
1389
+ selectBinding,
1390
+ setEffort,
1391
+ revertRole,
1392
+ choosePreset,
1393
+ chooseCustom,
1394
+ setPolicyValue,
1395
+ setParallelism,
1396
+ setSandbox,
1397
+ setAutocompact,
1398
+ discard,
1399
+ openReview,
1400
+ closeReview,
1401
+ confirmOffCatalog,
1402
+ };
1403
+ }
1404
+ function icon(name, className = "") {
1405
+ return `<svg class="ico${className ? ` ${className}` : ""}" aria-hidden="true" focusable="false"><use href="#i-${name}"></use></svg>`;
1406
+ }
1407
+ function capitalize(value) {
1408
+ return value.charAt(0).toUpperCase() + value.slice(1);
1409
+ }
1410
+ function renderFingerprint(mode, live = false) {
1411
+ const points = SETUP_POLICY_AXES.map((axis, row) => {
1412
+ const level = Math.max(0, axis.options.findIndex((option) => option.value === mode[axis.key]));
1413
+ return [3 + level * 6, 3 + row * 6];
1414
+ });
1415
+ const grid = [0, 1, 2].flatMap((row) => [0, 1, 2].map((col) => `<circle cx="${3 + col * 6}" cy="${3 + row * 6}" r="1" class="fp-dot"></circle>`)).join("");
1416
+ const line = points.map(([x, y], index) => `${index === 0 ? "M" : "L"}${x} ${y}`).join(" ");
1417
+ const dots = points.map(([x, y], index) => `<circle${live ? ` id="policy-fp-dot-${index}"` : ""} class="fp-pt" cx="${x}" cy="${y}" r="1.9"></circle>`).join("");
1418
+ return `<svg class="fp" viewBox="0 0 18 18" aria-hidden="true" focusable="false">${grid}<path${live ? ' id="policy-fp-line"' : ""} class="fp-line" d="${line}"></path>${dots}</svg>`;
1419
+ }
1420
+ function renderRoleRow(role) {
1421
+ const segments = REASONING_EFFORTS.map((effort) => `<button id="effort-${role}-${effort}" class="seg" type="button" role="radio" aria-checked="false" aria-label="${effort}" title="${effort}" data-act="effort" data-role="${role}" data-value="${effort}" disabled></button>`).join("");
1422
+ return `<div id="role-row-${role}" class="r-row" role="row">
1423
+ <div class="r-role" role="cell"><span class="r-dot" aria-hidden="true"></span><div><div class="r-name">${capitalize(role)}</div><div class="r-desc">${ROLE_COPY[role]}</div></div></div>
1424
+ <div class="r-model" role="cell"><button id="binding-${role}" class="model-trigger" type="button" aria-haspopup="listbox" aria-expanded="false" data-act="open-picker" data-role="${role}" disabled><span class="model-sk"><span class="sr-only">Loading</span></span>${icon("chevron-down", "chev")}</button></div>
1425
+ <div class="r-effort" role="cell"><div id="effort-${role}" class="meter lvl-0" role="radiogroup" aria-label="Reasoning effort for ${role}"><span class="meter-segs">${segments}</span><span id="effort-label-${role}" class="meter-label"></span><span id="effort-note-${role}" class="meter-note"></span></div></div>
1426
+ <div id="role-state-${role}" class="r-state" role="cell"></div>
1427
+ </div>`;
1428
+ }
1429
+ function renderPolicyTable() {
1430
+ const presetNames = Object.keys(ORCHESTRATOR_PRESETS);
1431
+ const columns = [...presetNames, "custom"];
1432
+ const heads = columns.map((name, index) => {
1433
+ const mode = name === "custom" ? ORCHESTRATOR_PRESETS.balanced : ORCHESTRATOR_PRESETS[name];
1434
+ return `<button id="policy-col-${name}" class="cmp-head c${index}" type="button" aria-pressed="false" data-act="${name === "custom" ? "custom" : "preset"}" data-value="${name}" disabled>${renderFingerprint(mode, name === "custom")}<span>${capitalize(name)}</span></button>`;
1435
+ }).join("");
1436
+ const rows = SETUP_POLICY_AXES.map((axis, row) => {
1437
+ const last = row === SETUP_POLICY_AXES.length - 1 ? " last" : "";
1438
+ const presetCells = presetNames.map((name, index) => {
1439
+ const option = axis.options.find((candidate) => candidate.value === ORCHESTRATOR_PRESETS[name][axis.key]);
1440
+ return `<div class="cmp-cell c${index}${last}">${icon(option.icon)}<span>${option.label}</span></div>`;
1441
+ }).join("");
1442
+ const start = axis.options.findIndex((option) => option.value === ORCHESTRATOR_PRESETS.balanced[axis.key]);
1443
+ const picks = axis.options.map((option) => `<button id="policy-pick-${axis.key}-${option.value}" class="cmp-pick" type="button" role="radio" aria-checked="false" aria-label="${option.label}" title="${option.label}" data-act="policy-value" data-key="${axis.key}" data-value="${option.value}" disabled>${icon(option.icon)}</button>`).join("");
1444
+ const custom = `<div class="cmp-cell custom c${presetNames.length}${last}"><div id="policy-pick-${axis.key}" class="cmp-picks at-${start}" role="radiogroup" aria-label="Custom: ${axis.label}"><span class="pick-thumb" aria-hidden="true"></span>${picks}</div><span id="policy-custom-label-${axis.key}" class="cmp-custom-label">${axis.options[start].label}</span></div>`;
1445
+ return `<div class="cmp-label${last}">${axis.label}</div>${presetCells}${custom}`;
1446
+ }).join("");
1447
+ return `<div class="cmp-wrap"><div id="policy-table" class="cmp col-none" role="group" aria-label="Orchestrator policy"><span class="cmp-hl" aria-hidden="true"></span><div class="cmp-corner"></div>${heads}${rows}</div></div>`;
1448
+ }
1449
+ export function renderSetupPage(options = {}) {
1450
+ const pages = options.pages ?? [{ label: "Setup", path: "/setup" }];
1451
+ const renderedTopbar = renderTopBar({ pages, activePath: "/setup", title: "Setup" });
1452
+ const topbar = pages.length === 1 && pages[0]?.path === "/setup"
1453
+ ? renderedTopbar.replace('class="brand" href="/"', 'class="brand" href="/setup"')
1454
+ : renderedTopbar;
1455
+ return `<!doctype html>
1456
+ <html lang="en">
1457
+ <head>
1458
+ <meta charset="utf-8">
1459
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1460
+ <title>CodeDeck setup</title>
1461
+ <link rel="icon" href="${LOGO_FAVICON_HREF}">
1462
+ <style>${BRAND_CSS}${SETUP_CSS}</style>
1463
+ </head>
1464
+ <body>
1465
+ ${renderSetupSprite()}
1466
+ ${topbar}
1467
+ <div id="setup-root" class="setup-root loading">
1468
+ <main class="setup-main">
1469
+ <header class="page-head">
1470
+ <div><h1>Setup</h1><p class="lede">Pick the harness and model each role runs on. Saving writes <code id="setup-config-path" class="mono">config.json</code>.</p></div>
1471
+ <div id="setup-catalog" class="catalog" style="--refresh-ms:${CATALOG_DISCOVERY_TIMEOUT_MS}ms"><div class="cat-main"><span id="catalog-status" class="catalog-text" role="status">Loading catalog</span><span class="cat-bar" aria-hidden="true"><span></span></span></div><button id="setup-refresh" class="icon-btn" type="button" data-act="refresh" aria-label="Refresh model catalog" title="Refresh model catalog">${icon("rotate-cw", "i-idle")}${icon("loader", "i-spin")}</button></div>
1472
+ </header>
1473
+ <p id="setup-error" class="load-error" role="alert" hidden></p>
1474
+
1475
+ <section class="sec" aria-labelledby="roles-title">
1476
+ <div class="sec-head"><h2 id="roles-title">Roles</h2><p>Click a model to change it. Nothing is written until you save.</p></div>
1477
+ <div class="roster" role="table" aria-labelledby="roles-title">
1478
+ <div class="r-row r-head" role="row"><span role="columnheader">Role</span><span role="columnheader">Harness and model</span><span role="columnheader">Reasoning effort</span><span role="columnheader"><span class="sr-only">State</span></span></div>
1479
+ ${SETUP_ROLE_ORDER.map((role) => renderRoleRow(role)).join("")}
1480
+ </div>
1481
+ </section>
1482
+
1483
+ <div class="bottom">
1484
+ <section id="policy-section" class="sec" aria-labelledby="policy-title">
1485
+ <div class="sec-head"><h2 id="policy-title"><span class="sec-dot" aria-hidden="true"></span>Orchestrator policy</h2><p>How much the orchestrator does itself before it hands work off.</p></div>
1486
+ ${renderPolicyTable()}
1487
+ <p id="policy-caption" class="policy-caption">Loading policy</p>
1488
+ <div class="pf"><label for="orchestrator-parallelism"><span class="pf-label">Parallel workers</span><span class="pf-copy">The most workers it runs at once. It goes into the orchestrator's prompt as guidance. Leave blank for no limit.</span></label><input id="orchestrator-parallelism" class="num" type="number" min="1" step="1" inputmode="numeric" placeholder="No limit" disabled></div>
1489
+ </section>
1490
+
1491
+ <section class="sec" aria-labelledby="runtime-title">
1492
+ <div class="sec-head"><h2 id="runtime-title">Runtime</h2><p>Defaults for new sessions.</p></div>
1493
+ <div class="rt">
1494
+ <div id="sandbox-row" class="rt-row">
1495
+ <div class="rt-text"><div class="rt-label"><span class="sec-dot" aria-hidden="true"></span>Sandbox <span class="tag">Codex</span></div><p id="sandbox-copy" class="rt-copy"></p></div>
1496
+ <div id="sandbox-control" class="seg-ctl at-none" role="radiogroup" aria-label="Codex sandbox"><span class="seg-thumb" aria-hidden="true"></span><button id="sandbox-workspace-write" type="button" role="radio" aria-checked="false" data-act="sandbox" data-value="workspace-write" disabled>${icon("shield-check")}Workspace write</button><button id="sandbox-danger-full-access" type="button" role="radio" aria-checked="false" data-act="sandbox" data-value="danger-full-access" disabled>${icon("shield-alert")}Full access</button></div>
1497
+ </div>
1498
+ <div id="autocompact-row" class="rt-row">
1499
+ <div class="rt-text"><div class="rt-label"><span class="sec-dot" aria-hidden="true"></span>Autocompact <span class="tag">Claude, OpenCode</span></div><p id="autocompact-copy" class="rt-copy"></p></div>
1500
+ <button id="setup-autocompact" class="switch unset" type="button" role="switch" aria-checked="false" aria-label="Autocompact" data-act="autocompact" disabled></button>
1501
+ </div>
1502
+ </div>
1503
+ </section>
1504
+ </div>
1505
+
1506
+ <div id="setup-bar" class="savebar" role="region" aria-label="Unsaved changes">
1507
+ <div class="sb-left"><span class="sb-dot" aria-hidden="true"></span><strong id="setup-action-status" class="sb-status" role="status" aria-live="polite">Loading setup</strong><span id="setup-change-list" class="sb-list"></span></div>
1508
+ <button id="setup-discard" class="btn ghost" type="button" data-act="discard" disabled>Discard</button>
1509
+ <button id="setup-review" class="btn primary" type="button" data-act="review" disabled>Review and save</button>
1510
+ </div>
1511
+ </main>
1512
+
1513
+ <div id="setup-scrim" class="scrim" data-act="close-review"></div>
1514
+ <aside id="setup-drawer" class="drawer" role="dialog" aria-modal="true" aria-labelledby="setup-drawer-title" aria-hidden="true">
1515
+ <header class="dr-head"><div><h2 id="setup-drawer-title">Review changes</h2><p id="setup-drawer-path" class="dr-path mono">config.json</p></div><button id="setup-drawer-close" class="icon-btn" type="button" data-act="close-review" aria-label="Close review">${icon("x")}</button></header>
1516
+ <div class="dr-body"><div id="setup-confirmations" class="confirmations"></div><div id="setup-result" class="result-content" aria-live="polite"></div></div>
1517
+ <footer class="dr-foot"><button class="btn ghost" type="button" data-act="close-review">Keep editing</button><button id="setup-apply" class="btn primary" type="button" data-act="apply" disabled>Save changes</button></footer>
1518
+ </aside>
1519
+
1520
+ <div id="setup-picker" class="picker" aria-hidden="true">
1521
+ <input id="setup-picker-search" class="search" type="text" role="combobox" aria-expanded="true" aria-controls="setup-picker-list" aria-autocomplete="list" aria-label="Search models" placeholder="Search models or type harness:model" autocomplete="off" spellcheck="false">
1522
+ <div id="setup-picker-list" class="pk-list" role="listbox" aria-label="Models"></div>
1523
+ </div>
1524
+ <div id="setup-toast" class="toast" role="status" aria-live="polite"></div>
1525
+ </div>
1526
+ <script>
1527
+ ${buildSetupSelection.toString()}
1528
+ ${createSetupPageController.toString()}
1529
+ globalThis.setupPage = createSetupPageController({
1530
+ fetcher: (url, init) => fetch(url, init),
1531
+ buildSelection: (values, bindings) => buildSetupSelection(values, bindings, {
1532
+ roles: ${JSON.stringify(ROLES)},
1533
+ efforts: ${JSON.stringify(REASONING_EFFORTS)},
1534
+ presets: ${JSON.stringify(ORCHESTRATOR_PRESETS)},
1535
+ }),
1536
+ roles: ${JSON.stringify(SETUP_ROLE_ORDER)},
1537
+ presets: ${JSON.stringify(ORCHESTRATOR_PRESETS)},
1538
+ efforts: ${JSON.stringify(REASONING_EFFORTS)},
1539
+ harnesses: ${JSON.stringify(AGENT_IDS)},
1540
+ axes: ${JSON.stringify(SETUP_POLICY_AXES)},
1541
+ expiredMessage: ${JSON.stringify(SETUP_SESSION_EXPIRED_MESSAGE)},
1542
+ document,
1543
+ });
1544
+ globalThis.setupPageReady = globalThis.setupPage.start().then(async (state) => {
1545
+ if (new URLSearchParams(location.search).get("refresh") === "1") await globalThis.setupPage.refreshCatalog();
1546
+ return state;
1547
+ });
1548
+ </script>
1549
+ </body>
1550
+ </html>`;
1551
+ }
1552
+ const LEVEL_RULES = [1, 2, 3, 4, 5].map((level) => `.meter.lvl-${level} .seg:nth-child(-n+${level})::before{background:var(--text)}.meter.changed.lvl-${level} .seg:nth-child(-n+${level})::before{background:var(--blue)}`).join("");
1553
+ const SEGMENT_DELAYS = [2, 3, 4, 5].map((n) => `.seg:nth-child(${n})::before{transition-delay:${(n - 1) * 28}ms}`).join("");
1554
+ const COLUMN_RULES = [0, 1, 2, 3].map((column) => `.cmp.col-${column} .cmp-hl{transform:translateX(${column * 100}%)}.cmp.col-${column} .c${column}{color:var(--text)}.cmp.col-${column} .cmp-cell.c${column}:not(.custom) .ico,.cmp.col-${column} .cmp-head.c${column} .fp{color:var(--blue-text)}`).join("");
1555
+ const PICK_RULES = [0, 1, 2].map((index) => `.cmp-picks.at-${index} .pick-thumb{transform:translateX(${index * 28}px)}.cmp-picks.at-${index} .cmp-pick:nth-of-type(${index + 1}){color:var(--text)}.cmp.col-3 .cmp-picks.at-${index} .cmp-pick:nth-of-type(${index + 1}){color:var(--blue-text)}`).join("");
1556
+ export const SETUP_CSS = `
1557
+ :root{--blue-text:#60a5fa;--blue-soft:rgba(0,112,243,.1);--blue-line:rgba(0,112,243,.55);--warn:#f5a524;--warn-text:#f7c56b;--ok-text:#8fe0bc;--err-text:#ff9592;--ease:cubic-bezier(.2,.8,.2,1)}
1558
+ body{font-size:13px;line-height:1.45;-webkit-font-smoothing:antialiased}
1559
+ button{cursor:pointer;color:inherit}button:disabled{cursor:not-allowed}
1560
+ :focus-visible{outline:2px solid var(--blue);outline-offset:2px}
1561
+ h1,h2,h3,p{margin:0}h1,h2{text-wrap:balance}
1562
+ [hidden]{display:none!important}
1563
+ .mono{font-family:var(--font-mono)}
1564
+ .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
1565
+ .sprite{position:absolute;width:0;height:0;overflow:hidden}
1566
+ .ico{display:block;flex:none;width:16px;height:16px}
1567
+ .topbar{height:52px;display:flex;align-items:center;gap:24px;padding:0 28px;border-bottom:1px solid var(--border);background:var(--bg)}
1568
+ .brand{display:flex;align-items:center;gap:8px;color:var(--text);text-decoration:none;font-weight:600;font-size:15px;letter-spacing:-.3px}.brand svg{display:block}
1569
+ .topbar-title{font-size:13px;color:var(--text-muted);border-left:1px solid var(--border-strong);padding-left:18px}.topbar nav{display:flex;gap:20px;margin-left:auto}.topbar nav a{color:var(--text-faint);font-size:12px;text-decoration:none}.topbar nav a:hover,.topbar nav a.active{color:var(--text)}
1570
+ .setup-main{width:min(100%,1240px);margin:0 auto;padding:28px 28px 0}
1571
+ .page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:20px;flex-wrap:wrap;margin-bottom:8px}
1572
+ .page-head h1{margin-bottom:6px;font-size:24px;font-weight:600;letter-spacing:-.6px}
1573
+ .lede{max-width:72ch;color:var(--text-muted)}.lede code{color:var(--text);font-size:12px;overflow-wrap:anywhere}
1574
+ .catalog{display:flex;align-items:center;gap:8px;min-width:0;color:var(--text-faint);font-size:12px}.catalog-text{display:inline-block}.catalog-text.warn{color:var(--warn-text)}
1575
+ .cat-main{position:relative;min-width:0}
1576
+ .cat-bar{position:absolute;left:0;right:0;bottom:-7px;height:2px;overflow:hidden;border-radius:2px;background:var(--border);opacity:0;transition:opacity .3s ease}
1577
+ .cat-bar>span{display:block;height:100%;border-radius:inherit;background:var(--blue);transform:scaleX(0);transform-origin:left;transition:transform 0s .3s}
1578
+ .catalog.refreshing .cat-bar,.catalog.done .cat-bar{opacity:1;transition:opacity .15s ease}
1579
+ .catalog.refreshing .cat-bar>span{transform:scaleX(.95);transition:transform var(--refresh-ms,12s) cubic-bezier(.3,.55,.45,1)}
1580
+ .catalog.done .cat-bar>span{transform:scaleX(1);transition:transform .25s var(--ease)}
1581
+ .icon-btn{display:inline-grid;place-items:center;flex:none;width:30px;height:30px;padding:0;border:1px solid var(--border-strong);border-radius:6px;background:var(--surface);color:var(--text-muted);transition:color .15s,background-color .15s}
1582
+ .icon-btn .ico{width:14px;height:14px}.icon-btn:hover:not(:disabled){color:var(--text);background:var(--surface-raised)}
1583
+ .icon-btn .i-spin,.icon-btn.spinning .i-idle{display:none}
1584
+ .icon-btn.spinning .i-spin{display:block;color:var(--blue-text);transform-origin:50% 50%;animation:cd-spin .75s linear infinite}
1585
+ .load-error{margin-top:16px;padding:10px 12px;border:1px solid rgba(229,72,77,.5);border-radius:7px;color:var(--err-text);font-size:12px}
1586
+ .sec{margin-top:32px}
1587
+ .sec-head{display:flex;align-items:baseline;justify-content:space-between;gap:6px 12px;flex-wrap:wrap;margin-bottom:12px}
1588
+ .sec-head h2{display:flex;align-items:center;font-size:14px;font-weight:600;letter-spacing:-.1px}.sec-head p{color:var(--text-faint);font-size:12px}
1589
+ .sec-dot{display:inline-block;flex:none;width:0;height:7px;border-radius:50%;background:var(--blue);transform:scale(0);transition:transform .25s var(--ease),width .25s var(--ease),margin-right .25s var(--ease)}
1590
+ .sec.changed .sec-head .sec-dot,.rt-row.changed .sec-dot{width:7px;margin-right:8px;transform:scale(1)}
1591
+ .tag{display:inline-flex;align-items:center;border:1px solid var(--border-strong);border-radius:999px;padding:1px 7px;color:var(--text-muted);font-size:10.5px;font-weight:400;white-space:nowrap}
1592
+ .tag.warn{border-color:rgba(245,165,36,.45);color:var(--warn-text)}
1593
+ .hchip{display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);font-size:12px;white-space:nowrap}
1594
+ .hmark{display:inline-grid;place-items:center;flex:none;width:22px;height:22px;border-radius:6px;background:var(--surface-raised)}
1595
+ .hmark svg{display:block;width:14px;height:14px}.hchip[data-h="omp"] .hmark svg{width:17px;height:13px}.hchip[data-h="opencode"] .hmark svg{color:var(--text)}
1596
+ .roster{border-top:1px solid var(--border)}
1597
+ .r-row{display:grid;grid-template-columns:minmax(190px,1fr) minmax(0,1.35fr) 236px 170px;align-items:center;gap:20px;padding:13px 12px;border-bottom:1px solid var(--border);transition:background-color .25s ease}
1598
+ .r-head{padding-block:8px;color:var(--text-faint);font-size:11.5px}
1599
+ .r-row.changed{background:var(--blue-soft)}
1600
+ .r-role{display:flex;align-items:flex-start;gap:10px;min-width:0}
1601
+ .r-dot{flex:none;width:7px;height:7px;margin-top:6px;border-radius:50%;background:var(--blue);transform:scale(0);transition:transform .25s var(--ease)}
1602
+ .r-row.changed .r-dot{transform:scale(1)}
1603
+ .r-name{font-size:13px;font-weight:600}.r-desc{color:var(--text-faint);font-size:12px}
1604
+ .model-trigger{display:flex;align-items:center;gap:10px;width:100%;min-height:38px;padding:6px 10px;border:1px solid var(--border-strong);border-radius:7px;background:var(--surface);text-align:left;transition:border-color .15s,background-color .15s}
1605
+ .model-trigger:hover:not(:disabled){border-color:#3a3a3a;background:#101010}
1606
+ .model-trigger[aria-expanded="true"]{border-color:var(--blue)}
1607
+ .model-trigger:disabled{opacity:.6}
1608
+ .model-body{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
1609
+ .model-text{display:flex;align-items:baseline;gap:8px;flex:1;min-width:0}
1610
+ .model-title{flex:none;max-width:100%;overflow:hidden;color:var(--text);font-size:12.5px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}
1611
+ .model-title.mono{flex:0 1 auto;font-size:12px;font-weight:400;white-space:normal;overflow-wrap:anywhere}
1612
+ .model-id{flex:0 1 auto;min-width:0;overflow:hidden;color:var(--text-faint);font-size:11px;text-overflow:ellipsis;white-space:nowrap}
1613
+ .model-sk{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
1614
+ .model-sk::before{content:"";flex:none;width:22px;height:22px;border-radius:6px}.model-sk::after{content:"";width:min(46%,220px);height:10px;border-radius:4px}
1615
+ .model-empty{flex:1;color:var(--text-faint);font-size:12px}
1616
+ .chev{width:14px;height:14px;color:var(--text-faint);transition:transform .2s var(--ease)}.model-trigger[aria-expanded="true"] .chev{transform:rotate(180deg)}
1617
+ .ready .model-body,.ready .r-state>*{animation:cd-in-a .26s var(--ease)}
1618
+ .meter{display:flex;align-items:center;gap:3px;min-height:24px}
1619
+ .meter-segs{display:flex;gap:3px}
1620
+ .seg{display:grid;place-items:center;width:22px;height:20px;padding:0;border:0;background:none}
1621
+ .seg::before{content:"";display:block;width:100%;height:8px;border-radius:2px;background:var(--border-strong);transition:background-color .18s ease}
1622
+ ${SEGMENT_DELAYS}
1623
+ ${LEVEL_RULES}
1624
+ .seg:not(:disabled):hover::before{filter:brightness(1.7)}
1625
+ .seg:disabled::before{opacity:.35}
1626
+ .meter-label{display:inline-block;min-width:48px;margin-left:8px;color:var(--text-muted);font-size:12px}
1627
+ .meter-note{display:none;color:var(--text-faint);font-size:12px}
1628
+ .meter.none .meter-segs,.meter.none .meter-label,.meter.unbound .meter-label{display:none}
1629
+ .meter.none .meter-note,.meter.unbound .meter-note{display:inline-block}
1630
+ .meter.unbound .meter-note{margin-left:8px}
1631
+ .r-state{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0;font-size:12px}
1632
+ .state-note{display:inline-flex;align-items:center;gap:5px;color:var(--text-faint)}.state-note .ico{width:13px;height:13px}
1633
+ .was{color:var(--text-faint);overflow-wrap:anywhere}.was-value{color:var(--text-muted);text-decoration:line-through;text-decoration-color:var(--text-faint)}
1634
+ .link-btn{display:inline-flex;align-items:center;gap:4px;padding:2px 4px;margin-left:-4px;border:0;border-radius:4px;background:none;color:var(--text-muted);font-size:12px}
1635
+ .link-btn .ico{width:13px;height:13px}.link-btn:hover{color:var(--text)}
1636
+ .bottom{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:48px}
1637
+ .cmp-wrap{overflow-x:auto;margin-inline:-4px;padding:0 4px 2px}
1638
+ .cmp{position:relative;display:grid;grid-template-columns:minmax(118px,.9fr) repeat(4,minmax(112px,1fr));grid-template-rows:repeat(4,auto);min-width:590px}
1639
+ .cmp>*{position:relative;z-index:1}
1640
+ .cmp .cmp-hl{position:absolute;inset:0;z-index:0;grid-column:2/3;grid-row:1/-1;border:1px dashed transparent;border-radius:9px;background:var(--blue-soft);box-shadow:inset 0 2px 0 var(--blue);transition:transform .38s var(--ease),background-color .25s,box-shadow .25s,border-color .25s}
1641
+ .cmp.col-none .cmp-hl{background:transparent;box-shadow:none;border-color:var(--border-strong)}
1642
+ .cmp-corner,.cmp-head,.cmp-label,.cmp-cell{border-bottom:1px solid var(--border)}
1643
+ .cmp-head{display:flex;align-items:center;gap:8px;padding:11px 12px;border-top:0;border-inline:0;background:none;color:var(--text-muted);font-size:12.5px;font-weight:600;text-align:left;transition:color .2s}
1644
+ .cmp-head:hover:not(:disabled){color:var(--text)}
1645
+ .fp{flex:none;width:18px;height:18px;color:var(--text-faint);transition:color .25s}
1646
+ .fp-dot{fill:var(--border-strong)}.fp-pt{fill:currentColor;transition:cx .32s var(--ease)}
1647
+ .fp-line{fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;transition:d .32s var(--ease)}
1648
+ .cmp-label{display:flex;align-items:center;padding:10px 12px 10px 0;color:var(--text-muted);font-size:12px}
1649
+ .cmp-cell{display:flex;align-items:center;gap:8px;padding:10px 12px;color:var(--text-faint);font-size:12px;transition:color .25s}
1650
+ .cmp-cell .ico{width:14px;height:14px;transition:color .25s}
1651
+ .cmp-cell.last,.cmp-label.last{border-bottom-color:transparent}
1652
+ .cmp-cell.custom{flex-direction:column;align-items:flex-start;gap:5px;padding-block:8px}
1653
+ ${COLUMN_RULES}
1654
+ .cmp-picks{position:relative;display:inline-flex;gap:2px;padding:2px;border:1px solid var(--border);border-radius:6px;background:var(--bg)}
1655
+ .pick-thumb{position:absolute;top:2px;left:2px;width:26px;height:22px;border-radius:4px;background:var(--surface-raised);transition:transform .3s var(--ease)}
1656
+ .cmp-pick{position:relative;display:grid;place-items:center;width:26px;height:22px;padding:0;border:0;border-radius:4px;background:none;color:var(--text-faint);transition:color .2s}
1657
+ .cmp-pick:hover:not(:disabled){color:var(--text)}
1658
+ ${PICK_RULES}
1659
+ .cmp-custom-label{display:inline-block;font-size:11.5px}
1660
+ .policy-caption{display:block;margin-top:12px;color:var(--text-muted);font-size:12px}
1661
+ .pf{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:16px;padding-top:14px;border-top:1px solid var(--border)}
1662
+ .pf label{display:grid;gap:2px}.pf-label{font-size:12.5px;font-weight:600}.pf-copy{max-width:52ch;color:var(--text-muted);font-size:12px}
1663
+ .num{flex:none;width:112px;min-height:34px;padding:6px 9px;border:1px solid var(--border-strong);border-radius:6px;background:var(--surface);color:var(--text);font:12px var(--font-mono)}
1664
+ .num::placeholder{color:var(--text-faint)}.num:focus{outline:2px solid var(--blue);outline-offset:1px}.num:disabled{opacity:.5}
1665
+ .rt{display:grid}
1666
+ .rt-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-block:12px;border-top:1px solid var(--border)}
1667
+ .rt-row:first-child{padding-top:0;border-top:0}
1668
+ .rt-label{display:flex;align-items:center;margin-bottom:2px;font-size:12.5px;font-weight:600}.rt-label .tag{margin-left:8px}
1669
+ .rt-copy{display:block;max-width:46ch;min-height:1.45em;color:var(--text-muted);font-size:12px;transition:color .2s}.rt-copy.warn{color:var(--warn-text)}
1670
+ .seg-ctl{position:relative;display:grid;flex:none;grid-template-columns:1fr 1fr;gap:3px;padding:3px;border:1px solid var(--border);border-radius:7px;background:var(--bg)}
1671
+ .seg-thumb{position:absolute;top:3px;bottom:3px;left:3px;width:calc((100% - 9px) / 2);border-radius:4px;background:var(--surface-raised);transition:transform .32s var(--ease),opacity .2s}
1672
+ .seg-ctl.at-1 .seg-thumb{transform:translateX(calc(100% + 3px))}.seg-ctl.at-none .seg-thumb{opacity:0}
1673
+ .seg-ctl button{position:relative;display:flex;align-items:center;justify-content:center;gap:6px;padding:5px 10px;border:0;border-radius:4px;background:none;color:var(--text-faint);font-size:11.5px;white-space:nowrap;transition:color .2s}
1674
+ .seg-ctl button .ico{width:14px;height:14px;transition:color .2s}
1675
+ .seg-ctl button:hover:not(:disabled){color:var(--text)}
1676
+ .seg-ctl.at-0 #sandbox-workspace-write,.seg-ctl.at-1 #sandbox-danger-full-access{color:var(--text)}
1677
+ .seg-ctl.at-0 #sandbox-workspace-write .ico{color:var(--success)}.seg-ctl.at-1 #sandbox-danger-full-access .ico{color:var(--warn)}
1678
+ .switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:1px solid var(--border-strong);border-radius:999px;background:var(--surface-raised);transition:background-color .2s,border-color .2s}
1679
+ .switch::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--text-muted);transition:transform .25s var(--ease),background-color .2s}
1680
+ .switch.unset{border-style:dashed}
1681
+ .switch[aria-checked="true"]{border-color:var(--blue);background:var(--blue)}.switch[aria-checked="true"]::after{background:#fff;transform:translateX(16px)}
1682
+ .switch:disabled{opacity:.5}
1683
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:34px;padding:7px 13px;border:1px solid var(--border-strong);border-radius:6px;background:var(--surface);font-size:12px;font-weight:600;white-space:nowrap;transition:background-color .15s,opacity .2s}
1684
+ .btn:hover:not(:disabled){background:var(--surface-raised)}
1685
+ .btn.primary{border-color:var(--blue);background:var(--blue);color:#fff}.btn.primary:hover:not(:disabled){background:#0063d9}
1686
+ .btn.ghost{border-color:transparent;background:none;color:var(--text-muted)}.btn.ghost:hover:not(:disabled){color:var(--text);background:var(--surface-raised)}
1687
+ .btn:disabled{opacity:.4}
1688
+ .savebar{position:sticky;bottom:0;z-index:5;display:flex;align-items:center;gap:8px;margin:36px -28px 0;padding:12px 28px;border-top:1px solid var(--border);background:rgba(0,0,0,.88);backdrop-filter:blur(10px)}
1689
+ .sb-left{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px;flex:1;min-width:0}
1690
+ .sb-dot{flex:none;width:7px;height:7px;border-radius:50%;background:var(--blue);align-self:center;transform:scale(0);margin-right:-10px;transition:transform .25s var(--ease),margin .25s var(--ease)}
1691
+ .savebar.dirty .sb-dot{transform:scale(1);margin-right:0}
1692
+ .sb-status{display:inline-block;font-size:12.5px}.savebar.has-error .sb-status{color:var(--err-text);font-weight:500}
1693
+ .sb-list{display:inline-block;color:var(--text-muted);font-size:12px;overflow-wrap:anywhere}
1694
+ .scrim{position:fixed;inset:0;z-index:20;background:rgba(0,0,0,.6);opacity:0;visibility:hidden;transition:opacity .3s ease,visibility 0s .3s}
1695
+ .scrim.open{opacity:1;visibility:visible;transition:opacity .3s ease}
1696
+ .drawer{position:fixed;top:0;right:0;bottom:0;z-index:21;display:flex;flex-direction:column;width:min(460px,100%);border-left:1px solid var(--border-strong);background:var(--surface);box-shadow:-24px 0 60px rgba(0,0,0,.5);transform:translateX(100%);visibility:hidden;transition:transform .36s var(--ease),visibility 0s .36s}
1697
+ .drawer.open{transform:none;visibility:visible;transition:transform .36s var(--ease)}
1698
+ .dr-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:18px 20px;border-bottom:1px solid var(--border)}
1699
+ .dr-head h2{font-size:15px;font-weight:600}.dr-path{margin-top:2px;color:var(--text-faint);font-size:11.5px;overflow-wrap:anywhere}
1700
+ .dr-body{flex:1;display:grid;align-content:start;gap:18px;padding:18px 20px;overflow:auto}
1701
+ .result-content{display:grid;gap:22px}.result-content:empty{display:none}
1702
+ .dr-section{animation:cd-in-a .32s var(--ease) both}
1703
+ .dr-section:nth-child(2){animation-delay:.05s}.dr-section:nth-child(3){animation-delay:.1s}.dr-section:nth-child(4){animation-delay:.15s}
1704
+ .dr-foot{display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--border)}
1705
+ .dr-label{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;color:var(--text-faint);font-size:11.5px;font-weight:400}
1706
+ .result-status{display:flex;align-items:flex-start;gap:10px;padding:11px 12px;border:1px solid var(--border-strong);border-radius:7px;background:var(--bg);font-size:12px}
1707
+ .result-status .ico{width:15px;height:15px;margin-top:1px}.result-status>span{display:grid;gap:2px}
1708
+ .result-msg{color:var(--text-muted);overflow-wrap:anywhere}
1709
+ .status-dry-run{border-color:var(--blue-line)}.status-dry-run .ico{color:var(--blue-text)}
1710
+ .status-applied,.status-unchanged{border-color:rgba(16,185,129,.45)}.status-applied .ico,.status-unchanged .ico{color:var(--success)}
1711
+ .status-error,.status-aborted{border-color:rgba(229,72,77,.55)}.status-error .ico,.status-aborted .ico{color:var(--error)}
1712
+ .ch-list{display:grid;gap:6px}
1713
+ .ch{display:grid;gap:4px;padding:10px 12px;border:1px solid var(--border);border-radius:7px;background:var(--bg)}
1714
+ .ch-name{font-size:12px;font-weight:600}
1715
+ .ch-vals{display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;color:var(--text-muted);font:11.5px var(--font-mono)}
1716
+ .ch-vals .before{color:var(--text-faint);text-decoration:line-through;text-decoration-color:var(--text-faint);overflow-wrap:anywhere}
1717
+ .ch-vals .after{color:var(--text);overflow-wrap:anywhere}.ch-to{color:var(--text-faint);font-family:var(--font-sans)}
1718
+ .marker{color:var(--blue-text);font:500 10.5px var(--font-sans)}
1719
+ .counts{display:flex;gap:8px;font:11.5px var(--font-mono)}.add{color:var(--ok-text)}.del{color:var(--err-text)}
1720
+ .diff{max-height:320px;overflow:auto;border:1px solid var(--border);border-radius:7px;background:var(--bg);font:11px/1.7 var(--font-mono)}
1721
+ .dl{display:grid;grid-template-columns:30px 30px 14px minmax(max-content,1fr);white-space:pre}
1722
+ .dl>span{padding-right:6px}.dl .no{color:#3d3d3d;text-align:right;user-select:none}.dl .mk{color:var(--text-faint);user-select:none}
1723
+ .dl.plus{background:rgba(16,185,129,.09)}.dl.plus .mk{color:var(--ok-text)}
1724
+ .dl.minus{background:rgba(229,72,77,.09)}.dl.minus .mk{color:var(--err-text)}
1725
+ .dl .k{color:#8c8c8c}.dl .s{color:#cfcfcf}.dl.plus .s{color:#bff0da}.dl.minus .s{color:#ffc6c4}
1726
+ .fold{padding:2px 0 2px 76px;border-block:1px solid var(--border);background:#0d0d0d;color:var(--text-faint);font:10.5px var(--font-sans)}
1727
+ .dr-note{margin-top:6px;color:var(--text-faint);font-size:11px}
1728
+ .checks{display:grid;gap:7px}
1729
+ .check{display:flex;align-items:flex-start;gap:8px;color:var(--text-muted);font-size:12px}
1730
+ .check .ico{width:14px;height:14px;margin-top:1px}.check.ok .ico{color:var(--success)}.check.warn .ico{color:var(--warn)}
1731
+ .check strong{color:var(--text);font-weight:500}.check-msg{display:block;color:var(--text-faint);overflow-wrap:anywhere}
1732
+ .empty{margin:0;color:var(--text-faint);font-size:12px}
1733
+ .confirmations{display:grid;gap:8px}.confirmations:empty{display:none}
1734
+ .confirm{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;border:1px solid rgba(245,165,36,.4);border-radius:7px;color:var(--warn-text);font-size:12px;animation:cd-in-a .3s var(--ease) both}
1735
+ .confirm input{margin-top:2px;accent-color:var(--warn)}.confirm .mono{overflow-wrap:anywhere}
1736
+ .dr-loading{display:grid;gap:10px;color:var(--text-faint);font-size:12px}
1737
+ .sk{display:block;height:34px;border-radius:7px;background:linear-gradient(90deg,#111 0%,#1c1c1c 50%,#111 100%);background-size:200% 100%;animation:cd-shimmer 1.2s linear infinite}.sk.short{width:62%}
1738
+ .raw-response{color:var(--text-faint);font-size:11.5px}.raw-response summary{cursor:pointer}
1739
+ .raw-response pre{max-height:320px;overflow:auto;margin:8px 0 0;padding:10px;border:1px solid var(--border);border-radius:7px;background:var(--bg);color:var(--text-muted);font:10.5px/1.5 var(--font-mono);white-space:pre-wrap;overflow-wrap:anywhere}
1740
+ .picker{position:fixed;top:0;left:0;z-index:30;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border-strong);border-radius:9px;background:var(--surface);box-shadow:0 18px 40px rgba(0,0,0,.6);opacity:0;visibility:hidden;transform:translateY(-4px) scale(.98);transform-origin:top left;transition:opacity .16s ease,transform .18s var(--ease),visibility 0s .18s}
1741
+ .picker.open{opacity:1;visibility:visible;transform:none;transition:opacity .16s ease,transform .18s var(--ease)}
1742
+ .search{display:block;width:100%;padding:11px 12px;border:0;border-bottom:1px solid var(--border);background:none;color:var(--text);font-size:12.5px;outline:none}
1743
+ .search::placeholder{color:var(--text-faint)}
1744
+ .pk-list{flex:1;min-height:0;padding:6px;overflow:auto}
1745
+ .pk-group+.pk-group{margin-top:6px;padding-top:6px;border-top:1px solid var(--border)}
1746
+ .pk-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:5px 6px}
1747
+ .pk-head.off .hchip{opacity:.5}
1748
+ .pk-note{min-width:0;color:var(--text-faint);font-size:11px;text-align:right;overflow-wrap:anywhere}
1749
+ .pk-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px 7px 34px;border:0;border-radius:5px;background:none;color:var(--text-muted);font-size:12px;text-align:left}
1750
+ .pk-item:hover,.pk-item.kbd{background:var(--surface-raised);color:var(--text)}
1751
+ .pk-title{flex:none;max-width:62%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pk-title.mono{flex:0 1 auto;max-width:none;white-space:normal;overflow-wrap:anywhere}
1752
+ .pk-id{flex:1;min-width:40px;overflow:hidden;color:var(--text-faint);font-size:11px;text-align:right;text-overflow:ellipsis;white-space:nowrap}
1753
+ .pk-item .tag{margin-left:auto}.pk-id+.tag{margin-left:0}
1754
+ .pk-item.sel{color:var(--text)}.pk-check{width:14px;height:14px;color:var(--blue-text)}
1755
+ .pk-custom{justify-content:space-between;padding-left:8px}
1756
+ .pk-empty{margin:0;padding:12px;color:var(--text-faint);font-size:12px}
1757
+ .toast{position:fixed;left:50%;bottom:24px;z-index:40;display:flex;align-items:center;gap:8px;max-width:calc(100% - 32px);padding:10px 14px;border:1px solid rgba(16,185,129,.45);border-radius:8px;background:var(--surface);box-shadow:0 10px 30px rgba(0,0,0,.5);font-size:12px;opacity:0;visibility:hidden;transform:translate(-50%,10px);transition:opacity .2s ease,transform .3s var(--ease),visibility 0s .3s}
1758
+ .toast.show{opacity:1;visibility:visible;transform:translate(-50%,0);transition:opacity .2s ease,transform .3s var(--ease)}
1759
+ .toast .ico{width:14px;height:14px;color:var(--success)}.toast span{overflow-wrap:anywhere}
1760
+ .swap-a{animation:cd-in-a .26s var(--ease)}.swap-b{animation:cd-in-b .26s var(--ease)}
1761
+ @keyframes cd-in-a{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
1762
+ @keyframes cd-in-b{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
1763
+ @keyframes cd-spin{to{transform:rotate(360deg)}}
1764
+ @keyframes cd-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
1765
+ .loading .model-sk::before,.loading .model-sk::after,.loading .seg::before,.loading .r-state::before,.loading .rt-copy::before,.loading .rt-copy::after,.loading .switch,.loading .num,.loading #setup-config-path,.loading .catalog-text,.loading .policy-caption,.loading .cmp-custom-label,.loading .sb-status{color:transparent!important;background:linear-gradient(90deg,#141414 0%,#232323 50%,#141414 100%);background-size:200% 100%;animation:cd-shimmer 1.4s linear infinite;user-select:none}
1766
+ .loading #setup-config-path,.loading .catalog-text,.loading .policy-caption,.loading .cmp-custom-label,.loading .sb-status{border-radius:4px}
1767
+ .loading .policy-caption{width:min(100%,420px)}
1768
+ .loading .seg:disabled::before,.loading .switch:disabled,.loading .num:disabled{opacity:1}
1769
+ .loading .r-state::before{content:"";width:52px;height:10px;border-radius:4px}
1770
+ .loading .rt-copy::before,.loading .rt-copy::after{content:"";display:block;width:min(100%,300px);height:10px;margin-top:5px;border-radius:4px}.loading .rt-copy::after{width:min(70%,190px)}
1771
+ .loading .switch,.loading .num{border-color:transparent}.loading .switch::after{opacity:0}.loading .num::placeholder{color:transparent}
1772
+ .loading .cmp-hl,.loading .pick-thumb{opacity:0}.loading .cmp-picks .cmp-pick{color:var(--border-strong)!important}.loading .cmp-custom-label{min-width:60px}
1773
+ .ready .meter-segs,.ready .switch,.ready .num,.ready .seg-ctl,.ready .cmp-picks,.ready .cmp-hl{animation:cd-fade .3s var(--ease)}
1774
+ @keyframes cd-fade{from{opacity:0}to{opacity:1}}
1775
+ .setup-root:not(.settled) *,.setup-root:not(.settled) *::before,.setup-root:not(.settled) *::after{transition:none!important}
1776
+ @media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition-duration:0s!important;transition-delay:0s!important}}
1777
+ @media (max-width:1080px){
1778
+ .r-row{grid-template-columns:minmax(0,1fr) minmax(0,1.2fr);gap:10px 20px}
1779
+ .r-head{display:none}
1780
+ .r-state{flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px}
1781
+ .bottom{grid-template-columns:minmax(0,1fr);gap:0}
1782
+ }
1783
+ @media (max-width:640px){
1784
+ .topbar{height:auto;min-height:52px;flex-wrap:wrap;gap:8px 12px;padding:9px 16px}
1785
+ .topbar-title{padding-left:10px;font-size:12px}.topbar nav{gap:12px}
1786
+ .setup-main{padding:20px 16px 0}
1787
+ .r-row{grid-template-columns:minmax(0,1fr);padding-inline:4px}
1788
+ .pf,#sandbox-row{flex-direction:column;align-items:flex-start}
1789
+ .seg-ctl{width:100%}
1790
+ .savebar{margin-inline:-16px;padding:10px 16px}
1791
+ .sb-list{display:none}
1792
+ }
1793
+ `;
1794
+ export const SETUP_PAGE = renderSetupPage();
1795
+ //# sourceMappingURL=setup-page.js.map