@logicseek/hello-cc 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/docs/dsh.md +3 -3
- package/docs/dsh.zh-CN.md +3 -3
- package/lib/cli/app.mjs +1 -1
- package/lib/cli/commands/hook.mjs +3 -1
- package/lib/cli/commands/lifecycle.mjs +14 -3
- package/lib/cli/commands/query.mjs +15 -10
- package/lib/cli/commands/run.mjs +7 -6
- package/lib/db/connection.mjs +82 -9
- package/lib/discover.mjs +4 -4
- package/lib/integrations/hooks.mjs +132 -40
- package/lib/integrations/peers/identity.mjs +42 -7
- package/lib/runtime/native/service.mjs +107 -24
- package/lib/runtime/native/store.mjs +63 -6
- package/lib/runtime/project-path.mjs +90 -7
- package/lib/shared/file-lock.mjs +62 -17
- package/lib/shared/socket-lock-worker.mjs +15 -6
- package/lib/web/codex-app-server.mjs +19 -1
- package/lib/web/codex-sessions.mjs +2 -0
- package/lib/web/cookie-auth.mjs +1 -1
- package/lib/web/http-routes.mjs +11 -7
- package/lib/web/http.mjs +20 -2
- package/lib/web/native-sessions.mjs +13 -6
- package/lib/web/peer-actions.mjs +27 -27
- package/lib/web/runtime-main.mjs +20 -3
- package/lib/web/runtime.mjs +18 -4
- package/lib/web/session-serialize.mjs +1 -1
- package/lib/web/startup.mjs +84 -10
- package/lib/web/task-results.mjs +8 -5
- package/lib/web/terminal-assets.mjs +18 -0
- package/lib/web/tmux-stream.mjs +3 -4
- package/lib/web/ui-codex.mjs +137 -20
- package/lib/web/ui-interactions.mjs +37 -3
- package/lib/web/ui-native.mjs +21 -9
- package/lib/web/ui-template.mjs +8 -2
- package/package.json +1 -1
- package/scripts/regression.mjs +8 -1
package/lib/web/ui-codex.mjs
CHANGED
|
@@ -42,6 +42,11 @@ export function codexPanelHtml() {
|
|
|
42
42
|
.codex-code-header { display:flex; align-items:center; justify-content:space-between; padding:4px 8px; color:var(--muted); font-size:var(--small-font); border-bottom:1px solid var(--border); }
|
|
43
43
|
.codex-code-header button { font-size:var(--small-font); min-height:28px; padding:3px 8px; }
|
|
44
44
|
.codex-code pre { margin:0; padding:10px 12px; overflow:auto; white-space:pre; font-family:var(--mono); font-size:var(--small-font); line-height:1.6; }
|
|
45
|
+
.codex-segment-nav { display:flex; flex-wrap:wrap; align-items:center; gap:6px; padding:6px 8px; border-bottom:1px solid var(--border); }
|
|
46
|
+
.codex-segment-nav button { min-height:32px; font-size:var(--small-font); padding:4px 8px; }
|
|
47
|
+
.codex-segment-nav span { color:var(--muted); font-size:var(--small-font); margin-right:auto; }
|
|
48
|
+
.codex-code [data-segment-content] pre { max-height:360px; }
|
|
49
|
+
.codex-code-prose pre { white-space:pre-wrap; overflow-wrap:anywhere; font-family:inherit; font-size:var(--body-font); }
|
|
45
50
|
.codex-diff-add { color:var(--ok); } .codex-diff-remove { color:var(--danger); } .codex-diff-header { color:var(--accent); }
|
|
46
51
|
.codex-review { max-width:900px; margin:16px auto 0; border-top:1px solid var(--border); padding-top:12px; }
|
|
47
52
|
.codex-review h3 { font-size:var(--small-font); margin:14px 0 6px; }
|
|
@@ -71,6 +76,8 @@ export function codexPanelHtml() {
|
|
|
71
76
|
.codex-header, .codex-scroll, .codex-composer { padding-left:12px; padding-right:12px; }
|
|
72
77
|
.codex-card { padding:10px; } .codex-card-agent { padding:2px; }
|
|
73
78
|
.codex-composer-actions button { min-height:36px; }
|
|
79
|
+
.codex-segment-nav span { flex-basis:100%; }
|
|
80
|
+
.codex-segment-nav button { min-height:44px; flex:1; }
|
|
74
81
|
.codex-approval dl { grid-template-columns:1fr; gap:3px; } .codex-approval dd { margin-bottom:6px; }
|
|
75
82
|
}
|
|
76
83
|
</style>
|
|
@@ -102,8 +109,11 @@ export function codexPanelScript() {
|
|
|
102
109
|
const views = new Map();
|
|
103
110
|
const approving = new Set();
|
|
104
111
|
const PAGE_SIZE = 80;
|
|
112
|
+
const SEGMENT_CHARS = 12000, SEGMENT_LINES = 200;
|
|
105
113
|
let copies = [], timelineHtml = '', timelineSignature = '', timelineCopies = [], entries = [];
|
|
114
|
+
let blocks = [], timelineBlocks = [], blockScope = '', blockOrder = 0;
|
|
106
115
|
let state = null, key = '', pending = null, submitting = false;
|
|
116
|
+
let stateRevision = 0, subjectRevision = 0, readSequence = 0;
|
|
107
117
|
const text = (en, zh) => window.hccUi.language === 'zh' ? zh : en;
|
|
108
118
|
const bridge = () => window.hccHandoff;
|
|
109
119
|
const escape = (value) => bridge().esc(String(value ?? ''));
|
|
@@ -118,7 +128,7 @@ export function codexPanelScript() {
|
|
|
118
128
|
function turnId() { return state?.turnId || activeThread()?.activeTurnId || ''; }
|
|
119
129
|
function view() {
|
|
120
130
|
if (!views.has(key)) {
|
|
121
|
-
views.set(key, { top: 0, follow: true, unread: false, signature: '', details: new Map(), startKey: '', start: 0, end: 0, total: 0 });
|
|
131
|
+
views.set(key, { top: 0, follow: true, unread: false, signature: '', details: new Map(), textPages: new Map(), codeScroll: new Map(), startKey: '', start: 0, end: 0, total: 0 });
|
|
122
132
|
if (views.size > 20) views.delete(views.keys().next().value);
|
|
123
133
|
}
|
|
124
134
|
return views.get(key);
|
|
@@ -145,16 +155,57 @@ export function codexPanelScript() {
|
|
|
145
155
|
};
|
|
146
156
|
return labels[status] || String(status || '');
|
|
147
157
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
158
|
+
function segmentRanges(content) {
|
|
159
|
+
const ranges = [];
|
|
160
|
+
let start = 0;
|
|
161
|
+
while (start < content.length) {
|
|
162
|
+
let end = Math.min(content.length, start + SEGMENT_CHARS), cursor = start;
|
|
163
|
+
for (let lines = 0; lines < SEGMENT_LINES; lines += 1) {
|
|
164
|
+
const newline = content.indexOf('\n', cursor);
|
|
165
|
+
if (newline === -1 || newline >= end) break;
|
|
166
|
+
cursor = newline + 1;
|
|
167
|
+
if (lines === SEGMENT_LINES - 1) end = cursor;
|
|
168
|
+
}
|
|
169
|
+
// A page boundary must not split an emoji or another surrogate pair.
|
|
170
|
+
if (end < content.length && /[\uD800-\uDBFF]/.test(content[end - 1]) && /[\uDC00-\uDFFF]/.test(content[end])) end -= 1;
|
|
171
|
+
ranges.push([start, end]); start = end;
|
|
172
|
+
}
|
|
173
|
+
return ranges;
|
|
174
|
+
}
|
|
175
|
+
function codeBody(content, diff) {
|
|
176
|
+
return diff ? content.split('\n').map(line => {
|
|
151
177
|
const cls = line.startsWith('@@') || line.startsWith('diff ') || line.startsWith('+++') || line.startsWith('---')
|
|
152
178
|
? 'codex-diff-header' : line.startsWith('+') ? 'codex-diff-add' : line.startsWith('-') ? 'codex-diff-remove' : '';
|
|
153
179
|
return '<span class="' + cls + '">' + escape(line) + '</span>';
|
|
154
180
|
}).join('\n') : escape(content);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
}
|
|
182
|
+
function segmentBody(block, index) {
|
|
183
|
+
const [start, end] = block.ranges[block.page];
|
|
184
|
+
const button = (action, label, disabled) => '<button type="button" data-segment="' + index + '" data-segment-action="' + action + '"'
|
|
185
|
+
+ (disabled ? ' disabled' : '') + '>' + escape(label) + '</button>';
|
|
186
|
+
return '<nav class="codex-segment-nav" aria-label="' + escape(text('Text sections', '内容分段')) + '"><span role="status" tabindex="-1">'
|
|
187
|
+
+ escape(text('Section ', '第 ') + (block.page + 1) + text(' of ', ' / ') + block.ranges.length + text(' · Full text available', ' 段 · 可查阅全文'))
|
|
188
|
+
+ '</span>' + button('first', text('First', '首段'), block.page === 0)
|
|
189
|
+
+ button('previous', text('Previous', '上一段'), block.page === 0)
|
|
190
|
+
+ button('next', text('Next', '下一段'), block.page === block.ranges.length - 1)
|
|
191
|
+
+ button('last', text('Last', '末段'), block.page === block.ranges.length - 1)
|
|
192
|
+
+ '</nav><pre tabindex="0" data-code-page="' + block.page + '"><code>' + codeBody(block.content.slice(start, end), block.diff) + '</code></pre>';
|
|
193
|
+
}
|
|
194
|
+
function codeBlock(value, language = '', diff = false, plain = false, identityScope = '') {
|
|
195
|
+
const content = String(value ?? ''), index = copies.push(content) - 1;
|
|
196
|
+
const ranges = segmentRanges(content), identity = identityScope || blockScope + ':' + blockOrder++;
|
|
197
|
+
let body, blockIndex = -1;
|
|
198
|
+
if (ranges.length > 1) {
|
|
199
|
+
blockIndex = blocks.length;
|
|
200
|
+
const page = Math.min(view().textPages.get(identity) || 0, ranges.length - 1);
|
|
201
|
+
const block = { content, ranges, diff, page, identity };
|
|
202
|
+
blocks.push(block);
|
|
203
|
+
body = '<div data-segment-content>' + segmentBody(block, blockIndex) + '</div>';
|
|
204
|
+
} else body = '<pre data-code-page="0"><code>' + codeBody(content, diff) + '</code></pre>';
|
|
205
|
+
return '<div class="codex-code' + (plain ? ' codex-code-prose' : '') + '" data-code-key="' + escape(identity) + '"' + (blockIndex < 0 ? '' : ' data-segment-block="' + blockIndex + '"')
|
|
206
|
+
+ '><div class="codex-code-header"><span>' + escape(language || text('Text', '文本'))
|
|
207
|
+
+ '</span><button type="button" data-copy="' + index + '">' + escape(ranges.length > 1 ? text('Copy full text', '复制全文') : text('Copy', '复制'))
|
|
208
|
+
+ '</button></div>' + body + '</div>';
|
|
158
209
|
}
|
|
159
210
|
function inline(value, allowBold = true) {
|
|
160
211
|
const tick = String.fromCharCode(96), source = String(value), fragments = [];
|
|
@@ -225,7 +276,9 @@ export function codexPanelScript() {
|
|
|
225
276
|
return '<div class="codex-markdown">' + parts.join('') + '</div>';
|
|
226
277
|
}
|
|
227
278
|
function prose(value) {
|
|
228
|
-
const
|
|
279
|
+
const content = String(value ?? '');
|
|
280
|
+
if (segmentRanges(content).length > 1) return codeBlock(content, text('Long message · Plain-text sections', '长消息 · 纯文本分段'), false, true);
|
|
281
|
+
const fence = String.fromCharCode(96).repeat(3), lines = content.split('\n');
|
|
229
282
|
let html = '', plain = [], code = [], language = '', inCode = false;
|
|
230
283
|
const flush = () => { if (plain.length) html += markdown(plain.join('\n')); plain = []; };
|
|
231
284
|
for (const line of lines) {
|
|
@@ -250,6 +303,7 @@ export function codexPanelScript() {
|
|
|
250
303
|
}
|
|
251
304
|
function itemCard(item, turn, index) {
|
|
252
305
|
const type = item.type || '', id = turn.id + ':' + (item.id || index), content = itemText(item);
|
|
306
|
+
blockScope = 'item:' + id; blockOrder = 0;
|
|
253
307
|
const error = item.status === 'failed' || item.error || (item.exitCode != null && item.exitCode !== 0);
|
|
254
308
|
const status = '<span class="codex-status' + (error ? ' codex-status-failed' : '') + '">' + escape(error ? text('Failed', '失败') : statusLabel(item.status)) + '</span>';
|
|
255
309
|
if (type === 'userMessage' || type === 'agentMessage') {
|
|
@@ -297,14 +351,64 @@ export function codexPanelScript() {
|
|
|
297
351
|
node.addEventListener('toggle', () => { if (key === currentKey && node.isConnected !== false) view().details.set(node.dataset.detailsKey, node.open); });
|
|
298
352
|
});
|
|
299
353
|
container.querySelectorAll('button[data-copy]').forEach(button => {
|
|
300
|
-
const content = copies[Number(button.dataset.copy)];
|
|
301
354
|
button.addEventListener('click', async () => {
|
|
355
|
+
if (key !== currentKey || button.isConnected === false) return;
|
|
356
|
+
// A hidden section may change while the visible HTML stays identical.
|
|
357
|
+
const content = copies[Number(button.dataset.copy)];
|
|
302
358
|
try {
|
|
303
359
|
await navigator.clipboard.writeText(content);
|
|
304
360
|
button.textContent = text('Copied', '已复制');
|
|
305
361
|
} catch (_) { button.textContent = text('Select text to copy', '请选择文本复制'); }
|
|
306
362
|
});
|
|
307
363
|
});
|
|
364
|
+
bindSegmentActions(container);
|
|
365
|
+
bindCodeScroll(container);
|
|
366
|
+
}
|
|
367
|
+
function bindCodeScroll(container) {
|
|
368
|
+
const currentKey = key;
|
|
369
|
+
container.querySelectorAll('pre[data-code-page]').forEach(pre => {
|
|
370
|
+
const identity = pre.closest('[data-code-key]')?.dataset.codeKey;
|
|
371
|
+
if (!identity) return;
|
|
372
|
+
const page = Number(pre.dataset.codePage), saved = view().codeScroll.get(identity);
|
|
373
|
+
if (saved?.page === page) { pre.scrollTop = saved.top; pre.scrollLeft = saved.left; }
|
|
374
|
+
// Scroll events from restoration are asynchronous. Ignore unchanged
|
|
375
|
+
// coordinates so restoring a block never opts out of follow mode.
|
|
376
|
+
let top = pre.scrollTop, left = pre.scrollLeft;
|
|
377
|
+
pre.addEventListener('scroll', () => {
|
|
378
|
+
if (key !== currentKey || pre.isConnected === false || (top === pre.scrollTop && left === pre.scrollLeft)) return;
|
|
379
|
+
top = pre.scrollTop; left = pre.scrollLeft;
|
|
380
|
+
const reading = view();
|
|
381
|
+
reading.codeScroll.set(identity, { page, top, left });
|
|
382
|
+
if (reading.codeScroll.size > 256) reading.codeScroll.delete(reading.codeScroll.keys().next().value);
|
|
383
|
+
if (top || left) { reading.follow = false; reading.top = scroller.scrollTop; updateJump(); }
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function bindSegmentActions(container) {
|
|
388
|
+
const currentKey = key;
|
|
389
|
+
container.querySelectorAll('button[data-segment]').forEach(button => {
|
|
390
|
+
const index = Number(button.dataset.segment);
|
|
391
|
+
button.addEventListener('click', () => {
|
|
392
|
+
const block = blocks[index];
|
|
393
|
+
if (key !== currentKey || button.isConnected === false || !block) return;
|
|
394
|
+
const action = button.dataset.segmentAction;
|
|
395
|
+
block.page = Math.max(0, Math.min(block.ranges.length - 1,
|
|
396
|
+
action === 'first' ? 0 : action === 'last' ? block.ranges.length - 1 : block.page + (action === 'next' ? 1 : -1)));
|
|
397
|
+
view().textPages.set(block.identity, block.page);
|
|
398
|
+
view().codeScroll.delete(block.identity);
|
|
399
|
+
if (view().textPages.size > 256) view().textPages.delete(view().textPages.keys().next().value);
|
|
400
|
+
const wrapper = button.closest('[data-segment-block]'), body = wrapper.querySelector('[data-segment-content]');
|
|
401
|
+
const top = wrapper.getBoundingClientRect().top, oldTop = scroller.scrollTop;
|
|
402
|
+
view().follow = false;
|
|
403
|
+
body.innerHTML = segmentBody(block, index);
|
|
404
|
+
bindSegmentActions(body);
|
|
405
|
+
bindCodeScroll(body);
|
|
406
|
+
const focus = body.querySelector('button[data-segment-action="' + action + '"]:not(:disabled)') || body.querySelector('[role="status"]');
|
|
407
|
+
focus?.focus({ preventScroll: true });
|
|
408
|
+
scroller.scrollTop = oldTop + wrapper.getBoundingClientRect().top - top;
|
|
409
|
+
view().top = scroller.scrollTop; updateJump();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
308
412
|
}
|
|
309
413
|
function renderTimeline(turns, reading) {
|
|
310
414
|
// Page by items rather than turns: a single tool-heavy turn can contain hundreds of cards.
|
|
@@ -327,7 +431,7 @@ export function codexPanelScript() {
|
|
|
327
431
|
if (!reading.follow && reading.signature && reading.signature !== updateSignature) reading.unread = true;
|
|
328
432
|
reading.signature = updateSignature;
|
|
329
433
|
if (signature !== timelineSignature) {
|
|
330
|
-
copies = [];
|
|
434
|
+
copies = []; blocks = [];
|
|
331
435
|
let previousTurn = null;
|
|
332
436
|
let html = visible.map(({ turn, turnIndex, item, index }) => {
|
|
333
437
|
let heading = '';
|
|
@@ -344,8 +448,8 @@ export function codexPanelScript() {
|
|
|
344
448
|
document.getElementById('codexTimeline').innerHTML = html;
|
|
345
449
|
timelineHtml = html; bindReadActions(document.getElementById('codexTimeline'));
|
|
346
450
|
}
|
|
347
|
-
timelineSignature = signature; timelineCopies = copies.slice();
|
|
348
|
-
} else copies = timelineCopies.slice();
|
|
451
|
+
timelineSignature = signature; timelineCopies = copies.slice(); timelineBlocks = blocks.slice();
|
|
452
|
+
} else { copies = timelineCopies.slice(); blocks = timelineBlocks.slice(); }
|
|
349
453
|
document.getElementById('codexHistory').hidden = entries.length <= PAGE_SIZE;
|
|
350
454
|
document.getElementById('codexHistory').setAttribute('aria-label', text('Conversation history', '会话历史'));
|
|
351
455
|
document.getElementById('codexEarlier').textContent = text('Earlier', '更早记录');
|
|
@@ -358,9 +462,10 @@ export function codexPanelScript() {
|
|
|
358
462
|
function sync() {
|
|
359
463
|
const next = draftKey();
|
|
360
464
|
if (next !== key) {
|
|
465
|
+
subjectRevision += 1;
|
|
361
466
|
if (key && state) view().top = scroller.scrollTop || 0;
|
|
362
467
|
key = next; pending = null; draft.value = ''; state = null;
|
|
363
|
-
timelineHtml = ''; timelineSignature = ''; timelineCopies = []; copies = []; entries = [];
|
|
468
|
+
timelineHtml = ''; timelineSignature = ''; timelineCopies = []; copies = []; entries = []; blocks = []; timelineBlocks = [];
|
|
364
469
|
document.getElementById('codexHistory').hidden = true;
|
|
365
470
|
for (const id of ['codexStatus', 'codexTimeline', 'codexDiff', 'codexPlan', 'codexApprovals', 'codexSubmission']) document.getElementById(id).textContent = '';
|
|
366
471
|
document.getElementById('codexReview').open = view().details.get('review') || false;
|
|
@@ -397,7 +502,10 @@ export function codexPanelScript() {
|
|
|
397
502
|
{ method: 'POST', body: JSON.stringify({ ...body, action_token: target.token, epoch: target.epoch }) });
|
|
398
503
|
}
|
|
399
504
|
function render(value, resetScroll = false) {
|
|
400
|
-
sync();
|
|
505
|
+
sync();
|
|
506
|
+
// Local paging and preference redraws reuse state; incoming snapshots do not.
|
|
507
|
+
if (value !== state) stateRevision += 1;
|
|
508
|
+
state = value; sync();
|
|
401
509
|
const thread = activeThread();
|
|
402
510
|
document.getElementById('codexStatus').textContent = [text('Executor', '执行器') + ': ' + statusLabel(state.status),
|
|
403
511
|
text('Thread', '会话') + ': ' + (thread?.id || ''), turnId() ? text('Turn', '当前轮') + ': ' + turnId() : text('Idle', '空闲'),
|
|
@@ -410,12 +518,13 @@ export function codexPanelScript() {
|
|
|
410
518
|
renderTimeline(turns, reading);
|
|
411
519
|
const last = turns[turns.length - 1];
|
|
412
520
|
const plan = Array.isArray(last?.plan) ? last.plan : [];
|
|
521
|
+
blockScope = 'plan:' + last?.id; blockOrder = 0;
|
|
413
522
|
document.getElementById('codexPlan').innerHTML = plan.length ? '<h3>' + escape(text('Plan', '执行计划')) + '</h3>'
|
|
414
523
|
+ (last.explanation ? prose(last.explanation) : '') + '<ol class="codex-plan">' + plan.map(step =>
|
|
415
524
|
'<li data-status="' + escape(step.status || '') + '"><span>' + escape(statusLabel(step.status)) + '</span><span>'
|
|
416
525
|
+ escape(step.step || '') + '</span></li>').join('') + '</ol>' : '';
|
|
417
526
|
document.getElementById('codexDiff').innerHTML = last?.diff ? '<h3>' + escape(text('Latest turn · Executor-reported changes', '最新一轮 · 执行器报告的变更'))
|
|
418
|
-
+ '</h3>' + codeBlock(last.diff, text('Diff', '差异'), true) : '';
|
|
527
|
+
+ '</h3>' + codeBlock(last.diff, text('Diff', '差异'), true, false, 'review:diff:' + last.id) : '';
|
|
419
528
|
const review = document.getElementById('codexReview');
|
|
420
529
|
review.hidden = !last?.diff && !plan.length;
|
|
421
530
|
bindReadActions(document.getElementById('codexPlan'));
|
|
@@ -425,6 +534,7 @@ export function codexPanelScript() {
|
|
|
425
534
|
const container = document.getElementById('codexApprovals');
|
|
426
535
|
window.hccInteractions.remember(container);
|
|
427
536
|
container.innerHTML = approvals.map((approval, index) => {
|
|
537
|
+
blockScope = 'approval:' + executorId + ':' + approval.requestId; blockOrder = 0;
|
|
428
538
|
const params = approval.params || {}, fields = [
|
|
429
539
|
[text('Operation', '操作'), approval.kind === 'userInput' ? text('Answer questions', '回答问题') : approval.kind === 'permissions' ? text('Grant permissions', '授予权限') : approval.method?.includes('fileChange') ? text('Change files', '修改文件') : text('Run command', '执行命令')],
|
|
430
540
|
[text('Command', '命令'), params.command], [text('Directory', '目录'), params.cwd],
|
|
@@ -432,8 +542,12 @@ export function codexPanelScript() {
|
|
|
432
542
|
[text('Permission root', '权限目录'), params.grantRoot]
|
|
433
543
|
].filter(([, value]) => value != null && value !== '');
|
|
434
544
|
return '<article class="codex-card codex-approval"><h3>' + escape(approval.kind === 'userInput' ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
|
|
435
|
-
+ '</h3><dl>' + fields.map(([label, value]) =>
|
|
436
|
-
|
|
545
|
+
+ '</h3><dl>' + fields.map(([label, value], fieldIndex) => {
|
|
546
|
+
const fullValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
547
|
+
return '<dt>' + escape(label) + '</dt><dd>'
|
|
548
|
+
+ (segmentRanges(fullValue).length > 1 ? codeBlock(fullValue, text('Full value', '完整字段'), false, true, blockScope + ':field:' + fieldIndex) : escape(fullValue))
|
|
549
|
+
+ '</dd>';
|
|
550
|
+
}).join('') + '</dl>'
|
|
437
551
|
+ (approval.truncated ? '<p class="codex-prose">' + escape(text('Parameters are truncated. Read the full operation before deciding.', '参数已截断,请核对完整操作后再决定。')) + '</p>' : '')
|
|
438
552
|
+ details('approval:' + approval.requestId, '<strong>' + escape(text('Raw parameters', '原始参数')) + '</strong>',
|
|
439
553
|
codeBlock(JSON.stringify(params, null, 2), 'JSON'), false)
|
|
@@ -533,12 +647,15 @@ export function codexPanelScript() {
|
|
|
533
647
|
});
|
|
534
648
|
document.getElementById('codexRecover').addEventListener('click', async () => {
|
|
535
649
|
const b = bridge(), target = subject();
|
|
650
|
+
const subjectVersion = subjectRevision, sequence = ++readSequence;
|
|
651
|
+
const current = () => matches(target) && subjectVersion === subjectRevision && sequence === readSequence;
|
|
536
652
|
try {
|
|
537
653
|
await b.api(route(target, 'read'));
|
|
538
|
-
if (!
|
|
654
|
+
if (!current()) return;
|
|
655
|
+
const version = stateRevision;
|
|
539
656
|
const result = await b.api(route(target, 'state'));
|
|
540
|
-
if (
|
|
541
|
-
} catch (error) { if (
|
|
657
|
+
if (current() && version === stateRevision) render(result.state);
|
|
658
|
+
} catch (error) { if (current()) message(error.detail || error.message); }
|
|
542
659
|
});
|
|
543
660
|
document.getElementById('codexClear').addEventListener('click', () => {
|
|
544
661
|
if (!confirm(text('Clear this draft and its unconfirmed submission marker? Check thread history first.', '清除草稿及未确认提交标记?请先核对会话历史。'))) return;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export function interactionPanelScript() {
|
|
3
3
|
return String.raw`(() => {
|
|
4
4
|
if (window.hccInteractions) return;
|
|
5
|
-
const drafts = new Map();
|
|
5
|
+
const drafts = new Map(), expanded = new Map();
|
|
6
6
|
let focus = null;
|
|
7
7
|
function remember(container) {
|
|
8
8
|
container.querySelectorAll('[data-interaction-field]').forEach(control => {
|
|
@@ -10,6 +10,8 @@ export function interactionPanelScript() {
|
|
|
10
10
|
drafts.set(control.dataset.interactionField, { value: control.value, checked: control.checked });
|
|
11
11
|
if (control === document.activeElement) focus = { key: control.dataset.interactionField, start: control.selectionStart, end: control.selectionEnd };
|
|
12
12
|
});
|
|
13
|
+
container.querySelectorAll('[data-interaction-details]').forEach(detail => expanded.set(detail.dataset.interactionDetails, detail.open));
|
|
14
|
+
while (expanded.size > 256) expanded.delete(expanded.keys().next().value);
|
|
13
15
|
while (drafts.size > 256) drafts.delete(drafts.keys().next().value);
|
|
14
16
|
}
|
|
15
17
|
function restore(container) {
|
|
@@ -22,6 +24,38 @@ export function interactionPanelScript() {
|
|
|
22
24
|
});
|
|
23
25
|
for (const key of drafts.keys()) if (!live.has(key)) drafts.delete(key);
|
|
24
26
|
if (focus && !live.has(focus.key)) focus = null;
|
|
27
|
+
const detailKeys = new Set();
|
|
28
|
+
container.querySelectorAll('[data-interaction-details]').forEach(detail => {
|
|
29
|
+
const key = detail.dataset.interactionDetails; detailKeys.add(key);
|
|
30
|
+
if (expanded.has(key)) detail.open = expanded.get(key);
|
|
31
|
+
});
|
|
32
|
+
for (const key of expanded.keys()) if (!detailKeys.has(key)) expanded.delete(key);
|
|
33
|
+
}
|
|
34
|
+
function approvalBlocked(request) {
|
|
35
|
+
const tool = request.params?.toolCall;
|
|
36
|
+
return Boolean(request.truncated || (request.method === 'session/request_permission' &&
|
|
37
|
+
(!tool || tool.contextPending || tool.contextTruncated || !Object.hasOwn(tool, 'rawInput') ||
|
|
38
|
+
!request.params.options?.some(option => option.kind === 'allow_once'))));
|
|
39
|
+
}
|
|
40
|
+
function preview(request, key, esc, text) {
|
|
41
|
+
const params = request.params || {}, tool = params.toolCall || {}, input = params.input || tool.rawInput || {};
|
|
42
|
+
const rows = [], row = (label, value) => { if (typeof value === 'string' && value) rows.push('<dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd>'); };
|
|
43
|
+
const name = params.tool || tool.title;
|
|
44
|
+
row(text('Tool', '工具'), name);
|
|
45
|
+
row(text('Command', '命令'), params.command || input.command);
|
|
46
|
+
row(text('File', '文件'), input.file_path || input.path || params.path || params.grantRoot);
|
|
47
|
+
row(text('Working directory', '工作目录'), params.cwd || input.cwd);
|
|
48
|
+
row(text('Reason', '原因'), params.reason || params.message);
|
|
49
|
+
const content = typeof input.content === 'string' ? input.content : null;
|
|
50
|
+
const previewLimit = 1024;
|
|
51
|
+
const contentPreview = content === null ? '' : '<p>' + esc(text('Content preview', '内容预览')) + '</p><pre class="hcc-interaction-content">' + esc(content.slice(0, previewLimit)) + '</pre>'
|
|
52
|
+
+ (content.length > previewLimit ? '<p>' + esc(text('Preview shortened; expand the full operation details to review all content.', '预览已缩短,展开完整操作详情可核对全部内容。')) + '</p>' : '');
|
|
53
|
+
const known = rows.length || ['permissions', 'userInput'].includes(request.kind);
|
|
54
|
+
const missingContext = request.method === 'session/request_permission' && approvalBlocked(request);
|
|
55
|
+
return (rows.length ? '<dl class="hcc-interaction-summary">' + rows.join('') + '</dl>' : '') + contentPreview
|
|
56
|
+
+ (missingContext ? '<p role="alert">' + esc(text('The operation input or one-time approval option is unavailable. Only rejection is available.', '操作输入或单次批准选项不可用,目前只能拒绝。')) + '</p>' : '')
|
|
57
|
+
+ (request.kind === 'approval' && !missingContext ? '<p>' + esc(text('Approval applies only to this operation. Later requests require another decision.', '仅批准本次操作,后续请求仍需单独确认。')) + '</p>' : '')
|
|
58
|
+
+ '<details class="hcc-interaction-details" data-interaction-details="' + esc(key) + '"' + (known ? '' : ' open') + '><summary>' + esc(text('Full operation details', '完整操作详情')) + '</summary><pre>' + esc(JSON.stringify(params, null, 2)) + '</pre></details>';
|
|
25
59
|
}
|
|
26
60
|
function form(request, key, esc, text) {
|
|
27
61
|
const field = suffix => ' data-interaction-field="' + esc(key + ':' + suffix) + '"';
|
|
@@ -72,7 +106,7 @@ export function interactionPanelScript() {
|
|
|
72
106
|
}
|
|
73
107
|
return { decision, permissions, scope: container.querySelector('[data-scope]').value };
|
|
74
108
|
}
|
|
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 };
|
|
109
|
+
function forget(key) { for (const field of drafts.keys()) if (field.startsWith(key + ':')) drafts.delete(field); expanded.delete(key); }
|
|
110
|
+
window.hccInteractions = { preview, approvalBlocked, form, payload, remember, restore, forget };
|
|
77
111
|
})();`;
|
|
78
112
|
}
|
package/lib/web/ui-native.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 || '');
|
|
@@ -94,15 +103,16 @@ export function nativePanelScript() {
|
|
|
94
103
|
window.hccInteractions.remember(container);
|
|
95
104
|
container.innerHTML = approvals.map((request, index) => '<article class="codex-card codex-approval"><h3>'
|
|
96
105
|
+ esc(request.kind === 'userInput' ? text('Your input is required', '等待您的答复') : text('Approval required', '等待人工审批'))
|
|
97
|
-
+ '</h3
|
|
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="' +
|
|
109
|
+
+ '<button data-response="' + index + '" data-decision="accept" data-truncated="' + window.hccInteractions.approvalBlocked(request) + '" 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
110
|
+ '<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
111
|
window.hccInteractions.restore(container);
|
|
103
112
|
container.querySelectorAll('button[data-response]').forEach(button => button.addEventListener('click', async () => {
|
|
104
113
|
if (!matches(target) || !bridge().canControl || responding.has(button.dataset.requestKey)) return;
|
|
105
114
|
const request = approvals[Number(button.dataset.response)], key = button.dataset.requestKey;
|
|
115
|
+
const version = observed;
|
|
106
116
|
responding.add(key); sync();
|
|
107
117
|
try {
|
|
108
118
|
const payload = window.hccInteractions.payload(request, ['permissions','userInput'].includes(request.kind) ? button.closest('article') : null, button.dataset.decision);
|
|
@@ -110,8 +120,10 @@ export function nativePanelScript() {
|
|
|
110
120
|
executorId:request.executorId, sessionId:request.sessionId, turnId:request.turnId, requestId:request.requestId,
|
|
111
121
|
...payload, action_token:bridge().actionToken, epoch:bridge().epoch })});
|
|
112
122
|
window.hccInteractions.forget(key);
|
|
113
|
-
if (matches(target)) { if (result.state) render(result.state); else void read(); }
|
|
114
|
-
} catch(error) {
|
|
123
|
+
if (matches(target) && version === observed) { if (result.state) render(result.state); else void read(); }
|
|
124
|
+
} catch(error) {
|
|
125
|
+
if (matches(target) && (state?.pendingApprovals || []).some(current => current.requestId === request.requestId && current.executorId === request.executorId)) notice(error.detail || error.message);
|
|
126
|
+
}
|
|
115
127
|
finally { responding.delete(key); sync(); }
|
|
116
128
|
}));
|
|
117
129
|
if (pending && (state.deliveries || []).some(delivery => delivery.submission_id === pending.id && delivery.message_id)) acknowledge(subject(),pending.id);
|
|
@@ -128,15 +140,15 @@ export function nativePanelScript() {
|
|
|
128
140
|
draft.addEventListener('input',()=>{ save(); sync(); });
|
|
129
141
|
document.getElementById('nativeSend').addEventListener('click',async()=>{
|
|
130
142
|
sync(); if (document.getElementById('nativeSend').disabled) return;
|
|
131
|
-
const target = subject(), text = draft.value, id = 's_' + crypto.randomUUID();
|
|
143
|
+
const target = subject(), text = draft.value, id = 's_' + crypto.randomUUID(), version = observed;
|
|
132
144
|
pending = {id,text,generation:target.generation}; save(); inflight.add(target.key); sync(); notice(tr('native.sending'));
|
|
133
145
|
try {
|
|
134
146
|
const result = await bridge().api(route(target,'send'),{method:'POST',body:JSON.stringify({text,submissionId:id,action_token:target.token,epoch:target.epoch})});
|
|
135
147
|
const receipt = result.receipt || result;
|
|
136
148
|
if (receipt.submission_id !== id || !Number.isSafeInteger(receipt.message_id)) throw new Error(tr('native.invalidReceipt'));
|
|
137
149
|
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')); }
|
|
150
|
+
if (matches(target) && version === observed) { if (result.state) render(result.state); else void read(); }
|
|
151
|
+
} catch(error) { if (matches(target) && pending?.id === id) notice((error.detail || error.message) + ' ' + tr('native.uncertain')); }
|
|
140
152
|
finally { inflight.delete(target.key); sync(); }
|
|
141
153
|
});
|
|
142
154
|
document.getElementById('nativeInterrupt').addEventListener('click',async()=>{
|
package/lib/web/ui-template.mjs
CHANGED
|
@@ -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)) {
|
|
@@ -2130,11 +2132,15 @@ export function webIndexHtml({ nonce, pane = false } = {}) {
|
|
|
2130
2132
|
};
|
|
2131
2133
|
}
|
|
2132
2134
|
|
|
2135
|
+
function terminalShellQuote(value) {
|
|
2136
|
+
return "'" + String(value).split("'").join(${SHELL_APOSTROPHE_ESCAPE}) + "'";
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2133
2139
|
function terminalCommandForAction(action, info) {
|
|
2134
2140
|
const session = info.session || {};
|
|
2135
|
-
const peerId = info.peerId;
|
|
2141
|
+
const peerId = terminalShellQuote(info.peerId);
|
|
2136
2142
|
const lines = {
|
|
2137
|
-
register: \`hcc register --peer \${peerId} --kind \${session.kind || 'other'} --role \${session.role || 'peer'}\`,
|
|
2143
|
+
register: \`hcc register --peer \${peerId} --kind \${terminalShellQuote(session.kind || 'other')} --role \${terminalShellQuote(session.role || 'peer')}\`,
|
|
2138
2144
|
inbox: \`hcc msg inbox --peer \${peerId}\`,
|
|
2139
2145
|
'task-next': \`hcc task next --peer \${peerId}\`,
|
|
2140
2146
|
state: \`hcc state --peer \${peerId}\`,
|
package/package.json
CHANGED
package/scripts/regression.mjs
CHANGED
|
@@ -4420,7 +4420,14 @@ async function multiProjectWebWorkflow() {
|
|
|
4420
4420
|
|
|
4421
4421
|
const htmlResponse = await fetch(currentRuntimeUrl('/'));
|
|
4422
4422
|
const html = await htmlResponse.text();
|
|
4423
|
-
|
|
4423
|
+
const startForm = html.match(/<form\b[^>]*\bid="startForm"[^>]*>[\s\S]*?<\/form>/)?.[0];
|
|
4424
|
+
if (!startForm) fail('web form missing startForm');
|
|
4425
|
+
// Approval details may display the working directory. The simplified launch
|
|
4426
|
+
// form must still derive it from the selected project instead of an input.
|
|
4427
|
+
if (startForm.includes('Working directory') || /<input\b[^>]*(?:id|name)="(?:cwd|workdir)"/.test(startForm)) {
|
|
4428
|
+
fail('web launch form still exposes Working directory');
|
|
4429
|
+
}
|
|
4430
|
+
for (const forbidden of ['Alias optional', 'Role tag', 'Command<input', 'commandbar', 'lineInput', 'Send text to active terminal']) {
|
|
4424
4431
|
if (html.includes(forbidden)) fail(`web form still exposes ${forbidden}`);
|
|
4425
4432
|
}
|
|
4426
4433
|
for (const expected of [
|