@logicseek/hello-cc 1.0.2 → 1.1.0-rc.1

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 (79) hide show
  1. package/CHANGELOG.md +111 -23
  2. package/README.md +10 -1
  3. package/README.zh-CN.md +8 -1
  4. package/docs/commands.md +28 -4
  5. package/docs/commands.zh-CN.md +20 -4
  6. package/docs/dsh.md +2 -2
  7. package/docs/dsh.zh-CN.md +3 -3
  8. package/docs/native.md +84 -10
  9. package/docs/native.zh-CN.md +66 -5
  10. package/docs/web-handoff.zh-CN.md +11 -3
  11. package/lib/cli/app.mjs +42 -1
  12. package/lib/cli/commands/coordination.mjs +7 -2
  13. package/lib/cli/commands/dsh.mjs +1 -1
  14. package/lib/cli/commands/gc.mjs +9 -4
  15. package/lib/cli/commands/hook.mjs +6 -1
  16. package/lib/cli/commands/install.mjs +48 -6
  17. package/lib/cli/commands/lifecycle.mjs +14 -3
  18. package/lib/cli/commands/native.mjs +16 -9
  19. package/lib/cli/commands/query.mjs +15 -10
  20. package/lib/cli/commands/run.mjs +7 -6
  21. package/lib/core/coordination/results.mjs +27 -10
  22. package/lib/db/connection.mjs +135 -10
  23. package/lib/db/schema.mjs +59 -0
  24. package/lib/discover.mjs +4 -4
  25. package/lib/guidance.mjs +3 -2
  26. package/lib/integrations/codex-interactions.mjs +13 -11
  27. package/lib/integrations/dsh-collaboration.mjs +6 -12
  28. package/lib/integrations/dsh-cordis.mjs +17 -13
  29. package/lib/integrations/dsh.mjs +12 -9
  30. package/lib/integrations/hooks.mjs +132 -40
  31. package/lib/integrations/mcp-elicitation.mjs +116 -0
  32. package/lib/integrations/mcp-url-elicitation.mjs +25 -0
  33. package/lib/integrations/native/interactions.mjs +2 -1
  34. package/lib/integrations/peers/identity.mjs +42 -7
  35. package/lib/process/identity.mjs +15 -14
  36. package/lib/runtime/buffer-evidence.mjs +2 -1
  37. package/lib/runtime/native/service.mjs +11 -5
  38. package/lib/runtime/native/store.mjs +70 -18
  39. package/lib/runtime/paths.mjs +17 -2
  40. package/lib/runtime/private-file.mjs +76 -0
  41. package/lib/runtime/private-state.mjs +546 -0
  42. package/lib/runtime/project-context.mjs +2 -1
  43. package/lib/runtime/project-path.mjs +148 -12
  44. package/lib/runtime/project-trust.mjs +79 -0
  45. package/lib/runtime/projects.mjs +6 -3
  46. package/lib/runtime/state-migration.mjs +697 -0
  47. package/lib/runtime/state.mjs +22 -11
  48. package/lib/shared/automatic-result-key.mjs +6 -0
  49. package/lib/shared/json-file.mjs +55 -6
  50. package/lib/ui/help.mjs +6 -4
  51. package/lib/web/codex-app-server.mjs +26 -5
  52. package/lib/web/codex-sessions.mjs +5 -3
  53. package/lib/web/cookie-auth.mjs +1 -1
  54. package/lib/web/external-sessions.mjs +54 -5
  55. package/lib/web/http-routes.mjs +13 -9
  56. package/lib/web/http.mjs +20 -2
  57. package/lib/web/native-sessions.mjs +14 -7
  58. package/lib/web/peer-actions.mjs +27 -27
  59. package/lib/web/project-contexts.mjs +2 -3
  60. package/lib/web/runtime-main.mjs +22 -5
  61. package/lib/web/runtime.mjs +18 -4
  62. package/lib/web/session-serialize.mjs +1 -1
  63. package/lib/web/startup.mjs +115 -22
  64. package/lib/web/task-results.mjs +20 -17
  65. package/lib/web/terminal-assets.mjs +18 -0
  66. package/lib/web/tls.mjs +84 -33
  67. package/lib/web/tmux-sessions.mjs +2 -2
  68. package/lib/web/tmux-stream.mjs +3 -4
  69. package/lib/web/ui-codex.mjs +161 -32
  70. package/lib/web/ui-interactions.mjs +137 -4
  71. package/lib/web/ui-native.mjs +31 -13
  72. package/lib/web/ui-template.mjs +10 -3
  73. package/package.json +1 -1
  74. package/scripts/mcp-form-live-fixture.mjs +81 -0
  75. package/scripts/native-installed-acceptance.mjs +413 -0
  76. package/scripts/regression.mjs +46 -29
  77. package/scripts/web-mcp-form-acceptance.mjs +119 -0
  78. package/scripts/web-mcp-form-live-acceptance.mjs +368 -0
  79. package/scripts/web-native-interaction-acceptance.mjs +77 -10
