@zq-silk/yui 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +15 -1
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +18 -9
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +65 -14
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +18 -3
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
  23. package/dist/controller/globalInputDelivery.js +2 -4
  24. package/dist/controller/jobControl.js +2 -0
  25. package/dist/controller/nativeHostInput.js +37 -0
  26. package/dist/controller/providerRetryDelivery.js +4 -2
  27. package/dist/controller/runtime.js +93 -2
  28. package/dist/controller/runtimeEventInbox.js +2 -2
  29. package/dist/controller/runtimeEventProcessor.js +3 -3
  30. package/dist/doctor/doctor.js +10 -0
  31. package/dist/execution/reviewMainRun.js +1 -0
  32. package/dist/execution/workItemMainRun.js +1 -0
  33. package/dist/executor/agentAdapter.js +3 -5
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +15 -7
  37. package/dist/integration/gitIntegrationService.js +7 -12
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/interaction/notificationPresentation.js +85 -0
  40. package/dist/interaction/operatorPresentation.js +1 -2
  41. package/dist/job/httpPreview.js +61 -0
  42. package/dist/job/jobRunner.js +12 -2
  43. package/dist/kernel/builtinCapabilities.js +3 -1
  44. package/dist/message/message.js +15 -1
  45. package/dist/output/rolePresentation.js +4 -1
  46. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  47. package/dist/repository/gitCompatibility.js +62 -0
  48. package/dist/repository/gitWorkspace.js +48 -71
  49. package/dist/repository/gitWorktreeInventory.js +121 -0
  50. package/dist/repository/project.js +74 -25
  51. package/dist/resources/resourceDiscovery.js +2 -2
  52. package/dist/resources/resourceGc.js +7 -4
  53. package/dist/resources/resourceRegistrar.js +2 -1
  54. package/dist/runtime/agentEndpoint.js +8 -0
  55. package/dist/runtime/agentHost.js +170 -3
  56. package/dist/runtime/codexAppServerRuntime.js +1 -1
  57. package/dist/runtime/codexNativeAccess.js +175 -0
  58. package/dist/runtime/controlledCodexTui.js +116 -0
  59. package/dist/runtime/publicReply.js +23 -0
  60. package/dist/runtime/runtimeObservation.js +48 -0
  61. package/dist/runtime/structuredProviderHost.js +16 -7
  62. package/dist/runtime/taskCostMetrics.js +133 -0
  63. package/dist/runtime/taskUsageMetrics.js +11 -2
  64. package/dist/runtime/taskUsageQuery.js +38 -0
  65. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  66. package/dist/scheduler/taskExecutionProjection.js +2 -0
  67. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  68. package/dist/storage/homeLayout.js +13 -0
  69. package/dist/storage/sqliteSchema.js +44 -0
  70. package/dist/storage/sqliteStore.js +53 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/storage/storeRpc.js +2 -0
  73. package/dist/storage/taskSearch.js +35 -0
  74. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  75. package/dist/task/ordinaryArchive.js +42 -0
  76. package/dist/tmux/tmuxManager.js +8 -8
  77. package/dist/web/assets/assetManifest.js +11 -51
  78. package/dist/web/assets/client/app/catalog.js +215 -0
  79. package/dist/web/assets/client/app/main.js +148 -0
  80. package/dist/web/assets/client/app/selection.js +139 -0
  81. package/dist/web/assets/client/app/settings.js +326 -0
  82. package/dist/web/assets/client/app/taskView.js +280 -0
  83. package/dist/web/assets/client/domain/context.js +96 -0
  84. package/dist/web/assets/client/domain/records.js +126 -0
  85. package/dist/web/assets/client/domain/runs.js +119 -0
  86. package/dist/web/assets/client/domain/taskForms.js +197 -0
  87. package/dist/web/assets/client/domain/vocab.js +69 -0
  88. package/dist/web/assets/client/domain/work.js +116 -0
  89. package/dist/web/assets/client/i18n/en.js +900 -0
  90. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  91. package/dist/web/assets/client/index.js +114 -0
  92. package/dist/web/assets/client/layout/resizer.js +37 -0
  93. package/dist/web/assets/client/layout/router.js +35 -0
  94. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  95. package/dist/web/assets/client/layout/sizing.js +95 -0
  96. package/dist/web/assets/client/layout/workspace.js +247 -0
  97. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  98. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  99. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  100. package/dist/web/assets/client/lib/i18n.js +54 -0
  101. package/dist/web/assets/client/lib/markdown.js +167 -0
  102. package/dist/web/assets/client/lib/prefs.js +28 -0
  103. package/dist/web/assets/client/lib/theme.js +68 -0
  104. package/dist/web/assets/client/ui/containers.js +101 -0
  105. package/dist/web/assets/client/ui/controls.js +51 -0
  106. package/dist/web/assets/client/ui/forms.js +48 -0
  107. package/dist/web/assets/client/ui/metrics.js +137 -0
  108. package/dist/web/assets/client/ui/primitives.js +92 -0
  109. package/dist/web/assets/client/ui/text.js +33 -0
  110. package/dist/web/assets/client/ui/toast.js +12 -0
  111. package/dist/web/assets/client/views/bodySearch.js +172 -0
  112. package/dist/web/assets/client/views/createTask.js +85 -0
  113. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  114. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  115. package/dist/web/assets/client/views/dock/preview.js +129 -0
  116. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  117. package/dist/web/assets/client/views/globalInput.js +67 -0
  118. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  119. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  120. package/dist/web/assets/client/views/skills.js +152 -0
  121. package/dist/web/assets/client/views/task/actions.js +36 -0
  122. package/dist/web/assets/client/views/task/delivery.js +185 -0
  123. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  124. package/dist/web/assets/client/views/task/files.js +272 -0
  125. package/dist/web/assets/client/views/task/observation.js +153 -0
  126. package/dist/web/assets/client/views/task/overview.js +161 -0
  127. package/dist/web/assets/client/views/task/page.js +131 -0
  128. package/dist/web/assets/client/views/task/records.js +163 -0
  129. package/dist/web/assets/client/views/task/runtime.js +130 -0
  130. package/dist/web/assets/client/views/task/timeline.js +190 -0
  131. package/dist/web/assets/shared/geometry.js +19 -0
  132. package/dist/web/assets/shared/taskTabs.js +8 -0
  133. package/dist/web/assets/shared/themes.js +93 -0
  134. package/dist/web/assets/shell/dialogs.js +63 -0
  135. package/dist/web/assets/shell/dock.js +40 -0
  136. package/dist/web/assets/shell/index.js +40 -0
  137. package/dist/web/assets/shell/settings.js +34 -0
  138. package/dist/web/assets/shell/sidebar.js +45 -0
  139. package/dist/web/assets/styles/index.js +41 -0
  140. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  141. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  142. package/dist/web/assets/styles/markdown.js +22 -3
  143. package/dist/web/assets/styles/tokens.js +10 -63
  144. package/dist/web/assets/styles/ui/controls.js +76 -0
  145. package/dist/web/assets/styles/ui/forms.js +46 -0
  146. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  147. package/dist/web/assets/styles/views/delivery.js +29 -0
  148. package/dist/web/assets/styles/views/dock.js +71 -0
  149. package/dist/web/assets/styles/views/page.js +32 -0
  150. package/dist/web/assets/styles/views/records.js +70 -0
  151. package/dist/web/assets/styles/views/settings.js +26 -0
  152. package/dist/web/assets/styles/views/sidebar.js +74 -0
  153. package/dist/web/assets/styles/views/task.js +80 -0
  154. package/dist/web/assets/styles/views/terminal.js +38 -0
  155. package/dist/web/taskPreviews.js +205 -0
  156. package/dist/web/tmuxWebTerminal.js +16 -9
  157. package/dist/web/webConversation.js +373 -0
  158. package/dist/web/webMaterials.js +155 -0
  159. package/dist/web/webServer.js +295 -18
  160. package/dist/web/webSettings.js +331 -0
  161. package/dist/web/webSkills.js +86 -0
  162. package/dist/web/webSnapshot.js +40 -8
  163. package/dist/web/webTaskSurface.js +106 -8
  164. package/dist/workspace/gitChangeSetCapture.js +3 -1
  165. package/docs/cli-information-contract.md +1 -1
  166. package/docs/cli-information-contract.zh-CN.md +1 -1
  167. package/docs/managed-turn-and-session-runtime.md +31 -0
  168. package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
  169. package/docs/release-workflow.md +54 -0
  170. package/docs/release-workflow.zh-CN.md +39 -0
  171. package/docs/storage-baseline.md +38 -1
  172. package/docs/storage-baseline.zh-CN.md +22 -0
  173. package/docs/task-discovery.md +28 -0
  174. package/docs/testing/verification-levels.md +2 -2
  175. package/i18n/README.zh-CN.md +1 -1
  176. package/package.json +1 -1
  177. package/skills/yui-leader/SKILL.md +5 -4
  178. package/skills/yui-leader/references/execution.md +2 -1
  179. package/skills/yui-operator/SKILL.md +3 -0
  180. package/skills/yui-reviewer/SKILL.md +4 -2
  181. package/skills/yui-runtime/SKILL.md +43 -2
  182. package/skills/yui-worker/SKILL.md +3 -2
  183. package/dist/web/assets/client/app.js +0 -808
  184. package/dist/web/assets/client/components.js +0 -477
  185. package/dist/web/assets/client/detail.js +0 -379
  186. package/dist/web/assets/client/dock.js +0 -309
  187. package/dist/web/assets/client/evidence.js +0 -364
  188. package/dist/web/assets/client/forms.js +0 -230
  189. package/dist/web/assets/client/i18n.js +0 -1277
  190. package/dist/web/assets/client/markdown.js +0 -114
  191. package/dist/web/assets/client/records.js +0 -198
  192. package/dist/web/assets/client/sections.js +0 -349
  193. package/dist/web/assets/client/theme.js +0 -71
  194. package/dist/web/assets/shell.js +0 -161
  195. package/dist/web/assets/styles/components.js +0 -216
  196. package/dist/web/assets/styles/views.js +0 -309
  197. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -0,0 +1,139 @@
