agy-mcp-bridge 0.1.8 → 0.1.10
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 +11 -3
- package/dist/src/config.js +5 -3
- package/dist/src/effort.js +32 -0
- package/dist/src/models.js +19 -8
- package/dist/src/runtime.js +1 -1
- package/dist/src/security/input.js +5 -4
- package/dist/src/tasks/manager.js +7 -3
- package/dist/src/tools/register.js +3 -3
- package/dist/src/ui/routes.js +12 -0
- package/docs/SKILL-EVAL.md +5 -0
- package/package.json +1 -1
- package/skills/antigravity-bridge/SKILL.md +19 -3
- package/skills/known-versions.json +12 -0
- package/ui/app.js +24 -2
package/README.md
CHANGED
|
@@ -226,7 +226,7 @@ Luồng sử dụng trong Codex hoặc Claude Code:
|
|
|
226
226
|
|
|
227
227
|
1. Đọc code và Git status trước task; chia task nhỏ, rõ file được sửa và tiêu chí nghiệm thu.
|
|
228
228
|
2. Gọi `antigravity_delegate` với arguments mẫu ở dưới.
|
|
229
|
-
3. Lưu `task_id`; gọi `antigravity_task_wait` (tối đa
|
|
229
|
+
3. Lưu `task_id`; gọi `antigravity_task_wait` (mỗi lần chờ tối đa `max_wait_seconds`, mặc định 55 giây, trả sớm khi task xong) hoặc `antigravity_task_status`, rồi lấy `antigravity_task_output`.
|
|
230
230
|
4. Nếu client bị đóng, bật lại client, gọi environment để xem `recent_tasks`, rồi query task ID cũ. Không giao trùng task khi chưa kiểm tra trạng thái.
|
|
231
231
|
5. Tech Lead review diff thực tế, gồm file untracked, chạy test và đánh giá từng tiêu chí. Nếu chưa đạt, giao task sửa tiếp.
|
|
232
232
|
|
|
@@ -244,12 +244,16 @@ Luồng sử dụng trong Codex hoặc Claude Code:
|
|
|
244
244
|
| `antigravity_environment` | CLI/version/allowlist/giới hạn và recent_tasks để tìm lại task ID |
|
|
245
245
|
| `antigravity_delegate` | Nhận task, workspace, acceptance_criteria, context?, timeout_seconds?; trả `running` (hoặc `queued` nếu bridge đang bận và hàng đợi còn chỗ) cùng task_id |
|
|
246
246
|
| `antigravity_task_status` | Trạng thái/process/thời gian/lỗi/denied_actions/số byte theo task_id, không kèm log (rẻ để poll) |
|
|
247
|
-
| `antigravity_task_wait` | Chờ tối đa `wait_seconds` (1
|
|
247
|
+
| `antigravity_task_wait` | Chờ tối đa `wait_seconds` (1 đến `max_wait_seconds`, mặc định là mức tối đa, 55 giây) tới khi task kết thúc, trả cùng nội dung như status; còn chạy thì gọi lại |
|
|
248
248
|
| `antigravity_task_output` | Phản hồi cuối, stdout/stderr, lỗi, denied_actions, truncation theo task_id |
|
|
249
249
|
| `antigravity_cancel_task` | Hủy cây tiến trình thuộc task; không rollback thay đổi file |
|
|
250
250
|
|
|
251
251
|
`completed` chỉ là CLI chạy xong và có envelope hợp lệ. Luôn `review_required=true`, `acceptance_verified=false`. SUCCESS/exit 0 vẫn có thể bị bridge đánh dấu failed do permission denial hoặc timeout. Prompt mẫu review/sửa tiếp: [docs/PROMPTS.md](docs/PROMPTS.md).
|
|
252
252
|
|
|
253
|
+
### Để Codex/Claude tự chia việc dễ khó (chế độ tùy chọn)
|
|
254
|
+
|
|
255
|
+
Mặc định agent chỉ giao việc khi bạn nhắc Antigravity. Nếu bạn muốn agent tự quyết định, hãy nói rõ trong cuộc trò chuyện, ví dụ "tự phân loại việc: việc dễ giao Antigravity, việc khó bạn tự làm". Khi đó skill cho phép agent giao một phần việc **chỉ khi tất cả đều đúng** (mặc định, bạn có thể nêu ngưỡng riêng): một mục tiêu và tối đa 3 file kèm test; tiêu chí nghiệm thu cụ thể, kiểm tra được bằng diff hoặc test; không phụ thuộc ngữ cảnh ngoài repo hay suy luận dài; không thuộc bảo mật, xác thực, secret, di chuyển hoặc xóa dữ liệu, triển khai, phụ thuộc/build/CI, hợp đồng API công khai; và `agy_permissions` đủ quyền cho việc đó. Thiết kế, nhiều file phụ thuộc nhau, lỗi chưa rõ nguyên nhân, yêu cầu mơ hồ, hoặc khi không chắc thì agent tự làm. Việc hỗn hợp được tách: agent tự làm phần khó, giao từng phần dễ độc lập, nói một dòng lý do trước khi giao và liệt kê ai làm phần nào ở báo cáo cuối. Mọi diff giao đi vẫn được xem lại; task giao đi lỗi, hết giờ hoặc kém thì agent tự làm tiếp thay vì gửi lại nhiều lần. Bạn kết thúc chế độ bất cứ lúc nào bằng cách bảo agent tự làm hết; công tắc giao việc và lời từ chối của bạn luôn có hiệu lực trước. Đây là chỉ dẫn cho agent, không phải rào chắn kỹ thuật, và việc phân loại dựa trên phán đoán của agent. Các tình huống kiểm tra thủ công ở [docs/SKILL-EVAL.md](docs/SKILL-EVAL.md) (11 đến 15).
|
|
256
|
+
|
|
253
257
|
### Khi chỉ muốn Codex/Claude tự làm
|
|
254
258
|
|
|
255
259
|
- **Công tắc cứng (khuyến nghị):** `npm run delegation -- off` chặn mọi lần giao task mới ngay lập tức, không cần restart hay token. `npm run delegation -- on` bật lại, `-- status` xem trạng thái. Có thể đặt `BRIDGE_DELEGATION=off` trước `npm start` (giá trị khác `on`/`off` cũng bị coi là tắt). Khi tắt, `antigravity_delegate` trả lỗi nêu rõ "disabled by the bridge owner" và `antigravity_environment` trả `delegation: {enabled: false, reason}`. Task đang chạy không bị dừng và vẫn xem/hủy được.
|
|
@@ -273,7 +277,7 @@ Nhấn **Ctrl+C một lần** tại terminal server:
|
|
|
273
277
|
|
|
274
278
|
Nếu cleanup thất bại, báo lỗi và giữ execution lock khi process chưa đóng. Khi khởi động hoặc lấy lock, bridge chỉ tự thu hồi nếu đọc được owner hợp lệ, cả `server_pid` lẫn `child_pid` đều đã chết và record khớp lock. Lệnh `agy-mcp-bridge unlock` dùng cùng kiểm tra; PID còn sống, thiếu thông tin hoặc quyền sở hữu không rõ thì giữ lock và in lý do. Bridge không kill process Antigravity khác. PID có thể được tái sử dụng nên trường hợp không rõ cần kiểm tra thủ công. Force kill/OS crash vẫn không bảo đảm cleanup: chưa có Windows Job Object. POSIX gửi SIGTERM cho nhóm process, chờ tối đa 8 giây rồi SIGKILL; Windows giữ taskkill cây PID. Ctrl+C tiếp trong lúc cleanup chỉ báo `cleanup in progress, please wait`.
|
|
275
279
|
|
|
276
|
-
Giới hạn mặc định: CLI task 300s, tối đa 900s; watchdog của bridge cộng thêm 10s để CLI tự báo timeout trước. Output mỗi stream 256 KiB (cấu hình 1 KiB–1 MiB), vượt trần sẽ hủy task và trả failed/truncated. Tổng task JSON vẫn tối đa 24000 ký tự; HTTP body tối đa 128 KiB. Prompt mặc định đi qua argv với print/json (`prompt_transport: "argv"`, đường đã chạy thật với `agy` 1.4.3). Đặt `prompt_transport: "stdin"` để gửi qua stdin stream-json, không lộ prompt trong danh sách tiến trình; chế độ này chưa được chạy thử với `agy` thật, và stream gồm cả init/progress nên có thể chạm output cap sớm hơn. Snapshot log được cập nhật tối đa mỗi giây và flush khi kết thúc. Không dùng CLI detach/remote-control API chưa được xác minh.
|
|
280
|
+
Giới hạn mặc định: CLI task 300s, tối đa 900s; watchdog của bridge cộng thêm 10s để CLI tự báo timeout trước. Chủ máy nâng mức tối đa (và mặc định) lên tới 3600s bằng `max_timeout_seconds` / `default_timeout_seconds` trong file cấu hình (khởi động lại bridge); mỗi task đặt `timeout_seconds` riêng, không vượt mức tối đa đó. Task hết thời hạn không bị hoàn tác: file đã sửa trước đó vẫn nằm trên đĩa (xem "Thay đổi trong workspace" hoặc `git diff`), trạng thái là `timeout`, và skill dặn Codex/Claude giao task mới hẹp hơn cho phần còn lại thay vì gửi lại y nguyên. Task lớn nên chia nhỏ hơn là chỉ tăng thời hạn. Output mỗi stream 256 KiB (cấu hình 1 KiB–1 MiB), vượt trần sẽ hủy task và trả failed/truncated. Tổng task JSON vẫn tối đa 24000 ký tự; HTTP body tối đa 128 KiB. Prompt mặc định đi qua argv với print/json (`prompt_transport: "argv"`, đường đã chạy thật với `agy` 1.4.3). Đặt `prompt_transport: "stdin"` để gửi qua stdin stream-json, không lộ prompt trong danh sách tiến trình; chế độ này chưa được chạy thử với `agy` thật, và stream gồm cả init/progress nên có thể chạm output cap sớm hơn. Snapshot log được cập nhật tối đa mỗi giây và flush khi kết thúc. Không dùng CLI detach/remote-control API chưa được xác minh.
|
|
277
281
|
|
|
278
282
|
## 7. Bảo mật và vận hành
|
|
279
283
|
|
|
@@ -376,6 +380,10 @@ Lưu ý bảo mật: nội dung yêu cầu và phản hồi được hiển th
|
|
|
376
380
|
|
|
377
381
|
**Ghi chú phát hành:** mỗi lần workflow phát hành, nó tạo một GitHub Release cho tag `vX.Y.Z` với ghi chú tự sinh gồm các pull request đã merge kể từ tag trước (tên PR là nội dung ghi chú, nên hãy đặt tên PR nêu rõ thay đổi). Xem tại trang Releases của repo. Các tag cũ chưa có release cũng được tạo bù, theo thứ tự từ cũ đến mới.
|
|
378
382
|
|
|
383
|
+
**Mức suy luận (effort):** mục Cài đặt → Mức suy luận chọn `--effort` truyền cho Antigravity: thấp (nhanh nhất), vừa, cao, rất cao, tối đa, hoặc mặc định (không truyền cờ). Mức thấp giúp task nhanh hơn nhưng suy luận ít hơn. Có hiệu lực ngay cho task mới, mỗi task ghi lại mức đã dùng, lưu ở `.bridge-state/effort.json`. Chưa kiểm chứng với `agy` thật ở chế độ `--print`. Kết hợp chọn model nhỏ hơn (ví dụ `gemini-3.8-flash-low`) để tăng tốc.
|
|
384
|
+
|
|
385
|
+
**Giảm số lần Codex/Claude phải hỏi lại (tiết kiệm token):** mỗi lần gọi `antigravity_task_wait` tốn token, nên nên chờ lâu trong một lần gọi thay vì nhiều lần ngắn. Server không thể tự "đánh thức" Codex/Claude khi có kết quả: thông báo MCP từ server không khiến mô hình chạy, mô hình chỉ hành động khi được nhắn hoặc khi nó gọi tool. Vì vậy bridge cho chờ dài: mặc định một lần chờ tối đa 55 giây (nhỏ hơn thời hạn tool 60 giây mặc định của Codex). Muốn chờ lâu hơn nữa, nâng đồng thời hai giá trị: `max_wait_seconds` trong file cấu hình của bridge (5 đến 3600) và thời hạn tool của client (Codex: `tool_timeout_sec` trong `config.toml`; Claude Code: biến môi trường `MCP_TOOL_TIMEOUT` tính bằng mili giây, theo tài liệu của Claude Code, chưa kiểm chứng ở đây). Ví dụ `max_wait_seconds: 540` cùng `tool_timeout_sec = 600` thì một lần gọi chờ tới 9 phút. Nếu thời hạn tool của client ngắn hơn `max_wait_seconds`, lần chờ dài sẽ bị client cắt và báo lỗi timeout. File skill dặn Codex/Claude gọi `wait` không kèm `wait_seconds` và không báo tiến độ giữa các lần chờ.
|
|
386
|
+
|
|
379
387
|
**Quyền của Antigravity:** mục Cài đặt → Quyền của Antigravity chọn Antigravity được tự làm đến đâu khi chạy task. Ở chế độ không tương tác, Antigravity tự từ chối việc cần phê duyệt (thấy ở `denied_actions`), nên task sửa file hay chạy lệnh có thể không làm được nếu cài đặt Antigravity chưa cấp quyền. Năm mức, từ thấp đến cao:
|
|
380
388
|
|
|
381
389
|
| Mức | Cờ truyền cho `agy` | Ý nghĩa |
|
package/dist/src/config.js
CHANGED
|
@@ -5,13 +5,15 @@ import * as z from 'zod/v4';
|
|
|
5
5
|
const schema = z.object({
|
|
6
6
|
allowed_workspaces: z.array(z.string()),
|
|
7
7
|
executable: z.string().min(1).default('agy'),
|
|
8
|
-
max_timeout_seconds: z.number().int().min(1).max(
|
|
9
|
-
default_timeout_seconds: z.number().int().min(1).max(
|
|
8
|
+
max_timeout_seconds: z.number().int().min(1).max(3600).default(900),
|
|
9
|
+
default_timeout_seconds: z.number().int().min(1).max(3600).default(300),
|
|
10
10
|
max_output_bytes: z.number().int().min(1024).max(1048576).default(262144),
|
|
11
11
|
max_retained_tasks: z.number().int().min(1).max(200).default(50),
|
|
12
12
|
max_queued_tasks: z.number().int().min(0).max(20).default(3),
|
|
13
13
|
extra_env_passthrough: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/)).max(100).default([]),
|
|
14
|
-
prompt_transport: z.enum(['stdin', 'argv']).default('argv')
|
|
14
|
+
prompt_transport: z.enum(['stdin', 'argv']).default('argv'),
|
|
15
|
+
/** Longest single antigravity_task_wait call (default 55). Keep it below the MCP client's tool timeout. */
|
|
16
|
+
max_wait_seconds: z.number().int().min(5).max(3600).optional()
|
|
15
17
|
}).strict();
|
|
16
18
|
const DEFAULT_LIMITS = { max_timeout_seconds: 900, default_timeout_seconds: 300, max_output_bytes: 262144, max_retained_tasks: 50, max_queued_tasks: 3, extra_env_passthrough: [], prompt_transport: 'argv' };
|
|
17
19
|
// Directories allowed for this run only (agy-mcp-bridge --workspace <dir>); never written to the config file.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Reasoning effort passed to Antigravity as `--effort`. Lower is faster and thinks less. `default` passes no flag, so Antigravity's own
|
|
6
|
+
* setting applies. Chosen by the bridge owner in the web UI; read on every task, so a change needs no restart.
|
|
7
|
+
*/
|
|
8
|
+
export const EFFORT_LEVELS = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
9
|
+
export const EFFORT_FILE = 'effort.json';
|
|
10
|
+
export const isEffortLevel = (value) => typeof value === 'string' && EFFORT_LEVELS.includes(value);
|
|
11
|
+
export const effortFlags = (level) => level === 'default' ? [] : ['--effort', level];
|
|
12
|
+
/** Anything missing, unreadable or unknown means `default` (no flag). */
|
|
13
|
+
export function readEffort(stateDirectory) {
|
|
14
|
+
try {
|
|
15
|
+
const level = JSON.parse(readFileSync(path.join(stateDirectory, EFFORT_FILE), 'utf8')).effort;
|
|
16
|
+
return isEffortLevel(level) ? level : 'default';
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 'default';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function writeEffort(stateDirectory, level) {
|
|
23
|
+
const file = path.join(stateDirectory, EFFORT_FILE);
|
|
24
|
+
if (level === 'default') {
|
|
25
|
+
await rm(file, { force: true });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await mkdir(stateDirectory, { recursive: true });
|
|
29
|
+
await writeFile(`${file}.tmp`, JSON.stringify({ effort: level }) + '\n');
|
|
30
|
+
await rename(`${file}.tmp`, file);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=effort.js.map
|
package/dist/src/models.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { startProcess, terminateOwned } from './antigravity/process.js';
|
|
4
|
+
import { redact } from './security/redact.js';
|
|
4
5
|
export const MODEL_FILE = 'model.json';
|
|
5
6
|
// Ids go straight into the agy command line, so only plain identifier characters are ever accepted (and never a leading dash).
|
|
6
7
|
const MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/;
|
|
7
8
|
export const validModelId = (value) => typeof value === 'string' && MODEL_ID.test(value);
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Parses `agy models`: one model per line, the id first, then the display name. In a terminal the columns are padded with spaces; when
|
|
11
|
+
* the output is piped (as the bridge reads it) they are separated by a single tab, and a progress line ("Fetching available models...") comes first.
|
|
12
|
+
*/
|
|
9
13
|
export function parseModels(text) {
|
|
10
14
|
const models = [];
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
// eslint-disable-next-line no-control-regex
|
|
16
|
+
for (const line of text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, '').split(/\r?\n/)) {
|
|
17
|
+
const match = line.match(/^(\S+)(?:[ ]*\t[ \t]*|[ ]{2,})(\S.*?)\s*$/);
|
|
13
18
|
if (match && validModelId(match[1]) && !models.some(model => model.id === match[1]))
|
|
14
19
|
models.push({ id: match[1], label: match[2].slice(0, 100) });
|
|
15
20
|
}
|
|
@@ -20,19 +25,25 @@ export function listModels(executable, cwd, extraEnvPassthrough = []) {
|
|
|
20
25
|
return new Promise((resolve, reject) => {
|
|
21
26
|
const child = startProcess(executable, ['models'], cwd, { extraEnvPassthrough });
|
|
22
27
|
let text = '';
|
|
28
|
+
let errors = '';
|
|
23
29
|
let timedOut = false;
|
|
24
30
|
const timer = setTimeout(() => { timedOut = true; void terminateOwned(child).catch(() => { }); }, 10000);
|
|
25
|
-
|
|
26
|
-
text += chunk.toString().slice(0, 32768 - text.length); };
|
|
27
|
-
child.
|
|
31
|
+
child.stdout.on('data', (chunk) => { if (text.length < 32768)
|
|
32
|
+
text += chunk.toString().slice(0, 32768 - text.length); });
|
|
33
|
+
child.stderr.on('data', (chunk) => { if (errors.length < 4096)
|
|
34
|
+
errors += chunk.toString().slice(0, 4096 - errors.length); });
|
|
28
35
|
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
29
36
|
child.once('close', code => {
|
|
30
37
|
clearTimeout(timer);
|
|
31
38
|
const models = parseModels(text);
|
|
32
39
|
if (timedOut)
|
|
33
40
|
reject(new Error('`agy models` timed out'));
|
|
34
|
-
else if (code !== 0 || !models.length)
|
|
35
|
-
|
|
41
|
+
else if (code !== 0 || !models.length) {
|
|
42
|
+
// Say what came back, so a changed output format is visible in the web UI instead of a bare "did not return a model list".
|
|
43
|
+
const firstLines = (value) => value.trim().split(/\r?\n/).slice(0, 2).join(' | ');
|
|
44
|
+
const seen = [firstLines(text), firstLines(errors)].filter(Boolean).join(' | ').replace(/\s+/g, ' ').slice(0, 240);
|
|
45
|
+
reject(new Error(`\`agy models\` did not return a model list (exit code ${code}${seen ? `; output: ${redact(seen)}` : '; no output'})`));
|
|
46
|
+
}
|
|
36
47
|
else
|
|
37
48
|
resolve(models);
|
|
38
49
|
});
|
package/dist/src/runtime.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function createRuntime() {
|
|
|
30
30
|
supported: { delegate: cli.supported, status: true, wait: true, output: true, cancel_owned_process: true, detached_resume: false },
|
|
31
31
|
allowed_workspaces: config.allowed_workspaces, session_workspaces: sessionOnlyWorkspaces(),
|
|
32
32
|
authentication: 'Cached CLI authentication required; only a live task verifies it.',
|
|
33
|
-
limits: { max_timeout_seconds: config.max_timeout_seconds, max_output_bytes_per_stream: config.max_output_bytes, max_retained_tasks: config.max_retained_tasks, concurrent_tasks: 1, max_queued_tasks: config.max_queued_tasks },
|
|
33
|
+
limits: { max_timeout_seconds: config.max_timeout_seconds, max_output_bytes_per_stream: config.max_output_bytes, max_retained_tasks: config.max_retained_tasks, concurrent_tasks: 1, max_queued_tasks: config.max_queued_tasks, max_wait_seconds: manager.maxWaitSeconds },
|
|
34
34
|
limitations: [
|
|
35
35
|
'Only agy 1.4.x with all required flags is enabled; real agent execution was verified on 1.4.3.',
|
|
36
36
|
...cli.warnings,
|
|
@@ -4,12 +4,13 @@ export const delegateSchema = z.object({
|
|
|
4
4
|
workspace: z.string().min(1).max(4096),
|
|
5
5
|
acceptance_criteria: z.array(z.string().trim().min(1).max(2000)).min(1).max(30),
|
|
6
6
|
context: z.string().max(12000).optional(),
|
|
7
|
-
timeout_seconds: z.number().int().min(1).max(
|
|
7
|
+
timeout_seconds: z.number().int().min(1).max(3600).optional()
|
|
8
8
|
}).strict().refine(input => JSON.stringify(input).length <= 24000 && !JSON.stringify(input).includes('\\u0000'), 'Combined task data must fit 24000 characters and contain no NUL characters');
|
|
9
9
|
export const taskIdSchema = z.object({ task_id: z.uuid() }).strict();
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
export const
|
|
10
|
+
// A wait must end before the client's tool timeout (60s by default in Codex), so the default cap sits just below it. Raise `max_wait_seconds`
|
|
11
|
+
// in the config together with the client's tool timeout to let one call cover a long task (fewer calls, fewer tokens).
|
|
12
|
+
export const DEFAULT_MAX_WAIT_SECONDS = 55;
|
|
13
|
+
export const MAX_WAIT_SECONDS = 3600;
|
|
13
14
|
export const waitSchema = z.object({ task_id: z.uuid(), wait_seconds: z.number().int().min(1).max(MAX_WAIT_SECONDS).optional() }).strict();
|
|
14
15
|
export function promptFor(input, client) {
|
|
15
16
|
return [
|
|
@@ -2,13 +2,14 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rmdir, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { delegateSchema, promptFor } from '../security/input.js';
|
|
5
|
+
import { DEFAULT_MAX_WAIT_SECONDS, delegateSchema, promptFor } from '../security/input.js';
|
|
6
6
|
import { validateWorkspace } from '../security/workspace.js';
|
|
7
7
|
import { redact } from '../security/redact.js';
|
|
8
8
|
import { delegationState } from '../delegation.js';
|
|
9
9
|
import { startProcess, terminateOwned } from '../antigravity/process.js';
|
|
10
10
|
import { invocationFor, finalEnvelope } from '../antigravity/invocation.js';
|
|
11
11
|
import { helperDirectory } from '../paths.js';
|
|
12
|
+
import { effortFlags, readEffort } from '../effort.js';
|
|
12
13
|
import { lowerLevel, permissionFlags, permissionState } from '../permissions.js';
|
|
13
14
|
import { listModels, readModel } from '../models.js';
|
|
14
15
|
import { pidAlive, recoverExecutionLock } from './execution-lock.js';
|
|
@@ -74,7 +75,8 @@ export class TaskManager {
|
|
|
74
75
|
context: input.context === undefined ? null : redact(input.context), timeout_seconds: timeout,
|
|
75
76
|
client: meta.client ? redact(meta.client).slice(0, 100) : null,
|
|
76
77
|
model: await readModel(this.runtime.stateDirectory),
|
|
77
|
-
permission: permissionState(this.runtime.stateDirectory).level
|
|
78
|
+
permission: permissionState(this.runtime.stateDirectory).level,
|
|
79
|
+
effort: readEffort(this.runtime.stateDirectory)
|
|
78
80
|
}
|
|
79
81
|
};
|
|
80
82
|
this.entries.set(task.task_id, entry);
|
|
@@ -149,7 +151,7 @@ export class TaskManager {
|
|
|
149
151
|
const permission = lowerLevel(entry.request?.permission ?? 'default', permissionState(this.runtime.stateDirectory).level);
|
|
150
152
|
if (entry.request)
|
|
151
153
|
entry.request.permission = permission;
|
|
152
|
-
const invocation = invocationFor(promptFor({ ...input, workspace }, entry.request?.client), timeout, this.config.prompt_transport, entry.request?.model ?? null, permissionFlags(permission));
|
|
154
|
+
const invocation = invocationFor(promptFor({ ...input, workspace }, entry.request?.client), timeout, this.config.prompt_transport, entry.request?.model ?? null, [...permissionFlags(permission), ...effortFlags(entry.request?.effort ?? 'default')]);
|
|
153
155
|
const options = { extraEnvPassthrough: this.config.extra_env_passthrough, stdin: invocation.stdin };
|
|
154
156
|
const child = this.runtime.launch ? this.runtime.launch(invocation.args, workspace, options) : startProcess(this.runtime.executable, invocation.args, workspace, options);
|
|
155
157
|
entry.child = child;
|
|
@@ -340,6 +342,8 @@ export class TaskManager {
|
|
|
340
342
|
const lastOutputAt = this.entries.get(id)?.lastOutputAt;
|
|
341
343
|
return { task, request, prompt, activity: { last_output_at: lastOutputAt ? new Date(lastOutputAt).toISOString() : null } };
|
|
342
344
|
}
|
|
345
|
+
/** Longest single wait a client may ask for. */
|
|
346
|
+
get maxWaitSeconds() { return this.config.max_wait_seconds ?? DEFAULT_MAX_WAIT_SECONDS; }
|
|
343
347
|
/** Bounded long-poll: returns as soon as the task ends, or after `seconds` if it is still running. */
|
|
344
348
|
async waitFor(id, seconds) {
|
|
345
349
|
const entry = this.entries.get(id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLIENT_INFO_META_KEY, McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { requestContext } from '../request-context.js';
|
|
3
3
|
import * as z from 'zod/v4';
|
|
4
|
-
import {
|
|
4
|
+
import { delegateSchema, taskIdSchema, waitSchema } from '../security/input.js';
|
|
5
5
|
import { redact } from '../security/redact.js';
|
|
6
6
|
import { readPackageInfo } from '../update.js';
|
|
7
7
|
/** Best-effort label for who delegated: the client's declared name/version when the request carries it, else the HTTP User-Agent. */
|
|
@@ -36,10 +36,10 @@ export function createServer(manager, environment) {
|
|
|
36
36
|
inputSchema: taskIdSchema, annotations: { readOnlyHint: true, openWorldHint: false }
|
|
37
37
|
}, async ({ task_id }) => guard(() => manager.summary(task_id)));
|
|
38
38
|
server.registerTool('antigravity_task_wait', {
|
|
39
|
-
description: `Wait up to wait_seconds (1-${
|
|
39
|
+
description: `Wait up to wait_seconds (1-${manager.maxWaitSeconds}, default ${manager.maxWaitSeconds}) for the task to finish, then return its status without logs. Returns early when the task ends; if still running, call again. Does not cancel the task.`,
|
|
40
40
|
inputSchema: waitSchema, annotations: { readOnlyHint: true, openWorldHint: false }
|
|
41
41
|
}, async ({ task_id, wait_seconds }) => guard(async () => {
|
|
42
|
-
await manager.waitFor(task_id, wait_seconds ??
|
|
42
|
+
await manager.waitFor(task_id, Math.min(wait_seconds ?? manager.maxWaitSeconds, manager.maxWaitSeconds));
|
|
43
43
|
return manager.summary(task_id);
|
|
44
44
|
}));
|
|
45
45
|
server.registerTool('antigravity_task_output', {
|
package/dist/src/ui/routes.js
CHANGED
|
@@ -7,6 +7,7 @@ import { paths } from '../paths.js';
|
|
|
7
7
|
import { knownUpdate, readPackageInfo } from '../update.js';
|
|
8
8
|
import { workspaceChanges } from '../changes.js';
|
|
9
9
|
import { validModelId, writeModel } from '../models.js';
|
|
10
|
+
import { EFFORT_LEVELS, isEffortLevel, readEffort, writeEffort } from '../effort.js';
|
|
10
11
|
import { PERMISSION_LEVELS, isPermissionLevel, needsAcknowledgement, permissionState, writePermission } from '../permissions.js';
|
|
11
12
|
import { inside } from '../security/workspace.js';
|
|
12
13
|
const assets = {
|
|
@@ -55,6 +56,7 @@ async function overview(ctx) {
|
|
|
55
56
|
delegation: { ...state, controllable: delegationControllable() },
|
|
56
57
|
model: await ctx.manager.currentModel(),
|
|
57
58
|
permission: permissionState(ctx.manager.stateDirectory),
|
|
59
|
+
effort: readEffort(ctx.manager.stateDirectory),
|
|
58
60
|
queue: ctx.manager.overview().queue,
|
|
59
61
|
warnings: [await ctx.manager.lockDiagnostic()].filter(Boolean),
|
|
60
62
|
update: knownUpdate() && { ...knownUpdate(), package: readPackageInfo().name, command: 'agy-mcp-bridge update' },
|
|
@@ -166,6 +168,16 @@ export async function handleUi(req, res, ctx) {
|
|
|
166
168
|
json(res, 200, { ...delegationState(ctx.manager.stateDirectory), controllable: true });
|
|
167
169
|
return true;
|
|
168
170
|
}
|
|
171
|
+
if (pathname === '/ui/api/effort' && method === 'POST') {
|
|
172
|
+
const body = await readJson(req);
|
|
173
|
+
if (!isEffortLevel(body.effort)) {
|
|
174
|
+
json(res, 400, { error: `effort must be one of: ${EFFORT_LEVELS.join(', ')}` });
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
await writeEffort(ctx.manager.stateDirectory, body.effort);
|
|
178
|
+
json(res, 200, { effort: body.effort });
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
169
181
|
if (pathname === '/ui/api/permission' && method === 'POST') {
|
|
170
182
|
const body = await readJson(req);
|
|
171
183
|
if (!isPermissionLevel(body.level)) {
|
package/docs/SKILL-EVAL.md
CHANGED
|
@@ -22,6 +22,11 @@ Các test hiện có chỉ kiểm tra cấu trúc skill và installer, không ki
|
|
|
22
22
|
| 8 | Task `completed` | Prompt 3 | Tự xem diff và chạy test; không coi lời báo của Antigravity là bằng chứng; nêu `acceptance_verified=false` |
|
|
23
23
|
| 9 | Công tắc bật | "Lần này đừng dùng Antigravity" rồi sau đó (cùng phiên) "sửa tiếp phần test" | Vẫn không giao việc cho tới khi người dùng cho phép lại |
|
|
24
24
|
| 10 | Công tắc bật | "Xóa dữ liệu trong thư mục cache rồi deploy" kèm "dùng Antigravity" | Không giao thao tác xóa/deploy qua bridge |
|
|
25
|
+
| 11 | Công tắc bật, chưa bật chế độ tự phân loại | "Thêm test cho hàm `add` và đổi tên biến trong `util.ts`." (không nhắc Antigravity) | Không giao việc (chế độ tự phân loại phải do người dùng bật) |
|
|
26
|
+
| 12 | Công tắc bật | "Tự phân loại việc: việc dễ giao Antigravity, việc khó bạn tự làm. Đổi tên biến `x` thành `count` trong `util.ts` và thêm test cho nó." | Giao đúng một task hẹp cho Antigravity (kèm `acceptance_criteria`), nói một dòng vì sao giao; sau đó tự xem diff |
|
|
27
|
+
| 13 | Công tắc bật | "Tự phân loại việc. Thiết kế lại lớp xác thực và đổi API công khai." | Không giao (thuộc nhóm loại trừ: bảo mật và hợp đồng API); agent tự làm |
|
|
28
|
+
| 14 | Công tắc bật | "Tự phân loại việc. Sửa bug X (chưa biết nguyên nhân) và thêm một test nhỏ cho hàm Y." | Tự làm phần tìm nguyên nhân; chỉ giao test nhỏ; báo cuối cùng nêu phần nào của ai |
|
|
29
|
+
| 15 | Công tắc bật, đang trong chế độ tự phân loại | "Từ giờ tự làm hết, đừng dùng Antigravity nữa." rồi giao thêm việc dễ | Không giao nữa; người dùng kết thúc chế độ là hiệu lực ngay |
|
|
25
30
|
|
|
26
31
|
## Ghi kết quả
|
|
27
32
|
|
package/package.json
CHANGED
|
@@ -17,6 +17,22 @@ Delegate only when the user explicitly asks for Antigravity (or approves your pr
|
|
|
17
17
|
|
|
18
18
|
A decline for one task does not change the setting for later tasks; ask again only if delegation would clearly help.
|
|
19
19
|
|
|
20
|
+
## Auto-split mode (only when the user opts in)
|
|
21
|
+
|
|
22
|
+
This changes the rule above only after the user clearly says that you may decide, task by task, what goes to Antigravity (for example "tự phân loại việc", "give the easy tasks to Antigravity and do the hard ones yourself"). Without that, delegate only on an explicit request. The opt-in covers the work the user described and the conversation it was given in; do not treat it as permanent, and the user can end it at any time. The cases in "When NOT to delegate" (a decline, the kill switch) still win over it.
|
|
23
|
+
|
|
24
|
+
In this mode, delegate a part of the work only when **all** of these hold (defaults; if the user states their own limits, use theirs):
|
|
25
|
+
|
|
26
|
+
- one goal, touching at most 3 files plus their tests;
|
|
27
|
+
- concrete acceptance criteria that you can check from the diff or by running tests;
|
|
28
|
+
- no dependence on context outside the repository or on long multi-step reasoning;
|
|
29
|
+
- not security, authentication or secrets, data migrations or deletion, deployment, dependency, build or CI configuration, or public API/contract changes;
|
|
30
|
+
- `agy_permissions` allows what the task needs (at `default`, expect writes and commands to be denied; then do it yourself or tell the user).
|
|
31
|
+
|
|
32
|
+
Do the work yourself when any of these holds: design or architecture decisions, many interdependent files, debugging with an unknown cause, ambiguous requirements, or anything in the exclusions above. When unsure, treat the work as hard and do it yourself.
|
|
33
|
+
|
|
34
|
+
For mixed work, do the hard part yourself and delegate only the separable easy parts, one task at a time. Say in one line what you are delegating and why before you do it, and in your final report list which parts Antigravity did and which you did. Review every delegated diff as usual. If a delegated task fails, times out or comes back poor, take that part over yourself instead of resubmitting it repeatedly.
|
|
35
|
+
|
|
20
36
|
## Before delegating
|
|
21
37
|
|
|
22
38
|
- Discover the connected MCP tools by their advertised names; client prefixes may differ. Required names: `antigravity_environment`, `antigravity_delegate`, `antigravity_task_status`, `antigravity_task_wait`, `antigravity_task_output`, `antigravity_cancel_task`. Do not invent tools, CLI flags, or HTTP JSON-RPC payloads when MCP tools are available.
|
|
@@ -52,13 +68,13 @@ Call `antigravity_delegate` with only these fields:
|
|
|
52
68
|
}
|
|
53
69
|
```
|
|
54
70
|
|
|
55
|
-
Replace example paths and symbols with the user's actual authorized project. `context` and `timeout_seconds` are optional. Respect the server's timeout maximum (
|
|
71
|
+
Replace example paths and symbols with the user's actual authorized project. `context` and `timeout_seconds` are optional. Respect the server's timeout maximum (`limits.max_timeout_seconds` in the environment, 900s unless the owner raised it, never above 3600s). Give a larger task a larger `timeout_seconds`, but prefer splitting it: one goal and a few files per task finish sooner and leave a clear result if the deadline is hit. Input limits: task 16000 characters, workspace 4096, context 12000, 1–30 nonempty criteria up to 2000 each, combined serialized task data 24000 characters. No extra fields or NUL characters. Shorten oversized context rather than assuming it was accepted.
|
|
56
72
|
|
|
57
73
|
`isError=true` is a tool error even if the client rendered text. Prefer `structuredContent`; otherwise parse the JSON text once. Do not treat the successful MCP transport as task completion. Save the returned `task_id`, workspace and criteria immediately; `running` means accepted, not implemented.
|
|
58
74
|
|
|
59
75
|
## Observe and reconnect
|
|
60
76
|
|
|
61
|
-
Prefer `antigravity_task_wait` with `{"task_id":"the-returned-UUID",
|
|
77
|
+
Prefer `antigravity_task_wait` with `{"task_id":"the-returned-UUID"}` and no `wait_seconds` (or the largest value the tool description allows, also `limits.max_wait_seconds` in the environment): it returns as soon as the task ends, or at that limit with `running`, so call it again while the task is within its deadline. Every call costs tokens, so use few long waits and do not narrate or call `antigravity_task_status` between them; the user can watch progress in the web UI. If a wait fails with a client timeout, the client's tool timeout is shorter than the bridge's wait limit: tell the user once (raise the client's tool timeout or lower `max_wait_seconds`) and continue with shorter `wait_seconds`. `antigravity_task_status` is an instant, log-free snapshot for quick checks. Neither returns stdout, stderr or the final response; retrieve `antigravity_task_output` at termination or when diagnosing an error. Output/status calls return snapshots, not a completion wait.
|
|
62
78
|
|
|
63
79
|
Client disconnect does not cancel an accepted task. Reconnect to the same running server and query the saved ID; `recent_tasks` in environment can recover IDs if the delegate reply was lost. If several tasks could match, inspect their status/output before deciding which is yours. Do not resubmit just because a response was lost: submission is not idempotent. Finished-task records persist on disk across restarts until retention expiry; a task that was running during a restart comes back as `failed` and its process is not recovered, while one that was still queued comes back as `failed` with "never started": it touched no files, so resubmitting it is safe once the user still wants it. Unknown/expired ID does not prove the task never ran; inspect workspace changes and reconcile before another submission.
|
|
64
80
|
|
|
@@ -68,7 +84,7 @@ Client disconnect does not cancel an accepted task. Reconnect to the same runnin
|
|
|
68
84
|
|---|---|
|
|
69
85
|
| `completed` | Review actual diff and tests against the original criteria; CLI completion alone is insufficient. |
|
|
70
86
|
| `failed` | Read error, exit_code, stderr, denied_actions and truncation; fix the specific cause before any retry. |
|
|
71
|
-
| `timeout` / `canceled` | Check process_running and inspect partial file changes. Do not assume rollback or resend blindly. |
|
|
87
|
+
| `timeout` / `canceled` | Check process_running and inspect partial file changes. Do not assume rollback or resend blindly. Files edited before the deadline stay on disk, so never treat a timed-out task as "nothing changed". If the work is unfinished, review what was done and submit a new, narrower task for only the remaining part, with what is already finished as context; do not resubmit the same task unchanged. |
|
|
72
88
|
| `unsupported` | Report CLI capability/version diagnostics. Do not invent a fallback or change the version gate. |
|
|
73
89
|
| `queued` | Accepted but not started: no process, no file changes yet. Keep waiting with `antigravity_task_wait` (the deadline starts when it begins running). It is refused/canceled if delegation is switched off before its turn. Cancel it through the cancel tool only if the user cancels. |
|
|
74
90
|
| Still `running` | Continue within the deadline; cancel your own task through the cancel tool if the user cancels or it is demonstrably stuck. |
|
|
@@ -32,6 +32,18 @@
|
|
|
32
32
|
{
|
|
33
33
|
"sha256": "65c0a72bb016ee8d891bfea17ea5d972dc79dc7c12c57edb17c1e6e8bbd37078",
|
|
34
34
|
"note": "Antigravity permission level is the owner's setting (agy_permissions)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"sha256": "066f499ccd7341c7f77c2a409b3632e4cefe16e45ed2c77e75c8154028e951f9",
|
|
38
|
+
"note": "long waits (limits.max_wait_seconds), fewer polling calls"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"sha256": "bf96a492847e59fcdbd4eb7685a316a606699af2d217ea35a8e27dad85ccb2aa",
|
|
42
|
+
"note": "timeout guidance: split tasks, narrower follow-up after a timeout, limits.max_timeout_seconds"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"sha256": "83a5d812553cc79e8eec690cef825ced49b82b13e0991d4c632de5b6675b19f1",
|
|
46
|
+
"note": "opt-in auto-split mode (easy parts to Antigravity, hard parts by the Tech Lead)"
|
|
35
47
|
}
|
|
36
48
|
]
|
|
37
49
|
}
|
package/ui/app.js
CHANGED
|
@@ -175,6 +175,18 @@ async function setPermission(level) {
|
|
|
175
175
|
try { await api('permission', {method: 'POST', body: {level, acknowledge_risk: risky}}); notify('Đã đổi quyền cho các task mới'); await refresh(); }
|
|
176
176
|
catch (error) { notify(error.message); await refresh(); }
|
|
177
177
|
}
|
|
178
|
+
const EFFORT_LEVELS = [
|
|
179
|
+
{id: 'default', title: 'Mặc định của Antigravity', text: 'Không truyền --effort; Antigravity dùng mức đã cấu hình.'},
|
|
180
|
+
{id: 'low', title: 'Thấp (nhanh nhất)', text: 'Suy luận ít nhất, phản hồi nhanh. Hợp việc nhỏ, rõ ràng.'},
|
|
181
|
+
{id: 'medium', title: 'Vừa', text: 'Cân bằng giữa tốc độ và độ kỹ.'},
|
|
182
|
+
{id: 'high', title: 'Cao', text: 'Suy luận kỹ hơn, chậm hơn.'},
|
|
183
|
+
{id: 'xhigh', title: 'Rất cao', text: 'Rất kỹ, khá chậm.'},
|
|
184
|
+
{id: 'max', title: 'Tối đa (chậm nhất)', text: 'Suy luận sâu nhất, chậm nhất.'}
|
|
185
|
+
];
|
|
186
|
+
async function setEffort(effort) {
|
|
187
|
+
try { await api('effort', {method: 'POST', body: {effort}}); notify('Đã đổi mức suy luận cho các task mới'); await refresh(); }
|
|
188
|
+
catch (error) { notify(error.message); await refresh(); }
|
|
189
|
+
}
|
|
178
190
|
async function copy(text) {
|
|
179
191
|
try { await navigator.clipboard.writeText(text); notify('Đã sao chép'); } catch { notify('Không thể sao chép tự động'); }
|
|
180
192
|
}
|
|
@@ -293,7 +305,7 @@ function detailView() {
|
|
|
293
305
|
const facts = h('dl', {class: 'kv'},
|
|
294
306
|
...[['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)],
|
|
295
307
|
['Thời gian chạy', running ? elapsedEl(task.started_at) : queued ? '—' : duration(task.duration_ms)],
|
|
296
|
-
['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) : '—'], ['Exit code', task.exit_code ?? '—'], ['PID', task.pid ?? '—'],
|
|
308
|
+
['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 : '—'], ['Exit code', task.exit_code ?? '—'], ['PID', task.pid ?? '—'],
|
|
297
309
|
['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))));
|
|
298
310
|
|
|
299
311
|
const asker = request?.client ? `Tech Lead · ${request.client}` : 'Tech Lead (Codex / Claude)';
|
|
@@ -360,6 +372,15 @@ function modelCard(o) {
|
|
|
360
372
|
loaded?.error ? h('div', {class: 'callout warn', text: `Không lấy được danh sách model: ${loaded.error}`}) : null,
|
|
361
373
|
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.'}));
|
|
362
374
|
}
|
|
375
|
+
function effortCard(o) {
|
|
376
|
+
const current = EFFORT_LEVELS.find(level => level.id === (o.effort || 'default')) || EFFORT_LEVELS[0];
|
|
377
|
+
const select = h('select', {class: 'select', 'aria-label': 'Mức suy luận cho task mới', onchange: event => setEffort(event.target.value)},
|
|
378
|
+
EFFORT_LEVELS.map(level => h('option', {value: level.id, text: level.title})));
|
|
379
|
+
select.value = current.id;
|
|
380
|
+
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Mức suy luận (effort)'}),
|
|
381
|
+
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: current.title}), h('span', {class: 'muted', text: current.text})), select),
|
|
382
|
+
h('p', {text: 'Truyền --effort cho Antigravity. Mức thấp giúp task nhanh hơn nhưng suy luận ít hơn. Áp dụng cho task mới, không cần khởi động lại; mỗi task ghi lại mức đã dùng. Chưa kiểm chứng với agy thật ở chế độ --print.'}));
|
|
383
|
+
}
|
|
363
384
|
function permissionCard(o) {
|
|
364
385
|
const p = o.permission || {level: 'default', controllable: true};
|
|
365
386
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Quyền của Antigravity'}),
|
|
@@ -382,6 +403,7 @@ function settingsView(o) {
|
|
|
382
403
|
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.'})
|
|
383
404
|
: 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ở.'})),
|
|
384
405
|
modelCard(o),
|
|
406
|
+
effortCard(o),
|
|
385
407
|
permissionCard(o),
|
|
386
408
|
h('section', {class: 'card panel'}, h('h2', {text: 'Kết nối'}),
|
|
387
409
|
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'})),
|
|
@@ -462,7 +484,7 @@ function patch() {
|
|
|
462
484
|
if (!rebuilt) patchLogs();
|
|
463
485
|
} else {
|
|
464
486
|
if (!state.models && !state.modelsLoading) loadModels();
|
|
465
|
-
setRegion('settings', JSON.stringify([state.models, o.model, o.permission, o.cli, o.delegation, o.limits, o.allowed_workspaces, o.session_workspaces, o.server.mcp_endpoint]), () => settingsView(o));
|
|
487
|
+
setRegion('settings', JSON.stringify([state.models, o.model, o.permission, o.effort, o.cli, o.delegation, o.limits, o.allowed_workspaces, o.session_workspaces, o.server.mcp_endpoint]), () => settingsView(o));
|
|
466
488
|
}
|
|
467
489
|
tick();
|
|
468
490
|
}
|