@@ -1,8 +1,12 @@
1
+ import { createMcpFormValidator } from '../integrations/mcp-elicitation.mjs';
2
+ import { createMcpUrlValidator } from '../integrations/mcp-url-elicitation.mjs';
3
+
1
4
  // Shared form controls for Web-owned and independent native executors.
2
5
  export function interactionPanelScript() {
3
6
  return String.raw`(() => {
4
7
  if (window.hccInteractions) return;
5
- const drafts = new Map();
8
+ const mcpForms = (${createMcpFormValidator.toString()})(), mcpUrls = (${createMcpUrlValidator.toString()})();
9
+ const drafts = new Map(), expanded = new Map();
6
10
  let focus = null;
7
11
  function remember(container) {
8
12
  container.querySelectorAll('[data-interaction-field]').forEach(control => {
@@ -10,7 +14,9 @@ export function interactionPanelScript() {
10
14
  drafts.set(control.dataset.interactionField, { value: control.value, checked: control.checked });
11
15
  if (control === document.activeElement) focus = { key: control.dataset.interactionField, start: control.selectionStart, end: control.selectionEnd };
12
16
  });
13
- while (drafts.size > 256) drafts.delete(drafts.keys().next().value);
17
+ container.querySelectorAll('[data-interaction-details]').forEach(detail => expanded.set(detail.dataset.interactionDetails, detail.open));
18
+ while (expanded.size > 256) expanded.delete(expanded.keys().next().value);
19
+ while (drafts.size > 8192) drafts.delete(drafts.keys().next().value);
14
20
  }
15
21
  function restore(container) {
16
22
  const live = new Set();
@@ -22,8 +28,116 @@ export function interactionPanelScript() {
22
28
  });
23
29
  for (const key of drafts.keys()) if (!live.has(key)) drafts.delete(key);
24
30
  if (focus && !live.has(focus.key)) focus = null;
31
+ const detailKeys = new Set();
32
+ container.querySelectorAll('[data-interaction-details]').forEach(detail => {
33
+ const key = detail.dataset.interactionDetails; detailKeys.add(key);
34
+ if (expanded.has(key)) detail.open = expanded.get(key);
35
+ });
36
+ for (const key of expanded.keys()) if (!detailKeys.has(key)) expanded.delete(key);
37
+ }
38
+ function requiresInput(request) {
39
+ return request.kind === 'userInput' || (request.kind === 'mcp' && request.params?.mode === 'form' && Object.keys(request.params.requestedSchema?.properties || {}).length > 0);
40
+ }
41
+ function approvalBlocked(request) {
42
+ if (request.kind === 'mcp') { try { (isUrl(request) ? mcpUrls : mcpForms).describe(request.params); } catch (_) { return true; } }
43
+ const tool = request.params?.toolCall;
44
+ return Boolean(request.truncated || (request.method === 'session/request_permission' &&
45
+ (!tool || tool.contextPending || tool.contextTruncated || !Object.hasOwn(tool, 'rawInput') ||
46
+ !request.params.options?.some(option => option.kind === 'allow_once'))));
47
+ }
48
+ function isUrl(request) { return request.kind === 'mcp' && request.params?.mode === 'url'; }
49
+ function acceptLabel(request, text) {
50
+ return isUrl(request) ? text('Open authorization page', '打开授权页面') : requiresInput(request) ? text('Send answers', '提交答复')
51
+ : request.kind === 'permissions' ? text('Grant selected permissions', '授予所选权限') : text('Approve once', '批准本次');
52
+ }
53
+ function previewParams(request) {
54
+ if (!isUrl(request)) return request.params || {};
55
+ let destination;
56
+ try { destination = mcpUrls.describe(request.params).origin; } catch (_) {}
57
+ return { mode: 'url', serverName: request.params?.serverName, destination };
58
+ }
59
+ function prepareUrl(request, decision, text) {
60
+ if (!isUrl(request) || decision !== 'accept') return null;
61
+ if (approvalBlocked(request)) throw new Error(text('This authorization request cannot be opened.', '当前授权请求不可打开。'));
62
+ const { href } = mcpUrls.describe(request.params);
63
+ // Reserve a tab within the explicit click; navigate only after the owning
64
+ // executor accepts the exact request. A stale lease leaves no auth page.
65
+ const page = window.open('about:blank', '_blank');
66
+ if (!page) throw new Error(text('Allow pop-ups for this site, then try again. The request has not been accepted.', '请允许本站弹出新标签页后重试;请求尚未接受。'));
67
+ let navigated = false;
68
+ try {
69
+ page.opener = null;
70
+ page.document.write('<meta name="referrer" content="no-referrer"><title>Authorization / 授权</title><p>Opening authorization page / 正在打开授权页面…</p>');
71
+ page.document.close();
72
+ } catch (error) { page.close(); throw error; }
73
+ return {
74
+ complete() {
75
+ if (page.closed) throw new Error(text('The request was accepted but the authorization tab was closed. Check the task result or request authorization again.', '请求已接受,但授权标签页已关闭。请查看任务结果或重新发起授权。'));
76
+ page.location.replace(href); navigated = true;
77
+ return text('Authorization page opened. Complete the steps there, then return to follow this task. Opening the page does not confirm authorization.', '授权页面已打开。请在该页面完成操作,再返回查看任务结果;打开页面不表示授权成功。');
78
+ },
79
+ cancel() { if (!navigated && !page.closed) page.close(); }
80
+ };
81
+ }
82
+ function preview(request, key, esc, text) {
83
+ const params = request.params || {}, tool = params.toolCall || {}, input = params.input || tool.rawInput || {};
84
+ const rows = [], row = (label, value) => { if (typeof value === 'string' && value) rows.push('<dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd>'); };
85
+ const name = params.tool || tool.title;
86
+ row(text('Tool', '工具'), name);
87
+ row(text('MCP server', 'MCP 服务'), params.serverName);
88
+ row(text('Command', '命令'), params.command || input.command);
89
+ row(text('File', '文件'), input.file_path || input.path || params.path || params.grantRoot);
90
+ row(text('Working directory', '工作目录'), params.cwd || input.cwd);
91
+ row(text('Reason', '原因'), params.reason || params.message);
92
+ const content = typeof input.content === 'string' ? input.content : null;
93
+ const previewLimit = 1024;
94
+ const contentPreview = content === null ? '' : '<p>' + esc(text('Content preview', '内容预览')) + '</p><pre class="hcc-interaction-content">' + esc(content.slice(0, previewLimit)) + '</pre>'
95
+ + (content.length > previewLimit ? '<p>' + esc(text('Preview shortened; expand the full operation details to review all content.', '预览已缩短,展开完整操作详情可核对全部内容。')) + '</p>' : '');
96
+ const known = rows.length || ['permissions', 'userInput'].includes(request.kind);
97
+ const missingContext = request.method === 'session/request_permission' && approvalBlocked(request);
98
+ return (rows.length ? '<dl class="hcc-interaction-summary">' + rows.join('') + '</dl>' : '') + contentPreview
99
+ + (missingContext ? '<p role="alert">' + esc(text('The operation input or one-time approval option is unavailable. Only rejection is available.', '操作输入或单次批准选项不可用,目前只能拒绝。')) + '</p>' : '')
100
+ + (request.kind === 'approval' && !missingContext ? '<p>' + esc(text('Approval applies only to this operation. Later requests require another decision.', '仅批准本次操作,后续请求仍需单独确认。')) + '</p>' : '')
101
+ + '<details class="hcc-interaction-details" data-interaction-details="' + esc(key) + '"' + (known ? '' : ' open') + '><summary>' + esc(text('Full operation details', '完整操作详情')) + '</summary><pre>' + esc(JSON.stringify(previewParams(request), null, 2)) + '</pre></details>';
102
+ }
103
+ function mcpForm(request, key, esc, text) {
104
+ let fields;
105
+ try { ({ fields } = mcpForms.describe(request.params)); }
106
+ catch (_) { return '<p role="alert">' + esc(text('This MCP request mode or schema is unsupported. Decline or cancel this request.', '暂不支持此 MCP 请求模式或表单结构,请拒绝或取消请求。')) + '</p>'; }
107
+ if (!fields.length) return '';
108
+ const attr = suffix => ' data-interaction-field="' + esc(key + ':mcp:' + suffix) + '"';
109
+ const select = (i, choices, value) => '<select data-mcp-field="' + i + '"' + attr(i) + '><option value="">' + esc(text('Choose a value', '请选择')) + '</option>'
110
+ + choices.map((choice, n) => '<option value="' + n + '"' + (value === choice.value ? ' selected' : '') + '>' + esc(choice.label) + '</option>').join('') + '</select>';
111
+ return '<p>' + esc(text('Required fields are marked *. Optional fields are sent only when selected. Responses are not saved in browser storage.', '带 * 的为必填项;可选项仅在勾选填写后发送。答复不会保存到浏览器存储。')) + '</p>'
112
+ + fields.map((field, i) => {
113
+ const label = esc(field.title || field.key) + (field.required ? ' *' : '');
114
+ const optional = field.required ? '' : '<label><input type="checkbox" data-mcp-use="' + i + '"' + attr(i + ':use') + '> ' + esc(text('Provide this optional field', '填写此可选项')) + '</label>';
115
+ let control;
116
+ if (field.kind === 'enum') control = select(i, field.options, field.default);
117
+ else if (field.type === 'boolean') control = select(i, [{ value: true, label: text('Yes', '是') }, { value: false, label: text('No', '否') }], field.default);
118
+ else if (field.kind === 'multi') control = field.options.map((option, n) => '<label><input type="checkbox" data-mcp-field="' + i + '" data-mcp-option="' + n + '"' + attr(i + ':' + n)
119
+ + (field.default?.includes(option.value) ? ' checked' : '') + '> ' + esc(option.label) + '</label>').join('');
120
+ else control = '<input data-mcp-field="' + i + '"' + attr(i) + ' type="' + (['number', 'integer'].includes(field.type) ? 'number' : 'text') + '"'
121
+ + (field.type === 'integer' ? ' step="1"' : field.type === 'number' ? ' step="any"' : ' maxlength="8192"')
122
+ + (field.minimum != null ? ' min="' + esc(field.minimum) + '"' : '') + (field.maximum != null ? ' max="' + esc(field.maximum) + '"' : '')
123
+ + ' value="' + esc(field.default ?? '') + '" autocomplete="off">';
124
+ const limits = [field.format, field.minLength != null ? text('Min length: ', '最短长度:') + field.minLength : '', field.maxLength != null ? text('Max length: ', '最长长度:') + field.maxLength : '',
125
+ field.minimum != null ? text('Minimum: ', '最小值:') + field.minimum : '', field.maximum != null ? text('Maximum: ', '最大值:') + field.maximum : '',
126
+ field.minItems != null ? text('Min selections: ', '至少选择:') + field.minItems : '', field.maxItems != null ? text('Max selections: ', '最多选择:') + field.maxItems : ''].filter(Boolean).join(' · ');
127
+ return '<fieldset class="hcc-mcp-field"><legend>' + label + '</legend>' + (field.description ? '<p>' + esc(field.description) + '</p>' : '') + optional
128
+ + (field.kind === 'multi' ? control : '<label><span class="sr-only">' + label + '</span>' + control + '</label>') + (limits ? '<small>' + esc(limits) + '</small>' : '') + '</fieldset>';
129
+ }).join('');
25
130
  }
26
131
  function form(request, key, esc, text) {
132
+ if (isUrl(request)) {
133
+ let destination;
134
+ try { destination = mcpUrls.describe(request.params); }
135
+ catch (_) { return '<p role="alert">' + esc(text('This authorization URL or identity is invalid. Decline or cancel this request.', '授权链接或请求标识无效,请拒绝或取消请求。')) + '</p>'; }
136
+ return '<p class="hcc-url-destination">' + esc(text('Authorization destination: ', '授权目标:')) + '<strong>' + esc(destination.origin) + '</strong></p>'
137
+ + '<p>' + esc(text('Open this page only if you trust the MCP server and destination. Finish authorization there, then return to this task. Opening acknowledges the request; it does not confirm authorization.', '确认信任此 MCP 服务和目标后打开页面,在该页面完成授权,再返回本任务查看结果。打开仅表示接受请求,不表示授权成功。')) + '</p>'
138
+ + (destination.loopback ? '<p>' + esc(text('This address belongs to the executor computer. A browser on another device may not reach it; open this task on the executor computer to authorize.', '此地址位于执行器所在电脑。其他设备的浏览器可能无法访问,请在执行器所在电脑打开本任务完成授权。')) + '</p>' : '');
139
+ }
140
+ if (request.kind === 'mcp') return mcpForm(request, key, esc, text);
27
141
  const field = suffix => ' data-interaction-field="' + esc(key + ':' + suffix) + '"';
28
142
  if (request.kind === 'userInput') return (request.params?.questions || []).map((q, i) => {
29
143
  const label = '<label>' + esc(q.header || '') + '<p>' + esc(q.question || '') + '</p>';
@@ -46,6 +160,25 @@ export function interactionPanelScript() {
46
160
  }
47
161
  function payload(request, container, decision) {
48
162
  if (decision !== 'accept') return { decision };
163
+ if (isUrl(request)) { mcpUrls.describe(request.params); return { decision }; }
164
+ if (request.kind === 'mcp') {
165
+ const { fields } = mcpForms.describe(request.params), content = Object.create(null);
166
+ for (const [i, field] of fields.entries()) {
167
+ if (!field.required && !container?.querySelector('[data-mcp-use="' + i + '"]')?.checked) continue;
168
+ const controls = container?.querySelectorAll('[data-mcp-field="' + i + '"]');
169
+ if (!controls?.length) throw new Error('MCP form is unavailable / MCP 表单不可用');
170
+ const control = controls[0];
171
+ if (field.kind === 'multi') content[field.key] = Array.from(controls).filter(item => item.checked).map(item => field.options[Number(item.dataset.mcpOption)]?.value);
172
+ else if (field.kind === 'enum' || field.type === 'boolean') {
173
+ if (!/^\d+$/.test(control.value)) throw new Error('Choose a value / 请选择:' + field.key);
174
+ content[field.key] = field.kind === 'enum' ? field.options[Number(control.value)]?.value : control.value === '0' ? true : control.value === '1' ? false : undefined;
175
+ } else if (['number', 'integer'].includes(field.type)) {
176
+ if (!control.value.trim()) throw new Error('Enter a number / 请填写数值:' + field.key);
177
+ content[field.key] = Number(control.value);
178
+ } else content[field.key] = control.value;
179
+ }
180
+ return { decision, content: mcpForms.validate(request.params, content) };
181
+ }
49
182
  if (request.kind === 'userInput') {
50
183
  const answers = Object.create(null);
51
184
  for (const [i, q] of (request.params?.questions || []).entries()) {
@@ -72,7 +205,7 @@ export function interactionPanelScript() {
72
205
  }
73
206
  return { decision, permissions, scope: container.querySelector('[data-scope]').value };
74
207
  }
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 };
208
+ function forget(key) { for (const field of drafts.keys()) if (field.startsWith(key + ':')) drafts.delete(field); expanded.delete(key); }
209
+ window.hccInteractions = { preview, previewParams, approvalBlocked, requiresInput, acceptLabel, prepareUrl, form, payload, remember, restore, forget };
77
210
  })();`;
78
211
  }
