@logicseek/hello-cc 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/mcp.md +9 -0
  11. package/docs/mcp.zh-CN.md +23 -0
  12. package/docs/native.md +295 -0
  13. package/docs/native.zh-CN.md +253 -0
  14. package/docs/web-handoff.zh-CN.md +53 -0
  15. package/lib/cli/app.mjs +25 -6
  16. package/lib/cli/commands/doctor.mjs +17 -2
  17. package/lib/cli/commands/dsh.mjs +79 -0
  18. package/lib/cli/commands/hook.mjs +241 -90
  19. package/lib/cli/commands/lock.mjs +34 -19
  20. package/lib/cli/commands/mcp.mjs +28 -0
  21. package/lib/cli/commands/native.mjs +130 -0
  22. package/lib/cli/commands/run.mjs +66 -21
  23. package/lib/cli/commands/task.mjs +6 -3
  24. package/lib/cli-args.mjs +4 -0
  25. package/lib/coordination-state.mjs +15 -1
  26. package/lib/core/coordination/event-retention.mjs +14 -2
  27. package/lib/core/coordination/gc-plan.mjs +3 -13
  28. package/lib/core/coordination/locks.mjs +45 -0
  29. package/lib/core/coordination/results.mjs +79 -0
  30. package/lib/core/peers/bindings.mjs +9 -3
  31. package/lib/diagnostics/codex.mjs +121 -0
  32. package/lib/integrations/codex-interactions.mjs +114 -0
  33. package/lib/integrations/dsh-collaboration.mjs +170 -0
  34. package/lib/integrations/dsh-cordis.d.ts +19 -0
  35. package/lib/integrations/dsh-cordis.mjs +177 -0
  36. package/lib/integrations/dsh.bundle.yml +7 -0
  37. package/lib/integrations/dsh.mjs +287 -0
  38. package/lib/integrations/native/claude.mjs +451 -0
  39. package/lib/integrations/native/codex.mjs +242 -0
  40. package/lib/integrations/native/dsh-acp.mjs +343 -0
  41. package/lib/integrations/native/index.mjs +19 -0
  42. package/lib/integrations/native/interactions.mjs +53 -0
  43. package/lib/integrations/native/jsonrpc.mjs +185 -0
  44. package/lib/integrations/peers/identity.mjs +36 -10
  45. package/lib/integrations/peers/processes.mjs +22 -8
  46. package/lib/integrations/providers.mjs +6 -2
  47. package/lib/mcp/scope.mjs +104 -0
  48. package/lib/mcp/stdio.mjs +80 -0
  49. package/lib/mcp/tools.mjs +197 -0
  50. package/lib/process/identity.mjs +3 -1
  51. package/lib/release/release-notes.mjs +9 -0
  52. package/lib/runtime/native/client.mjs +38 -0
  53. package/lib/runtime/native/service.mjs +511 -0
  54. package/lib/runtime/native/store.mjs +121 -0
  55. package/lib/ui/help.mjs +5 -2
  56. package/lib/web/adoption-state.mjs +55 -0
  57. package/lib/web/auto-attach.mjs +18 -2
  58. package/lib/web/codex-app-server.mjs +557 -0
  59. package/lib/web/codex-sessions.mjs +287 -0
  60. package/lib/web/control-lease.mjs +204 -0
  61. package/lib/web/http-routes.mjs +197 -15
  62. package/lib/web/http.mjs +5 -4
  63. package/lib/web/native-sessions.mjs +286 -0
  64. package/lib/web/project-contexts.mjs +2 -1
  65. package/lib/web/pty-sessions.mjs +60 -33
  66. package/lib/web/runtime-main.mjs +98 -6
  67. package/lib/web/session-serialize.mjs +0 -0
  68. package/lib/web/task-results.mjs +76 -0
  69. package/lib/web/tmux-sessions.mjs +92 -11
  70. package/lib/web/tmux-stream.mjs +2 -2
  71. package/lib/web/ui-codex.mjs +550 -0
  72. package/lib/web/ui-command-palette.mjs +119 -0
  73. package/lib/web/ui-handoff.mjs +102 -0
  74. package/lib/web/ui-history.mjs +118 -0
  75. package/lib/web/ui-i18n.mjs +841 -0
  76. package/lib/web/ui-interactions.mjs +78 -0
  77. package/lib/web/ui-login.mjs +101 -0
  78. package/lib/web/ui-native.mjs +165 -0
  79. package/lib/web/ui-preferences.mjs +278 -0
  80. package/lib/web/ui-review.mjs +132 -0
  81. package/lib/web/ui-template.mjs +1106 -576
  82. package/lib/web/ui-terminal-find.mjs +98 -0
  83. package/lib/web/ui-workbench.mjs +94 -0
  84. package/lib/web/ui-workspace.mjs +222 -0
  85. package/package.json +11 -3
  86. package/scripts/check-free-identifiers.mjs +67 -8
  87. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  88. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  89. package/scripts/dsh-installed-acceptance.mjs +247 -0
  90. package/scripts/dsh-official-acceptance.mjs +180 -0
  91. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  92. package/scripts/github-release.mjs +3 -1
  93. package/scripts/native-live-acceptance.mjs +369 -0
  94. package/scripts/regression.mjs +243 -89
  95. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  96. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  97. package/scripts/web-native-model-acceptance.mjs +138 -0
