agy-mcp-bridge 0.1.19 → 0.1.21
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/README.md +71 -421
- package/config/workspaces.example.json +4 -0
- package/dist/src/agents/antigravity.js +1 -1
- package/dist/src/agents/registry.js +1 -1
- package/dist/src/antigravity/process.js +5 -3
- package/dist/src/config.js +9 -0
- package/dist/src/isolation/orphans.js +281 -0
- package/dist/src/isolation/worktree.js +195 -5
- package/dist/src/models.js +2 -2
- package/dist/src/runtime.js +27 -5
- package/dist/src/tasks/manager.js +91 -8
- package/docs/HARDENING.md +13 -1
- package/docs/VERIFICATION.md +6 -2
- package/package.json +2 -1
- package/ui/app.css +10 -2
- package/ui/app.js +105 -44
package/ui/app.js
CHANGED
|
@@ -56,15 +56,20 @@ function duration(ms) {
|
|
|
56
56
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}p ${s % 60}s`;
|
|
57
57
|
}
|
|
58
58
|
function agoEl(iso, className) { return h('span', {class: className, 'data-ago': iso, title: clock(iso), text: ago(iso)}); }
|
|
59
|
-
/**
|
|
59
|
+
/** Secondary line under the progress bar; the "why" lives in ACTIVITY_HINT, shown as a tooltip. */
|
|
60
|
+
const ACTIVITY_HINT = 'Agy ở chế độ print/json chỉ in khi xong, nên "chưa có output" là bình thường. ' +
|
|
61
|
+
'Log của agy lớn dần là dấu hiệu tiến trình còn sống; khi còn hoạt động, bridge gia hạn hạn mềm thay vì cắt đúng lúc hết giờ.';
|
|
60
62
|
function activityText(activity) {
|
|
61
63
|
if (!activity) return 'Chưa có thông tin hoạt động.';
|
|
62
64
|
const parts = [];
|
|
63
|
-
if (activity.log_updated_at) parts.push(`
|
|
64
|
-
else if (activity.soft_deadline) parts.push('
|
|
65
|
-
parts.push(activity.last_output_at ? `
|
|
65
|
+
if (activity.log_updated_at) parts.push(`log agy: ${ago(activity.log_updated_at)} (${bytes(activity.log_bytes ?? 0)})`);
|
|
66
|
+
else if (activity.soft_deadline) parts.push('log agy: chưa có dữ liệu');
|
|
67
|
+
parts.push(activity.last_output_at ? `output cuối ${ago(activity.last_output_at)}` : 'chưa có output');
|
|
66
68
|
const soft = activity.soft_deadline;
|
|
67
|
-
if (soft)
|
|
69
|
+
if (soft) {
|
|
70
|
+
if (soft.extensions) parts.push(`đã gia hạn ${soft.extensions} lần`);
|
|
71
|
+
else parts.push(soft.next_check_at ? `chưa gia hạn, kiểm tra lại sau ${Math.max(0, Math.round((Date.parse(soft.next_check_at) - Date.now()) / 1000))}s` : 'chưa gia hạn');
|
|
72
|
+
}
|
|
68
73
|
return parts.join(' · ');
|
|
69
74
|
}
|
|
70
75
|
/** Steps agy reported (step progress setting): newest first, each with the raw event (redacted) one click away. */
|
|
@@ -258,6 +263,11 @@ const USAGE_LABELS = {input_tokens: 'Token vào', output_tokens: 'Token ra', tot
|
|
|
258
263
|
const usageName = key => USAGE_LABELS[key] ? `${USAGE_LABELS[key]} (${key})` : key;
|
|
259
264
|
const number = value => Number(value).toLocaleString('vi-VN', {maximumFractionDigits: 2});
|
|
260
265
|
const usageLine = usage => usage && Object.keys(usage).length ? Object.entries(usage).map(([key, value]) => `${key} ${number(value)}`).join(' · ') : null;
|
|
266
|
+
/** One number in the row, the whole breakdown one hover away. */
|
|
267
|
+
const usageBrief = usage => {
|
|
268
|
+
if (!usage || !Object.keys(usage).length) return '—';
|
|
269
|
+
return 'total_tokens' in usage ? `${number(usage.total_tokens)} token` : `${Object.keys(usage).length} trường usage`;
|
|
270
|
+
};
|
|
261
271
|
async function copy(text) {
|
|
262
272
|
try { await navigator.clipboard.writeText(text); notify('Đã sao chép'); } catch { notify('Không thể sao chép tự động'); }
|
|
263
273
|
}
|
|
@@ -304,7 +314,7 @@ function taskItems(o) {
|
|
|
304
314
|
class: 'task-item', role: 'listitem', 'data-task': task.task_id, 'aria-current': String(task.task_id === state.selected), onclick: () => select(task.task_id)
|
|
305
315
|
}, h('div', {class: 'row'}, pill(task.status), agoEl(task.queued_at || task.started_at, 'meta')),
|
|
306
316
|
h('div', {class: 'title', text: task.title || '(không có nội dung yêu cầu)'}),
|
|
307
|
-
h('div', {class: 'meta mono', text: `${task.agent}${task.isolation === 'worktree' ? ' · worktree' : ''} · ${folder(task.
|
|
317
|
+
h('div', {class: 'meta mono', text: `${task.agent}${task.isolation === 'worktree' ? ' · worktree' : ''} · ${folder(task.workspace)}`, title: task.task_id}),
|
|
308
318
|
h('div', {class: 'meta'}, task.status === 'running' ? elapsedEl(task.started_at, 'Đang chạy ') : task.status === 'queued' ? 'Đang xếp hàng…' : `Mất ${duration(task.duration_ms)}`,
|
|
309
319
|
task.tokens !== null && task.tokens !== undefined ? h('span', {class: overTask(o, task) ? 'over-budget' : '', text: ` · ${number(task.tokens)} token${overTask(o, task) ? ' (vượt mức)' : ''}`}) : null)));
|
|
310
320
|
}
|
|
@@ -315,6 +325,13 @@ function details(key, title, ...body) {
|
|
|
315
325
|
el.addEventListener('toggle', () => { el.open ? state.open.add(key) : state.open.delete(key); });
|
|
316
326
|
return el;
|
|
317
327
|
}
|
|
328
|
+
/** Text that answers "why / what if" instead of "what is happening now" gets collapsed until asked for. */
|
|
329
|
+
function collapsible(key, title, ...body) {
|
|
330
|
+
const el = details(key, title, ...body);
|
|
331
|
+
el.classList.add('more');
|
|
332
|
+
return el;
|
|
333
|
+
}
|
|
334
|
+
function more(key, ...body) { return collapsible(key, 'Xem thêm', ...body); }
|
|
318
335
|
|
|
319
336
|
const CHANGE_LABEL = {modified: 'Sửa', deleted: 'Xóa', added: 'Thêm', 'type-changed': 'Đổi kiểu'};
|
|
320
337
|
function changesBody(task) {
|
|
@@ -361,6 +378,21 @@ function scopeBody(task) {
|
|
|
361
378
|
return h('div', {class: 'callout', text: `${scope.changed} file thay đổi, tất cả nằm trong owns.`});
|
|
362
379
|
}
|
|
363
380
|
|
|
381
|
+
/**
|
|
382
|
+
* What the bridge did with the processes the agent left behind. Absent while the task runs and while the release
|
|
383
|
+
* query is still open, so a missing line is not a failure to clean up.
|
|
384
|
+
*/
|
|
385
|
+
function cleanupBody(task) {
|
|
386
|
+
const c = task.cleanup;
|
|
387
|
+
if (!c) return null;
|
|
388
|
+
if (!c.checked) return h('div', {class: 'callout warn', text: `Không kiểm tra được cây tiến trình agent: ${c.reason || 'lỗi không xác định'}`});
|
|
389
|
+
const parts = [c.reaped.length ? `đã giải phóng ${c.reaped.length} cây tiến trình (${c.reaped.join(', ')})` : 'không còn cây tiến trình nào cần dọn'];
|
|
390
|
+
if (c.skipped.length) parts.push(`${c.skipped.length} nhánh vẫn chạy: ${c.skipped.map(s => `${s.pid} — ${s.guard}`).join('; ')}`);
|
|
391
|
+
if (typeof c.tracked === 'number') parts.push(`đã ghi danh ${c.tracked} tiến trình trong lúc agent chạy`);
|
|
392
|
+
if (c.group_signalled === true) parts.push('đã gửi tín hiệu tới cả nhóm tiến trình');
|
|
393
|
+
return h('div', {class: c.skipped.length ? 'callout warn' : 'callout', text: `Tiến trình agent để lại: ${parts.join(' · ')}`});
|
|
394
|
+
}
|
|
395
|
+
|
|
364
396
|
function detailView() {
|
|
365
397
|
const d = state.detail;
|
|
366
398
|
if (!d) return h('div', {class: 'card empty'}, state.selected ? 'Đang tải…' : 'Chọn một task để xem nội dung trao đổi.');
|
|
@@ -368,18 +400,25 @@ function detailView() {
|
|
|
368
400
|
const running = task.status === 'running';
|
|
369
401
|
const queued = task.status === 'queued';
|
|
370
402
|
const agentName = task.agent || 'agent';
|
|
403
|
+
const place = task.worktree || task.workspace;
|
|
404
|
+
const metaBits = [folder(task.workspace), agentName,
|
|
405
|
+
task.isolation === 'worktree' ? 'worktree' : 'tuần tự',
|
|
406
|
+
request && 'model' in request && request.model ? request.model : null,
|
|
407
|
+
running ? elapsedEl(task.started_at, 'đã chạy ') : queued ? 'đang xếp hàng' : `mất ${duration(task.duration_ms)}`,
|
|
408
|
+
state.detail?.stats?.usage && 'total_tokens' in state.detail.stats.usage ? `${number(state.detail.stats.usage.total_tokens)} token` : null].filter(Boolean);
|
|
371
409
|
const head = h('div', {class: 'detail-head'},
|
|
372
410
|
h('div', {}, h('h2', {}, 'Task ', h('span', {class: 'mono', text: task.task_id.slice(0, 8)}), pill(task.status)),
|
|
373
|
-
h('div', {class: 'muted mono',
|
|
374
|
-
|
|
411
|
+
h('div', {class: 'meta-line muted mono', title: place},
|
|
412
|
+
metaBits.reduce((acc, value) => acc.length ? [...acc, ' · ', value] : [value], []))),
|
|
375
413
|
running || queued ? h('button', {class: 'btn danger small', onclick: () => cancel(task.task_id), text: 'Hủy task'}) : null);
|
|
376
414
|
const activity = state.detail?.activity;
|
|
377
415
|
const progress = running ? h('div', {class: 'progress', 'aria-label': 'Tiến độ'},
|
|
378
416
|
h('div', {class: 'bar', 'data-bar-start': task.started_at, 'data-bar-total': String((request?.timeout_seconds ?? 300) * 1000), role: 'presentation'}, h('span', {})),
|
|
379
417
|
h('div', {class: 'muted'}, elapsedEl(task.started_at, 'Đã chạy '), ` / giới hạn ${request?.timeout_seconds ?? '—'}s`),
|
|
380
|
-
h('div', {class: 'muted', id: 'activity-line'}, activityText(activity))) : null;
|
|
418
|
+
h('div', {class: 'muted', id: 'activity-line', title: ACTIVITY_HINT}, activityText(activity))) : null;
|
|
381
419
|
const facts = h('dl', {class: 'kv'},
|
|
382
|
-
...[['
|
|
420
|
+
...[['Thư mục làm việc', task.workspace], ['Worktree', task.worktree || '—'],
|
|
421
|
+
['Agent', agentName], ['Cách ly', task.isolation === 'worktree' ? 'worktree (song song)' : 'tuần tự'],
|
|
383
422
|
['Nhánh', task.branch || '—'], ['Quyền tự duyệt', task.autonomy || 'auto'],
|
|
384
423
|
['Gửi lúc', clock(task.queued_at || task.started_at)], ['Bắt đầu chạy', queued ? 'Chưa chạy' : clock(task.started_at)], ['Kết thúc', clock(task.ended_at)],
|
|
385
424
|
['Thời gian chạy', running ? elapsedEl(task.started_at) : queued ? '—' : duration(task.duration_ms)],
|
|
@@ -407,6 +446,7 @@ function detailView() {
|
|
|
407
446
|
task.denied_actions.length ? h('div', {}, h('div', {class: 'msg-head', text: 'Thao tác bị từ chối quyền'}),
|
|
408
447
|
h('div', {class: 'chips'}, task.denied_actions.map(a => h('span', {class: 'chip', text: a.display_name || a.action})))) : null,
|
|
409
448
|
scopeBody(task),
|
|
449
|
+
cleanupBody(task),
|
|
410
450
|
task.error ? h('div', {class: 'callout bad', text: task.error}) : null,
|
|
411
451
|
task.status === 'completed' ? h('div', {class: 'callout warn', text: 'CLI báo hoàn tất nhưng chưa được xác minh. Tech Lead cần xem diff thực tế và chạy test.'}) : null
|
|
412
452
|
];
|
|
@@ -418,10 +458,11 @@ function detailView() {
|
|
|
418
458
|
h('div', {id: 'log-note'}, task[`${state.logTab}_truncated`] ? h('div', {class: 'callout warn', text: 'Log đã bị cắt vì vượt giới hạn.'}) : null),
|
|
419
459
|
h('pre', {class: 'log', id: 'log-pre', text: logText(task)}));
|
|
420
460
|
|
|
421
|
-
return h('article', {class: 'card detail'}, head, progress,
|
|
461
|
+
return h('article', {class: 'card detail'}, head, progress, h('div', {class: 'convo'}, asked, replied),
|
|
422
462
|
changesPanel(task),
|
|
423
463
|
details('steps', `Các bước ${agentName} báo cáo`, h('div', {id: 'steps-body'}, stepsBody(state.detail?.activity))),
|
|
424
464
|
details('logs', `Log của tiến trình ${agentName}`, logs),
|
|
465
|
+
collapsible('more:fact', 'Xem thêm thông số task', facts),
|
|
425
466
|
prompt ? details('prompt', 'Prompt đầy đủ gửi tới agy (đã lọc secret)', h('pre', {class: 'log', text: prompt})) : null);
|
|
426
467
|
}
|
|
427
468
|
|
|
@@ -452,7 +493,7 @@ function modelCard(o) {
|
|
|
452
493
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: o.model || 'Mặc định của Antigravity'}),
|
|
453
494
|
h('span', {class: 'muted', text: o.model ? 'Bridge truyền --model này cho mọi task mới.' : 'Bridge không truyền --model; Antigravity tự dùng model đã cấu hình.'})), select),
|
|
454
495
|
loaded?.error ? h('div', {class: 'callout warn', text: `Không lấy được danh sách model: ${loaded.error}`}) : null,
|
|
455
|
-
h('p', {text: 'Áp dụng cho task mới, không cần khởi động lại. Task đang chạy hoặc đang chờ giữ model đã chốt lúc nhận. Danh sách lấy từ `agy models`; model được ghi lại trong từng task.'}));
|
|
496
|
+
more('more:model', h('p', {text: 'Áp dụng cho task mới, không cần khởi động lại. Task đang chạy hoặc đang chờ giữ model đã chốt lúc nhận. Danh sách lấy từ `agy models`; model được ghi lại trong từng task.'})));
|
|
456
497
|
}
|
|
457
498
|
function progressCard(o) {
|
|
458
499
|
const enabled = Boolean(o.progress?.enabled);
|
|
@@ -460,8 +501,10 @@ function progressCard(o) {
|
|
|
460
501
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Hiển thị tiến độ từng bước (stream-json)'}),
|
|
461
502
|
h('label', {class: 'perm-row' + (enabled ? ' on' : '')}, toggle,
|
|
462
503
|
h('span', {}, h('strong', {text: enabled ? 'Đang bật' : 'Đang tắt'}),
|
|
463
|
-
h('span', {class: 'muted', text: 'Bridge chạy agy
|
|
464
|
-
|
|
504
|
+
h('span', {class: 'muted', text: 'Bridge chạy agy ở --output-format stream-json; các bước hiện trong chi tiết task. Áp dụng cho task mới.'}))),
|
|
505
|
+
more('more:progress',
|
|
506
|
+
h('p', {text: 'Ngoài kết quả cuối, agy báo các sự kiện init và step_update khi đang làm việc (đã thấy bằng probe-stream trên agy 1.4.x). Các bước là bằng chứng "còn sống" thật cho phần gia hạn tự động.'}),
|
|
507
|
+
h('div', {class: 'callout warn', text: 'Chưa kiểm chứng: nội dung chi tiết bên trong step_update (nên hiển thị là tóm tắt tự động kèm dữ liệu gốc), và việc kết quả cuối ở chế độ này có cùng dạng với chế độ JSON. Nếu task báo lỗi "CLI output is not a valid stream-json final result" hoặc "stream result fields", hãy tắt tùy chọn này để quay về chế độ JSON đã kiểm chứng và gửi tôi thông báo lỗi.'})));
|
|
465
508
|
}
|
|
466
509
|
function budgetCard(o) {
|
|
467
510
|
const budget = o.budget || {task_tokens: null, day_tokens: null, today_tokens: 0};
|
|
@@ -475,7 +518,7 @@ function budgetCard(o) {
|
|
|
475
518
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Mức token (đèn cảnh báo)'}),
|
|
476
519
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: 'Mỗi task'}), h('span', {class: 'muted', text: 'Task nào dùng nhiều hơn mức này sẽ được đánh dấu "vượt mức" trong danh sách.'})), chooser('task_tokens', BUDGET_TASK_PRESETS, 'Mức token mỗi task')),
|
|
477
520
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: 'Mỗi ngày'}), h('span', {class: 'muted', text: `Hôm nay đã dùng ${number(budget.today_tokens)} token qua bridge${budget.over_day ? ' — đã vượt mức.' : '.'}`})), chooser('day_tokens', BUDGET_DAY_PRESETS, 'Mức token mỗi ngày')),
|
|
478
|
-
h('div', {class: 'callout warn', text: 'Đây chỉ là đèn báo, không phải hạn mức thật và không dừng task: agy không cho bridge đọc hạn mức, và số token của một task chỉ biết khi task kết thúc. Số liệu là tổng token (total_tokens) agy báo cho các task chạy qua bridge, tính theo ngày giờ máy của bạn.'}));
|
|
521
|
+
more('more:budget', h('div', {class: 'callout warn', text: 'Đây chỉ là đèn báo, không phải hạn mức thật và không dừng task: agy không cho bridge đọc hạn mức, và số token của một task chỉ biết khi task kết thúc. Số liệu là tổng token (total_tokens) agy báo cho các task chạy qua bridge, tính theo ngày giờ máy của bạn.'})));
|
|
479
522
|
}
|
|
480
523
|
function softTimeoutCard(o) {
|
|
481
524
|
const soft = o.soft_timeout || {enabled: false, limit_seconds: 7200};
|
|
@@ -488,9 +531,11 @@ function softTimeoutCard(o) {
|
|
|
488
531
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Gia hạn khi Antigravity còn hoạt động'}),
|
|
489
532
|
h('label', {class: 'perm-row' + (soft.enabled ? ' on' : '')}, toggle,
|
|
490
533
|
h('span', {}, h('strong', {text: soft.enabled ? 'Đang bật' : 'Đang tắt (task dừng đúng hạn)'}),
|
|
491
|
-
h('span', {class: 'muted', text: 'Đến hạn
|
|
534
|
+
h('span', {class: 'muted', text: 'Đến hạn, bridge kiểm tra agy còn làm việc không và gia hạn thay vì dừng ngay.'}))),
|
|
492
535
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: 'Giới hạn tuyệt đối'}), h('span', {class: 'muted', text: 'Dù còn hoạt động, task vẫn dừng khi tổng thời gian chạy tới mức này.'})), select),
|
|
493
|
-
|
|
536
|
+
more('more:soft-timeout',
|
|
537
|
+
h('p', {text: 'Cụ thể: khi tới hạn của task, bridge không dừng ngay mà kiểm tra xem agy còn hoạt động không (có byte mới ở output, hoặc file log của agy vừa được ghi). Nếu còn thì gia hạn rồi kiểm tra lại sau 1 phút, rồi 5 phút, rồi mỗi 10 phút; nếu không thấy hoạt động trong khoảng đó thì dừng task. Khi bật, bridge yêu cầu agy ghi log vào .bridge-state/agy-logs.'}),
|
|
538
|
+
h('div', {class: 'callout warn', text: 'Chưa kiểm chứng: tôi chưa biết agy có ghi log trong lúc suy luận ở chế độ --print. Bật rồi giao một task và xem dòng "log agy:" trong chi tiết task: nếu số KB tăng khi agy đang chạy thì tín hiệu này dùng được. Nếu log không tăng, task sẽ dừng đúng hạn như khi tắt tính năng (không tệ hơn).'})));
|
|
494
539
|
}
|
|
495
540
|
function waitCard(o) {
|
|
496
541
|
const wait = o.wait || {seconds: 55, client_tool_timeout_seconds: 115};
|
|
@@ -502,9 +547,10 @@ function waitCard(o) {
|
|
|
502
547
|
const minutes = Math.round(wait.client_tool_timeout_seconds / 60);
|
|
503
548
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Thời gian chờ mỗi lần Codex / Claude gọi (tiết kiệm token)'}),
|
|
504
549
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: `${wait.seconds} giây`}),
|
|
505
|
-
h('span', {class: 'muted', text: '
|
|
506
|
-
|
|
507
|
-
|
|
550
|
+
h('span', {class: 'muted', text: 'Client bị giữ trong một lệnh duy nhất tối đa mức này; càng dài càng ít lần hỏi lại. Có hiệu lực ngay, không cần khởi động lại.'})), select),
|
|
551
|
+
more('more:wait',
|
|
552
|
+
h('div', {class: 'callout warn'}, `Phải đặt thời hạn tool của client LỚN HƠN mức này, nếu không client sẽ cắt lần chờ và báo timeout. Với mức hiện tại: Codex, trong ~/.codex/config.toml mục [mcp_servers.antigravity] đặt `, h('code', {text: `tool_timeout_sec = ${wait.client_tool_timeout_seconds}`}), ` (khoảng ${minutes} phút), rồi mở lại Codex. Claude Code: biến môi trường MCP_TOOL_TIMEOUT (mili giây) lớn hơn ${wait.seconds * 1000}, theo tài liệu của Claude Code (chưa kiểm chứng ở đây). Lệnh \`agy-mcp-bridge setup codex\` in sẵn dòng này theo mức đang chọn.`),
|
|
553
|
+
h('p', {text: 'Khi giao task, Codex gọi chờ và bị giữ lại trong một lệnh duy nhất cho đến khi Antigravity xong hoặc hết thời gian này. Trong lúc bị giữ, Codex không tốn token. Nếu bạn chưa chắc về thời hạn tool của client, giữ mức 55 giây.'})));
|
|
508
554
|
}
|
|
509
555
|
function effortCard(o) {
|
|
510
556
|
const current = EFFORT_LEVELS.find(level => level.id === (o.effort || 'default')) || EFFORT_LEVELS[0];
|
|
@@ -517,14 +563,21 @@ function effortCard(o) {
|
|
|
517
563
|
}
|
|
518
564
|
function permissionCard(o) {
|
|
519
565
|
const p = o.permission || {level: 'default', controllable: true};
|
|
566
|
+
const current = PERMISSION_LEVELS.find(level => level.id === p.level) || PERMISSION_LEVELS[0];
|
|
567
|
+
const sandbox = h('div', {class: 'callout warn', text: 'Danh sách "Workspace được phép" chỉ kiểm tra đường dẫn của task, không phải sandbox. Với mức Tự động hoặc Toàn quyền, Antigravity có thể tác động ra ngoài thư mục đó.'});
|
|
568
|
+
const risky = p.level === 'auto' || p.level === 'full';
|
|
520
569
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Quyền của Antigravity'}),
|
|
521
|
-
h('p', {text: 'Mức quyền Antigravity có khi làm task. Chỉ bạn đổi được ở đây; Codex / Claude không đổi được. Áp dụng cho task mới, không cần khởi động lại. Hạ mức xuống thì áp dụng cả cho task đang chờ; nâng mức lên thì không.'}),
|
|
522
570
|
h('div', {class: 'perm-list', role: 'radiogroup', 'aria-label': 'Quyền của Antigravity'}, PERMISSION_LEVELS.map(level =>
|
|
523
571
|
h('label', {class: `perm-row${level.id === 'full' ? ' danger' : ''}${p.level === level.id ? ' on' : ''}`},
|
|
524
572
|
h('input', {type: 'radio', name: 'perm', value: level.id, checked: p.level === level.id, disabled: !p.controllable, onchange: () => setPermission(level.id)}),
|
|
525
|
-
h('span', {}, h('strong', {text: level.title})
|
|
573
|
+
h('span', {}, h('strong', {text: level.title}))))),
|
|
574
|
+
h('p', {text: current.text}),
|
|
575
|
+
risky ? sandbox : null,
|
|
526
576
|
p.controllable ? null : h('div', {class: 'callout warn', text: `Đang bị khóa bởi biến môi trường BRIDGE_AGY_PERMISSIONS${p.reason && p.reason !== 'BRIDGE_AGY_PERMISSIONS' ? ` (${p.reason})` : ''}. Đổi biến rồi khởi động lại server để mở.`}),
|
|
527
|
-
|
|
577
|
+
more('more:permission',
|
|
578
|
+
h('p', {text: 'Mức quyền Antigravity có khi làm task. Chỉ bạn đổi được ở đây; Codex / Claude không đổi được. Áp dụng cho task mới, không cần khởi động lại. Hạ mức xuống thì áp dụng cả cho task đang chờ; nâng mức lên thì không.'}),
|
|
579
|
+
h('ul', {class: 'list'}, PERMISSION_LEVELS.map(level => h('li', {}, h('strong', {text: `${level.title}: `}), level.text))),
|
|
580
|
+
risky ? null : sandbox));
|
|
528
581
|
}
|
|
529
582
|
function usageView(u) {
|
|
530
583
|
const head = h('div', {class: 'page-head'}, h('div', {}, h('h1', {text: 'Sử dụng'}), h('p', {text: 'Số liệu bridge ghi lại từ các task đã chạy qua nó.'})),
|
|
@@ -541,33 +594,38 @@ function usageView(u) {
|
|
|
541
594
|
const sums = Object.entries(t.usage);
|
|
542
595
|
const usageCard = h('section', {class: 'card panel wide'}, h('h2', {text: 'Usage do agy báo'}),
|
|
543
596
|
sums.length ? [h('dl', {class: 'rows'}, sums.map(([key, value]) => row(usageName(key), number(value)))),
|
|
544
|
-
h('p', {class: 'muted', text: `Cộng dồn từ ${number(t.with_usage)} task có trường usage trong kết quả của agy. Tên trường do agy quyết định; bridge cộng mọi giá trị số.`})]
|
|
597
|
+
more('more:usage-sums', h('p', {class: 'muted', text: `Cộng dồn từ ${number(t.with_usage)} task có trường usage trong kết quả của agy. Tên trường do agy quyết định; bridge cộng mọi giá trị số.`}))]
|
|
545
598
|
: h('p', {class: 'muted', text: 'Chưa có task nào mà kết quả của agy kèm số liệu usage. Với task chạy ở chế độ JSON hoặc stream, nếu agy trả trường usage thì tổng sẽ hiện ở đây.'}));
|
|
546
599
|
const peak = Math.max(1, ...u.by_day.map(d => d.tasks));
|
|
547
|
-
const
|
|
600
|
+
const bar = d => {
|
|
548
601
|
const fill = h('div', {class: 'bar-fill'}); fill.style.width = `${(d.tasks / peak * 100).toFixed(1)}%`;
|
|
549
602
|
return h('div', {class: 'bar-row'}, h('span', {class: 'muted mono', text: `${d.day.slice(8)}/${d.day.slice(5, 7)}`}), h('div', {class: 'bar-track'}, fill),
|
|
550
|
-
h('span', {class: 'mono', text:
|
|
551
|
-
}
|
|
552
|
-
const
|
|
603
|
+
h('span', {class: 'mono', text: `${d.tasks} · ${duration(d.duration_ms)}`}));
|
|
604
|
+
};
|
|
605
|
+
const active = u.by_day.filter(d => d.tasks);
|
|
606
|
+
const dayCard = h('section', {class: 'card panel wide'}, h('h2', {text: '14 ngày gần nhất'}),
|
|
607
|
+
active.length ? h('div', {class: 'bars'}, active.map(bar)) : h('p', {class: 'muted', text: 'Không có task nào trong 14 ngày gần nhất.'}),
|
|
608
|
+
active.length && active.length < u.by_day.length ? h('p', {class: 'muted', text: `${u.by_day.length - active.length} ngày còn lại không có task.`}) : null);
|
|
553
609
|
const groupTable = (title, groups, firstColumn) => h('section', {class: 'card panel wide'}, h('h2', {text: title}),
|
|
554
610
|
groups.length ? h('table', {class: 'usage-table'}, h('thead', {}, h('tr', {}, ...[firstColumn, 'Task', 'Hoàn tất', 'Thời gian', 'Usage'].map(c => h('th', {text: c})))),
|
|
555
|
-
h('tbody', {}, groups.map(g => h('tr', {}, h('td', {class: 'mono', text: g.key}), h('td', {text: number(g.tasks)}), h('td', {text: number(g.completed)}), h('td', {text: duration(g.duration_ms)}),
|
|
611
|
+
h('tbody', {}, groups.map(g => h('tr', {}, h('td', {class: 'mono', text: g.key}), h('td', {text: number(g.tasks)}), h('td', {text: number(g.completed)}), h('td', {text: duration(g.duration_ms)}),
|
|
612
|
+
h('td', {class: 'muted', title: usageLine(g.usage) ?? '', text: usageBrief(g.usage)})))))
|
|
556
613
|
: h('p', {class: 'muted', text: 'Chưa có dữ liệu.'}));
|
|
557
614
|
const recent = h('section', {class: 'card panel wide'}, h('h2', {text: 'Task gần nhất'}),
|
|
558
615
|
u.recent.length ? h('ul', {class: 'path-list'}, u.recent.map(r => h('li', {}, `${clock(r.at)} · ${STATUS[r.status] || r.status} · ${r.model || 'model mặc định'} · effort ${r.effort || 'mặc định'} · ${duration(r.duration_ms)}`,
|
|
559
|
-
|
|
560
|
-
return [head,
|
|
616
|
+
r.usage && Object.keys(r.usage).length ? h('span', {class: 'tag', title: usageLine(r.usage), text: usageBrief(r.usage)}) : null))) : h('p', {class: 'muted', text: 'Chưa có task nào.'}));
|
|
617
|
+
return [head, cards, usageCard, dayCard, groupTable('Theo model', u.by_model, 'Model'), groupTable('Theo mức suy luận', u.by_effort, 'Effort'), recent,
|
|
618
|
+
more('more:usage', callout)];
|
|
561
619
|
}
|
|
562
620
|
function settingsView(o) {
|
|
563
621
|
const delegation = o.delegation;
|
|
564
|
-
return [h('div', {class: 'page-head'}, h('div', {}, h('h1', {text: 'Cài đặt'}), h('p', {text: '
|
|
622
|
+
return [h('div', {class: 'page-head'}, h('div', {}, h('h1', {text: 'Cài đặt'}), h('p', {text: 'Mọi thứ đổi được ở đây áp dụng cho task mới, không cần khởi động lại. Giới hạn cứng vẫn nằm trong config/workspaces.json.'}))),
|
|
565
623
|
h('div', {class: 'settings'},
|
|
566
624
|
h('section', {class: 'card panel wide'}, h('h2', {text: 'Giao việc cho agent'}),
|
|
567
625
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: delegation.enabled ? 'Đang bật' : 'Đã tắt'}),
|
|
568
626
|
h('span', {class: 'muted', text: delegation.enabled ? 'Codex / Claude có thể giao task mới.' : `Task mới bị từ chối. ${delegation.reason || ''}`})),
|
|
569
627
|
switchControl(delegation.enabled, delegation.controllable, 'Cho phép giao việc cho Antigravity')),
|
|
570
|
-
delegation.controllable ? h('p', {text: 'Tắt để buộc Codex / Claude tự làm. Task đang chạy hoặc đang chờ không bị dừng, nhưng task đang chờ sẽ bị hủy khi đến lượt. Có hiệu lực ngay, không cần khởi động lại.'})
|
|
628
|
+
delegation.controllable ? more('more:delegation', h('p', {text: 'Tắt để buộc Codex / Claude tự làm. Task đang chạy hoặc đang chờ không bị dừng, nhưng task đang chờ sẽ bị hủy khi đến lượt. Có hiệu lực ngay, không cần khởi động lại.'}))
|
|
571
629
|
: h('div', {class: 'callout warn', text: 'Đang bị khóa bởi biến môi trường BRIDGE_DELEGATION. Đổi biến rồi khởi động lại server để mở.'})),
|
|
572
630
|
modelCard(o),
|
|
573
631
|
effortCard(o),
|
|
@@ -578,30 +636,33 @@ function settingsView(o) {
|
|
|
578
636
|
permissionCard(o),
|
|
579
637
|
h('section', {class: 'card panel'}, h('h2', {text: 'Kết nối'}),
|
|
580
638
|
h('div', {class: 'copy-row'}, h('code', {text: o.server.mcp_endpoint}), h('button', {class: 'btn small', onclick: () => copy(o.server.mcp_endpoint), text: 'Sao chép'})),
|
|
581
|
-
h('dl', {class: 'rows'}, row('
|
|
582
|
-
|
|
583
|
-
h('
|
|
639
|
+
h('dl', {class: 'rows'}, row('Phiên bản bridge', o.server.version)),
|
|
640
|
+
h('p', {class: 'callout warn', text: 'Không có xác thực: bất kỳ tiến trình nào chạy trên máy này với cùng user đều có thể điều khiển bridge. Không mở cổng ra ngoài.'}),
|
|
641
|
+
more('more:connect', h('dl', {class: 'rows'}, row('Giao thức', 'MCP Streamable HTTP'), row('Xác thực', 'Không — chỉ 127.0.0.1'), row('Khởi động', clock(o.server.started_at))))),
|
|
584
642
|
h('section', {class: 'card panel wide'}, h('h2', {text: 'Agent CLI'}),
|
|
585
643
|
h('div', {class: 'agent-list'}, (o.agents || []).length ? (o.agents || []).map(agent => h('div', {class: 'switch-row'},
|
|
586
644
|
h('div', {}, h('strong', {text: `${agent.label} · ${agent.id}`}),
|
|
587
|
-
h('span', {class: 'muted', text: [agent.enabled ? 'đang bật' : 'đã tắt', agent.version || 'không rõ phiên bản'
|
|
645
|
+
h('span', {class: 'muted', text: [agent.enabled ? 'đang bật' : 'đã tắt', agent.version || 'không rõ phiên bản'].filter(Boolean).join(' · ')})),
|
|
588
646
|
h('span', {class: `pill ${agent.enabled && agent.available ? 'ok' : agent.enabled ? 'bad' : 'warn'}`, text: !agent.enabled ? 'Tắt' : agent.available ? 'Sẵn sàng' : 'Không có'}))) : h('p', {class: 'muted', text: 'Chưa có agent nào được cấu hình.'})),
|
|
589
647
|
h('p', {class: 'muted', text: `Agent mặc định: ${o.default_agent || 'không có'}. Thứ tự ưu tiên đặt trong config/workspaces.json.`}),
|
|
590
|
-
(o.agents || []).some(agent => (agent.warnings || []).length) ? h('div', {class: 'callout warn', text: (o.agents || []).flatMap(agent => agent.warnings || []).join(' · ')}) : null
|
|
648
|
+
(o.agents || []).some(agent => (agent.warnings || []).length) ? h('div', {class: 'callout warn', text: (o.agents || []).flatMap(agent => agent.warnings || []).join(' · ')}) : null,
|
|
649
|
+
more('more:agents', h('ul', {class: 'list'}, (o.agents || []).map(agent => h('li', {},
|
|
650
|
+
h('strong', {text: `${agent.label}: `}), (agent.capabilities || []).join(', ') || 'không có capability nào'))))),
|
|
591
651
|
h('section', {class: 'card panel'}, h('h2', {text: 'Antigravity CLI'}),
|
|
592
|
-
h('dl', {class: 'rows'}, row('Trạng thái', o.cli.bridge_status), row('Phiên bản', o.cli.version)
|
|
593
|
-
|
|
594
|
-
h('
|
|
652
|
+
h('dl', {class: 'rows'}, row('Trạng thái', o.cli.bridge_status), row('Phiên bản', o.cli.version)),
|
|
653
|
+
h('p', {text: o.cli.authentication}),
|
|
654
|
+
more('more:cli', h('dl', {class: 'rows'}, row('Đường dẫn', o.cli.executable || '—'), row('Cách chạy', o.cli.execution_method || '—')))),
|
|
595
655
|
h('section', {class: 'card panel'}, h('h2', {text: 'Workspace được phép'}),
|
|
596
656
|
o.allowed_workspaces.length ? h('ul', {class: 'path-list'}, o.allowed_workspaces.map(p => h('li', {}, p, (o.session_workspaces || []).includes(p) ? h('span', {class: 'tag', text: 'chỉ lần chạy này'}) : null)))
|
|
597
657
|
: h('div', {class: 'callout warn', text: 'Chưa có workspace nào. Thêm đường dẫn tuyệt đối vào config/workspaces.json rồi khởi động lại.'})),
|
|
598
658
|
h('section', {class: 'card panel'}, h('h2', {text: 'Giới hạn'}),
|
|
599
|
-
h('
|
|
659
|
+
h('p', {text: `${o.limits.concurrent_normal_tasks ?? 1} task thường + ${o.limits.max_parallel_worktrees ?? o.concurrency?.max_parallel_worktrees ?? '—'} worktree cùng lúc, hàng đợi tối đa ${o.limits.max_queued_tasks ?? '—'}, timeout tối đa ${o.limits.max_timeout_seconds ?? '—'}s.`}),
|
|
660
|
+
more('more:limits', h('dl', {class: 'rows'}, row('Timeout tối đa', `${o.limits.max_timeout_seconds ?? '—'}s`), row('Output mỗi luồng', bytes(o.limits.max_output_bytes_per_stream ?? 0)),
|
|
600
661
|
row('Task lưu giữ', o.limits.max_retained_tasks ?? '—'), row('Task thường chạy cùng lúc', o.limits.concurrent_normal_tasks ?? 1),
|
|
601
662
|
row('Worktree chạy song song', o.limits.max_parallel_worktrees ?? o.concurrency?.max_parallel_worktrees ?? '—'),
|
|
602
663
|
row('Quyền tự duyệt mặc định', o.default_autonomy ?? 'auto'),
|
|
603
|
-
row('Hàng đợi tối đa', o.limits.max_queued_tasks ?? '—'))),
|
|
604
|
-
h('section', {class: 'card panel wide'},
|
|
664
|
+
row('Hàng đợi tối đa', o.limits.max_queued_tasks ?? '—')))),
|
|
665
|
+
h('section', {class: 'card panel wide'}, collapsible('more:limitations', 'Lưu ý khi dùng', h('ul', {class: 'list'}, o.limitations.map(item => h('li', {text: item}))))),
|
|
605
666
|
h('section', {class: 'card panel wide'}, h('h2', {text: 'Dừng bridge'}),
|
|
606
667
|
h('p', {text: 'Đóng tab không dừng server. Nút này tắt HTTP server, hủy task đang chạy hoặc đang chờ và dọn các tiến trình agent do bridge khởi tạo. Không thể hoàn tác.'}),
|
|
607
668
|
h('button', {class: 'btn danger', onclick: shutdownBridge, text: 'Tắt bridge'})))];
|