@logicseek/hello-cc 1.0.1 → 1.0.2
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 +105 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +9 -0
- package/docs/commands.md +53 -0
- package/docs/commands.zh-CN.md +48 -0
- package/docs/dsh.md +137 -0
- package/docs/dsh.zh-CN.md +139 -0
- package/docs/mcp.md +9 -0
- package/docs/mcp.zh-CN.md +23 -0
- package/docs/native.md +295 -0
- package/docs/native.zh-CN.md +253 -0
- package/docs/web-handoff.zh-CN.md +53 -0
- package/lib/cli/app.mjs +25 -6
- package/lib/cli/commands/doctor.mjs +17 -2
- package/lib/cli/commands/dsh.mjs +79 -0
- package/lib/cli/commands/hook.mjs +241 -90
- package/lib/cli/commands/lock.mjs +34 -19
- package/lib/cli/commands/mcp.mjs +28 -0
- package/lib/cli/commands/native.mjs +130 -0
- package/lib/cli/commands/run.mjs +66 -21
- package/lib/cli/commands/task.mjs +6 -3
- package/lib/cli-args.mjs +4 -0
- package/lib/coordination-state.mjs +15 -1
- package/lib/core/coordination/event-retention.mjs +14 -2
- package/lib/core/coordination/gc-plan.mjs +3 -13
- package/lib/core/coordination/locks.mjs +45 -0
- package/lib/core/coordination/results.mjs +79 -0
- package/lib/core/peers/bindings.mjs +9 -3
- package/lib/diagnostics/codex.mjs +121 -0
- package/lib/integrations/codex-interactions.mjs +114 -0
- package/lib/integrations/dsh-collaboration.mjs +170 -0
- package/lib/integrations/dsh-cordis.d.ts +19 -0
- package/lib/integrations/dsh-cordis.mjs +177 -0
- package/lib/integrations/dsh.bundle.yml +7 -0
- package/lib/integrations/dsh.mjs +287 -0
- package/lib/integrations/native/claude.mjs +451 -0
- package/lib/integrations/native/codex.mjs +242 -0
- package/lib/integrations/native/dsh-acp.mjs +343 -0
- package/lib/integrations/native/index.mjs +19 -0
- package/lib/integrations/native/interactions.mjs +53 -0
- package/lib/integrations/native/jsonrpc.mjs +185 -0
- package/lib/integrations/peers/identity.mjs +36 -10
- package/lib/integrations/peers/processes.mjs +22 -8
- package/lib/integrations/providers.mjs +6 -2
- package/lib/mcp/scope.mjs +104 -0
- package/lib/mcp/stdio.mjs +80 -0
- package/lib/mcp/tools.mjs +197 -0
- package/lib/process/identity.mjs +3 -1
- package/lib/release/release-notes.mjs +9 -0
- package/lib/runtime/native/client.mjs +38 -0
- package/lib/runtime/native/service.mjs +511 -0
- package/lib/runtime/native/store.mjs +121 -0
- package/lib/ui/help.mjs +5 -2
- package/lib/web/adoption-state.mjs +55 -0
- package/lib/web/auto-attach.mjs +18 -2
- package/lib/web/codex-app-server.mjs +557 -0
- package/lib/web/codex-sessions.mjs +287 -0
- package/lib/web/control-lease.mjs +204 -0
- package/lib/web/http-routes.mjs +197 -15
- package/lib/web/http.mjs +5 -4
- package/lib/web/native-sessions.mjs +286 -0
- package/lib/web/project-contexts.mjs +2 -1
- package/lib/web/pty-sessions.mjs +60 -33
- package/lib/web/runtime-main.mjs +98 -6
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/task-results.mjs +76 -0
- package/lib/web/tmux-sessions.mjs +92 -11
- package/lib/web/tmux-stream.mjs +2 -2
- package/lib/web/ui-codex.mjs +550 -0
- package/lib/web/ui-command-palette.mjs +119 -0
- package/lib/web/ui-handoff.mjs +102 -0
- package/lib/web/ui-history.mjs +118 -0
- package/lib/web/ui-i18n.mjs +841 -0
- package/lib/web/ui-interactions.mjs +78 -0
- package/lib/web/ui-login.mjs +101 -0
- package/lib/web/ui-native.mjs +165 -0
- package/lib/web/ui-preferences.mjs +278 -0
- package/lib/web/ui-review.mjs +132 -0
- package/lib/web/ui-template.mjs +1106 -576
- package/lib/web/ui-terminal-find.mjs +98 -0
- package/lib/web/ui-workbench.mjs +94 -0
- package/lib/web/ui-workspace.mjs +222 -0
- package/package.json +11 -3
- package/scripts/check-free-identifiers.mjs +67 -8
- package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
- package/scripts/dsh-cordis-acceptance.mjs +200 -0
- package/scripts/dsh-installed-acceptance.mjs +247 -0
- package/scripts/dsh-official-acceptance.mjs +180 -0
- package/scripts/dsh-web-startup-acceptance.mjs +87 -0
- package/scripts/github-release.mjs +3 -1
- package/scripts/native-live-acceptance.mjs +369 -0
- package/scripts/regression.mjs +243 -89
- package/scripts/web-handoff-installed-acceptance.mjs +98 -0
- package/scripts/web-native-interaction-acceptance.mjs +204 -0
- package/scripts/web-native-model-acceptance.mjs +138 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export function filterCommands(commands, query) {
|
|
2
|
+
const words = String(query || '').toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
3
|
+
return commands.filter(command => {
|
|
4
|
+
const text = [command.id, command.label, command.detail, command.group, command.keywords].join(' ').toLocaleLowerCase();
|
|
5
|
+
return words.every(word => text.includes(word));
|
|
6
|
+
}).slice(0, 80);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Modified shortcuts only. Plain Ctrl+F/K/P remain available to terminal apps.
|
|
10
|
+
export function shortcutAction(event, { modalOpen = false, paletteOpen = false, terminalAvailable = false } = {}) {
|
|
11
|
+
if (event.isComposing || event.repeat || event.altKey || !(event.ctrlKey || event.metaKey) || !event.shiftKey) return null;
|
|
12
|
+
const key = String(event.key || '').toLowerCase();
|
|
13
|
+
if (key === 'p' && (!modalOpen || paletteOpen)) return 'palette';
|
|
14
|
+
if (modalOpen) return null;
|
|
15
|
+
if (key === 'l') return 'sessions';
|
|
16
|
+
if (key === 'f' && terminalAvailable) return 'terminal-find';
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const commandPaletteCss = `
|
|
21
|
+
.dialog.command-dialog { width:min(640px,calc(100vw - 24px)); padding:0; gap:0; overflow:hidden; }
|
|
22
|
+
.command-dialog .dialog-heading { padding:14px 16px 0; }
|
|
23
|
+
.command-query { margin:12px 16px; width:calc(100% - 32px); }
|
|
24
|
+
.command-list { max-height:min(50vh,420px); overflow:auto; border-top:1px solid var(--border); }
|
|
25
|
+
.command-option { display:flex; align-items:center; width:100%; min-height:58px; height:auto; padding:10px 16px; gap:12px; text-align:left; border:0; border-radius:0; background:transparent; }
|
|
26
|
+
.command-option[aria-selected="true"] { background:var(--selection-bg); }
|
|
27
|
+
.command-option:disabled { opacity:.55; }
|
|
28
|
+
.command-copy { flex:1; min-width:0; }
|
|
29
|
+
.command-copy strong,.command-copy small { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
30
|
+
.command-copy small { margin-top:4px; color:var(--muted); font-size:12px; }
|
|
31
|
+
.command-option kbd,.command-hint kbd { font:11px var(--mono); color:var(--muted); white-space:nowrap; }
|
|
32
|
+
.command-hint { display:flex; gap:12px; flex-wrap:wrap; padding:10px 16px; border-top:1px solid var(--border); color:var(--muted); font-size:12px; }
|
|
33
|
+
.command-empty { padding:20px 16px; color:var(--muted); }
|
|
34
|
+
.command-help { flex:1; }
|
|
35
|
+
@media(max-width:1099px) { #commandsBtn kbd { display:none; } }
|
|
36
|
+
@media(max-width:500px) { .command-option kbd { display:none; } .command-list { max-height:48dvh; } }
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
export function commandPaletteHtml() {
|
|
40
|
+
return `<div class="dialog-overlay" id="commandDialog" role="dialog" aria-modal="true" aria-labelledby="commandTitle" hidden>
|
|
41
|
+
<div class="dialog command-dialog">
|
|
42
|
+
<header class="dialog-heading"><h3 id="commandTitle" data-i18n="commands.title">Command palette</h3><button id="commandClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
|
|
43
|
+
<input class="command-query" id="commandQuery" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="commandList" data-i18n-placeholder="commands.placeholder" data-i18n-aria="commands.search" placeholder="Search actions, projects or sessions…" aria-label="Search commands" autocomplete="off">
|
|
44
|
+
<div id="commandList" class="command-list" role="listbox" data-i18n-aria="commands.results" aria-label="Commands"></div>
|
|
45
|
+
<div class="command-hint"><span class="command-help" id="commandHint" role="status" aria-live="polite"></span><span><kbd>↑ ↓</kbd> <span data-i18n="commands.navigate">Navigate</span></span><span><kbd>Enter</kbd> <span data-i18n="commands.open">Open</span></span><span><kbd>Esc</kbd> <span data-i18n="close">Close</span></span></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function commandPaletteScript() {
|
|
51
|
+
return `(() => {
|
|
52
|
+
const filter = ${filterCommands.toString()};
|
|
53
|
+
const shortcut = ${shortcutAction.toString()};
|
|
54
|
+
const host = window.hccCommandHost;
|
|
55
|
+
const dialog = document.getElementById('commandDialog');
|
|
56
|
+
const query = document.getElementById('commandQuery');
|
|
57
|
+
const list = document.getElementById('commandList');
|
|
58
|
+
const hint = document.getElementById('commandHint');
|
|
59
|
+
const esc = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
|
|
60
|
+
const tr = key => window.hccUi.tr(key);
|
|
61
|
+
let visible = [], selected = 0;
|
|
62
|
+
function refresh() {
|
|
63
|
+
if (dialog.hidden) return;
|
|
64
|
+
const previous = visible[selected]?.id;
|
|
65
|
+
visible = filter(host.commands(), query.value);
|
|
66
|
+
selected = Math.max(0, visible.findIndex(command => command.id === previous));
|
|
67
|
+
const scroll = list.scrollTop;
|
|
68
|
+
list.innerHTML = visible.length ? visible.map((command, index) => '<button type="button" tabindex="-1" role="option" class="command-option" id="commandOption-' + index + '" data-command-index="' + index + '" aria-selected="' + (index === selected) + '" aria-disabled="' + (command.enabled === false) + '" ' + (command.enabled === false ? 'disabled' : '') + '><span class="command-copy"><strong>' + esc(command.label) + '</strong><small>' + esc(command.group) + (command.detail ? ' · ' + esc(command.detail) : '') + '</small></span>' + (command.shortcut ? '<kbd>' + esc(command.shortcut) + '</kbd>' : '') + '</button>').join('') : '<div class="command-empty">' + esc(tr('commands.empty')) + '</div>';
|
|
69
|
+
list.scrollTop = scroll;
|
|
70
|
+
query.setAttribute('aria-expanded', 'true');
|
|
71
|
+
if (visible.length) query.setAttribute('aria-activedescendant', 'commandOption-' + selected);
|
|
72
|
+
else query.removeAttribute('aria-activedescendant');
|
|
73
|
+
hint.textContent = tr('commands.count').replace('{count}', String(visible.length)) + (visible.length === 80 ? ' · ' + tr('commands.refine') : '');
|
|
74
|
+
list.querySelectorAll('[data-command-index]').forEach(button => button.addEventListener('click', () => run(Number(button.dataset.commandIndex))));
|
|
75
|
+
}
|
|
76
|
+
function open() {
|
|
77
|
+
if (!dialog.hidden) return;
|
|
78
|
+
if (host.modalOpen()) return;
|
|
79
|
+
query.value = ''; visible = []; selected = 0;
|
|
80
|
+
host.openDialog(dialog, query); refresh();
|
|
81
|
+
}
|
|
82
|
+
function run(index) {
|
|
83
|
+
// Re-resolve from current project data, not a stale row captured before polling.
|
|
84
|
+
const command = host.commands().find(item => item.id === visible[index]?.id);
|
|
85
|
+
if (!command || command.enabled === false) { refresh(); hint.textContent = tr('commands.unavailable'); return; }
|
|
86
|
+
host.closeDialog(dialog);
|
|
87
|
+
try { Promise.resolve(command.run()).catch(host.error); } catch(error) { host.error(error); }
|
|
88
|
+
}
|
|
89
|
+
query.addEventListener('input', refresh);
|
|
90
|
+
query.addEventListener('keydown', event => {
|
|
91
|
+
if (event.isComposing) return;
|
|
92
|
+
if (event.key === 'Enter') { event.preventDefault(); if (visible.length) run(selected); return; }
|
|
93
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || !visible.length) return;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
selected = event.key === 'Home' ? 0 : event.key === 'End' ? visible.length - 1 : (selected + (event.key === 'ArrowDown' ? 1 : -1) + visible.length) % visible.length;
|
|
96
|
+
list.querySelectorAll('[data-command-index]').forEach((button, index) => button.setAttribute('aria-selected', String(index === selected)));
|
|
97
|
+
query.setAttribute('aria-activedescendant', 'commandOption-' + selected);
|
|
98
|
+
document.getElementById('commandOption-' + selected)?.scrollIntoView({block:'nearest'});
|
|
99
|
+
});
|
|
100
|
+
const context = () => ({modalOpen:host.modalOpen(), paletteOpen:!dialog.hidden, terminalAvailable:host.terminalAvailable()});
|
|
101
|
+
const captures = event => Boolean(shortcut(event, context()) || window.hccTerminalFind?.capturesEscape(event));
|
|
102
|
+
document.addEventListener('keydown', event => {
|
|
103
|
+
if (event.defaultPrevented || event.isComposing || event.repeat) return;
|
|
104
|
+
if (window.hccTerminalFind?.capturesEscape(event) && !host.modalOpen()) {
|
|
105
|
+
event.preventDefault(); window.hccTerminalFind.close(); return;
|
|
106
|
+
}
|
|
107
|
+
const action = shortcut(event, context());
|
|
108
|
+
if (!action) return;
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
if (action === 'palette') { if (dialog.hidden) open(); else host.closeDialog(dialog); }
|
|
111
|
+
if (action === 'sessions') host.focusSessions();
|
|
112
|
+
if (action === 'terminal-find') window.hccTerminalFind.open();
|
|
113
|
+
}, true);
|
|
114
|
+
document.getElementById('commandsBtn').addEventListener('click', open);
|
|
115
|
+
document.getElementById('commandClose').addEventListener('click', () => host.closeDialog(dialog));
|
|
116
|
+
window.addEventListener('hcc:preferences', refresh);
|
|
117
|
+
window.hccCommandPalette = {open, refresh, captures};
|
|
118
|
+
})();`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Browser handoff state is scoped to the runtime project and session. Storage
|
|
2
|
+
// failures fall back to memory; delivery records never trigger an automatic send.
|
|
3
|
+
export function createHandoffStore(storage, { scope = '' } = {}) {
|
|
4
|
+
const memory = new Map();
|
|
5
|
+
const suffix = scope ? ':pane:' + JSON.stringify(String(scope)) : '';
|
|
6
|
+
const key = (kind, project, session = '') => 'hcc.handoff.v1.' + kind + ':' + JSON.stringify([project, session]) + suffix;
|
|
7
|
+
function read(name) {
|
|
8
|
+
if (memory.has(name)) return memory.get(name);
|
|
9
|
+
try {
|
|
10
|
+
const raw = storage?.getItem(name);
|
|
11
|
+
return raw ? JSON.parse(raw) : null;
|
|
12
|
+
} catch { return null; }
|
|
13
|
+
}
|
|
14
|
+
function write(name, value) {
|
|
15
|
+
memory.set(name, value);
|
|
16
|
+
try { storage?.setItem(name, JSON.stringify(value)); } catch {}
|
|
17
|
+
}
|
|
18
|
+
function draft(project, session) {
|
|
19
|
+
const value = read(key('draft', project, session));
|
|
20
|
+
if (!value || typeof value.text !== 'string') return { text: '', pending: null };
|
|
21
|
+
const pending = value.pending && typeof value.pending === 'object' && typeof value.pending.inputId === 'string' && typeof value.pending.text === 'string'
|
|
22
|
+
? { inputId: value.pending.inputId, text: value.pending.text, status: value.pending.status === 'pending' ? 'pending' : 'uncertain' } : null;
|
|
23
|
+
return { text: value.text, pending };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
selected(project) {
|
|
27
|
+
const value = read(key('selected', project));
|
|
28
|
+
return value && ['managed', 'detected'].includes(value.type) && typeof value.id === 'string' ? value : null;
|
|
29
|
+
},
|
|
30
|
+
select(project, type, id) { write(key('selected', project), { type, id }); },
|
|
31
|
+
draft,
|
|
32
|
+
saveDraft(project, session, text) {
|
|
33
|
+
const value = draft(project, session);
|
|
34
|
+
value.text = String(text || '');
|
|
35
|
+
write(key('draft', project, session), value);
|
|
36
|
+
return value;
|
|
37
|
+
},
|
|
38
|
+
submit(project, session, inputId, text) {
|
|
39
|
+
const value = draft(project, session);
|
|
40
|
+
if (value.pending) return false;
|
|
41
|
+
value.pending = { inputId, text, status: 'pending' };
|
|
42
|
+
write(key('draft', project, session), value);
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
acknowledge(project, session, inputId) {
|
|
46
|
+
const value = draft(project, session);
|
|
47
|
+
if (!value.pending || value.pending.inputId !== inputId) return false;
|
|
48
|
+
if (value.text === value.pending.text) value.text = '';
|
|
49
|
+
value.pending = null;
|
|
50
|
+
write(key('draft', project, session), value);
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
uncertain(project, session) {
|
|
54
|
+
const value = draft(project, session);
|
|
55
|
+
if (!value.pending) return false;
|
|
56
|
+
value.pending.status = 'uncertain';
|
|
57
|
+
write(key('draft', project, session), value);
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
review(project, session) {
|
|
61
|
+
const value = draft(project, session);
|
|
62
|
+
value.pending = null;
|
|
63
|
+
write(key('draft', project, session), value);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function uiHandoffScript() {
|
|
69
|
+
return 'window.hccHandoffStore = (' + createHandoffStore.toString() + ')((() => { try { return window.localStorage; } catch { return null; } })(), { scope: window.hccUi?.draftScope || window.hccDraftScope || "" });';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const handoffCss = `
|
|
73
|
+
.main > .toolbar, .main > .handoff-bar, .main > .terminal-composer { flex-shrink:0; }
|
|
74
|
+
.main > #codexPanel { flex:1; min-height:0; height:auto !important; }
|
|
75
|
+
.handoff-bar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:8px 12px; border-bottom:1px solid var(--border); background:var(--panel); }
|
|
76
|
+
.handoff-status { flex:1 1 220px; font-size:12px; color:var(--muted); }
|
|
77
|
+
.handoff-detail { width:100%; font-size:11px; color:var(--muted); overflow-wrap:anywhere; }
|
|
78
|
+
.handoff-bar button { height:30px; font-size:12px; }
|
|
79
|
+
.terminal-composer { display:grid; gap:6px; padding:10px 12px; border-top:1px solid var(--border); background:var(--panel); }
|
|
80
|
+
.terminal-composer textarea { width:100%; min-height:64px; max-height:160px; resize:vertical; font:inherit; border:1px solid var(--border); border-radius:6px; padding:8px; background:var(--input-bg); color:var(--text); }
|
|
81
|
+
.terminal-composer .composer-actions { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
|
82
|
+
.terminal-composer .composer-note { flex:1 1 220px; font-size:11px; color:var(--muted); }
|
|
83
|
+
#activeTask { overflow-wrap:anywhere; color:var(--text); white-space:normal; font-size:12px; }
|
|
84
|
+
[hidden] { display:none !important; }
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
export function handoffBarHtml() {
|
|
88
|
+
return `<div class="handoff-bar" id="handoffBar" hidden>
|
|
89
|
+
<div class="handoff-status handoff-chips" id="handoffStatus" role="status" aria-live="polite"><span class="handoff-chip" id="handoffConnection"></span><span class="handoff-chip" id="handoffProcess"></span><span class="handoff-chip" id="handoffAccess"></span></div>
|
|
90
|
+
<button id="claimControlBtn" type="button" data-i18n="handoff.claim">Take control</button>
|
|
91
|
+
<button id="releaseControlBtn" type="button" data-i18n="handoff.release" hidden>Release control</button>
|
|
92
|
+
<button id="reconnectBtn" type="button" data-i18n="handoff.reconnect">Reconnect</button>
|
|
93
|
+
<details class="handoff-detail"><summary id="handoffHelp">Attachment details</summary><div id="handoffDetail"></div></details>
|
|
94
|
+
</div>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function terminalComposerHtml() {
|
|
98
|
+
return `<form class="terminal-composer" id="terminalComposer" hidden>
|
|
99
|
+
<label><span data-i18n="handoff.draft">Terminal input draft</span><textarea id="terminalDraft" rows="2" data-i18n-placeholder="handoff.draftPlaceholder" placeholder="Write terminal input here; Ctrl+Enter sends it"></textarea></label>
|
|
100
|
+
<div class="composer-actions"><span class="composer-note" id="draftStatus" role="status" aria-live="polite"></span><button id="reviewDraftBtn" type="button" data-i18n="handoff.review" hidden>Reviewed; keep draft</button><button class="primary" id="sendDraftBtn" type="submit" data-i18n="handoff.send">Send + Enter</button></div>
|
|
101
|
+
</form>`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function codexHistoryHtml() {
|
|
2
|
+
return `<div class="dialog-overlay" id="historyDialog" role="dialog" aria-modal="true" aria-labelledby="historyTitle" hidden>
|
|
3
|
+
<div class="dialog auxiliary-dialog">
|
|
4
|
+
<header class="dialog-heading"><h3 id="historyTitle" data-i18n="history.title">Codex history</h3><button id="historyClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
|
|
5
|
+
<p class="dialog-help" data-i18n="history.help">Browse saved history, then explicitly resume or fork it in a separate executor.</p>
|
|
6
|
+
<p id="historyNotice" class="dialog-help" role="status" aria-live="polite"></p>
|
|
7
|
+
<div class="history-layout"><div class="history-list"><button id="historyRefresh" type="button" data-i18n="refresh">Refresh</button><div id="historyThreads"></div><button id="historyMore" type="button" data-i18n="history.more" hidden>Load more</button></div>
|
|
8
|
+
<div class="history-preview"><h4 id="historySelected"></h4><div id="historyContent"></div>
|
|
9
|
+
<label class="confirmation-row"><input id="historyConfirmed" type="checkbox"><span data-i18n="history.confirm">I checked the original executor. Opening starts a separate executor; resuming the same thread requires stopping the original.</span></label>
|
|
10
|
+
<div class="btns"><button id="historyResume" type="button" data-i18n="history.resume" disabled>Resume in a new executor</button><button id="historyFork" type="button" data-i18n="history.fork" disabled>Fork into a new executor</button></div>
|
|
11
|
+
</div></div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function codexHistoryScript() {
|
|
17
|
+
return String.raw`(() => {
|
|
18
|
+
const dialog = document.getElementById('historyDialog');
|
|
19
|
+
const bridge = () => window.hccHandoff;
|
|
20
|
+
const tr = (key, fallback = '') => bridge().tr(key, fallback);
|
|
21
|
+
const esc = value => bridge().esc(String(value ?? ''));
|
|
22
|
+
let root = '', entries = [], cursor = null, selected = null, busy = false, generation = 0;
|
|
23
|
+
const same = (project, version) => project === root && project === bridge().projectRoot && version === generation && !dialog.hidden;
|
|
24
|
+
const route = (path, project = root) => path + (path.includes('?') ? '&' : '?') + 'root=' + encodeURIComponent(project);
|
|
25
|
+
function notice(value) { document.getElementById('historyNotice').textContent = value || ''; }
|
|
26
|
+
function liveSources() {
|
|
27
|
+
return (bridge().sessions || []).filter(session => session.status === 'running' && session.binding?.provider === 'codex' && session.binding.provider_session_id === selected?.id);
|
|
28
|
+
}
|
|
29
|
+
function controlsSource(sources) { return sources.every(session => session.id === bridge().active && bridge().canControl); }
|
|
30
|
+
function sync() {
|
|
31
|
+
const confirmed = document.getElementById('historyConfirmed').checked;
|
|
32
|
+
const sources = liveSources(), unavailable = busy || !selected || !confirmed || root !== bridge().projectRoot;
|
|
33
|
+
document.getElementById('historyResume').disabled = unavailable || sources.length > 0;
|
|
34
|
+
document.getElementById('historyFork').disabled = unavailable || !controlsSource(sources);
|
|
35
|
+
if (!busy && sources.length) notice(tr(controlsSource(sources) ? 'history.stopOriginal' : 'history.sourceControlRequired'));
|
|
36
|
+
document.getElementById('historyRefresh').disabled = busy;
|
|
37
|
+
document.getElementById('historyMore').hidden = !cursor;
|
|
38
|
+
document.getElementById('historyMore').disabled = busy;
|
|
39
|
+
}
|
|
40
|
+
function renderList() {
|
|
41
|
+
const container = document.getElementById('historyThreads');
|
|
42
|
+
container.innerHTML = entries.map(entry => '<button type="button" class="history-thread" data-thread="' + esc(entry.id) + '" aria-pressed="' + (entry.id === selected?.id) + '"><strong>' + esc(entry.name || entry.preview || entry.id) + '</strong><span>' + esc(entry.id) + '</span><span>' + esc(entry.cwd || '') + '</span></button>').join('') || '<p class="dialog-help">' + esc(tr('history.empty')) + '</p>';
|
|
43
|
+
container.querySelectorAll('[data-thread]').forEach(button => button.addEventListener('click', () => select(button.dataset.thread)));
|
|
44
|
+
sync();
|
|
45
|
+
}
|
|
46
|
+
async function load(more = false) {
|
|
47
|
+
if (busy || !root) return;
|
|
48
|
+
const project = root, version = ++generation;
|
|
49
|
+
busy = true; notice(tr('history.loading')); sync();
|
|
50
|
+
try {
|
|
51
|
+
const result = await bridge().api(route('/api/codex/threads' + (more && cursor ? '?cursor=' + encodeURIComponent(cursor) : ''), project));
|
|
52
|
+
if (!same(project, version)) return;
|
|
53
|
+
const incoming = Array.isArray(result.threads) ? result.threads : [];
|
|
54
|
+
entries = [...new Map((more ? entries.concat(incoming) : incoming).map(entry => [entry.id, entry])).values()];
|
|
55
|
+
cursor = result.nextCursor || null;
|
|
56
|
+
notice(''); renderList();
|
|
57
|
+
} catch (error) { if (same(project, version)) notice(error.detail || error.message); }
|
|
58
|
+
finally { if (project === root && version === generation) { busy = false; sync(); } }
|
|
59
|
+
}
|
|
60
|
+
async function select(id) {
|
|
61
|
+
if (busy) return;
|
|
62
|
+
const project = root, version = ++generation;
|
|
63
|
+
selected = entries.find(entry => entry.id === id) || { id };
|
|
64
|
+
document.getElementById('historyConfirmed').checked = false;
|
|
65
|
+
document.getElementById('historySelected').textContent = selected.name || selected.preview || id;
|
|
66
|
+
document.getElementById('historyContent').textContent = '';
|
|
67
|
+
busy = true; renderList(); notice(tr('history.loading'));
|
|
68
|
+
try {
|
|
69
|
+
const result = await bridge().api(route('/api/codex/threads/' + encodeURIComponent(id), project));
|
|
70
|
+
if (!same(project, version)) return;
|
|
71
|
+
selected = result.thread || selected;
|
|
72
|
+
renderPreview();
|
|
73
|
+
notice('');
|
|
74
|
+
} catch (error) { if (same(project, version)) notice(error.detail || error.message); }
|
|
75
|
+
finally { if (project === root && version === generation) { busy = false; sync(); } }
|
|
76
|
+
}
|
|
77
|
+
function renderPreview() {
|
|
78
|
+
document.getElementById('historyContent').innerHTML = (selected?.turns || []).map(turn => '<section class="history-turn"><strong>' + esc(turn.status || '') + '</strong>' + (turn.items || []).map(item => {
|
|
79
|
+
const content = Array.isArray(item.content) ? item.content.map(part => part.text || '').join('\n') : item.text || item.command || item.aggregatedOutput || '';
|
|
80
|
+
const label = item.type === 'userMessage' ? tr('history.user') : item.type === 'agentMessage' ? tr('history.assistant') : item.type === 'commandExecution' ? tr('history.command') : item.type === 'fileChange' ? tr('history.files') : tr('history.event');
|
|
81
|
+
return '<article><strong>' + esc(label) + '</strong><pre>' + esc(content || (item.changes ? JSON.stringify(item.changes, null, 2) : '')) + '</pre></article>';
|
|
82
|
+
}).join('') + '</section>').join('') || '<p class="dialog-help">' + esc(tr('history.noTurns')) + '</p>';
|
|
83
|
+
}
|
|
84
|
+
async function reopen(fork) {
|
|
85
|
+
if (busy || !selected || !document.getElementById('historyConfirmed').checked || root !== bridge().projectRoot) return;
|
|
86
|
+
const sources = liveSources();
|
|
87
|
+
if (sources.length && (!fork || !controlsSource(sources))) { sync(); return; }
|
|
88
|
+
const project = root, threadId = selected.id, version = ++generation;
|
|
89
|
+
const input = fork ? { confirmed: true, ...(sources.length ? { actionToken: bridge().actionToken, epoch: bridge().epoch } : {}) } : { kind: 'codex', transport: 'app-server', mode: 'resume', resume: threadId, handoffConfirmed: true };
|
|
90
|
+
busy = true; sync(); notice(tr('history.opening'));
|
|
91
|
+
try {
|
|
92
|
+
const result = await bridge().api(fork ? route('/api/codex/threads/' + encodeURIComponent(threadId) + '/fork', project) : route('/api/sessions', project), {
|
|
93
|
+
method: 'POST', body: JSON.stringify(input)
|
|
94
|
+
});
|
|
95
|
+
if (project !== bridge().projectRoot) return;
|
|
96
|
+
await bridge().refreshSessions();
|
|
97
|
+
if (!same(project, version)) return;
|
|
98
|
+
bridge().closeDialog(dialog);
|
|
99
|
+
if (result.session?.id) bridge().openManaged(result.session.id);
|
|
100
|
+
} catch (error) { if (same(project, version)) notice((error.detail || error.message) + (error.code ? ' [' + error.code + ']' : '') + ' ' + tr('history.checkSessions')); }
|
|
101
|
+
finally { if (project === root && version === generation) { busy = false; sync(); } }
|
|
102
|
+
}
|
|
103
|
+
function open() {
|
|
104
|
+
root = bridge().projectRoot; generation++; entries = []; selected = null; cursor = null; busy = false;
|
|
105
|
+
document.getElementById('historyContent').textContent = ''; document.getElementById('historySelected').textContent = '';
|
|
106
|
+
document.getElementById('historyConfirmed').checked = false;
|
|
107
|
+
bridge().openDialog(dialog, document.getElementById('historyRefresh')); renderList(); void load();
|
|
108
|
+
}
|
|
109
|
+
document.getElementById('historyClose').addEventListener('click', () => bridge().closeDialog(dialog));
|
|
110
|
+
document.getElementById('historyRefresh').addEventListener('click', () => load());
|
|
111
|
+
document.getElementById('historyMore').addEventListener('click', () => load(true));
|
|
112
|
+
document.getElementById('historyConfirmed').addEventListener('change', sync);
|
|
113
|
+
document.getElementById('historyResume').addEventListener('click', () => reopen(false));
|
|
114
|
+
document.getElementById('historyFork').addEventListener('click', () => reopen(true));
|
|
115
|
+
window.addEventListener('hcc:preferences', () => { if (!dialog.hidden) { renderList(); renderPreview(); } });
|
|
116
|
+
window.hccHistory = { open, sync, reset() { generation++; busy = false; if (!dialog.hidden) bridge().closeDialog(dialog); } };
|
|
117
|
+
})();`;
|
|
118
|
+
}
|