@harness-mix/cli 0.1.3 → 0.1.5

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 (126) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +26 -96
  3. package/docs/chatgpt-web-sidebar.md +7 -1
  4. package/docs/cross-platform.md +15 -0
  5. package/docs/harness-handoff-design.md +377 -0
  6. package/docs/harness-management.md +28 -1
  7. package/docs/native-codex.md +21 -10
  8. package/output/native-build/harness-mix-shim.exe +0 -0
  9. package/output/native-build/renderer-extension.js +972 -35
  10. package/package.json +21 -3
  11. package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
  12. package/scripts/codex-accounts-test.cjs +81 -0
  13. package/scripts/codex-accounts-ui-smoke.cjs +70 -0
  14. package/scripts/e2e-integrations.cjs +52 -0
  15. package/scripts/e2e-native-host.cjs +39 -0
  16. package/scripts/handoff-checkpoint-test.cjs +83 -0
  17. package/scripts/install-native.cjs +29 -0
  18. package/scripts/integrations-test.cjs +64 -0
  19. package/scripts/integrations-ui-smoke.cjs +63 -0
  20. package/scripts/native-protocol-test.cjs +78 -4
  21. package/scripts/native-secret-test.cjs +2 -2
  22. package/scripts/native-shim-test.cjs +20 -4
  23. package/scripts/native-ui-smoke.cjs +58 -2
  24. package/scripts/publish-native-package.cjs +37 -0
  25. package/scripts/support/integrations-mcp-fixture.cjs +15 -0
  26. package/scripts/switch-harness-test.cjs +27 -1
  27. package/src/main/adapters/acp.js +3 -3
  28. package/src/main/adapters/antigravity.js +10 -13
  29. package/src/main/adapters/claude.js +10 -3
  30. package/src/main/adapters/codex-app-server.js +20 -13
  31. package/src/main/adapters/codex.js +6 -4
  32. package/src/main/adapters/dsh.js +2 -1
  33. package/src/main/adapters/grok.js +4 -3
  34. package/src/main/adapters/managed-mcp.js +12 -0
  35. package/src/main/adapters/native-acp.js +4 -4
  36. package/src/main/adapters/opencode.js +9 -3
  37. package/src/main/adapters/pi.js +1 -0
  38. package/src/main/harness-adapter/manifest.js +17 -2
  39. package/src/main/host/handoff-access.js +53 -0
  40. package/src/main/host/handoff-checkpoints.js +206 -0
  41. package/src/main/host/handoff-mcp.cjs +38 -0
  42. package/src/main/host/handoff-tools.js +13 -0
  43. package/src/main/host/handoff.js +28 -6
  44. package/src/main/host/integrations.js +201 -0
  45. package/src/main/host/runtime.js +75 -35
  46. package/src/main/host/store.js +4 -4
  47. package/src/main/native/codex-accounts.js +260 -0
  48. package/src/main/native/host.js +1 -1
  49. package/src/main/native/launcher.js +2 -2
  50. package/src/main/native/protocol.js +74 -23
  51. package/src/main/native/redact.js +1 -1
  52. package/src/main/native/rs/crates/shim/src/main.rs +155 -12
  53. package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
  54. package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
  55. package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
  56. package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
  57. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
  58. package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
  59. package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
  60. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
  61. package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
  62. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
  63. package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
  64. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
  65. package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
  66. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
  67. package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
  68. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
  69. package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
  70. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
  71. package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
  72. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
  73. package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
  74. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
  75. package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
  76. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
  77. package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
  78. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
  79. package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
  80. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
  81. package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
  82. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
  83. package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
  84. package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -1
  86. package/src/native-ui/renderer-extension/src/index.ts +28 -0
  87. package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
  88. package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
  89. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
  90. package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
  91. package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
  92. package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
  93. package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
  94. package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
  95. package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
  96. package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
  97. package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
  98. package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
  99. package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
  100. package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
  101. package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
  102. package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
  103. package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
  104. package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
  105. package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
  106. package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
  107. package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
  108. package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
  109. package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
  110. package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
  111. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
  112. package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
  113. package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
  114. package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
  115. package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
  116. package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
  117. package/src/native-ui/shared-contracts/dist/index.js +1 -1
  118. package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
  119. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
  120. package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
  121. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
  122. package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
  123. package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
  124. package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
  125. package/src/native-ui/shared-contracts/src/index.ts +8 -1
  126. package/src/native-ui/shared-contracts/src/thread-harness-switch.ts +17 -0
