@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3
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 +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
export function agentStartHtml() {
|
|
2
|
+
return `<div class="dialog-overlay" id="startDialog" role="dialog" aria-modal="true" aria-labelledby="startDialogTitle" hidden>
|
|
3
|
+
<div class="dialog">
|
|
4
|
+
<header class="dialog-heading"><h3 id="startDialogTitle">New Agent</h3><button id="startDialogClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
|
|
5
|
+
<p class="dialog-help" id="startDialogHelp" data-i18n="startDialogHelp">Choose an agent and its working directory. It will keep running when this page closes.</p>
|
|
6
|
+
<p class="dialog-help" id="startDialogError" role="alert" hidden></p>
|
|
7
|
+
<div class="dialog-target"><span data-i18n="startProject">Target project</span>: <span id="startProjectPath"></span></div>
|
|
8
|
+
<form class="form" id="startForm">
|
|
9
|
+
<p class="dialog-help" id="agentResumeInfo" hidden></p><div id="agentLaunchFields">
|
|
10
|
+
<p class="dialog-help" id="agentDefaultsNotice" role="status" hidden></p><button id="agentDefaultsRetry" type="button" data-i18n="agent.defaultsRetry" hidden>Reload defaults for unedited fields</button>
|
|
11
|
+
<label><span data-i18n="agent.provider">Agent</span><select id="kind"><option value="codex">Codex</option><option value="claude">Claude</option><option value="dsh">DeepSeek Harness</option><option value="shell">Shell</option></select></label>
|
|
12
|
+
<label><span data-i18n="agent.cwd">Working directory</span><input id="agentCwd" type="text" required autocomplete="off" aria-describedby="agentCwdHelp"></label>
|
|
13
|
+
<p class="dialog-help" id="agentCwdHelp" data-i18n="agent.cwdHelp">Use this project directory or a subdirectory within it.</p>
|
|
14
|
+
<label id="agentModelField"><span data-i18n="agent.model">Model (optional)</span><input id="agentModel" type="text" maxlength="256" autocomplete="off" data-i18n-placeholder="agent.modelPlaceholder" placeholder="Provider default"></label>
|
|
15
|
+
<label><span data-i18n="agent.name">Name (optional)</span><input id="agentName" type="text" maxlength="128" autocomplete="off" aria-describedby="agentNameHelp"></label>
|
|
16
|
+
<p class="dialog-help" id="agentNameHelp" data-i18n="agent.nameHelp">Leave blank for an automatic name, or use letters, numbers, dots, hyphens and underscores. Start with a letter or number.</p>
|
|
17
|
+
<details id="agentAdvanced"><summary data-i18n="agent.advanced">Advanced options</summary>
|
|
18
|
+
<label id="transportField"><span data-i18n="agent.interface">Interface</span><select id="transport"><option value="native" data-i18n="agent.background">Background Agent</option><option value="tmux" data-i18n="handoff.cli">Terminal CLI</option><option value="app-server" data-i18n="handoff.appServer">Structured App Server (opt in)</option></select></label>
|
|
19
|
+
<p class="dialog-help" id="agentConfigHelp" data-i18n="agent.configHelp" hidden>Terminal CLI and App Server use the provider's existing model configuration.</p>
|
|
20
|
+
<div class="start-options" id="agentModeFields" hidden>
|
|
21
|
+
<label><span data-i18n="mode">Mode</span><select id="startMode"><option value="new" data-i18n="mode.new">new</option></select></label>
|
|
22
|
+
<label data-resume-field><span data-i18n="session">Session</span><select id="resumeSelect"></select></label>
|
|
23
|
+
<label data-resume-field data-resume-custom style="display:none"><span data-i18n="sessionId">Session id</span><input id="resumeArg" data-i18n-placeholder="sessionIdPlaceholder" placeholder="session id or name"></label>
|
|
24
|
+
</div>
|
|
25
|
+
<label id="appServerResumeField" hidden><span style="display:flex;align-items:flex-start;gap:8px"><input type="checkbox" id="appServerResumeConfirm"><span data-i18n="handoff.resumeConfirm">I have stopped the original executor. Resuming history starts a separate executor.</span></span></label>
|
|
26
|
+
</details>
|
|
27
|
+
</div>
|
|
28
|
+
<button id="agentForgetRecovery" type="button" data-i18n="agent.forgetRecovery" hidden>Reviewed; forget recovery record</button>
|
|
29
|
+
<div class="btns"><button id="startCancelBtn" type="button" data-i18n="dialog.cancel">Cancel</button><button id="agentStartBtn" class="primary" type="submit" data-i18n="start">Start</button></div>
|
|
30
|
+
</form>
|
|
31
|
+
</div>
|
|
32
|
+
</div>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function agentStartRuntime(browser = globalThis) {
|
|
36
|
+
const { window, document } = browser;
|
|
37
|
+
const bridge = window.hccHandoff, byId = id => document.getElementById(id);
|
|
38
|
+
const dialog = byId('startDialog'), form = byId('startForm'), kind = byId('kind'), transport = byId('transport');
|
|
39
|
+
const mode = byId('startMode'), resume = byId('resumeSelect'), argument = byId('resumeArg');
|
|
40
|
+
const error = byId('startDialogError'), name = byId('agentName'), cwd = byId('agentCwd'), model = byId('agentModel');
|
|
41
|
+
const choices = new Map(), recoveries = new Map();
|
|
42
|
+
const launchDrafts = new Map();
|
|
43
|
+
let pending = false, created = null, resumeTarget = null, revision = 0, resumeRequest = 0, project = '', previousKind = kind.value, previousMode = '';
|
|
44
|
+
let defaults = null, defaultsLoading = false, defaultsFailed = false, defaultsRequest = 0;
|
|
45
|
+
let launchProvider = kind.value, launchTransport = 'native', launchNative = true, providerTouched = false, interfaceTouched = false, shownCwd = '', shownModel = '', freshLaunch = false;
|
|
46
|
+
const tr = key => bridge.tr(key);
|
|
47
|
+
const native = () => transport.value === 'native';
|
|
48
|
+
const current = visit => revision === visit && bridge.projectRoot === project && !dialog.hidden;
|
|
49
|
+
const recoveryKey = root => 'hcc.agentCreation:' + root;
|
|
50
|
+
function rememberRecovery(root, value) {
|
|
51
|
+
if (value) recoveries.set(root, value); else recoveries.delete(root);
|
|
52
|
+
window.hccUi?.safeSet(recoveryKey(root), JSON.stringify(value));
|
|
53
|
+
}
|
|
54
|
+
function recoveryFor(root) {
|
|
55
|
+
if (recoveries.has(root)) return recoveries.get(root);
|
|
56
|
+
try {
|
|
57
|
+
const value = JSON.parse(window.hccUi?.safeGet(recoveryKey(root)) || 'null');
|
|
58
|
+
if (value && typeof value.id === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value.id) && value.recovery === true
|
|
59
|
+
&& (value.operation !== 'resume' || (typeof value.oldOwner === 'string' && value.oldOwner && typeof value.sessionId === 'string' && value.sessionId))) return value;
|
|
60
|
+
} catch (_) {}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function nativePeer() {
|
|
64
|
+
return 'web-' + (window.crypto?.randomUUID?.() || Date.now().toString(36) + '-' + Math.random().toString(36).slice(2));
|
|
65
|
+
}
|
|
66
|
+
function recoveryNotice(value) {
|
|
67
|
+
if (value.operation === 'resume') return tr(value.confirmed ? 'agent.resumeRefreshFailed' : 'agent.resumeUnconfirmed').replace('{name}',value.id);
|
|
68
|
+
return tr(value.confirmed ? 'agent.createdRefreshFailed' : 'agent.creationUnconfirmed').replace('{name}', value.id);
|
|
69
|
+
}
|
|
70
|
+
function recoveredSession(recovery) {
|
|
71
|
+
return bridge.sessions?.find(item => item.type === 'native' && [item.id,item.peer_id].includes(recovery.id)
|
|
72
|
+
&& (recovery.operation !== 'resume' || (item.kind === recovery.provider && item.status === 'running' && item.native_connected === true
|
|
73
|
+
&& item.executor_id && item.executor_id !== recovery.oldOwner && (!recovery.executorId || item.executor_id === recovery.executorId)
|
|
74
|
+
&& item.binding?.provider_session_id === recovery.sessionId)));
|
|
75
|
+
}
|
|
76
|
+
function defaultCwd(provider) {
|
|
77
|
+
const relative = defaults?.providers?.[provider]?.cwd;
|
|
78
|
+
return relative && relative !== '.' ? project.replace(/[\\/]+$/, '') + '/' + relative : project;
|
|
79
|
+
}
|
|
80
|
+
function launchDraft(provider, connection = launchTransport) {
|
|
81
|
+
const key = provider + ':' + connection;
|
|
82
|
+
if (!launchDrafts.has(key)) launchDrafts.set(key,{provider,connection,cwd:connection === 'native' ? defaultCwd(provider) : project,
|
|
83
|
+
model:connection === 'native' ? defaults?.providers?.[provider]?.model || '' : '',cwdTouched:false,modelTouched:false});
|
|
84
|
+
return launchDrafts.get(key);
|
|
85
|
+
}
|
|
86
|
+
function captureLaunchDraft() {
|
|
87
|
+
if (!project) return;
|
|
88
|
+
const draft = launchDraft(launchProvider);
|
|
89
|
+
if (cwd.value !== shownCwd) { draft.cwdTouched = true; draft.cwd = cwd.value; }
|
|
90
|
+
if (launchNative && model.value !== shownModel) { draft.modelTouched = true; draft.model = model.value; }
|
|
91
|
+
}
|
|
92
|
+
function showLaunchDraft() {
|
|
93
|
+
const draft = launchDraft(launchProvider);
|
|
94
|
+
cwd.value = draft.cwdTouched || launchNative ? draft.cwd : project;
|
|
95
|
+
model.value = draft.model;
|
|
96
|
+
shownCwd = cwd.value; shownModel = model.value;
|
|
97
|
+
}
|
|
98
|
+
function defaultsStatus() {
|
|
99
|
+
byId('agentDefaultsNotice').hidden = !defaultsLoading && !defaultsFailed;
|
|
100
|
+
byId('agentDefaultsNotice').textContent = defaultsLoading ? tr('agent.defaultsLoading') : defaultsFailed ? tr('agent.defaultsFailed') : '';
|
|
101
|
+
byId('agentDefaultsRetry').hidden = !defaultsFailed;
|
|
102
|
+
byId('agentDefaultsRetry').disabled = pending || defaultsLoading;
|
|
103
|
+
}
|
|
104
|
+
async function readDefaults() {
|
|
105
|
+
if (!window.hccAgentDefaults || created || resumeTarget || !project) return;
|
|
106
|
+
const visit = revision, request = ++defaultsRequest, expectedProvider = kind.value;
|
|
107
|
+
defaultsLoading = true; defaultsFailed = false; defaultsStatus(); setPending(pending);
|
|
108
|
+
try {
|
|
109
|
+
const result = await window.hccAgentDefaults.read(project);
|
|
110
|
+
if (!current(visit) || request !== defaultsRequest || created || resumeTarget) return;
|
|
111
|
+
captureLaunchDraft(); defaults = result;
|
|
112
|
+
const currentDraft = launchDraft(launchProvider);
|
|
113
|
+
const canChooseProvider = !providerTouched && !interfaceTouched && kind.value === expectedProvider && !currentDraft.cwdTouched && !currentDraft.modelTouched;
|
|
114
|
+
for (const draft of launchDrafts.values()) {
|
|
115
|
+
if (draft.connection !== 'native') continue;
|
|
116
|
+
if (!draft.cwdTouched) draft.cwd = defaultCwd(draft.provider);
|
|
117
|
+
if (!draft.modelTouched) draft.model = defaults.providers?.[draft.provider]?.model || '';
|
|
118
|
+
}
|
|
119
|
+
if (canChooseProvider && ['codex','claude','dsh'].includes(defaults.defaultProvider)) kind.value = defaults.defaultProvider;
|
|
120
|
+
sync(); showLaunchDraft();
|
|
121
|
+
} catch (_) {
|
|
122
|
+
if (current(visit) && request === defaultsRequest) defaultsFailed = true;
|
|
123
|
+
} finally {
|
|
124
|
+
if (current(visit) && request === defaultsRequest) { defaultsLoading = false; defaultsStatus(); setPending(pending); }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toggleResume() {
|
|
129
|
+
const custom = !native() && mode.value === 'resume' && resume.value === '__custom__';
|
|
130
|
+
document.querySelector('[data-resume-custom]').style.display = custom ? '' : 'none';
|
|
131
|
+
argument.required = custom;
|
|
132
|
+
}
|
|
133
|
+
async function loadResumable() {
|
|
134
|
+
const visit = revision, request = ++resumeRequest, provider = kind.value, connection = transport.value;
|
|
135
|
+
let entries = [];
|
|
136
|
+
try { entries = (await bridge.api('/api/resumable')).resumable || []; } catch (_) {}
|
|
137
|
+
if (!current(visit) || request !== resumeRequest || kind.value !== provider || transport.value !== connection || mode.value !== 'resume') return;
|
|
138
|
+
const selected = resume.value;
|
|
139
|
+
resume.innerHTML = entries.filter(item => item.provider === provider).map(item => {
|
|
140
|
+
const id = item.resume || item.session_id || item.session_name || '';
|
|
141
|
+
const label = (item.name && item.name !== id ? item.name + ' · ' : '') + id + ' (' + item.peer + ')';
|
|
142
|
+
return '<option value="' + bridge.esc(id) + '">' + bridge.esc(label) + '</option>';
|
|
143
|
+
}).join('') + '<option value="__custom__">' + bridge.esc(tr('customSession')) + '</option>';
|
|
144
|
+
if ([...resume.options].some(option => option.value === selected)) resume.value = selected;
|
|
145
|
+
toggleResume();
|
|
146
|
+
}
|
|
147
|
+
function clearResume() {
|
|
148
|
+
resumeRequest++; resume.innerHTML = ''; argument.value = ''; argument.required = false;
|
|
149
|
+
byId('appServerResumeConfirm').checked = false;
|
|
150
|
+
}
|
|
151
|
+
function sync() {
|
|
152
|
+
captureLaunchDraft();
|
|
153
|
+
const provider = kind.value;
|
|
154
|
+
const allowed = provider === 'shell' ? ['tmux'] : provider === 'dsh' ? ['native']
|
|
155
|
+
: provider === 'codex' ? ['native','tmux','app-server'] : ['native','tmux'];
|
|
156
|
+
const changed = provider !== previousKind;
|
|
157
|
+
const selected = changed ? choices.get(provider) : transport.value;
|
|
158
|
+
transport.innerHTML = allowed.map(value => '<option value="' + value + '">' + bridge.esc(tr(value === 'native' ? 'agent.background' : value === 'tmux' ? 'handoff.cli' : 'handoff.appServer')) + '</option>').join('');
|
|
159
|
+
transport.value = allowed.includes(selected) ? selected : allowed[0];
|
|
160
|
+
choices.set(provider, transport.value);
|
|
161
|
+
transport.disabled = allowed.length === 1;
|
|
162
|
+
if (launchProvider !== provider || launchTransport !== transport.value) {
|
|
163
|
+
launchProvider = provider; launchTransport = transport.value; launchNative = native(); showLaunchDraft();
|
|
164
|
+
}
|
|
165
|
+
const modes = native() || provider === 'shell' ? ['new'] : provider === 'claude' ? ['new','resume','continue']
|
|
166
|
+
: transport.value === 'app-server' ? ['new','resume'] : ['new','resume','last'];
|
|
167
|
+
const selectedMode = changed ? 'new' : mode.value;
|
|
168
|
+
mode.innerHTML = modes.map(value => '<option value="' + value + '">' + bridge.esc(tr('mode.' + value)) + '</option>').join('');
|
|
169
|
+
mode.value = modes.includes(selectedMode) ? selectedMode : 'new';
|
|
170
|
+
if (changed || mode.value !== 'resume') clearResume();
|
|
171
|
+
byId('agentModelField').hidden = !native(); model.disabled = !native();
|
|
172
|
+
byId('agentModeFields').hidden = native() || provider === 'shell';
|
|
173
|
+
byId('agentConfigHelp').hidden = native() || provider === 'shell';
|
|
174
|
+
byId('appServerResumeField').hidden = !(transport.value === 'app-server' && mode.value === 'resume');
|
|
175
|
+
document.querySelector('[data-resume-field]:not([data-resume-custom])').style.display = mode.value === 'resume' ? '' : 'none';
|
|
176
|
+
const modeKey = [provider,transport.value,mode.value].join(':');
|
|
177
|
+
previousKind = provider;
|
|
178
|
+
if (mode.value === 'resume' && modeKey !== previousMode) void loadResumable();
|
|
179
|
+
previousMode = modeKey;
|
|
180
|
+
toggleResume();
|
|
181
|
+
}
|
|
182
|
+
function setPending(value) {
|
|
183
|
+
pending = value;
|
|
184
|
+
const resuming = resumeTarget || (created?.operation === 'resume' ? created : null);
|
|
185
|
+
const titleKey = resuming ? 'agent.resumeTitle' : 'newSession';
|
|
186
|
+
const actionKey = created?.recovery ? 'agent.checkCreated' : created ? 'agent.openCreated' : resuming ? 'history.resume' : 'start';
|
|
187
|
+
byId('agentLaunchFields').hidden = Boolean(resuming);
|
|
188
|
+
byId('startDialogHelp').hidden = Boolean(resuming);
|
|
189
|
+
byId('agentResumeInfo').hidden = !resuming;
|
|
190
|
+
byId('agentResumeInfo').textContent = resuming ? tr('agent.resumeInfo').replace('{name}',resuming.peer || resuming.id).replace('{session}',resuming.sessionId) : '';
|
|
191
|
+
byId('startDialogTitle').setAttribute('data-i18n',titleKey);
|
|
192
|
+
byId('startDialogTitle').textContent = tr(titleKey);
|
|
193
|
+
byId('agentStartBtn').disabled = value || defaultsLoading || Boolean(resumeTarget?.rejected);
|
|
194
|
+
byId('agentStartBtn').setAttribute('data-i18n',actionKey);
|
|
195
|
+
byId('agentStartBtn').textContent = tr(actionKey);
|
|
196
|
+
byId('agentForgetRecovery').hidden = !created?.recovery;
|
|
197
|
+
byId('agentForgetRecovery').disabled = value;
|
|
198
|
+
byId('openStartDialog').disabled = value;
|
|
199
|
+
form.setAttribute('aria-busy', String(value));
|
|
200
|
+
defaultsStatus();
|
|
201
|
+
}
|
|
202
|
+
function open(loadDefaults = true) {
|
|
203
|
+
if (pending) return;
|
|
204
|
+
resumeTarget = null;
|
|
205
|
+
revision++;
|
|
206
|
+
if (project !== bridge.projectRoot || freshLaunch) {
|
|
207
|
+
project = bridge.projectRoot; cwd.value = project; name.value = ''; model.value = ''; clearResume(); mode.value = 'new';
|
|
208
|
+
defaults = null; launchDrafts.clear(); choices.clear(); providerTouched = false; interfaceTouched = false;
|
|
209
|
+
kind.value = 'codex'; transport.value = 'native'; launchProvider = 'codex'; launchTransport = 'native'; launchNative = true; shownCwd = project; shownModel = ''; freshLaunch = false;
|
|
210
|
+
}
|
|
211
|
+
created = recoveryFor(project);
|
|
212
|
+
byId('startProjectPath').textContent = project;
|
|
213
|
+
error.hidden = !created;
|
|
214
|
+
if (created) error.textContent = recoveryNotice(created);
|
|
215
|
+
byId('appServerResumeConfirm').checked = false;
|
|
216
|
+
previousMode = '';
|
|
217
|
+
bridge.openDialog(dialog, kind); sync(); setPending(false);
|
|
218
|
+
if (loadDefaults && !created) void readDefaults();
|
|
219
|
+
}
|
|
220
|
+
function closed() { revision++; resumeRequest++; defaultsRequest++; defaultsLoading = false; defaultsFailed = false; created = null; resumeTarget = null; setPending(pending); }
|
|
221
|
+
function reset() { closed(); if (!dialog.hidden) bridge.closeDialog(dialog); }
|
|
222
|
+
async function submit(event) {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
if (pending || defaultsLoading || dialog.hidden || project !== bridge.projectRoot || resumeTarget?.rejected) return;
|
|
225
|
+
const visit = revision, submittedProject = project;
|
|
226
|
+
let attempted = null, admitted = false;
|
|
227
|
+
error.hidden = true;
|
|
228
|
+
try {
|
|
229
|
+
if (!created) {
|
|
230
|
+
let payload, endpoint = '/api/sessions';
|
|
231
|
+
if (resumeTarget) {
|
|
232
|
+
payload = {owner:resumeTarget.owner,sessionId:resumeTarget.sessionId,confirmed:true};
|
|
233
|
+
endpoint = '/api/native/history/' + encodeURIComponent(resumeTarget.peer) + '/resume';
|
|
234
|
+
attempted = {id:resumeTarget.peer,provider:resumeTarget.provider,operation:'resume',oldOwner:resumeTarget.owner,sessionId:resumeTarget.sessionId,recovery:true,confirmed:false};
|
|
235
|
+
} else {
|
|
236
|
+
sync();
|
|
237
|
+
const id = name.value.trim(), directory = cwd.value.trim() || project;
|
|
238
|
+
if (id && (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(id) || id.toLowerCase() === 'all')) {
|
|
239
|
+
name.focus(); throw new Error(tr('agent.badName'));
|
|
240
|
+
}
|
|
241
|
+
payload = { kind:kind.value, cwd:directory, ...(id ? { id } : {}) };
|
|
242
|
+
if (native()) {
|
|
243
|
+
payload.transport = 'native';
|
|
244
|
+
payload.id = id || nativePeer();
|
|
245
|
+
payload.model = model.value.trim() || null;
|
|
246
|
+
} else {
|
|
247
|
+
payload.mode = mode.value;
|
|
248
|
+
if (transport.value === 'app-server') payload.transport = 'app-server';
|
|
249
|
+
if (mode.value === 'resume') {
|
|
250
|
+
payload.resume = resume.value && resume.value !== '__custom__' ? resume.value : argument.value.trim();
|
|
251
|
+
if (!payload.resume) { argument.focus(); throw new Error(tr('agent.resumeRequired')); }
|
|
252
|
+
if (transport.value === 'app-server') {
|
|
253
|
+
if (!byId('appServerResumeConfirm').checked) throw new Error(tr('handoff.resumeConfirm'));
|
|
254
|
+
payload.handoffConfirmed = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (native()) {
|
|
259
|
+
attempted = { id:payload.id, provider:payload.kind, recovery:true, confirmed:false };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
setPending(true);
|
|
263
|
+
// Save the original identity before a write. A lost response can only
|
|
264
|
+
// trigger discovery of this operation, never another creation/resume.
|
|
265
|
+
if (attempted) rememberRecovery(submittedProject,attempted);
|
|
266
|
+
const result = await bridge.api(endpoint, { method:'POST', body:JSON.stringify(payload) });
|
|
267
|
+
admitted = true;
|
|
268
|
+
const receipt = attempted ? { ...attempted,confirmed:true,...(attempted.operation === 'resume' && result.session?.executor_id ? {executorId:result.session.executor_id} : {}) } : null;
|
|
269
|
+
if (receipt) rememberRecovery(submittedProject,receipt);
|
|
270
|
+
if (!current(visit)) return;
|
|
271
|
+
created = attempted?.operation === 'resume' ? receipt : result.session;
|
|
272
|
+
} else setPending(true);
|
|
273
|
+
await bridge.refreshSessions();
|
|
274
|
+
if (!current(visit)) return;
|
|
275
|
+
const session = created.recovery ? recoveredSession(created) : created;
|
|
276
|
+
if (!session) throw new Error(tr('agent.awaitingDiscovery'));
|
|
277
|
+
if (created.operation !== 'resume' && !resumeTarget) freshLaunch = true;
|
|
278
|
+
rememberRecovery(submittedProject, null);
|
|
279
|
+
bridge.closeDialog(dialog);
|
|
280
|
+
bridge.openManaged(session.id);
|
|
281
|
+
} catch (failure) {
|
|
282
|
+
const recoverable = ['NATIVE_CREATE_UNCONFIRMED','NATIVE_RESUME_UNCONFIRMED','NATIVE_WORKER_DISCOVERY_FAILED','REQUEST_TIMEOUT','REQUEST_SUPERSEDED'].includes(failure.code)
|
|
283
|
+
|| (!failure.status && !failure.code);
|
|
284
|
+
if (attempted && !admitted && recoverable) {
|
|
285
|
+
const peer = failure.extra?.peer;
|
|
286
|
+
const recovery = { ...attempted, ...(attempted.operation !== 'resume' && typeof peer === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(peer) ? { id:peer } : {}),
|
|
287
|
+
...(attempted.operation === 'resume' && typeof failure.extra?.executorId === 'string' ? {executorId:failure.extra.executorId} : {}),
|
|
288
|
+
confirmed: failure.extra?.created === true };
|
|
289
|
+
rememberRecovery(submittedProject, recovery);
|
|
290
|
+
if (current(visit)) created = recovery;
|
|
291
|
+
} else if (attempted && !admitted) {
|
|
292
|
+
rememberRecovery(submittedProject,null);
|
|
293
|
+
if (current(visit) && attempted.operation === 'resume' && resumeTarget) resumeTarget.rejected = true;
|
|
294
|
+
}
|
|
295
|
+
if (!current(visit)) return;
|
|
296
|
+
error.textContent = (created ? (created.recovery ? recoveryNotice(created) : tr('agent.createdRefreshFailed').replace('{name}', created.id)) + ' ' : '')
|
|
297
|
+
+ bridge.tr('error.' + failure.code, failure.detail || failure.message);
|
|
298
|
+
error.hidden = false;
|
|
299
|
+
} finally { setPending(false); }
|
|
300
|
+
}
|
|
301
|
+
form.addEventListener('submit',submit);
|
|
302
|
+
function resumeNative(worker) {
|
|
303
|
+
if (pending || !worker || worker.resumable !== true || worker.status !== 'closed' || worker.owned !== false || !worker.owner || !worker.sessionId) return;
|
|
304
|
+
open(false);
|
|
305
|
+
// An outstanding operation takes precedence over any newly selected worker.
|
|
306
|
+
if (created) return;
|
|
307
|
+
resumeTarget = {...worker}; setPending(false);
|
|
308
|
+
return submit({preventDefault() {}});
|
|
309
|
+
}
|
|
310
|
+
kind.addEventListener('change', () => { providerTouched = true; sync(); });
|
|
311
|
+
for (const field of [transport,mode]) field.addEventListener('change', () => { interfaceTouched = true; sync(); });
|
|
312
|
+
cwd.addEventListener('input', () => { const draft = launchDraft(launchProvider); draft.cwdTouched = true; draft.cwd = cwd.value; shownCwd = cwd.value; });
|
|
313
|
+
model.addEventListener('input', () => { const draft = launchDraft(launchProvider); draft.modelTouched = true; draft.model = model.value; shownModel = model.value; });
|
|
314
|
+
byId('agentDefaultsRetry').addEventListener('click',readDefaults);
|
|
315
|
+
resume.addEventListener('change', toggleResume);
|
|
316
|
+
byId('agentForgetRecovery').addEventListener('click', () => {
|
|
317
|
+
if (pending || !created?.recovery || dialog.hidden || bridge.projectRoot !== project) return;
|
|
318
|
+
if (!window.confirm(tr('agent.forgetRecoveryConfirm').replace('{name}', created.id))) return;
|
|
319
|
+
rememberRecovery(project, null); created = null; resumeTarget = null; error.hidden = true; setPending(false);
|
|
320
|
+
});
|
|
321
|
+
const controller = { open, sync, closed, reset, resumeNative };
|
|
322
|
+
Object.defineProperty(controller, 'pending', { get: () => pending });
|
|
323
|
+
window.hccAgentStart = controller;
|
|
324
|
+
window.addEventListener?.('hcc:preferences',() => { if (!dialog.hidden) setPending(pending); });
|
|
325
|
+
sync();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function agentStartScript() { return '(' + agentStartRuntime.toString() + ')();'; }
|
|
329
|
+
|
|
330
|
+
export const installAgentStart = agentStartRuntime;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function codexAccountPanelHtml(prefix) {
|
|
2
|
+
return '<details id="' + prefix + 'Account" class="hcc-codex-account" hidden><summary id="' + prefix + 'AccountLabel">Account and limits</summary>'
|
|
3
|
+
+ '<p id="' + prefix + 'AccountSummary" role="status"></p><div id="' + prefix + 'AccountLimits"></div>'
|
|
4
|
+
+ '<p id="' + prefix + 'AccountChecked" class="hcc-account-muted"></p><button id="' + prefix + 'AccountRead" type="button">Refresh account and limits</button>'
|
|
5
|
+
+ '<p id="' + prefix + 'AccountNote" class="hcc-account-muted"></p></details>';
|
|
6
|
+
}
|
|
7
|
+
export function codexAccountPanelCss() {
|
|
8
|
+
return '.hcc-codex-account{max-width:900px;margin:12px auto;padding:12px;border:1px solid var(--border);border-radius:8px;overflow-wrap:anywhere}'
|
|
9
|
+
+ '.hcc-codex-account summary{cursor:pointer;font-weight:600}.hcc-codex-account p{margin:8px 0;font-size:var(--small-font)}'
|
|
10
|
+
+ '.hcc-account-muted{color:var(--muted)}.hcc-account-window{display:grid;gap:4px;margin:10px 0}.hcc-account-window progress{width:100%;height:8px;accent-color:var(--accent)}'
|
|
11
|
+
+ '.hcc-codex-account[data-warning="true"]{border-color:var(--warn)}.hcc-codex-account button{min-height:36px}'
|
|
12
|
+
+ '@media(max-width:640px){.hcc-codex-account{padding:10px}.hcc-codex-account button{min-height:44px;width:100%}}';
|
|
13
|
+
}
|
|
14
|
+
export function codexAccountPanelScript() { return '(' + installCodexAccountPanel.toString() + ')();'; }
|
|
15
|
+
|
|
16
|
+
export function installCodexAccountPanel() {
|
|
17
|
+
(() => {
|
|
18
|
+
if (window.hccCodexAccount) return;
|
|
19
|
+
const text = (en,zh) => window.hccUi.language === 'zh' ? zh : en;
|
|
20
|
+
const esc = value => window.hccHandoff.esc(String(value ?? ''));
|
|
21
|
+
const rendered = new Map();
|
|
22
|
+
function label(account) {
|
|
23
|
+
if (!account || account.status === 'unknown') return text('Not read yet','暂未读取');
|
|
24
|
+
if (account.status !== 'ready') return account.reason === 'disconnected' ? text('Executor disconnected','执行器已断开') : text('Temporarily unavailable','暂不可用');
|
|
25
|
+
if (account.authentication === 'providerManaged') return text('Provider-managed authentication','供应商自行管理认证');
|
|
26
|
+
if (account.authentication === 'required') return text('Local Codex sign-in required','需要在本地 Codex 登录');
|
|
27
|
+
if (account.authentication !== 'authenticated') return text('Authentication unknown','认证状态未知');
|
|
28
|
+
return (account.type === 'chatgpt' ? 'ChatGPT' + (account.planType ? ' · ' + account.planType : '')
|
|
29
|
+
: account.type === 'apiKey' ? 'API key' : 'Amazon Bedrock') + text(' · Signed in',' · 已登录');
|
|
30
|
+
}
|
|
31
|
+
function date(value) {
|
|
32
|
+
if (!Number.isFinite(value)) return '';
|
|
33
|
+
try { return new Date(value).toLocaleString(window.hccUi.language === 'zh' ? 'zh-CN' : 'en-US'); } catch (_) { return ''; }
|
|
34
|
+
}
|
|
35
|
+
function windowHtml(value, fallback) {
|
|
36
|
+
if (!value) return '';
|
|
37
|
+
const duration = value.windowDurationMins;
|
|
38
|
+
const name = duration ? (duration % 1440 === 0 ? duration / 1440 + text(' day window',' 天窗口')
|
|
39
|
+
: duration % 60 === 0 ? duration / 60 + text(' hour window',' 小时窗口') : duration + text(' minute window',' 分钟窗口')) : fallback;
|
|
40
|
+
const reset = value.resetsAt == null ? '' : text(' · Resets ',' · 重置时间 ') + date(value.resetsAt * 1000);
|
|
41
|
+
return '<div class="hcc-account-window"><span>' + esc(name + text(' · Used ',' · 已用 ') + value.usedPercent + '%' + reset)
|
|
42
|
+
+ '</span><progress max="100" value="' + Math.min(100,Math.max(0,value.usedPercent)) + '" aria-label="' + esc(name + text(' usage',' 用量')) + '"></progress></div>';
|
|
43
|
+
}
|
|
44
|
+
function render(prefix, account, { supported = true, connected = true, pending = false } = {}) {
|
|
45
|
+
const panel = document.getElementById(prefix + 'Account');
|
|
46
|
+
if (!panel) return;
|
|
47
|
+
const signature = JSON.stringify([account, supported, connected, pending, window.hccUi.language]);
|
|
48
|
+
if (rendered.get(prefix) === signature) return;
|
|
49
|
+
rendered.set(prefix, signature);
|
|
50
|
+
panel.hidden = !supported;
|
|
51
|
+
if (!supported) return;
|
|
52
|
+
const limits = account?.rateLimits;
|
|
53
|
+
panel.dataset.warning = String(account?.authentication === 'required' || limits?.buckets?.some(bucket => bucket.reached || (bucket.credits?.hasCredits === false && !bucket.credits.unlimited) || [bucket.primary,bucket.secondary].some(value => value?.usedPercent >= 100)) || false);
|
|
54
|
+
document.getElementById(prefix + 'AccountLabel').textContent = text('Account and limits','账号与限额') + ' · ' + label(account);
|
|
55
|
+
document.getElementById(prefix + 'AccountSummary').textContent = label(account)
|
|
56
|
+
+ (account?.stale ? text(' · Prior data, needs refresh',' · 历史状态,待刷新') : '');
|
|
57
|
+
let html = '';
|
|
58
|
+
if (limits?.status === 'ready' || limits?.stale) html = (limits.buckets || []).map(bucket => '<div><strong>' + esc(bucket.id || 'Codex') + '</strong>'
|
|
59
|
+
+ windowHtml(bucket.primary,text('Primary window','主要窗口')) + windowHtml(bucket.secondary,text('Secondary window','次要窗口'))
|
|
60
|
+
+ (bucket.credits ? '<p>' + esc(bucket.credits.unlimited ? text('Credits: unlimited','额度:不限量') : bucket.credits.hasCredits ? text('Credits available','有可用额度') : text('No available credits','无可用额度')) + '</p>' : '')
|
|
61
|
+
+ (bucket.reached ? '<p>' + esc(text('A usage or credit limit has been reached. Check the local Codex account.','已达到用量或额度限制,请检查本地 Codex 账号。')) + '</p>' : '') + '</div>').join('');
|
|
62
|
+
if (limits?.stale) html = '<p>' + esc(text('Prior usage data; the current read is unavailable.','以下是历史用量,本次读取暂不可用。')) + '</p>' + html;
|
|
63
|
+
if (limits?.status !== 'ready') html += '<p>' + esc(account?.authentication === 'required'
|
|
64
|
+
? text('Sign in locally to read subscription limits.','请先在本地登录,再读取订阅限额。') : limits?.status === 'notApplicable'
|
|
65
|
+
? text('Codex subscription limits do not apply to this provider. Its quota is unknown here.','当前供应商不适用 Codex 订阅限额;这里无法确认它的配额。')
|
|
66
|
+
: limits?.reason === 'unsupported' ? text('This Codex version does not provide subscription limits.','此 Codex 版本未提供订阅限额。')
|
|
67
|
+
: text('Limits are unavailable; missing data does not mean zero usage.','限额暂不可用;缺失数据不代表用量为零。')) + '</p>';
|
|
68
|
+
document.getElementById(prefix + 'AccountLimits').innerHTML = html;
|
|
69
|
+
const checked = [account?.checkedAt ? text('Account checked: ','账号读取时间:') + date(account.checkedAt) : '',
|
|
70
|
+
limits?.checkedAt ? text('Limits checked: ','限额读取时间:') + date(limits.checkedAt) : ''].filter(Boolean).join(' · ');
|
|
71
|
+
document.getElementById(prefix + 'AccountChecked').textContent = checked;
|
|
72
|
+
const button = document.getElementById(prefix + 'AccountRead');
|
|
73
|
+
button.disabled = !connected || pending;
|
|
74
|
+
button.textContent = pending ? text('Reading…','读取中…') : text('Refresh account and limits','刷新账号与限额');
|
|
75
|
+
document.getElementById(prefix + 'AccountNote').textContent = account?.authentication === 'providerManaged'
|
|
76
|
+
? text('This provider does not require OpenAI sign-in. A real task is needed to verify its authentication.','当前供应商无需 OpenAI 登录,认证是否有效需通过实际任务确认。')
|
|
77
|
+
: text('Read-only data from this executor. Login changes stay in local Codex; no proactive token refresh is requested.','只读查看此执行器的状态。登录变更仍由本地 Codex 管理,此操作不请求主动刷新 token。');
|
|
78
|
+
}
|
|
79
|
+
window.hccCodexAccount = { render };
|
|
80
|
+
})();
|
|
81
|
+
}
|