@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
package/lib/web/ui-codex.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { safeMarkdownScript, installSafeMarkdown } from './ui-safe-markdown.mjs';
|
|
2
|
+
import { interactionPanelScript, installInteractionPanel } from './ui-interactions.mjs';
|
|
3
|
+
import { codexAccountPanelHtml, codexAccountPanelCss, codexAccountPanelScript, installCodexAccountPanel } from './ui-codex-account.mjs';
|
|
4
|
+
import { sessionToolsCss, sessionToolsHtml, sessionToolsScript, metricsHtml, traceFilterHtml, installSessionTools } from './ui-session-tools.mjs';
|
|
2
5
|
|
|
3
6
|
export function codexPanelHtml() {
|
|
4
7
|
return `<style>
|
|
@@ -63,7 +66,11 @@ export function codexPanelHtml() {
|
|
|
63
66
|
.hcc-interaction-form label { display:grid; gap:5px; overflow-wrap:anywhere; }
|
|
64
67
|
.hcc-interaction-form label:has(input[type=checkbox]) { display:flex; align-items:flex-start; }
|
|
65
68
|
.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; }
|
|
66
|
-
.
|
|
69
|
+
.hcc-mcp-field { min-width:0; border:1px solid var(--border); border-radius:6px; padding:10px; }
|
|
70
|
+
.hcc-mcp-field legend,.hcc-mcp-field p { overflow-wrap:anywhere; }
|
|
71
|
+
.hcc-mcp-field small { color:var(--muted); }
|
|
72
|
+
.hcc-mcp-field .sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
|
|
73
|
+
.codex-approval-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:12px; }
|
|
67
74
|
.codex-jump { flex:none; text-align:center; padding:6px; border-top:1px solid var(--border); }
|
|
68
75
|
.codex-composer { flex:none; padding:12px 16px; border-top:1px solid var(--border); background:var(--panel); max-height:40%; overflow:auto; }
|
|
69
76
|
.codex-composer-inner { max-width:900px; margin:auto; }
|
|
@@ -80,12 +87,19 @@ export function codexPanelHtml() {
|
|
|
80
87
|
.codex-segment-nav button { min-height:44px; flex:1; }
|
|
81
88
|
.codex-approval dl { grid-template-columns:1fr; gap:3px; } .codex-approval dd { margin-bottom:6px; }
|
|
82
89
|
}
|
|
90
|
+
${codexAccountPanelCss()}
|
|
91
|
+
${sessionToolsCss}
|
|
92
|
+
.codex-event-source { width:min(780px,calc(100vw - 24px)); max-height:80dvh; overflow:auto; padding:16px; background:var(--panel); color:var(--text); border:1px solid var(--border); border-radius:8px; }
|
|
93
|
+
.codex-event-source textarea { width:100%; min-height:240px; max-height:50dvh; resize:vertical; font-family:var(--mono); color:var(--text); background:var(--input-bg); }
|
|
94
|
+
.codex-event-source .row { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
83
95
|
</style>
|
|
84
96
|
<section id="codexPanel" hidden aria-label="Codex">
|
|
85
97
|
<header class="codex-header"><div id="codexStatus" role="status"></div><button id="codexApprovalJump" type="button" hidden>Approval required</button></header>
|
|
86
98
|
<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>
|
|
87
99
|
<div id="codexScroll" class="codex-scroll" tabindex="0" aria-label="Conversation">
|
|
88
100
|
<div id="codexApprovals" aria-live="polite"></div>
|
|
101
|
+
${codexAccountPanelHtml('codex')}
|
|
102
|
+
${traceFilterHtml('codex')}${metricsHtml('codex')}
|
|
89
103
|
<div id="codexTimeline"></div>
|
|
90
104
|
<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>
|
|
91
105
|
</div>
|
|
@@ -93,30 +107,44 @@ export function codexPanelHtml() {
|
|
|
93
107
|
<footer class="codex-composer"><div class="codex-composer-inner">
|
|
94
108
|
<label for="codexDraft" id="codexDraftLabel">Message saved in this browser</label>
|
|
95
109
|
<textarea id="codexDraft" rows="3"></textarea>
|
|
110
|
+
${sessionToolsHtml('codex')}
|
|
96
111
|
<div class="codex-composer-actions">
|
|
97
112
|
<button id="codexSend" class="primary" type="button">Send</button><button id="codexInterrupt" type="button">Interrupt turn</button>
|
|
98
113
|
<button id="codexRecover" type="button">Read current state</button><button id="codexClear" type="button">Clear draft</button>
|
|
99
114
|
</div><p id="codexSubmission" role="status"></p>
|
|
100
115
|
</div></footer>
|
|
116
|
+
<dialog id="codexEventDialog" class="codex-event-source" aria-labelledby="codexEventTitle"><h3 id="codexEventTitle">Reported event</h3><p id="codexEventBoundary"></p><textarea id="codexEventSource" readonly aria-label="Reported event JSON"></textarea><div class="row"><button id="codexEventCopy" type="button">Copy full event</button><button id="codexEventClose" type="button">Back to result</button></div></dialog>
|
|
101
117
|
</section>`;
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
export function codexPanelScript() {
|
|
105
|
-
return interactionPanelScript() +
|
|
121
|
+
return sessionToolsScript() + safeMarkdownScript() + codexAccountPanelScript() + interactionPanelScript() + "(" + installCodexPanel.toString() + ")(false);";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function installCodexPanel(installDependencies = true) {
|
|
125
|
+
if (installDependencies) { installSessionTools(); installSafeMarkdown(); installCodexAccountPanel(); installInteractionPanel(); }
|
|
106
126
|
const panel = document.getElementById('codexPanel');
|
|
107
127
|
const draft = document.getElementById('codexDraft');
|
|
108
128
|
const scroller = document.getElementById('codexScroll');
|
|
109
129
|
const views = new Map();
|
|
110
130
|
const approving = new Set();
|
|
131
|
+
const accountPending = new Set(), accountReads = new Set();
|
|
111
132
|
const PAGE_SIZE = 80;
|
|
112
133
|
const SEGMENT_CHARS = 12000, SEGMENT_LINES = 200;
|
|
113
134
|
let copies = [], timelineHtml = '', timelineSignature = '', timelineCopies = [], entries = [];
|
|
114
135
|
let blocks = [], timelineBlocks = [], blockScope = '', blockOrder = 0;
|
|
115
136
|
let state = null, key = '', pending = null, submitting = false;
|
|
116
137
|
let stateRevision = 0, subjectRevision = 0, readSequence = 0;
|
|
138
|
+
let inspectedEvent = '';
|
|
117
139
|
const text = (en, zh) => window.hccUi.language === 'zh' ? zh : en;
|
|
118
140
|
const bridge = () => window.hccHandoff;
|
|
119
141
|
const escape = (value) => bridge().esc(String(value ?? ''));
|
|
142
|
+
const contextTools = window.hccCreateSessionTools({ document, window, prefix: 'codex', bridge, draft, save: () => { save(); sync(); }, state: () => state, text, escape });
|
|
143
|
+
const traceFilter = window.hccCreateTraceFilter({ document, prefix: 'codex', text, escape, changed: () => {
|
|
144
|
+
if (!state) return;
|
|
145
|
+
const reading = view(); reading.follow = false; reading.startKey = ''; reading.top = 0; timelineSignature = '';
|
|
146
|
+
render(state, true);
|
|
147
|
+
} });
|
|
120
148
|
function draftKey() { return 'hcc.codexDraft:' + bridge().projectRoot + ':' + bridge().active + (bridge().draftScope ? ':' + bridge().draftScope : ''); }
|
|
121
149
|
function subject() { const b = bridge(); return { root: b.projectRoot, id: b.active, key: draftKey(), token: b.actionToken, epoch: b.epoch }; }
|
|
122
150
|
function matches(target) { return target.root === bridge().projectRoot && target.id === bridge().active; }
|
|
@@ -207,74 +235,7 @@ export function codexPanelScript() {
|
|
|
207
235
|
+ '</span><button type="button" data-copy="' + index + '">' + escape(ranges.length > 1 ? text('Copy full text', '复制全文') : text('Copy', '复制'))
|
|
208
236
|
+ '</button></div>' + body + '</div>';
|
|
209
237
|
}
|
|
210
|
-
|
|
211
|
-
const tick = String.fromCharCode(96), source = String(value), fragments = [];
|
|
212
|
-
let index = 0, plain = '';
|
|
213
|
-
const flush = () => { if (plain) fragments.push(escape(plain)); plain = ''; };
|
|
214
|
-
const codeEnd = start => {
|
|
215
|
-
const end = source.indexOf(tick, start + 1), newline = source.indexOf('\n', start + 1);
|
|
216
|
-
return end > start + 1 && (newline === -1 || end < newline) ? end : -1;
|
|
217
|
-
};
|
|
218
|
-
while (index < source.length) {
|
|
219
|
-
if (source[index] === tick && codeEnd(index) !== -1) {
|
|
220
|
-
const end = codeEnd(index);
|
|
221
|
-
flush(); fragments.push('<code>' + escape(source.slice(index + 1, end)) + '</code>'); index = end + 1;
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
const marker = source.slice(index, index + 2);
|
|
225
|
-
if (allowBold && (marker === '**' || marker === '__')) {
|
|
226
|
-
let end = index + 2;
|
|
227
|
-
while (end < source.length && source[end] !== '\n') {
|
|
228
|
-
// Code spans are opaque: bold markers inside them are literal text.
|
|
229
|
-
if (source[end] === tick && codeEnd(end) !== -1) { end = codeEnd(end) + 1; continue; }
|
|
230
|
-
if (source.startsWith(marker, end)) break;
|
|
231
|
-
end += 1;
|
|
232
|
-
}
|
|
233
|
-
if (end > index + 2 && source.startsWith(marker, end)) {
|
|
234
|
-
flush(); fragments.push('<strong>' + inline(source.slice(index + 2, end), false) + '</strong>'); index = end + 2;
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
plain += source[index]; index += 1;
|
|
239
|
-
}
|
|
240
|
-
flush(); return fragments.join('');
|
|
241
|
-
}
|
|
242
|
-
function markdown(value) {
|
|
243
|
-
const lines = String(value).split('\n'), parts = [];
|
|
244
|
-
let paragraph = [], list = [], listType = '', listStart = '', quote = [];
|
|
245
|
-
const flushParagraph = () => { if (paragraph.length) parts.push('<p class="codex-prose">' + inline(paragraph.join('\n')) + '</p>'); paragraph = []; };
|
|
246
|
-
const flushList = () => {
|
|
247
|
-
if (list.length) parts.push('<' + listType + (listType === 'ol' && listStart !== '1' ? ' start="' + listStart + '"' : '') + '>'
|
|
248
|
-
+ list.map(line => '<li>' + inline(line) + '</li>').join('') + '</' + listType + '>');
|
|
249
|
-
list = []; listType = ''; listStart = '';
|
|
250
|
-
};
|
|
251
|
-
const flushQuote = () => { if (quote.length) parts.push('<blockquote>' + quote.map(line => inline(line)).join('<br>') + '</blockquote>'); quote = []; };
|
|
252
|
-
for (const line of lines) {
|
|
253
|
-
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
254
|
-
const unordered = /^\s{0,3}[-+*]\s+(.+)$/.exec(line);
|
|
255
|
-
const ordered = /^\s{0,3}(\d{1,9})[.)]\s+(.+)$/.exec(line);
|
|
256
|
-
const quoted = /^\s{0,3}>\s?(.*)$/.exec(line);
|
|
257
|
-
if (heading) {
|
|
258
|
-
flushParagraph(); flushList(); flushQuote();
|
|
259
|
-
const tag = 'h' + heading[1].length;
|
|
260
|
-
parts.push('<' + tag + '>' + inline(heading[2].replace(/\s+#+\s*$/, '')) + '</' + tag + '>');
|
|
261
|
-
} else if (unordered || ordered) {
|
|
262
|
-
flushParagraph(); flushQuote();
|
|
263
|
-
const nextType = unordered ? 'ul' : 'ol';
|
|
264
|
-
if (listType && listType !== nextType) flushList();
|
|
265
|
-
if (!listType) { listType = nextType; listStart = ordered ? String(Number(ordered[1])) : ''; }
|
|
266
|
-
list.push(unordered ? unordered[1] : ordered[2]);
|
|
267
|
-
} else if (quoted) {
|
|
268
|
-
flushParagraph(); flushList(); quote.push(quoted[1]);
|
|
269
|
-
} else {
|
|
270
|
-
flushList(); flushQuote();
|
|
271
|
-
if (!line.trim()) flushParagraph();
|
|
272
|
-
else paragraph.push(line);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
flushParagraph(); flushList(); flushQuote();
|
|
276
|
-
return '<div class="codex-markdown">' + parts.join('') + '</div>';
|
|
277
|
-
}
|
|
238
|
+
const markdown = window.hccSafeMarkdown(escape);
|
|
278
239
|
function prose(value) {
|
|
279
240
|
const content = String(value ?? '');
|
|
280
241
|
if (segmentRanges(content).length > 1) return codeBlock(content, text('Long message · Plain-text sections', '长消息 · 纯文本分段'), false, true);
|
|
@@ -309,7 +270,7 @@ export function codexPanelScript() {
|
|
|
309
270
|
if (type === 'userMessage' || type === 'agentMessage') {
|
|
310
271
|
return '<article class="codex-card codex-card-' + (type === 'userMessage' ? 'user' : 'agent') + '" data-card-key="' + escape(id)
|
|
311
272
|
+ '"><div class="codex-card-heading"><strong>' + escape(type === 'userMessage' ? text('You', '你') : text('Assistant', '助手'))
|
|
312
|
-
+ '</strong></div>' + prose(content) + '</article>';
|
|
273
|
+
+ '</strong><button type="button" data-codex-event="' + escape(id) + '">' + escape(text('Event details', '事件详情')) + '</button></div>' + prose(content) + '</article>';
|
|
313
274
|
}
|
|
314
275
|
const titles = {
|
|
315
276
|
commandExecution: text('Command', '命令'), fileChange: text('File changes', '文件修改'),
|
|
@@ -343,10 +304,24 @@ export function codexPanelScript() {
|
|
|
343
304
|
const summary = '<strong>' + escape(titles[type] || type || text('Event', '事件')) + '</strong><span class="codex-tool-preview">'
|
|
344
305
|
+ escape(String(preview).slice(0, 180)) + '</span>' + status;
|
|
345
306
|
return '<article class="codex-card' + (error ? ' codex-card-error' : '') + '" data-card-key="' + escape(id) + '">'
|
|
346
|
-
+ details(id, summary, body, Boolean(error) || !['completed', 'declined'].includes(item.status)) + '</article>';
|
|
307
|
+
+ details(id, summary, body, Boolean(error) || !['completed', 'declined'].includes(item.status)) + '<button type="button" data-codex-event="' + escape(id) + '">' + escape(text('Event details', '事件详情')) + '</button></article>';
|
|
347
308
|
}
|
|
348
309
|
function bindReadActions(container) {
|
|
349
310
|
const currentKey = key;
|
|
311
|
+
container.querySelectorAll('button[data-codex-event]').forEach(button => button.addEventListener('click', () => {
|
|
312
|
+
if (key !== currentKey) return;
|
|
313
|
+
const entry = entries.find(({turn, item, index}) => turn.id + ':' + (item?.id || index) === button.dataset.codexEvent);
|
|
314
|
+
if (!entry) return;
|
|
315
|
+
inspectedEvent = JSON.stringify({ threadId: activeThread()?.id, turnId: entry.turn.id, item: entry.item }, null, 2);
|
|
316
|
+
let end = Math.min(inspectedEvent.length, 12000);
|
|
317
|
+
if (end < inspectedEvent.length && /[\uD800-\uDBFF]/.test(inspectedEvent[end-1])) end--;
|
|
318
|
+
document.getElementById('codexEventSource').value = inspectedEvent.slice(0,end);
|
|
319
|
+
document.getElementById('codexEventTitle').textContent = text('Reported event', '执行器报告的事件') + ' · ' + entry.turn.id;
|
|
320
|
+
document.getElementById('codexEventBoundary').textContent = end + ' / ' + inspectedEvent.length + text(' characters shown. This is the retained item, not an inferred transcript.', ' 个字符已显示;这是保留的事件内容,不是推断的完整记录。');
|
|
321
|
+
document.getElementById('codexEventCopy').textContent = text('Copy full event', '复制完整事件');
|
|
322
|
+
document.getElementById('codexEventClose').textContent = text('Back to result', '回到结果');
|
|
323
|
+
document.getElementById('codexEventDialog').showModal();
|
|
324
|
+
}));
|
|
350
325
|
container.querySelectorAll('details[data-details-key]').forEach(node => {
|
|
351
326
|
node.addEventListener('toggle', () => { if (key === currentKey && node.isConnected !== false) view().details.set(node.dataset.detailsKey, node.open); });
|
|
352
327
|
});
|
|
@@ -415,6 +390,12 @@ export function codexPanelScript() {
|
|
|
415
390
|
entries = turns.flatMap((turn, turnIndex) => (turn.items?.length ? turn.items : [null]).map((item, index) => ({
|
|
416
391
|
turn, turnIndex, item, index, id: JSON.stringify([turn.id, item?.id ?? index])
|
|
417
392
|
})));
|
|
393
|
+
const total = entries.length;
|
|
394
|
+
entries = entries.filter(({ turn, item }) => window.hccTraceMatches({ turnId: turn.id,
|
|
395
|
+
kind: item?.type === 'fileChange' ? 'files' : ['commandExecution','mcpToolCall','dynamicToolCall','webSearch'].includes(item?.type) ? 'tool' : item?.error ? 'error' : 'message',
|
|
396
|
+
status: item?.status || turn.status, exitCode: item?.exitCode, title: item?.type, text: item ? JSON.stringify(item) : turn.error?.message || '',
|
|
397
|
+
changes: item?.changes || [] }, traceFilter.value()));
|
|
398
|
+
traceFilter.render(turns.map(turn => turn.id), entries.length, total);
|
|
418
399
|
let start = reading.follow ? Math.max(0, entries.length - PAGE_SIZE)
|
|
419
400
|
: reading.startKey ? entries.findIndex(entry => entry.id === reading.startKey) : 0;
|
|
420
401
|
// If retention removed the old anchor, show the oldest surviving history.
|
|
@@ -465,14 +446,22 @@ export function codexPanelScript() {
|
|
|
465
446
|
subjectRevision += 1;
|
|
466
447
|
if (key && state) view().top = scroller.scrollTop || 0;
|
|
467
448
|
key = next; pending = null; draft.value = ''; state = null;
|
|
449
|
+
traceFilter.reset(key);
|
|
450
|
+
traceFilter.render([], 0, 0);
|
|
451
|
+
inspectedEvent = ''; document.getElementById('codexEventSource').value = '';
|
|
452
|
+
document.getElementById('codexEventDialog').close?.();
|
|
468
453
|
timelineHtml = ''; timelineSignature = ''; timelineCopies = []; copies = []; entries = []; blocks = []; timelineBlocks = [];
|
|
469
454
|
document.getElementById('codexHistory').hidden = true;
|
|
470
455
|
for (const id of ['codexStatus', 'codexTimeline', 'codexDiff', 'codexPlan', 'codexApprovals', 'codexSubmission']) document.getElementById(id).textContent = '';
|
|
471
456
|
document.getElementById('codexReview').open = view().details.get('review') || false;
|
|
457
|
+
document.getElementById('codexAccount').open = view().details.get('account') || false;
|
|
472
458
|
document.getElementById('codexReview').hidden = true;
|
|
473
459
|
scroller.scrollTop = view().top;
|
|
474
460
|
try { const stored = JSON.parse(window.hccUi.safeGet(key) || '{}'); draft.value = stored.text || ''; pending = stored.pending || null; } catch (_) {}
|
|
475
461
|
}
|
|
462
|
+
window.hccCodexAccount.render('codex', state?.account, { connected: state?.status === 'ready', pending: accountPending.has(key + '/' + state?.executorId) });
|
|
463
|
+
contextTools.render();
|
|
464
|
+
window.hccRenderReportedMetrics({ document, prefix: 'codex', state, text, escape });
|
|
476
465
|
document.getElementById('codexSend').textContent = turnId() ? text('Add instruction', '追加指令') : text('Send', '发送');
|
|
477
466
|
document.getElementById('codexDraftLabel').textContent = text('Message saved in this browser', '消息草稿保存在当前浏览器');
|
|
478
467
|
document.getElementById('codexInterrupt').textContent = text('Interrupt turn', '中断当前轮');
|
|
@@ -506,6 +495,12 @@ export function codexPanelScript() {
|
|
|
506
495
|
// Local paging and preference redraws reuse state; incoming snapshots do not.
|
|
507
496
|
if (value !== state) stateRevision += 1;
|
|
508
497
|
state = value; sync();
|
|
498
|
+
const accountKey = key + '/' + state.executorId;
|
|
499
|
+
if (state.status === 'ready' && state.executorId && !accountReads.has(accountKey)) {
|
|
500
|
+
accountReads.add(accountKey);
|
|
501
|
+
if (accountReads.size > 100) accountReads.delete(accountReads.values().next().value);
|
|
502
|
+
void readAccount();
|
|
503
|
+
}
|
|
509
504
|
const thread = activeThread();
|
|
510
505
|
document.getElementById('codexStatus').textContent = [text('Executor', '执行器') + ': ' + statusLabel(state.status),
|
|
511
506
|
text('Thread', '会话') + ': ' + (thread?.id || ''), turnId() ? text('Turn', '当前轮') + ': ' + turnId() : text('Idle', '空闲'),
|
|
@@ -536,12 +531,12 @@ export function codexPanelScript() {
|
|
|
536
531
|
container.innerHTML = approvals.map((approval, index) => {
|
|
537
532
|
blockScope = 'approval:' + executorId + ':' + approval.requestId; blockOrder = 0;
|
|
538
533
|
const params = approval.params || {}, fields = [
|
|
539
|
-
[text('Operation', '操作'), approval.kind === 'userInput' ? text('Answer questions', '回答问题') : approval.kind === 'permissions' ? text('Grant permissions', '授予权限') : approval.method?.includes('fileChange') ? text('Change files', '修改文件') : text('Run command', '执行命令')],
|
|
534
|
+
[text('Operation', '操作'), approval.kind === 'mcp' ? text('MCP request', 'MCP 请求') : approval.kind === 'userInput' ? text('Answer questions', '回答问题') : approval.kind === 'permissions' ? text('Grant permissions', '授予权限') : approval.method?.includes('fileChange') ? text('Change files', '修改文件') : text('Run command', '执行命令')],
|
|
540
535
|
[text('Command', '命令'), params.command], [text('Directory', '目录'), params.cwd],
|
|
541
|
-
[text('Reason', '原因'), params.reason], [text('Path', '路径'), params.path],
|
|
536
|
+
[text('MCP server', 'MCP 服务'), params.serverName], [text('Reason', '原因'), params.reason || params.message], [text('Path', '路径'), params.path],
|
|
542
537
|
[text('Permission root', '权限目录'), params.grantRoot]
|
|
543
538
|
].filter(([, value]) => value != null && value !== '');
|
|
544
|
-
return '<article class="codex-card codex-approval"><h3>' + escape(approval
|
|
539
|
+
return '<article class="codex-card codex-approval"><h3>' + escape(window.hccInteractions.requiresInput(approval) ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
|
|
545
540
|
+ '</h3><dl>' + fields.map(([label, value], fieldIndex) => {
|
|
546
541
|
const fullValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
547
542
|
return '<dt>' + escape(label) + '</dt><dd>'
|
|
@@ -550,11 +545,12 @@ export function codexPanelScript() {
|
|
|
550
545
|
}).join('') + '</dl>'
|
|
551
546
|
+ (approval.truncated ? '<p class="codex-prose">' + escape(text('Parameters are truncated. Read the full operation before deciding.', '参数已截断,请核对完整操作后再决定。')) + '</p>' : '')
|
|
552
547
|
+ details('approval:' + approval.requestId, '<strong>' + escape(text('Raw parameters', '原始参数')) + '</strong>',
|
|
553
|
-
codeBlock(JSON.stringify(
|
|
548
|
+
codeBlock(JSON.stringify(window.hccInteractions.previewParams(approval), null, 2), 'JSON'), false)
|
|
554
549
|
+ '<div class="hcc-interaction-form">' + window.hccInteractions.form(approval, approvalKey(target, executorId, approval.requestId), escape, text) + '</div>'
|
|
555
|
-
+ '<div class="codex-approval-actions"><button data-approval="' + index + '" data-decision="accept" data-truncated="' +
|
|
556
|
-
+ escape(
|
|
557
|
-
+ escape(approval.kind === 'userInput' ? text('Cancel questions', '取消问答') : text('Decline', '拒绝')) + '</button
|
|
550
|
+
+ '<div class="codex-approval-actions"><button data-approval="' + index + '" data-decision="accept" data-truncated="' + window.hccInteractions.approvalBlocked(approval) + '" data-request-key="' + escape(approvalKey(target, executorId, approval.requestId)) + '" type="button" class="primary">'
|
|
551
|
+
+ escape(window.hccInteractions.acceptLabel(approval, text)) + '</button><button data-approval="' + index + '" data-decision="' + (approval.kind === 'userInput' ? 'cancel' : 'decline') + '" data-request-key="' + escape(approvalKey(target, executorId, approval.requestId)) + '" type="button">'
|
|
552
|
+
+ escape(approval.kind === 'userInput' ? text('Cancel questions', '取消问答') : text('Decline', '拒绝')) + '</button>'
|
|
553
|
+
+ (approval.kind === 'mcp' ? '<button data-approval="' + index + '" data-decision="cancel" data-request-key="' + escape(approvalKey(target, executorId, approval.requestId)) + '" type="button">' + escape(text('Cancel request', '取消请求')) + '</button>' : '') + '</div></article>';
|
|
558
554
|
}).join('');
|
|
559
555
|
window.hccInteractions.restore(container);
|
|
560
556
|
bindReadActions(container);
|
|
@@ -566,11 +562,18 @@ export function codexPanelScript() {
|
|
|
566
562
|
const requestKey = approvalKey(target, executorId, approval.requestId);
|
|
567
563
|
approving.add(requestKey); sync();
|
|
568
564
|
const requestTarget = { ...target, token: bridge().actionToken, epoch: bridge().epoch };
|
|
569
|
-
|
|
565
|
+
let authorization;
|
|
566
|
+
try {
|
|
567
|
+
const payload = window.hccInteractions.payload(approval, ['userInput','permissions','mcp'].includes(approval.kind) ? button.closest('article') : null, button.dataset.decision);
|
|
568
|
+
authorization = window.hccInteractions.prepareUrl(approval, button.dataset.decision, text);
|
|
569
|
+
await action('approve', { executorId, threadId: approval.threadId,
|
|
570
570
|
turnId: approval.turnId, requestId: approval.requestId,
|
|
571
|
-
...
|
|
572
|
-
window.hccInteractions.forget(requestKey);
|
|
573
|
-
|
|
571
|
+
...payload }, requestTarget);
|
|
572
|
+
window.hccInteractions.forget(requestKey);
|
|
573
|
+
const opened = authorization?.complete();
|
|
574
|
+
if (opened && matches(target)) message(opened);
|
|
575
|
+
}
|
|
576
|
+
catch (error) { authorization?.cancel(); if (matches(target)) message(error.detail || error.message); }
|
|
574
577
|
finally { approving.delete(requestKey); if (matches(target)) sync(); }
|
|
575
578
|
});
|
|
576
579
|
});
|
|
@@ -611,7 +614,25 @@ export function codexPanelScript() {
|
|
|
611
614
|
scroller.scrollTop = 0; view().top = 0; view().follow = pinned() && view().end === view().total; updateJump();
|
|
612
615
|
});
|
|
613
616
|
document.getElementById('codexReview').addEventListener('toggle', () => { view().details.set('review', document.getElementById('codexReview').open); });
|
|
617
|
+
document.getElementById('codexAccount').addEventListener('toggle', () => { view().details.set('account', document.getElementById('codexAccount').open); });
|
|
618
|
+
async function readAccount() {
|
|
619
|
+
if (!state?.executorId || state.status !== 'ready' || accountPending.has(key + '/' + state.executorId)) return;
|
|
620
|
+
const target = { ...subject(), executorId: state.executorId, revision: subjectRevision }, version = stateRevision;
|
|
621
|
+
const accountScope = target.key + '/' + target.executorId;
|
|
622
|
+
accountPending.add(accountScope); sync();
|
|
623
|
+
try {
|
|
624
|
+
const result = await bridge().api(route(target, 'account') + '&executorId=' + encodeURIComponent(target.executorId));
|
|
625
|
+
if (matches(target) && target.revision === subjectRevision && state?.executorId === result.executorId && version === stateRevision) render({ ...state, account: result.account });
|
|
626
|
+
} catch (error) { if (matches(target) && target.revision === subjectRevision && state?.executorId === target.executorId && version === stateRevision) message(error.detail || error.message); }
|
|
627
|
+
finally { accountPending.delete(accountScope); if (matches(target)) sync(); }
|
|
628
|
+
}
|
|
629
|
+
document.getElementById('codexAccountRead').addEventListener('click', readAccount);
|
|
614
630
|
draft.addEventListener('input', save);
|
|
631
|
+
document.getElementById('codexEventClose').addEventListener('click', () => document.getElementById('codexEventDialog').close());
|
|
632
|
+
document.getElementById('codexEventCopy').addEventListener('click', async () => {
|
|
633
|
+
try { await navigator.clipboard.writeText(inspectedEvent); document.getElementById('codexEventCopy').textContent = text('Copied', '已复制'); }
|
|
634
|
+
catch { document.getElementById('codexEventCopy').textContent = text('Select text to copy', '请选择文本复制'); }
|
|
635
|
+
});
|
|
615
636
|
draft.addEventListener('keydown', event => {
|
|
616
637
|
if (!event.isComposing && event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
617
638
|
event.preventDefault();
|
|
@@ -663,5 +684,4 @@ export function codexPanelScript() {
|
|
|
663
684
|
});
|
|
664
685
|
window.addEventListener('hcc:preferences', () => { if (state && !panel.hidden) render(state); });
|
|
665
686
|
window.hccCodex = { render, sync };
|
|
666
|
-
})();`;
|
|
667
687
|
}
|
|
@@ -47,10 +47,26 @@ export function commandPaletteHtml() {
|
|
|
47
47
|
</div>`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export function commandPaletteScript() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
export function commandPaletteScript() { return '(' + installCommandPalette.toString() + ')();'; }
|
|
51
|
+
|
|
52
|
+
export function installCommandPalette() {
|
|
53
|
+
(() => {
|
|
54
|
+
const filter = function filterCommands(commands, query) {
|
|
55
|
+
const words = String(query || '').toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
56
|
+
return commands.filter(command => {
|
|
57
|
+
const text = [command.id, command.label, command.detail, command.group, command.keywords].join(' ').toLocaleLowerCase();
|
|
58
|
+
return words.every(word => text.includes(word));
|
|
59
|
+
}).slice(0, 80);
|
|
60
|
+
};
|
|
61
|
+
const shortcut = function shortcutAction(event, { modalOpen = false, paletteOpen = false, terminalAvailable = false } = {}) {
|
|
62
|
+
if (event.isComposing || event.repeat || event.altKey || !(event.ctrlKey || event.metaKey) || !event.shiftKey) return null;
|
|
63
|
+
const key = String(event.key || '').toLowerCase();
|
|
64
|
+
if (key === 'p' && (!modalOpen || paletteOpen)) return 'palette';
|
|
65
|
+
if (modalOpen) return null;
|
|
66
|
+
if (key === 'l') return 'sessions';
|
|
67
|
+
if (key === 'f' && terminalAvailable) return 'terminal-find';
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
54
70
|
const host = window.hccCommandHost;
|
|
55
71
|
const dialog = document.getElementById('commandDialog');
|
|
56
72
|
const query = document.getElementById('commandQuery');
|
|
@@ -115,5 +131,5 @@ export function commandPaletteScript() {
|
|
|
115
131
|
document.getElementById('commandClose').addEventListener('click', () => host.closeDialog(dialog));
|
|
116
132
|
window.addEventListener('hcc:preferences', refresh);
|
|
117
133
|
window.hccCommandPalette = {open, refresh, captures};
|
|
118
|
-
})()
|
|
134
|
+
})();
|
|
119
135
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
export const fileWritesCss = `
|
|
2
|
+
.files-upload { border:1px solid var(--border); border-radius:8px; padding:8px; }
|
|
3
|
+
.files-upload form { display:grid; gap:6px; padding-top:8px; }
|
|
4
|
+
.files-upload input { width:100%; min-width:0; }
|
|
5
|
+
.files-upload input[type=file] { height:auto; font-size:var(--small-font); }
|
|
6
|
+
.files-editor { flex:1; min-height:0; display:flex; flex-direction:column; gap:8px; }
|
|
7
|
+
.files-editor textarea { flex:1; min-height:150px; width:100%; resize:vertical; background:var(--input-bg); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:10px; font:var(--small-font)/1.6 var(--mono); }
|
|
8
|
+
.files-write-status { margin:0; overflow-wrap:anywhere; font-size:var(--small-font); color:var(--muted); }
|
|
9
|
+
.files-server-copy { overflow:auto; max-height:160px; }
|
|
10
|
+
.files-server-copy pre { white-space:pre-wrap; overflow-wrap:anywhere; }
|
|
11
|
+
`;
|
|
12
|
+
export function fileUploadHtml() {
|
|
13
|
+
return `<details class="files-upload" id="filesUploadPanel"><summary data-i18n="files.upload">Upload file</summary><form id="filesUploadForm">
|
|
14
|
+
<input id="filesUploadInput" type="file" data-i18n-aria="files.chooseUpload" aria-label="Choose one file to upload">
|
|
15
|
+
<label><span data-i18n="files.uploadName">Target file name</span><input id="filesUploadName" type="text" autocomplete="off" maxlength="255"></label>
|
|
16
|
+
<p id="filesUploadTarget" class="files-write-status"></p><p class="files-write-status" data-i18n="files.uploadHelp">One file, up to 10 MiB. Existing files will not be replaced.</p>
|
|
17
|
+
<div class="files-toolbar"><button id="filesUploadBtn" type="submit" data-i18n="files.upload">Upload file</button><button id="filesUploadCheck" type="button" data-i18n="files.checkUpload" hidden>Check target file</button></div>
|
|
18
|
+
<p id="filesUploadStatus" class="files-write-status" role="status" aria-live="polite"></p></form></details>`;
|
|
19
|
+
}
|
|
20
|
+
export function fileEditorHtml() {
|
|
21
|
+
return `<div class="files-toolbar"><button id="filesEdit" type="button" data-i18n="files.edit" hidden>Edit text</button></div>
|
|
22
|
+
<div id="filesEditPanel" class="files-editor" hidden><p class="files-write-status" data-i18n="files.draftHelp">Drafts stay in this page when you switch files or close this window. Reloading the page can discard them.</p>
|
|
23
|
+
<textarea id="filesEditor" spellcheck="false" data-i18n-aria="files.editText" aria-label="File text"></textarea>
|
|
24
|
+
<div class="files-toolbar"><button id="filesSave" type="button" data-i18n="files.save">Save file</button><button id="filesCancelEdit" type="button" data-i18n="files.keepDraft">Back to preview</button><button id="filesVerifySave" type="button" data-i18n="files.verifySave" hidden>Read saved file</button><button id="filesDiscardEdit" type="button" data-i18n="files.discardEdit">Discard draft and reload</button></div>
|
|
25
|
+
<p id="filesEditStatus" class="files-write-status" role="status" aria-live="polite"></p><details id="filesServerCopy" class="files-server-copy" hidden><summary data-i18n="files.serverCopy">Current saved content</summary><pre id="filesServerContent"></pre></details>
|
|
26
|
+
</div>`;
|
|
27
|
+
}
|
|
28
|
+
export function editableFile(value) {
|
|
29
|
+
return Boolean(value && value.editable !== false && ['text','markdown','html'].includes(value.kind) && value.encoding === 'utf8' && !value.truncated
|
|
30
|
+
&& typeof value.revision === 'string' && value.revision && typeof value.content === 'string' && value.size <= 1024*1024);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createFileWrites({browser=globalThis,context,updated,uploaded}) {
|
|
34
|
+
const {window,document}=browser, bridge=window.hccHandoff, node=id=>document.getElementById(id), tr=key=>bridge.tr(key);
|
|
35
|
+
const drafts=new Map(), uploads=new Map(); let active=null, upload=null;
|
|
36
|
+
const key=(root,path,identity='')=>JSON.stringify([root,identity,path]);
|
|
37
|
+
const route=(path,root)=>path+(path.includes('?') ? '&' : '?')+'root='+encodeURIComponent(root);
|
|
38
|
+
const writable=()=>bridge.draftScope!=='auxiliary';
|
|
39
|
+
const bound=now=>!now.hidden && now.root===bridge.projectRoot && (now.identity || '')===(bridge.projectIdentity || '');
|
|
40
|
+
const visible=target=>{ const now=context(); return bound(now) && now.root===target.root && now.identity===target.identity && now.path===target.path; };
|
|
41
|
+
const uploadVisible=target=>{ const now=context(); return bound(now) && now.root===target.root && now.identity===target.identity && now.directory===target.directory; };
|
|
42
|
+
const sameVisit=visit=>{const now=context();return bound(now) && now.root===visit.root && now.identity===visit.identity && now.visit===visit.visit && now.path===visit.path;};
|
|
43
|
+
const editorText=value=>value.replace(/\r\n?/g,'\n');
|
|
44
|
+
const utf8=value=>new browser.TextEncoder().encode(value);
|
|
45
|
+
const hash=async bytes=>Array.from(new browser.Uint8Array(await browser.crypto.subtle.digest('SHA-256',bytes)),byte=>byte.toString(16).padStart(2,'0')).join('');
|
|
46
|
+
const uncertain=error=>!error.status || error.status>=500 || ['REQUEST_TIMEOUT','REQUEST_SUPERSEDED','FILE_WRITE_UNCONFIRMED'].includes(error.code);
|
|
47
|
+
function saveUploadRecovery(target) {
|
|
48
|
+
const value=target.uncertain ? {path:target.path,name:target.name,size:target.size,hash:target.hash,uncertain:true} : null;
|
|
49
|
+
window.hccUi?.safeSet('hcc.fileUpload:'+key(target.root,target.directory,target.identity),JSON.stringify(value));
|
|
50
|
+
}
|
|
51
|
+
function uploadFor(root,directory,identity) {
|
|
52
|
+
const storageKey=key(root,directory,identity);
|
|
53
|
+
if (!uploads.has(storageKey)) {
|
|
54
|
+
const target={root,directory,identity,file:null,name:'',status:'',error:'',busy:false,uncertain:false};
|
|
55
|
+
try {
|
|
56
|
+
const saved=JSON.parse(window.hccUi?.safeGet('hcc.fileUpload:'+storageKey)||'null');
|
|
57
|
+
if (saved?.uncertain && typeof saved.path==='string' && typeof saved.hash==='string' && /^[a-f0-9]{64}$/.test(saved.hash) && Number.isSafeInteger(saved.size)) Object.assign(target,saved,{status:'files.uploadUncertain'});
|
|
58
|
+
} catch {}
|
|
59
|
+
uploads.set(storageKey,target);
|
|
60
|
+
}
|
|
61
|
+
return uploads.get(storageKey);
|
|
62
|
+
}
|
|
63
|
+
function editorRender() {
|
|
64
|
+
const now=context(), candidate=now.value, target=active;
|
|
65
|
+
const eligible=writable() && (editableFile(candidate) || Boolean(target));
|
|
66
|
+
node('filesEdit').hidden=!eligible || Boolean(target?.editing);
|
|
67
|
+
node('filesEdit').textContent=tr(target?.content!==target?.base ? 'files.resumeDraft' : 'files.edit');
|
|
68
|
+
const editing=Boolean(target?.editing && visible(target));
|
|
69
|
+
node('filesEditPanel').hidden=!editing; node('filesPreview').hidden=editing;
|
|
70
|
+
if (!editing) return;
|
|
71
|
+
if (node('filesEditor').value!==target.content) node('filesEditor').value=target.content;
|
|
72
|
+
node('filesEditor').disabled=Boolean(target.discarding);
|
|
73
|
+
node('filesSave').disabled=target.busy || target.checking || target.conflict || target.uncertain || target.content===target.base;
|
|
74
|
+
node('filesDiscardEdit').disabled=target.busy || target.checking;
|
|
75
|
+
node('filesVerifySave').hidden=!target.conflict && !target.uncertain;
|
|
76
|
+
node('filesVerifySave').disabled=target.busy || target.checking;
|
|
77
|
+
node('filesEditStatus').textContent=tr(target.status || (target.content===target.base ? 'files.editReady' : 'files.unsaved'))
|
|
78
|
+
+(target.newline==='mixed' ? ' '+tr('files.mixedNewlines') : '')+(target.error ? ' '+target.error : '');
|
|
79
|
+
node('filesServerCopy').hidden=!target.latest;
|
|
80
|
+
node('filesServerContent').textContent=target.latest?.content || '';
|
|
81
|
+
}
|
|
82
|
+
function uploadRender() {
|
|
83
|
+
const now=context();
|
|
84
|
+
node('filesUploadPanel').hidden=!writable();
|
|
85
|
+
if (!writable() || !now.root) return;
|
|
86
|
+
const next=uploadFor(now.root,now.directory,now.identity);
|
|
87
|
+
if (upload!==next) { upload=next; node('filesUploadInput').value=''; }
|
|
88
|
+
node('filesUploadName').value=upload.name;
|
|
89
|
+
const locked=upload.busy || upload.uncertain;
|
|
90
|
+
node('filesUploadInput').disabled=locked; node('filesUploadName').disabled=locked;
|
|
91
|
+
node('filesUploadBtn').disabled=locked || !upload.file || !upload.name || upload.blockedName===upload.name;
|
|
92
|
+
node('filesUploadCheck').hidden=!upload.uncertain; node('filesUploadCheck').disabled=upload.busy;
|
|
93
|
+
const path=upload.path && locked ? upload.path : [now.directory,upload.name].filter(Boolean).join('/');
|
|
94
|
+
node('filesUploadTarget').textContent=path ? tr('files.uploadTarget')+' '+path : '';
|
|
95
|
+
node('filesUploadStatus').textContent=(upload.status ? tr(upload.status) : '')+(upload.error ? ' '+upload.error : '');
|
|
96
|
+
}
|
|
97
|
+
function sync() { editorRender(); uploadRender(); }
|
|
98
|
+
function select() {
|
|
99
|
+
const now=context(); active=drafts.get(key(now.root,now.path,now.identity)) || null;
|
|
100
|
+
if (active && now.value && !active.busy && !active.uncertain && now.value.revision!==active.revision) {
|
|
101
|
+
if (active.content===active.base && editableFile(now.value)) {
|
|
102
|
+
active.content=editorText(now.value.content); active.base=active.content; active.revision=now.value.revision; active.newline=now.value.newline;
|
|
103
|
+
} else { active.conflict=true; active.status='files.editConflict'; }
|
|
104
|
+
}
|
|
105
|
+
sync();
|
|
106
|
+
}
|
|
107
|
+
function suspend() { active=null; node('filesEditPanel').hidden=true; node('filesEdit').hidden=true; node('filesPreview').hidden=false; }
|
|
108
|
+
function begin() {
|
|
109
|
+
const now=context(); if (!writable() || !bound(now)) return;
|
|
110
|
+
if (!active) {
|
|
111
|
+
if (!editableFile(now.value)) return;
|
|
112
|
+
active={root:now.root,identity:now.identity,path:now.path,revision:now.value.revision,base:editorText(now.value.content),content:editorText(now.value.content),newline:now.value.newline,status:'',error:'',editing:true,busy:false,conflict:false,uncertain:false};
|
|
113
|
+
drafts.set(key(now.root,now.path,now.identity),active);
|
|
114
|
+
}
|
|
115
|
+
active.editing=true; editorRender(); node('filesEditor').focus();
|
|
116
|
+
}
|
|
117
|
+
async function save() {
|
|
118
|
+
const target=active, visit={...context()};
|
|
119
|
+
if (!writable() || !target || !visible(target) || target.busy || target.checking || target.conflict || target.uncertain || target.content===target.base) return;
|
|
120
|
+
const submittedEditor=target.content, content=target.newline==='crlf' ? target.content.replace(/\n/g,'\r\n') : target.content;
|
|
121
|
+
if (utf8(content).length>1024*1024) { target.status='files.textTooLarge'; editorRender(); return; }
|
|
122
|
+
target.submitted=content; target.busy=true; target.status='files.saving'; target.error=''; editorRender();
|
|
123
|
+
try {
|
|
124
|
+
const result=await bridge.api(route('/api/files/content',target.root),{method:'PUT',body:JSON.stringify({path:target.path,revision:target.revision,content})});
|
|
125
|
+
if (result.saved!==true || result.path!==target.path || typeof result.revision!=='string') throw new Error(tr('files.saveUncertain'));
|
|
126
|
+
const next={...visit.value,...result,content,bom:content.startsWith('\uFEFF'),newline:content.includes('\r\n') ? 'crlf' : content.includes('\n') ? 'lf' : 'none'};
|
|
127
|
+
target.base=submittedEditor; target.revision=next.revision; target.newline=next.newline; target.status=target.content===submittedEditor ? 'files.saved' : 'files.savedWithDraft'; target.submitted=null;
|
|
128
|
+
if (sameVisit(visit)) updated(next);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
target.error=error.detail || error.message;
|
|
131
|
+
if (error.status===409) { target.conflict=true; target.status='files.editConflict'; }
|
|
132
|
+
else if (uncertain(error)) { target.uncertain=true; target.status='files.saveUncertain'; }
|
|
133
|
+
else { target.status='files.saveFailed'; target.submitted=null; }
|
|
134
|
+
} finally { target.busy=false; if (visible(target)) editorRender(); }
|
|
135
|
+
}
|
|
136
|
+
async function verify() {
|
|
137
|
+
const target=active, visit={...context()};
|
|
138
|
+
if (!writable() || !target || !visible(target) || target.busy || target.checking) return;
|
|
139
|
+
target.checking=true; target.error=''; editorRender();
|
|
140
|
+
try {
|
|
141
|
+
const next=await bridge.api(route('/api/files/preview?path='+encodeURIComponent(target.path),target.root));
|
|
142
|
+
target.latest=next;
|
|
143
|
+
if (editableFile(next) && target.submitted!==null && target.submitted!==undefined && next.content===target.submitted) {
|
|
144
|
+
target.base=editorText(next.content); target.revision=next.revision; target.newline=next.newline; target.uncertain=false; target.conflict=false; target.submitted=null;
|
|
145
|
+
target.status=target.content===target.base ? 'files.savedVerified' : 'files.savedWithDraft';
|
|
146
|
+
if (sameVisit(visit)) updated(next);
|
|
147
|
+
} else { target.conflict=true; target.uncertain=false; target.status='files.readbackDifferent'; }
|
|
148
|
+
} catch (error) { target.error=error.detail || error.message; }
|
|
149
|
+
finally { target.checking=false; if (visible(target)) editorRender(); }
|
|
150
|
+
}
|
|
151
|
+
async function discard() {
|
|
152
|
+
const target=active, visit={...context()};
|
|
153
|
+
if (!writable() || !target || !visible(target) || target.busy || target.checking) return;
|
|
154
|
+
if ((target.content!==target.base || target.uncertain || target.conflict) && !window.confirm(tr('files.discardConfirm'))) return;
|
|
155
|
+
const confirmedContent=target.content;
|
|
156
|
+
target.checking=true; target.discarding=true; target.error=''; editorRender();
|
|
157
|
+
try {
|
|
158
|
+
const next=await bridge.api(route('/api/files/preview?path='+encodeURIComponent(target.path),target.root));
|
|
159
|
+
if (!sameVisit(visit)) return;
|
|
160
|
+
if (target.content!==confirmedContent) { target.latest=next; target.status='files.discardChanged'; return; }
|
|
161
|
+
drafts.delete(key(target.root,target.path,target.identity)); active=null; updated(next); select();
|
|
162
|
+
} catch (error) { target.error=error.detail || error.message; }
|
|
163
|
+
finally { target.checking=false; target.discarding=false; if (visible(target)) editorRender(); }
|
|
164
|
+
}
|
|
165
|
+
async function uploadFile(event) {
|
|
166
|
+
event?.preventDefault(); const target=upload, visit={...context()};
|
|
167
|
+
if (!writable() || !target || !uploadVisible(target) || target.busy || target.uncertain || !target.file || target.blockedName===target.name) return;
|
|
168
|
+
if (!target.name || ['.','..'].includes(target.name) || /[\\/\u0000-\u001f\u007f]/.test(target.name)) { target.status='files.badUploadName'; uploadRender(); return; }
|
|
169
|
+
if (target.file.size>10*1024*1024) { target.status='files.uploadTooLarge'; uploadRender(); return; }
|
|
170
|
+
target.path=[target.directory,target.name].filter(Boolean).join('/'); target.size=target.file.size; target.busy=true; target.status='files.uploading'; target.error=''; uploadRender();
|
|
171
|
+
let sent=false;
|
|
172
|
+
try {
|
|
173
|
+
const bytes=new browser.Uint8Array(await target.file.arrayBuffer());
|
|
174
|
+
target.hash=await hash(bytes);
|
|
175
|
+
// File reading and hashing can outlive the selection. Only this visit may
|
|
176
|
+
// begin a write; a started request then retains its original root/path.
|
|
177
|
+
if (!sameVisit(visit) || context().directory!==target.directory) { target.status='files.uploadReady'; return; }
|
|
178
|
+
let binary=''; for (let offset=0;offset<bytes.length;offset+=32768) binary+=String.fromCharCode(...bytes.subarray(offset,offset+32768));
|
|
179
|
+
target.uncertain=true; saveUploadRecovery(target); sent=true;
|
|
180
|
+
const result=await bridge.api(route('/api/files/upload',target.root),{method:'POST',body:JSON.stringify({path:target.path,encoding:'base64',content:browser.btoa(binary)})});
|
|
181
|
+
if (result.created!==true || result.path!==target.path || result.contentHash!==target.hash || result.size!==target.size) throw new Error(tr('files.uploadUncertain'));
|
|
182
|
+
target.uncertain=false; saveUploadRecovery(target); target.status='files.uploaded'; target.file=null;
|
|
183
|
+
if (uploadVisible(target)) node('filesUploadInput').value='';
|
|
184
|
+
if (sameVisit(visit) && context().directory===target.directory) uploaded(result.path || target.path);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
target.error=error.detail || error.message;
|
|
187
|
+
target.uncertain=sent && uncertain(error); saveUploadRecovery(target);
|
|
188
|
+
target.status=target.uncertain ? 'files.uploadUncertain' : error.status===409 ? 'files.uploadExists' : 'files.uploadFailed';
|
|
189
|
+
} finally { target.busy=false; if (uploadVisible(target)) uploadRender(); }
|
|
190
|
+
}
|
|
191
|
+
async function checkUpload() {
|
|
192
|
+
const target=upload, visit={...context()};
|
|
193
|
+
if (!writable() || !target?.uncertain || target.busy || !uploadVisible(target)) return;
|
|
194
|
+
target.busy=true; target.error=''; uploadRender();
|
|
195
|
+
try {
|
|
196
|
+
const result=await bridge.api(route('/api/files/status?path='+encodeURIComponent(target.path),target.root));
|
|
197
|
+
if (result.size===target.size && result.contentHash===target.hash) {
|
|
198
|
+
target.uncertain=false; target.file=null; target.status='files.uploadVerified'; saveUploadRecovery(target);
|
|
199
|
+
if (sameVisit(visit) && context().directory===target.directory) uploaded(target.path);
|
|
200
|
+
} else { target.status='files.uploadDifferent'; target.uncertain=false; target.blockedName=target.name; saveUploadRecovery(target); }
|
|
201
|
+
} catch (error) {
|
|
202
|
+
target.error=error.detail || error.message;
|
|
203
|
+
if (error.status===404) { target.uncertain=false; target.status='files.uploadAbsent'; saveUploadRecovery(target); }
|
|
204
|
+
} finally { target.busy=false; if (uploadVisible(target)) uploadRender(); }
|
|
205
|
+
}
|
|
206
|
+
node('filesEdit').addEventListener('click',begin);
|
|
207
|
+
node('filesEditor').addEventListener('input',()=>{if(active) {active.content=node('filesEditor').value; if(!active.busy&&!active.conflict&&!active.uncertain) active.status=''; editorRender();}});
|
|
208
|
+
node('filesSave').addEventListener('click',save); node('filesVerifySave').addEventListener('click',verify); node('filesDiscardEdit').addEventListener('click',discard);
|
|
209
|
+
node('filesCancelEdit').addEventListener('click',()=>{if(active) {active.editing=false;editorRender();}});
|
|
210
|
+
node('filesUploadForm').addEventListener('submit',uploadFile); node('filesUploadCheck').addEventListener('click',checkUpload);
|
|
211
|
+
node('filesUploadInput').addEventListener('change',()=>{
|
|
212
|
+
if(!upload || upload.busy || upload.uncertain) return;
|
|
213
|
+
upload.file=node('filesUploadInput').files?.[0] || null; upload.name=upload.file?.name || ''; upload.status=''; upload.error=''; upload.path=''; uploadRender();
|
|
214
|
+
});
|
|
215
|
+
node('filesUploadName').addEventListener('input',()=>{if(upload&&!upload.busy&&!upload.uncertain) {upload.name=node('filesUploadName').value;upload.path='';uploadRender();}});
|
|
216
|
+
window.addEventListener('beforeunload',event=>{
|
|
217
|
+
if ([...drafts.values()].some(draft=>draft.content!==draft.base || draft.busy || draft.uncertain)) { event.preventDefault(); event.returnValue=''; }
|
|
218
|
+
});
|
|
219
|
+
return {select,suspend,sync};
|
|
220
|
+
}
|