@@ -0,0 +1,330 @@
1
+ import type { ExternalRendererAgent } from "./agent-selection-state.js";
2
+ import type { HarnessHandoffIncludes, HarnessHandoffIntent } from "@codexhost/shared-contracts";
3
+ import { createRendererAgentIcon, RENDERER_AGENT_LABELS } from "./renderer-agent-icon.js";
4
+ import type { RendererSettingsLocale } from "./settings/localization.js";
5
+
6
+ export const HARNESS_HANDOFF_NOTE_MAX_LENGTH = 2000;
7
+
8
+ export interface RendererHarnessHandoffRequest {
9
+ from: ExternalRendererAgent;
10
+ to: ExternalRendererAgent;
11
+ note: string;
12
+ intent: HarnessHandoffIntent;
13
+ includes: HarnessHandoffIncludes;
14
+ }
15
+
16
+ export interface RendererHarnessHandoffControl {
17
+ dialog: HTMLDialogElement;
18
+ note: HTMLTextAreaElement;
19
+ intent: HTMLSelectElement;
20
+ includeInputs: Record<keyof HarnessHandoffIncludes, HTMLInputElement>;
21
+ confirm: HTMLButtonElement;
22
+ cancel: HTMLButtonElement;
23
+ error: HTMLElement;
24
+ open(from: ExternalRendererAgent, to: ExternalRendererAgent, locale: RendererSettingsLocale): void;
25
+ close(): void;
26
+ setSubmitting(submitting: boolean): void;
27
+ showError(message: string): void;
28
+ dispose(): void;
29
+ }
30
+
31
+ export function rendererHarnessHandoffMessages(locale: RendererSettingsLocale) {
32
+ return locale === "zh-CN"
33
+ ? {
34
+ title: "接力当前任务",
35
+ description: "保留此任务窗口、对话记录和文件现场,只更换继续执行的 Harness。",
36
+ noteLabel: "交接说明(可选)",
37
+ intentLabel: "接力方式",
38
+ intentOptions: { continue: "继续执行", "execute-plan": "执行上一方案", review: "独立审查", reanalyze: "重新分析" },
39
+ includeLabel: "将要交接",
40
+ includeOptions: { conversation: "近期对话", plan: "当前计划", evidence: "测试与工具证据", files: "文件变更", unresolved: "未解决问题" },
41
+ notePlaceholder: "例如:按上面的方案继续实现,先核对当前文件状态和未完成步骤",
42
+ context: "下一条消息会自动携带近期对话、最新方案、计划和变更文件。",
43
+ running: "若当前回合仍在运行,请先停止或等待完成。",
44
+ cancel: "取消",
45
+ confirm: "确认接力",
46
+ submitting: "正在切换…",
47
+ }
48
+ : {
49
+ title: "Hand off this task",
50
+ description: "Keep this task, transcript, and working tree; only change the Harness that continues it.",
51
+ noteLabel: "Handoff note (optional)",
52
+ intentLabel: "Handoff mode",
53
+ intentOptions: { continue: "Continue", "execute-plan": "Execute prior plan", review: "Independent review", reanalyze: "Reanalyze" },
54
+ includeLabel: "Include",
55
+ includeOptions: { conversation: "Recent conversation", plan: "Current plan", evidence: "Test and tool evidence", files: "File changes", unresolved: "Open issues" },
56
+ notePlaceholder: "For example: implement the plan above; verify the working tree and remaining steps first",
57
+ context: "Your next message will include recent context, the latest solution, plan, and changed files.",
58
+ running: "If a turn is still running, stop it or wait for it to finish first.",
59
+ cancel: "Cancel",
60
+ confirm: "Hand off",
61
+ submitting: "Switching…",
62
+ };
63
+ }
64
+
65
+ function setButtonChrome(button: HTMLButtonElement, primary = false): void {
66
+ button.style.height = "34px";
67
+ button.style.padding = "0 14px";
68
+ button.style.border = primary ? "1px solid color-mix(in srgb, #5b8cff 70%, CanvasText 30%)" : "1px solid color-mix(in srgb, CanvasText 18%, transparent)";
69
+ button.style.borderRadius = "7px";
70
+ button.style.background = primary ? "#4f7ff0" : "color-mix(in srgb, Canvas 94%, CanvasText 6%)";
71
+ button.style.color = primary ? "white" : "CanvasText";
72
+ button.style.font = "500 13px/1 system-ui, sans-serif";
73
+ button.style.cursor = "pointer";
74
+ }
75
+
76
+ export function mountRendererHarnessHandoff(
77
+ composerId: string,
78
+ onConfirm: (request: RendererHarnessHandoffRequest) => void,
79
+ ): RendererHarnessHandoffControl {
80
+ const dialog = document.createElement("dialog");
81
+ dialog.id = `${composerId}-harness-handoff`;
82
+ dialog.setAttribute("aria-modal", "true");
83
+ dialog.style.width = "min(440px, calc(100vw - 32px))";
84
+ dialog.style.boxSizing = "border-box";
85
+ dialog.style.padding = "0";
86
+ dialog.style.border = "1px solid color-mix(in srgb, CanvasText 18%, transparent)";
87
+ dialog.style.borderRadius = "14px";
88
+ dialog.style.background = "Canvas";
89
+ dialog.style.color = "CanvasText";
90
+ dialog.style.boxShadow = "0 24px 70px rgba(0, 0, 0, 0.42)";
91
+ dialog.style.font = "13px/1.45 system-ui, sans-serif";
92
+ dialog.style.zIndex = "2147483647";
93
+
94
+ const frame = document.createElement("div");
95
+ frame.style.display = "flex";
96
+ frame.style.flexDirection = "column";
97
+ frame.style.gap = "14px";
98
+ frame.style.padding = "20px";
99
+
100
+ const title = document.createElement("h2");
101
+ title.style.margin = "0";
102
+ title.style.font = "600 17px/1.25 system-ui, sans-serif";
103
+ const description = document.createElement("p");
104
+ description.style.margin = "-6px 0 0";
105
+ description.style.opacity = "0.72";
106
+
107
+ const route = document.createElement("div");
108
+ route.style.display = "grid";
109
+ route.style.gridTemplateColumns = "1fr auto 1fr";
110
+ route.style.alignItems = "center";
111
+ route.style.gap = "10px";
112
+ route.style.padding = "12px";
113
+ route.style.borderRadius = "10px";
114
+ route.style.background = "color-mix(in srgb, CanvasText 6%, transparent)";
115
+ const fromBox = document.createElement("div");
116
+ const toBox = document.createElement("div");
117
+ for (const box of [fromBox, toBox]) {
118
+ box.style.display = "flex";
119
+ box.style.alignItems = "center";
120
+ box.style.gap = "8px";
121
+ box.style.minWidth = "0";
122
+ box.style.fontWeight = "600";
123
+ }
124
+ const arrow = document.createElement("span");
125
+ arrow.textContent = "→";
126
+ arrow.setAttribute("aria-hidden", "true");
127
+ arrow.style.opacity = "0.55";
128
+ route.append(fromBox, arrow, toBox);
129
+
130
+ const intentLabel = document.createElement("label");
131
+ intentLabel.style.display = "flex";
132
+ intentLabel.style.flexDirection = "column";
133
+ intentLabel.style.gap = "7px";
134
+ const intentLabelText = document.createElement("span");
135
+ intentLabelText.style.fontWeight = "600";
136
+ const intent = document.createElement("select");
137
+ intent.style.height = "36px";
138
+ intent.style.padding = "0 10px";
139
+ intent.style.border = "1px solid color-mix(in srgb, CanvasText 20%, transparent)";
140
+ intent.style.borderRadius = "8px";
141
+ intent.style.background = "Canvas";
142
+ intent.style.color = "CanvasText";
143
+ intent.style.font = "13px/1 system-ui, sans-serif";
144
+ for (const value of ["continue", "execute-plan", "review", "reanalyze"] as const) {
145
+ const option = document.createElement("option");
146
+ option.value = value;
147
+ intent.append(option);
148
+ }
149
+ intentLabel.append(intentLabelText, intent);
150
+
151
+ const includeSection = document.createElement("fieldset");
152
+ includeSection.style.margin = "0";
153
+ includeSection.style.padding = "10px 11px";
154
+ includeSection.style.border = "1px solid color-mix(in srgb, CanvasText 14%, transparent)";
155
+ includeSection.style.borderRadius = "8px";
156
+ const includeLegend = document.createElement("legend");
157
+ includeLegend.style.padding = "0 5px";
158
+ includeLegend.style.fontWeight = "600";
159
+ includeSection.append(includeLegend);
160
+ const includeGrid = document.createElement("div");
161
+ includeGrid.style.display = "grid";
162
+ includeGrid.style.gridTemplateColumns = "repeat(2, minmax(0, 1fr))";
163
+ includeGrid.style.gap = "8px 12px";
164
+ const includeInputs = {} as Record<keyof HarnessHandoffIncludes, HTMLInputElement>;
165
+ const includeLabels = {} as Record<keyof HarnessHandoffIncludes, Text>;
166
+ for (const key of ["conversation", "plan", "evidence", "files", "unresolved"] as const) {
167
+ const row = document.createElement("label");
168
+ row.style.display = "flex";
169
+ row.style.alignItems = "center";
170
+ row.style.gap = "7px";
171
+ const input = document.createElement("input");
172
+ input.type = "checkbox";
173
+ input.checked = true;
174
+ const text = document.createTextNode("");
175
+ row.append(input, text);
176
+ includeGrid.append(row);
177
+ includeInputs[key] = input;
178
+ includeLabels[key] = text;
179
+ }
180
+ includeSection.append(includeGrid);
181
+
182
+ const label = document.createElement("label");
183
+ label.style.display = "flex";
184
+ label.style.flexDirection = "column";
185
+ label.style.gap = "7px";
186
+ const labelText = document.createElement("span");
187
+ labelText.style.fontWeight = "600";
188
+ const note = document.createElement("textarea");
189
+ note.rows = 3;
190
+ note.maxLength = HARNESS_HANDOFF_NOTE_MAX_LENGTH;
191
+ note.style.boxSizing = "border-box";
192
+ note.style.width = "100%";
193
+ note.style.minHeight = "76px";
194
+ note.style.resize = "vertical";
195
+ note.style.padding = "10px 11px";
196
+ note.style.border = "1px solid color-mix(in srgb, CanvasText 20%, transparent)";
197
+ note.style.borderRadius = "8px";
198
+ note.style.outline = "none";
199
+ note.style.background = "color-mix(in srgb, Canvas 96%, CanvasText 4%)";
200
+ note.style.color = "CanvasText";
201
+ note.style.font = "13px/1.45 system-ui, sans-serif";
202
+ label.append(labelText, note);
203
+
204
+ const context = document.createElement("p");
205
+ context.style.margin = "0";
206
+ context.style.padding = "9px 10px";
207
+ context.style.borderRadius = "8px";
208
+ context.style.background = "color-mix(in srgb, #5b8cff 10%, transparent)";
209
+ const running = document.createElement("p");
210
+ running.style.margin = "-8px 0 0";
211
+ running.style.opacity = "0.65";
212
+
213
+ const error = document.createElement("p");
214
+ error.setAttribute("role", "alert");
215
+ error.hidden = true;
216
+ error.style.margin = "-8px 0 0";
217
+ error.style.color = "#ef6666";
218
+
219
+ const actions = document.createElement("div");
220
+ actions.style.display = "flex";
221
+ actions.style.justifyContent = "flex-end";
222
+ actions.style.gap = "8px";
223
+ const cancel = document.createElement("button");
224
+ cancel.type = "button";
225
+ setButtonChrome(cancel);
226
+ const confirm = document.createElement("button");
227
+ confirm.type = "button";
228
+ setButtonChrome(confirm, true);
229
+ actions.append(cancel, confirm);
230
+ frame.append(title, description, route, intentLabel, includeSection, label, context, running, error, actions);
231
+ dialog.append(frame);
232
+ document.body.append(dialog);
233
+
234
+ let from: ExternalRendererAgent = "pi";
235
+ let to: ExternalRendererAgent = "claude-code";
236
+ let locale: RendererSettingsLocale = "en";
237
+ let submitting = false;
238
+ const render = (): void => {
239
+ const messages = rendererHarnessHandoffMessages(locale);
240
+ title.textContent = messages.title;
241
+ description.textContent = messages.description;
242
+ labelText.textContent = messages.noteLabel;
243
+ intentLabelText.textContent = messages.intentLabel;
244
+ includeLegend.textContent = messages.includeLabel;
245
+ for (const [value, text] of Object.entries(messages.intentOptions)) {
246
+ const option = [...intent.options].find(candidate => candidate.value === value);
247
+ if (option) option.textContent = text;
248
+ }
249
+ for (const key of Object.keys(includeInputs) as Array<keyof HarnessHandoffIncludes>) includeLabels[key].data = messages.includeOptions[key];
250
+ note.placeholder = messages.notePlaceholder;
251
+ context.textContent = messages.context;
252
+ running.textContent = messages.running;
253
+ cancel.textContent = messages.cancel;
254
+ confirm.textContent = submitting ? messages.submitting : messages.confirm;
255
+ fromBox.replaceChildren(createRendererAgentIcon(from, 24), document.createTextNode(RENDERER_AGENT_LABELS[from]));
256
+ toBox.replaceChildren(createRendererAgentIcon(to, 24), document.createTextNode(RENDERER_AGENT_LABELS[to]));
257
+ note.disabled = submitting;
258
+ intent.disabled = submitting;
259
+ includeSection.disabled = submitting;
260
+ cancel.disabled = submitting;
261
+ confirm.disabled = submitting;
262
+ cancel.style.cursor = submitting ? "not-allowed" : "pointer";
263
+ confirm.style.cursor = submitting ? "wait" : "pointer";
264
+ confirm.setAttribute("aria-busy", String(submitting));
265
+ };
266
+ const close = (): void => {
267
+ if (submitting) return;
268
+ if (dialog.open) dialog.close();
269
+ };
270
+ cancel.addEventListener("click", close);
271
+ confirm.addEventListener("click", () => {
272
+ if (submitting) return;
273
+ onConfirm({
274
+ from,
275
+ to,
276
+ note: note.value.trim(),
277
+ intent: intent.value as HarnessHandoffIntent,
278
+ includes: Object.fromEntries(Object.entries(includeInputs).map(([key, input]) => [key, input.checked])) as unknown as HarnessHandoffIncludes,
279
+ });
280
+ });
281
+ note.addEventListener("keydown", (event) => {
282
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter" && !submitting) {
283
+ event.preventDefault();
284
+ confirm.click();
285
+ }
286
+ });
287
+ dialog.addEventListener("cancel", (event) => {
288
+ if (submitting) event.preventDefault();
289
+ });
290
+
291
+ const control: RendererHarnessHandoffControl = {
292
+ dialog,
293
+ note,
294
+ intent,
295
+ includeInputs,
296
+ confirm,
297
+ cancel,
298
+ error,
299
+ open(nextFrom, nextTo, nextLocale) {
300
+ from = nextFrom;
301
+ to = nextTo;
302
+ locale = nextLocale;
303
+ submitting = false;
304
+ note.value = "";
305
+ intent.value = "continue";
306
+ for (const input of Object.values(includeInputs)) input.checked = true;
307
+ error.hidden = true;
308
+ error.textContent = "";
309
+ render();
310
+ if (!dialog.open) dialog.showModal();
311
+ queueMicrotask(() => note.focus());
312
+ },
313
+ close,
314
+ setSubmitting(nextSubmitting) {
315
+ submitting = nextSubmitting;
316
+ render();
317
+ },
318
+ showError(message) {
319
+ error.textContent = message;
320
+ error.hidden = false;
321
+ },
322
+ dispose() {
323
+ submitting = false;
324
+ if (dialog.open) dialog.close();
325
+ dialog.remove();
326
+ },
327
+ };
328
+ render();
329
+ return control;
330
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+
3
+ const serverSchema = z.object({ id: z.string(), name: z.string(), command: z.string(), args: z.array(z.string()), enabled: z.boolean(), scope: z.enum(['global', 'project']), editable: z.boolean(), effective: z.boolean(), appliedSessions: z.number() });
4
+ const skillSchema = z.object({ name: z.string(), path: z.string(), root: z.string(), enabled: z.boolean(), writable: z.boolean(), scope: z.enum(['global', 'project']), status: z.string() });
5
+ const snapshotSchema = z.object({ harnessId: z.string(), mcpSupported: z.boolean(), skillsSupported: z.boolean(), servers: z.array(serverSchema), native: z.array(z.object({ sessionId: z.string(), name: z.string(), status: z.string(), tools: z.array(z.string()) })), skills: z.array(skillSchema), note: z.string() });
6
+ const catalogSchema = z.object({ harnesses: z.array(z.object({ id: z.string(), name: z.string(), available: z.boolean(), mcp: z.boolean(), skills: z.boolean() })) });
7
+ export type IntegrationSnapshot = z.infer<typeof snapshotSchema>;
8
+ export interface IntegrationScope { harnessId: string; scope: 'global' | 'project'; cwd?: string }
9
+ export interface ManagedServer { name: string; command: string; args: string[]; enabled: boolean }
10
+ export interface SkillChange { name: string; root?: string; source?: string; action: 'install' | 'enable' | 'disable' }
11
+ export interface RendererIntegrationsClient {
12
+ integrationCatalog(): Promise<z.infer<typeof catalogSchema>>;
13
+ listIntegrations(input: IntegrationScope): Promise<IntegrationSnapshot>;
14
+ saveMcp(input: IntegrationScope & { server: ManagedServer }): Promise<unknown>;
15
+ removeMcp(input: IntegrationScope & { id: string }): Promise<unknown>;
16
+ changeSkill(input: IntegrationScope & SkillChange): Promise<unknown>;
17
+ }
18
+ export function createRendererIntegrationsClient(send: (method: string, params: unknown) => Promise<unknown>): RendererIntegrationsClient {
19
+ return {
20
+ integrationCatalog: async () => catalogSchema.parse(await send('codexhost/integrations/catalog', {})),
21
+ listIntegrations: async input => snapshotSchema.parse(await send('codexhost/integrations/list', input)),
22
+ saveMcp: input => send('codexhost/integrations/mcp/save', input),
23
+ removeMcp: input => send('codexhost/integrations/mcp/remove', input),
24
+ changeSkill: input => send('codexhost/integrations/skill/change', input),
25
+ };
26
+ }
@@ -1,3 +1,4 @@
1
+ import { createRendererIntegrationsClient, type RendererIntegrationsClient } from './renderer-integrations-client.js';
1
2
  import {
2
3
  harnessAccountListResultSchema,
3
4
  type HarnessAccountListResult,
@@ -127,6 +128,7 @@ export const THREAD_TOKEN_USAGE_UPDATED_METHOD = "thread/tokenUsage/updated";
127
128
  export const UPDATE_CHECK_METHOD = "codexhost/update/check";
128
129
  export const UPDATE_START_METHOD = "codexhost/update/start";
129
130
  export const UPDATE_STATUS_METHOD = "codexhost/update/status";
131
+ export const RUNTIME_VERSION_METHOD = "harness-mix/runtime/version";
130
132
  export const CODEX_ACCOUNT_LIST_METHOD = "codexhost/account/list";
131
133
  export const CODEX_ACCOUNT_REFRESH_METHOD = "codexhost/account/refresh";
132
134
  export const CODEX_ACCOUNT_CREATE_METHOD = "codexhost/account/create";
@@ -171,7 +173,7 @@ function notificationTarget(manager: RequestManagerCandidate): RequestManagerCan
171
173
  return nested && typeof nested.addNotificationCallback === "function" ? nested : null;
172
174
  }
