@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.
- package/CHANGELOG.md +8 -0
- package/README.md +26 -96
- package/docs/chatgpt-web-sidebar.md +7 -1
- package/docs/cross-platform.md +15 -0
- package/docs/harness-handoff-design.md +377 -0
- package/docs/harness-management.md +28 -1
- package/docs/native-codex.md +21 -10
- package/output/native-build/harness-mix-shim.exe +0 -0
- package/output/native-build/renderer-extension.js +972 -35
- package/package.json +21 -3
- package/scripts/chatgpt-context-ui-smoke.cjs +16 -0
- package/scripts/codex-accounts-test.cjs +81 -0
- package/scripts/codex-accounts-ui-smoke.cjs +70 -0
- package/scripts/e2e-integrations.cjs +52 -0
- package/scripts/e2e-native-host.cjs +39 -0
- package/scripts/handoff-checkpoint-test.cjs +83 -0
- package/scripts/install-native.cjs +29 -0
- package/scripts/integrations-test.cjs +64 -0
- package/scripts/integrations-ui-smoke.cjs +63 -0
- package/scripts/native-protocol-test.cjs +78 -4
- package/scripts/native-secret-test.cjs +2 -2
- package/scripts/native-shim-test.cjs +20 -4
- package/scripts/native-ui-smoke.cjs +58 -2
- package/scripts/publish-native-package.cjs +37 -0
- package/scripts/support/integrations-mcp-fixture.cjs +15 -0
- package/scripts/switch-harness-test.cjs +27 -1
- package/src/main/adapters/acp.js +3 -3
- package/src/main/adapters/antigravity.js +10 -13
- package/src/main/adapters/claude.js +10 -3
- package/src/main/adapters/codex-app-server.js +20 -13
- package/src/main/adapters/codex.js +6 -4
- package/src/main/adapters/dsh.js +2 -1
- package/src/main/adapters/grok.js +4 -3
- package/src/main/adapters/managed-mcp.js +12 -0
- package/src/main/adapters/native-acp.js +4 -4
- package/src/main/adapters/opencode.js +9 -3
- package/src/main/adapters/pi.js +1 -0
- package/src/main/harness-adapter/manifest.js +17 -2
- package/src/main/host/handoff-access.js +53 -0
- package/src/main/host/handoff-checkpoints.js +206 -0
- package/src/main/host/handoff-mcp.cjs +38 -0
- package/src/main/host/handoff-tools.js +13 -0
- package/src/main/host/handoff.js +28 -6
- package/src/main/host/integrations.js +201 -0
- package/src/main/host/runtime.js +75 -35
- package/src/main/host/store.js +4 -4
- package/src/main/native/codex-accounts.js +260 -0
- package/src/main/native/host.js +1 -1
- package/src/main/native/launcher.js +2 -2
- package/src/main/native/protocol.js +74 -23
- package/src/main/native/redact.js +1 -1
- package/src/main/native/rs/crates/shim/src/main.rs +155 -12
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/harness-mix-settings.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/index.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/index.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-agent-picker.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts +36 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-chatgpt-context.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-codex-account-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts +3 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-composer-dom.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts +53 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-harness-handoff.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts +80 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-integrations-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +7 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-list.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/accounts-page.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/integrations-page.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +8 -4
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/harness-mix-settings.ts +3 -1
- package/src/native-ui/renderer-extension/src/index.ts +28 -0
- package/src/native-ui/renderer-extension/src/renderer-agent-icon.ts +4 -4
- package/src/native-ui/renderer-extension/src/renderer-agent-picker.ts +46 -6
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +159 -40
- package/src/native-ui/renderer-extension/src/renderer-chatgpt-context.ts +83 -0
- package/src/native-ui/renderer-extension/src/renderer-codex-account-state.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-composer-dom.ts +10 -0
- package/src/native-ui/renderer-extension/src/renderer-harness-handoff.ts +330 -0
- package/src/native-ui/renderer-extension/src/renderer-integrations-client.ts +26 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +17 -1
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/accounts-list.ts +16 -1
- package/src/native-ui/renderer-extension/src/settings/accounts-page.ts +32 -3
- package/src/native-ui/renderer-extension/src/settings/integrations-page.ts +102 -0
- package/src/native-ui/renderer-extension/src/settings/localization.ts +30 -13
- package/src/native-ui/renderer-extension/src/settings/pages.ts +47 -18
- package/src/native-ui/renderer-extension/src/settings/shell.css +13 -0
- package/src/native-ui/renderer-extension/test/renderer-agent-picker.test.ts +44 -13
- package/src/native-ui/renderer-extension/test/renderer-chatgpt-context.test.ts +18 -0
- package/src/native-ui/renderer-extension/test/renderer-codex-account-state.test.ts +13 -1
- package/src/native-ui/renderer-extension/test/renderer-harness-handoff.test.ts +20 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +9 -2
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -1
- package/src/native-ui/renderer-extension/test/settings/pages.test.ts +77 -8
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts +19 -4
- package/src/native-ui/shared-contracts/dist/codex-accounts.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/codex-accounts.js +2 -0
- package/src/native-ui/shared-contracts/dist/codex-accounts.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.d.ts +2 -2
- package/src/native-ui/shared-contracts/dist/index.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/index.js +1 -1
- package/src/native-ui/shared-contracts/dist/index.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts +29 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.d.ts.map +1 -1
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js +13 -0
- package/src/native-ui/shared-contracts/dist/thread-harness-switch.js.map +1 -1
- package/src/native-ui/shared-contracts/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/shared-contracts/src/codex-accounts.ts +2 -0
- package/src/native-ui/shared-contracts/src/index.ts +8 -1
- 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
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|