@@ -15,6 +15,9 @@ export function nativePanelHtml() {
15
15
  .native-receipt { border:1px solid var(--border); border-radius:7px; padding:9px; font-size:var(--small-font); overflow-wrap:anywhere; }
16
16
  .native-composer { border-top:1px solid var(--border); max-height:45%; overflow:auto; }
17
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
+ .hcc-interaction-content,.hcc-interaction-details pre { white-space:pre-wrap; overflow-wrap:anywhere; max-height:180px; overflow:auto; margin:6px 0; }
19
+ .hcc-interaction-details { margin:8px 0; }
20
+ .hcc-interaction-details summary { cursor:pointer; color:var(--muted); }
18
21
  .native-controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:8px; }
19
22
  .native-controls label { display:flex; gap:6px; align-items:center; font-size:var(--small-font); }
20
23
  #nativeNotice { color:var(--muted); font-size:var(--small-font); overflow-wrap:anywhere; }
@@ -67,7 +70,12 @@ export function nativePanelScript() {
67
70
  if (stored.pending?.id !== submissionId) return false;
68
71
  if (stored.text === stored.pending.text) stored.text = '';
69
72
  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')); }
73
+ // A-B-A navigation can return before its fresh executor snapshot arrives.
74
+ // Reconcile the matching local submission even while state is still null.
75
+ if (key === target.key && pending?.id === submissionId) {
76
+ draft.value = stored.text || ''; pending = null;
77
+ notice(matches(target) ? tr('native.queued') : '');
78
+ }
71
79
  return true;
72
80
  }
