@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.
Files changed (146) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/docs/codex-account.zh-CN.md +38 -0
  3. package/docs/codex-protocol.md +59 -0
  4. package/docs/commands.md +9 -0
  5. package/docs/commands.zh-CN.md +7 -0
  6. package/docs/macos-process-identity.md +2 -0
  7. package/docs/native.md +179 -14
  8. package/docs/native.zh-CN.md +136 -6
  9. package/docs/web-handoff.zh-CN.md +118 -8
  10. package/lib/cli/app.mjs +47 -4
  11. package/lib/cli/commands/coordination.mjs +14 -4
  12. package/lib/cli/commands/dsh.mjs +1 -1
  13. package/lib/cli/commands/gc.mjs +22 -15
  14. package/lib/cli/commands/hook.mjs +19 -3
  15. package/lib/cli/commands/install.mjs +80 -10
  16. package/lib/cli/commands/misc.mjs +36 -9
  17. package/lib/cli/commands/native.mjs +4 -38
  18. package/lib/cli/commands/peer.mjs +3 -0
  19. package/lib/cli/commands/run.mjs +13 -6
  20. package/lib/cli/commands/tmux.mjs +1 -2
  21. package/lib/cli-runtime.mjs +19 -2
  22. package/lib/core/coordination/results.mjs +27 -10
  23. package/lib/core/sessions/hook-root-identity.mjs +47 -0
  24. package/lib/core/sessions/launch.mjs +7 -1
  25. package/lib/db/connection.mjs +140 -42
  26. package/lib/db/schema.mjs +59 -0
  27. package/lib/diagnostics/codex-protocol.mjs +105 -0
  28. package/lib/guidance.mjs +21 -36
  29. package/lib/integrations/codex-account.mjs +143 -0
  30. package/lib/integrations/codex-interactions.mjs +13 -11
  31. package/lib/integrations/dsh-collaboration.mjs +6 -12
  32. package/lib/integrations/dsh-cordis.mjs +17 -13
  33. package/lib/integrations/dsh.mjs +16 -11
  34. package/lib/integrations/mcp-elicitation.mjs +116 -0
  35. package/lib/integrations/mcp-url-elicitation.mjs +25 -0
  36. package/lib/integrations/native/claude.mjs +24 -6
  37. package/lib/integrations/native/codex.mjs +38 -7
  38. package/lib/integrations/native/dsh-acp.mjs +67 -24
  39. package/lib/integrations/native/index.mjs +4 -1
  40. package/lib/integrations/native/interactions.mjs +2 -1
  41. package/lib/integrations/native/jsonrpc.mjs +37 -5
  42. package/lib/integrations/native/telemetry.mjs +30 -0
  43. package/lib/process/identity.mjs +4 -0
  44. package/lib/process/pinned-cwd.mjs +302 -0
  45. package/lib/process/pinned-guidance.mjs +138 -0
  46. package/lib/process/pinned-project-state.mjs +105 -0
  47. package/lib/process/selected-cwd-identity.mjs +95 -0
  48. package/lib/runtime/buffer-evidence.mjs +2 -1
  49. package/lib/runtime/client.mjs +9 -0
  50. package/lib/runtime/native/client.mjs +4 -1
  51. package/lib/runtime/native/history.mjs +140 -0
  52. package/lib/runtime/native/launcher.mjs +106 -0
  53. package/lib/runtime/native/service.mjs +64 -8
  54. package/lib/runtime/native/store.mjs +83 -58
  55. package/lib/runtime/paths.mjs +17 -2
  56. package/lib/runtime/private-file.mjs +76 -0
  57. package/lib/runtime/private-state.mjs +546 -0
  58. package/lib/runtime/project-context.mjs +2 -1
  59. package/lib/runtime/project-path.mjs +92 -20
  60. package/lib/runtime/project-trust.mjs +79 -0
  61. package/lib/runtime/projects.mjs +9 -3
  62. package/lib/runtime/state-migration.mjs +697 -0
  63. package/lib/runtime/state.mjs +69 -27
  64. package/lib/shared/automatic-result-key.mjs +6 -0
  65. package/lib/shared/json-file.mjs +55 -6
  66. package/lib/shared/redact.mjs +2 -1
  67. package/lib/ui/help.mjs +6 -4
  68. package/lib/web/agent-defaults.mjs +110 -0
  69. package/lib/web/auto-attach.mjs +3 -2
  70. package/lib/web/browser/contracts.d.ts +10 -0
  71. package/lib/web/browser/core.mjs +1997 -0
  72. package/lib/web/browser/project-data.mjs +158 -0
  73. package/lib/web/browser/session-sync.mjs +82 -0
  74. package/lib/web/browser-assets.mjs +19 -0
  75. package/lib/web/buffer-gc-runtime.mjs +2 -2
  76. package/lib/web/codex-app-server.mjs +111 -37
  77. package/lib/web/codex-sessions.mjs +61 -12
  78. package/lib/web/context-files.mjs +82 -0
  79. package/lib/web/cookie-auth.mjs +51 -3
  80. package/lib/web/external-sessions.mjs +76 -9
  81. package/lib/web/http-routes.mjs +238 -25
  82. package/lib/web/http.mjs +50 -4
  83. package/lib/web/liveness-reaper.mjs +2 -2
  84. package/lib/web/native-sessions.mjs +323 -14
  85. package/lib/web/project-contexts.mjs +236 -44
  86. package/lib/web/project-file-write.mjs +249 -0
  87. package/lib/web/project-files.mjs +268 -0
  88. package/lib/web/pty-sessions.mjs +11 -3
  89. package/lib/web/runtime-main.mjs +132 -80
  90. package/lib/web/session-serialize.mjs +87 -8
  91. package/lib/web/session-state-sync.mjs +98 -0
  92. package/lib/web/shutdown-diagnostics.mjs +38 -0
  93. package/lib/web/shutdown-response.mjs +21 -0
  94. package/lib/web/shutdown-state.mjs +91 -0
  95. package/lib/web/startup.mjs +103 -35
  96. package/lib/web/task-results.mjs +19 -19
  97. package/lib/web/tls.mjs +84 -33
  98. package/lib/web/tmux-sessions.mjs +73 -9
  99. package/lib/web/tmux-stream.mjs +36 -8
  100. package/lib/web/ui-agent-defaults.mjs +149 -0
  101. package/lib/web/ui-agent-start.mjs +330 -0
  102. package/lib/web/ui-codex-account.mjs +81 -0
  103. package/lib/web/ui-codex.mjs +105 -85
  104. package/lib/web/ui-command-palette.mjs +21 -5
  105. package/lib/web/ui-file-writes.mjs +220 -0
  106. package/lib/web/ui-files.mjs +231 -0
  107. package/lib/web/ui-handoff.mjs +10 -2
  108. package/lib/web/ui-history.mjs +175 -105
  109. package/lib/web/ui-i18n.mjs +280 -8
  110. package/lib/web/ui-interactions.mjs +107 -6
  111. package/lib/web/ui-native-timeline.mjs +326 -0
  112. package/lib/web/ui-native.mjs +93 -43
  113. package/lib/web/ui-review.mjs +5 -3
  114. package/lib/web/ui-safe-markdown.mjs +79 -0
  115. package/lib/web/ui-session-tools.mjs +224 -0
  116. package/lib/web/ui-template.mjs +29 -1968
  117. package/lib/web/ui-terminal-find.mjs +5 -3
  118. package/lib/web/ui-workbench.mjs +5 -3
  119. package/lib/web/ui-workspace.mjs +3 -0
  120. package/package.json +8 -2
  121. package/scripts/benchmark-session-sync.mjs +53 -0
  122. package/scripts/codex-protocol.mjs +88 -0
  123. package/scripts/dsh-approval-layers-acceptance.mjs +172 -0
  124. package/scripts/dsh-package-provenance.mjs +118 -0
  125. package/scripts/mcp-form-live-fixture.mjs +81 -0
  126. package/scripts/native-bounded-live-acceptance.mjs +193 -0
  127. package/scripts/native-fixture-root.mjs +49 -0
  128. package/scripts/native-installed-acceptance.mjs +611 -0
  129. package/scripts/native-stability-checks.mjs +40 -0
  130. package/scripts/regression-cookie-expiry.mjs +70 -0
  131. package/scripts/regression-http-transport.mjs +9 -0
  132. package/scripts/regression-terminal-marker.mjs +95 -0
  133. package/scripts/regression.mjs +481 -219
  134. package/scripts/shim-reentry-probe.mjs +105 -0
  135. package/scripts/web-acceptance-diagnostics.mjs +90 -0
  136. package/scripts/web-codex-account-acceptance.mjs +178 -0
  137. package/scripts/web-file-preview-checks.mjs +302 -0
  138. package/scripts/web-mcp-form-acceptance.mjs +134 -0
  139. package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
  140. package/scripts/web-native-handoff-checks.mjs +128 -0
  141. package/scripts/web-native-interaction-acceptance.mjs +219 -18
  142. package/scripts/web-native-interaction-diagnostics.mjs +80 -0
  143. package/scripts/web-native-model-acceptance.mjs +22 -7
  144. package/scripts/web-session-tools-checks.mjs +112 -0
  145. package/scripts/web-workbench-acceptance.mjs +435 -0
  146. package/scripts/web-workbench-installed-acceptance.mjs +122 -0
