@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,550 @@
|
|
|
1
|
+
import { interactionPanelScript } from './ui-interactions.mjs';
|
|
2
|
+
|
|
3
|
+
export function codexPanelHtml() {
|
|
4
|
+
return `<style>
|
|
5
|
+
#codexPanel { flex:1; min-height:0; min-width:0; overflow:hidden; }
|
|
6
|
+
#codexPanel:not([hidden]) { display:flex; flex-direction:column; }
|
|
7
|
+
#codexPanel[hidden] { display:none; }
|
|
8
|
+
.codex-header { flex:none; display:flex; align-items:center; gap:10px; padding:10px 16px; border-bottom:1px solid var(--border); }
|
|
9
|
+
#codexStatus { color:var(--muted); font-size:var(--small-font); overflow-wrap:anywhere; }
|
|
10
|
+
#codexApprovalJump { flex:none; margin-left:auto; color:var(--warn); border-color:var(--warn); }
|
|
11
|
+
.codex-history { flex:none; display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:8px; padding:8px 12px; border-bottom:1px solid var(--border); }
|
|
12
|
+
.codex-history[hidden] { display:none; }
|
|
13
|
+
.codex-history span { color:var(--muted); font-size:var(--small-font); }
|
|
14
|
+
.codex-history button { min-height:36px; }
|
|
15
|
+
.codex-scroll { flex:1; min-height:0; overflow:auto; padding:0 16px 16px; overflow-anchor:none; scrollbar-gutter:stable; }
|
|
16
|
+
.codex-turn { max-width:900px; margin:20px auto 0; }
|
|
17
|
+
.codex-turn-heading { display:flex; align-items:center; gap:8px; color:var(--muted); font-size:var(--small-font); margin-bottom:10px; }
|
|
18
|
+
.codex-turn-heading span:last-child { margin-left:auto; }
|
|
19
|
+
.codex-card { padding:12px 14px; margin:8px 0; border:1px solid var(--border); border-radius:10px; background:var(--card-bg); min-width:0; }
|
|
20
|
+
.codex-card-user { background:var(--panel-2); border-left:3px solid var(--accent); }
|
|
21
|
+
.codex-card-agent { border-color:transparent; background:transparent; padding-left:2px; padding-right:2px; }
|
|
22
|
+
.codex-card-error { border-color:var(--danger); }
|
|
23
|
+
.codex-card-heading { font-size:var(--small-font); color:var(--muted); display:flex; gap:10px; align-items:center; margin-bottom:8px; }
|
|
24
|
+
.codex-card-heading strong { color:var(--text); font-weight:600; }
|
|
25
|
+
.codex-status { margin-left:auto; flex:none; color:var(--muted); }
|
|
26
|
+
.codex-status-failed { color:var(--danger); }
|
|
27
|
+
.codex-prose { white-space:pre-wrap; overflow-wrap:anywhere; line-height:1.65; margin:8px 0; }
|
|
28
|
+
.codex-markdown { line-height:1.65; overflow-wrap:anywhere; }
|
|
29
|
+
.codex-markdown h1, .codex-markdown h2, .codex-markdown h3, .codex-markdown h4, .codex-markdown h5, .codex-markdown h6 { line-height:1.4; margin:16px 0 8px; font-weight:600; }
|
|
30
|
+
.codex-markdown h1 { font-size:22px; } .codex-markdown h2 { font-size:19px; } .codex-markdown h3 { font-size:17px; }
|
|
31
|
+
.codex-markdown h4, .codex-markdown h5, .codex-markdown h6 { font-size:var(--body-font); }
|
|
32
|
+
.codex-markdown ul, .codex-markdown ol { margin:8px 0; padding-left:24px; }
|
|
33
|
+
.codex-markdown li { padding-left:2px; margin:4px 0; }
|
|
34
|
+
.codex-markdown blockquote { margin:10px 0; padding:4px 12px; border-left:3px solid var(--border); color:var(--muted); }
|
|
35
|
+
.codex-markdown :not(pre) > code { font-family:var(--mono); font-size:.92em; padding:2px 5px; border-radius:4px; background:var(--panel-2); }
|
|
36
|
+
.codex-tool > summary, .codex-review > summary { cursor:pointer; display:flex; align-items:center; gap:8px; overflow-wrap:anywhere; }
|
|
37
|
+
.codex-tool > summary::before, .codex-review > summary::before { content:"›"; color:var(--muted); }
|
|
38
|
+
.codex-tool[open] > summary::before, .codex-review[open] > summary::before { content:"⌄"; }
|
|
39
|
+
.codex-tool > summary strong { flex:none; font-size:var(--small-font); }
|
|
40
|
+
.codex-tool-preview { color:var(--muted); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; min-width:0; flex:1; font-family:var(--mono); font-size:var(--small-font); }
|
|
41
|
+
.codex-code { border:1px solid var(--border); border-radius:8px; overflow:hidden; margin:10px 0; background:var(--input-bg); }
|
|
42
|
+
.codex-code-header { display:flex; align-items:center; justify-content:space-between; padding:4px 8px; color:var(--muted); font-size:var(--small-font); border-bottom:1px solid var(--border); }
|
|
43
|
+
.codex-code-header button { font-size:var(--small-font); min-height:28px; padding:3px 8px; }
|
|
44
|
+
.codex-code pre { margin:0; padding:10px 12px; overflow:auto; white-space:pre; font-family:var(--mono); font-size:var(--small-font); line-height:1.6; }
|
|
45
|
+
.codex-diff-add { color:var(--ok); } .codex-diff-remove { color:var(--danger); } .codex-diff-header { color:var(--accent); }
|
|
46
|
+
.codex-review { max-width:900px; margin:16px auto 0; border-top:1px solid var(--border); padding-top:12px; }
|
|
47
|
+
.codex-review h3 { font-size:var(--small-font); margin:14px 0 6px; }
|
|
48
|
+
.codex-plan { list-style:none; padding:0; margin:8px 0; display:grid; gap:8px; }
|
|
49
|
+
.codex-plan li { display:flex; gap:10px; align-items:baseline; overflow-wrap:anywhere; }
|
|
50
|
+
.codex-plan li span:first-child { min-width:72px; color:var(--muted); font-size:var(--small-font); }
|
|
51
|
+
.codex-plan li[data-status="completed"] span:first-child { color:var(--ok); }
|
|
52
|
+
.codex-plan li[data-status="inProgress"] span:first-child { color:var(--accent); }
|
|
53
|
+
.codex-approval { max-width:900px; margin:16px auto; border-color:var(--warn); }
|
|
54
|
+
.codex-approval h3 { margin:0 0 10px; font-size:var(--body-font); color:var(--warn); }
|
|
55
|
+
.codex-approval dl { display:grid; grid-template-columns:auto minmax(0,1fr); gap:6px 12px; margin:10px 0; }
|
|
56
|
+
.codex-approval dt { color:var(--muted); font-size:var(--small-font); } .codex-approval dd { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
57
|
+
.hcc-interaction-form { display:grid; gap:10px; margin:12px 0; }
|
|
58
|
+
.hcc-interaction-form label { display:grid; gap:5px; overflow-wrap:anywhere; }
|
|
59
|
+
.hcc-interaction-form label:has(input[type=checkbox]) { display:flex; align-items:flex-start; }
|
|
60
|
+
.hcc-interaction-form input:not([type=checkbox]), .hcc-interaction-form select { width:100%; min-height:36px; min-width:0; background:var(--input-bg); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:6px; }
|
|
61
|
+
.codex-approval-actions { display:flex; gap:8px; margin-top:12px; }
|
|
62
|
+
.codex-jump { flex:none; text-align:center; padding:6px; border-top:1px solid var(--border); }
|
|
63
|
+
.codex-composer { flex:none; padding:12px 16px; border-top:1px solid var(--border); background:var(--panel); max-height:40%; overflow:auto; }
|
|
64
|
+
.codex-composer-inner { max-width:900px; margin:auto; }
|
|
65
|
+
.codex-composer label { display:block; color:var(--muted); font-size:var(--small-font); margin-bottom:6px; }
|
|
66
|
+
#codexDraft { display:block; width:100%; box-sizing:border-box; font:inherit; color:var(--text); background:var(--input-bg); border:1px solid var(--border); border-radius:8px; padding:10px; resize:vertical; min-height:64px; max-height:180px; }
|
|
67
|
+
.codex-composer-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
68
|
+
#codexSubmission { font-size:var(--small-font); color:var(--muted); margin:8px 0 0; overflow-wrap:anywhere; }
|
|
69
|
+
#codexSubmission:empty { display:none; }
|
|
70
|
+
@media (max-width:640px) {
|
|
71
|
+
.codex-header, .codex-scroll, .codex-composer { padding-left:12px; padding-right:12px; }
|
|
72
|
+
.codex-card { padding:10px; } .codex-card-agent { padding:2px; }
|
|
73
|
+
.codex-composer-actions button { min-height:36px; }
|
|
74
|
+
.codex-approval dl { grid-template-columns:1fr; gap:3px; } .codex-approval dd { margin-bottom:6px; }
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
77
|
+
<section id="codexPanel" hidden aria-label="Codex">
|
|
78
|
+
<header class="codex-header"><div id="codexStatus" role="status"></div><button id="codexApprovalJump" type="button" hidden>Approval required</button></header>
|
|
79
|
+
<nav id="codexHistory" class="codex-history" hidden aria-label="Conversation history"><button id="codexEarlier" type="button" aria-controls="codexTimeline">Earlier</button><span id="codexHistoryRange" role="status"></span><button id="codexLater" type="button" aria-controls="codexTimeline">Newer</button></nav>
|
|
80
|
+
<div id="codexScroll" class="codex-scroll" tabindex="0" aria-label="Conversation">
|
|
81
|
+
<div id="codexApprovals" aria-live="polite"></div>
|
|
82
|
+
<div id="codexTimeline"></div>
|
|
83
|
+
<details id="codexReview" class="codex-review" hidden data-details-key="review"><summary id="codexReviewLabel">Latest turn changes and plan</summary><div id="codexPlan"></div><div id="codexDiff"></div></details>
|
|
84
|
+
</div>
|
|
85
|
+
<div id="codexJumpBar" class="codex-jump" hidden><button id="codexJump" type="button">Back to latest</button></div>
|
|
86
|
+
<footer class="codex-composer"><div class="codex-composer-inner">
|
|
87
|
+
<label for="codexDraft" id="codexDraftLabel">Message saved in this browser</label>
|
|
88
|
+
<textarea id="codexDraft" rows="3"></textarea>
|
|
89
|
+
<div class="codex-composer-actions">
|
|
90
|
+
<button id="codexSend" class="primary" type="button">Send</button><button id="codexInterrupt" type="button">Interrupt turn</button>
|
|
91
|
+
<button id="codexRecover" type="button">Read current state</button><button id="codexClear" type="button">Clear draft</button>
|
|
92
|
+
</div><p id="codexSubmission" role="status"></p>
|
|
93
|
+
</div></footer>
|
|
94
|
+
</section>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function codexPanelScript() {
|
|
98
|
+
return interactionPanelScript() + String.raw`(() => {
|
|
99
|
+
const panel = document.getElementById('codexPanel');
|
|
100
|
+
const draft = document.getElementById('codexDraft');
|
|
101
|
+
const scroller = document.getElementById('codexScroll');
|
|
102
|
+
const views = new Map();
|
|
103
|
+
const approving = new Set();
|
|
104
|
+
const PAGE_SIZE = 80;
|
|
105
|
+
let copies = [], timelineHtml = '', timelineSignature = '', timelineCopies = [], entries = [];
|
|
106
|
+
let state = null, key = '', pending = null, submitting = false;
|
|
107
|
+
const text = (en, zh) => window.hccUi.language === 'zh' ? zh : en;
|
|
108
|
+
const bridge = () => window.hccHandoff;
|
|
109
|
+
const escape = (value) => bridge().esc(String(value ?? ''));
|
|
110
|
+
function draftKey() { return 'hcc.codexDraft:' + bridge().projectRoot + ':' + bridge().active + (bridge().draftScope ? ':' + bridge().draftScope : ''); }
|
|
111
|
+
function subject() { const b = bridge(); return { root: b.projectRoot, id: b.active, key: draftKey(), token: b.actionToken, epoch: b.epoch }; }
|
|
112
|
+
function matches(target) { return target.root === bridge().projectRoot && target.id === bridge().active; }
|
|
113
|
+
function route(target, name) { return '/api/sessions/' + encodeURIComponent(target.id) + '/codex/' + name + '?root=' + encodeURIComponent(target.root); }
|
|
114
|
+
function approvalKey(target, executorId, requestId) { return JSON.stringify([target.root, target.id, executorId, requestId]); }
|
|
115
|
+
function save() { window.hccUi.safeSet(key, JSON.stringify({ text: draft.value, pending })); }
|
|
116
|
+
function message(value) { document.getElementById('codexSubmission').textContent = value; }
|
|
117
|
+
function activeThread() { return state?.threads?.find(t => t.id === (state.currentThreadId || state.threadId)) || state?.threads?.[0]; }
|
|
118
|
+
function turnId() { return state?.turnId || activeThread()?.activeTurnId || ''; }
|
|
119
|
+
function view() {
|
|
120
|
+
if (!views.has(key)) {
|
|
121
|
+
views.set(key, { top: 0, follow: true, unread: false, signature: '', details: new Map(), startKey: '', start: 0, end: 0, total: 0 });
|
|
122
|
+
if (views.size > 20) views.delete(views.keys().next().value);
|
|
123
|
+
}
|
|
124
|
+
return views.get(key);
|
|
125
|
+
}
|
|
126
|
+
function pinned() { return scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop < 48; }
|
|
127
|
+
function updateJump() {
|
|
128
|
+
document.getElementById('codexJumpBar').hidden = view().follow;
|
|
129
|
+
document.getElementById('codexJump').textContent = view().unread
|
|
130
|
+
? text('New updates · Back to latest', '有新内容 · 回到最新')
|
|
131
|
+
: text('Back to latest', '回到最新');
|
|
132
|
+
}
|
|
133
|
+
function statusLabel(value) {
|
|
134
|
+
const status = typeof value === 'object' ? value?.type : value;
|
|
135
|
+
const labels = {
|
|
136
|
+
completed: text('Completed', '已完成'), failed: text('Failed', '失败'),
|
|
137
|
+
interrupted: text('Interrupted', '已中断'), inProgress: text('Running', '进行中'),
|
|
138
|
+
pending: text('Pending', '待处理'), ready: text('Ready', '就绪'),
|
|
139
|
+
idle: text('Idle', '空闲'), active: text('Active', '活跃'),
|
|
140
|
+
starting: text('Starting', '启动中'), stopped: text('Stopped', '已停止'),
|
|
141
|
+
exited: text('Exited', '已退出'), disconnected: text('Disconnected', '已断开'),
|
|
142
|
+
new: text('Not started', '未启动'), connecting: text('Connecting', '连接中'), closed: text('Closed', '已关闭'),
|
|
143
|
+
notLoaded: text('Not loaded', '未加载'), systemError: text('System error', '系统错误'),
|
|
144
|
+
declined: text('Declined', '已拒绝'), error: text('Error', '错误')
|
|
145
|
+
};
|
|
146
|
+
return labels[status] || String(status || '');
|
|
147
|
+
}
|
|
148
|
+
function codeBlock(value, language = '', diff = false) {
|
|
149
|
+
const content = String(value ?? ''), index = copies.push(content) - 1;
|
|
150
|
+
const body = diff ? content.split('\n').map(line => {
|
|
151
|
+
const cls = line.startsWith('@@') || line.startsWith('diff ') || line.startsWith('+++') || line.startsWith('---')
|
|
152
|
+
? 'codex-diff-header' : line.startsWith('+') ? 'codex-diff-add' : line.startsWith('-') ? 'codex-diff-remove' : '';
|
|
153
|
+
return '<span class="' + cls + '">' + escape(line) + '</span>';
|
|
154
|
+
}).join('\n') : escape(content);
|
|
155
|
+
return '<div class="codex-code"><div class="codex-code-header"><span>' + escape(language || text('Text', '文本'))
|
|
156
|
+
+ '</span><button type="button" data-copy="' + index + '">' + escape(text('Copy', '复制'))
|
|
157
|
+
+ '</button></div><pre><code>' + body + '</code></pre></div>';
|
|
158
|
+
}
|
|
159
|
+
function inline(value, allowBold = true) {
|
|
160
|
+
const tick = String.fromCharCode(96), source = String(value), fragments = [];
|
|
161
|
+
let index = 0, plain = '';
|
|
162
|
+
const flush = () => { if (plain) fragments.push(escape(plain)); plain = ''; };
|
|
163
|
+
const codeEnd = start => {
|
|
164
|
+
const end = source.indexOf(tick, start + 1), newline = source.indexOf('\n', start + 1);
|
|
165
|
+
return end > start + 1 && (newline === -1 || end < newline) ? end : -1;
|
|
166
|
+
};
|
|
167
|
+
while (index < source.length) {
|
|
168
|
+
if (source[index] === tick && codeEnd(index) !== -1) {
|
|
169
|
+
const end = codeEnd(index);
|
|
170
|
+
flush(); fragments.push('<code>' + escape(source.slice(index + 1, end)) + '</code>'); index = end + 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const marker = source.slice(index, index + 2);
|
|
174
|
+
if (allowBold && (marker === '**' || marker === '__')) {
|
|
175
|
+
let end = index + 2;
|
|
176
|
+
while (end < source.length && source[end] !== '\n') {
|
|
177
|
+
// Code spans are opaque: bold markers inside them are literal text.
|
|
178
|
+
if (source[end] === tick && codeEnd(end) !== -1) { end = codeEnd(end) + 1; continue; }
|
|
179
|
+
if (source.startsWith(marker, end)) break;
|
|
180
|
+
end += 1;
|
|
181
|
+
}
|
|
182
|
+
if (end > index + 2 && source.startsWith(marker, end)) {
|
|
183
|
+
flush(); fragments.push('<strong>' + inline(source.slice(index + 2, end), false) + '</strong>'); index = end + 2;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
plain += source[index]; index += 1;
|
|
188
|
+
}
|
|
189
|
+
flush(); return fragments.join('');
|
|
190
|
+
}
|
|
191
|
+
function markdown(value) {
|
|
192
|
+
const lines = String(value).split('\n'), parts = [];
|
|
193
|
+
let paragraph = [], list = [], listType = '', listStart = '', quote = [];
|
|
194
|
+
const flushParagraph = () => { if (paragraph.length) parts.push('<p class="codex-prose">' + inline(paragraph.join('\n')) + '</p>'); paragraph = []; };
|
|
195
|
+
const flushList = () => {
|
|
196
|
+
if (list.length) parts.push('<' + listType + (listType === 'ol' && listStart !== '1' ? ' start="' + listStart + '"' : '') + '>'
|
|
197
|
+
+ list.map(line => '<li>' + inline(line) + '</li>').join('') + '</' + listType + '>');
|
|
198
|
+
list = []; listType = ''; listStart = '';
|
|
199
|
+
};
|
|
200
|
+
const flushQuote = () => { if (quote.length) parts.push('<blockquote>' + quote.map(line => inline(line)).join('<br>') + '</blockquote>'); quote = []; };
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
203
|
+
const unordered = /^\s{0,3}[-+*]\s+(.+)$/.exec(line);
|
|
204
|
+
const ordered = /^\s{0,3}(\d{1,9})[.)]\s+(.+)$/.exec(line);
|
|
205
|
+
const quoted = /^\s{0,3}>\s?(.*)$/.exec(line);
|
|
206
|
+
if (heading) {
|
|
207
|
+
flushParagraph(); flushList(); flushQuote();
|
|
208
|
+
const tag = 'h' + heading[1].length;
|
|
209
|
+
parts.push('<' + tag + '>' + inline(heading[2].replace(/\s+#+\s*$/, '')) + '</' + tag + '>');
|
|
210
|
+
} else if (unordered || ordered) {
|
|
211
|
+
flushParagraph(); flushQuote();
|
|
212
|
+
const nextType = unordered ? 'ul' : 'ol';
|
|
213
|
+
if (listType && listType !== nextType) flushList();
|
|
214
|
+
if (!listType) { listType = nextType; listStart = ordered ? String(Number(ordered[1])) : ''; }
|
|
215
|
+
list.push(unordered ? unordered[1] : ordered[2]);
|
|
216
|
+
} else if (quoted) {
|
|
217
|
+
flushParagraph(); flushList(); quote.push(quoted[1]);
|
|
218
|
+
} else {
|
|
219
|
+
flushList(); flushQuote();
|
|
220
|
+
if (!line.trim()) flushParagraph();
|
|
221
|
+
else paragraph.push(line);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
flushParagraph(); flushList(); flushQuote();
|
|
225
|
+
return '<div class="codex-markdown">' + parts.join('') + '</div>';
|
|
226
|
+
}
|
|
227
|
+
function prose(value) {
|
|
228
|
+
const fence = String.fromCharCode(96).repeat(3), lines = String(value ?? '').split('\n');
|
|
229
|
+
let html = '', plain = [], code = [], language = '', inCode = false;
|
|
230
|
+
const flush = () => { if (plain.length) html += markdown(plain.join('\n')); plain = []; };
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
if (line.trimStart().startsWith(fence)) {
|
|
233
|
+
if (inCode) { html += codeBlock(code.join('\n'), language); code = []; }
|
|
234
|
+
else { flush(); language = line.trimStart().slice(3).trim(); }
|
|
235
|
+
inCode = !inCode;
|
|
236
|
+
} else if (inCode) code.push(line);
|
|
237
|
+
else plain.push(line);
|
|
238
|
+
}
|
|
239
|
+
if (inCode) html += codeBlock(code.join('\n'), language);
|
|
240
|
+
flush(); return html;
|
|
241
|
+
}
|
|
242
|
+
function details(id, summary, content, defaultOpen = false, cls = 'codex-tool') {
|
|
243
|
+
const open = view().details.has(id) ? view().details.get(id) : defaultOpen;
|
|
244
|
+
return '<details class="' + cls + '" data-details-key="' + escape(id) + '"' + (open ? ' open' : '')
|
|
245
|
+
+ '><summary>' + summary + '</summary>' + content + '</details>';
|
|
246
|
+
}
|
|
247
|
+
function itemText(item) {
|
|
248
|
+
if (Array.isArray(item.content)) return item.content.map(part => part.text || part.type || '').join('\n');
|
|
249
|
+
return item.text || (typeof item.content === 'string' ? item.content : '') || '';
|
|
250
|
+
}
|
|
251
|
+
function itemCard(item, turn, index) {
|
|
252
|
+
const type = item.type || '', id = turn.id + ':' + (item.id || index), content = itemText(item);
|
|
253
|
+
const error = item.status === 'failed' || item.error || (item.exitCode != null && item.exitCode !== 0);
|
|
254
|
+
const status = '<span class="codex-status' + (error ? ' codex-status-failed' : '') + '">' + escape(error ? text('Failed', '失败') : statusLabel(item.status)) + '</span>';
|
|
255
|
+
if (type === 'userMessage' || type === 'agentMessage') {
|
|
256
|
+
return '<article class="codex-card codex-card-' + (type === 'userMessage' ? 'user' : 'agent') + '" data-card-key="' + escape(id)
|
|
257
|
+
+ '"><div class="codex-card-heading"><strong>' + escape(type === 'userMessage' ? text('You', '你') : text('Assistant', '助手'))
|
|
258
|
+
+ '</strong></div>' + prose(content) + '</article>';
|
|
259
|
+
}
|
|
260
|
+
const titles = {
|
|
261
|
+
commandExecution: text('Command', '命令'), fileChange: text('File changes', '文件修改'),
|
|
262
|
+
mcpToolCall: text('Tool call', '工具调用'), dynamicToolCall: text('Tool call', '工具调用'),
|
|
263
|
+
webSearch: text('Web search', '网页搜索'), reasoning: text('Reasoning', '推理'),
|
|
264
|
+
imageView: text('Image', '图片'), enteredReviewMode: text('Review started', '开始审阅'),
|
|
265
|
+
exitedReviewMode: text('Review completed', '完成审阅'), contextCompaction: text('Context compacted', '上下文压缩')
|
|
266
|
+
};
|
|
267
|
+
let body = '', preview = item.command || item.tool || item.query || content.split('\n')[0] || '';
|
|
268
|
+
if (type === 'commandExecution') {
|
|
269
|
+
if (item.command) body += codeBlock(item.command, text('Command', '命令'));
|
|
270
|
+
if (item.cwd) body += '<div class="codex-prose">' + escape(text('Directory: ', '目录:') + item.cwd) + '</div>';
|
|
271
|
+
if (item.aggregatedOutput) body += codeBlock(item.aggregatedOutput, text('Output', '输出'));
|
|
272
|
+
if (item.exitCode != null) body += '<div class="codex-card-heading">' + escape(text('Exit code: ', '退出码:') + item.exitCode) + '</div>';
|
|
273
|
+
} else if (type === 'fileChange' && Array.isArray(item.changes)) {
|
|
274
|
+
preview = item.changes.map(change => change.path || '').filter(Boolean).join(', ');
|
|
275
|
+
body = item.changes.map((change, fileIndex) => {
|
|
276
|
+
const kind = typeof change.kind === 'object' ? change.kind?.type : change.kind || '';
|
|
277
|
+
const label = { add: text('Added', '新增'), delete: text('Deleted', '删除'), update: text('Modified', '修改') }[kind] || kind;
|
|
278
|
+
return details(id + ':file:' + fileIndex,
|
|
279
|
+
'<strong>' + escape(change.path || text('File', '文件')) + '</strong><span class="codex-status">' + escape(label) + '</span>',
|
|
280
|
+
codeBlock(change.diff || JSON.stringify(change, null, 2), text('Executor-reported diff', '执行器报告的差异'), true), false);
|
|
281
|
+
}).join('');
|
|
282
|
+
} else {
|
|
283
|
+
if (content) body += prose(content);
|
|
284
|
+
const output = item.aggregatedOutput || item.result;
|
|
285
|
+
if (output) body += codeBlock(typeof output === 'string' ? output : JSON.stringify(output, null, 2), text('Result', '结果'));
|
|
286
|
+
if (!body) body = codeBlock(JSON.stringify(item, null, 2));
|
|
287
|
+
}
|
|
288
|
+
if (item.error) body += prose(typeof item.error === 'string' ? item.error : JSON.stringify(item.error, null, 2));
|
|
289
|
+
const summary = '<strong>' + escape(titles[type] || type || text('Event', '事件')) + '</strong><span class="codex-tool-preview">'
|
|
290
|
+
+ escape(String(preview).slice(0, 180)) + '</span>' + status;
|
|
291
|
+
return '<article class="codex-card' + (error ? ' codex-card-error' : '') + '" data-card-key="' + escape(id) + '">'
|
|
292
|
+
+ details(id, summary, body, Boolean(error) || !['completed', 'declined'].includes(item.status)) + '</article>';
|
|
293
|
+
}
|
|
294
|
+
function bindReadActions(container) {
|
|
295
|
+
const currentKey = key;
|
|
296
|
+
container.querySelectorAll('details[data-details-key]').forEach(node => {
|
|
297
|
+
node.addEventListener('toggle', () => { if (key === currentKey && node.isConnected !== false) view().details.set(node.dataset.detailsKey, node.open); });
|
|
298
|
+
});
|
|
299
|
+
container.querySelectorAll('button[data-copy]').forEach(button => {
|
|
300
|
+
const content = copies[Number(button.dataset.copy)];
|
|
301
|
+
button.addEventListener('click', async () => {
|
|
302
|
+
try {
|
|
303
|
+
await navigator.clipboard.writeText(content);
|
|
304
|
+
button.textContent = text('Copied', '已复制');
|
|
305
|
+
} catch (_) { button.textContent = text('Select text to copy', '请选择文本复制'); }
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function renderTimeline(turns, reading) {
|
|
310
|
+
// Page by items rather than turns: a single tool-heavy turn can contain hundreds of cards.
|
|
311
|
+
entries = turns.flatMap((turn, turnIndex) => (turn.items?.length ? turn.items : [null]).map((item, index) => ({
|
|
312
|
+
turn, turnIndex, item, index, id: JSON.stringify([turn.id, item?.id ?? index])
|
|
313
|
+
})));
|
|
314
|
+
let start = reading.follow ? Math.max(0, entries.length - PAGE_SIZE)
|
|
315
|
+
: reading.startKey ? entries.findIndex(entry => entry.id === reading.startKey) : 0;
|
|
316
|
+
// If retention removed the old anchor, show the oldest surviving history.
|
|
317
|
+
start = Math.max(0, start);
|
|
318
|
+
const visible = entries.slice(start, start + PAGE_SIZE);
|
|
319
|
+
reading.start = start; reading.end = start + visible.length; reading.total = entries.length;
|
|
320
|
+
reading.startKey = visible[0]?.id || '';
|
|
321
|
+
const contentSignature = JSON.stringify(visible.map(entry =>
|
|
322
|
+
[entry.turn.id, entry.turnIndex, entry.turn.status, entry.index, entry.item]));
|
|
323
|
+
const signature = window.hccUi.language + ':' + contentSignature;
|
|
324
|
+
const last = turns[turns.length - 1];
|
|
325
|
+
const updateSignature = JSON.stringify([contentSignature, turns.map(turn => [turn.id, turn.status, turn.items?.length || 0]),
|
|
326
|
+
last?.items?.[last.items.length - 1], last?.diff, last?.plan, state.events?.[state.events.length - 1]]);
|
|
327
|
+
if (!reading.follow && reading.signature && reading.signature !== updateSignature) reading.unread = true;
|
|
328
|
+
reading.signature = updateSignature;
|
|
329
|
+
if (signature !== timelineSignature) {
|
|
330
|
+
copies = [];
|
|
331
|
+
let previousTurn = null;
|
|
332
|
+
let html = visible.map(({ turn, turnIndex, item, index }) => {
|
|
333
|
+
let heading = '';
|
|
334
|
+
if (turn !== previousTurn) {
|
|
335
|
+
heading = (previousTurn ? '</section>' : '') + '<section class="codex-turn"><header class="codex-turn-heading"><strong>'
|
|
336
|
+
+ escape(text('Turn ', '第 ') + (turnIndex + 1) + (window.hccUi.language === 'zh' ? ' 轮' : ''))
|
|
337
|
+
+ '</strong><span>' + escape(statusLabel(turn.status)) + '</span></header>';
|
|
338
|
+
previousTurn = turn;
|
|
339
|
+
}
|
|
340
|
+
return heading + (item ? itemCard(item, turn, index) : '');
|
|
341
|
+
}).join('');
|
|
342
|
+
html = html ? html + '</section>' : '<p class="codex-prose">' + escape(text('Send a message to start this thread.', '发送消息开始当前会话。')) + '</p>';
|
|
343
|
+
if (html !== timelineHtml) {
|
|
344
|
+
document.getElementById('codexTimeline').innerHTML = html;
|
|
345
|
+
timelineHtml = html; bindReadActions(document.getElementById('codexTimeline'));
|
|
346
|
+
}
|
|
347
|
+
timelineSignature = signature; timelineCopies = copies.slice();
|
|
348
|
+
} else copies = timelineCopies.slice();
|
|
349
|
+
document.getElementById('codexHistory').hidden = entries.length <= PAGE_SIZE;
|
|
350
|
+
document.getElementById('codexHistory').setAttribute('aria-label', text('Conversation history', '会话历史'));
|
|
351
|
+
document.getElementById('codexEarlier').textContent = text('Earlier', '更早记录');
|
|
352
|
+
document.getElementById('codexEarlier').disabled = start === 0;
|
|
353
|
+
document.getElementById('codexLater').textContent = text('Newer', '更新记录');
|
|
354
|
+
document.getElementById('codexLater').disabled = reading.end === entries.length;
|
|
355
|
+
document.getElementById('codexHistoryRange').textContent = text('Events ', '第 ') + (entries.length ? start + 1 : 0)
|
|
356
|
+
+ '–' + reading.end + text(' of ', ' 条,共 ') + entries.length + text('', ' 条');
|
|
357
|
+
}
|
|
358
|
+
function sync() {
|
|
359
|
+
const next = draftKey();
|
|
360
|
+
if (next !== key) {
|
|
361
|
+
if (key && state) view().top = scroller.scrollTop || 0;
|
|
362
|
+
key = next; pending = null; draft.value = ''; state = null;
|
|
363
|
+
timelineHtml = ''; timelineSignature = ''; timelineCopies = []; copies = []; entries = [];
|
|
364
|
+
document.getElementById('codexHistory').hidden = true;
|
|
365
|
+
for (const id of ['codexStatus', 'codexTimeline', 'codexDiff', 'codexPlan', 'codexApprovals', 'codexSubmission']) document.getElementById(id).textContent = '';
|
|
366
|
+
document.getElementById('codexReview').open = view().details.get('review') || false;
|
|
367
|
+
document.getElementById('codexReview').hidden = true;
|
|
368
|
+
scroller.scrollTop = view().top;
|
|
369
|
+
try { const stored = JSON.parse(window.hccUi.safeGet(key) || '{}'); draft.value = stored.text || ''; pending = stored.pending || null; } catch (_) {}
|
|
370
|
+
}
|
|
371
|
+
document.getElementById('codexSend').textContent = turnId() ? text('Add instruction', '追加指令') : text('Send', '发送');
|
|
372
|
+
document.getElementById('codexDraftLabel').textContent = text('Message saved in this browser', '消息草稿保存在当前浏览器');
|
|
373
|
+
document.getElementById('codexInterrupt').textContent = text('Interrupt turn', '中断当前轮');
|
|
374
|
+
document.getElementById('codexRecover').textContent = text('Read current state', '读取当前状态');
|
|
375
|
+
document.getElementById('codexClear').textContent = text('Clear draft', '清除草稿');
|
|
376
|
+
document.getElementById('codexReviewLabel').textContent = text('Latest turn changes and plan', '最新一轮的变更与计划');
|
|
377
|
+
scroller.setAttribute('aria-label', text('Conversation', '会话内容'));
|
|
378
|
+
draft.placeholder = text('Write a message · Ctrl / ⌘ + Enter to send', '输入消息 · Ctrl / ⌘ + Enter 发送');
|
|
379
|
+
updateJump();
|
|
380
|
+
document.getElementById('codexSend').disabled = !bridge().canControl || submitting || !!pending;
|
|
381
|
+
document.getElementById('codexInterrupt').disabled = !bridge().canControl || !turnId();
|
|
382
|
+
document.getElementById('codexApprovals').querySelectorAll('button[data-approval]').forEach(button => {
|
|
383
|
+
button.disabled = !bridge().canControl || approving.has(button.dataset.requestKey) || (button.dataset.decision === 'accept' && button.dataset.truncated === 'true');
|
|
384
|
+
});
|
|
385
|
+
const approvals = state?.pendingApprovals || [], approvalJump = document.getElementById('codexApprovalJump');
|
|
386
|
+
approvalJump.hidden = !approvals.length;
|
|
387
|
+
approvalJump.textContent = text('Approval required: ', '待审批:') + approvals.length + text(' · View', ' · 查看');
|
|
388
|
+
if (pending) message(text('Submission is unconfirmed. Read the current state and check history before clearing the draft or sending again.', '提交结果尚未确认。请读取当前状态并核对历史,再清除草稿或重新发送。'));
|
|
389
|
+
if (state?.uncertainSubmissions?.length) {
|
|
390
|
+
document.getElementById('codexSend').disabled = true;
|
|
391
|
+
message(text('Executor admission is uncertain. Read history; if no acknowledgement arrives, stop this executor and explicitly resume its saved thread.', '执行器接收结果不确定。请读取历史;若回执仍未到达,请停止当前执行器后显式恢复保存的会话。'));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function action(name, body, target = subject()) {
|
|
395
|
+
const b = bridge();
|
|
396
|
+
return b.api(route(target, name),
|
|
397
|
+
{ method: 'POST', body: JSON.stringify({ ...body, action_token: target.token, epoch: target.epoch }) });
|
|
398
|
+
}
|
|
399
|
+
function render(value, resetScroll = false) {
|
|
400
|
+
sync(); state = value; sync();
|
|
401
|
+
const thread = activeThread();
|
|
402
|
+
document.getElementById('codexStatus').textContent = [text('Executor', '执行器') + ': ' + statusLabel(state.status),
|
|
403
|
+
text('Thread', '会话') + ': ' + (thread?.id || ''), turnId() ? text('Turn', '当前轮') + ': ' + turnId() : text('Idle', '空闲'),
|
|
404
|
+
state.truncated ? text('Retained history is partial', '保留历史不完整') : ''].filter(Boolean).join(' · ');
|
|
405
|
+
const turns = thread?.turns || [];
|
|
406
|
+
const reading = view(), oldTop = resetScroll ? 0 : reading.top;
|
|
407
|
+
// Keep the visible message anchored even when the executor drops older retained turns.
|
|
408
|
+
const anchor = resetScroll ? null : [...scroller.querySelectorAll('[data-card-key]')].find(node => node.getBoundingClientRect().bottom > scroller.getBoundingClientRect().top);
|
|
409
|
+
const anchorKey = anchor?.dataset.cardKey, anchorTop = anchor?.getBoundingClientRect().top;
|
|
410
|
+
renderTimeline(turns, reading);
|
|
411
|
+
const last = turns[turns.length - 1];
|
|
412
|
+
const plan = Array.isArray(last?.plan) ? last.plan : [];
|
|
413
|
+
document.getElementById('codexPlan').innerHTML = plan.length ? '<h3>' + escape(text('Plan', '执行计划')) + '</h3>'
|
|
414
|
+
+ (last.explanation ? prose(last.explanation) : '') + '<ol class="codex-plan">' + plan.map(step =>
|
|
415
|
+
'<li data-status="' + escape(step.status || '') + '"><span>' + escape(statusLabel(step.status)) + '</span><span>'
|
|
416
|
+
+ escape(step.step || '') + '</span></li>').join('') + '</ol>' : '';
|
|
417
|
+
document.getElementById('codexDiff').innerHTML = last?.diff ? '<h3>' + escape(text('Latest turn · Executor-reported changes', '最新一轮 · 执行器报告的变更'))
|
|
418
|
+
+ '</h3>' + codeBlock(last.diff, text('Diff', '差异'), true) : '';
|
|
419
|
+
const review = document.getElementById('codexReview');
|
|
420
|
+
review.hidden = !last?.diff && !plan.length;
|
|
421
|
+
bindReadActions(document.getElementById('codexPlan'));
|
|
422
|
+
bindReadActions(document.getElementById('codexDiff'));
|
|
423
|
+
const approvals = state.pendingApprovals || [];
|
|
424
|
+
const target = subject(), executorId = state.executorId;
|
|
425
|
+
const container = document.getElementById('codexApprovals');
|
|
426
|
+
window.hccInteractions.remember(container);
|
|
427
|
+
container.innerHTML = approvals.map((approval, index) => {
|
|
428
|
+
const params = approval.params || {}, fields = [
|
|
429
|
+
[text('Operation', '操作'), approval.kind === 'userInput' ? text('Answer questions', '回答问题') : approval.kind === 'permissions' ? text('Grant permissions', '授予权限') : approval.method?.includes('fileChange') ? text('Change files', '修改文件') : text('Run command', '执行命令')],
|
|
430
|
+
[text('Command', '命令'), params.command], [text('Directory', '目录'), params.cwd],
|
|
431
|
+
[text('Reason', '原因'), params.reason], [text('Path', '路径'), params.path],
|
|
432
|
+
[text('Permission root', '权限目录'), params.grantRoot]
|
|
433
|
+
].filter(([, value]) => value != null && value !== '');
|
|
434
|
+
return '<article class="codex-card codex-approval"><h3>' + escape(approval.kind === 'userInput' ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
|
|
435
|
+
+ '</h3><dl>' + fields.map(([label, value]) => '<dt>' + escape(label) + '</dt><dd>'
|
|
436
|
+
+ escape(typeof value === 'string' ? value : JSON.stringify(value)) + '</dd>').join('') + '</dl>'
|
|
437
|
+
+ (approval.truncated ? '<p class="codex-prose">' + escape(text('Parameters are truncated. Read the full operation before deciding.', '参数已截断,请核对完整操作后再决定。')) + '</p>' : '')
|
|
438
|
+
+ details('approval:' + approval.requestId, '<strong>' + escape(text('Raw parameters', '原始参数')) + '</strong>',
|
|
439
|
+
codeBlock(JSON.stringify(params, null, 2), 'JSON'), false)
|
|
440
|
+
+ '<div class="hcc-interaction-form">' + window.hccInteractions.form(approval, approvalKey(target, executorId, approval.requestId), escape, text) + '</div>'
|
|
441
|
+
+ '<div class="codex-approval-actions"><button data-approval="' + index + '" data-decision="accept" data-truncated="' + !!approval.truncated + '" data-request-key="' + escape(approvalKey(target, executorId, approval.requestId)) + '" type="button" class="primary">'
|
|
442
|
+
+ escape(approval.kind === 'userInput' ? text('Send answers', '提交答复') : approval.kind === 'permissions' ? text('Grant selected permissions', '授予所选权限') : text('Approve once', '批准本次')) + '</button><button data-approval="' + index + '" data-decision="' + (approval.kind === 'userInput' ? 'cancel' : 'decline') + '" data-request-key="' + escape(approvalKey(target, executorId, approval.requestId)) + '" type="button">'
|
|
443
|
+
+ escape(approval.kind === 'userInput' ? text('Cancel questions', '取消问答') : text('Decline', '拒绝')) + '</button></div></article>';
|
|
444
|
+
}).join('');
|
|
445
|
+
window.hccInteractions.restore(container);
|
|
446
|
+
bindReadActions(container);
|
|
447
|
+
container.querySelectorAll('button[data-approval]').forEach(button => {
|
|
448
|
+
button.disabled = !bridge().canControl || approving.has(button.dataset.requestKey) || (button.dataset.decision === 'accept' && button.dataset.truncated === 'true');
|
|
449
|
+
button.addEventListener('click', async () => {
|
|
450
|
+
if (!matches(target) || !bridge().canControl || approving.has(button.dataset.requestKey)) return;
|
|
451
|
+
const approval = approvals[Number(button.dataset.approval)];
|
|
452
|
+
const requestKey = approvalKey(target, executorId, approval.requestId);
|
|
453
|
+
approving.add(requestKey); sync();
|
|
454
|
+
const requestTarget = { ...target, token: bridge().actionToken, epoch: bridge().epoch };
|
|
455
|
+
try { await action('approve', { executorId, threadId: approval.threadId,
|
|
456
|
+
turnId: approval.turnId, requestId: approval.requestId,
|
|
457
|
+
...window.hccInteractions.payload(approval, ['userInput','permissions'].includes(approval.kind) ? button.closest('article') : null, button.dataset.decision) }, requestTarget);
|
|
458
|
+
window.hccInteractions.forget(requestKey); }
|
|
459
|
+
catch (error) { if (matches(target)) message(error.detail || error.message); }
|
|
460
|
+
finally { approving.delete(requestKey); if (matches(target)) sync(); }
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
if (reading.follow) scroller.scrollTop = scroller.scrollHeight;
|
|
464
|
+
else {
|
|
465
|
+
scroller.scrollTop = oldTop;
|
|
466
|
+
if (anchorKey) {
|
|
467
|
+
const restored = [...scroller.querySelectorAll('[data-card-key]')].find(node => node.dataset.cardKey === anchorKey);
|
|
468
|
+
if (restored) scroller.scrollTop += restored.getBoundingClientRect().top - anchorTop;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
reading.top = scroller.scrollTop;
|
|
472
|
+
updateJump();
|
|
473
|
+
}
|
|
474
|
+
scroller.addEventListener('scroll', () => {
|
|
475
|
+
// Clearing the old DOM clamps scrollTop before the next subject has state.
|
|
476
|
+
// Those layout events must not overwrite the saved position of that subject.
|
|
477
|
+
if (!state) return;
|
|
478
|
+
const current = view(); current.top = scroller.scrollTop; current.follow = pinned() && current.end === current.total;
|
|
479
|
+
if (current.follow) current.unread = false;
|
|
480
|
+
updateJump();
|
|
481
|
+
});
|
|
482
|
+
document.getElementById('codexJump').addEventListener('click', () => {
|
|
483
|
+
view().follow = true; view().unread = false;
|
|
484
|
+
if (state) render(state);
|
|
485
|
+
});
|
|
486
|
+
function pageHistory(direction) {
|
|
487
|
+
if (!state) return;
|
|
488
|
+
const reading = view(), start = Math.max(0, Math.min(entries.length - PAGE_SIZE, reading.start + direction * PAGE_SIZE));
|
|
489
|
+
reading.startKey = entries[start]?.id || ''; reading.follow = false;
|
|
490
|
+
reading.signature = ''; reading.top = 0;
|
|
491
|
+
render(state, true);
|
|
492
|
+
scroller.focus?.({ preventScroll: true });
|
|
493
|
+
}
|
|
494
|
+
document.getElementById('codexEarlier').addEventListener('click', () => pageHistory(-1));
|
|
495
|
+
document.getElementById('codexLater').addEventListener('click', () => pageHistory(1));
|
|
496
|
+
document.getElementById('codexApprovalJump').addEventListener('click', () => {
|
|
497
|
+
scroller.scrollTop = 0; view().top = 0; view().follow = pinned() && view().end === view().total; updateJump();
|
|
498
|
+
});
|
|
499
|
+
document.getElementById('codexReview').addEventListener('toggle', () => { view().details.set('review', document.getElementById('codexReview').open); });
|
|
500
|
+
draft.addEventListener('input', save);
|
|
501
|
+
draft.addEventListener('keydown', event => {
|
|
502
|
+
if (!event.isComposing && event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
if (!document.getElementById('codexSend').disabled) document.getElementById('codexSend').click();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
document.getElementById('codexSend').addEventListener('click', async () => {
|
|
508
|
+
if (!bridge().canControl || !draft.value.trim() || pending || submitting) return;
|
|
509
|
+
const submittedText = draft.value;
|
|
510
|
+
pending = 's_' + crypto.randomUUID(); submitting = true; save(); sync();
|
|
511
|
+
const target = subject(), submissionId = pending;
|
|
512
|
+
try {
|
|
513
|
+
const currentTurn = turnId();
|
|
514
|
+
await action(currentTurn ? 'steer' : 'turn', { text: submittedText, submissionId, turnId: currentTurn }, target);
|
|
515
|
+
let stored = {};
|
|
516
|
+
try { stored = JSON.parse(window.hccUi.safeGet(target.key) || '{}'); } catch (_) {}
|
|
517
|
+
if (stored.pending === submissionId) {
|
|
518
|
+
stored.pending = null;
|
|
519
|
+
if (stored.text === submittedText) stored.text = '';
|
|
520
|
+
window.hccUi.safeSet(target.key, JSON.stringify(stored));
|
|
521
|
+
}
|
|
522
|
+
if (matches(target)) {
|
|
523
|
+
pending = stored.pending || null; draft.value = stored.text || '';
|
|
524
|
+
message(text('Executor confirmed submission.', '执行器已确认提交。'));
|
|
525
|
+
}
|
|
526
|
+
} catch (error) { if (matches(target)) message((error.detail || error.message) + ' ' + text('Check current state before resending.', '重新发送前请核对当前状态。')); }
|
|
527
|
+
finally { submitting = false; sync(); }
|
|
528
|
+
});
|
|
529
|
+
document.getElementById('codexInterrupt').addEventListener('click', async () => {
|
|
530
|
+
const target = subject();
|
|
531
|
+
try { await action('interrupt', { turnId: turnId() }, target); if (matches(target)) message(text('Interrupt requested; awaiting state update.', '已请求中断,等待状态更新。')); }
|
|
532
|
+
catch (error) { if (matches(target)) message(error.detail || error.message); }
|
|
533
|
+
});
|
|
534
|
+
document.getElementById('codexRecover').addEventListener('click', async () => {
|
|
535
|
+
const b = bridge(), target = subject();
|
|
536
|
+
try {
|
|
537
|
+
await b.api(route(target, 'read'));
|
|
538
|
+
if (!matches(target)) return;
|
|
539
|
+
const result = await b.api(route(target, 'state'));
|
|
540
|
+
if (matches(target)) render(result.state);
|
|
541
|
+
} catch (error) { if (matches(target)) message(error.detail || error.message); }
|
|
542
|
+
});
|
|
543
|
+
document.getElementById('codexClear').addEventListener('click', () => {
|
|
544
|
+
if (!confirm(text('Clear this draft and its unconfirmed submission marker? Check thread history first.', '清除草稿及未确认提交标记?请先核对会话历史。'))) return;
|
|
545
|
+
pending = null; draft.value = ''; save(); message(''); sync();
|
|
546
|
+
});
|
|
547
|
+
window.addEventListener('hcc:preferences', () => { if (state && !panel.hidden) render(state); });
|
|
548
|
+
window.hccCodex = { render, sync };
|
|
549
|
+
})();`;
|
|
550
|
+
}
|