173
175
 
174
- export interface RendererModelClient extends Partial<RendererSessionImportClient> {
176
+ export interface RendererModelClient extends Partial<RendererSessionImportClient>, Partial<RendererIntegrationsClient> {
175
177
  listCollaborationAgents?(): Promise<Array<{ id: string; name: string; available: boolean; lead: boolean }>>;
176
178
  currentHostId?(): string | null;
177
179
  listHarnessPlugins?(): Promise<HarnessPluginListResult>;
@@ -197,6 +199,7 @@ export interface RendererModelClient extends Partial<RendererSessionImportClient
197
199
  checkUpdate(): Promise<UpdateCheckResult>;
198
200
  startUpdate(): Promise<UpdateStartResult>;
199
201
  readUpdateStatus(): Promise<UpdateStatusResult>;
202
+ readCurrentVersion?(): Promise<{ version: string }>;
200
203
  inspectCodexAccountUsage?(input: CodexAccountUsageParams): Promise<CodexAccountUsageResult>;
201
204
  consumeCodexAccountResetCredit?(
202
205
  input: CodexAccountResetCreditConsumeParams,
@@ -213,6 +216,7 @@ export interface RendererModelClient extends Partial<RendererSessionImportClient
213
216
  cancelCodexAccountLogin(
214
217
  input: CodexAccountLoginCancelParams,
215
218
  ): Promise<CodexAccountLoginCancelResult>;
219
+ logoutCodexAccount?(): Promise<CodexAccountMutationResult>;
216
220
  subscribeCodexAccountLogin(listener: (result: CodexAccountLoginCompleted) => void): () => void;
217
221
  }
218
222
 
@@ -331,6 +335,7 @@ export function createRendererModelClient(
331
335
  if (!Array.isArray(result) || !result.every(a => a && typeof a.id === 'string' && typeof a.name === 'string' && typeof a.available === 'boolean' && typeof a.lead === 'boolean')) throw new Error('Invalid collaboration catalog');
332
336
  return result;
333
337
  },
338
+ ...createRendererIntegrationsClient((method, params) => manager.sendRequest(method, params)),
334
339
  ...createRendererSessionImportClient(async (method, params) =>
335
340
  manager.sendRequest(method, params),
336
341
  ),
@@ -477,6 +482,13 @@ export function createRendererModelClient(
477
482
  );
478
483
  return updateStatusResultSchema.parse(result);
479
484
  },
485
+ async readCurrentVersion(): Promise<{ version: string }> {
486
+ const result = await manager.sendRequest(RUNTIME_VERSION_METHOD, {});
487
+ if (!isRecord(result) || typeof result.version !== "string" || !result.version.trim()) {
488
+ throw new Error("Invalid Harness Mix version response");
489
+ }
490
+ return { version: result.version.trim() };
491
+ },
480
492
  async inspectCodexAccountUsage(
481
493
  input: CodexAccountUsageParams,
482
494
  ): Promise<CodexAccountUsageResult> {
@@ -549,6 +561,10 @@ export function createRendererModelClient(
549
561
  );
550
562
  return codexAccountLoginCancelResultSchema.parse(result);
551
563
  },
564
+ async logoutCodexAccount(): Promise<CodexAccountMutationResult> {
565
+ const result = await manager.sendRequest("codexhost/account/logout", {});
566
+ return codexAccountMutationResultSchema.parse(result);
567
+ },
552
568
  subscribeCodexAccountLogin(listener: (result: CodexAccountLoginCompleted) => void): () => void {
553
569
  const notifications = notificationTarget(source);
554
570
  if (!notifications?.addNotificationCallback) {
@@ -24,6 +24,7 @@ const UPDATE_CHECK_TIMEOUT_MS = 5_000;
24
24
  const UPDATE_RETRY_DELAYS_MS = [1_000, 3_000, 10_000, 30_000] as const;
25
25
 
26
26
  export interface RendererSettingsLifecycleOptions {
27
+ getIntegrationsClient?(): import('./renderer-integrations-client.js').RendererIntegrationsClient | null;
27
28
  getUpdateClient?(): RendererUpdateClient | null;
28
29
  getConnectionDiagnostics?(): RendererConnectionDiagnostics | null;
29
30
  getAccountClient?(): RendererCodexAccountClient | null;
@@ -74,6 +75,7 @@ export function installRendererSettingsLifecycle(
74
75
  await options.openImportedThread(threadId, signal);
75
76
  if (!disposed && !signal.aborted) shell?.close();
76
77
  },
78
+ options.getIntegrationsClient,
77
79
  );
78
80
  const nextShell = installRendererSettingsShell(definitions, messages, ownerWindow.document);
79
81
  const nextTrigger = installRendererSettingsHeaderTrigger({
@@ -12,6 +12,10 @@ import type { RendererSettingsMessages } from "./localization.js";
12
12
 
13
13
  let resetDetailsSequence = 0;
14
14
 
15
+ export function isCodexAccountAuthenticated(account: CodexAccountSummary): boolean {
16
+ return account.authenticated ?? Boolean(account.email);
17
+ }
18
+
15
19
  function accountPlanLabel(planType: CodexAccountSummary["planType"]): string | null {
16
20
  if (!planType || planType === "unknown") return null;
17
21
  if (planType === "free") return "Free";
@@ -90,6 +94,7 @@ export function renderAccountRows(
90
94
  resetExpanded: boolean;
91
95
  onActivate: () => void;
92
96
  onSignIn: () => void;
97
+ onSignOut?: () => void;
93
98
  onDelete: () => void;
94
99
  onRetry: () => void;
95
100
  onUseReset?: () => void;
@@ -176,7 +181,7 @@ export function renderAccountRows(
176
181
  activate.addEventListener("click", input.onActivate);
177
182
  actions.append(activate);
178
183
  }
179
- if (!account.email) {
184
+ if (!isCodexAccountAuthenticated(account)) {
180
185
  const signIn = document.createElement("button");
181
186
  signIn.type = "button";
182
187
  signIn.className = "settings-account-action";
@@ -186,6 +191,16 @@ export function renderAccountRows(
186
191
  signIn.addEventListener("click", input.onSignIn);
187
192
  actions.append(signIn);
188
193
  }
194
+ if (account.management === "native" && isCodexAccountAuthenticated(account) && input.onSignOut) {
195
+ const signOut = document.createElement("button");
196
+ signOut.type = "button";
197
+ signOut.className = "settings-account-action";
198
+ signOut.textContent = messages.accountSignOut;
199
+ signOut.dataset.accountFocus = `${account.accountId}:logout`;
200
+ signOut.disabled = input.actionsDisabled;
201
+ signOut.addEventListener("click", input.onSignOut);
202
+ actions.append(signOut);
203
+ }
189
204
  // isDefault protects the native Account home; active selects the Account for
190
205
  // new tasks. Preserve these distinct Host semantics when exposing deletion.
191
206
  if (!account.isDefault) {
@@ -20,6 +20,7 @@ import type {
20
20
  import {
21
21
  accountListFocusRestorer,
22
22
  createAccountsTable,
23
+ isCodexAccountAuthenticated,
23
24
  renderAccountRows,
24
25
  } from "./accounts-list.js";
25
26
  import { mountHarnessAccounts, type RendererHarnessAccountClient } from "./harness-accounts.js";
@@ -44,6 +45,7 @@ export interface RendererCodexAccountClient extends RendererHarnessAccountClient
44
45
  cancelCodexAccountLogin(
45
46
  input: CodexAccountLoginCancelParams,
46
47
  ): Promise<CodexAccountLoginCancelResult>;
48
+ logoutCodexAccount?(): Promise<CodexAccountMutationResult>;
47
49
  subscribeCodexAccountLogin?(listener: (result: CodexAccountLoginCompleted) => void): () => void;
48
50
  }
49
51
 
@@ -224,13 +226,13 @@ export function createAccountsSettingsPage(
224
226
  const restoreFocus = accountListFocusRestorer(list, search);
225
227
  body.replaceChildren();
226
228
  status.textContent = loginMessage ?? "";
227
- connectedCount.textContent = String(accounts.filter((account) => account.email).length);
229
+ connectedCount.textContent = String(accounts.filter(isCodexAccountAuthenticated).length);
228
230
  search.disabled = login !== null || loginStartingAccountId !== null;
229
231
  for (const [display, button] of displayButtons) {
230
232
  button.setAttribute("aria-pressed", String(display === usageDisplay));
231
233
  }
232
234
  refreshUsage.disabled =
233
- ((!getClient()?.inspectCodexAccountUsage || !accounts.some((account) => account.email)) &&
235
+ ((!getClient()?.inspectCodexAccountUsage || !accounts.some(isCodexAccountAuthenticated)) &&
234
236
  !getClient()?.listHarnessAccounts) ||
235
237
  harnessAccounts?.refreshing === true ||
236
238
  [...usageByAccountId.values()].some((usage) => usage.status === "loading") ||
@@ -249,6 +251,9 @@ export function createAccountsSettingsPage(
249
251
  emptyRow.append(emptyCell);
250
252
  body.append(emptyRow);
251
253
  }
254
+ // The stock Codex account remains protected, while additional Accounts
255
+ // are native Codex profiles isolated by CODEX_HOME.
256
+ add.hidden = false;
252
257
  add.disabled = accountBusy();
253
258
  for (const account of visibleAccounts) {
254
259
  body.append(
@@ -262,6 +267,9 @@ export function createAccountsSettingsPage(
262
267
  onActivate: () =>
263
268
  mutate(() => client().activateCodexAccount({ accountId: account.accountId })),
264
269
  onSignIn: () => startLogin(account.accountId),
270
+ ...(account.management === "native" && getClient()?.logoutCodexAccount
271
+ ? { onSignOut: () => signOut() }
272
+ : {}),
265
273
  onDelete: () => deleteAccount(account.accountId),
266
274
  onRetry: () => {
267
275
  usageByAccountId.delete(account.accountId);
@@ -341,7 +349,7 @@ export function createAccountsSettingsPage(
341
349
  };
342
350
  const loadUsage = (nextAccounts: readonly CodexAccountSummary[]): void => {
343
351
  const inspect = getClient()?.inspectCodexAccountUsage;
344
- const signedIn = nextAccounts.filter((account) => account.email);
352
+ const signedIn = nextAccounts.filter(isCodexAccountAuthenticated);
345
353
  const keep = new Set(signedIn.map((account) => account.accountId));
346
354
  for (const accountId of [...usageByAccountId.keys()]) {
347
355
  if (!keep.has(accountId)) usageByAccountId.delete(accountId);
@@ -455,6 +463,27 @@ export function createAccountsSettingsPage(
455
463
  },
456
464
  });
457
465
  };
466
+ const signOut = (): void => {
467
+ const logout = getClient()?.logoutCodexAccount;
468
+ if (!logout || accountBusy()) return;
469
+ if (document.defaultView?.confirm?.(messages.accountSignOutConfirm) === false) return;
470
+ accountActivating = true;
471
+ loginMessage = messages.accountSigningOut;
472
+ render();
473
+ void context.runLatest(() => logout(), {
474
+ success(result) {
475
+ accountActivating = false;
476
+ loginMessage = messages.accountSignedOut;
477
+ usageByAccountId.clear();
478
+ setAccounts([result.account]);
479
+ },
480
+ failure(error) {
481
+ accountActivating = false;
482
+ loginMessage = errorMessage(error, messages.accountSignOutFailed);
483
+ render();
484
+ },
485
+ });
486
+ };
458
487
  const createAndLogin = (): void => {
459
488
  if (accountBusy()) return;
460
489
  accountCreating = true;