@@ -0,0 +1,231 @@
1
+ import { createSafeMarkdown } from './ui-safe-markdown.mjs';
2
+ import { createFileWrites, fileWritesCss, fileUploadHtml, fileEditorHtml } from './ui-file-writes.mjs';
3
+
4
+ const escape = value => String(value).replace(/[&<>"']/g, character => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[character]));
5
+ export function renderFileMarkdown(content) {
6
+ const markdown = createSafeMarkdown(escape), parts = [];
7
+ let prose = [], code = null, fence = '';
8
+ for (const line of String(content).split('\n')) {
9
+ const marker = /^\s{0,3}(`{3,}|~{3,})(.*)$/.exec(line);
10
+ if (!code && marker) {
11
+ if (prose.length) parts.push(markdown(prose.join('\n')));
12
+ prose = []; code = []; fence = marker[1];
13
+ } else if (code && marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) {
14
+ parts.push('<pre><code>' + escape(code.join('\n')) + '</code></pre>'); code = null;
15
+ } else (code || prose).push(line);
16
+ }
17
+ if (code) parts.push('<pre><code>' + escape(code.join('\n')) + '</code></pre>');
18
+ if (prose.length) parts.push(markdown(prose.join('\n')));
19
+ return parts.join('');
20
+ }
21
+
22
+ export function staticPreviewCss(value) {
23
+ const css = String(value).replace(/\/\*[\s\S]*?\*\//g, '');
24
+ // Escape sequences and resource-bearing CSS are omitted rather than decoded
25
+ // into a second URL language. The preview CSP independently blocks requests.
26
+ const resourceSyntax = /\\|url\s*\(|@import|@font-face|image-set\s*\(|expression\s*\(|behavior\s*:|-moz-binding|https?:/i;
27
+ return resourceSyntax.test(css) ? '' : css;
28
+ }
29
+
30
+ export function staticPreviewNode(node) {
31
+ if (node.nodeType === 3) return escape(node.textContent || '');
32
+ if (node.nodeType !== 1 || (node.namespaceURI && node.namespaceURI !== 'http://www.w3.org/1999/xhtml')) return '';
33
+ const tag = String(node.localName).toLowerCase();
34
+ if (['script','iframe','object','embed','link','meta','base','title','template','noscript','svg','math','form','input','select','textarea','button','audio','video','source'].includes(tag)) return '';
35
+ if (tag === 'style') return '<style>' + staticPreviewCss(node.textContent).replace(/</g, '\\3c ') + '</style>';
36
+ const body = [...(node.childNodes || [])].map(staticPreviewNode).join('');
37
+ if (!['html','head','body','main','article','section','header','footer','nav','aside','div','span','p','h1','h2','h3','h4','h5','h6','ul','ol','li','dl','dt','dd','table','thead','tbody','tfoot','tr','th','td','caption','colgroup','col','pre','code','blockquote','strong','em','b','i','u','s','small','sub','sup','br','hr','img','a','figure','figcaption','details','summary','time'].includes(tag)) return body;
38
+ if (['html','head','body'].includes(tag)) return body;
39
+ const attrs = [];
40
+ for (const attr of [...(node.attributes || [])]) {
41
+ const name = attr.name.toLowerCase();
42
+ if (['id','class','title','alt','width','height','colspan','rowspan','scope','dir','lang'].includes(name)) attrs.push(name + '="' + escape(attr.value) + '"');
43
+ else if (name === 'style') { const css = staticPreviewCss(attr.value); if (css) attrs.push('style="' + escape(css) + '"'); }
44
+ else if (tag === 'img' && name === 'src' && /^data:image\/(?:png|jpeg|gif|webp);base64,[A-Za-z0-9+/=\s]+$/i.test(attr.value)) attrs.push('src="' + escape(attr.value) + '"');
45
+ }
46
+ return '<' + tag + (attrs.length ? ' ' + attrs.join(' ') : '') + '>' + body + (['br','hr','img','col'].includes(tag) ? '' : '</' + tag + '>');
47
+ }
48
+
49
+ export function staticHtmlPreview(content, document) {
50
+ // Template contents are inert: parsing cannot start a subresource fetch or
51
+ // execute a script. Rebuild the output rather than trusting the original DOM.
52
+ const template = document.createElement('template'); template.innerHTML = String(content);
53
+ const body = [...template.content.childNodes].map(staticPreviewNode).join('');
54
+ const csp = "default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'none'; media-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
55
+ return '<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="' + escape(csp)
56
+ + '"><meta name="referrer" content="no-referrer"><style>body{margin:20px;overflow-wrap:anywhere}img{max-width:100%;height:auto}pre{white-space:pre-wrap}</style></head><body>' + body + '</body></html>';
57
+ }
58
+
59
+ export const filesCss = `
60
+ ${fileWritesCss}
61
+ .files-dialog { width:min(1180px,96vw); height:min(850px,92dvh); max-width:none; display:flex; flex-direction:column; gap:12px; overflow:hidden; }
62
+ .files-project,.files-meta { color:var(--muted); font-size:var(--small-font); overflow-wrap:anywhere; margin:0; }
63
+ .files-layout { flex:1; min-height:0; display:grid; grid-template-columns:minmax(220px,29%) minmax(0,1fr); gap:12px; }
64
+ .files-browser,.files-view { min-width:0; min-height:0; display:flex; flex-direction:column; gap:8px; }
65
+ .files-browser { border-right:1px solid var(--border); padding-right:12px; }
66
+ .files-path-form,.files-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:6px; }
67
+ .files-path-form input { min-width:0; flex:1; }
68
+ .files-entries { overflow:auto; flex:1; min-height:0; display:flex; flex-direction:column; gap:4px; }
69
+ .files-entries button { height:auto; min-height:36px; width:100%; text-align:left; overflow-wrap:anywhere; flex:none; }
70
+ .files-entries button[aria-current="true"] { background:var(--selection-bg); border-color:var(--accent); }
71
+ .files-preview { flex:1; min-width:0; min-height:0; overflow:auto; border:1px solid var(--border); border-radius:8px; padding:12px; }
72
+ .files-preview pre { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; font:var(--small-font)/1.6 var(--mono); }
73
+ .files-preview img { display:block; max-width:100%; height:auto; margin:auto; }
74
+ .files-preview iframe { border:0; width:100%; height:100%; min-height:350px; background:white; }
75
+ .files-preview:has(iframe) { padding:0; }
76
+ .files-preview .codex-markdown { max-width:850px; margin:auto; }
77
+ #filesBack { display:none; }
78
+ .files-dialog [hidden] { display:none!important; }
79
+ .files-dialog[data-main-window-required="true"] .files-layout { grid-template-columns:1fr; }
80
+ .files-dialog[data-main-window-required="true"] .files-browser,.files-dialog[data-main-window-required="true"] .files-toolbar { display:none; }
81
+ @media(max-width:700px) {
82
+ .files-dialog { width:96vw; height:94dvh; padding:12px; }
83
+ .files-layout { display:flex; }
84
+ .files-browser,.files-view { flex:1; }
85
+ .files-browser { border:0; padding:0; }
86
+ .files-dialog[data-mobile-view="tree"] .files-view { display:none; }
87
+ .files-dialog[data-mobile-view="preview"] .files-browser { display:none; }
88
+ #filesBack { display:inline-flex; }
89
+ .files-toolbar button,.files-entries button { min-height:44px; }
90
+ }
91
+ `;
92
+
93
+ export function filesHtml() {
94
+ return `<div class="dialog-overlay" id="filesDialog" role="dialog" aria-modal="true" aria-labelledby="filesTitle" hidden><section class="dialog files-dialog" id="filesPanel" data-mobile-view="tree">
95
+ <header class="dialog-heading"><h3 id="filesTitle" data-i18n="files.title">Project files</h3><button id="filesClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
96
+ <p class="files-project" id="filesProject"></p><p class="dialog-help" data-i18n="files.readOnly">Preview, upload and edit project files. File contents are not sent to an Agent.</p>
97
+ <div class="files-layout"><section class="files-browser">
98
+ <form class="files-path-form" id="filesPathForm"><input id="filesPath" type="text" autocomplete="off" data-i18n-aria="files.path" aria-label="Project relative directory"><button id="filesGo" type="submit" data-i18n="files.go">Go</button></form>
99
+ <div class="files-toolbar"><button id="filesUp" type="button" data-i18n="files.up">Parent directory</button><button id="filesRefresh" type="button" data-i18n="refresh">Refresh</button></div>
100
+ ${fileUploadHtml()}
101
+ <p class="dialog-help" id="filesTreeStatus" role="status"></p><div class="files-entries" id="filesEntries" data-i18n-aria="files.title" aria-label="Project files"></div>
102
+ </section><section class="files-view">
103
+ <div class="files-toolbar"><button id="filesBack" type="button" data-i18n="files.back">Back to files</button><strong id="filesName"></strong><button id="filesSourceBtn" type="button" data-i18n="files.source" aria-pressed="false" hidden>Source</button><button id="filesReload" type="button" data-i18n="refresh" hidden>Refresh</button><a id="filesDownload" data-i18n="files.download" hidden>Download PDF</a></div>
104
+ <p class="files-meta" id="filesMeta"></p><p class="dialog-help" id="filesPreviewStatus" role="status"></p>${fileEditorHtml()}<div class="files-preview" id="filesPreview" tabindex="0"></div>
105
+ </section></div>
106
+ </section></div>`;
107
+ }
108
+
109
+ export function installFiles(browser = globalThis) {
110
+ const { window, document } = browser, bridge = window.hccHandoff, byId = id => document.getElementById(id);
111
+ const dialog = byId('filesDialog'), panel = byId('filesPanel'), preview = byId('filesPreview');
112
+ const tr = key => bridge.tr(key);
113
+ let revision = 0, treeSequence = 0, fileSequence = 0, root = '', rootIdentity = '', directory = '', selected = '', value = null, source = false;
114
+ const urls = new Set();
115
+ const writes = createFileWrites({browser,
116
+ context:()=>({root,identity:rootIdentity,directory,path:selected,value,hidden:dialog.hidden,visit:revision+':'+fileSequence}),
117
+ updated:next=>{value=next;render();writes.select();},
118
+ uploaded:path=>{void tree(directory);void openFile(path);}
119
+ });
120
+ const current = visit => !dialog.hidden && visit === revision && root === bridge.projectRoot && rootIdentity === (bridge.projectIdentity || '');
121
+ function release() { for (const url of urls) browser.URL.revokeObjectURL(url); urls.clear(); }
122
+ function clearPreview() {
123
+ writes.suspend();
124
+ preview.replaceChildren(); release(); value = null;
125
+ for (const id of ['filesName','filesMeta','filesPreviewStatus']) byId(id).textContent = '';
126
+ for (const id of ['filesSourceBtn','filesReload','filesDownload']) byId(id).hidden = true;
127
+ byId('filesDownload').removeAttribute('href');
128
+ }
129
+ function blobUrl(content, mime) {
130
+ const bytes = browser.Uint8Array.from(browser.atob(content), character => character.charCodeAt(0));
131
+ const url = browser.URL.createObjectURL(new browser.Blob([bytes], { type:mime })); urls.add(url); return url;
132
+ }
133
+ function render() {
134
+ if (!value) return;
135
+ preview.replaceChildren(); release();
136
+ const download = byId('filesDownload'); download.hidden = true; download.removeAttribute('href');
137
+ byId('filesName').textContent = value.name || value.path;
138
+ byId('filesMeta').textContent = value.path + ' · ' + (Number.isFinite(value.size) ? value.size + ' bytes' : '');
139
+ const textual = value.encoding === 'utf8' && ['text','markdown','html'].includes(value.kind);
140
+ const sourceOnly = value.kind === 'html' && value.truncated;
141
+ byId('filesSourceBtn').hidden = !textual || value.kind === 'text' || sourceOnly;
142
+ byId('filesSourceBtn').setAttribute('aria-pressed',String(source));
143
+ byId('filesSourceBtn').textContent = tr(source ? 'files.rendered' : 'files.source');
144
+ byId('filesReload').hidden = false;
145
+ byId('filesPreviewStatus').textContent = [value.truncated ? tr('files.truncated') : '',
146
+ value.kind === 'html' ? tr('files.htmlLimit') : value.kind === 'pdf' ? tr('files.pdfHelp') : ''].filter(Boolean).join(' ');
147
+ if (textual && (source || sourceOnly || value.kind === 'text')) {
148
+ const pre = document.createElement('pre'); pre.textContent = value.content || ''; preview.appendChild(pre);
149
+ } else if (value.kind === 'markdown' && textual) {
150
+ preview.innerHTML = renderFileMarkdown(value.content || '');
151
+ } else if (value.kind === 'html' && textual) {
152
+ const frame = document.createElement('iframe'); frame.id = 'filesFrame'; frame.title = tr('files.staticHtml');
153
+ frame.setAttribute('sandbox',''); frame.referrerPolicy = 'no-referrer'; frame.srcdoc = staticHtmlPreview(value.content || '',document); preview.appendChild(frame);
154
+ } else if (value.kind === 'image' && value.encoding === 'base64' && /^image\/(png|jpeg|gif|webp)$/.test(value.mime)) {
155
+ const img = document.createElement('img'); img.alt = value.name || value.path; img.src = blobUrl(value.content,value.mime); preview.appendChild(img);
156
+ } else if (value.kind === 'pdf' && value.encoding === 'base64' && value.mime === 'application/pdf' && browser.atob(value.content.slice(0,8)).startsWith('%PDF-')) {
157
+ const url = blobUrl(value.content,'application/pdf');
158
+ const frame = document.createElement('iframe'); frame.id = 'filesFrame'; frame.title = value.name || 'PDF';
159
+ frame.referrerPolicy = 'no-referrer'; frame.src = url; preview.appendChild(frame);
160
+ download.href = url; download.download = value.name || 'document.pdf'; download.hidden = false;
161
+ } else preview.textContent = tr('files.unsupported');
162
+ writes.sync();
163
+ }
164
+ async function tree(path = '') {
165
+ if (bridge.draftScope === 'auxiliary') return;
166
+ const visit = revision, sequence = ++treeSequence;
167
+ directory = path; byId('filesPath').value = path; byId('filesUp').disabled = !path;
168
+ writes.sync();
169
+ byId('filesTreeStatus').textContent = tr('files.loading'); byId('filesEntries').replaceChildren();
170
+ try {
171
+ const result = await bridge.api('/api/files/tree?path=' + encodeURIComponent(path));
172
+ if (!current(visit) || sequence !== treeSequence) return;
173
+ directory = result.path || ''; byId('filesPath').value = directory; byId('filesUp').disabled = !directory;
174
+ writes.sync();
175
+ const entries = Array.isArray(result.entries) ? result.entries : [];
176
+ byId('filesTreeStatus').textContent = result.truncated ? tr('files.treeTruncated') : entries.length ? '' : tr('files.empty');
177
+ for (const entry of entries) {
178
+ if (!['file','directory'].includes(entry.type) || typeof entry.path !== 'string') continue;
179
+ const button = document.createElement('button'); button.type = 'button'; button.dataset.filePath = entry.path; button.dataset.fileType = entry.type;
180
+ button.textContent = (entry.type === 'directory' ? '▸ ' : '') + entry.name;
181
+ button.setAttribute('aria-current',String(entry.path === selected));
182
+ button.addEventListener('click', () => entry.type === 'directory' ? tree(entry.path) : openFile(entry.path));
183
+ byId('filesEntries').appendChild(button);
184
+ }
185
+ } catch (error) { if (current(visit) && sequence === treeSequence) byId('filesTreeStatus').textContent = error.detail || error.message; }
186
+ }
187
+ async function openFile(path) {
188
+ if (bridge.draftScope === 'auxiliary') return;
189
+ const visit = revision, sequence = ++fileSequence;
190
+ selected = path; source = false; clearPreview(); panel.dataset.mobileView = 'preview';
191
+ for (const button of byId('filesEntries').children) button.setAttribute('aria-current',String(button.dataset.filePath === path));
192
+ byId('filesName').textContent = path; byId('filesPreviewStatus').textContent = tr('files.loading');
193
+ byId('filesReload').hidden = false;
194
+ try {
195
+ const result = await bridge.api('/api/files/preview?path=' + encodeURIComponent(path));
196
+ if (!current(visit) || sequence !== fileSequence) return;
197
+ value = result; render(); writes.select();
198
+ } catch (error) { if (current(visit) && sequence === fileSequence) { byId('filesPreviewStatus').textContent = error.detail || error.message; writes.select(); } }
199
+ }
200
+ function closed() { revision++; treeSequence++; fileSequence++; clearPreview(); }
201
+ function reset() { closed(); if (!dialog.hidden) bridge.closeDialog(dialog); }
202
+ function open(path = '') {
203
+ if (bridge.draftScope === 'auxiliary') {
204
+ try {
205
+ if (window.parent !== window && window.parent.hccHandoff?.projectRoot === bridge.projectRoot && window.parent.hccHandoff?.projectIdentity === bridge.projectIdentity && window.parent.hccFiles) {
206
+ window.parent.hccFiles.open(path); return;
207
+ }
208
+ } catch { /* A detached or cross-origin parent cannot accept this request. */ }
209
+ closed(); root = bridge.projectRoot; panel.dataset.mobileView = 'preview';
210
+ panel.dataset.mainWindowRequired = 'true';
211
+ byId('filesProject').textContent = root; preview.textContent = tr('files.mainWindowRequired');
212
+ bridge.openDialog(dialog,byId('filesClose')); return;
213
+ }
214
+ delete panel.dataset.mainWindowRequired;
215
+ closed(); root = bridge.projectRoot; rootIdentity = bridge.projectIdentity || ''; directory = ''; selected = ''; panel.dataset.mobileView = path ? 'preview' : 'tree';
216
+ byId('filesProject').textContent = root; bridge.openDialog(dialog,byId('filesPath'));
217
+ void tree(path ? path.split('/').slice(0,-1).join('/') : '');
218
+ if (path) void openFile(path); else preview.textContent = tr('files.select');
219
+ }
220
+ byId('filesBtn').addEventListener('click',() => open());
221
+ byId('filesClose').addEventListener('click',() => bridge.closeDialog(dialog));
222
+ byId('filesPathForm').addEventListener('submit',event => { event.preventDefault(); void tree(byId('filesPath').value.trim().replace(/^\.\/?$/,'')); });
223
+ byId('filesUp').addEventListener('click',() => tree(directory.split('/').slice(0,-1).join('/')));
224
+ byId('filesRefresh').addEventListener('click',() => tree(directory));
225
+ byId('filesReload').addEventListener('click',() => selected && openFile(selected));
226
+ byId('filesBack').addEventListener('click',() => { panel.dataset.mobileView = 'tree'; byId('filesPath').focus(); });
227
+ byId('filesSourceBtn').addEventListener('click',() => { source = !source; render(); });
228
+ window.addEventListener('hcc:preferences',() => { if (!dialog.hidden) render(); });
229
+ window.addEventListener('pagehide',closed);
230
+ window.hccFiles = { open,closed,reset };
231
+ }
@@ -74,7 +74,13 @@ export const handoffCss = `
74
74
  .main > #codexPanel { flex:1; min-height:0; height:auto !important; }
75
75
  .handoff-bar { display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:8px 12px; border-bottom:1px solid var(--border); background:var(--panel); }
76
76
  .handoff-status { flex:1 1 220px; font-size:12px; color:var(--muted); }
77
- .handoff-detail { width:100%; font-size:11px; color:var(--muted); overflow-wrap:anywhere; }
77
+ .handoff-detail { flex:0 1 auto; min-width:0; font-size:11px; color:var(--muted); overflow-wrap:anywhere; }
78
+ .handoff-detail > summary { cursor:pointer; padding:6px 0; }
79
+ .handoff-detail[open] { flex-basis:100%; }
80
+ .handoff-detail[open] > summary { margin-bottom:4px; }
81
+ .active-identity { display:grid; grid-template-columns:auto minmax(0,1fr); gap:5px 10px; margin:8px 0; }
82
+ .active-identity dt { color:var(--muted); }
83
+ .active-identity dd { margin:0; color:var(--text); font-family:var(--mono); overflow-wrap:anywhere; }
78
84
  .handoff-bar button { height:30px; font-size:12px; }
79
85
  .terminal-composer { display:grid; gap:6px; padding:10px 12px; border-top:1px solid var(--border); background:var(--panel); }
80
86
  .terminal-composer textarea { width:100%; min-height:64px; max-height:160px; resize:vertical; font:inherit; border:1px solid var(--border); border-radius:6px; padding:8px; background:var(--input-bg); color:var(--text); }
@@ -90,7 +96,7 @@ export function handoffBarHtml() {
90
96
  <button id="claimControlBtn" type="button" data-i18n="handoff.claim">Take control</button>
91
97
  <button id="releaseControlBtn" type="button" data-i18n="handoff.release" hidden>Release control</button>
92
98
  <button id="reconnectBtn" type="button" data-i18n="handoff.reconnect">Reconnect</button>
93
- <details class="handoff-detail"><summary id="handoffHelp">Attachment details</summary><div id="handoffDetail"></div></details>
99
+ <details class="handoff-detail" id="activeDetails"><summary id="handoffHelp">Session details</summary><div id="activeTask" hidden></div><dl class="active-identity" id="activeIdentity"></dl><div id="handoffDetail"></div></details>
94
100
  </div>`;
95
101
  }
96
102
 
@@ -100,3 +106,5 @@ export function terminalComposerHtml() {
100
106
  <div class="composer-actions"><span class="composer-note" id="draftStatus" role="status" aria-live="polite"></span><button id="reviewDraftBtn" type="button" data-i18n="handoff.review" hidden>Reviewed; keep draft</button><button class="primary" id="sendDraftBtn" type="submit" data-i18n="handoff.send">Send + Enter</button></div>
101
107
  </form>`;
102
108
  }
109
+
110
+ export function installHandoff() { const window = globalThis.window; window.hccHandoffStore = createHandoffStore((() => { try { return window.localStorage; } catch { return null; } })(), { scope: window.hccUi?.draftScope || window.hccDraftScope || '' }); }
@@ -1,118 +1,188 @@
1
+ import { nativeTimeline } from './ui-native-timeline.mjs';
2
+
1
3
  export function codexHistoryHtml() {
2
4
  return `<div class="dialog-overlay" id="historyDialog" role="dialog" aria-modal="true" aria-labelledby="historyTitle" hidden>
3
5
  <div class="dialog auxiliary-dialog">
4
- <header class="dialog-heading"><h3 id="historyTitle" data-i18n="history.title">Codex history</h3><button id="historyClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
5
- <p class="dialog-help" data-i18n="history.help">Browse saved history, then explicitly resume or fork it in a separate executor.</p>
6
- <p id="historyNotice" class="dialog-help" role="status" aria-live="polite"></p>
6
+ <header class="dialog-heading"><h3 id="historyTitle" data-i18n="history.title">History</h3><button id="historyClose" type="button" data-i18n-aria="close" aria-label="Close">×</button></header>
7
+ <div class="session-tool-row"><label><span data-i18n="history.source">Source</span> <select id="historySource"><option value="native" data-i18n="history.native">HCC retained history</option><option value="codex" data-i18n="history.codex">Codex saved threads</option></select></label>
8
+ <label id="historyProviderField"><span data-i18n="agent.provider">Agent</span> <select id="historyProvider"><option value="all" data-i18n="history.allProviders">All providers</option><option value="codex">Codex</option><option value="claude">Claude</option><option value="dsh">DeepSeek Harness</option></select></label></div>
9
+ <p class="dialog-help" id="historyHelp"></p><p id="historyNotice" class="dialog-help" role="status" aria-live="polite"></p>
10
+ <p id="historyUnverified" class="dialog-help" role="status" aria-live="polite"></p>
7
11
  <div class="history-layout"><div class="history-list"><button id="historyRefresh" type="button" data-i18n="refresh">Refresh</button><div id="historyThreads"></div><button id="historyMore" type="button" data-i18n="history.more" hidden>Load more</button></div>
8
- <div class="history-preview"><h4 id="historySelected"></h4><div id="historyContent"></div>
9
- <label class="confirmation-row"><input id="historyConfirmed" type="checkbox"><span data-i18n="history.confirm">I checked the original executor. Opening starts a separate executor; resuming the same thread requires stopping the original.</span></label>
12
+ <div class="history-preview"><h4 id="historySelected"></h4><p id="historyIdentity" class="dialog-help"></p><p id="historyRetention" class="dialog-help"></p>
13
+ <p id="historyResumeReason" class="dialog-help"></p>
14
+ <label class="confirmation-row"><input id="historyConfirmed" type="checkbox"><span id="historyConfirmLabel"></span></label>
10
15
  <div class="btns"><button id="historyResume" type="button" data-i18n="history.resume" disabled>Resume in a new executor</button><button id="historyFork" type="button" data-i18n="history.fork" disabled>Fork into a new executor</button></div>
16
+ <div class="session-tool-row" id="historyEventControls" hidden><button id="historyEventsPrevious" type="button" data-i18n="history.previousEvents">Previous events</button><span id="historyEventPage"></span><button id="historyEventsNext" type="button" data-i18n="history.nextEvents">Next events</button><button id="historyEventsReload" type="button" data-i18n="refresh">Refresh</button><label><span data-i18n="history.view">View</span> <select id="historyNativeView"><option value="conversation" data-i18n="history.output">Recorded output</option><option value="trace" data-i18n="history.trace">Event trace</option></select></label></div>
17
+ <div id="historyContent"></div><details id="historyReceipts" hidden><summary data-i18n="history.receipts">Recent delivery receipts</summary><pre id="historyReceiptContent"></pre></details>
11
18
  </div></div>
12
19
  </div>
13
20
  </div>`;
14
21
  }
15
22
 
16
- export function codexHistoryScript() {
17
- return String.raw`(() => {
18
- const dialog = document.getElementById('historyDialog');
19
- const bridge = () => window.hccHandoff;
20
- const tr = (key, fallback = '') => bridge().tr(key, fallback);
21
- const esc = value => bridge().esc(String(value ?? ''));
22
- let root = '', entries = [], cursor = null, selected = null, busy = false, generation = 0;
23
- const same = (project, version) => project === root && project === bridge().projectRoot && version === generation && !dialog.hidden;
24
- const route = (path, project = root) => path + (path.includes('?') ? '&' : '?') + 'root=' + encodeURIComponent(project);
25
- function notice(value) { document.getElementById('historyNotice').textContent = value || ''; }
26
- function liveSources() {
27
- return (bridge().sessions || []).filter(session => session.status === 'running' && session.binding?.provider === 'codex' && session.binding.provider_session_id === selected?.id);
28
- }
29
- function controlsSource(sources) { return sources.every(session => session.id === bridge().active && bridge().canControl); }
30
- function sync() {
31
- const confirmed = document.getElementById('historyConfirmed').checked;
32
- const sources = liveSources(), unavailable = busy || !selected || !confirmed || root !== bridge().projectRoot;
33
- document.getElementById('historyResume').disabled = unavailable || sources.length > 0;
34
- document.getElementById('historyFork').disabled = unavailable || !controlsSource(sources);
35
- if (!busy && sources.length) notice(tr(controlsSource(sources) ? 'history.stopOriginal' : 'history.sourceControlRequired'));
36
- document.getElementById('historyRefresh').disabled = busy;
37
- document.getElementById('historyMore').hidden = !cursor;
38
- document.getElementById('historyMore').disabled = busy;
39
- }
40
- function renderList() {
41
- const container = document.getElementById('historyThreads');
42
- container.innerHTML = entries.map(entry => '<button type="button" class="history-thread" data-thread="' + esc(entry.id) + '" aria-pressed="' + (entry.id === selected?.id) + '"><strong>' + esc(entry.name || entry.preview || entry.id) + '</strong><span>' + esc(entry.id) + '</span><span>' + esc(entry.cwd || '') + '</span></button>').join('') || '<p class="dialog-help">' + esc(tr('history.empty')) + '</p>';
43
- container.querySelectorAll('[data-thread]').forEach(button => button.addEventListener('click', () => select(button.dataset.thread)));
44
- sync();
45
- }
46
- async function load(more = false) {
47
- if (busy || !root) return;
48
- const project = root, version = ++generation;
49
- busy = true; notice(tr('history.loading')); sync();
50
- try {
51
- const result = await bridge().api(route('/api/codex/threads' + (more && cursor ? '?cursor=' + encodeURIComponent(cursor) : ''), project));
52
- if (!same(project, version)) return;
53
- const incoming = Array.isArray(result.threads) ? result.threads : [];
54
- entries = [...new Map((more ? entries.concat(incoming) : incoming).map(entry => [entry.id, entry])).values()];
55
- cursor = result.nextCursor || null;
56
- notice(''); renderList();
57
- } catch (error) { if (same(project, version)) notice(error.detail || error.message); }
58
- finally { if (project === root && version === generation) { busy = false; sync(); } }
59
- }
60
- async function select(id) {
61
- if (busy) return;
62
- const project = root, version = ++generation;
63
- selected = entries.find(entry => entry.id === id) || { id };
64
- document.getElementById('historyConfirmed').checked = false;
65
- document.getElementById('historySelected').textContent = selected.name || selected.preview || id;
66
- document.getElementById('historyContent').textContent = '';
67
- busy = true; renderList(); notice(tr('history.loading'));
68
- try {
69
- const result = await bridge().api(route('/api/codex/threads/' + encodeURIComponent(id), project));
70
- if (!same(project, version)) return;
71
- selected = result.thread || selected;
72
- renderPreview();
73
- notice('');
74
- } catch (error) { if (same(project, version)) notice(error.detail || error.message); }
75
- finally { if (project === root && version === generation) { busy = false; sync(); } }
76
- }
77
- function renderPreview() {
78
- document.getElementById('historyContent').innerHTML = (selected?.turns || []).map(turn => '<section class="history-turn"><strong>' + esc(turn.status || '') + '</strong>' + (turn.items || []).map(item => {
79
- const content = Array.isArray(item.content) ? item.content.map(part => part.text || '').join('\n') : item.text || item.command || item.aggregatedOutput || '';
80
- const label = item.type === 'userMessage' ? tr('history.user') : item.type === 'agentMessage' ? tr('history.assistant') : item.type === 'commandExecution' ? tr('history.command') : item.type === 'fileChange' ? tr('history.files') : tr('history.event');
81
- return '<article><strong>' + esc(label) + '</strong><pre>' + esc(content || (item.changes ? JSON.stringify(item.changes, null, 2) : '')) + '</pre></article>';
82
- }).join('') + '</section>').join('') || '<p class="dialog-help">' + esc(tr('history.noTurns')) + '</p>';
83
- }
84
- async function reopen(fork) {
85
- if (busy || !selected || !document.getElementById('historyConfirmed').checked || root !== bridge().projectRoot) return;
86
- const sources = liveSources();
87
- if (sources.length && (!fork || !controlsSource(sources))) { sync(); return; }
88
- const project = root, threadId = selected.id, version = ++generation;
89
- const input = fork ? { confirmed: true, ...(sources.length ? { actionToken: bridge().actionToken, epoch: bridge().epoch } : {}) } : { kind: 'codex', transport: 'app-server', mode: 'resume', resume: threadId, handoffConfirmed: true };
90
- busy = true; sync(); notice(tr('history.opening'));
91
- try {
92
- const result = await bridge().api(fork ? route('/api/codex/threads/' + encodeURIComponent(threadId) + '/fork', project) : route('/api/sessions', project), {
93
- method: 'POST', body: JSON.stringify(input)
94
- });
95
- if (project !== bridge().projectRoot) return;
96
- await bridge().refreshSessions();
97
- if (!same(project, version)) return;
98
- bridge().closeDialog(dialog);
99
- if (result.session?.id) bridge().openManaged(result.session.id);
100
- } catch (error) { if (same(project, version)) notice((error.detail || error.message) + (error.code ? ' [' + error.code + ']' : '') + ' ' + tr('history.checkSessions')); }
101
- finally { if (project === root && version === generation) { busy = false; sync(); } }
23
+ export function codexHistoryScript() { return '(' + installHistory.toString() + ')(globalThis,(' + nativeTimeline.toString() + '));'; }
24
+
25
+ export function installHistory(browser = globalThis, timeline = nativeTimeline) {
26
+ const { window, document } = browser, byId = id => document.getElementById(id);
27
+ const dialog = byId('historyDialog'), bridge = () => window.hccHandoff;
28
+ const tr = (key, fallback = '') => bridge().tr(key, fallback), esc = value => bridge().esc(String(value ?? ''));
29
+ let root = '', source = 'native', entries = [], cursor = null, selected = null, ready = false, busy = false, generation = 0;
30
+ let unverifiedCount = 0;
31
+ let events = [], deliveries = [], nextAfter = null, pageStarts = [0], page = 0, runtimeAvailable = true;
32
+ const isNative = () => source === 'native';
33
+ const same = (project, version) => project === root && project === bridge().projectRoot && version === generation && !dialog.hidden;
34
+ const route = (path, project = root) => path + (path.includes('?') ? '&' : '?') + 'root=' + encodeURIComponent(project);
35
+ const entryId = entry => isNative() ? entry.peer : entry.id;
36
+ function notice(value) { byId('historyNotice').textContent = value || ''; }
37
+ function liveSources() {
38
+ return (bridge().sessions || []).filter(session => session.status === 'running' && (isNative()
39
+ ? session.type === 'native' && [session.id,session.peer_id].includes(selected?.peer)
40
+ : session.binding?.provider === 'codex' && session.binding.provider_session_id === selected?.id));
41
+ }
42
+ function controlsSource(sources) { return sources.every(session => session.id === bridge().active && bridge().canControl); }
43
+ function nativeEligible() { return ready && selected?.resumable === true && selected.status === 'closed' && selected.owned === false && Boolean(selected.owner && selected.sessionId); }
44
+ function updatedLabel(value) {
45
+ if (!value) return '';
46
+ const date = new Date(value);
47
+ return Number.isFinite(date.getTime()) ? date.toLocaleString() : String(value);
48
+ }
49
+ function sync() {
50
+ if (!dialog.hidden && !root && bridge().projectRoot) { root = bridge().projectRoot; void load(); return; }
51
+ const confirmed = byId('historyConfirmed').checked, sources = liveSources();
52
+ const unavailable = busy || !ready || !selected || !confirmed || root !== bridge().projectRoot || Boolean(window.hccAgentStart?.pending);
53
+ byId('historyProviderField').hidden = !isNative();
54
+ byId('historyHelp').textContent = tr(isNative() ? 'history.nativeHelp' : 'history.help');
55
+ byId('historyUnverified').textContent = !isNative() && unverifiedCount > 0
56
+ ? tr('history.unverified').replace('{count}', String(unverifiedCount)) : '';
57
+ byId('historyConfirmLabel').textContent = tr(isNative() ? 'history.nativeConfirm' : 'history.confirm');
58
+ byId('historyResume').disabled = unavailable || sources.length > 0 || (isNative() && !nativeEligible());
59
+ byId('historyFork').hidden = isNative(); byId('historyFork').disabled = isNative() || unavailable || !controlsSource(sources);
60
+ byId('historyResumeReason').textContent = isNative() && selected && !nativeEligible()
61
+ ? tr('history.resumeReason.' + (selected.resumeReason || 'unavailable'), selected.resumeDetail || tr('history.resumeUnavailable')) : '';
62
+ if (!busy && sources.length) notice(tr(isNative() ? 'history.nativeActive' : controlsSource(sources) ? 'history.stopOriginal' : 'history.sourceControlRequired'));
63
+ byId('historyRefresh').disabled = busy; byId('historyMore').hidden = isNative() || !cursor; byId('historyMore').disabled = busy;
64
+ byId('historyEventControls').hidden = !isNative() || !ready;
65
+ byId('historyEventsPrevious').disabled = busy || page === 0; byId('historyEventsNext').disabled = busy || nextAfter == null;
66
+ byId('historyEventsReload').disabled = busy; byId('historyReceipts').hidden = !isNative() || !ready;
67
+ }
68
+ function clearSelected() {
69
+ selected = null; ready = false; events = []; deliveries = []; nextAfter = null; pageStarts = [0]; page = 0;
70
+ byId('historyConfirmed').checked = false; byId('historyReceipts').open = false;
71
+ for (const id of ['historySelected','historyContent','historyIdentity','historyRetention','historyReceiptContent']) byId(id).textContent = '';
72
+ }
73
+ function renderList() {
74
+ const provider = byId('historyProvider').value || 'all';
75
+ const filtered = isNative() && provider !== 'all' ? entries.filter(entry => entry.provider === provider) : entries;
76
+ const container = byId('historyThreads');
77
+ container.innerHTML = filtered.map(entry => '<button type="button" class="history-thread" data-thread="' + esc(entryId(entry)) + '" aria-pressed="' + (entryId(entry) === (selected && entryId(selected))) + '"><strong>'
78
+ + esc(isNative() ? entry.peer : entry.name || entry.preview || entry.id) + '</strong><span>'
79
+ + esc(isNative() ? [entry.provider,entry.status,updatedLabel(entry.updatedAt)].filter(Boolean).join(' · ') : entry.id) + '</span><span>' + esc(entry.cwd || '') + '</span></button>').join('')
80
+ || '<p class="dialog-help">' + esc(tr(isNative() ? 'history.nativeEmpty' : 'history.empty')) + '</p>';
81
+ container.querySelectorAll('[data-thread]').forEach(button => button.addEventListener('click', () => select(button.dataset.thread))); sync();
82
+ }
83
+ async function load(more = false) {
84
+ if (busy) return;
85
+ if (!root) { notice(tr('history.projectLoading')); return; }
86
+ const project = root, version = ++generation, native = isNative();
87
+ if (!more) { clearSelected(); unverifiedCount = 0; }
88
+ busy = true; notice(tr('history.loading')); sync();
89
+ try {
90
+ const result = await bridge().api(route(native ? '/api/native/history' : '/api/codex/threads' + (more && cursor ? '?cursor=' + encodeURIComponent(cursor) : ''), project));
91
+ if (!same(project,version)) return;
92
+ const incoming = (native ? result.workers : result.threads) || [];
93
+ entries = [...new Map((more ? entries.concat(incoming) : incoming).map(entry => [entryId(entry),entry])).values()];
94
+ cursor = native ? null : result.nextCursor || null; runtimeAvailable = result.runtimeAvailable !== false;
95
+ const hidden = Number(result.unverifiedCount);
96
+ if (!native && Number.isSafeInteger(hidden) && hidden > 0) unverifiedCount += hidden;
97
+ notice(native ? [runtimeAvailable ? '' : tr('history.runtimeOffline'),result.truncated ? tr('history.listTruncated') : ''].filter(Boolean).join(' ') : ''); renderList();
98
+ } catch (error) { if (same(project,version)) notice(error.detail || error.message); }
99
+ finally { if (project === root && version === generation) { busy = false; sync(); } }
100
+ }
101
+ async function select(id, targetPage = 0) {
102
+ if (busy) return;
103
+ const project = root, version = ++generation, native = isNative();
104
+ if (!selected || entryId(selected) !== id) { clearSelected(); selected = entries.find(entry => entryId(entry) === id) || (native ? {peer:id} : {id}); }
105
+ ready = false; byId('historyConfirmed').checked = false;
106
+ byId('historySelected').textContent = selected.name || selected.preview || id; byId('historyContent').textContent = ''; byId('historyReceiptContent').textContent = '';
107
+ busy = true; renderList(); notice(tr('history.loading'));
108
+ try {
109
+ const result = await bridge().api(route(native ? '/api/native/history/' + encodeURIComponent(id) + '?after=' + (pageStarts[targetPage] || 0) : '/api/codex/threads/' + encodeURIComponent(id),project));
110
+ if (!same(project,version)) return;
111
+ selected = (native ? result.worker : result.thread) || selected; ready = true;
112
+ if (native) {
113
+ runtimeAvailable = result.runtimeAvailable !== false;
114
+ events = Array.isArray(result.events) ? result.events : []; deliveries = Array.isArray(result.deliveries) ? result.deliveries : [];
115
+ nextAfter = result.truncated === true && Number.isSafeInteger(result.nextAfter) && result.nextAfter > (pageStarts[targetPage] || 0) ? result.nextAfter : null;
116
+ page = targetPage; if (nextAfter != null) pageStarts[page + 1] = nextAfter; pageStarts.length = page + (nextAfter == null ? 1 : 2);
117
+ }
118
+ renderPreview(); notice(native && !runtimeAvailable ? tr('history.runtimeOffline') : '');
119
+ } catch (error) { if (same(project,version)) notice(error.detail || error.message); }
120
+ finally { if (project === root && version === generation) { busy = false; sync(); } }
121
+ }
122
+ function renderPreview() {
123
+ if (!ready) return;
124
+ if (isNative()) {
125
+ byId('historyIdentity').textContent = [selected.provider,selected.peer,selected.status,selected.sessionId,selected.cwd].filter(Boolean).join(' · ');
126
+ byId('historyRetention').textContent = tr('history.retention');
127
+ byId('historyEventPage').textContent = tr('history.eventPage').replace('{page}',String(page + 1)).replace('{count}',String(events.length))
128
+ + (events.length ? ' · #' + events[0].id + '–#' + events.at(-1).id : '');
129
+ byId('historyReceiptContent').textContent = JSON.stringify(deliveries,null,2);
130
+ const trace = byId('historyNativeView').value === 'trace';
131
+ byId('historyContent').innerHTML = (trace ? events.map(entry => '<details class="history-turn"><summary>#' + esc(entry.id) + ' · ' + esc((entry.payload || entry).type || '') + '</summary><pre>' + esc(JSON.stringify(entry.payload || entry,null,2)) + '</pre></details>')
132
+ : timeline(events,selected.provider).map(record => '<section class="history-turn"><strong>' + esc(record.role === 'user' ? tr('history.user') : record.role === 'assistant' ? tr('history.assistant') : record.title || record.kind)
133
+ + '</strong><small> · #' + esc(record.eventId) + (record.status ? ' · ' + esc(record.status) : '') + '</small><pre>'
134
+ + esc([record.text,record.command,record.input,record.output,record.changes?.length ? JSON.stringify(record.changes,null,2) : ''].filter(Boolean).join('\n\n')) + '</pre></section>')).join('')
135
+ || '<p class="dialog-help">' + esc(tr('history.noEvents')) + '</p>'; return;
102
136
  }
103
- function open() {
104
- root = bridge().projectRoot; generation++; entries = []; selected = null; cursor = null; busy = false;
105
- document.getElementById('historyContent').textContent = ''; document.getElementById('historySelected').textContent = '';
106
- document.getElementById('historyConfirmed').checked = false;
107
- bridge().openDialog(dialog, document.getElementById('historyRefresh')); renderList(); void load();
137
+ byId('historyContent').innerHTML = (selected?.turns || []).map(turn => '<section class="history-turn"><strong>' + esc(turn.status || '') + '</strong>' + (turn.items || []).map(item => {
138
+ const content = Array.isArray(item.content) ? item.content.map(part => part.text || '').join('\n') : item.text || item.command || item.aggregatedOutput || '';
139
+ const label = item.type === 'userMessage' ? tr('history.user') : item.type === 'agentMessage' ? tr('history.assistant') : item.type === 'commandExecution' ? tr('history.command') : item.type === 'fileChange' ? tr('history.files') : tr('history.event');
140
+ return '<article><strong>' + esc(label) + '</strong><pre>' + esc(content || (item.changes ? JSON.stringify(item.changes,null,2) : '')) + '</pre></article>';
141
+ }).join('') + '</section>').join('') || '<p class="dialog-help">' + esc(tr('history.noTurns')) + '</p>';
142
+ }
143
+ async function reopen(fork) {
144
+ if (busy || !ready || !selected || !byId('historyConfirmed').checked || root !== bridge().projectRoot) return;
145
+ const sources = liveSources();
146
+ if (isNative()) {
147
+ if (fork || !nativeEligible() || sources.length || window.hccAgentStart?.pending) { sync(); return; }
148
+ const worker = { ...selected }; bridge().closeDialog(dialog); return window.hccAgentStart.resumeNative(worker);
108
149
  }
109
- document.getElementById('historyClose').addEventListener('click', () => bridge().closeDialog(dialog));
110
- document.getElementById('historyRefresh').addEventListener('click', () => load());
111
- document.getElementById('historyMore').addEventListener('click', () => load(true));
112
- document.getElementById('historyConfirmed').addEventListener('change', sync);
113
- document.getElementById('historyResume').addEventListener('click', () => reopen(false));
114
- document.getElementById('historyFork').addEventListener('click', () => reopen(true));
115
- window.addEventListener('hcc:preferences', () => { if (!dialog.hidden) { renderList(); renderPreview(); } });
116
- window.hccHistory = { open, sync, reset() { generation++; busy = false; if (!dialog.hidden) bridge().closeDialog(dialog); } };
117
- })();`;
150
+ if (sources.length && (!fork || !controlsSource(sources))) { sync(); return; }
151
+ const project = root, threadId = selected.id, version = ++generation;
152
+ const input = fork ? { confirmed:true, ...(sources.length ? {actionToken:bridge().actionToken,epoch:bridge().epoch} : {}) }
153
+ : {kind:'codex',transport:'app-server',mode:'resume',resume:threadId,handoffConfirmed:true};
154
+ busy = true; sync(); notice(tr('history.opening'));
155
+ try {
156
+ const result = await bridge().api(fork ? route('/api/codex/threads/' + encodeURIComponent(threadId) + '/fork',project) : route('/api/sessions',project),{method:'POST',body:JSON.stringify(input)});
157
+ if (project !== bridge().projectRoot) return;
158
+ await bridge().refreshSessions(); if (!same(project,version)) return;
159
+ bridge().closeDialog(dialog); if (result.session?.id) bridge().openManaged(result.session.id);
160
+ } catch (error) { if (same(project,version)) notice((error.detail || error.message) + (error.code ? ' [' + error.code + ']' : '') + ' ' + tr('history.checkSessions')); }
161
+ finally { if (project === root && version === generation) { busy = false; sync(); } }
162
+ }
163
+ function closed() { generation++; busy = false; ready = false; byId('historyConfirmed').checked = false; }
164
+ function open(nextSource = 'native') {
165
+ root = bridge().projectRoot; generation++; source = nextSource === 'codex' ? 'codex' : 'native'; entries = []; cursor = null; unverifiedCount = 0; busy = false; clearSelected();
166
+ byId('historySource').value = source; byId('historyProvider').value = 'all'; byId('historyNativeView').value = 'conversation';
167
+ bridge().openDialog(dialog,byId('historySource')); renderList(); void load();
168
+ }
169
+ byId('historySource').addEventListener('change',() => {
170
+ generation++; busy = false; source = byId('historySource').value === 'codex' ? 'codex' : 'native'; entries = []; cursor = null; unverifiedCount = 0; clearSelected(); renderList(); void load();
171
+ });
172
+ byId('historyProvider').addEventListener('change',() => {
173
+ // Keep an in-flight list read: its response uses the current filter. A
174
+ // detail read is fenced when its selected provider is left behind.
175
+ if (selected) { generation++; busy = false; clearSelected(); notice(''); }
176
+ renderList();
177
+ });
178
+ byId('historyNativeView').addEventListener('change',renderPreview);
179
+ byId('historyEventsPrevious').addEventListener('click',() => { if (page > 0 && selected) return select(selected.peer,page - 1); });
180
+ byId('historyEventsNext').addEventListener('click',() => { if (nextAfter != null && selected) return select(selected.peer,page + 1); });
181
+ byId('historyEventsReload').addEventListener('click',() => selected && select(selected.peer,page));
182
+ byId('historyClose').addEventListener('click',() => { closed(); bridge().closeDialog(dialog); });
183
+ byId('historyRefresh').addEventListener('click',() => load()); byId('historyMore').addEventListener('click',() => load(true));
184
+ byId('historyConfirmed').addEventListener('change',sync); byId('historyResume').addEventListener('click',() => reopen(false));
185
+ byId('historyFork').addEventListener('click',() => reopen(true));
186
+ window.addEventListener('hcc:preferences',() => { if (!dialog.hidden) { renderList(); renderPreview(); } });
187
+ window.hccHistory = {open,sync,closed,reset() { closed(); if (!dialog.hidden) bridge().closeDialog(dialog); }};
118
188
  }