@@ -0,0 +1,78 @@
1
+ // Shared form controls for Web-owned and independent native executors.
2
+ export function interactionPanelScript() {
3
+ return String.raw`(() => {
4
+ if (window.hccInteractions) return;
5
+ const drafts = new Map();
6
+ let focus = null;
7
+ function remember(container) {
8
+ container.querySelectorAll('[data-interaction-field]').forEach(control => {
9
+ if (!control.dataset.interactionField) return;
10
+ drafts.set(control.dataset.interactionField, { value: control.value, checked: control.checked });
11
+ if (control === document.activeElement) focus = { key: control.dataset.interactionField, start: control.selectionStart, end: control.selectionEnd };
12
+ });
13
+ while (drafts.size > 256) drafts.delete(drafts.keys().next().value);
14
+ }
15
+ function restore(container) {
16
+ const live = new Set();
17
+ container.querySelectorAll('[data-interaction-field]').forEach(control => {
18
+ live.add(control.dataset.interactionField);
19
+ const saved = drafts.get(control.dataset.interactionField);
20
+ if (saved) { control.value = saved.value; if (control.type === 'checkbox') control.checked = saved.checked; }
21
+ if (focus && focus.key === control.dataset.interactionField) { control.focus?.({preventScroll:true}); if (typeof focus.start === 'number') control.setSelectionRange?.(focus.start, focus.end); }
22
+ });
23
+ for (const key of drafts.keys()) if (!live.has(key)) drafts.delete(key);
24
+ if (focus && !live.has(focus.key)) focus = null;
25
+ }
26
+ function form(request, key, esc, text) {
27
+ const field = suffix => ' data-interaction-field="' + esc(key + ':' + suffix) + '"';
28
+ if (request.kind === 'userInput') return (request.params?.questions || []).map((q, i) => {
29
+ const label = '<label>' + esc(q.header || '') + '<p>' + esc(q.question || '') + '</p>';
30
+ const free = '<input data-answer="' + i + '" type="' + (q.isSecret ? 'password' : 'text') + '" maxlength="8192" autocomplete="off"' + field('answer:' + i) + '>';
31
+ if (!q.options?.length) return label + free + '</label>';
32
+ return label + '<select data-question="' + i + '"' + field('question:' + i) + '><option value="">' + esc(text('Choose an answer', '请选择答案')) + '</option>'
33
+ + q.options.map((o, n) => '<option value="' + n + '">' + esc(o.label + (o.description ? ' — ' + o.description : '')) + '</option>').join('')
34
+ + (q.isOther ? '<option value="other">' + esc(text('Other (enter below)', '其他(在下方输入)')) + '</option>' : '') + '</select>'
35
+ + (q.isOther ? free : '') + '</label>';
36
+ }).join('');
37
+ if (request.kind !== 'permissions') return '';
38
+ const p = request.params?.permissions || {}, fs = p.fileSystem || {};
39
+ const choice = (name, index, label) => '<label><input type="checkbox" data-permission="' + name + '" data-index="' + index + '"' + field(name + ':' + index) + '> ' + esc(label) + '</label>';
40
+ return '<p>' + esc(text('Select the requested permissions to grant. Unchecked permissions remain denied.', '勾选要授予的请求权限,未勾选的权限不授予。')) + '</p>'
41
+ + (p.network?.enabled === true ? choice('network', 0, text('Network access', '网络访问')) : '')
42
+ + ['read', 'write'].map(type => (fs[type] || []).map((path, i) => (fs.entries || []).some(entry => entry.access === type && entry.path?.type === 'path' && entry.path.path === path) ? '' : choice(type, i, text(type === 'read' ? 'Read: ' : 'Write: ', type === 'read' ? '读取:' : '写入:') + path)).join('')).join('')
43
+ + (fs.entries || []).map((entry, i) => entry.access === 'deny' ? '<p>' + esc(text('Retained denial: ', '保留禁止规则:') + JSON.stringify(entry.path)) + '</p>'
44
+ : choice('entries', i, (entry.access === 'write' ? text('Write: ', '写入:') : text('Read: ', '读取:')) + JSON.stringify(entry.path))).join('')
45
+ + '<label>' + esc(text('Permission duration', '权限有效期')) + '<select data-scope' + field('scope') + '><option value="turn">' + esc(text('This turn', '仅本轮')) + '</option><option value="session">' + esc(text('This session', '本会话')) + '</option></select></label>';
46
+ }
47
+ function payload(request, container, decision) {
48
+ if (decision !== 'accept') return { decision };
49
+ if (request.kind === 'userInput') {
50
+ const answers = Object.create(null);
51
+ for (const [i, q] of (request.params?.questions || []).entries()) {
52
+ const select = container.querySelector('[data-question="' + i + '"]');
53
+ const value = select && select.value !== 'other' ? q.options?.[Number(select.value)]?.label : container.querySelector('[data-answer="' + i + '"]')?.value;
54
+ if ((select && select.value === '') || !value?.trim()) throw new Error('Please answer every question / 请回答每个问题');
55
+ answers[q.id] = { answers: [value] };
56
+ }
57
+ return { answers };
58
+ }
59
+ if (request.kind !== 'permissions') return { decision };
60
+ const wanted = request.params.permissions, permissions = {}, fs = {};
61
+ container.querySelectorAll('[data-permission]').forEach(control => {
62
+ if (!control.checked) return;
63
+ const field = control.dataset.permission;
64
+ if (field === 'network') permissions.network = { enabled: true };
65
+ else { fs[field] ||= []; fs[field].push(wanted.fileSystem[field][Number(control.dataset.index)]); }
66
+ });
67
+ if (Object.keys(fs).length) {
68
+ const denies = (wanted.fileSystem.entries || []).filter(entry => entry.access === 'deny');
69
+ if (denies.length) fs.entries = [...(fs.entries || []), ...denies];
70
+ if (wanted.fileSystem.globScanMaxDepth != null) fs.globScanMaxDepth = wanted.fileSystem.globScanMaxDepth;
71
+ permissions.fileSystem = fs;
72
+ }
73
+ return { decision, permissions, scope: container.querySelector('[data-scope]').value };
74
+ }
75
+ function forget(key) { for (const field of drafts.keys()) if (field.startsWith(key + ':')) drafts.delete(field); }
76
+ window.hccInteractions = { form, payload, remember, restore, forget };
77
+ })();`;
78
+ }
@@ -0,0 +1,101 @@
1
+ import { uiPreferencesCss, uiPreferencesScript, preferenceControlsHtml } from './ui-preferences.mjs';
2
+
3
+ // Authentication stays server-side. Only appearance preferences are shared with
4
+ // the console; the access token is sent once and never added to browser storage.
5
+ export function renderWebLoginPage({ inlineScriptNonce }) {
6
+ return `<!doctype html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11
+ <title>hello-cc — sign in</title>
12
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23087b65'/%3E%3Cpath d='m14 20 14 12-14 12m20 0h16' fill='none' stroke='white' stroke-width='5'/%3E%3C/svg%3E">
13
+ <script${inlineScriptNonce}>${uiPreferencesScript()}</script>
14
+ <style>
15
+ ${uiPreferencesCss}
16
+ * { box-sizing: border-box; }
17
+ body { margin: 0; min-height: 100vh; min-height: 100dvh; padding: 24px; display: grid; place-items: center; background: var(--bg); color: var(--text); font-family: var(--sans); }
18
+ .login-card { width: min(100%, 460px); padding: 32px; border: 1px solid var(--border); border-radius: 16px; background: var(--panel); box-shadow: 0 20px 60px var(--shadow, rgba(0, 0, 0, .15)); }
19
+ .login-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 24px; font-weight: 700; font-size: 18px; letter-spacing: -.03em; }
20
+ .login-mark { color: var(--accent); font-family: var(--mono); }
21
+ h1 { margin: 0; font-size: 24px; line-height: 1.4; letter-spacing: -.025em; }
22
+ .login-help { color: var(--muted); font-size: 14px; line-height: 1.65; margin: 10px 0 24px; }
23
+ label { display: grid; gap: 7px; color: var(--muted); font-size: 13px; }
24
+ input, select, button { font: inherit; }
25
+ input, select { width: 100%; min-width: 0; min-height: 40px; padding: 9px 10px; border-radius: 7px; border: 1px solid var(--border); background: var(--input-bg, var(--bg)); color: var(--text); }
26
+ input::placeholder { color: var(--muted); opacity: .8; }
27
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
28
+ .login-submit { width: 100%; min-height: 42px; margin-top: 16px; border-radius: 7px; border: 1px solid var(--accent); background: var(--accent); color: var(--on-accent, #06211b); font-weight: 600; cursor: pointer; }
29
+ .login-submit:hover { filter: brightness(1.08); }
30
+ .login-submit:disabled { cursor: wait; opacity: .65; }
31
+ .login-error { color: var(--danger); font-size: 13px; line-height: 1.5; margin-top: 12px; min-height: 20px; }
32
+ .login-preferences { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; border-top: 1px solid var(--border); padding-top: 20px; margin-top: 20px; }
33
+ .login-preferences select { font-size: 12px; padding-left: 7px; }
34
+ @media (max-width: 480px) { body { padding: 16px; } .login-card { padding: 24px; } h1 { font-size: 22px; } .login-preferences { grid-template-columns: 1fr; } }
35
+ @media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <main class="login-card">
40
+ <div class="login-brand"><span class="login-mark" aria-hidden="true">&gt;_</span><span>hello-cc</span></div>
41
+ <h1 data-i18n="loginTitle">Sign in to hello-cc</h1>
42
+ <p class="login-help" data-i18n="loginHelp">Enter the access token shown when you start the web console.</p>
43
+ <form id="loginForm">
44
+ <label for="tok"><span data-i18n="accessToken">Access token</span>
45
+ <input id="tok" name="token" type="password" autocomplete="off" data-i18n-placeholder="tokenPlaceholder" placeholder="Paste your access token" required autofocus aria-describedby="err">
46
+ </label>
47
+ <button class="login-submit" id="go" type="submit" data-i18n="signIn">Sign in</button>
48
+ <div class="login-error" id="err" role="alert" aria-live="polite"></div>
49
+ </form>
50
+ <div class="login-preferences" aria-label="Settings" data-i18n-aria="settings">${preferenceControlsHtml({ terminal: false })}</div>
51
+ </main>
52
+ <script${inlineScriptNonce}>
53
+ const ui = window.hccUi;
54
+ const input = document.getElementById('tok');
55
+ const err = document.getElementById('err');
56
+ const button = document.getElementById('go');
57
+ const form = document.getElementById('loginForm');
58
+ let pending = false;
59
+ let errorKey = '';
60
+ function renderLoginText() {
61
+ ui.translate();
62
+ document.title = 'hello-cc — ' + ui.tr('signIn');
63
+ button.textContent = ui.tr(pending ? 'signingIn' : 'signIn');
64
+ err.textContent = errorKey ? ui.tr(errorKey) : '';
65
+ }
66
+ ui.bindControls();
67
+ window.addEventListener('hcc:preferences', renderLoginText);
68
+ renderLoginText();
69
+ form.addEventListener('submit', async (event) => {
70
+ event.preventDefault();
71
+ if (pending) return;
72
+ const token = input.value.trim();
73
+ if (!token) { errorKey = 'tokenRequired'; renderLoginText(); input.focus(); return; }
74
+ pending = true;
75
+ errorKey = '';
76
+ button.disabled = true;
77
+ input.setAttribute('aria-invalid', 'false');
78
+ form.setAttribute('aria-busy', 'true');
79
+ renderLoginText();
80
+ try {
81
+ const res = await fetch('/login', {
82
+ method: 'POST',
83
+ headers: { 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({ token })
85
+ });
86
+ if (res.ok) { location.href = '/'; return; }
87
+ errorKey = res.status === 401 || res.status === 403 ? 'invalidToken' : 'networkError';
88
+ input.setAttribute('aria-invalid', errorKey === 'invalidToken' ? 'true' : 'false');
89
+ } catch {
90
+ errorKey = 'networkError';
91
+ } finally {
92
+ pending = false;
93
+ button.disabled = false;
94
+ form.setAttribute('aria-busy', 'false');
95
+ renderLoginText();
96
+ }
97
+ });
98
+ </script>
99
+ </body>
100
+ </html>`;
101
+ }
@@ -0,0 +1,165 @@
1
+ import { interactionPanelScript } from './ui-interactions.mjs';
2
+
3
+ export function nativePanelHtml() {
4
+ return `<style>
5
+ #nativePanel { flex:1; min-width:0; min-height:0; overflow:hidden; flex-direction:column; }
6
+ #nativePanel[hidden] { display:none; }
7
+ #nativePanel:not([hidden]) { display:flex; }
8
+ .native-header,.native-composer { flex:none; padding:12px 16px; background:var(--panel); }
9
+ .native-header { border-bottom:1px solid var(--border); }
10
+ .native-header p { margin:6px 0 0; color:var(--muted); font-size:var(--small-font); overflow-wrap:anywhere; }
11
+ .native-scroll { flex:1; min-height:0; overflow:auto; padding:0 16px 12px; }
12
+ .native-event { padding:10px 0; border-bottom:1px solid var(--border); }
13
+ .native-event pre { white-space:pre-wrap; overflow-wrap:anywhere; margin:6px 0; line-height:1.6; }
14
+ .native-receipts { display:grid; gap:8px; margin:10px 0; }
15
+ .native-receipt { border:1px solid var(--border); border-radius:7px; padding:9px; font-size:var(--small-font); overflow-wrap:anywhere; }
16
+ .native-composer { border-top:1px solid var(--border); max-height:45%; overflow:auto; }
17
+ #nativeDraft { width:100%; min-height:70px; max-height:170px; resize:vertical; padding:10px; font:inherit; background:var(--input-bg); color:var(--text); border:1px solid var(--border); border-radius:7px; }
18
+ .native-controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:8px; }
19
+ .native-controls label { display:flex; gap:6px; align-items:center; font-size:var(--small-font); }
20
+ #nativeNotice { color:var(--muted); font-size:var(--small-font); overflow-wrap:anywhere; }
21
+ </style><section id="nativePanel" hidden aria-label="Native worker">
22
+ <header class="native-header"><strong id="nativeStatus" role="status"></strong><p id="nativeIdentity"></p><p data-i18n="native.approvalPolicy">The current controller can answer interactive requests here.</p></header>
23
+ <div class="native-scroll"><div id="nativeApprovals"></div><h4 data-i18n="native.receipts">Durable delivery receipts</h4><div id="nativeReceipts" class="native-receipts"></div><h4 data-i18n="native.output">Executor output</h4><div id="nativeEvents"></div></div>
24
+ <footer class="native-composer"><label for="nativeDraft" data-i18n="native.draft">Message draft saved in this browser</label><textarea id="nativeDraft" rows="3"></textarea>
25
+ <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>
26
+ <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>
27
+ <p id="nativeNotice" role="status" aria-live="polite"></p>
28
+ </footer>
29
+ </section>`;
30
+ }
31
+
32
+ export function nativePanelScript() {
33
+ return interactionPanelScript() + String.raw`(() => {
34
+ const bridge = () => window.hccHandoff, panel = document.getElementById('nativePanel'), draft = document.getElementById('nativeDraft');
35
+ const tr = (key, fallback = '') => bridge().tr(key, fallback), esc = value => bridge().esc(String(value ?? ''));
36
+ const events = new Map(), inflight = new Set(), responding = new Set();
37
+ let key = '', state = null, pending = null, observed = 0;
38
+ const storageKey = () => 'hcc.nativeDraft:' + JSON.stringify([bridge().projectRoot,bridge().active]) + (bridge().draftScope ? ':pane:' + JSON.stringify(bridge().draftScope) : '');
39
+ function save() { if (key) window.hccUi.safeSet(key,JSON.stringify({text:draft.value,pending})); }
40
+ 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}; }
41
+ const matches = target => target.root === bridge().projectRoot && target.id === bridge().active && (!target.generation || target.generation === state?.generation) && (!target.owner || target.owner === state?.owner);
42
+ const route = (target,name) => '/api/sessions/' + encodeURIComponent(target.id) + '/native/' + name + '?root=' + encodeURIComponent(target.root);
43
+ function notice(message) { document.getElementById('nativeNotice').textContent = message || ''; }
44
+ function sync() {
45
+ const next = storageKey();
46
+ if (next !== key) {
47
+ key = next; state = null; pending = null; events.clear(); observed++; draft.value = '';
48
+ for (const id of ['nativeStatus','nativeIdentity','nativeReceipts','nativeEvents','nativeNotice','nativeApprovals']) document.getElementById(id).textContent = '';
49
+ document.getElementById('nativeCloseConfirmed').checked = false;
50
+ try { const saved = JSON.parse(window.hccUi.safeGet(key) || '{}'); draft.value = saved.text || ''; pending = saved.pending || null; } catch (_) {}
51
+ }
52
+ const writable = bridge().session?.type === 'native' && bridge().canControl && state?.connected && !state.closing;
53
+ const sending = inflight.has(key);
54
+ document.getElementById('nativeSend').disabled = !writable || sending || !!pending || !draft.value.trim() || state?.quarantined || state?.capabilities?.send === false;
55
+ document.getElementById('nativeInterrupt').disabled = !writable || sending || !state?.turnId || state?.capabilities?.interrupt === false;
56
+ document.getElementById('nativeClose').disabled = !writable || sending || !document.getElementById('nativeCloseConfirmed').checked;
57
+ document.getElementById('nativeReviewPending').hidden = !pending;
58
+ document.getElementById('nativeReviewPending').disabled = sending;
59
+ document.getElementById('nativeRead').disabled = bridge().session?.type !== 'native';
60
+ document.getElementById('nativeApprovals').querySelectorAll('button[data-response]').forEach(button => {
61
+ button.disabled = !writable || responding.has(button.dataset.requestKey) || (button.dataset.decision === 'accept' && button.dataset.truncated === 'true');
62
+ });
63
+ if (pending) notice(tr('native.uncertain'));
64
+ }
65
+ function acknowledge(target, submissionId) {
66
+ let stored = {}; try { stored = JSON.parse(window.hccUi.safeGet(target.key) || '{}'); } catch (_) {}
67
+ if (stored.pending?.id !== submissionId) return false;
68
+ if (stored.text === stored.pending.text) stored.text = '';
69
+ stored.pending = null; window.hccUi.safeSet(target.key,JSON.stringify(stored));
70
+ if (matches(target)) { draft.value = stored.text || ''; pending = null; notice(tr('native.queued')); }
71
+ return true;
72
+ }
73
+ function render(value) {
74
+ sync();
75
+ if (!value || bridge().session?.type !== 'native' || (value.peer !== bridge().session?.peer_id && value.peer !== bridge().active) || (value.root && value.root !== bridge().projectRoot)) return;
76
+ if (Number.isSafeInteger(state?.generation) && Number.isSafeInteger(value.generation) && value.generation < state.generation) return;
77
+ const replaced = state && (state.generation !== value.generation || state.owner !== value.owner);
78
+ if (replaced) { events.clear(); document.getElementById('nativeCloseConfirmed').checked = false; }
79
+ state = value; observed++;
80
+ for (const entry of value.events || []) events.set(entry.id,entry);
81
+ while (events.size > 100) events.delete(events.keys().next().value);
82
+ document.getElementById('nativeStatus').textContent = tr('native.sameExecutor') + ' · ' + (state.provider || '') + ' · ' + tr('native.state.' + state.status, state.status || '');
83
+ document.getElementById('nativeIdentity').textContent = [state.peer,state.sessionId,state.turnId].filter(Boolean).join(' · ');
84
+ document.getElementById('nativeReceipts').innerHTML = (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>';
85
+ document.getElementById('nativeEvents').innerHTML = [...events.values()].sort((a,b)=>a.id-b.id).map(entry => {
86
+ const event = entry.payload || entry;
87
+ 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 || '';
88
+ const label = ['output','message','delta'].includes(event.type) ? tr('native.output') : event.type === 'error' ? tr('native.error') : tr('native.event');
89
+ return text ? '<article class="native-event"><strong>' + esc(label) + '</strong><pre>' + esc(text) + '</pre></article>' : '';
90
+ }).join('') || '<p>' + esc(tr('native.noOutput')) + '</p>';
91
+ const approvals = state.pendingApprovals || [], target = subject(), container = document.getElementById('nativeApprovals');
92
+ const requestKey = request => JSON.stringify([target.root, target.id, state.executorId, request.requestId]);
93
+ const text = (en, zh) => ['zh','zh-CN'].includes(window.hccUi.language) ? zh : en;
94
+ window.hccInteractions.remember(container);
95
+ container.innerHTML = approvals.map((request, index) => '<article class="codex-card codex-approval"><h3>'
96
+ + esc(request.kind === 'userInput' ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
97
+ + '</h3><pre>' + esc(JSON.stringify(request.params, null, 2)) + '</pre>'
98
+ + (request.truncated ? '<p>' + esc(text('Truncated; cancel or decline only.', '参数已截断,只能取消或拒绝。')) + '</p>' : '')
99
+ + '<div class="hcc-interaction-form">' + window.hccInteractions.form(request, requestKey(request), esc, text) + '</div><div class="native-controls">'
100
+ + '<button data-response="' + index + '" data-decision="accept" data-truncated="' + !!request.truncated + '" data-request-key="' + esc(requestKey(request)) + '">' + esc(request.kind === 'userInput' ? text('Send answers', '提交答复') : request.kind === 'permissions' ? text('Grant selected permissions', '授予所选权限') : text('Approve once', '批准本次')) + '</button>'
101
+ + '<button data-response="' + index + '" data-decision="' + (request.kind === 'userInput' ? 'cancel' : 'decline') + '" data-request-key="' + esc(requestKey(request)) + '">' + esc(text('Decline / cancel', '拒绝 / 取消')) + '</button></div></article>').join('');
102
+ window.hccInteractions.restore(container);
103
+ container.querySelectorAll('button[data-response]').forEach(button => button.addEventListener('click', async () => {
104
+ if (!matches(target) || !bridge().canControl || responding.has(button.dataset.requestKey)) return;
105
+ const request = approvals[Number(button.dataset.response)], key = button.dataset.requestKey;
106
+ responding.add(key); sync();
107
+ try {
108
+ const payload = window.hccInteractions.payload(request, ['permissions','userInput'].includes(request.kind) ? button.closest('article') : null, button.dataset.decision);
109
+ const result = await bridge().api(route(target, 'respond'), {method:'POST', body:JSON.stringify({
110
+ executorId:request.executorId, sessionId:request.sessionId, turnId:request.turnId, requestId:request.requestId,
111
+ ...payload, action_token:bridge().actionToken, epoch:bridge().epoch })});
112
+ window.hccInteractions.forget(key);
113
+ if (matches(target)) { if (result.state) render(result.state); else void read(); }
114
+ } catch(error) { if(matches(target)) notice(error.detail || error.message); }
115
+ finally { responding.delete(key); sync(); }
116
+ }));
117
+ if (pending && (state.deliveries || []).some(delivery => delivery.submission_id === pending.id && delivery.message_id)) acknowledge(subject(),pending.id);
118
+ sync();
119
+ }
120
+ async function read() {
121
+ if (bridge().session?.type !== 'native') return;
122
+ const target = subject(), version = observed;
123
+ try {
124
+ const result = await bridge().api(route(target,'state'));
125
+ if (matches(target) && version === observed) render(result.state || result);
126
+ } catch (error) { if (matches(target)) notice(error.detail || error.message); }
127
+ }
128
+ draft.addEventListener('input',()=>{ save(); sync(); });
129
+ document.getElementById('nativeSend').addEventListener('click',async()=>{
130
+ sync(); if (document.getElementById('nativeSend').disabled) return;
131
+ const target = subject(), text = draft.value, id = 's_' + crypto.randomUUID();
132
+ pending = {id,text,generation:target.generation}; save(); inflight.add(target.key); sync(); notice(tr('native.sending'));
133
+ try {
134
+ const result = await bridge().api(route(target,'send'),{method:'POST',body:JSON.stringify({text,submissionId:id,action_token:target.token,epoch:target.epoch})});
135
+ const receipt = result.receipt || result;
136
+ if (receipt.submission_id !== id || !Number.isSafeInteger(receipt.message_id)) throw new Error(tr('native.invalidReceipt'));
137
+ acknowledge(target,id);
138
+ if (matches(target)) { if (result.state) render(result.state); else void read(); }
139
+ } catch(error) { if (matches(target)) notice((error.detail || error.message) + ' ' + tr('native.uncertain')); }
140
+ finally { inflight.delete(target.key); sync(); }
141
+ });
142
+ document.getElementById('nativeInterrupt').addEventListener('click',async()=>{
143
+ sync(); if (document.getElementById('nativeInterrupt').disabled) return;
144
+ const target = subject(); inflight.add(target.key); sync();
145
+ try { await bridge().api(route(target,'interrupt'),{method:'POST',body:JSON.stringify({turnId:state.turnId || state.activeDelivery?.turn_id,action_token:target.token,epoch:target.epoch})}); if(matches(target)){notice(tr('native.interruptRequested'));void read();} }
146
+ catch(error){if(matches(target))notice(error.detail || error.message);}
147
+ finally{inflight.delete(target.key);sync();}
148
+ });
149
+ document.getElementById('nativeCloseConfirmed').addEventListener('change',sync);
150
+ document.getElementById('nativeClose').addEventListener('click',async()=>{
151
+ sync(); if(document.getElementById('nativeClose').disabled)return;
152
+ const target=subject();inflight.add(target.key);sync();
153
+ try{await bridge().api(route(target,'close'),{method:'POST',body:JSON.stringify({action_token:target.token,epoch:target.epoch})});if(matches(target)){notice(tr('native.closed'));await bridge().refreshSessions();}}
154
+ catch(error){if(matches(target))notice(error.detail || error.message);}
155
+ finally{inflight.delete(target.key);sync();}
156
+ });
157
+ document.getElementById('nativeRead').addEventListener('click',read);
158
+ document.getElementById('nativeReviewPending').addEventListener('click',()=>{
159
+ if (!pending || inflight.has(key) || !confirm(tr('native.reviewConfirm'))) return;
160
+ pending=null;save();notice(tr('native.retained'));sync();
161
+ });
162
+ window.addEventListener('hcc:preferences',()=>{if(state&&!panel.hidden)render(state);});
163
+ window.hccNative={render,sync,read};
164
+ })();`;
165
+ }
@@ -0,0 +1,278 @@
1
+ import { UI_TRANSLATIONS } from './ui-i18n.mjs';
2
+
3
+ // Shared by the login page and the console so preferences take effect before paint.
4
+ export const uiPreferencesCss = `
5
+ :root {
6
+ color-scheme: dark;
7
+ --bg: #101214;
8
+ --panel: #181b1f;
9
+ --panel-2: #20242a;
10
+ --border: #303640;
11
+ --text: #eef2f6;
12
+ --muted: #a3adba;
13
+ --accent: #40c4aa;
14
+ --on-accent: #06211b;
15
+ --warn: #f2bb4f;
16
+ --danger: #ff6b6b;
17
+ --ok: #75d17c;
18
+ --input-bg: #0d0f12;
19
+ --terminal-bg: #0b0d10;
20
+ --card-bg: #111418;
21
+ --hover-bg: #29323c;
22
+ --primary-bg: #1b5f54;
23
+ --primary-border: #2b9c86;
24
+ --danger-bg: #5d252a;
25
+ --danger-border: #aa444c;
26
+ --selection-bg: #23465a;
27
+ --overlay: rgba(0, 0, 0, .64);
28
+ --control-height: 36px;
29
+ --space: 12px;
30
+ --small-font: 12px;
31
+ --body-font: 14px;
32
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
33
+ --sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
34
+ }
35
+ :root[data-theme="light"] {
36
+ color-scheme: light;
37
+ --bg: #eef2f6;
38
+ --panel: #f8fafc;
39
+ --panel-2: #e9eef4;
40
+ --border: #cbd4df;
41
+ --text: #17212b;
42
+ --muted: #526273;
43
+ --accent: #087b65;
44
+ --on-accent: #ffffff;
45
+ --warn: #986013;
46
+ --danger: #b5303d;
47
+ --ok: #23743b;
48
+ --input-bg: #ffffff;
49
+ --terminal-bg: #ffffff;
50
+ --card-bg: #ffffff;
51
+ --hover-bg: #e1e9f2;
52
+ --primary-bg: #d1f2e8;
53
+ --primary-border: #249079;
54
+ --danger-bg: #ffe7e9;
55
+ --danger-border: #cc6670;
56
+ --selection-bg: #c8e8f8;
57
+ --overlay: rgba(16, 24, 32, .42);
58
+ }
59
+ :root[data-density="compact"] {
60
+ --control-height: 30px;
61
+ --space: 8px;
62
+ --small-font: 11px;
63
+ --body-font: 13px;
64
+ }
65
+ `;
66
+
67
+ export const TERMINAL_THEMES = Object.freeze({
68
+ dark: Object.freeze({
69
+ background: '#0b0d10', foreground: '#eef2f6', cursor: '#7dd3fc', cursorAccent: '#0b0d10',
70
+ selectionBackground: '#23465a', selectionForeground: '#ffffff',
71
+ black: '#1c2026', red: '#ff6b6b', green: '#75d17c', yellow: '#f2bb4f',
72
+ blue: '#7baaff', magenta: '#c99bff', cyan: '#40c4cc', white: '#d6dde6',
73
+ brightBlack: '#727f91', brightRed: '#ff9696', brightGreen: '#a1e8a7', brightYellow: '#ffe08a',
74
+ brightBlue: '#aecaff', brightMagenta: '#dfbfff', brightCyan: '#82e4eb', brightWhite: '#ffffff',
75
+ }),
76
+ light: Object.freeze({
77
+ background: '#ffffff', foreground: '#17212b', cursor: '#086b99', cursorAccent: '#ffffff',
78
+ selectionBackground: '#c8e8f8', selectionForeground: '#17212b',
79
+ black: '#17212b', red: '#b5303d', green: '#23743b', yellow: '#986013',
80
+ blue: '#225dac', magenta: '#8144a8', cyan: '#087b83', white: '#69788a',
81
+ brightBlack: '#526273', brightRed: '#ca3d49', brightGreen: '#2b8243', brightYellow: '#a57213',
82
+ brightBlue: '#286ac2', brightMagenta: '#9451bc', brightCyan: '#09868d', brightWhite: '#8996a6',
83
+ }),
84
+ });
85
+
86
+ function scriptJson(value) {
87
+ return JSON.stringify(value)
88
+ .replace(/</g, '\\u003c')
89
+ .replace(/>/g, '\\u003e')
90
+ .replace(/&/g, '\\u0026')
91
+ .replace(/\u2028/g, '\\u2028')
92
+ .replace(/\u2029/g, '\\u2029');
93
+ }
94
+
95
+ export function uiPreferencesScript() {
96
+ return `(() => {
97
+ const dictionaries = ${scriptJson(UI_TRANSLATIONS)};
98
+ const fontSizes = [12, 13, 14, 15, 16, 18, 20];
99
+ const keys = {
100
+ language: 'hcc.lang', theme: 'hcc.theme', density: 'hcc.density',
101
+ terminalTheme: 'hcc.terminalTheme', fontSize: 'hcc.fontSize'
102
+ };
103
+ const defaults = {
104
+ language: 'system', theme: 'system', density: 'comfortable',
105
+ terminalTheme: 'system', fontSize: 14
106
+ };
107
+ const choices = {
108
+ language: ['system', 'en', 'zh'], theme: ['system', 'dark', 'light'],
109
+ density: ['comfortable', 'compact'], terminalTheme: ['system', 'dark', 'light']
110
+ };
111
+ const ids = {
112
+ language: 'langSelect', theme: 'themeSelect', density: 'densitySelect',
113
+ terminalTheme: 'terminalThemeSelect', fontSize: 'fontSizeSelect'
114
+ };
115
+ const bound = new WeakSet();
116
+ const memoryStorage = new Map();
117
+ function safeGet(key) {
118
+ if (memoryStorage.has(key)) return memoryStorage.get(key);
119
+ try { return window.localStorage.getItem(key); } catch (_) { return null; }
120
+ }
121
+ function safeSet(key, value) {
122
+ const text = String(value);
123
+ memoryStorage.set(key, text);
124
+ try { window.localStorage.setItem(key, text); return true; } catch (_) { return false; }
125
+ }
126
+ function valid(key, value) {
127
+ if (key === 'fontSize') {
128
+ const number = typeof value === 'number' ? value : Number(value);
129
+ return fontSizes.includes(number) ? number : undefined;
130
+ }
131
+ return choices[key] && choices[key].includes(value) ? value : undefined;
132
+ }
133
+ let preferences = Object.freeze(Object.fromEntries(Object.keys(defaults).map((key) => {
134
+ const value = valid(key, safeGet(keys[key]));
135
+ return [key, value === undefined ? defaults[key] : value];
136
+ })));
137
+ let colorMedia = null;
138
+ try {
139
+ if (typeof window.matchMedia === 'function') colorMedia = window.matchMedia('(prefers-color-scheme: dark)');
140
+ } catch (_) {}
141
+ function language() {
142
+ return preferences.language === 'system'
143
+ ? ((navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en')
144
+ : preferences.language;
145
+ }
146
+ function theme() {
147
+ return preferences.theme === 'system' ? (colorMedia ? (colorMedia.matches ? 'dark' : 'light') : 'dark') : preferences.theme;
148
+ }
149
+ function tr(key, fallback = '') {
150
+ return dictionaries[language()]?.[key] || dictionaries.en?.[key] || fallback || key;
151
+ }
152
+ function nodes(root, selector) {
153
+ const result = Array.from(root.querySelectorAll(selector));
154
+ if (root.matches && root.matches(selector)) result.unshift(root);
155
+ return result;
156
+ }
157
+ function translate(root = document) {
158
+ nodes(root, '[data-i18n]').forEach((element) => {
159
+ element.textContent = tr(element.getAttribute('data-i18n'), element.textContent);
160
+ });
161
+ [
162
+ ['data-i18n-placeholder', 'placeholder'], ['data-i18n-title', 'title'],
163
+ ['data-i18n-aria', 'aria-label'], ['data-i18n-aria-label', 'aria-label']
164
+ ].forEach(([source, target]) => {
165
+ nodes(root, '[' + source + ']').forEach((element) => {
166
+ element.setAttribute(target, tr(element.getAttribute(source), element.getAttribute(target) || ''));
167
+ });
168
+ });
169
+ }
170
+ function syncControls(root = document) {
171
+ Object.entries(ids).forEach(([key, id]) => {
172
+ nodes(root, '[id="' + id + '"]').forEach((element) => { element.value = String(preferences[key]); });
173
+ });
174
+ }
175
+ function apply() {
176
+ document.documentElement.lang = language() === 'zh' ? 'zh-CN' : 'en';
177
+ document.documentElement.dataset.theme = theme();
178
+ document.documentElement.dataset.density = preferences.density;
179
+ syncControls();
180
+ translate();
181
+ }
182
+ function notify(changed) {
183
+ window.dispatchEvent(new CustomEvent('hcc:preferences', {
184
+ detail: { preferences, language: language(), theme: theme(), changed }
185
+ }));
186
+ }
187
+ function update(patch) {
188
+ if (!patch || typeof patch !== 'object') return preferences;
189
+ const next = { ...preferences };
190
+ const changed = [];
191
+ Object.keys(defaults).forEach((key) => {
192
+ if (!Object.prototype.hasOwnProperty.call(patch, key)) return;
193
+ const value = valid(key, patch[key]);
194
+ if (value === undefined || value === next[key]) return;
195
+ next[key] = value;
196
+ changed.push(key);
197
+ safeSet(keys[key], value);
198
+ });
199
+ preferences = Object.freeze(next);
200
+ apply();
201
+ if (changed.length) notify(changed);
202
+ return preferences;
203
+ }
204
+ function bindControls(root = document) {
205
+ Object.entries(ids).forEach(([key, id]) => {
206
+ nodes(root, '[id="' + id + '"]').forEach((element) => {
207
+ if (bound.has(element)) return;
208
+ bound.add(element);
209
+ element.addEventListener('change', () => update({ [key]: element.value }));
210
+ });
211
+ });
212
+ syncControls(root);
213
+ translate(root);
214
+ }
215
+ window.hccUi = {
216
+ get preferences() { return preferences; },
217
+ get language() { return language(); },
218
+ get theme() { return theme(); },
219
+ safeGet, safeSet, tr, translate, update, bindControls
220
+ };
221
+ apply();
222
+ const followSystemTheme = () => {
223
+ if (preferences.theme !== 'system') return;
224
+ apply();
225
+ notify(['resolvedTheme']);
226
+ };
227
+ if (colorMedia?.addEventListener) colorMedia.addEventListener('change', followSystemTheme);
228
+ else if (colorMedia?.addListener) colorMedia.addListener(followSystemTheme);
229
+ window.addEventListener('languagechange', () => {
230
+ if (preferences.language !== 'system') return;
231
+ apply();
232
+ notify(['resolvedLanguage']);
233
+ });
234
+ })();`;
235
+ }
236
+
237
+ export function preferenceControlsHtml({ terminal = false } = {}) {
238
+ const common = `
239
+ <label class="preference-field" for="langSelect">
240
+ <span data-i18n="language">Language</span>
241
+ <select id="langSelect">
242
+ <option value="system" data-i18n="setting.system">Follow system</option>
243
+ <option value="en">English</option>
244
+ <option value="zh">中文</option>
245
+ </select>
246
+ </label>
247
+ <label class="preference-field" for="themeSelect">
248
+ <span data-i18n="theme">Appearance</span>
249
+ <select id="themeSelect">
250
+ <option value="system" data-i18n="setting.system">Follow system</option>
251
+ <option value="dark" data-i18n="theme.dark">Dark</option>
252
+ <option value="light" data-i18n="theme.light">Light</option>
253
+ </select>
254
+ </label>
255
+ <label class="preference-field" for="densitySelect">
256
+ <span data-i18n="density">Density</span>
257
+ <select id="densitySelect">
258
+ <option value="comfortable" data-i18n="density.comfortable">Comfortable</option>
259
+ <option value="compact" data-i18n="density.compact">Compact</option>
260
+ </select>
261
+ </label>`;
262
+ if (!terminal) return common;
263
+ return common + `
264
+ <label class="preference-field" for="terminalThemeSelect">
265
+ <span data-i18n="terminalTheme">Terminal appearance</span>
266
+ <select id="terminalThemeSelect">
267
+ <option value="system" data-i18n="terminalTheme.follow">Follow appearance</option>
268
+ <option value="dark" data-i18n="theme.dark">Dark</option>
269
+ <option value="light" data-i18n="theme.light">Light</option>
270
+ </select>
271
+ </label>
272
+ <label class="preference-field" for="fontSizeSelect">
273
+ <span data-i18n="fontSize">Terminal font size</span>
274
+ <select id="fontSizeSelect">
275
+ ${[12, 13, 14, 15, 16, 18, 20].map((size) => `<option value="${size}">${size} px</option>`).join('\n ')}
276
+ </select>
277
+ </label>`;
278
+ }