@logicseek/hello-cc 1.0.5 → 1.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/docs/codex-account.zh-CN.md +38 -0
- package/docs/codex-protocol.md +59 -0
- package/docs/commands.md +9 -0
- package/docs/commands.zh-CN.md +7 -0
- package/docs/macos-process-identity.md +2 -0
- package/docs/native.md +179 -14
- package/docs/native.zh-CN.md +136 -6
- package/docs/web-handoff.zh-CN.md +118 -8
- package/lib/cli/app.mjs +47 -4
- package/lib/cli/commands/coordination.mjs +14 -4
- package/lib/cli/commands/dsh.mjs +1 -1
- package/lib/cli/commands/gc.mjs +22 -15
- package/lib/cli/commands/hook.mjs +19 -3
- package/lib/cli/commands/install.mjs +80 -10
- package/lib/cli/commands/misc.mjs +36 -9
- package/lib/cli/commands/native.mjs +4 -38
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +13 -6
- package/lib/cli/commands/tmux.mjs +1 -2
- package/lib/cli-runtime.mjs +19 -2
- package/lib/core/coordination/results.mjs +27 -10
- package/lib/core/sessions/hook-root-identity.mjs +47 -0
- package/lib/core/sessions/launch.mjs +7 -1
- package/lib/db/connection.mjs +140 -42
- package/lib/db/schema.mjs +59 -0
- package/lib/diagnostics/codex-protocol.mjs +105 -0
- package/lib/guidance.mjs +21 -36
- package/lib/integrations/codex-account.mjs +143 -0
- package/lib/integrations/codex-interactions.mjs +13 -11
- package/lib/integrations/dsh-collaboration.mjs +6 -12
- package/lib/integrations/dsh-cordis.mjs +17 -13
- package/lib/integrations/dsh.mjs +16 -11
- package/lib/integrations/mcp-elicitation.mjs +116 -0
- package/lib/integrations/mcp-url-elicitation.mjs +25 -0
- package/lib/integrations/native/claude.mjs +24 -6
- package/lib/integrations/native/codex.mjs +38 -7
- package/lib/integrations/native/dsh-acp.mjs +67 -24
- package/lib/integrations/native/index.mjs +4 -1
- package/lib/integrations/native/interactions.mjs +2 -1
- package/lib/integrations/native/jsonrpc.mjs +37 -5
- package/lib/integrations/native/telemetry.mjs +30 -0
- package/lib/process/identity.mjs +4 -0
- package/lib/process/pinned-cwd.mjs +302 -0
- package/lib/process/pinned-guidance.mjs +138 -0
- package/lib/process/pinned-project-state.mjs +105 -0
- package/lib/process/selected-cwd-identity.mjs +95 -0
- package/lib/runtime/buffer-evidence.mjs +2 -1
- package/lib/runtime/client.mjs +9 -0
- package/lib/runtime/native/client.mjs +4 -1
- package/lib/runtime/native/history.mjs +140 -0
- package/lib/runtime/native/launcher.mjs +106 -0
- package/lib/runtime/native/service.mjs +64 -8
- package/lib/runtime/native/store.mjs +83 -58
- package/lib/runtime/paths.mjs +17 -2
- package/lib/runtime/private-file.mjs +76 -0
- package/lib/runtime/private-state.mjs +546 -0
- package/lib/runtime/project-context.mjs +2 -1
- package/lib/runtime/project-path.mjs +92 -20
- package/lib/runtime/project-trust.mjs +79 -0
- package/lib/runtime/projects.mjs +9 -3
- package/lib/runtime/state-migration.mjs +697 -0
- package/lib/runtime/state.mjs +69 -27
- package/lib/shared/automatic-result-key.mjs +6 -0
- package/lib/shared/json-file.mjs +55 -6
- package/lib/shared/redact.mjs +2 -1
- package/lib/ui/help.mjs +6 -4
- package/lib/web/agent-defaults.mjs +110 -0
- package/lib/web/auto-attach.mjs +3 -2
- package/lib/web/browser/contracts.d.ts +10 -0
- package/lib/web/browser/core.mjs +1997 -0
- package/lib/web/browser/project-data.mjs +158 -0
- package/lib/web/browser/session-sync.mjs +82 -0
- package/lib/web/browser-assets.mjs +19 -0
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/codex-app-server.mjs +111 -37
- package/lib/web/codex-sessions.mjs +61 -12
- package/lib/web/context-files.mjs +82 -0
- package/lib/web/cookie-auth.mjs +51 -3
- package/lib/web/external-sessions.mjs +76 -9
- package/lib/web/http-routes.mjs +238 -25
- package/lib/web/http.mjs +50 -4
- package/lib/web/liveness-reaper.mjs +2 -2
- package/lib/web/native-sessions.mjs +323 -14
- package/lib/web/project-contexts.mjs +236 -44
- package/lib/web/project-file-write.mjs +249 -0
- package/lib/web/project-files.mjs +268 -0
- package/lib/web/pty-sessions.mjs +11 -3
- package/lib/web/runtime-main.mjs +132 -80
- package/lib/web/session-serialize.mjs +87 -8
- package/lib/web/session-state-sync.mjs +98 -0
- package/lib/web/shutdown-diagnostics.mjs +38 -0
- package/lib/web/shutdown-response.mjs +21 -0
- package/lib/web/shutdown-state.mjs +91 -0
- package/lib/web/startup.mjs +103 -35
- package/lib/web/task-results.mjs +19 -19
- package/lib/web/tls.mjs +84 -33
- package/lib/web/tmux-sessions.mjs +73 -9
- package/lib/web/tmux-stream.mjs +36 -8
- package/lib/web/ui-agent-defaults.mjs +149 -0
- package/lib/web/ui-agent-start.mjs +330 -0
- package/lib/web/ui-codex-account.mjs +81 -0
- package/lib/web/ui-codex.mjs +105 -85
- package/lib/web/ui-command-palette.mjs +21 -5
- package/lib/web/ui-file-writes.mjs +220 -0
- package/lib/web/ui-files.mjs +231 -0
- package/lib/web/ui-handoff.mjs +10 -2
- package/lib/web/ui-history.mjs +175 -105
- package/lib/web/ui-i18n.mjs +280 -8
- package/lib/web/ui-interactions.mjs +107 -6
- package/lib/web/ui-native-timeline.mjs +326 -0
- package/lib/web/ui-native.mjs +93 -43
- package/lib/web/ui-review.mjs +5 -3
- package/lib/web/ui-safe-markdown.mjs +79 -0
- package/lib/web/ui-session-tools.mjs +224 -0
- package/lib/web/ui-template.mjs +29 -1968
- package/lib/web/ui-terminal-find.mjs +5 -3
- package/lib/web/ui-workbench.mjs +5 -3
- package/lib/web/ui-workspace.mjs +3 -0
- package/package.json +8 -2
- package/scripts/benchmark-session-sync.mjs +53 -0
- package/scripts/codex-protocol.mjs +88 -0
- package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
- package/scripts/dsh-package-provenance.mjs +118 -0
- package/scripts/mcp-form-live-fixture.mjs +81 -0
- package/scripts/native-bounded-live-acceptance.mjs +193 -0
- package/scripts/native-fixture-root.mjs +49 -0
- package/scripts/native-installed-acceptance.mjs +611 -0
- package/scripts/native-stability-checks.mjs +40 -0
- package/scripts/regression-cookie-expiry.mjs +70 -0
- package/scripts/regression-http-transport.mjs +9 -0
- package/scripts/regression-terminal-marker.mjs +95 -0
- package/scripts/regression.mjs +481 -219
- package/scripts/shim-reentry-probe.mjs +105 -0
- package/scripts/web-acceptance-diagnostics.mjs +90 -0
- package/scripts/web-codex-account-acceptance.mjs +178 -0
- package/scripts/web-file-preview-checks.mjs +302 -0
- package/scripts/web-mcp-form-acceptance.mjs +134 -0
- package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
- package/scripts/web-native-handoff-checks.mjs +128 -0
- package/scripts/web-native-interaction-acceptance.mjs +219 -18
- package/scripts/web-native-interaction-diagnostics.mjs +80 -0
- package/scripts/web-native-model-acceptance.mjs +22 -7
- package/scripts/web-session-tools-checks.mjs +112 -0
- package/scripts/web-workbench-acceptance.mjs +435 -0
- package/scripts/web-workbench-installed-acceptance.mjs +122 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { safeMarkdownScript, installSafeMarkdown } from './ui-safe-markdown.mjs';
|
|
2
|
+
import { sessionToolsScript, installSessionTools } from './ui-session-tools.mjs';
|
|
3
|
+
|
|
4
|
+
// Normalize only facts supplied by the native adapters. In particular receipts
|
|
5
|
+
// do not contain prompt text, and a resumed executor does not replay history.
|
|
6
|
+
export function nativeTimeline(entries, provider = '') {
|
|
7
|
+
const records = [], keyed = new Map(), streams = new Map(), outputShadows = new Map();
|
|
8
|
+
const string = value => typeof value === 'string' ? value : '';
|
|
9
|
+
const json = value => value == null ? '' : typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
10
|
+
const textParts = content => (Array.isArray(content) ? content : []).map(part =>
|
|
11
|
+
part.type === 'text' ? string(part.text) : part.type === 'content' && part.content?.type === 'text' ? string(part.content.text) : '').filter(Boolean).join('\n');
|
|
12
|
+
function put(key, next) {
|
|
13
|
+
if (keyed.has(key)) { const record = keyed.get(key); const ids = record.eventIds || []; Object.assign(record, next); record.eventIds = [...new Set([...ids, next.eventId])]; }
|
|
14
|
+
else { const record = { key, ...next, eventIds: [next.eventId] }; records.push(record); keyed.set(key, record); }
|
|
15
|
+
return keyed.get(key);
|
|
16
|
+
}
|
|
17
|
+
for (const entry of entries.slice(-100)) {
|
|
18
|
+
const event = entry.payload || entry, source = event.provider || provider;
|
|
19
|
+
const association = event.turnId || event.submissionId || (event.parentToolUseId ? 'parent:' + event.parentToolUseId : 'event:' + entry.id);
|
|
20
|
+
const scope = JSON.stringify([source, event.sessionId, association, event.parentToolUseId || '']);
|
|
21
|
+
const base = { eventId: entry.id, scope, turnId: event.turnId || '', parentToolUseId: event.parentToolUseId || '', status: '' };
|
|
22
|
+
const identity = event.itemId || event.messageId;
|
|
23
|
+
function message(value, { delta = false, role = 'assistant', id = identity } = {}) {
|
|
24
|
+
if (!value) return;
|
|
25
|
+
const streamScope = scope + ':' + role, stream = streams.get(streamScope);
|
|
26
|
+
const namedKey = id ? scope + ':message:' + id : null;
|
|
27
|
+
// Claude assigns the message UUID only on its final message, whereas
|
|
28
|
+
// Codex already supplies the item ID on each delta.
|
|
29
|
+
const canFinishAnonymousStream = !delta && source === 'claude' && stream && !keyed.get(stream)?.explicitIdentity;
|
|
30
|
+
const key = namedKey && !canFinishAnonymousStream ? namedKey : stream || namedKey || scope + ':message:event:' + entry.id;
|
|
31
|
+
const previous = keyed.get(key);
|
|
32
|
+
const record = put(key, { ...base, kind: 'message', role, streamed: delta, explicitIdentity: id || '', text: delta ? (previous?.text || '') + value : value });
|
|
33
|
+
if (namedKey) keyed.set(namedKey, record);
|
|
34
|
+
if (delta) streams.set(streamScope, key); else streams.delete(streamScope);
|
|
35
|
+
}
|
|
36
|
+
function endStreams() { for (const role of ['assistant', 'user', 'output']) streams.delete(scope + ':' + role); }
|
|
37
|
+
if (['delta', 'output', 'message'].includes(event.type)) {
|
|
38
|
+
// These providers' adapters emit assistant text. Unknown providers have
|
|
39
|
+
// no inferred role. Explicit roles always take precedence.
|
|
40
|
+
const role = event.role || (['codex', 'claude', 'dsh'].includes(source) ? 'assistant' : 'output');
|
|
41
|
+
message(string(event.text), { delta: event.type !== 'message', role });
|
|
42
|
+
if (source === 'dsh' && event.type === 'output') outputShadows.set(scope, string(event.text));
|
|
43
|
+
} else if (event.type === 'session.update') {
|
|
44
|
+
const update = event.update || {}, type = update.sessionUpdate;
|
|
45
|
+
if (['agent_message_chunk', 'user_message_chunk', 'agent_thought_chunk'].includes(type)) {
|
|
46
|
+
if (update.content?.type === 'text') {
|
|
47
|
+
if (type === 'agent_thought_chunk') put(scope + ':thought:' + entry.id,
|
|
48
|
+
{ ...base, kind: 'reasoning', text: string(update.content.text) });
|
|
49
|
+
else {
|
|
50
|
+
const duplicateOutput = type === 'agent_message_chunk' && outputShadows.get(scope) === update.content.text;
|
|
51
|
+
outputShadows.delete(scope);
|
|
52
|
+
if (!duplicateOutput) message(string(update.content.text), { delta: true, role: type === 'user_message_chunk' ? 'user' : 'assistant', id: null });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else if (['tool_call', 'tool_call_update'].includes(type)) {
|
|
56
|
+
endStreams();
|
|
57
|
+
const key = scope + ':tool:' + (update.toolCallId || entry.id), previous = keyed.get(key) || {};
|
|
58
|
+
const tool = { ...(previous.tool || {}), ...update };
|
|
59
|
+
put(key, { ...base, kind: tool.kind === 'edit' ? 'files' : 'tool', title: tool.title || tool.kind || '',
|
|
60
|
+
status: tool.status || '', text: textParts(tool.content), input: json(tool.rawInput), output: json(tool.rawOutput),
|
|
61
|
+
changes: (Array.isArray(tool.content) ? tool.content : []).filter(part => part.type === 'diff').map(part => ({ path: part.path, diff: part.newText, oldText: part.oldText })), tool });
|
|
62
|
+
} else if (type === 'plan') put(scope + ':plan', { ...base, kind: 'plan', text: json(update.entries) });
|
|
63
|
+
else put(scope + ':event:' + entry.id, { ...base, kind: 'event', title: type || 'session.update', text: json(update) });
|
|
64
|
+
} else if (event.type === 'item') {
|
|
65
|
+
endStreams();
|
|
66
|
+
const key = scope + ':item:' + (event.item?.id || entry.id);
|
|
67
|
+
const item = { ...(keyed.get(key)?.item || {}), ...(event.item || {}) };
|
|
68
|
+
put(key, { ...base, kind: item.type === 'fileChange' ? 'files' : 'tool',
|
|
69
|
+
title: item.command || item.tool || item.type || '', status: item.status || event.phase || '',
|
|
70
|
+
text: string(item.aggregatedOutput) || json(item.result), input: json(item.arguments),
|
|
71
|
+
changes: Array.isArray(item.changes) ? item.changes : [], exitCode: item.exitCode, command: string(item.command), item });
|
|
72
|
+
} else if (event.type === 'diff' || event.type === 'plan') {
|
|
73
|
+
put(scope + ':' + event.type, { ...base, kind: event.type === 'diff' ? 'files' : 'plan',
|
|
74
|
+
text: string(event.diff) || json(event.plan), explanation: string(event.explanation) });
|
|
75
|
+
} else if (event.type === 'error') {
|
|
76
|
+
put(scope + ':error:' + entry.id, { ...base, kind: 'error', title: event.code || event.error?.code || '',
|
|
77
|
+
text: string(event.text) || string(event.message) || json(event.error) });
|
|
78
|
+
} else if (event.type === 'completed') {
|
|
79
|
+
const output = string(event.text), messages = records.filter(record => record.scope === scope && record.kind === 'message' && record.role === 'assistant');
|
|
80
|
+
// Completion may repeat the final message (Codex/Claude) or the complete
|
|
81
|
+
// concatenation of committed chunks (dsh). Do not print that reply twice.
|
|
82
|
+
const duplicate = messages.some(record => record.text === output) ||
|
|
83
|
+
messages.map(record => record.text).join('') === output || messages.map(record => record.text).join('\n\n') === output;
|
|
84
|
+
if (output && !duplicate) {
|
|
85
|
+
if (event.status === 'failed') put(scope + ':completion-error:' + entry.id, { ...base, kind: 'error', text: output });
|
|
86
|
+
else {
|
|
87
|
+
// dsh completion contains the full accumulated turn text; the event
|
|
88
|
+
// window may have evicted early chunks. Replace surviving fragments
|
|
89
|
+
// instead of showing those fragments again beside the full answer.
|
|
90
|
+
if (source === 'dsh') {
|
|
91
|
+
for (let index = records.length - 1; index >= 0; index--) {
|
|
92
|
+
const record = records[index];
|
|
93
|
+
if (record.scope === scope && record.kind === 'message' && record.role === 'assistant' && record.streamed) records.splice(index, 1);
|
|
94
|
+
}
|
|
95
|
+
endStreams();
|
|
96
|
+
}
|
|
97
|
+
message(output, { id: 'completion:' + entry.id });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
endStreams();
|
|
101
|
+
put(scope + ':completion:' + entry.id, { ...base, kind: 'status', terminal: true, status: event.status || '', text: event.stopReason || '' });
|
|
102
|
+
} else if (event.type === 'status' || event.type === 'opened' || event.type === 'closed') {
|
|
103
|
+
put(scope + ':status:' + entry.id, { ...base, kind: 'status', status: event.status || event.type, text: '' });
|
|
104
|
+
} else if (!['queued', 'account', 'usage', 'metadata', 'approval', 'interaction.pending', 'interaction.resolved', 'interaction.expired', 'permission', 'interrupt.requested'].includes(event.type)) {
|
|
105
|
+
put(scope + ':event:' + entry.id, { ...base, kind: 'event', title: event.type || '', text: json(event) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return records;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function nativeTimelineScript() {
|
|
112
|
+
return sessionToolsScript() + safeMarkdownScript() + 'window.hccNativeTimelineModel = (' + nativeTimeline.toString() + ');'
|
|
113
|
+
+ 'window.hccNativeTimelineView = (' + createNativeTimelineView.toString() + ');';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function installNativeTimeline() {
|
|
117
|
+
installSafeMarkdown(); installSessionTools();
|
|
118
|
+
window.hccNativeTimelineModel = nativeTimeline;
|
|
119
|
+
window.hccNativeTimelineView = createNativeTimelineView;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createNativeTimelineView({ document, window, escape, text }) {
|
|
123
|
+
const byId = id => document.getElementById(id), container = byId('nativeEvents'), scroll = byId('nativeScroll');
|
|
124
|
+
const markdown = window.hccSafeMarkdown(escape), views = new Map();
|
|
125
|
+
let scope = '', currentEntries = [], currentState = {}, html = '', copies = [], blocks = [];
|
|
126
|
+
const traceFilter = window.hccCreateTraceFilter({ document, prefix: 'native', text, escape, changed: () => {
|
|
127
|
+
if (!scope) return;
|
|
128
|
+
view().top = 0; view().follow = false; render(currentEntries, currentState, scope); scroll.scrollTop = 0;
|
|
129
|
+
} });
|
|
130
|
+
const view = () => views.get(scope);
|
|
131
|
+
const pinned = () => !Number.isFinite(scroll.scrollHeight) || scroll.scrollHeight - scroll.clientHeight - scroll.scrollTop < 48;
|
|
132
|
+
function remember() {
|
|
133
|
+
if (!view()) return;
|
|
134
|
+
view().top = scroll.scrollTop || 0;
|
|
135
|
+
for (const node of container.querySelectorAll('details[data-native-detail]')) {
|
|
136
|
+
if (node.dataset.nativeDetail) view().details.set(node.dataset.nativeDetail, node.open);
|
|
137
|
+
}
|
|
138
|
+
for (const node of container.querySelectorAll('pre[data-native-code]')) {
|
|
139
|
+
if (node.dataset.nativeCode) view().codeScroll.set(node.dataset.nativeCode, { top: node.scrollTop, left: node.scrollLeft });
|
|
140
|
+
}
|
|
141
|
+
for (const id of ['nativeIdentityDetails', 'nativeReceiptsDetails']) view().details.set(id, Boolean(byId(id).open));
|
|
142
|
+
for (const map of [view().details, view().pages, view().codeScroll]) while (map.size > 256) map.delete(map.keys().next().value);
|
|
143
|
+
}
|
|
144
|
+
function choose(next) {
|
|
145
|
+
if (scope === next) return;
|
|
146
|
+
remember(); scope = next; html = ''; copies = []; blocks = []; traceFilter.reset(next);
|
|
147
|
+
if (!views.has(scope)) views.set(scope, { mode: 'conversation', top: 0, follow: true, details: new Map(), pages: new Map(), codeScroll: new Map() });
|
|
148
|
+
while (views.size > 20) views.delete(views.keys().next().value);
|
|
149
|
+
for (const id of ['nativeIdentityDetails', 'nativeReceiptsDetails']) byId(id).open = view().details.get(id) || false;
|
|
150
|
+
scroll.scrollTop = view().top;
|
|
151
|
+
}
|
|
152
|
+
function ranges(value) {
|
|
153
|
+
const result = [];
|
|
154
|
+
for (let start = 0; start < value.length;) {
|
|
155
|
+
let end = Math.min(start + 12000, value.length), lines = 0;
|
|
156
|
+
for (let i = start; i < end; i++) if (value[i] === '\n' && ++lines === 200) { end = i + 1; break; }
|
|
157
|
+
if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]) && /[\uDC00-\uDFFF]/.test(value[end])) end--;
|
|
158
|
+
result.push([start, end]); start = end;
|
|
159
|
+
}
|
|
160
|
+
return result.length ? result : [[0, 0]];
|
|
161
|
+
}
|
|
162
|
+
function code(value, identity, label = '') {
|
|
163
|
+
const content = String(value ?? ''), parts = ranges(content), page = Math.min(view().pages.get(identity) || 0, parts.length - 1);
|
|
164
|
+
const index = copies.push(content) - 1;
|
|
165
|
+
blocks[index] = { identity, pages: parts.length, page };
|
|
166
|
+
return '<div class="native-content-actions"><span>' + escape(label || text('Text', '文本')) + '</span>'
|
|
167
|
+
+ '<button type="button" data-native-copy="' + index + '">' + escape(text('Copy full text', '复制全文')) + '</button>'
|
|
168
|
+
+ (parts.length > 1 ? '<button type="button" data-native-page="' + index + '" data-step="-1"' + (page ? '' : ' disabled') + '>' + escape(text('Previous', '上一段')) + '</button>'
|
|
169
|
+
+ '<span>' + (page + 1) + ' / ' + parts.length + '</span><button type="button" data-native-page="' + index + '" data-step="1"' + (page + 1 < parts.length ? '' : ' disabled') + '>' + escape(text('Next', '下一段')) + '</button>' : '')
|
|
170
|
+
+ '</div><pre tabindex="0" data-native-code="' + escape(identity) + '"><code>' + escape(content.slice(...parts[page])) + '</code></pre>';
|
|
171
|
+
}
|
|
172
|
+
function prose(value, identity) {
|
|
173
|
+
if (ranges(value).length > 1) return code(value, identity, text('Long message · Plain-text sections', '长消息 · 纯文本分段'));
|
|
174
|
+
const fence = String.fromCharCode(96).repeat(3), parts = value.split('\n');
|
|
175
|
+
let plain = [], source = [], language = '', inCode = false, result = '', block = 0;
|
|
176
|
+
const flush = () => { if (plain.length) result += markdown(plain.join('\n')); plain = []; };
|
|
177
|
+
for (const line of parts) {
|
|
178
|
+
if (line.trimStart().startsWith(fence)) {
|
|
179
|
+
if (inCode) { result += code(source.join('\n'), identity + ':' + block++, language); source = []; }
|
|
180
|
+
else { flush(); language = line.trimStart().slice(3).trim(); }
|
|
181
|
+
inCode = !inCode;
|
|
182
|
+
} else (inCode ? source : plain).push(line);
|
|
183
|
+
}
|
|
184
|
+
if (inCode) result += code(source.join('\n'), identity + ':' + block++, language);
|
|
185
|
+
flush(); return result;
|
|
186
|
+
}
|
|
187
|
+
function details(identity, label, body, status = '') {
|
|
188
|
+
// User disclosure state is restored on the DOM after a content update. It
|
|
189
|
+
// must not alter the cached markup for an otherwise unchanged snapshot.
|
|
190
|
+
return '<details data-native-detail="' + escape(identity) + '"><summary>' + escape(label)
|
|
191
|
+
+ (status ? '<small>' + escape(status) + '</small>' : '') + '</summary>' + body + '</details>';
|
|
192
|
+
}
|
|
193
|
+
function card(record) {
|
|
194
|
+
const identity = record.key, labels = { tool: text('Tool / command', '工具 / 命令'), files: text('File changes', '文件变更'), plan: text('Plan', '计划'), reasoning: text('Reasoning', '推理'), error: text('Error', '错误'), event: text('Other event', '其他事件') };
|
|
195
|
+
const locate = '<button type="button" data-native-locate="' + escape(record.eventId) + '">' + escape(text('Locate event #', '定位事件 #') + record.eventId) + '</button>';
|
|
196
|
+
if (record.kind === 'status') return record.terminal ? '<p class="native-meta" data-native-card="' + escape(identity) + '">' + escape(text('Turn status: ', '回合状态:') + (record.status || text('Not reported', '未报告')) + (record.text ? ' · ' + record.text : '')) + ' ' + locate + '</p>' : '';
|
|
197
|
+
const role = record.role === 'user' ? text('You', '你') : record.role === 'assistant'
|
|
198
|
+
? (record.parentToolUseId ? text('Assistant · Subtask', '助手 · 子任务') : text('Assistant', '助手')) : text('Executor output', '执行器输出');
|
|
199
|
+
let body;
|
|
200
|
+
if (record.kind === 'message') body = '<h4>' + escape(role) + '</h4>' + prose(record.text, identity);
|
|
201
|
+
else {
|
|
202
|
+
const content = (record.explanation ? prose(record.explanation, identity + ':explanation') : '')
|
|
203
|
+
+ (record.command ? code(record.command, identity + ':command', text('Command', '命令')) : '')
|
|
204
|
+
+ (record.text ? code(record.text, identity + ':text', labels[record.kind]) : '')
|
|
205
|
+
+ (record.input ? code(record.input, identity + ':input', text('Input', '输入')) : '')
|
|
206
|
+
+ (record.output ? code(record.output, identity + ':output', text('Output', '输出')) : '')
|
|
207
|
+
+ (record.exitCode != null ? '<p>' + escape(text('Exit code: ', '退出码:') + record.exitCode) + '</p>' : '')
|
|
208
|
+
+ (record.changes || []).map((change, index) => '<h4>' + escape(change.path || text('File', '文件')) + '</h4>'
|
|
209
|
+
+ (change.oldText != null ? code(change.oldText, identity + ':old:' + index, text('Before', '修改前')) : '')
|
|
210
|
+
+ code(change.diff || '', identity + ':diff:' + index, change.oldText != null ? text('After', '修改后') : text('Reported diff', '报告的差异'))).join('');
|
|
211
|
+
body = record.kind === 'error' ? '<h4>' + escape(labels.error) + '</h4>' + content
|
|
212
|
+
: details(identity, labels[record.kind] + (record.title ? ' · ' + String(record.title).slice(0, 180) : ''), content, record.status);
|
|
213
|
+
}
|
|
214
|
+
return '<article class="native-event native-event-' + escape(record.kind) + (record.role === 'user' ? ' native-event-user' : '') + '" data-native-card="' + escape(identity) + '">' + body + '<div class="native-content-actions">' + locate + '</div></article>';
|
|
215
|
+
}
|
|
216
|
+
function render(entries, state, nextScope) {
|
|
217
|
+
const changedScope = scope !== nextScope;
|
|
218
|
+
if (!changedScope) remember();
|
|
219
|
+
choose(nextScope); currentEntries = entries; currentState = state;
|
|
220
|
+
const reading = view(); copies = []; blocks = [];
|
|
221
|
+
const normalized = window.hccNativeTimelineModel(entries, state.provider);
|
|
222
|
+
const filtered = normalized.filter(record => window.hccTraceMatches(record, traceFilter.value()));
|
|
223
|
+
const matches = new Set(filtered.flatMap(record => record.eventIds || [record.eventId]));
|
|
224
|
+
const trace = entries.filter(entry => matches.has(entry.id) || window.hccTraceMatches({ turnId: (entry.payload || entry).turnId,
|
|
225
|
+
kind: (entry.payload || entry).type === 'error' ? 'error' : 'event', status: (entry.payload || entry).status,
|
|
226
|
+
text: JSON.stringify(entry.payload || entry) }, traceFilter.value()));
|
|
227
|
+
traceFilter.render(entries.map(entry => (entry.payload || entry).turnId), reading.mode === 'trace' ? trace.length : filtered.length, reading.mode === 'trace' ? entries.length : normalized.length);
|
|
228
|
+
const next = (reading.mode === 'trace' ? trace.map(entry => '<article class="native-event" data-native-trace-id="' + escape(entry.id) + '">' + details('trace:' + entry.id,
|
|
229
|
+
'#' + entry.id + ' · ' + (entry.payload?.type || entry.type || text('Event', '事件')), code(JSON.stringify(entry.payload || entry, null, 2), 'trace:' + entry.id))
|
|
230
|
+
+ (normalized.some(record => record.eventIds?.includes(entry.id)) ? '<button type="button" data-native-result="' + escape(entry.id) + '">' + escape(text('Locate result', '定位结果')) + '</button>' : '') + '</article>').join('')
|
|
231
|
+
: filtered.map(card).join('')) || '<p class="native-meta">' + escape(entries.length ? text('No matching retained records.', '保留记录中没有匹配项。') : text('No retained output yet.', '暂无保留的输出。')) + '</p>';
|
|
232
|
+
if (html !== next) {
|
|
233
|
+
container.innerHTML = next; html = next;
|
|
234
|
+
for (const node of container.querySelectorAll('details[data-native-detail]')) {
|
|
235
|
+
node.open = reading.details.get(node.dataset.nativeDetail) || false;
|
|
236
|
+
}
|
|
237
|
+
for (const node of container.querySelectorAll('pre[data-native-code]')) {
|
|
238
|
+
const saved = reading.codeScroll.get(node.dataset.nativeCode);
|
|
239
|
+
if (saved) { node.scrollTop = saved.top; node.scrollLeft = saved.left; }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const bounds = entries.length ? '#' + entries[0].id + '–#' + entries[entries.length - 1].id : '0';
|
|
243
|
+
byId('nativeHistoryRange').textContent = text('Retained events ', '保留事件 ') + bounds + ' · ' + entries.length + '/100 · '
|
|
244
|
+
+ text('Recent window, not a full transcript. Submitted prompts may be absent.', '最近事件窗口,非完整会话;可能不含提交的原文。');
|
|
245
|
+
for (const [id, mode] of [['nativeConversation', 'conversation'], ['nativeTrace', 'trace']]) {
|
|
246
|
+
byId(id).textContent = mode === 'conversation' ? text('Conversation', '对话') : text('Event trace', '事件轨迹');
|
|
247
|
+
byId(id).setAttribute?.('aria-pressed', String(reading.mode === mode));
|
|
248
|
+
}
|
|
249
|
+
byId('nativeIdentityLabel').textContent = text('Executor details', '执行器详情');
|
|
250
|
+
byId('nativeReceiptsLabel').textContent = text('Delivery receipts', '投递回执') + ' · ' + (state.deliveries || []).length;
|
|
251
|
+
const count = (state.pendingApprovals || []).length;
|
|
252
|
+
byId('nativeApprovalJump').hidden = !count;
|
|
253
|
+
byId('nativeApprovalJump').textContent = text('Needs your response', '等待你的答复') + ' · ' + count;
|
|
254
|
+
byId('nativeJump').textContent = text('Back to latest', '回到最新');
|
|
255
|
+
byId('nativeJumpBar').hidden = reading.follow;
|
|
256
|
+
scroll.scrollTop = reading.follow ? scroll.scrollHeight : reading.top;
|
|
257
|
+
}
|
|
258
|
+
for (const [id, mode] of [['nativeConversation', 'conversation'], ['nativeTrace', 'trace']]) byId(id).addEventListener('click', () => {
|
|
259
|
+
if (!scope) return;
|
|
260
|
+
remember(); view().mode = mode; view().top = 0; view().follow = false;
|
|
261
|
+
render(currentEntries, currentState, scope); scroll.scrollTop = 0; view().top = 0;
|
|
262
|
+
});
|
|
263
|
+
scroll.addEventListener('scroll', () => {
|
|
264
|
+
if (!view() || !currentEntries.length) return;
|
|
265
|
+
view().follow = pinned(); view().top = scroll.scrollTop; byId('nativeJumpBar').hidden = view().follow;
|
|
266
|
+
});
|
|
267
|
+
byId('nativeJump').addEventListener('click', () => { if (view()) { view().follow = true; scroll.scrollTop = scroll.scrollHeight; byId('nativeJumpBar').hidden = true; } });
|
|
268
|
+
byId('nativeApprovalJump').addEventListener('click', () => {
|
|
269
|
+
scroll.scrollTop = 0;
|
|
270
|
+
byId('nativeApprovals').querySelector?.('input,select,textarea,button')?.focus();
|
|
271
|
+
});
|
|
272
|
+
container.addEventListener('click', async event => {
|
|
273
|
+
const button = event.target.closest?.('button');
|
|
274
|
+
if (!button || !container.contains?.(button)) return;
|
|
275
|
+
if (button.dataset.nativeLocate !== undefined || button.dataset.nativeResult !== undefined) {
|
|
276
|
+
const toTrace = button.dataset.nativeLocate !== undefined, id = Number(toTrace ? button.dataset.nativeLocate : button.dataset.nativeResult);
|
|
277
|
+
remember(); view().follow = false; view().mode = toTrace ? 'trace' : 'conversation';
|
|
278
|
+
traceFilter.clear();
|
|
279
|
+
const record = toTrace ? null : window.hccNativeTimelineModel(currentEntries, currentState.provider).find(item => item.eventIds?.includes(id));
|
|
280
|
+
if (toTrace) view().details.set('trace:' + id, true);
|
|
281
|
+
else if (record) view().details.set(record.key, true);
|
|
282
|
+
render(currentEntries, currentState, scope);
|
|
283
|
+
const target = [...container.querySelectorAll(toTrace ? '[data-native-trace-id]' : '[data-native-card]')].find(node => toTrace ? Number(node.dataset.nativeTraceId) === id : node.dataset.nativeCard === record?.key);
|
|
284
|
+
if (target) { target.scrollIntoView?.({ block: 'center' }); target.setAttribute?.('tabindex', '-1'); target.focus?.({ preventScroll: true }); view().top = scroll.scrollTop; }
|
|
285
|
+
} else if (button.dataset.nativeCopy !== undefined) {
|
|
286
|
+
try { await window.navigator.clipboard.writeText(copies[Number(button.dataset.nativeCopy)]); button.textContent = text('Copied', '已复制'); }
|
|
287
|
+
catch { button.textContent = text('Select text to copy', '请选择文本复制'); }
|
|
288
|
+
} else if (button.dataset.nativePage !== undefined) {
|
|
289
|
+
const block = blocks[Number(button.dataset.nativePage)];
|
|
290
|
+
if (!block) return;
|
|
291
|
+
remember(); view().follow = false;
|
|
292
|
+
view().pages.set(block.identity, Math.max(0, Math.min(block.pages - 1, block.page + Number(button.dataset.step))));
|
|
293
|
+
view().codeScroll.delete(block.identity);
|
|
294
|
+
render(currentEntries, currentState, scope);
|
|
295
|
+
const next = container.querySelector?.('[data-native-page="' + button.dataset.nativePage + '"][data-step="' + button.dataset.step + '"]:not(:disabled)')
|
|
296
|
+
|| container.querySelector?.('[data-native-code]');
|
|
297
|
+
next?.focus({ preventScroll: true });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
return { render, reset(nextScope) { render([], {}, nextScope); } };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const nativeTimelineCss = `
|
|
304
|
+
.native-header-top { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
305
|
+
.native-header-top strong { flex:1; min-width:0; }
|
|
306
|
+
.native-navigation { display:flex; gap:6px; align-items:center; margin:10px 0 0; }
|
|
307
|
+
.native-navigation button[aria-pressed=true] { color:var(--accent); border-color:var(--accent); }
|
|
308
|
+
.native-history-range { margin:8px 0 0; color:var(--muted); font-size:var(--small-font); }
|
|
309
|
+
.native-event { max-width:900px; margin:10px auto; padding:14px; border:1px solid var(--border); border-radius:8px; background:var(--panel); }
|
|
310
|
+
.native-event-message { border:0; padding:10px 2px; background:transparent; }
|
|
311
|
+
.native-event-user { padding:14px; background:var(--panel-2); border:1px solid var(--border); }
|
|
312
|
+
.native-event-error { border-color:var(--danger); }
|
|
313
|
+
.native-event h4 { margin:0 0 8px; font-size:var(--small-font); color:var(--muted); }
|
|
314
|
+
.native-event details summary { cursor:pointer; overflow-wrap:anywhere; line-height:1.6; }
|
|
315
|
+
.native-event details summary small { display:block; color:var(--muted); }
|
|
316
|
+
.native-event .codex-prose { white-space:pre-wrap; overflow-wrap:anywhere; line-height:1.7; }
|
|
317
|
+
.native-event pre { white-space:pre-wrap; overflow-wrap:anywhere; margin:8px 0; padding:10px; max-height:340px; overflow:auto; background:var(--panel-2); border-radius:6px; }
|
|
318
|
+
.native-event blockquote { margin-left:0; padding-left:12px; border-left:3px solid var(--border); color:var(--muted); }
|
|
319
|
+
.native-reading-jump { padding:6px; text-align:center; border-top:1px solid var(--border); }
|
|
320
|
+
.native-meta { color:var(--muted); font-size:var(--small-font); margin:10px 0; }
|
|
321
|
+
.native-meta summary { cursor:pointer; padding:5px 0; }
|
|
322
|
+
.native-meta p { overflow-wrap:anywhere; }
|
|
323
|
+
.native-content-actions { display:flex; gap:6px; align-items:center; flex-wrap:wrap; margin-top:8px; }
|
|
324
|
+
.native-content-actions span { color:var(--muted); font-size:var(--small-font); }
|
|
325
|
+
@media(max-width:640px) { .native-header,.native-composer { padding:10px 12px; } .native-scroll { padding:0 12px 10px; } .native-navigation button,.native-content-actions button { min-height:40px; } }
|
|
326
|
+
`;
|
package/lib/web/ui-native.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nativeTimelineCss, nativeTimelineScript, installNativeTimeline } from './ui-native-timeline.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 nativePanelHtml() {
|
|
4
7
|
return `<style>
|
|
@@ -21,10 +24,17 @@ export function nativePanelHtml() {
|
|
|
21
24
|
.native-controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:8px; }
|
|
22
25
|
.native-controls label { display:flex; gap:6px; align-items:center; font-size:var(--small-font); }
|
|
23
26
|
#nativeNotice { color:var(--muted); font-size:var(--small-font); overflow-wrap:anywhere; }
|
|
27
|
+
${codexAccountPanelCss()}
|
|
28
|
+
${nativeTimelineCss}
|
|
29
|
+
${sessionToolsCss}
|
|
24
30
|
</style><section id="nativePanel" hidden aria-label="Native worker">
|
|
25
|
-
<header class="native-header"><strong id="nativeStatus" role="status"></strong><
|
|
26
|
-
|
|
27
|
-
<
|
|
31
|
+
<header class="native-header"><div class="native-header-top"><strong id="nativeStatus" role="status"></strong><button id="nativeApprovalJump" type="button" hidden>Needs your response</button></div>
|
|
32
|
+
<nav class="native-navigation" aria-label="Native session views"><button id="nativeConversation" type="button" aria-pressed="true">Conversation</button><button id="nativeTrace" type="button" aria-pressed="false">Event trace</button></nav><p id="nativeHistoryRange" class="native-history-range"></p></header>
|
|
33
|
+
<div id="nativeScroll" class="native-scroll" tabindex="0" aria-label="Native conversation"><div id="nativeApprovals"></div>${codexAccountPanelHtml('native')}${traceFilterHtml('native')}${metricsHtml('native')}<div id="nativeEvents"></div>
|
|
34
|
+
<details id="nativeReceiptsDetails" class="native-meta"><summary id="nativeReceiptsLabel">Delivery receipts</summary><div id="nativeReceipts" class="native-receipts"></div></details>
|
|
35
|
+
<details id="nativeIdentityDetails" class="native-meta"><summary id="nativeIdentityLabel">Executor details</summary><p id="nativeIdentity"></p><p data-i18n="native.approvalPolicy">The current controller can answer interactive requests here.</p></details></div>
|
|
36
|
+
<div id="nativeJumpBar" class="native-reading-jump" hidden><button id="nativeJump" type="button">Back to latest</button></div>
|
|
37
|
+
<footer class="native-composer"><label for="nativeDraft" data-i18n="native.draft">Message draft saved in this browser</label><textarea id="nativeDraft" rows="3"></textarea>${sessionToolsHtml('native')}
|
|
28
38
|
<div class="native-controls"><button id="nativeSend" class="primary" type="button" data-i18n="native.send" disabled>Queue message</button><button id="nativeInterrupt" type="button" data-i18n="native.interrupt" disabled>Interrupt current turn</button><button id="nativeRead" type="button" data-i18n="native.read">Refresh receipts</button><button id="nativeReviewPending" type="button" data-i18n="native.reviewPending" hidden>Reviewed; keep draft</button></div>
|
|
29
39
|
<div class="native-controls"><label><input id="nativeCloseConfirmed" type="checkbox"><span data-i18n="native.closeConfirm">Stop this executor</span></label><button id="nativeClose" class="danger" type="button" data-i18n="native.close" disabled>Close executor</button></div>
|
|
30
40
|
<p id="nativeNotice" role="status" aria-live="polite"></p>
|
|
@@ -33,25 +43,38 @@ export function nativePanelHtml() {
|
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export function nativePanelScript() {
|
|
36
|
-
return interactionPanelScript() +
|
|
46
|
+
return nativeTimelineScript() + codexAccountPanelScript() + interactionPanelScript() + "(" + installNativePanel.toString() + ")(false);";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function installNativePanel(installDependencies = true) {
|
|
50
|
+
if (installDependencies) { installNativeTimeline(); installCodexAccountPanel(); installInteractionPanel(); }
|
|
37
51
|
const bridge = () => window.hccHandoff, panel = document.getElementById('nativePanel'), draft = document.getElementById('nativeDraft');
|
|
38
52
|
const tr = (key, fallback = '') => bridge().tr(key, fallback), esc = value => bridge().esc(String(value ?? ''));
|
|
39
53
|
const events = new Map(), inflight = new Set(), responding = new Set();
|
|
40
|
-
|
|
54
|
+
const accountPending = new Set(), accountReads = new Set();
|
|
55
|
+
const text = (en, zh) => ['zh','zh-CN'].includes(window.hccUi.language) ? zh : en;
|
|
56
|
+
const timeline = window.hccNativeTimelineView({ document, window, escape: esc, text });
|
|
57
|
+
let key = '', state = null, pending = null, observed = 0, approvalSignature = '';
|
|
58
|
+
const contextTools = window.hccCreateSessionTools({ document, window, prefix: 'native', bridge, draft, save: () => { save(); sync(); }, state: () => state, text, escape: esc });
|
|
41
59
|
const storageKey = () => 'hcc.nativeDraft:' + JSON.stringify([bridge().projectRoot,bridge().active]) + (bridge().draftScope ? ':pane:' + JSON.stringify(bridge().draftScope) : '');
|
|
42
60
|
function save() { if (key) window.hccUi.safeSet(key,JSON.stringify({text:draft.value,pending})); }
|
|
43
61
|
function subject() { const b = bridge(); return {root:b.projectRoot,id:b.active,key:storageKey(),token:b.actionToken,epoch:b.epoch,generation:state?.generation,owner:state?.owner}; }
|
|
44
62
|
const matches = target => target.root === bridge().projectRoot && target.id === bridge().active && (!target.generation || target.generation === state?.generation) && (!target.owner || target.owner === state?.owner);
|
|
63
|
+
const accountScope = target => JSON.stringify([target.key,target.generation,target.owner,target.sessionId]);
|
|
45
64
|
const route = (target,name) => '/api/sessions/' + encodeURIComponent(target.id) + '/native/' + name + '?root=' + encodeURIComponent(target.root);
|
|
46
65
|
function notice(message) { document.getElementById('nativeNotice').textContent = message || ''; }
|
|
47
66
|
function sync() {
|
|
48
67
|
const next = storageKey();
|
|
49
68
|
if (next !== key) {
|
|
50
69
|
key = next; state = null; pending = null; events.clear(); observed++; draft.value = '';
|
|
51
|
-
|
|
70
|
+
approvalSignature = ''; timeline.reset(key);
|
|
71
|
+
for (const id of ['nativeStatus','nativeIdentity','nativeReceipts','nativeNotice','nativeApprovals']) document.getElementById(id).textContent = '';
|
|
52
72
|
document.getElementById('nativeCloseConfirmed').checked = false;
|
|
73
|
+
document.getElementById('nativeAccount').open = false;
|
|
53
74
|
try { const saved = JSON.parse(window.hccUi.safeGet(key) || '{}'); draft.value = saved.text || ''; pending = saved.pending || null; } catch (_) {}
|
|
54
75
|
}
|
|
76
|
+
window.hccCodexAccount.render('native', state?.account, { supported: Boolean(state?.capabilities?.accountRead),
|
|
77
|
+
connected: Boolean(state?.connected), pending: accountPending.has(accountScope({ ...subject(), sessionId: state?.sessionId })) });
|
|
55
78
|
const writable = bridge().session?.type === 'native' && bridge().canControl && state?.connected && !state.closing;
|
|
56
79
|
const sending = inflight.has(key);
|
|
57
80
|
document.getElementById('nativeSend').disabled = !writable || sending || !!pending || !draft.value.trim() || state?.quarantined || state?.capabilities?.send === false;
|
|
@@ -64,6 +87,8 @@ export function nativePanelScript() {
|
|
|
64
87
|
button.disabled = !writable || responding.has(button.dataset.requestKey) || (button.dataset.decision === 'accept' && button.dataset.truncated === 'true');
|
|
65
88
|
});
|
|
66
89
|
if (pending) notice(tr('native.uncertain'));
|
|
90
|
+
contextTools.render();
|
|
91
|
+
window.hccRenderReportedMetrics({ document, prefix: 'native', state, text, escape: esc });
|
|
67
92
|
}
|
|
68
93
|
function acknowledge(target, submissionId) {
|
|
69
94
|
let stored = {}; try { stored = JSON.parse(window.hccUi.safeGet(target.key) || '{}'); } catch (_) {}
|
|
@@ -83,49 +108,63 @@ export function nativePanelScript() {
|
|
|
83
108
|
if (!value || bridge().session?.type !== 'native' || (value.peer !== bridge().session?.peer_id && value.peer !== bridge().active) || (value.root && value.root !== bridge().projectRoot)) return;
|
|
84
109
|
if (Number.isSafeInteger(state?.generation) && Number.isSafeInteger(value.generation) && value.generation < state.generation) return;
|
|
85
110
|
const replaced = state && (state.generation !== value.generation || state.owner !== value.owner);
|
|
86
|
-
if (replaced) { events.clear(); document.getElementById('nativeCloseConfirmed').checked = false; }
|
|
111
|
+
if (replaced) { events.clear(); approvalSignature = ''; notice(''); document.getElementById('nativeCloseConfirmed').checked = false; }
|
|
87
112
|
if (state !== value) observed++;
|
|
88
113
|
state = value;
|
|
114
|
+
const accountKey = accountScope({ ...subject(), sessionId: state.sessionId });
|
|
115
|
+
if (state.connected && state.capabilities?.accountRead && !accountReads.has(accountKey)) {
|
|
116
|
+
accountReads.add(accountKey);
|
|
117
|
+
if (accountReads.size > 100) accountReads.delete(accountReads.values().next().value);
|
|
118
|
+
void readAccount();
|
|
119
|
+
}
|
|
89
120
|
for (const entry of value.events || []) events.set(entry.id,entry);
|
|
90
121
|
while (events.size > 100) events.delete(events.keys().next().value);
|
|
122
|
+
const recent = [...events.values()].sort((a,b)=>a.id-b.id).map(entry => entry.payload || entry);
|
|
123
|
+
const metricEvent = recent.findLast(event => event.metrics), metadataEvent = recent.findLast(event => event.runtimeMetadata);
|
|
124
|
+
state = { ...state, metrics: state.metrics || metricEvent?.metrics, runtimeMetadata: state.runtimeMetadata || metadataEvent?.runtimeMetadata };
|
|
91
125
|
document.getElementById('nativeStatus').textContent = tr('native.sameExecutor') + ' · ' + (state.provider || '') + ' · ' + tr('native.state.' + state.status, state.status || '');
|
|
92
126
|
document.getElementById('nativeIdentity').textContent = [state.peer,state.sessionId,state.turnId].filter(Boolean).join(' · ');
|
|
93
|
-
|
|
94
|
-
document.getElementById('
|
|
95
|
-
const event = entry.payload || entry;
|
|
96
|
-
const text = event.text || event.message || event.command || event.item?.aggregatedOutput || event.item?.command || event.diff || (event.plan ? JSON.stringify(event.plan,null,2) : '') || event.error?.message || '';
|
|
97
|
-
const label = ['output','message','delta'].includes(event.type) ? tr('native.output') : event.type === 'error' ? tr('native.error') : tr('native.event');
|
|
98
|
-
return text ? '<article class="native-event"><strong>' + esc(label) + '</strong><pre>' + esc(text) + '</pre></article>' : '';
|
|
99
|
-
}).join('') || '<p>' + esc(tr('native.noOutput')) + '</p>';
|
|
127
|
+
const receiptsHtml = (state.deliveries || []).map(delivery => '<article class="native-receipt"><strong>#' + esc(delivery.message_id) + ' · ' + esc(tr('native.delivery.' + delivery.state,delivery.state)) + '</strong><div>' + esc(delivery.submission_id) + (delivery.turn_id ? ' · ' + esc(delivery.turn_id) : '') + '</div>' + (delivery.detail ? '<details><summary>' + esc(tr('details')) + '</summary><pre>' + esc(typeof delivery.detail === 'string' ? delivery.detail : JSON.stringify(delivery.detail,null,2)) + '</pre></details>' : '') + '</article>').join('') || '<p>' + esc(tr('native.noReceipts')) + '</p>';
|
|
128
|
+
if (document.getElementById('nativeReceipts').innerHTML !== receiptsHtml) document.getElementById('nativeReceipts').innerHTML = receiptsHtml;
|
|
100
129
|
const approvals = state.pendingApprovals || [], target = subject(), container = document.getElementById('nativeApprovals');
|
|
101
130
|
const requestKey = request => JSON.stringify([target.root, target.id, state.executorId, request.requestId]);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
const nextApprovalSignature = JSON.stringify([target.root, target.id, state.executorId, state.generation, state.owner, approvals, window.hccUi.language]);
|
|
132
|
+
if (approvalSignature !== nextApprovalSignature) {
|
|
133
|
+
window.hccInteractions.remember(container);
|
|
134
|
+
container.innerHTML = approvals.map((request, index) => '<article class="codex-card codex-approval"><h3>'
|
|
135
|
+
+ esc(window.hccInteractions.requiresInput(request) ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
|
|
136
|
+
+ '</h3>' + window.hccInteractions.preview(request, requestKey(request), esc, text)
|
|
137
|
+
+ (request.truncated ? '<p>' + esc(text('Truncated; cancel or decline only.', '参数已截断,只能取消或拒绝。')) + '</p>' : '')
|
|
138
|
+
+ '<div class="hcc-interaction-form">' + window.hccInteractions.form(request, requestKey(request), esc, text) + '</div><div class="native-controls">'
|
|
139
|
+
+ '<button data-response="' + index + '" data-decision="accept" data-truncated="' + window.hccInteractions.approvalBlocked(request) + '" data-request-key="' + esc(requestKey(request)) + '">' + esc(window.hccInteractions.acceptLabel(request, text)) + '</button>'
|
|
140
|
+
+ '<button data-response="' + index + '" data-decision="' + (request.kind === 'userInput' ? 'cancel' : 'decline') + '" data-request-key="' + esc(requestKey(request)) + '">' + esc(request.kind === 'mcp' ? text('Decline', '拒绝') : text('Decline / cancel', '拒绝 / 取消')) + '</button>'
|
|
141
|
+
+ (request.kind === 'mcp' ? '<button data-response="' + index + '" data-decision="cancel" data-request-key="' + esc(requestKey(request)) + '">' + esc(text('Cancel request', '取消请求')) + '</button>' : '') + '</div></article>').join('');
|
|
142
|
+
window.hccInteractions.restore(container);
|
|
143
|
+
container.querySelectorAll('button[data-response]').forEach(button => button.addEventListener('click', async () => {
|
|
144
|
+
if (!matches(target) || !bridge().canControl || responding.has(button.dataset.requestKey)) return;
|
|
145
|
+
const request = approvals[Number(button.dataset.response)], key = button.dataset.requestKey;
|
|
146
|
+
const version = observed;
|
|
147
|
+
notice(''); responding.add(key); sync();
|
|
148
|
+
let authorization;
|
|
149
|
+
try {
|
|
150
|
+
const payload = window.hccInteractions.payload(request, ['permissions','userInput','mcp'].includes(request.kind) ? button.closest('article') : null, button.dataset.decision);
|
|
151
|
+
authorization = window.hccInteractions.prepareUrl(request, button.dataset.decision, text);
|
|
152
|
+
const result = await bridge().api(route(target, 'respond'), {method:'POST', body:JSON.stringify({
|
|
153
|
+
executorId:request.executorId, sessionId:request.sessionId, turnId:request.turnId, requestId:request.requestId,
|
|
154
|
+
...payload, action_token:bridge().actionToken, epoch:bridge().epoch })});
|
|
155
|
+
window.hccInteractions.forget(key);
|
|
156
|
+
const opened = authorization?.complete();
|
|
157
|
+
if (opened && matches(target)) notice(opened);
|
|
158
|
+
if (matches(target) && version === observed) { if (result.state) render(result.state); else void read(); }
|
|
159
|
+
} catch(error) {
|
|
160
|
+
authorization?.cancel();
|
|
161
|
+
if (matches(target) && (state?.pendingApprovals || []).some(current => current.requestId === request.requestId && current.executorId === request.executorId)) notice(error.detail || error.message);
|
|
162
|
+
}
|
|
163
|
+
finally { responding.delete(key); sync(); }
|
|
164
|
+
}));
|
|
165
|
+
approvalSignature = nextApprovalSignature;
|
|
166
|
+
}
|
|
167
|
+
timeline.render([...events.values()].sort((a,b)=>a.id-b.id), state, JSON.stringify([key,state.generation,state.owner]));
|
|
129
168
|
if (pending && (state.deliveries || []).some(delivery => delivery.submission_id === pending.id && delivery.message_id)) acknowledge(subject(),pending.id);
|
|
130
169
|
sync();
|
|
131
170
|
}
|
|
@@ -137,6 +176,18 @@ export function nativePanelScript() {
|
|
|
137
176
|
if (matches(target) && version === observed) render(result.state || result);
|
|
138
177
|
} catch (error) { if (matches(target)) notice(error.detail || error.message); }
|
|
139
178
|
}
|
|
179
|
+
async function readAccount() {
|
|
180
|
+
if (!state?.connected || !state.sessionId || !state.capabilities?.accountRead || accountPending.has(accountScope({ ...subject(), sessionId: state.sessionId }))) return;
|
|
181
|
+
const target = { ...subject(), sessionId: state.sessionId }, version = observed;
|
|
182
|
+
const scope = accountScope(target); accountPending.add(scope); sync();
|
|
183
|
+
try {
|
|
184
|
+
const query = new URLSearchParams({ generation: target.generation, owner: target.owner, sessionId: target.sessionId });
|
|
185
|
+
const result = await bridge().api(route(target,'account') + '&' + query);
|
|
186
|
+
if (matches(target) && version === observed) render(result.state || result);
|
|
187
|
+
} catch (error) { if (matches(target) && version === observed) notice(error.detail || error.message); }
|
|
188
|
+
finally { accountPending.delete(scope); if (target.root === bridge().projectRoot && target.id === bridge().active) sync(); }
|
|
189
|
+
}
|
|
190
|
+
document.getElementById('nativeAccountRead').addEventListener('click', readAccount);
|
|
140
191
|
draft.addEventListener('input',()=>{ save(); sync(); });
|
|
141
192
|
document.getElementById('nativeSend').addEventListener('click',async()=>{
|
|
142
193
|
sync(); if (document.getElementById('nativeSend').disabled) return;
|
|
@@ -173,5 +224,4 @@ export function nativePanelScript() {
|
|
|
173
224
|
});
|
|
174
225
|
window.addEventListener('hcc:preferences',()=>{if(state&&!panel.hidden)render(state);});
|
|
175
226
|
window.hccNative={render,sync,read};
|
|
176
|
-
})();`;
|
|
177
227
|
}
|
package/lib/web/ui-review.mjs
CHANGED
|
@@ -42,8 +42,10 @@ export function reviewPanelHtml() {
|
|
|
42
42
|
</div></div>`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function reviewPanelScript() {
|
|
46
|
-
|
|
45
|
+
export function reviewPanelScript() { return '(' + installReview.toString() + ')();'; }
|
|
46
|
+
|
|
47
|
+
export function installReview() {
|
|
48
|
+
(() => {
|
|
47
49
|
const dialog = document.getElementById('reviewDialog'), form = document.getElementById('verificationForm');
|
|
48
50
|
const bridge = () => window.hccHandoff, tr = (key, fallback = '') => bridge().tr(key, fallback), esc = value => bridge().esc(String(value ?? ''));
|
|
49
51
|
let target = null, data = null, generation = 0, busy = false;
|
|
@@ -128,5 +130,5 @@ export function reviewPanelScript() {
|
|
|
128
130
|
document.getElementById('reviewClose').addEventListener('click', () => bridge().closeDialog(dialog));
|
|
129
131
|
window.addEventListener('hcc:preferences', () => { if (!dialog.hidden) render(); });
|
|
130
132
|
window.hccReview = { open,sync,reset() { generation++; busy = false; target = null; if (!dialog.hidden) bridge().closeDialog(dialog); } };
|
|
131
|
-
})()
|
|
133
|
+
})();
|
|
132
134
|
}
|