73
81
  function render(value) {
@@ -76,7 +84,8 @@ export function nativePanelScript() {
76
84
  if (Number.isSafeInteger(state?.generation) && Number.isSafeInteger(value.generation) && value.generation < state.generation) return;
77
85
  const replaced = state && (state.generation !== value.generation || state.owner !== value.owner);
78
86
  if (replaced) { events.clear(); document.getElementById('nativeCloseConfirmed').checked = false; }
79
- state = value; observed++;
87
+ if (state !== value) observed++;
88
+ state = value;
80
89
  for (const entry of value.events || []) events.set(entry.id,entry);
81
90
  while (events.size > 100) events.delete(events.keys().next().value);
82
91
  document.getElementById('nativeStatus').textContent = tr('native.sameExecutor') + ' · ' + (state.provider || '') + ' · ' + tr('native.state.' + state.status, state.status || '');
@@ -93,25 +102,34 @@ export function nativePanelScript() {
93
102
  const text = (en, zh) => ['zh','zh-CN'].includes(window.hccUi.language) ? zh : en;
94
103
  window.hccInteractions.remember(container);
95
104
  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>'
105
+ + esc(window.hccInteractions.requiresInput(request) ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
106
+ + '</h3>' + window.hccInteractions.preview(request, requestKey(request), esc, text)
98
107
  + (request.truncated ? '<p>' + esc(text('Truncated; cancel or decline only.', '参数已截断,只能取消或拒绝。')) + '</p>' : '')
99
108
  + '<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('');
109
+ + '<button data-response="' + index + '" data-decision="accept" data-truncated="' + window.hccInteractions.approvalBlocked(request) + '" data-request-key="' + esc(requestKey(request)) + '">' + esc(window.hccInteractions.acceptLabel(request, text)) + '</button>'
110
+ + '<button data-response="' + index + '" data-decision="' + (request.kind === 'userInput' ? 'cancel' : 'decline') + '" data-request-key="' + esc(requestKey(request)) + '">' + esc(request.kind === 'mcp' ? text('Decline', '拒绝') : text('Decline / cancel', '拒绝 / 取消')) + '</button>'
111
+ + (request.kind === 'mcp' ? '<button data-response="' + index + '" data-decision="cancel" data-request-key="' + esc(requestKey(request)) + '">' + esc(text('Cancel request', '取消请求')) + '</button>' : '') + '</div></article>').join('');
102
112
  window.hccInteractions.restore(container);
103
113
  container.querySelectorAll('button[data-response]').forEach(button => button.addEventListener('click', async () => {
104
114
  if (!matches(target) || !bridge().canControl || responding.has(button.dataset.requestKey)) return;
105
115
  const request = approvals[Number(button.dataset.response)], key = button.dataset.requestKey;
106
- responding.add(key); sync();
116
+ const version = observed;
117
+ notice(''); responding.add(key); sync();
118
+ let authorization;
107
119
  try {
108
- const payload = window.hccInteractions.payload(request, ['permissions','userInput'].includes(request.kind) ? button.closest('article') : null, button.dataset.decision);
120
+ const payload = window.hccInteractions.payload(request, ['permissions','userInput','mcp'].includes(request.kind) ? button.closest('article') : null, button.dataset.decision);
121
+ authorization = window.hccInteractions.prepareUrl(request, button.dataset.decision, text);
109
122
  const result = await bridge().api(route(target, 'respond'), {method:'POST', body:JSON.stringify({
110
123
  executorId:request.executorId, sessionId:request.sessionId, turnId:request.turnId, requestId:request.requestId,
111
124
  ...payload, action_token:bridge().actionToken, epoch:bridge().epoch })});
112
125
  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); }
126
+ const opened = authorization?.complete();
127
+ if (opened && matches(target)) notice(opened);
128
+ if (matches(target) && version === observed) { if (result.state) render(result.state); else void read(); }
129
+ } catch(error) {
130
+ authorization?.cancel();
131
+ if (matches(target) && (state?.pendingApprovals || []).some(current => current.requestId === request.requestId && current.executorId === request.executorId)) notice(error.detail || error.message);
132
+ }
115
133
  finally { responding.delete(key); sync(); }
116
134
  }));
117
135
  if (pending && (state.deliveries || []).some(delivery => delivery.submission_id === pending.id && delivery.message_id)) acknowledge(subject(),pending.id);
@@ -128,15 +146,15 @@ export function nativePanelScript() {
128
146
  draft.addEventListener('input',()=>{ save(); sync(); });
129
147
  document.getElementById('nativeSend').addEventListener('click',async()=>{
130
148
  sync(); if (document.getElementById('nativeSend').disabled) return;
131
- const target = subject(), text = draft.value, id = 's_' + crypto.randomUUID();
149
+ const target = subject(), text = draft.value, id = 's_' + crypto.randomUUID(), version = observed;
132
150
  pending = {id,text,generation:target.generation}; save(); inflight.add(target.key); sync(); notice(tr('native.sending'));
133
151
  try {
134
152
  const result = await bridge().api(route(target,'send'),{method:'POST',body:JSON.stringify({text,submissionId:id,action_token:target.token,epoch:target.epoch})});
135
153
  const receipt = result.receipt || result;
136
154
  if (receipt.submission_id !== id || !Number.isSafeInteger(receipt.message_id)) throw new Error(tr('native.invalidReceipt'));
137
155
  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')); }
156
+ if (matches(target) && version === observed) { if (result.state) render(result.state); else void read(); }
157
+ } catch(error) { if (matches(target) && pending?.id === id) notice((error.detail || error.message) + ' ' + tr('native.uncertain')); }
140
158
  finally { inflight.delete(target.key); sync(); }
141
159
  });
142
160
  document.getElementById('nativeInterrupt').addEventListener('click',async()=>{
@@ -11,6 +11,8 @@ import { commandPaletteCss, commandPaletteHtml, commandPaletteScript } from './u
11
11
  import { terminalFindCss, terminalFindHtml, terminalFindScript } from './ui-terminal-find.mjs';
12
12
  import { workspaceCss, workspaceBarHtml, workspaceSecondaryHtml, workspaceScript } from './ui-workspace.mjs';
13
13
 
14
+ const SHELL_APOSTROPHE_ESCAPE = JSON.stringify(`'"'"'`);
15
+
14
16
  function nonceAttribute(nonce) {
15
17
  const value = String(nonce || '');
16
18
  if (!/^[A-Za-z0-9_-]{16,}$/.test(value)) {
@@ -604,7 +606,8 @@ export function webIndexHtml({ nonce, pane = false } = {}) {
604
606
  @media (max-width: 480px) {
605
607
  .global-header { padding: 0 10px; }
606
608
  .global-header > strong { font-size: 15px; }
607
- .header-controls { gap: 6px; }
609
+ .header-controls { gap: 6px; flex: 1; min-width: 0; overflow-x: auto; scrollbar-width: thin; }
610
+ .header-controls > * { flex-shrink: 0; }
608
611
  .global-header #connState { max-width: 88px; overflow: hidden; text-overflow: ellipsis; }
609
612
  .toolbar { flex-wrap: wrap; padding: 8px 10px; }
610
613
  .toolbar .title { flex-basis: 100%; }
@@ -2130,11 +2133,15 @@ export function webIndexHtml({ nonce, pane = false } = {}) {
2130
2133
  };
2131
2134
  }
2132
2135
 
2136
+ function terminalShellQuote(value) {
2137
+ return "'" + String(value).split("'").join(${SHELL_APOSTROPHE_ESCAPE}) + "'";
2138
+ }
2139
+
2133
2140
  function terminalCommandForAction(action, info) {
2134
2141
  const session = info.session || {};
2135
- const peerId = info.peerId;
2142
+ const peerId = terminalShellQuote(info.peerId);
2136
2143
  const lines = {
2137
- register: \`hcc register --peer \${peerId} --kind \${session.kind || 'other'} --role \${session.role || 'peer'}\`,
2144
+ register: \`hcc register --peer \${peerId} --kind \${terminalShellQuote(session.kind || 'other')} --role \${terminalShellQuote(session.role || 'peer')}\`,
2138
2145
  inbox: \`hcc msg inbox --peer \${peerId}\`,
2139
2146
  'task-next': \`hcc task next --peer \${peerId}\`,
2140
2147
  state: \`hcc state --peer \${peerId}\`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logicseek/hello-cc",
3
- "version": "1.0.2",
3
+ "version": "1.1.0-rc.1",
4
4
  "description": "Local control plane for coordinating Claude Code, Codex, and other coding CLI sessions.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,81 @@
1
+ // Disposable stdio MCP server for installed-model form acceptance.
2
+ // Only synthetic inputs from this harness are recorded; no provider credentials.
3
+ import fs from 'node:fs';
4
+ import readline from 'node:readline';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ const args = process.argv.slice(2);
8
+ if (!args.includes('--stdio')) {
9
+ console.log('Acceptance fixture only: node scripts/mcp-form-live-fixture.mjs --stdio --log FILE');
10
+ process.exit(0);
11
+ }
12
+ const logIndex = args.indexOf('--log');
13
+ if (logIndex < 0 || !args[logIndex + 1]) throw new Error('--log FILE is required');
14
+ const logPath = args[logIndex + 1];
15
+ const pending = new Map();
16
+ const write = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\n');
17
+ const record = value => fs.appendFileSync(logPath, JSON.stringify({ at: new Date().toISOString(), pid: process.pid, ...value }) + '\n', { mode: 0o600 });
18
+ const reply = (id, result) => write({ id, result });
19
+ const schema = {
20
+ type: 'object',
21
+ properties: {
22
+ project: { type: 'string', title: 'Project name / 项目名', minLength: 3, maxLength: 64 },
23
+ count: { type: 'integer', title: 'Run count / 次数', minimum: 1, maximum: 4 },
24
+ enabled: { type: 'boolean', title: 'Enable extra operation / 开启额外操作' },
25
+ region: { type: 'string', title: 'Region / 地区', enum: ['eu', 'us'], enumNames: ['Europe', 'USA'] },
26
+ features: { type: 'array', title: 'Features / 功能', minItems: 1, maxItems: 2,
27
+ items: { type: 'string', enum: ['logs', 'tests'] } },
28
+ note: { type: 'string', title: 'Optional note / 可选备注', default: 'suggested-but-not-automatically-submitted' }
29
+ },
30
+ required: ['project', 'count', 'enabled', 'region', 'features']
31
+ };
32
+ const lines = readline.createInterface({ input: process.stdin });
33
+ lines.on('line', line => {
34
+ let message;
35
+ try { message = JSON.parse(line); } catch { write({ id: null, error: { code: -32700, message: 'Invalid JSON' } }); return; }
36
+ const { id, method, params = {} } = message;
37
+ if (!method) {
38
+ const entry = pending.get(id);
39
+ if (!entry) return;
40
+ pending.delete(id); clearTimeout(entry.timer);
41
+ record({ event: 'form-response', phase: entry.phase, elicitationId: id, response: message.result ?? null,
42
+ ...(message.error ? { error: { code: message.error.code } } : {}) });
43
+ if (message.error) { reply(entry.callId, { content: [{ type: 'text', text: 'Form failed' }], isError: true }); return; }
44
+ reply(entry.callId, { content: [{ type: 'text', text: JSON.stringify({ phase: entry.phase, ...message.result }) }] });
45
+ return;
46
+ }
47
+ if (method === 'initialize') {
48
+ record({ event: 'initialize', protocolVersion: params.protocolVersion, clientName: params.clientInfo?.name,
49
+ elicitationCapabilities: params.capabilities?.elicitation ?? null });
50
+ reply(id, { protocolVersion: params.protocolVersion, capabilities: { tools: {} },
51
+ serverInfo: { name: 'hcc-form-live-acceptance', version: '1.0.0' } });
52
+ } else if (method === 'notifications/initialized') {
53
+ record({ event: 'ready' });
54
+ } else if (method === 'ping') {
55
+ reply(id, {});
56
+ } else if (method === 'tools/list') {
57
+ reply(id, { tools: [{ name: 'collect_project_settings',
58
+ description: 'Ask the human for project settings in a typed MCP form. Wait for their response and return the selected values.',
59
+ inputSchema: { type: 'object', properties: { phase: { type: 'string', enum: ['native', 'web-owned'] } },
60
+ required: ['phase'], additionalProperties: false } }] });
61
+ } else if (method === 'tools/call') {
62
+ const phase = params.arguments?.phase;
63
+ if (params.name !== 'collect_project_settings' || !['native', 'web-owned'].includes(phase)) {
64
+ write({ id, error: { code: -32602, message: 'Unknown acceptance tool or phase' } }); return;
65
+ }
66
+ const elicitationId = 'acceptance-form-' + randomUUID();
67
+ const timer = setTimeout(() => {
68
+ if (!pending.delete(elicitationId)) return;
69
+ record({ event: 'form-timeout', phase, elicitationId });
70
+ reply(id, { content: [{ type: 'text', text: 'Acceptance form timed out' }], isError: true });
71
+ }, 240000);
72
+ pending.set(elicitationId, { callId: id, phase, timer });
73
+ record({ event: 'tool-called', phase, elicitationId });
74
+ write({ id: elicitationId, method: 'elicitation/create', params: { mode: 'form',
75
+ message: 'Acceptance only: provide synthetic project settings / 验收表单:填写合成项目参数', requestedSchema: schema } });
76
+ record({ event: 'form-requested', phase, elicitationId });
77
+ } else if (id !== undefined) {
78
+ write({ id, error: { code: -32601, message: 'Method not found' } });
79
+ }
80
+ });
81
+ lines.on('close', () => { for (const entry of pending.values()) clearTimeout(entry.timer); pending.clear(); });