agy-mcp-bridge 0.1.14 → 0.1.15
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 +2 -0
- package/dist/src/budget.js +66 -0
- package/dist/src/tasks/manager.js +2 -2
- package/dist/src/ui/routes.js +22 -4
- package/dist/src/usage.js +9 -0
- package/package.json +1 -1
- package/ui/app.css +2 -0
- package/ui/app.js +31 -6
package/README.md
CHANGED
|
@@ -409,6 +409,8 @@ Lưu ý bảo mật: nội dung yêu cầu và phản hồi được hiển th
|
|
|
409
409
|
|
|
410
410
|
**Theo dõi mức sử dụng (tab "Sử dụng"):** kết quả JSON của `agy` có các trường `conversation_id`, `duration_seconds`, `num_turns`, `response`, `status` và `usage`. Bridge ghi lại, cho mỗi task đã chạy xong, thời gian chạy, thời gian `agy` báo, số lượt, số bước (khi bật tiến độ) và **mọi giá trị số trong `usage`** (giữ nguyên tên trường, trường lồng nhau nối bằng dấu chấm, ví dụ `cache.read_tokens`). Tab "Sử dụng" hiện tổng task (theo trạng thái), tổng thời gian, usage cộng dồn, biểu đồ 14 ngày (theo múi giờ của trình duyệt), bảng theo model và theo mức suy luận, và các task gần nhất; chi tiết mỗi task có dòng "Usage (agy báo)". Dữ liệu nằm ở `.bridge-state/usage.jsonl` (mỗi task một dòng, giữ qua các lần khởi động lại, tự cắt bớt khi file quá 1 MB, không ghi đường dẫn hay nội dung task) và `stats` trong bản ghi task.
|
|
411
411
|
|
|
412
|
+
**Mức token (đèn cảnh báo):** Cài đặt → "Mức token" cho đặt hai mức, mặc định **không giới hạn**: mỗi task và mỗi ngày (theo giờ máy của bạn). Danh sách task hiện số token (`total_tokens` agy báo, hoặc input + output nếu không có) và đánh dấu "vượt mức" task nào quá mức; thẻ "Token hôm nay" ở trang Task đổi sang "Vượt mức" khi tổng ngày chạm mức. Đâ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 có lệnh nào cho bridge đọc hạn mức, và token của một task chỉ biết khi nó kết thúc. Mức lưu ở `.bridge-state/budget.json`, đổi không cần khởi động lại.
|
|
413
|
+
|
|
412
414
|
- **Đây là mức dùng qua bridge, không phải hạn mức.** `agy` không có lệnh xem hạn mức còn lại và bridge không bịa ra con số đó; xem hạn mức trên tài khoản Antigravity của bạn.
|
|
413
415
|
- Bridge **không biết trước** các trường bên trong `usage` (tên trường do `agy` quyết định): nó cộng mọi giá trị số; một vài tên phổ biến (`input_tokens`, `output_tokens`, `total_tokens`...) được dịch nhãn. Task nào kết quả không có `usage` thì ghi nhận là "không có", không phải 0.
|
|
414
416
|
- Chỉ thống kê task bắt đầu từ bản có tính năng này trở đi. Số liệu usage không được gửi cho Codex/Claude (không tốn token).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readUsage, totalTokens, USAGE_FILE } from './usage.js';
|
|
5
|
+
/**
|
|
6
|
+
* Token budget: two limits the owner sets in the web UI, a warning light and nothing more. agy exposes no quota to read, and the token count of
|
|
7
|
+
* a task is only known when its result arrives, so the bridge cannot stop a task midway; it can show which finished tasks went over the per-task
|
|
8
|
+
* limit and whether today's total passed the daily limit. Read on every overview, so a change needs no restart.
|
|
9
|
+
*/
|
|
10
|
+
export const BUDGET_FILE = 'budget.json';
|
|
11
|
+
export const MIN_BUDGET_TOKENS = 1000;
|
|
12
|
+
export const MAX_BUDGET_TOKENS = 1_000_000_000;
|
|
13
|
+
export const isBudgetTokens = (value) => typeof value === 'number' && Number.isInteger(value) && value >= MIN_BUDGET_TOKENS && value <= MAX_BUDGET_TOKENS;
|
|
14
|
+
const valid = (value) => isBudgetTokens(value) ? value : null;
|
|
15
|
+
/** Anything missing, unreadable or out of range means "no limit". */
|
|
16
|
+
export function readBudget(stateDirectory) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(readFileSync(path.join(stateDirectory, BUDGET_FILE), 'utf8'));
|
|
19
|
+
return { task_tokens: valid(parsed.task_tokens), day_tokens: valid(parsed.day_tokens) };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { task_tokens: null, day_tokens: null };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function writeBudget(stateDirectory, value) {
|
|
26
|
+
const file = path.join(stateDirectory, BUDGET_FILE);
|
|
27
|
+
if (value.task_tokens === null && value.day_tokens === null) {
|
|
28
|
+
await rm(file, { force: true });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const limit of [value.task_tokens, value.day_tokens])
|
|
32
|
+
if (limit !== null && !isBudgetTokens(limit))
|
|
33
|
+
throw new Error(`limits must be whole numbers from ${MIN_BUDGET_TOKENS} to ${MAX_BUDGET_TOKENS}, or null for no limit`);
|
|
34
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
35
|
+
await writeFile(`${file}.tmp`, JSON.stringify(value) + '\n');
|
|
36
|
+
await rename(`${file}.tmp`, file);
|
|
37
|
+
}
|
|
38
|
+
/** Tokens used on the viewer's current local day (`tzOffsetMinutes` as `getTimezoneOffset()` returns it). */
|
|
39
|
+
export function tokensToday(records, now, tzOffsetMinutes) {
|
|
40
|
+
const offsetMs = -tzOffsetMinutes * 60000;
|
|
41
|
+
const dayOf = (ms) => new Date(ms + offsetMs).toISOString().slice(0, 10);
|
|
42
|
+
const today = dayOf(now);
|
|
43
|
+
return records.reduce((total, record) => dayOf(Date.parse(record.at)) === today ? total + (totalTokens(record.usage) ?? 0) : total, 0);
|
|
44
|
+
}
|
|
45
|
+
// The overview is polled every few seconds; the ledger is re-read only when its size or time changed.
|
|
46
|
+
let cache = null;
|
|
47
|
+
async function cachedRecords(stateDirectory) {
|
|
48
|
+
const file = path.join(stateDirectory, USAGE_FILE);
|
|
49
|
+
let key;
|
|
50
|
+
try {
|
|
51
|
+
const info = await stat(file);
|
|
52
|
+
key = `${file}:${info.size}:${info.mtimeMs}`;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
if (cache?.key !== key)
|
|
58
|
+
cache = { key, records: await readUsage(stateDirectory) };
|
|
59
|
+
return cache.records;
|
|
60
|
+
}
|
|
61
|
+
export async function budgetState(stateDirectory, tzOffsetMinutes, now = Date.now()) {
|
|
62
|
+
const budget = readBudget(stateDirectory);
|
|
63
|
+
const today = tokensToday(await cachedRecords(stateDirectory), now, tzOffsetMinutes);
|
|
64
|
+
return { ...budget, today_tokens: today, over_day: budget.day_tokens !== null && today >= budget.day_tokens };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -9,7 +9,7 @@ import { redact } from '../security/redact.js';
|
|
|
9
9
|
import { delegationState } from '../delegation.js';
|
|
10
10
|
import { startProcess, terminateOwned } from '../antigravity/process.js';
|
|
11
11
|
import { invocationFor, finalEnvelope } from '../antigravity/invocation.js';
|
|
12
|
-
import { appendUsage, extractStats } from '../usage.js';
|
|
12
|
+
import { appendUsage, extractStats, totalTokens } from '../usage.js';
|
|
13
13
|
import { MAX_RECENT_STEPS, rawStep, readProgress, summarizeStep } from '../progress.js';
|
|
14
14
|
import { helperDirectory } from '../paths.js';
|
|
15
15
|
import { readWait } from '../wait-setting.js';
|
|
@@ -425,7 +425,7 @@ export class TaskManager {
|
|
|
425
425
|
/** All retained tasks, newest first, without logs. */
|
|
426
426
|
list() {
|
|
427
427
|
return [...this.entries.entries()]
|
|
428
|
-
.map(([id, entry]) => ({ ...this.summary(id), title: entry.request?.task.split('\n').find(line => line.trim())?.trim().slice(0, 140) ?? null }))
|
|
428
|
+
.map(([id, entry]) => ({ ...this.summary(id), title: entry.request?.task.split('\n').find(line => line.trim())?.trim().slice(0, 140) ?? null, tokens: totalTokens(entry.stats?.usage) }))
|
|
429
429
|
.sort((a, b) => Date.parse(b.queued_at ?? b.started_at) - Date.parse(a.queued_at ?? a.started_at));
|
|
430
430
|
}
|
|
431
431
|
/** Models offered by the installed Antigravity CLI (read on demand, never at startup). */
|
package/dist/src/ui/routes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { validModelId, writeModel } from '../models.js';
|
|
|
10
10
|
import { readProgress, writeProgress } from '../progress.js';
|
|
11
11
|
import { readUsage, summarizeUsage } from '../usage.js';
|
|
12
12
|
import { EFFORT_LEVELS, isEffortLevel, readEffort, writeEffort } from '../effort.js';
|
|
13
|
+
import { MAX_BUDGET_TOKENS, MIN_BUDGET_TOKENS, budgetState, isBudgetTokens, readBudget, writeBudget } from '../budget.js';
|
|
13
14
|
import { MAX_ABSOLUTE_LIMIT_SECONDS, MIN_ABSOLUTE_LIMIT_SECONDS, isAbsoluteLimit, readSoftTimeout, writeSoftTimeout } from '../soft-timeout.js';
|
|
14
15
|
import { MAX_SETTING_WAIT_SECONDS, MIN_WAIT_SECONDS, clientToolTimeoutSeconds, isWaitSeconds, writeWait } from '../wait-setting.js';
|
|
15
16
|
import { PERMISSION_LEVELS, isPermissionLevel, needsAcknowledgement, permissionState, writePermission } from '../permissions.js';
|
|
@@ -44,7 +45,7 @@ async function readJson(req) {
|
|
|
44
45
|
throw new Error('Expected a JSON object');
|
|
45
46
|
return value;
|
|
46
47
|
}
|
|
47
|
-
async function overview(ctx) {
|
|
48
|
+
async function overview(ctx, tzOffsetMinutes = 0) {
|
|
48
49
|
const env = ctx.environment;
|
|
49
50
|
const state = delegationState(ctx.manager.stateDirectory);
|
|
50
51
|
return {
|
|
@@ -62,6 +63,7 @@ async function overview(ctx) {
|
|
|
62
63
|
permission: permissionState(ctx.manager.stateDirectory),
|
|
63
64
|
effort: readEffort(ctx.manager.stateDirectory),
|
|
64
65
|
soft_timeout: readSoftTimeout(ctx.manager.stateDirectory),
|
|
66
|
+
budget: await budgetState(ctx.manager.stateDirectory, tzOffsetMinutes),
|
|
65
67
|
progress: readProgress(ctx.manager.stateDirectory),
|
|
66
68
|
wait: { seconds: ctx.manager.maxWaitSeconds, client_tool_timeout_seconds: clientToolTimeoutSeconds(ctx.manager.maxWaitSeconds) },
|
|
67
69
|
queue: ctx.manager.overview().queue,
|
|
@@ -141,13 +143,29 @@ export async function handleUi(req, res, ctx) {
|
|
|
141
143
|
json(res, 200, { ok: true });
|
|
142
144
|
return true;
|
|
143
145
|
}
|
|
146
|
+
const tzParam = Number(new URL(req.url ?? '', 'http://localhost').searchParams.get('tz') ?? '0');
|
|
147
|
+
const tzOffset = Number.isInteger(tzParam) && Math.abs(tzParam) <= 840 ? tzParam : 0;
|
|
144
148
|
if (pathname === '/ui/api/overview' && method === 'GET') {
|
|
145
|
-
json(res, 200, await overview(ctx));
|
|
149
|
+
json(res, 200, await overview(ctx, tzOffset));
|
|
146
150
|
return true;
|
|
147
151
|
}
|
|
148
152
|
if (pathname === '/ui/api/usage' && method === 'GET') {
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
json(res, 200, summarizeUsage(await readUsage(ctx.manager.stateDirectory), { tzOffsetMinutes: tzOffset }));
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
if (pathname === '/ui/api/budget' && method === 'POST') {
|
|
157
|
+
const body = await readJson(req);
|
|
158
|
+
const current = readBudget(ctx.manager.stateDirectory);
|
|
159
|
+
const pick = (key) => body[key] === undefined ? current[key] : body[key];
|
|
160
|
+
const next = { task_tokens: pick('task_tokens'), day_tokens: pick('day_tokens') };
|
|
161
|
+
for (const limit of [next.task_tokens, next.day_tokens]) {
|
|
162
|
+
if (limit !== null && !isBudgetTokens(limit)) {
|
|
163
|
+
json(res, 400, { error: `limits must be whole numbers from ${MIN_BUDGET_TOKENS} to ${MAX_BUDGET_TOKENS}, or null for no limit` });
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await writeBudget(ctx.manager.stateDirectory, next);
|
|
168
|
+
json(res, 200, await budgetState(ctx.manager.stateDirectory, tzOffset));
|
|
151
169
|
return true;
|
|
152
170
|
}
|
|
153
171
|
const detail = pathname.match(new RegExp(`^/ui/api/tasks/(${UUID})$`));
|
package/dist/src/usage.js
CHANGED
|
@@ -117,4 +117,13 @@ export function summarizeUsage(records, options = {}) {
|
|
|
117
117
|
recent: records.slice(-15).reverse().map(r => ({ at: r.at, status: r.status, model: r.model, effort: r.effort, duration_ms: r.duration_ms, agy_duration_seconds: r.agy_duration_seconds, num_turns: r.num_turns, usage: r.usage }))
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
/** Tokens a task cost according to agy: its own total when it reports one, else input plus output; null when it reported neither. */
|
|
121
|
+
export function totalTokens(usage) {
|
|
122
|
+
if (!usage)
|
|
123
|
+
return null;
|
|
124
|
+
if (typeof usage.total_tokens === 'number')
|
|
125
|
+
return usage.total_tokens;
|
|
126
|
+
const parts = [usage.input_tokens, usage.output_tokens].filter((value) => typeof value === 'number');
|
|
127
|
+
return parts.length ? parts.reduce((total, value) => total + value, 0) : null;
|
|
128
|
+
}
|
|
120
129
|
//# sourceMappingURL=usage.js.map
|
package/package.json
CHANGED
package/ui/app.css
CHANGED
|
@@ -226,3 +226,5 @@ pre.log { margin: 0; padding: 12px; border-radius: 10px; background: #0b0e14; co
|
|
|
226
226
|
.usage-table { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
|
227
227
|
.usage-table th, .usage-table td { padding: 7px 8px; text-align: left; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
|
|
228
228
|
.usage-table th { font-weight: 600; color: var(--muted); }
|
|
229
|
+
|
|
230
|
+
.over-budget { color: var(--bad); font-weight: 600; }
|
package/ui/app.js
CHANGED
|
@@ -143,7 +143,7 @@ async function login(token) {
|
|
|
143
143
|
|
|
144
144
|
async function refresh(force) {
|
|
145
145
|
try {
|
|
146
|
-
state.overview = await api(
|
|
146
|
+
state.overview = await api(`overview?tz=${new Date().getTimezoneOffset()}`); state.connected = true;
|
|
147
147
|
const tasks = state.overview.tasks;
|
|
148
148
|
if (!state.selected || !tasks.some(task => task.task_id === state.selected)) state.selected = tasks[0]?.task_id ?? null;
|
|
149
149
|
state.detail = state.selected ? await api(`tasks/${state.selected}`).catch(() => null) : null;
|
|
@@ -241,6 +241,12 @@ async function setSoftTimeout(enabled, limit_seconds) {
|
|
|
241
241
|
await refresh();
|
|
242
242
|
} catch (error) { notify(error.message); await refresh(); }
|
|
243
243
|
}
|
|
244
|
+
const BUDGET_TASK_PRESETS = [null, 50000, 100000, 200000, 500000, 1000000];
|
|
245
|
+
const BUDGET_DAY_PRESETS = [null, 200000, 500000, 1000000, 2000000, 5000000];
|
|
246
|
+
async function setBudget(body) {
|
|
247
|
+
try { await api(`budget?tz=${new Date().getTimezoneOffset()}`, {method: 'POST', body}); notify('Đã lưu mức token'); await refresh(); }
|
|
248
|
+
catch (error) { notify(error.message); await refresh(); }
|
|
249
|
+
}
|
|
244
250
|
async function setProgress(enabled) {
|
|
245
251
|
try { await api('progress', {method: 'POST', body: {enabled}}); notify(enabled ? 'Đã bật tiến độ từng bước (áp dụng cho task mới)' : 'Đã tắt tiến độ từng bước'); await refresh(); }
|
|
246
252
|
catch (error) { notify(error.message); await refresh(); }
|
|
@@ -294,6 +300,8 @@ function statsView(o) {
|
|
|
294
300
|
stat('Antigravity CLI', o.cli.version && o.cli.version !== 'unavailable' ? `agy ${o.cli.version}` : 'Không tìm thấy', o.cli.execution_method || 'Chưa thể giao task'),
|
|
295
301
|
stat('Giao việc', [switchControl(o.delegation.enabled, o.delegation.controllable, 'Cho phép giao việc cho Antigravity'),
|
|
296
302
|
h('span', {class: `pill ${o.delegation.enabled ? 'ok' : 'warn'}`, text: o.delegation.enabled ? 'Đang bật' : 'Đã tắt'})], o.delegation.reason || 'Codex / Claude được phép giao task'),
|
|
303
|
+
stat('Token hôm nay', [number(o.budget?.today_tokens ?? 0), o.budget?.day_tokens ? h('span', {class: `pill ${o.budget.over_day ? 'bad' : 'ok'}`, text: o.budget.over_day ? 'Vượt mức' : 'Trong mức'}) : null],
|
|
304
|
+
o.budget?.day_tokens ? `mức ngày ${number(o.budget.day_tokens)}${o.budget.over_day ? ' — đã vượt' : ''}` : 'chưa đặt mức ngày (Cài đặt)'),
|
|
297
305
|
stat('Task', `${count('running')} đang chạy`, `${queued ? `${queued} đang chờ · ` : ''}${o.tasks.length} task gần đây`)
|
|
298
306
|
];
|
|
299
307
|
}
|
|
@@ -302,6 +310,7 @@ function filteredTasks(o) {
|
|
|
302
310
|
const q = state.q.trim().toLowerCase();
|
|
303
311
|
return o.tasks.filter(task => MATCH[state.filter](task) && (!q || [task.title, task.workspace, task.task_id].some(v => (v || '').toLowerCase().includes(q))));
|
|
304
312
|
}
|
|
313
|
+
const overTask = (o, task) => Boolean(o.budget?.task_tokens) && task.tokens !== null && task.tokens !== undefined && task.tokens > o.budget.task_tokens;
|
|
305
314
|
function taskItems(o) {
|
|
306
315
|
if (!o.tasks.length) return [h('div', {class: 'card empty'}, h('strong', {text: 'Chưa có task nào'}), 'Khi Codex hoặc Claude giao việc cho Antigravity, task sẽ hiện ở đây.')];
|
|
307
316
|
const tasks = filteredTasks(o);
|
|
@@ -311,7 +320,8 @@ function taskItems(o) {
|
|
|
311
320
|
}, h('div', {class: 'row'}, pill(task.status), agoEl(task.queued_at || task.started_at, 'meta')),
|
|
312
321
|
h('div', {class: 'title', text: task.title || '(không có nội dung yêu cầu)'}),
|
|
313
322
|
h('div', {class: 'meta mono', text: `${folder(task.workspace)} · ${task.task_id.slice(0, 8)}`}),
|
|
314
|
-
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)}
|
|
323
|
+
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)}`,
|
|
324
|
+
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)));
|
|
315
325
|
}
|
|
316
326
|
function taskCount(o) { return `${filteredTasks(o).length}/${o.tasks.length} task`; }
|
|
317
327
|
|
|
@@ -375,7 +385,7 @@ function detailView() {
|
|
|
375
385
|
const facts = h('dl', {class: 'kv'},
|
|
376
386
|
...[['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)],
|
|
377
387
|
['Thời gian chạy', running ? elapsedEl(task.started_at) : queued ? '—' : duration(task.duration_ms)],
|
|
378
|
-
['Giới hạn', request ? `${request.timeout_seconds}s` : '—'], ['Model', request && 'model' in request ? request.model || 'mặc định' : '—'], ['Quyền', request && request.permission ? (PERMISSION_LEVELS.find(l => l.id === request.permission)?.title || request.permission) : '—'], ['Effort', request && request.effort ? request.effort : '—'], ['Usage (agy báo)', usageLine(state.detail?.stats?.usage) ?? '—'], ['Exit code', task.exit_code ?? '—'], ['PID', task.pid ?? '—'],
|
|
388
|
+
['Giới hạn', request ? `${request.timeout_seconds}s` : '—'], ['Model', request && 'model' in request ? request.model || 'mặc định' : '—'], ['Quyền', request && request.permission ? (PERMISSION_LEVELS.find(l => l.id === request.permission)?.title || request.permission) : '—'], ['Effort', request && request.effort ? request.effort : '—'], ['Tổng token', state.detail?.stats?.usage && 'total_tokens' in state.detail.stats.usage ? number(state.detail.stats.usage.total_tokens) : '—'], ['Usage (agy báo)', usageLine(state.detail?.stats?.usage) ?? '—'], ['Exit code', task.exit_code ?? '—'], ['PID', task.pid ?? '—'],
|
|
379
389
|
['CLI status', task.cli_status ?? '—']].map(([k, v]) => h('div', {}, h('dt', {text: k}), h('dd', {}, typeof v === 'string' || typeof v === 'number' ? String(v) : v))));
|
|
380
390
|
|
|
381
391
|
const asker = request?.client ? `Tech Lead · ${request.client}` : 'Tech Lead (Codex / Claude)';
|
|
@@ -452,6 +462,20 @@ function progressCard(o) {
|
|
|
452
462
|
h('span', {class: 'muted', text: 'Bridge chạy agy với --output-format stream-json: 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 hiện trong chi tiết task, và là bằng chứng "còn sống" thật cho phần gia hạn. Áp dụng cho task mới, không cần khởi động lại.'}))),
|
|
453
463
|
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.'}));
|
|
454
464
|
}
|
|
465
|
+
function budgetCard(o) {
|
|
466
|
+
const budget = o.budget || {task_tokens: null, day_tokens: null, today_tokens: 0};
|
|
467
|
+
const chooser = (key, presets, label) => {
|
|
468
|
+
const options = presets.includes(budget[key]) ? presets : [...presets, budget[key]];
|
|
469
|
+
const select = h('select', {class: 'select', 'aria-label': label, onchange: event => setBudget({[key]: event.target.value === '' ? null : Number(event.target.value)})},
|
|
470
|
+
...options.map(value => h('option', {value: value === null ? '' : String(value), text: value === null ? 'Không giới hạn' : `${number(value)} token`})));
|
|
471
|
+
select.value = budget[key] === null ? '' : String(budget[key]);
|
|
472
|
+
return select;
|
|
473
|
+
};
|
|
474
|
+
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Mức token (đèn cảnh báo)'}),
|
|
475
|
+
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')),
|
|
476
|
+
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')),
|
|
477
|
+
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.'}));
|
|
478
|
+
}
|
|
455
479
|
function softTimeoutCard(o) {
|
|
456
480
|
const soft = o.soft_timeout || {enabled: false, limit_seconds: 7200};
|
|
457
481
|
const known = SOFT_LIMITS.some(l => l.seconds === soft.limit_seconds);
|
|
@@ -548,6 +572,7 @@ function settingsView(o) {
|
|
|
548
572
|
effortCard(o),
|
|
549
573
|
waitCard(o),
|
|
550
574
|
progressCard(o),
|
|
575
|
+
budgetCard(o),
|
|
551
576
|
softTimeoutCard(o),
|
|
552
577
|
permissionCard(o),
|
|
553
578
|
h('section', {class: 'card panel'}, h('h2', {text: 'Kết nối'}),
|
|
@@ -622,8 +647,8 @@ function patch() {
|
|
|
622
647
|
o.permission && (o.permission.level === 'full' || o.permission.level === 'auto') ? h('div', {class: 'banner warn', role: 'alert', text: o.permission.level === 'full' ? 'Antigravity đang ở mức TOÀN QUYỀN: mọi thao tác được tự duyệt, kể cả ngoài thư mục dự án. Đổi lại ở Cài đặt.' : 'Antigravity đang ở chế độ Tự động: nó tự quyết định hành động. Đổi lại ở Cài đặt.'}) : null,
|
|
623
648
|
o.update ? h('div', {class: 'banner info', role: 'status'}, `Có bản mới ${o.update.package} ${o.update.latest} (đang chạy ${o.update.current}). Dừng server (Ctrl+C) rồi chạy `, h('code', {text: o.update.command})) : null].filter(Boolean));
|
|
624
649
|
if (regions.view === 'tasks') {
|
|
625
|
-
setRegion('stats', JSON.stringify([o.tasks.map(t => [t.task_id, t.status]), o.cli, o.delegation, o.server.accepting_tasks]), () => statsView(o));
|
|
626
|
-
setRegion('list', JSON.stringify([state.q, state.filter, state.selected, o.tasks.map(t => stable(t))]), () => taskItems(o));
|
|
650
|
+
setRegion('stats', JSON.stringify([o.tasks.map(t => [t.task_id, t.status]), o.budget, o.cli, o.delegation, o.server.accepting_tasks]), () => statsView(o));
|
|
651
|
+
setRegion('list', JSON.stringify([state.q, state.filter, state.selected, o.budget?.task_tokens, o.tasks.map(t => stable(t))]), () => taskItems(o));
|
|
627
652
|
regions.count.textContent = taskCount(o);
|
|
628
653
|
const d = state.detail;
|
|
629
654
|
const detailKey = JSON.stringify([state.selected, state.logTab, state.changes, d && {...d, activity: null, task: {...stable(d.task), stdout: '', stderr: '', stdout_bytes: 0, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false}}]);
|
|
@@ -637,7 +662,7 @@ function patch() {
|
|
|
637
662
|
setRegion('usage', JSON.stringify(state.usage), () => usageView(state.usage));
|
|
638
663
|
} else {
|
|
639
664
|
if (!state.models && !state.modelsLoading) loadModels();
|
|
640
|
-
setRegion('settings', JSON.stringify([state.models, o.model, o.permission, o.effort, o.wait, o.soft_timeout, o.progress, o.cli, o.delegation, o.limits, o.allowed_workspaces, o.session_workspaces, o.server.mcp_endpoint]), () => settingsView(o));
|
|
665
|
+
setRegion('settings', JSON.stringify([state.models, o.model, o.permission, o.effort, o.wait, o.budget && [o.budget.task_tokens, o.budget.day_tokens], o.soft_timeout, o.progress, o.cli, o.delegation, o.limits, o.allowed_workspaces, o.session_workspaces, o.server.mcp_endpoint]), () => settingsView(o));
|
|
641
666
|
}
|
|
642
667
|
tick();
|
|
643
668
|
}
|