@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,79 @@
|
|
|
1
|
+
// Shared deliberately small Markdown renderer. HTML and links remain escaped text.
|
|
2
|
+
// The callback is supplied by each surface so escaping stays consistent.
|
|
3
|
+
export function createSafeMarkdown(escape) {
|
|
4
|
+
function inline(value, allowBold = true) {
|
|
5
|
+
const tick = String.fromCharCode(96), source = String(value), fragments = [];
|
|
6
|
+
let index = 0, plain = '';
|
|
7
|
+
const flush = () => { if (plain) fragments.push(escape(plain)); plain = ''; };
|
|
8
|
+
const codeEnd = start => {
|
|
9
|
+
const end = source.indexOf(tick, start + 1), newline = source.indexOf('\n', start + 1);
|
|
10
|
+
return end > start + 1 && (newline === -1 || end < newline) ? end : -1;
|
|
11
|
+
};
|
|
12
|
+
while (index < source.length) {
|
|
13
|
+
if (source[index] === tick && codeEnd(index) !== -1) {
|
|
14
|
+
const end = codeEnd(index);
|
|
15
|
+
flush(); fragments.push('<code>' + escape(source.slice(index + 1, end)) + '</code>'); index = end + 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const marker = source.slice(index, index + 2);
|
|
19
|
+
if (allowBold && (marker === '**' || marker === '__')) {
|
|
20
|
+
let end = index + 2;
|
|
21
|
+
while (end < source.length && source[end] !== '\n') {
|
|
22
|
+
// Code spans are opaque: bold markers inside them are literal text.
|
|
23
|
+
if (source[end] === tick && codeEnd(end) !== -1) { end = codeEnd(end) + 1; continue; }
|
|
24
|
+
if (source.startsWith(marker, end)) break;
|
|
25
|
+
end += 1;
|
|
26
|
+
}
|
|
27
|
+
if (end > index + 2 && source.startsWith(marker, end)) {
|
|
28
|
+
flush(); fragments.push('<strong>' + inline(source.slice(index + 2, end), false) + '</strong>'); index = end + 2;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
plain += source[index]; index += 1;
|
|
33
|
+
}
|
|
34
|
+
flush(); return fragments.join('');
|
|
35
|
+
}
|
|
36
|
+
function markdown(value) {
|
|
37
|
+
const lines = String(value).split('\n'), parts = [];
|
|
38
|
+
let paragraph = [], list = [], listType = '', listStart = '', quote = [];
|
|
39
|
+
const flushParagraph = () => { if (paragraph.length) parts.push('<p class="codex-prose">' + inline(paragraph.join('\n')) + '</p>'); paragraph = []; };
|
|
40
|
+
const flushList = () => {
|
|
41
|
+
if (list.length) parts.push('<' + listType + (listType === 'ol' && listStart !== '1' ? ' start="' + listStart + '"' : '') + '>'
|
|
42
|
+
+ list.map(line => '<li>' + inline(line) + '</li>').join('') + '</' + listType + '>');
|
|
43
|
+
list = []; listType = ''; listStart = '';
|
|
44
|
+
};
|
|
45
|
+
const flushQuote = () => { if (quote.length) parts.push('<blockquote>' + quote.map(line => inline(line)).join('<br>') + '</blockquote>'); quote = []; };
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
const heading = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
48
|
+
const unordered = /^\s{0,3}[-+*]\s+(.+)$/.exec(line);
|
|
49
|
+
const ordered = /^\s{0,3}(\d{1,9})[.)]\s+(.+)$/.exec(line);
|
|
50
|
+
const quoted = /^\s{0,3}>\s?(.*)$/.exec(line);
|
|
51
|
+
if (heading) {
|
|
52
|
+
flushParagraph(); flushList(); flushQuote();
|
|
53
|
+
const tag = 'h' + heading[1].length;
|
|
54
|
+
parts.push('<' + tag + '>' + inline(heading[2].replace(/\s+#+\s*$/, '')) + '</' + tag + '>');
|
|
55
|
+
} else if (unordered || ordered) {
|
|
56
|
+
flushParagraph(); flushQuote();
|
|
57
|
+
const nextType = unordered ? 'ul' : 'ol';
|
|
58
|
+
if (listType && listType !== nextType) flushList();
|
|
59
|
+
if (!listType) { listType = nextType; listStart = ordered ? String(Number(ordered[1])) : ''; }
|
|
60
|
+
list.push(unordered ? unordered[1] : ordered[2]);
|
|
61
|
+
} else if (quoted) {
|
|
62
|
+
flushParagraph(); flushList(); quote.push(quoted[1]);
|
|
63
|
+
} else {
|
|
64
|
+
flushList(); flushQuote();
|
|
65
|
+
if (!line.trim()) flushParagraph();
|
|
66
|
+
else paragraph.push(line);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
flushParagraph(); flushList(); flushQuote();
|
|
70
|
+
return '<div class="codex-markdown">' + parts.join('') + '</div>';
|
|
71
|
+
}
|
|
72
|
+
return markdown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function safeMarkdownScript() {
|
|
76
|
+
return 'window.hccSafeMarkdown ||= (' + createSafeMarkdown.toString() + ');';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function installSafeMarkdown() { const window = globalThis.window; window.hccSafeMarkdown ||= createSafeMarkdown; }
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} ReportedMetrics Facts received from the current executor; never text estimates.
|
|
3
|
+
* @property {string} source Provider/protocol source.
|
|
4
|
+
* @property {'turn'|'session'} scope The upstream counter's actual scope.
|
|
5
|
+
* @property {string=} turnId
|
|
6
|
+
* @property {number=} observedAt Local receipt timestamp in milliseconds.
|
|
7
|
+
* @property {number=} inputTokens
|
|
8
|
+
* @property {number=} outputTokens
|
|
9
|
+
* @property {number=} cachedInputTokens
|
|
10
|
+
* @property {number=} totalTokens
|
|
11
|
+
* @property {number=} contextTokens Actual occupancy, not cumulative usage.
|
|
12
|
+
* @property {number=} contextWindow Reported window limit.
|
|
13
|
+
* @property {number=} durationMs Provider-reported elapsed time.
|
|
14
|
+
* @property {string=} model
|
|
15
|
+
* @typedef {Object} RuntimeMetadata
|
|
16
|
+
* @property {string=} model
|
|
17
|
+
* @property {string=} permissionMode Read only. No configuration mutation is implied.
|
|
18
|
+
* @property {{name:string, description?:string}[]=} commands Explicitly advertised text commands only.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export function reportedMetrics(value) {
|
|
22
|
+
if (!value || typeof value.source !== 'string' || !['turn', 'session'].includes(value.scope)) return null;
|
|
23
|
+
const result = { source: value.source, scope: value.scope };
|
|
24
|
+
for (const key of ['turnId', 'model']) if (typeof value[key] === 'string') result[key] = value[key];
|
|
25
|
+
for (const key of ['observedAt', 'inputTokens', 'outputTokens', 'cachedInputTokens', 'totalTokens', 'contextTokens', 'contextWindow', 'durationMs']) {
|
|
26
|
+
if (typeof value[key] === 'number' && Number.isFinite(value[key]) && value[key] >= 0) result[key] = value[key];
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** UI-local text references do not grant provider file access or retrieve another transcript. */
|
|
32
|
+
export function sessionCapabilities(state = {}) {
|
|
33
|
+
const metadata = state.runtimeMetadata || {}, caps = state.capabilities || {};
|
|
34
|
+
return { textReferences: true, fileUpload: false, transcriptImport: false,
|
|
35
|
+
modelSelection: false, permissionSelection: false, send: caps.send !== false,
|
|
36
|
+
interrupt: caps.interrupt === true, steer: caps.steer === true,
|
|
37
|
+
commands: (Array.isArray(metadata.commands) ? metadata.commands : []).filter(command =>
|
|
38
|
+
typeof command?.name === 'string' && /^\/?[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(command.name))
|
|
39
|
+
.map(command => ({ name: '/' + command.name.replace(/^\//, ''), description: typeof command.description === 'string' ? command.description : '' })) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function contextReference(kind, value) {
|
|
43
|
+
if (typeof value !== 'string' || !value.trim() || /[\u0000-\u001f\u007f]/.test(value)) return null;
|
|
44
|
+
if (kind === 'file') {
|
|
45
|
+
const path = value.trim().replace(/\\/g, '/');
|
|
46
|
+
if (/^(?:\/|~|[A-Za-z]:)/.test(path) || path.split('/').some(part => part === '..' || !part)) return null;
|
|
47
|
+
return '[file reference: ' + JSON.stringify(path.replace(/^\.\//, '')) + ']';
|
|
48
|
+
}
|
|
49
|
+
if (kind === 'session') return '[session reference: ' + JSON.stringify(value) + ']';
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Search only retained, provider-reported records. Filters combine with AND. */
|
|
54
|
+
export function traceMatches(record, filter = {}) {
|
|
55
|
+
if (filter.turnId && String(record.turnId || '') !== filter.turnId) return false;
|
|
56
|
+
if (filter.kind === 'failed' && !(record.kind === 'error' || ['failed', 'error'].includes(record.status) || (record.exitCode != null && record.exitCode !== 0))) return false;
|
|
57
|
+
if (filter.kind === 'tools' && record.kind !== 'tool') return false;
|
|
58
|
+
if (filter.kind === 'files' && record.kind !== 'files') return false;
|
|
59
|
+
const query = String(filter.query || '').trim().toLocaleLowerCase();
|
|
60
|
+
if (!query) return true;
|
|
61
|
+
const haystack = [record.turnId, record.title, record.text, record.command, record.input, record.output,
|
|
62
|
+
...(record.changes || []).flatMap(change => [change.path, change.diff, change.oldText])].filter(value => value != null).join('\n').toLocaleLowerCase();
|
|
63
|
+
return query.split(/\s+/).every(word => haystack.includes(word));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sessionToolsHtml(prefix) {
|
|
67
|
+
return `<details class="session-tools" id="${prefix}ContextTools"><summary id="${prefix}ContextLabel">References and capabilities</summary>
|
|
68
|
+
<p id="${prefix}ContextHelp"></p><div class="session-tool-row"><input id="${prefix}FileQuery" type="search" autocomplete="off" aria-label="Project relative file path"><button id="${prefix}FileSearch" type="button">Find files</button></div>
|
|
69
|
+
<div class="session-tool-row"><select id="${prefix}FileResults" aria-label="Project files"></select><button id="${prefix}FileOpen" type="button">Open file</button><button id="${prefix}FileInsert" type="button">Reference file</button></div>
|
|
70
|
+
<div class="session-tool-row"><select id="${prefix}SessionResults" aria-label="Existing sessions"></select><button id="${prefix}SessionInsert" type="button">Reference session</button></div>
|
|
71
|
+
<div class="session-tool-row" id="${prefix}CommandRow" hidden><select id="${prefix}Commands" aria-label="Advertised commands"></select><button id="${prefix}CommandInsert" type="button">Insert command</button></div>
|
|
72
|
+
<p id="${prefix}CapabilitySummary"></p><p id="${prefix}ContextNotice" role="status" aria-live="polite"></p></details>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function metricsHtml(prefix) {
|
|
76
|
+
return `<details class="session-metrics"><summary id="${prefix}MetricsLabel">Reported run metrics</summary><dl id="${prefix}Metrics"></dl><p id="${prefix}MetricsSource"></p></details>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function traceFilterHtml(prefix) {
|
|
80
|
+
return `<details class="session-trace-tools"><summary id="${prefix}TraceFilterLabel">Find in retained history</summary><div class="session-tool-row">
|
|
81
|
+
<input id="${prefix}TraceQuery" type="search" autocomplete="off" aria-label="Find in retained history">
|
|
82
|
+
<select id="${prefix}TraceKind" aria-label="Event kind"></select><select id="${prefix}TraceTurn" aria-label="Turn"></select>
|
|
83
|
+
<button id="${prefix}TraceClear" type="button">Clear filters</button></div><p id="${prefix}TraceCount" role="status"></p></details>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createSessionTools({ document, window, prefix, bridge, draft, save, state, text, escape }) {
|
|
87
|
+
const node = suffix => document.getElementById(prefix + suffix);
|
|
88
|
+
let scope = '', sequence = 0, paths = [], sessionValues = [], commandValues = [], renderSignature = '';
|
|
89
|
+
const subject = () => JSON.stringify([bridge().projectRoot, bridge().active, state()?.executorId, state()?.generation, state()?.owner]);
|
|
90
|
+
const notice = value => { node('ContextNotice').textContent = value; };
|
|
91
|
+
function insert(value) {
|
|
92
|
+
if (!value) return;
|
|
93
|
+
if (scope !== subject()) { render(); notice(text('Session changed. Choose a reference again.', '会话已切换,请重新选择引用。')); return; }
|
|
94
|
+
const cursor = typeof draft.selectionStart === 'number' ? draft.selectionStart : draft.value.length;
|
|
95
|
+
const end = typeof draft.selectionEnd === 'number' ? draft.selectionEnd : cursor;
|
|
96
|
+
draft.value = draft.value.slice(0, cursor) + (cursor && draft.value[cursor - 1] !== '\n' ? '\n' : '') + value + '\n' + draft.value.slice(end);
|
|
97
|
+
save(); draft.focus(); notice(text('Added to your draft; nothing was sent or read.', '已加入草稿;尚未发送,也未读取引用内容。'));
|
|
98
|
+
}
|
|
99
|
+
function render() {
|
|
100
|
+
const next = subject(), current = state() || {}, metadata = current.runtimeMetadata || {};
|
|
101
|
+
if (scope !== next) { scope = next; sequence++; paths = []; node('FileQuery').value = ''; node('FileResults').innerHTML = ''; node('FileSearch').disabled = false; node('ContextTools').open = false; notice(''); }
|
|
102
|
+
const signature = JSON.stringify([next, metadata, current.capabilities, bridge().sessions, text('en', 'zh')]);
|
|
103
|
+
if (signature === renderSignature) return;
|
|
104
|
+
renderSignature = signature;
|
|
105
|
+
const capabilities = window.hccSessionCapabilities(current);
|
|
106
|
+
node('ContextLabel').textContent = text('References and capabilities', '引用与执行器能力');
|
|
107
|
+
node('ContextHelp').textContent = text('References add plain text only. Files and session history are not read or uploaded.', '引用仅加入明确的文本标记,不会读取或上传文件及会话历史。');
|
|
108
|
+
node('FileQuery').placeholder = text('Project relative path', '项目相对路径');
|
|
109
|
+
node('FileSearch').textContent = text('Find files', '查找文件');
|
|
110
|
+
node('FileInsert').textContent = text('Reference file', '引用文件');
|
|
111
|
+
node('FileOpen').textContent = text('Open file', '打开文件');
|
|
112
|
+
node('SessionInsert').textContent = text('Reference session', '引用会话');
|
|
113
|
+
node('CommandInsert').textContent = text('Insert command', '插入指令');
|
|
114
|
+
node('FileInsert').disabled = !paths.length;
|
|
115
|
+
node('FileOpen').disabled = !paths.length;
|
|
116
|
+
sessionValues = (bridge().sessions || []).map(session => ({ value: [session.kind || session.binding?.provider || 'other', session.peer_id || session.id, session.binding?.provider_session_id || session.binding?.provider_session_name || session.provider_session_label || ''].filter(Boolean).join(':'), label: session.task?.title || session.name || session.peer_id || session.id }));
|
|
117
|
+
const sessionsHtml = sessionValues.map((value, index) => '<option value="' + index + '">' + escape(value.label + ' · ' + value.value) + '</option>').join('');
|
|
118
|
+
if (node('SessionResults').innerHTML !== sessionsHtml) node('SessionResults').innerHTML = sessionsHtml;
|
|
119
|
+
node('SessionInsert').disabled = !sessionValues.length;
|
|
120
|
+
commandValues = capabilities.commands;
|
|
121
|
+
node('CommandRow').hidden = !commandValues.length;
|
|
122
|
+
const commandsHtml = commandValues.map((command, index) => '<option value="' + index + '">' + escape(command.name + (command.description ? ' · ' + command.description : '')) + '</option>').join('');
|
|
123
|
+
if (node('Commands').innerHTML !== commandsHtml) node('Commands').innerHTML = commandsHtml;
|
|
124
|
+
node('CapabilitySummary').textContent = text('Current executor · Model: ', '当前执行器 · 模型:') + (metadata.model || text('not reported', '未报告'))
|
|
125
|
+
+ text(' · Permissions: ', ' · 权限:') + (metadata.permissionMode || text('not reported', '未报告'))
|
|
126
|
+
+ text(' (read only). Model/permission changes, uploads and history import are not provided here.', '(只读)。此处不提供模型/权限切换、附件上传或历史导入。')
|
|
127
|
+
+ (!commandValues.length ? text(' No commands advertised by this executor.', ' 当前执行器未声明可用指令。') : '');
|
|
128
|
+
}
|
|
129
|
+
node('FileSearch').addEventListener('click', async () => {
|
|
130
|
+
const target = subject(), revision = ++sequence; node('FileSearch').disabled = true;
|
|
131
|
+
try {
|
|
132
|
+
const result = await bridge().api('/api/context/files?root=' + encodeURIComponent(bridge().projectRoot) + '&query=' + encodeURIComponent(node('FileQuery').value));
|
|
133
|
+
if (target !== subject() || revision !== sequence) return;
|
|
134
|
+
paths = (Array.isArray(result.paths) ? result.paths : []).filter(path => window.hccContextReference('file', path));
|
|
135
|
+
node('FileResults').innerHTML = paths.map((path, index) => '<option value="' + index + '">' + escape(path) + '</option>').join('');
|
|
136
|
+
node('FileInsert').disabled = !paths.length;
|
|
137
|
+
node('FileOpen').disabled = !paths.length;
|
|
138
|
+
notice(paths.length + text(' matching files', ' 个匹配文件') + (result.truncated ? text(' · Narrow your query for more results.', ' · 结果不完整,可缩小搜索范围。') : ''));
|
|
139
|
+
} catch (error) { if (target === subject() && revision === sequence) notice(error.detail || error.message); }
|
|
140
|
+
finally { if (target === subject() && revision === sequence) node('FileSearch').disabled = false; }
|
|
141
|
+
});
|
|
142
|
+
node('FileInsert').addEventListener('click', () => insert(window.hccContextReference('file', paths[Number(node('FileResults').value)])));
|
|
143
|
+
node('FileOpen').addEventListener('click', () => {
|
|
144
|
+
if (scope !== subject()) { render(); notice(text('Session changed. Choose a file again.', '会话已切换,请重新选择文件。')); return; }
|
|
145
|
+
const path = paths[Number(node('FileResults').value)];
|
|
146
|
+
if (path) window.hccFiles?.open(path);
|
|
147
|
+
});
|
|
148
|
+
node('SessionInsert').addEventListener('click', () => insert(window.hccContextReference('session', sessionValues[Number(node('SessionResults').value)]?.value)));
|
|
149
|
+
node('CommandInsert').addEventListener('click', () => insert(commandValues[Number(node('Commands').value)]?.name));
|
|
150
|
+
return { render };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function renderReportedMetrics({ document, prefix, state, text, escape }) {
|
|
154
|
+
const value = reportedMetrics(state?.metrics), unknown = text('Not reported', '未报告');
|
|
155
|
+
const target = document.getElementById(prefix + 'Metrics'), signature = JSON.stringify([value, unknown]);
|
|
156
|
+
if (target.dataset.metricsSignature === signature) return;
|
|
157
|
+
target.dataset.metricsSignature = signature;
|
|
158
|
+
const labels = [['model', 'Reported model', '报告的模型'], ['inputTokens', 'Input tokens', '输入 token'], ['outputTokens', 'Output tokens', '输出 token'],
|
|
159
|
+
['cachedInputTokens', 'Cached input tokens', '缓存输入 token'], ['totalTokens', 'Total tokens', '总 token'],
|
|
160
|
+
['contextTokens', 'Context occupancy', '上下文占用'], ['contextWindow', 'Context window limit', '上下文窗口上限'], ['durationMs', 'Duration (ms)', '耗时(毫秒)']];
|
|
161
|
+
document.getElementById(prefix + 'MetricsLabel').textContent = text('Reported run metrics', '运行指标(上游报告)');
|
|
162
|
+
document.getElementById(prefix + 'Metrics').innerHTML = labels.map(([key, en, zh]) => '<dt>' + escape(text(en, zh)) + '</dt><dd>' + escape(value?.[key] ?? unknown) + '</dd>').join('');
|
|
163
|
+
document.getElementById(prefix + 'MetricsSource').textContent = value ? value.source + ' · ' + text('Scope: ', '统计范围:') + (value.scope === 'turn' ? text('turn', '轮次') : text('session', '会话')) + (value.turnId ? ' · ' + value.turnId : '')
|
|
164
|
+
+ (value.observedAt ? ' · ' + text('Received ', '接收于 ') + new Date(value.observedAt).toLocaleString() : '')
|
|
165
|
+
: text('No upstream metrics reported. Unknown is not zero. Account limits are shown separately.', '上游尚未报告运行指标。未知不等于零,账号限额另行展示。');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function createTraceFilter({ document, prefix, text, escape, changed }) {
|
|
169
|
+
const node = suffix => document.getElementById(prefix + suffix);
|
|
170
|
+
let filter = { query: '', kind: 'all', turnId: '' }, subject = '', optionsSignature = '';
|
|
171
|
+
const notify = () => { filter = { query: node('TraceQuery').value, kind: node('TraceKind').value, turnId: node('TraceTurn').value }; changed(filter); };
|
|
172
|
+
for (const [suffix, event] of [['TraceQuery', 'input'], ['TraceKind', 'change'], ['TraceTurn', 'change']]) node(suffix).addEventListener(event, notify);
|
|
173
|
+
node('TraceClear').addEventListener('click', () => { filter = { query: '', kind: 'all', turnId: '' }; changed(filter); });
|
|
174
|
+
return { value: () => filter, clear() { filter = { query: '', kind: 'all', turnId: '' }; }, reset(scope) { if (subject !== scope) { subject = scope; filter = { query: '', kind: 'all', turnId: '' }; } },
|
|
175
|
+
render(turnIds, count, total) {
|
|
176
|
+
node('TraceFilterLabel').textContent = text('Find in retained history', '检索保留的历史');
|
|
177
|
+
node('TraceQuery').placeholder = text('Text, path, command or turn ID', '文本、路径、命令或轮次 ID');
|
|
178
|
+
node('TraceQuery').value = filter.query;
|
|
179
|
+
const signature = JSON.stringify([turnIds, filter.turnId, text('en', 'zh')]);
|
|
180
|
+
if (optionsSignature !== signature) {
|
|
181
|
+
optionsSignature = signature;
|
|
182
|
+
const kinds = [['all', 'All events', '所有事件'], ['failed', 'Failures', '失败'], ['tools', 'Tools', '工具'], ['files', 'File changes', '文件变更']];
|
|
183
|
+
node('TraceKind').innerHTML = kinds.map(([value, en, zh]) => '<option value="' + value + '">' + escape(text(en, zh)) + '</option>').join('');
|
|
184
|
+
const retained = [...new Set(turnIds.filter(Boolean))];
|
|
185
|
+
node('TraceTurn').innerHTML = '<option value="">' + escape(text('All turns', '所有轮次')) + '</option>' + retained.map(id => '<option value="' + escape(id) + '">' + escape(id) + '</option>').join('')
|
|
186
|
+
+ (filter.turnId && !retained.includes(filter.turnId) ? '<option value="' + escape(filter.turnId) + '">' + escape(filter.turnId + text(' · Outside retained history', ' · 不在保留窗口')) + '</option>' : '');
|
|
187
|
+
}
|
|
188
|
+
node('TraceKind').value = filter.kind;
|
|
189
|
+
node('TraceTurn').value = filter.turnId;
|
|
190
|
+
node('TraceClear').textContent = text('Clear filters', '清除筛选');
|
|
191
|
+
node('TraceCount').textContent = count + ' / ' + total + text(' retained records · Search does not fetch missing history.', ' 条保留记录 · 检索不会补取缺失历史。');
|
|
192
|
+
} };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function sessionToolsScript() {
|
|
196
|
+
return 'window.hccReportedMetrics = (' + reportedMetrics.toString() + ');'
|
|
197
|
+
+ 'window.hccSessionCapabilities = (' + sessionCapabilities.toString() + ');'
|
|
198
|
+
+ 'window.hccContextReference = (' + contextReference.toString() + ');'
|
|
199
|
+
+ 'window.hccTraceMatches = (' + traceMatches.toString() + ');'
|
|
200
|
+
+ 'window.hccCreateSessionTools = (' + createSessionTools.toString() + ');'
|
|
201
|
+
+ 'window.hccCreateTraceFilter = (' + createTraceFilter.toString() + ');'
|
|
202
|
+
+ 'window.hccRenderReportedMetrics = (' + renderReportedMetrics.toString().replace('reportedMetrics(state?.metrics)', 'window.hccReportedMetrics(state?.metrics)') + ');';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function installSessionTools() {
|
|
206
|
+
window.hccReportedMetrics = reportedMetrics;
|
|
207
|
+
window.hccSessionCapabilities = sessionCapabilities;
|
|
208
|
+
window.hccContextReference = contextReference;
|
|
209
|
+
window.hccTraceMatches = traceMatches;
|
|
210
|
+
window.hccCreateSessionTools = createSessionTools;
|
|
211
|
+
window.hccCreateTraceFilter = createTraceFilter;
|
|
212
|
+
window.hccRenderReportedMetrics = renderReportedMetrics;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const sessionToolsCss = `
|
|
216
|
+
.session-tools,.session-metrics,.session-trace-tools { font-size:var(--small-font); color:var(--muted); margin:8px 0; }
|
|
217
|
+
.session-tools summary,.session-metrics summary,.session-trace-tools summary { cursor:pointer; padding:5px 0; }
|
|
218
|
+
.session-tools p,.session-metrics p,.session-trace-tools p { overflow-wrap:anywhere; margin:6px 0; }
|
|
219
|
+
.session-tool-row { display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin-top:6px; }
|
|
220
|
+
.session-tool-row input,.session-tool-row select { flex:1 1 150px; min-width:0; }
|
|
221
|
+
.session-tool-row button { flex:0 0 auto; }
|
|
222
|
+
.session-metrics dl { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:5px 12px; margin:6px 0; }
|
|
223
|
+
.session-metrics dd { margin:0; color:var(--text); overflow-wrap:anywhere; }
|
|
224
|
+
`;
|