1
+ export const SELECTION_SCRIPT = String.raw `
2
+ // Which Task is selected and its loaded detail: selecting one, leaving it,
3
+ // following the URL, and reading the Context snapshot (the only input to core
4
+ // rendering) plus the optional runtime observation. This module is the only
5
+ // writer of state.selected, state.detail and state.detailKey; app/taskView
6
+ // draws the result. Leaving a Task with an unsent draft asks first; the page
7
+ // itself warns before unload.
8
+ import { applyUrl, readQuery, syncUrl, urlTaskId } from "/assets/js/layout/router.js";
9
+
10
+ // Fingerprint of the rendered payload. Quiet polling skips identical reads so
11
+ // local view state (open blocks, list pages, drafts) survives.
12
+ function fingerprint(value) {
13
+ const text = JSON.stringify(value);
14
+ let hash = 0;
15
+ for (let i = 0; i < text.length; i += 1) hash = (hash * 31 + text.charCodeAt(i)) | 0;
16
+ return text.length + ":" + hash;
17
+ }
18
+
19
+ // deps: { state, t, api, toast, workspace, taskView, renderTaskList }
20
+ // state writes selected, detail, detailKey
21
+ // api context, inspect and observation reads
22
+ // workspace layout/workspace: layout and the dock on entering/leaving
23
+ // taskView app/taskView: draws the selected Task and the overview
24
+ // renderTaskList() redraw the sidebar list's selection (app/catalog)
25
+ export function createSelection(deps) {
26
+ window.addEventListener("beforeunload", function (event) {
27
+ if (!deps.taskView.hasUnsent()) return;
28
+ event.preventDefault();
29
+ event.returnValue = "";
30
+ });
31
+ return {
32
+ selectTask: function (taskId) { return selectTask(deps, taskId); },
33
+ clearSelection: function () { clearSelection(deps); },
34
+ followUrl: function (params) { followUrl(deps, params); },
35
+ // Quietly re-read the selected Task; rejects when the read fails.
36
+ reload: function () { return load(deps, deps.state.selected, false); }
37
+ };
38
+ }
39
+
40
+ function canLeave(deps) {
41
+ return !deps.taskView.hasUnsent() || window.confirm(deps.t("confirm.leave"));
42
+ }
43
+
44
+ async function selectTask(deps, taskId) {
45
+ const state = deps.state;
46
+ // Re-selecting the visible Task is not navigation.
47
+ if (state.selected === taskId && state.detail) return;
48
+ if (state.selected !== taskId && !canLeave(deps)) { syncUrl(state, true); return; }
49
+ if (urlTaskId() !== taskId) {
50
+ const params = readQuery();
51
+ params.delete("section");
52
+ applyUrl(params, true);
53
+ }
54
+ // Show taskId as selected before its detail arrives.
55
+ state.selected = taskId;
56
+ state.detail = null;
57
+ deps.workspace.enterTask();
58
+ deps.taskView.enterTask();
59
+ syncUrl(state, state.selected === urlTaskId());
60
+ deps.workspace.updateLayout();
61
+ deps.renderTaskList();
62
+ try {
63
+ await load(deps, taskId, true);
64
+ } catch {
65
+ if (state.selected !== taskId) return;
66
+ deps.taskView.showDetailError();
67
+ deps.toast(deps.t("errors.detail"));
68
+ }
69
+ }
70
+
71
+ function clearSelection(deps) {
72
+ if (!canLeave(deps)) return;
73
+ dropSelection(deps);
74
+ syncUrl(deps.state, !urlTaskId());
75
+ deps.workspace.updateLayout();
76
+ deps.taskView.showOverview();
77
+ deps.renderTaskList();
78
+ deps.taskView.updateSessionTargets();
79
+ }
80
+
81
+ // Drop the selected Task and its dock discussion; callers redraw.
82
+ function dropSelection(deps) {
83
+ deps.state.selected = null;
84
+ deps.state.detail = null;
85
+ deps.taskView.leaveTask();
86
+ deps.workspace.leaveTask();
87
+ }
88
+
89
+ // The Task and section named by the URL, on load and on back/forward.
90
+ function followUrl(deps, params) {
91
+ const state = deps.state;
92
+ const taskId = params.get("task");
93
+ if (taskId) {
94
+ if (taskId === state.selected && state.detail) deps.taskView.applySection(params.get("section"));
95
+ else selectTask(deps, taskId);
96
+ return;
97
+ }
98
+ if (state.selected && !canLeave(deps)) { syncUrl(state, true); return; }
99
+ dropSelection(deps);
100
+ deps.workspace.updateLayout();
101
+ deps.taskView.showOverview();
102
+ }
103
+
104
+ async function load(deps, taskId, navigate) {
105
+ const state = deps.state;
106
+ if (navigate) deps.taskView.showLoading();
107
+ // Rendering consumes the current Context snapshot, not event pages.
108
+ const snapshot = await deps.api.workbench(taskId);
109
+ const core = snapshot.core;
110
+ const task = snapshot.task;
111
+ if (state.selected !== taskId) return;
112
+ const previous = state.detail && state.detail.task.id === taskId ? state.detail : null;
113
+ const detail = {
114
+ task: task, core: core, briefValue: snapshot.brief, evidenceSummary: snapshot.evidence,
115
+ viewState: previous ? previous.viewState : {},
116
+ runtime: previous ? previous.runtime : null,
117
+ runtimeStatus: previous ? previous.runtimeStatus : "waiting",
118
+ runtimeObservedAt: previous ? previous.runtimeObservedAt : new Date().toISOString()
119
+ };
120
+ state.detail = detail;
121
+ state.detailKey = fingerprint(core);
122
+ deps.taskView.render(navigate);
123
+ if (navigate) deps.taskView.applySection(readQuery().get("section"));
124
+ observe(deps, taskId, detail);
125
+ }
126
+
127
+ // The optional observation never participates in core readiness.
128
+ function observe(deps, taskId, detail) {
129
+ function settle(runtime, status) {
130
+ if (deps.state.detail !== detail) return;
131
+ detail.runtime = runtime;
132
+ detail.runtimeStatus = status;
133
+ detail.runtimeObservedAt = new Date().toISOString();
134
+ deps.taskView.observationArrived(detail);
135
+ }
136
+ deps.api.observation(taskId).then(function (runtime) { settle(runtime, "available"); })
137
+ .catch(function () { settle(null, "unavailable"); });
138
+ }
139
+ `;
@@ -0,0 +1,326 @@
1
+ export const SETTINGS_SCRIPT = String.raw `
2
+ import { h } from "/assets/js/lib/dom.js";
3
+ import { requestJson, submitMutation } from "/assets/js/lib/api.js";
4
+ import { createI18n } from "/assets/js/lib/i18n.js";
5
+ import { createThemeController } from "/assets/js/lib/theme.js";
6
+ import { clearPreference, readSessionAccessMode, writeSessionAccessMode } from "/assets/js/lib/prefs.js";
7
+ import { mountSkills } from "/assets/js/views/skills.js";
8
+
9
+ const $ = selector => document.querySelector(selector);
10
+ const i18n = createI18n($("#locale-select"));
11
+ const t = i18n.t;
12
+ mountSkills($("#skill-browser"), t);
13
+ const theme = createThemeController($("#theme-options"), t);
14
+ i18n.subscribe(() => theme.render());
15
+ const access = $("#access-mode");
16
+ access.value = readSessionAccessMode();
17
+ access.addEventListener("change", () => {
18
+ $("#browser-receipt").textContent = writeSessionAccessMode(access.value) ? t("settings.browserSaved") : t("settings.storageFailed");
19
+ });
20
+ $("#reset-layout").addEventListener("click", () => {
21
+ const keys = ["yui.sidebar.width", "yui.dock.width", "yui.dock.side", "yui.dock.open"];
22
+ const results = keys.map(clearPreference);
23
+ $("#browser-receipt").textContent = results.every(Boolean) ? t("settings.layoutReset") : t("settings.storageFailed");
24
+ });
25
+
26
+ const container = $("#settings-groups");
27
+ const status = $("#settings-status");
28
+ const search = $("#settings-search");
29
+ const groups = [];
30
+ let searchGeneration = 0;
31
+ let searchTimer;
32
+ const more = h("button.btn", { type: "button", hidden: true }, t("settings.more"));
33
+ container.after(more);
34
+ window.addEventListener("beforeunload", event => {
35
+ if (!groups.some(g => g.dirty)) return;
36
+ event.preventDefault(); event.returnValue = "";
37
+ });
38
+
39
+ function format(value) { return typeof value === "string" ? value : JSON.stringify(value, null, 2); }
40
+ function inputValue(field, control) {
41
+ if (field.kind === "json") return JSON.parse(control.value);
42
+ if (field.kind === "number") {
43
+ if (!control.value.trim() || !Number.isFinite(Number(control.value))) throw new Error(t("settings.invalidNumber"));
44
+ return Number(control.value);
45
+ }
46
+ if (field.kind === "boolean") return control.value === "true";
47
+ return control.value;
48
+ }
49
+
50
+ function editor(field, group, state) {
51
+ const labelText = t("settings.field." + field.key, field.label);
52
+ const label = h("label.field", null, h("span", null, labelText));
53
+ let control;
54
+ if (field.kind === "boolean" || field.choices && field.choices.length) {
55
+ const values = field.kind === "boolean" ? ["true", "false"] : field.choices;
56
+ control = h("select", null, values.map(v => h("option", { value: v }, v || t("settings.inherit"))));
57
+ if (!values.includes(String(field.value ?? ""))) control.append(h("option", { value: String(field.value ?? "") }, String(field.value ?? "")));
58
+ } else control = h(field.kind === "json" || field.kind === "multiline" ? "textarea" : "input", {
59
+ ...(field.kind === "number" ? { type: "number", step: "any" } : {}),
60
+ ...(field.kind === "json" || field.kind === "multiline" ? { rows: 4 } : {})
61
+ });
62
+ const initial = field.kind === "json" ? JSON.stringify(field.value, null, 2) : String(field.value ?? "");
63
+ control.value = initial;
64
+ label.append(control);
65
+ const reset = h("input", { type: "checkbox" });
66
+ const row = h("div.settings-field", { dataset: { field: field.key } }, label,
67
+ field.summary ? h("p", null, field.summary) : null,
68
+ h("small", null, t("settings.current") + ": " + (field.value === "" ? t("settings.inherit") : format(field.value)) +
69
+ (field.source ? " · " + t("settings.source." + field.source, field.source) : "") +
70
+ (Object.hasOwn(field, "defaultValue") ? " · " + t("settings.default") + ": " + format(field.defaultValue) : "")),
71
+ field.takesEffect ? h("small", null, field.takesEffect) : null,
72
+ field.reset ? h("label.settings-reset", null, reset, t("settings.reset")) : null);
73
+ let unavailable = false;
74
+ function mark() {
75
+ control.disabled = reset.checked || unavailable;
76
+ state.dirty = state.editors.some(e => e.changed());
77
+ state.save.disabled = !state.dirty || state.pending || state.capabilityPending || state.unknown;
78
+ if (!state.unknown && !state.pending) state.receipt.textContent = state.dirty ? t("settings.unsaved") : "";
79
+ }
80
+ function listen() {
81
+ control.addEventListener("input", mark);
82
+ control.addEventListener("change", () => { mark(); item.onChange?.(); });
83
+ }
84
+ reset.addEventListener("change", mark);
85
+ const hint = h("small");
86
+ const choiceActions = h("div.settings-actions");
87
+ row.append(choiceActions, hint);
88
+ const item = { row, get control() { return control; }, field,
89
+ get value() { return reset.checked ? "" : control.value; },
90
+ get blocked() { return unavailable && !reset.checked && control.value !== "" && control.value !== initial; },
91
+ changed: () => reset.checked || control.value !== initial,
92
+ change: () => reset.checked || (["model", "effort"].includes(field.key) && field.reset && control.value === "")
93
+ ? { key: field.key, reset: true } : { key: field.key, value: inputValue(field, control) },
94
+ setChoices(choices, options = {}) {
95
+ const value = control.value;
96
+ unavailable = options.available === false || (!choices.length && options.allowCustom === false);
97
+ const values = choices.map(c => c.value);
98
+ const selected = h("select", null, h("option", { value: "" }, t("settings.inherit")),
99
+ choices.map(c => h("option", { value: c.value }, c.label)));
100
+ if (value && !values.includes(value)) selected.append(h("option", { value }, value + " · " + t("settings.notListed")));
101
+ control = choices.length ? selected : h("input");
102
+ control.value = value;
103
+ control.disabled = reset.checked || unavailable;
104
+ listen();
105
+ label.replaceChildren(h("span", null, labelText), control);
106
+ choiceActions.replaceChildren();
107
+ if (choices.length && options.allowCustom && !unavailable) {
108
+ const custom = h("button.btn", { type: "button" }, t("settings.custom"));
109
+ custom.addEventListener("click", () => {
110
+ const value = control.value;
111
+ control = h("input", { value });
112
+ control.disabled = reset.checked;
113
+ listen(); label.replaceChildren(h("span", null, labelText), control); choiceActions.replaceChildren();
114
+ control.focus();
115
+ });
116
+ choiceActions.append(custom);
117
+ }
118
+ hint.textContent = (options.reason || "") +
119
+ (value && !values.includes(value) ? " " + t("settings.notListedHelp") : "");
120
+ }
121
+ };
122
+ reset.addEventListener("change", () => item.onChange?.());
123
+ listen();
124
+ return item;
125
+ }
126
+
127
+ function applyCapabilities(state, result) {
128
+ const catalog = result.catalog;
129
+ const model = state.editors.find(e => e.field.key === "model");
130
+ const effort = state.editors.find(e => e.field.key === "effort");
131
+ if (!model) return;
132
+ const modelField = catalog.fields.find(f => f.key === "model");
133
+ model.setChoices(catalog.models, modelField || { allowCustom: true });
134
+ function updateEffort() {
135
+ if (!effort) return;
136
+ const selected = !model.value ? catalog.models.find(m => m.isDefault)
137
+ : catalog.models.find(m => m.value === model.value)
138
+ || catalog.models.find(m => m.resolvedModel === model.value && !m.isDefault)
139
+ || catalog.models.find(m => m.resolvedModel === model.value);
140
+ const field = catalog.fields.find(f => f.key === "effort");
141
+ const choices = selected?.efforts.length ? selected.efforts : field?.choices || [];
142
+ effort.setChoices(choices, {
143
+ allowCustom: !choices.length && field?.allowCustom !== false,
144
+ available: choices.length ? true : field?.available,
145
+ reason: selected?.defaultEffort ? t("settings.nativeEffort") + ": " + selected.defaultEffort : field?.reason
146
+ });
147
+ }
148
+ model.onChange = updateEffort;
149
+ updateEffort();
150
+ }
151
+
152
+ function renderGroup(state, group) {
153
+ const generation = state.renderGeneration = (state.renderGeneration || 0) + 1;
154
+ state.group = group; state.editors = []; state.dirty = false; state.unknown = false;
155
+ state.capabilityPending = false;
156
+ const form = h("form.settings-stack");
157
+ const receipt = h("p.settings-receipt", { role: "status", "aria-live": "polite" });
158
+ const save = h("button.btn.btn-primary", { type: "submit", disabled: true }, t("settings.save"));
159
+ state.save = save; state.receipt = receipt;
160
+ form.append(h("p", null, group.notice));
161
+ const isRole = group.id.startsWith("role/");
162
+ if (isRole) form.append(h("p", null, "Agent: " + group.agentId + " · " + t("settings.roleScope")));
163
+ const advanced = h("details", { open: !!search.value.trim() }, h("summary", null, t("settings.advancedFields")));
164
+ const common = h("div.settings-grid");
165
+ group.fields.forEach(field => {
166
+ const item = editor(field, group, state);
167
+ state.editors.push(item);
168
+ if (isRole) (["model", "effort"].includes(field.key) ? common : advanced).append(item.row);
169
+ else form.append(item.row);
170
+ });
171
+ if (isRole) form.append(common, advanced);
172
+ const acknowledge = h("input", { type: "checkbox" });
173
+ if (group.id.startsWith("role/") || group.id.startsWith("agent/")) form.append(h("label.settings-reset", null,
174
+ acknowledge, t("settings.acknowledge")));
175
+ const compare = h("button.btn", { type: "button" }, t("settings.compare"));
176
+ compare.addEventListener("click", async () => {
177
+ compare.disabled = true;
178
+ try {
179
+ const current = await requestJson("/api/settings/group?" + new URLSearchParams({ id: group.id }));
180
+ const values = h("div.settings-observation", null,
181
+ h("p", null, current.notice),
182
+ current.fields.map(field => h("p", null,
183
+ t("settings.field." + field.key, field.label) + ": " +
184
+ (field.value === "" ? t("settings.inherit") : format(field.value)) +
185
+ (field.source ? " · " + t("settings.source." + field.source, field.source) : ""))));
186
+ const agentChanged = current.agentId !== group.agentId;
187
+ if (agentChanged) values.append(h("p.settings-error", null, t("settings.agentChanged")));
188
+ const adopt = h("button.btn", { type: "button", disabled: state.unknown || agentChanged }, t("settings.useBaseline"));
189
+ adopt.addEventListener("click", () => {
190
+ state.group = current;
191
+ receipt.textContent = t("settings.baselineChanged");
192
+ values.remove(); adopt.remove();
193
+ });
194
+ state.content.append(values, adopt);
195
+ } catch (error) { receipt.textContent = error.message; }
196
+ finally { compare.disabled = false; }
197
+ });
198
+ form.append(h("div.settings-actions", null, save, compare), receipt);
199
+ form.addEventListener("submit", async event => {
200
+ event.preventDefault();
201
+ if (state.pending || state.capabilityPending || state.unknown) return;
202
+ let changes;
203
+ if (state.editors.some(e => e.blocked)) {
204
+ receipt.textContent = t("settings.unsupportedDraft"); return;
205
+ }
206
+ try { changes = state.editors.filter(e => e.changed()).map(e => e.change()); }
207
+ catch (error) { receipt.textContent = error.message; receipt.classList.add("settings-error"); return; }
208
+ if (!changes.length) return;
209
+ state.pending = true; save.disabled = true;
210
+ // Keep controls frozen during one submission. Failed saves keep all drafts.
211
+ const controls = [...form.querySelectorAll("input,select,textarea,button")];
212
+ const disabled = controls.map(c => c.disabled);
213
+ controls.forEach(c => { c.disabled = true; });
214
+ try {
215
+ const result = await submitMutation("settings/" + group.id, "/api/settings/group", {
216
+ id: group.id, revision: state.group.revision, changes, acknowledgeLive: acknowledge.checked
217
+ });
218
+ renderGroup(state, result.group);
219
+ state.receipt.textContent = t("settings.saved") + "\n" + result.output + "\n" + result.adoption + "\n" + result.refresh;
220
+ } catch (error) {
221
+ state.unknown = error.disposition !== "not-submitted";
222
+ receipt.textContent = error.message + "\n" + (state.unknown ? t("settings.unknown") : t("settings.draftKept"));
223
+ receipt.classList.add("settings-error");
224
+ controls.forEach((c, i) => { c.disabled = disabled[i]; });
225
+ } finally {
226
+ state.pending = false;
227
+ state.save.disabled = !state.dirty || state.unknown;
228
+ }
229
+ });
230
+ if (group.observation) form.append(h("details", null, h("summary", null, t("settings.observation")),
231
+ h("pre.settings-observation", null, JSON.stringify(group.observation, null, 2))));
232
+ if (group.agentId) {
233
+ const info = h("p", { role: "status" }, t("settings.notQueried"));
234
+ const query = h("button.btn", { type: "button" }, t("settings.queryCapabilities"));
235
+ const refresh = h("button.btn", { type: "button" }, t("settings.refreshCapabilities"));
236
+ const output = h("div");
237
+ async function load(force) {
238
+ if (state.pending || state.capabilityPending) return;
239
+ if (state.editors.some(e => e.field.key === "activeAgentId" && e.changed())) {
240
+ info.textContent = t("settings.saveAgentFirst"); return;
241
+ }
242
+ state.capabilityPending = true; save.disabled = true;
243
+ query.disabled = refresh.disabled = true; info.textContent = t("settings.querying");
244
+ try {
245
+ const result = await requestJson("/api/settings/capabilities?" + new URLSearchParams({ id: group.id, refresh: String(force) }));
246
+ if (state.renderGeneration !== generation) return;
247
+ if (state.pending) { info.textContent = t("settings.notQueried"); return; }
248
+ if (result.catalog.agentId !== group.agentId) {
249
+ info.textContent = t("settings.agentChanged"); return;
250
+ }
251
+ if (state.editors.some(e => e.field.key === "activeAgentId" && e.changed())) {
252
+ info.textContent = t("settings.saveAgentFirst"); return;
253
+ }
254
+ info.textContent = t("settings.capability." + result.source, result.source) + " · " + (result.fetchedAt || result.attemptedAt) +
255
+ (result.failure ? " · " + result.failure.code + ": " + result.failure.message : "");
256
+ applyCapabilities(state, result);
257
+ output.replaceChildren(
258
+ ...result.catalog.warnings.map(w => h("p", null, w)),
259
+ h("details", null, h("summary", null, t("settings.capabilityDetails")),
260
+ h("pre.settings-observation", null, JSON.stringify(result.catalog, null, 2))));
261
+ } catch (error) { info.textContent = t("settings.queryFailed") + ": " + error.message; }
262
+ finally {
263
+ query.disabled = refresh.disabled = false;
264
+ if (state.renderGeneration === generation) {
265
+ state.capabilityPending = false;
266
+ state.save.disabled = !state.dirty || state.pending || state.unknown;
267
+ }
268
+ }
269
+ }
270
+ query.addEventListener("click", () => load(false)); refresh.addEventListener("click", () => load(true));
271
+ const capabilities = h("div", null, h("div.settings-actions", null, query, refresh), info, output);
272
+ if (isRole) common.before(capabilities);
273
+ else form.append(capabilities);
274
+ }
275
+ state.content.replaceChildren(form);
276
+ }
277
+
278
+ async function loadGroup(state) {
279
+ if (state.loaded || state.loading) return;
280
+ state.loading = true;
281
+ state.content.textContent = t("settings.loading");
282
+ try {
283
+ renderGroup(state, await requestJson("/api/settings/group?" + new URLSearchParams({ id: state.meta.id })));
284
+ state.loaded = true;
285
+ } catch (error) {
286
+ state.content.replaceChildren(h("p.settings-error", null, error.message),
287
+ h("button.btn", { type: "button", onclick: () => loadGroup(state) }, t("settings.retry")));
288
+ } finally { state.loading = false; }
289
+ }
290
+ search.addEventListener("input", () => {
291
+ searchGeneration++;
292
+ clearTimeout(searchTimer);
293
+ searchTimer = setTimeout(() => start(), 200);
294
+ });
295
+ async function start(cursor) {
296
+ const generation = ++searchGeneration;
297
+ const query = search.value.trim();
298
+ status.textContent = t("settings.loading");
299
+ more.disabled = true;
300
+ try {
301
+ const data = await requestJson("/api/settings?" + new URLSearchParams({ q: query, cursor: cursor || "0" }));
302
+ if (generation !== searchGeneration) return;
303
+ if (!cursor) groups.forEach(state => { state.root.hidden = true; });
304
+ data.groups.forEach(meta => {
305
+ const existing = groups.find(g => g.meta.id === meta.id);
306
+ if (existing) {
307
+ existing.root.hidden = false;
308
+ if (query) { existing.root.open = true; void loadGroup(existing); }
309
+ return;
310
+ }
311
+ const content = h("div");
312
+ const root = h("details.settings-card", { open: meta.id === "system" || !!query },
313
+ h("summary", null, t("settings.section." + meta.section, meta.section) + " · " + meta.title), content);
314
+ const state = { meta, root, content, dirty: false, loaded: false, loading: false };
315
+ groups.push(state); container.append(root);
316
+ root.addEventListener("toggle", () => { if (root.open) void loadGroup(state); });
317
+ if (root.open) void loadGroup(state);
318
+ });
319
+ status.textContent = data.total ? String(data.total) + " " + t("settings.groups") : t("settings.noMatches");
320
+ more.hidden = !data.nextCursor;
321
+ more.onclick = () => start(data.nextCursor);
322
+ } catch (error) { status.textContent = error.message; }
323
+ finally { if (generation === searchGeneration) more.disabled = false; }
324
+ }
325
+ void start();
326
+ `;