@linkshell/host 0.4.35 → 0.4.37
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/dist/host/src/drivers/acp/antigravity.d.ts +171 -4
- package/dist/host/src/drivers/acp/antigravity.js +1709 -527
- package/dist/host/src/drivers/acp/antigravity.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +1 -0
- package/dist/host/src/drivers/acp/driver.js +28 -1
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/mapper.js +4 -0
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/claude/driver.js +1 -1
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +2 -0
- package/dist/host/src/hub.js +10 -0
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/store.d.ts +1 -0
- package/dist/host/src/store.js +4 -0
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { accessSync, closeSync, constants, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
|
|
6
6
|
import { DatabaseSync } from "node:sqlite";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { RpcError, } from "@linkshell/wire";
|
|
10
|
+
import { QUESTION_OPTIONS } from "../../questions.js";
|
|
11
|
+
import { nestUnder } from "../nesting.js";
|
|
10
12
|
import { AcpDriver } from "./driver.js";
|
|
11
13
|
const execFileAsync = promisify(execFile);
|
|
12
14
|
/**
|
|
@@ -168,6 +170,25 @@ export function resolveAntigravityLaunch(env = process.env) {
|
|
|
168
170
|
version,
|
|
169
171
|
};
|
|
170
172
|
}
|
|
173
|
+
function readHead(filePath, bytes) {
|
|
174
|
+
let fd;
|
|
175
|
+
try {
|
|
176
|
+
fd = openSync(filePath, "r");
|
|
177
|
+
const buffer = Buffer.alloc(bytes);
|
|
178
|
+
const read = readSync(fd, buffer, 0, bytes, 0);
|
|
179
|
+
return buffer.subarray(0, read).toString("utf8");
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (fd !== undefined)
|
|
186
|
+
closeSync(fd);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Watchers, discovery and every RPC resolve the desktop; the answer only changes when the app is installed or moved.
|
|
190
|
+
const DESKTOP_CACHE_MS = 5000;
|
|
191
|
+
let desktopCache;
|
|
171
192
|
/**
|
|
172
193
|
* Resolves an installed Antigravity Desktop app (`/Applications/Antigravity.app`)
|
|
173
194
|
* and its conversation store (`~/.gemini/antigravity`).
|
|
@@ -175,6 +196,14 @@ export function resolveAntigravityLaunch(env = process.env) {
|
|
|
175
196
|
export function resolveAntigravityDesktop(env = process.env) {
|
|
176
197
|
if (env.PATH === "" && !env.ANTIGRAVITY_APP_DATA_DIR)
|
|
177
198
|
return undefined;
|
|
199
|
+
const key = `${env.HOME ?? ""}\n${env.ANTIGRAVITY_APP_DATA_DIR ?? ""}`;
|
|
200
|
+
if (desktopCache && desktopCache.key === key && Date.now() - desktopCache.at < DESKTOP_CACHE_MS)
|
|
201
|
+
return desktopCache.value;
|
|
202
|
+
const value = resolveAntigravityDesktopUncached(env);
|
|
203
|
+
desktopCache = value ? { key, at: Date.now(), value } : undefined;
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function resolveAntigravityDesktopUncached(env) {
|
|
178
207
|
const home = env.HOME || homedir();
|
|
179
208
|
const dataCandidates = [
|
|
180
209
|
env.ANTIGRAVITY_APP_DATA_DIR?.trim(),
|
|
@@ -197,15 +226,9 @@ export function resolveAntigravityDesktop(env = process.env) {
|
|
|
197
226
|
const logPath = logCandidates.find((candidate) => existsSync(candidate));
|
|
198
227
|
let version = "desktop";
|
|
199
228
|
if (logPath) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (match?.[1])
|
|
204
|
-
version = match[1];
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
// ignore unreadable log
|
|
208
|
-
}
|
|
229
|
+
const match = /Language server version:\s*([^\s\r\n]+)/i.exec(readHead(logPath, 16 * 1024));
|
|
230
|
+
if (match?.[1])
|
|
231
|
+
version = match[1];
|
|
209
232
|
}
|
|
210
233
|
if (version === "desktop" && process.platform === "darwin") {
|
|
211
234
|
try {
|
|
@@ -250,16 +273,20 @@ export function antigravitySpec() {
|
|
|
250
273
|
authStatus: antigravityAuthStatus,
|
|
251
274
|
};
|
|
252
275
|
}
|
|
276
|
+
/** Used only while the language server can't be asked (GetSlashCommands); mirrors its system commands. */
|
|
253
277
|
const ANTIGRAVITY_DESKTOP_COMMANDS = [
|
|
254
278
|
{ name: "goal", description: "设定持续目标,让 Antigravity 持续推进直到完成", hint: "<目标描述>" },
|
|
255
279
|
{ name: "plan", description: "先制定分步执行计划,再动手实现", hint: "<任务描述>" },
|
|
256
280
|
{ name: "boost", description: "深度思考、多视角分析与严格验证模式", hint: "<复杂问题或任务>" },
|
|
257
281
|
{ name: "teamwork-preview", description: "启动多 Agent 协作团队共同完成大型项目", hint: "<项目任务>" },
|
|
258
282
|
{ name: "grill-me", description: "通过互动提问对齐方案细节与设计决策", hint: "<主题>" },
|
|
283
|
+
{ name: "browser", description: "调用浏览器子 Agent 完成网页任务", hint: "<网页任务>" },
|
|
259
284
|
{ name: "compact", description: "压缩并总结当前对话上下文以释放窗口空间" },
|
|
260
285
|
{ name: "schedule", description: "创建定时任务或后台周期自动化", hint: "<定时指令>" },
|
|
261
286
|
{ name: "learn", description: "总结并持久化当前经验与约定供后续会话使用" },
|
|
262
287
|
];
|
|
288
|
+
/** Chinese descriptions for the language server's system commands; skills keep their own (they are user-authored). */
|
|
289
|
+
const SYSTEM_COMMAND_TEXT = new Map(ANTIGRAVITY_DESKTOP_COMMANDS.map((command) => [command.name, command]));
|
|
263
290
|
function unquoteArg(value) {
|
|
264
291
|
if (typeof value !== "string")
|
|
265
292
|
return value;
|
|
@@ -291,6 +318,13 @@ function parseToolArgs(raw) {
|
|
|
291
318
|
function stripTruncationMarkers(text) {
|
|
292
319
|
return text.replace(/\n?\.\.\.\s*<truncated\s+\d+\s+bytes>\s*\.\.\.\n?/gi, "\n").replace(/<truncated\s+\d+\s+bytes>/gi, "");
|
|
293
320
|
}
|
|
321
|
+
function stripStepHeader(text) {
|
|
322
|
+
return text.replace(/^Created At: [^\n]*\n(?:Completed At: [^\n]*\n)?/, "");
|
|
323
|
+
}
|
|
324
|
+
function toolOutputText(step) {
|
|
325
|
+
const raw = typeof step.content === "string" && step.content.trim() ? step.content : step.error ?? "";
|
|
326
|
+
return stripStepHeader(stripTruncationMarkers(raw)).trim();
|
|
327
|
+
}
|
|
294
328
|
export function cleanAntigravityUserInput(content) {
|
|
295
329
|
const closedMatch = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/i.exec(content);
|
|
296
330
|
if (closedMatch)
|
|
@@ -395,7 +429,112 @@ function readStepMediaBlocks(media) {
|
|
|
395
429
|
}
|
|
396
430
|
return blocks;
|
|
397
431
|
}
|
|
398
|
-
function
|
|
432
|
+
function parseAskQuestionEntries(args) {
|
|
433
|
+
const rawQuestions = Array.isArray(args.questions) ? args.questions : [];
|
|
434
|
+
const entries = [];
|
|
435
|
+
for (const raw of rawQuestions) {
|
|
436
|
+
if (!raw || typeof raw !== "object")
|
|
437
|
+
continue;
|
|
438
|
+
const rec = raw;
|
|
439
|
+
const question = typeof rec.question === "string" ? rec.question.trim() : "";
|
|
440
|
+
if (!question)
|
|
441
|
+
continue;
|
|
442
|
+
const isMultiSelect = rec.is_multi_select === true || rec.isMultiSelect === true;
|
|
443
|
+
const rawOpts = Array.isArray(rec.options) ? rec.options : [];
|
|
444
|
+
const options = rawOpts
|
|
445
|
+
.map((opt, idx) => {
|
|
446
|
+
if (typeof opt === "string")
|
|
447
|
+
return { id: String(idx + 1), text: opt };
|
|
448
|
+
if (opt && typeof opt === "object") {
|
|
449
|
+
const o = opt;
|
|
450
|
+
const text = typeof o.text === "string" ? o.text : typeof o.label === "string" ? o.label : "";
|
|
451
|
+
const id = typeof o.id === "string" ? o.id : String(idx + 1);
|
|
452
|
+
return text ? { id, text } : undefined;
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
})
|
|
456
|
+
.filter((entry) => Boolean(entry));
|
|
457
|
+
entries.push({
|
|
458
|
+
question,
|
|
459
|
+
...(isMultiSelect ? { isMultiSelect: true } : {}),
|
|
460
|
+
...(options.length > 0 ? { options } : {}),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return entries;
|
|
464
|
+
}
|
|
465
|
+
function str(value) {
|
|
466
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
467
|
+
}
|
|
468
|
+
/** Diffs bigger than this stay out of the event log; the phone shows the file path and summary instead. */
|
|
469
|
+
const MAX_DIFF_CHARS = 200_000;
|
|
470
|
+
function editContent(name, path, args) {
|
|
471
|
+
const diffs = [];
|
|
472
|
+
if (name === "write_to_file") {
|
|
473
|
+
const newText = typeof args.CodeContent === "string" ? args.CodeContent : undefined;
|
|
474
|
+
if (newText !== undefined)
|
|
475
|
+
diffs.push({ type: "diff", path, oldText: null, newText });
|
|
476
|
+
}
|
|
477
|
+
else if (name === "replace_file_content") {
|
|
478
|
+
const oldText = typeof args.TargetContent === "string" ? args.TargetContent : undefined;
|
|
479
|
+
const newText = typeof args.ReplacementContent === "string" ? args.ReplacementContent : undefined;
|
|
480
|
+
if (oldText !== undefined && newText !== undefined)
|
|
481
|
+
diffs.push({ type: "diff", path, oldText, newText });
|
|
482
|
+
}
|
|
483
|
+
else if (name === "multi_replace_file_content") {
|
|
484
|
+
const chunks = Array.isArray(args.ReplacementChunks) ? args.ReplacementChunks : [];
|
|
485
|
+
for (const chunk of chunks) {
|
|
486
|
+
if (!chunk || typeof chunk !== "object")
|
|
487
|
+
continue;
|
|
488
|
+
const rec = chunk;
|
|
489
|
+
if (typeof rec.TargetContent === "string" && typeof rec.ReplacementContent === "string") {
|
|
490
|
+
diffs.push({ type: "diff", path, oldText: rec.TargetContent, newText: rec.ReplacementContent });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const size = diffs.reduce((total, diff) => total + (diff.type === "diff" ? diff.newText.length + (diff.oldText?.length ?? 0) : 0), 0);
|
|
495
|
+
return diffs.length > 0 && size <= MAX_DIFF_CHARS ? diffs : undefined;
|
|
496
|
+
}
|
|
497
|
+
/** The sub-agent conversations a tool output names (invoke_subagent answers with their ids). */
|
|
498
|
+
function subagentIdsFromOutput(output) {
|
|
499
|
+
if (!output)
|
|
500
|
+
return [];
|
|
501
|
+
return [...output.matchAll(/"conversationId":\s*"([0-9a-f-]{36})"/gi)].map((match) => match[1]);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* invoke_subagent can start several sub-agents at once; the call itself carries
|
|
505
|
+
* the first, and each further one gets a call of its own so its work nests apart.
|
|
506
|
+
*/
|
|
507
|
+
function extraSpawns(call, toolCallId, output) {
|
|
508
|
+
if (call.name !== "invoke_subagent")
|
|
509
|
+
return [];
|
|
510
|
+
const ids = subagentIdsFromOutput(output);
|
|
511
|
+
const specs = (() => {
|
|
512
|
+
const args = parseToolArgs(call.args);
|
|
513
|
+
return Array.isArray(args.Subagents) ? args.Subagents : [];
|
|
514
|
+
})();
|
|
515
|
+
return ids.slice(1).map((nativeSessionId, offset) => {
|
|
516
|
+
const spec = specs[offset + 1] ?? {};
|
|
517
|
+
const role = str(spec.Role);
|
|
518
|
+
return {
|
|
519
|
+
sessionUpdate: "tool_call",
|
|
520
|
+
toolCallId: `${toolCallId}#${offset + 1}`,
|
|
521
|
+
title: role ?? "Sub-agent",
|
|
522
|
+
name: "invoke_subagent",
|
|
523
|
+
kind: "other",
|
|
524
|
+
status: "completed",
|
|
525
|
+
detail: {
|
|
526
|
+
type: "subagent",
|
|
527
|
+
action: "spawn",
|
|
528
|
+
...(str(spec.Prompt) ? { task: str(spec.Prompt).slice(0, 2000) } : {}),
|
|
529
|
+
...(str(spec.TypeName) ? { agentType: str(spec.TypeName) } : {}),
|
|
530
|
+
...(role ? { name: role } : {}),
|
|
531
|
+
nativeSessionId,
|
|
532
|
+
canCancel: true,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function mapAntigravityToolCall(stepIndex, callIndex, call, output, pending = false) {
|
|
399
538
|
const name = call.name ?? "tool";
|
|
400
539
|
const args = parseToolArgs(call.args);
|
|
401
540
|
const summary = (typeof args.toolSummary === "string" && args.toolSummary.trim()) ||
|
|
@@ -405,6 +544,8 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
405
544
|
let kind = "other";
|
|
406
545
|
let rawInput = args;
|
|
407
546
|
let locations;
|
|
547
|
+
let detail;
|
|
548
|
+
let content;
|
|
408
549
|
if (name === "run_command") {
|
|
409
550
|
kind = "execute";
|
|
410
551
|
rawInput = {
|
|
@@ -412,15 +553,33 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
412
553
|
cwd: typeof args.Cwd === "string" ? args.Cwd : undefined,
|
|
413
554
|
};
|
|
414
555
|
}
|
|
415
|
-
else if (name === "view_file") {
|
|
556
|
+
else if (name === "view_file" || name === "view_code_item") {
|
|
416
557
|
kind = "read";
|
|
417
|
-
const path =
|
|
558
|
+
const path = str(args.AbsolutePath) ?? str(args.File);
|
|
418
559
|
const line = typeof args.StartLine === "number" ? args.StartLine : undefined;
|
|
419
560
|
if (path) {
|
|
420
561
|
rawInput = { file_path: path };
|
|
421
562
|
locations = [{ path, ...(line ? { line } : {}) }];
|
|
422
563
|
}
|
|
423
564
|
}
|
|
565
|
+
else if (name === "list_dir") {
|
|
566
|
+
kind = "read";
|
|
567
|
+
const path = str(args.DirectoryPath);
|
|
568
|
+
if (path) {
|
|
569
|
+
rawInput = { path };
|
|
570
|
+
locations = [{ path }];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else if (name === "grep_search" || name === "find_by_name" || name === "codebase_search") {
|
|
574
|
+
kind = "search";
|
|
575
|
+
const path = str(args.SearchPath) ?? str(args.SearchDirectory) ?? str(args.DirectoryPath);
|
|
576
|
+
rawInput = {
|
|
577
|
+
query: str(args.Query) ?? str(args.Pattern) ?? summary,
|
|
578
|
+
...(path ? { path } : {}),
|
|
579
|
+
};
|
|
580
|
+
if (path)
|
|
581
|
+
locations = [{ path }];
|
|
582
|
+
}
|
|
424
583
|
else if (name === "replace_file_content" || name === "multi_replace_file_content" || name === "write_to_file") {
|
|
425
584
|
kind = "edit";
|
|
426
585
|
const path = typeof args.TargetFile === "string" ? args.TargetFile : undefined;
|
|
@@ -428,124 +587,146 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
428
587
|
if (path) {
|
|
429
588
|
rawInput = { file_path: path };
|
|
430
589
|
locations = [{ path, ...(line ? { line } : {}) }];
|
|
590
|
+
content = editContent(name, path, args);
|
|
431
591
|
}
|
|
432
592
|
}
|
|
433
593
|
else if (name === "search_web") {
|
|
434
594
|
kind = "search";
|
|
435
|
-
|
|
595
|
+
const query = typeof args.query === "string" ? args.query : summary;
|
|
596
|
+
rawInput = { query };
|
|
597
|
+
detail = { type: "web_search", query };
|
|
436
598
|
}
|
|
437
599
|
else if (name === "read_url_content") {
|
|
438
600
|
kind = "fetch";
|
|
439
601
|
rawInput = { url: typeof args.Url === "string" ? args.Url : summary };
|
|
440
602
|
}
|
|
441
|
-
|
|
603
|
+
else if (name === "call_mcp_tool" || name === "read_resource" || name === "list_resources") {
|
|
604
|
+
const server = str(args.ServerName) ?? "mcp";
|
|
605
|
+
const tool = str(args.ToolName) ?? (name === "call_mcp_tool" ? "tool" : name);
|
|
606
|
+
rawInput = (args.Arguments && typeof args.Arguments === "object" ? args.Arguments : args);
|
|
607
|
+
detail = { type: "mcp", server, tool };
|
|
608
|
+
}
|
|
609
|
+
else if (name === "invoke_subagent") {
|
|
610
|
+
const first = Array.isArray(args.Subagents) ? args.Subagents[0] : undefined;
|
|
611
|
+
const nativeSessionId = subagentIdsFromOutput(output?.text)[0];
|
|
612
|
+
detail = {
|
|
613
|
+
type: "subagent",
|
|
614
|
+
action: "spawn",
|
|
615
|
+
...(str(first?.Prompt) ? { task: str(first?.Prompt).slice(0, 2000) } : {}),
|
|
616
|
+
...(str(first?.TypeName) ? { agentType: str(first?.TypeName) } : {}),
|
|
617
|
+
...(str(first?.Role) ? { name: str(first?.Role) } : {}),
|
|
618
|
+
...(nativeSessionId ? { nativeSessionId, canCancel: true } : {}),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
else if (name === "send_message") {
|
|
622
|
+
const recipient = str(args.Recipient);
|
|
623
|
+
detail = { type: "subagent", action: "message", ...(recipient ? { nativeSessionId: recipient } : {}) };
|
|
624
|
+
}
|
|
625
|
+
else if (name === "manage_subagents") {
|
|
626
|
+
const action = str(args.Action);
|
|
627
|
+
detail = { type: "subagent", action: action === "list" ? "list" : "stop" };
|
|
628
|
+
}
|
|
629
|
+
else if (name === "ask_question") {
|
|
630
|
+
const entries = parseAskQuestionEntries(args);
|
|
631
|
+
if (entries.length > 0) {
|
|
632
|
+
detail = {
|
|
633
|
+
type: "question",
|
|
634
|
+
text: entries[0].question,
|
|
635
|
+
...(entries.length > 1 ? { more: entries.length - 1 } : {}),
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const cleanedOutput = output?.text || undefined;
|
|
442
640
|
return {
|
|
443
641
|
sessionUpdate: "tool_call",
|
|
444
642
|
toolCallId,
|
|
445
643
|
title: summary,
|
|
446
644
|
name,
|
|
447
645
|
kind,
|
|
448
|
-
status: "completed",
|
|
646
|
+
status: pending ? "in_progress" : output?.failed ? "failed" : "completed",
|
|
449
647
|
rawInput,
|
|
450
648
|
...(cleanedOutput ? { rawOutput: { output: cleanedOutput } } : {}),
|
|
649
|
+
...(content ? { content } : {}),
|
|
451
650
|
...(locations ? { locations } : {}),
|
|
651
|
+
...(detail ? { detail } : {}),
|
|
452
652
|
};
|
|
453
653
|
}
|
|
654
|
+
export function newTranscriptParseState() {
|
|
655
|
+
return { goal: null };
|
|
656
|
+
}
|
|
454
657
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
658
|
+
* Turns transcript steps into history items, carrying `state` across calls so
|
|
659
|
+
* the transcript can be read a few lines at a time. Tool calls whose outputs
|
|
660
|
+
* haven't been written yet are `in_progress`; their outputs arrive later as
|
|
661
|
+
* `tool_call_update`s.
|
|
458
662
|
*/
|
|
459
|
-
export function
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
let
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return { items: [], nextLine: fromLine };
|
|
468
|
-
}
|
|
469
|
-
let activeGoal = null;
|
|
470
|
-
let lastModelLabel;
|
|
471
|
-
// Scan lines prior to `fromLine` so incremental parsing retains active goal & model state.
|
|
472
|
-
const scanLimit = Math.min(fromLine, lines.length);
|
|
473
|
-
for (let i = 0; i < scanLimit; i++) {
|
|
474
|
-
try {
|
|
475
|
-
const step = JSON.parse(lines[i]);
|
|
476
|
-
if (step.type === "USER_INPUT" && typeof step.content === "string") {
|
|
477
|
-
const modelLabel = extractModelSelectionFromContent(step.content);
|
|
478
|
-
if (modelLabel)
|
|
479
|
-
lastModelLabel = modelLabel;
|
|
480
|
-
const rawText = cleanAntigravityUserInput(step.content);
|
|
481
|
-
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(rawText)) {
|
|
482
|
-
activeGoal = null;
|
|
483
|
-
}
|
|
484
|
-
else if (/^\/goal(?:\s+|$)/i.test(rawText) || /\/goal is a \[Slash Command\]:/i.test(step.content)) {
|
|
485
|
-
const objective = rawText.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
486
|
-
activeGoal = { objective, status: "active" };
|
|
487
|
-
}
|
|
488
|
-
else if (activeGoal?.status === "complete") {
|
|
489
|
-
activeGoal = null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
else if (step.type === "SYSTEM_MESSAGE") {
|
|
493
|
-
const nextGoal = parseGoalSystemMessage(step.content, activeGoal);
|
|
494
|
-
if (nextGoal)
|
|
495
|
-
activeGoal = nextGoal;
|
|
496
|
-
}
|
|
497
|
-
else if (step.type === "PLANNER_RESPONSE" && typeof step.content === "string") {
|
|
498
|
-
const { goalComplete, goalCancelled } = cleanAntigravityAgentContent(step.content);
|
|
499
|
-
if (goalCancelled) {
|
|
500
|
-
activeGoal = null;
|
|
501
|
-
}
|
|
502
|
-
else if (goalComplete) {
|
|
503
|
-
activeGoal = { ...(activeGoal ?? { objective: "持续目标" }), status: "complete" };
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
catch {
|
|
508
|
-
// ignore malformed historical line
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
if (fromLine >= lines.length)
|
|
512
|
-
return { items: [], nextLine: lines.length, lastModelLabel, goal: activeGoal };
|
|
513
|
-
const parsed = [];
|
|
514
|
-
for (let i = fromLine; i < lines.length; i++) {
|
|
515
|
-
try {
|
|
516
|
-
const step = JSON.parse(lines[i]);
|
|
517
|
-
parsed.push({ index: step.step_index ?? i, step });
|
|
518
|
-
}
|
|
519
|
-
catch {
|
|
520
|
-
// skip partial trailing JSON line
|
|
521
|
-
return { items: [], nextLine: i, lastModelLabel, goal: activeGoal };
|
|
663
|
+
export function parseTranscriptSteps(parsed, state) {
|
|
664
|
+
const items = [];
|
|
665
|
+
let pendingAskQuestion;
|
|
666
|
+
let lastPlanner = -1;
|
|
667
|
+
for (let i = parsed.length - 1; i >= 0; i--) {
|
|
668
|
+
if (parsed[i].step.type === "PLANNER_RESPONSE") {
|
|
669
|
+
lastPlanner = i;
|
|
670
|
+
break;
|
|
522
671
|
}
|
|
523
672
|
}
|
|
524
|
-
const items = [];
|
|
525
673
|
for (let i = 0; i < parsed.length; i++) {
|
|
526
674
|
const { index: stepIndex, step } = parsed[i];
|
|
675
|
+
state.lastStepIndex = stepIndex;
|
|
676
|
+
if (step.type === "GENERIC") {
|
|
677
|
+
const open = state.openCalls;
|
|
678
|
+
if (!open)
|
|
679
|
+
continue;
|
|
680
|
+
const callIdx = open.consumed;
|
|
681
|
+
const call = open.calls[callIdx];
|
|
682
|
+
open.consumed += 1;
|
|
683
|
+
if (open.consumed >= open.calls.length)
|
|
684
|
+
state.openCalls = undefined;
|
|
685
|
+
const text = toolOutputText(step);
|
|
686
|
+
if (call) {
|
|
687
|
+
const toolCallId = call.id || `agy:tc:${open.stepIndex}:${callIdx}`;
|
|
688
|
+
// The output is what names a spawned sub-agent's conversation, so its detail is only complete now.
|
|
689
|
+
const settled = mapAntigravityToolCall(open.stepIndex, callIdx, call, { text, failed: step.status === "ERROR" });
|
|
690
|
+
const detail = settled.sessionUpdate === "tool_call" ? settled.detail : undefined;
|
|
691
|
+
items.push({
|
|
692
|
+
itemId: `agy:tc-out:${stepIndex}`,
|
|
693
|
+
updates: [
|
|
694
|
+
{
|
|
695
|
+
sessionUpdate: "tool_call_update",
|
|
696
|
+
toolCallId,
|
|
697
|
+
status: step.status === "ERROR" ? "failed" : "completed",
|
|
698
|
+
...(detail ? { detail } : {}),
|
|
699
|
+
...(text ? { rawOutput: { output: text } } : {}),
|
|
700
|
+
},
|
|
701
|
+
...extraSpawns(call, toolCallId, text),
|
|
702
|
+
],
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
state.openCalls = undefined;
|
|
527
708
|
if (step.type === "USER_INPUT") {
|
|
528
709
|
if (typeof step.content === "string") {
|
|
529
710
|
const modelLabel = extractModelSelectionFromContent(step.content);
|
|
530
711
|
if (modelLabel)
|
|
531
|
-
lastModelLabel = modelLabel;
|
|
712
|
+
state.lastModelLabel = modelLabel;
|
|
532
713
|
}
|
|
533
714
|
const rawText = typeof step.content === "string" ? cleanAntigravityUserInput(step.content) : "";
|
|
534
715
|
const mediaBlocks = readStepMediaBlocks(step.media);
|
|
535
716
|
let displayText = rawText;
|
|
536
717
|
let goalUpdate;
|
|
537
718
|
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(rawText)) {
|
|
538
|
-
|
|
719
|
+
state.goal = null;
|
|
539
720
|
goalUpdate = { sessionUpdate: "ls_goal", goal: null };
|
|
540
721
|
}
|
|
541
722
|
else if (/^\/goal(?:\s+|$)/i.test(rawText) || (typeof step.content === "string" && /\/goal is a \[Slash Command\]:/i.test(step.content))) {
|
|
542
723
|
const objective = rawText.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
543
724
|
displayText = objective;
|
|
544
|
-
|
|
545
|
-
goalUpdate = { sessionUpdate: "ls_goal", goal:
|
|
725
|
+
state.goal = { objective, status: "active" };
|
|
726
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: state.goal };
|
|
546
727
|
}
|
|
547
|
-
else if (
|
|
548
|
-
|
|
728
|
+
else if (state.goal?.status === "complete") {
|
|
729
|
+
state.goal = null;
|
|
549
730
|
goalUpdate = { sessionUpdate: "ls_goal", goal: null };
|
|
550
731
|
}
|
|
551
732
|
if (!displayText && mediaBlocks.length === 0 && !goalUpdate)
|
|
@@ -553,35 +734,36 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
553
734
|
const messageId = `agy:user:${stepIndex}`;
|
|
554
735
|
const updates = [];
|
|
555
736
|
for (const mediaBlock of mediaBlocks) {
|
|
556
|
-
updates.push({
|
|
557
|
-
sessionUpdate: "user_message_chunk",
|
|
558
|
-
messageId,
|
|
559
|
-
content: mediaBlock,
|
|
560
|
-
});
|
|
737
|
+
updates.push({ sessionUpdate: "user_message_chunk", messageId, content: mediaBlock });
|
|
561
738
|
}
|
|
562
739
|
if (displayText) {
|
|
563
|
-
updates.push({
|
|
564
|
-
sessionUpdate: "user_message_chunk",
|
|
565
|
-
messageId,
|
|
566
|
-
content: { type: "text", text: displayText },
|
|
567
|
-
});
|
|
740
|
+
updates.push({ sessionUpdate: "user_message_chunk", messageId, content: { type: "text", text: displayText } });
|
|
568
741
|
}
|
|
569
|
-
if (goalUpdate)
|
|
742
|
+
if (goalUpdate)
|
|
570
743
|
updates.push(goalUpdate);
|
|
744
|
+
items.push({ itemId: messageId, updates });
|
|
745
|
+
}
|
|
746
|
+
else if (step.type === "SYSTEM_MESSAGE") {
|
|
747
|
+
const nextGoal = parseGoalSystemMessage(step.content, state.goal);
|
|
748
|
+
if (nextGoal) {
|
|
749
|
+
state.goal = nextGoal;
|
|
750
|
+
items.push({ itemId: `agy:goal:${stepIndex}`, updates: [{ sessionUpdate: "ls_goal", goal: nextGoal }] });
|
|
571
751
|
}
|
|
752
|
+
}
|
|
753
|
+
else if (step.type === "CHECKPOINT") {
|
|
754
|
+
// A compaction: the transcript keeps the summary the agent resumes from; the phone shows that it happened.
|
|
755
|
+
const toolCallId = `agy:checkpoint:${stepIndex}`;
|
|
572
756
|
items.push({
|
|
573
|
-
itemId:
|
|
574
|
-
updates,
|
|
757
|
+
itemId: toolCallId,
|
|
758
|
+
updates: [{ sessionUpdate: "tool_call", toolCallId, title: "上下文已压缩", kind: "think", status: "completed", detail: { type: "compaction" } }],
|
|
575
759
|
});
|
|
576
760
|
}
|
|
577
|
-
else if (step.type === "
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
updates: [{ sessionUpdate: "ls_goal", goal: activeGoal }],
|
|
584
|
-
});
|
|
761
|
+
else if (step.type === "ERROR_MESSAGE" || step.type === "CORTEX_STEP_TYPE_ERROR_MESSAGE") {
|
|
762
|
+
const errText = typeof step.content === "string" && step.content.trim()
|
|
763
|
+
? stripTruncationMarkers(step.content).trim()
|
|
764
|
+
: (step.error ?? "").trim();
|
|
765
|
+
if (errText) {
|
|
766
|
+
items.push({ itemId: `agy:err:${stepIndex}`, updates: [{ sessionUpdate: "ls_error", code: "agent_error", message: errText }] });
|
|
585
767
|
}
|
|
586
768
|
}
|
|
587
769
|
else if (step.type === "PLANNER_RESPONSE") {
|
|
@@ -590,107 +772,288 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
590
772
|
const cleanThought = stripTruncationMarkers(step.thinking).trim();
|
|
591
773
|
if (cleanThought) {
|
|
592
774
|
const thoughtId = `agy:thought:${stepIndex}`;
|
|
593
|
-
updates.push({
|
|
594
|
-
sessionUpdate: "agent_thought_chunk",
|
|
595
|
-
messageId: thoughtId,
|
|
596
|
-
content: { type: "text", text: cleanThought },
|
|
597
|
-
}, {
|
|
598
|
-
sessionUpdate: "ls_message_done",
|
|
599
|
-
messageId: thoughtId,
|
|
600
|
-
role: "thought",
|
|
601
|
-
});
|
|
775
|
+
updates.push({ sessionUpdate: "agent_thought_chunk", messageId: thoughtId, content: { type: "text", text: cleanThought } }, { sessionUpdate: "ls_message_done", messageId: thoughtId, role: "thought" });
|
|
602
776
|
}
|
|
603
777
|
}
|
|
604
778
|
const calls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
|
|
605
779
|
if (calls.length > 0) {
|
|
606
|
-
//
|
|
780
|
+
// The GENERIC steps that follow hold these calls' outputs, in order.
|
|
607
781
|
const outputs = [];
|
|
608
782
|
let lookahead = i + 1;
|
|
609
783
|
while (lookahead < parsed.length && parsed[lookahead].step.type === "GENERIC" && outputs.length < calls.length) {
|
|
610
|
-
|
|
784
|
+
const output = parsed[lookahead].step;
|
|
785
|
+
outputs.push({ text: toolOutputText(output), failed: output.status === "ERROR" });
|
|
611
786
|
lookahead++;
|
|
612
787
|
}
|
|
788
|
+
const atTail = lookahead >= parsed.length;
|
|
613
789
|
calls.forEach((call, callIdx) => {
|
|
614
|
-
|
|
790
|
+
const output = outputs[callIdx];
|
|
791
|
+
updates.push(mapAntigravityToolCall(stepIndex, callIdx, call, output, !output && atTail));
|
|
792
|
+
updates.push(...extraSpawns(call, call.id || `agy:tc:${stepIndex}:${callIdx}`, output?.text));
|
|
793
|
+
if (call.name === "ask_question" && !output && atTail) {
|
|
794
|
+
const args = parseToolArgs(call.args);
|
|
795
|
+
const askEntries = parseAskQuestionEntries(args);
|
|
796
|
+
if (askEntries.length > 0) {
|
|
797
|
+
pendingAskQuestion = {
|
|
798
|
+
stepIndex: stepIndex + 1 + callIdx,
|
|
799
|
+
toolCallId: call.id || `agy:tc:${stepIndex}:${callIdx}`,
|
|
800
|
+
title: str(args.toolSummary)?.trim() || str(args.toolAction)?.trim() || "Antigravity 有问题需要你回答",
|
|
801
|
+
questions: askEntries,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
}
|
|
615
805
|
});
|
|
806
|
+
if (outputs.length < calls.length && atTail) {
|
|
807
|
+
state.openCalls = { stepIndex, calls, consumed: outputs.length };
|
|
808
|
+
}
|
|
809
|
+
if (lookahead > i + 1) {
|
|
810
|
+
for (let j = i + 1; j < lookahead; j++)
|
|
811
|
+
state.lastStepIndex = parsed[j].index;
|
|
812
|
+
i = lookahead - 1;
|
|
813
|
+
}
|
|
616
814
|
}
|
|
617
815
|
if (typeof step.content === "string" && step.content.trim()) {
|
|
618
816
|
const { text: cleanText, goalComplete, goalCancelled } = cleanAntigravityAgentContent(step.content);
|
|
619
817
|
if (cleanText) {
|
|
620
818
|
const messageId = `agy:msg:${stepIndex}`;
|
|
621
|
-
updates.push({
|
|
622
|
-
sessionUpdate: "agent_message_chunk",
|
|
623
|
-
messageId,
|
|
624
|
-
content: { type: "text", text: cleanText },
|
|
625
|
-
}, {
|
|
626
|
-
sessionUpdate: "ls_message_done",
|
|
627
|
-
messageId,
|
|
628
|
-
role: "agent",
|
|
629
|
-
});
|
|
819
|
+
updates.push({ sessionUpdate: "agent_message_chunk", messageId, content: { type: "text", text: cleanText } }, { sessionUpdate: "ls_message_done", messageId, role: "agent" });
|
|
630
820
|
}
|
|
631
821
|
if (goalCancelled) {
|
|
632
|
-
|
|
822
|
+
state.goal = null;
|
|
633
823
|
updates.push({ sessionUpdate: "ls_goal", goal: null });
|
|
634
824
|
}
|
|
635
825
|
else if (goalComplete) {
|
|
636
|
-
|
|
637
|
-
updates.push({ sessionUpdate: "ls_goal", goal:
|
|
826
|
+
state.goal = { ...(state.goal ?? { objective: "持续目标" }), status: "complete" };
|
|
827
|
+
updates.push({ sessionUpdate: "ls_goal", goal: state.goal });
|
|
638
828
|
}
|
|
639
829
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
830
|
+
// The context in use is the latest reply's prompt; earlier replies in the batch are superseded.
|
|
831
|
+
if (i === lastPlanner || (lastPlanner >= 0 && parsed[lastPlanner].index === stepIndex)) {
|
|
832
|
+
const input = (step.input_tokens ?? 0) + (step.cache_read_tokens ?? 0);
|
|
833
|
+
if (input > 0) {
|
|
834
|
+
updates.push({
|
|
835
|
+
sessionUpdate: "usage_update",
|
|
836
|
+
usedTokens: input,
|
|
837
|
+
tokens: {
|
|
838
|
+
totalTokens: input + (step.output_tokens ?? 0),
|
|
839
|
+
inputTokens: step.input_tokens ?? 0,
|
|
840
|
+
outputTokens: step.output_tokens ?? 0,
|
|
841
|
+
cachedReadTokens: step.cache_read_tokens ?? 0,
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
}
|
|
645
845
|
}
|
|
846
|
+
if (updates.length > 0)
|
|
847
|
+
items.push({ itemId: `agy:step:${stepIndex}`, updates });
|
|
646
848
|
}
|
|
647
849
|
}
|
|
648
|
-
return { items,
|
|
850
|
+
return { items, ...(pendingAskQuestion ? { pendingAskQuestion } : {}) };
|
|
649
851
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
852
|
+
function parseTranscriptLines(lines, startLine) {
|
|
853
|
+
const parsed = [];
|
|
854
|
+
lines.forEach((line, offset) => {
|
|
855
|
+
if (!line.trim())
|
|
856
|
+
return;
|
|
857
|
+
try {
|
|
858
|
+
const step = JSON.parse(line);
|
|
859
|
+
parsed.push({ index: step.step_index ?? startLine + offset, step });
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// a malformed line can't be recovered; skip it rather than stall every later line
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return parsed;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Converts an Antigravity Desktop `transcript_full.jsonl` (or `transcript.jsonl`)
|
|
869
|
+
* file into LinkShell's `HistoryItem[]` list, tracking Goal Mode (`ls_goal`),
|
|
870
|
+
* model changes (`<USER_SETTINGS_CHANGE>`), and uploaded media. Lines before
|
|
871
|
+
* `fromLine` only rebuild state.
|
|
872
|
+
*/
|
|
873
|
+
export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
874
|
+
const state = newTranscriptParseState();
|
|
875
|
+
if (!existsSync(transcriptPath))
|
|
876
|
+
return { items: [], nextLine: fromLine, state, endOffset: 0 };
|
|
877
|
+
let text;
|
|
654
878
|
try {
|
|
655
|
-
|
|
656
|
-
const match = /listening on \w+ port at (\d+) for HTTP\b/i.exec(head);
|
|
657
|
-
if (match?.[1])
|
|
658
|
-
httpPort = Number(match[1]);
|
|
879
|
+
text = readFileSync(transcriptPath, "utf8");
|
|
659
880
|
}
|
|
660
881
|
catch {
|
|
661
|
-
return
|
|
882
|
+
return { items: [], nextLine: fromLine, state, endOffset: 0 };
|
|
883
|
+
}
|
|
884
|
+
// A line still being written has no newline yet: it is read next time, whole.
|
|
885
|
+
const complete = text.endsWith("\n") ? text : text.slice(0, text.lastIndexOf("\n") + 1);
|
|
886
|
+
const lines = complete.split("\n").filter((line) => line.trim().length > 0);
|
|
887
|
+
const scanLimit = Math.min(fromLine, lines.length);
|
|
888
|
+
parseTranscriptSteps(parseTranscriptLines(lines.slice(0, scanLimit), 0), state);
|
|
889
|
+
const result = parseTranscriptSteps(parseTranscriptLines(lines.slice(scanLimit), scanLimit), state);
|
|
890
|
+
return {
|
|
891
|
+
items: result.items,
|
|
892
|
+
nextLine: lines.length,
|
|
893
|
+
lastStepIndex: state.lastStepIndex,
|
|
894
|
+
lastModelLabel: state.lastModelLabel,
|
|
895
|
+
goal: state.goal,
|
|
896
|
+
...(result.pendingAskQuestion ? { pendingAskQuestion: result.pendingAskQuestion } : {}),
|
|
897
|
+
state,
|
|
898
|
+
endOffset: Buffer.byteLength(complete, "utf8"),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Follows a transcript by byte offset so a long conversation (tens of MB) is
|
|
903
|
+
* never re-read: each poll parses only the lines appended since the last.
|
|
904
|
+
*/
|
|
905
|
+
export class TranscriptTail {
|
|
906
|
+
path;
|
|
907
|
+
state;
|
|
908
|
+
offset;
|
|
909
|
+
remainder = Buffer.alloc(0);
|
|
910
|
+
constructor(path, state, offset) {
|
|
911
|
+
this.path = path;
|
|
912
|
+
this.state = state;
|
|
913
|
+
this.offset = offset;
|
|
914
|
+
}
|
|
915
|
+
read(currentPath) {
|
|
916
|
+
let skipThrough;
|
|
917
|
+
if (currentPath !== this.path) {
|
|
918
|
+
// transcript.jsonl gave way to transcript_full.jsonl: re-read, but only emit what is new.
|
|
919
|
+
this.path = currentPath;
|
|
920
|
+
this.offset = 0;
|
|
921
|
+
this.remainder = Buffer.alloc(0);
|
|
922
|
+
skipThrough = this.state.lastStepIndex;
|
|
923
|
+
}
|
|
924
|
+
let fd;
|
|
925
|
+
let chunk;
|
|
926
|
+
try {
|
|
927
|
+
fd = openSync(this.path, "r");
|
|
928
|
+
const size = fstatSync(fd).size;
|
|
929
|
+
if (size < this.offset) {
|
|
930
|
+
this.offset = 0;
|
|
931
|
+
this.remainder = Buffer.alloc(0);
|
|
932
|
+
skipThrough = this.state.lastStepIndex;
|
|
933
|
+
}
|
|
934
|
+
if (size === this.offset)
|
|
935
|
+
return { items: [], changed: false };
|
|
936
|
+
chunk = Buffer.alloc(size - this.offset);
|
|
937
|
+
const read = readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
938
|
+
chunk = chunk.subarray(0, read);
|
|
939
|
+
this.offset += read;
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
return { items: [], changed: false };
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
if (fd !== undefined)
|
|
946
|
+
closeSync(fd);
|
|
947
|
+
}
|
|
948
|
+
const data = this.remainder.length ? Buffer.concat([this.remainder, chunk]) : chunk;
|
|
949
|
+
const end = data.lastIndexOf(0x0a) + 1;
|
|
950
|
+
this.remainder = Buffer.from(data.subarray(end));
|
|
951
|
+
if (end === 0)
|
|
952
|
+
return { items: [], changed: true };
|
|
953
|
+
let steps = parseTranscriptLines(data.subarray(0, end).toString("utf8").split("\n"), 0);
|
|
954
|
+
if (skipThrough !== undefined)
|
|
955
|
+
steps = steps.filter((entry) => entry.index > skipThrough);
|
|
956
|
+
const result = parseTranscriptSteps(steps, this.state);
|
|
957
|
+
return { ...result, changed: true };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// Every watcher tick and every send needs the connection; finding it spawns `ps`, so it is kept while it answers.
|
|
961
|
+
const LS_CACHE_MS = 15_000;
|
|
962
|
+
let lsCache;
|
|
963
|
+
let lsLookup;
|
|
964
|
+
/** Whether the desktop app's language server was found running within the last minute. */
|
|
965
|
+
function languageServerSeen() {
|
|
966
|
+
return Boolean(lsCache && Date.now() - lsCache.at < 60_000);
|
|
967
|
+
}
|
|
968
|
+
async function lsAnswers(conn) {
|
|
969
|
+
try {
|
|
970
|
+
const response = await fetch(`http://127.0.0.1:${conn.httpPort}/exa.language_server_pb.LanguageServerService/GetStatus`, {
|
|
971
|
+
method: "POST",
|
|
972
|
+
headers: { "Content-Type": "application/json", "x-codeium-csrf-token": conn.csrfToken },
|
|
973
|
+
body: "{}",
|
|
974
|
+
signal: AbortSignal.timeout(2000),
|
|
975
|
+
});
|
|
976
|
+
return response.ok;
|
|
662
977
|
}
|
|
663
|
-
|
|
978
|
+
catch {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function findRunningLanguageServer(desktop) {
|
|
983
|
+
if (!desktop?.logPath || !existsSync(desktop.logPath))
|
|
984
|
+
return undefined;
|
|
985
|
+
const logPath = desktop.logPath;
|
|
986
|
+
if (lsCache && lsCache.logPath === logPath && Date.now() - lsCache.at < LS_CACHE_MS)
|
|
987
|
+
return lsCache.conn;
|
|
988
|
+
lsLookup ??= lookupLanguageServer(desktop, logPath).finally(() => {
|
|
989
|
+
lsLookup = undefined;
|
|
990
|
+
});
|
|
991
|
+
return lsLookup;
|
|
992
|
+
}
|
|
993
|
+
async function lookupLanguageServer(desktop, logPath) {
|
|
994
|
+
// The log starts over with each launch; if it was appended to instead, the latest launch's port is the last one.
|
|
995
|
+
const ports = [...readHead(logPath, 64 * 1024).matchAll(/listening on \w+ port at (\d+) for HTTP\b/gi)].map((match) => Number(match[1]));
|
|
996
|
+
const httpPort = ports.at(-1);
|
|
997
|
+
if (!httpPort) {
|
|
998
|
+
lsCache = undefined;
|
|
664
999
|
return undefined;
|
|
1000
|
+
}
|
|
1001
|
+
const tokens = [];
|
|
665
1002
|
try {
|
|
666
1003
|
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "args="], { timeout: 3000 });
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1004
|
+
const lines = stdout.split("\n").filter((line) => line.includes("language_server") && line.includes("--csrf_token"));
|
|
1005
|
+
// Another Antigravity product (the IDE) runs its own language_server: the desktop app's binary goes first.
|
|
1006
|
+
const own = (line) => (desktop.languageServerPath && line.includes(desktop.languageServerPath) ? 1 : 0);
|
|
1007
|
+
lines.sort((a, b) => own(b) - own(a));
|
|
1008
|
+
for (const line of lines) {
|
|
1009
|
+
const token = /--csrf_token[=\s]+([^\s]+)/.exec(line)?.[1];
|
|
1010
|
+
if (token && !tokens.includes(token))
|
|
1011
|
+
tokens.push(token);
|
|
674
1012
|
}
|
|
675
1013
|
}
|
|
676
1014
|
catch {
|
|
677
|
-
//
|
|
1015
|
+
// ps unavailable: nothing to authenticate with
|
|
678
1016
|
}
|
|
1017
|
+
for (const csrfToken of tokens) {
|
|
1018
|
+
const conn = { httpPort, csrfToken };
|
|
1019
|
+
if (tokens.length === 1 || (await lsAnswers(conn))) {
|
|
1020
|
+
lsCache = { logPath, at: Date.now(), conn };
|
|
1021
|
+
return conn;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
lsCache = undefined;
|
|
679
1025
|
return undefined;
|
|
680
1026
|
}
|
|
681
|
-
async function callLanguageServer(conn, method, body) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1027
|
+
async function callLanguageServer(conn, method, body, timeoutMs = 10_000) {
|
|
1028
|
+
let response;
|
|
1029
|
+
try {
|
|
1030
|
+
response = await fetch(`http://127.0.0.1:${conn.httpPort}/exa.language_server_pb.LanguageServerService/${method}`, {
|
|
1031
|
+
method: "POST",
|
|
1032
|
+
headers: {
|
|
1033
|
+
"Content-Type": "application/json",
|
|
1034
|
+
"x-codeium-csrf-token": conn.csrfToken,
|
|
1035
|
+
},
|
|
1036
|
+
body: JSON.stringify(body),
|
|
1037
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
// The app quit or restarted on another port: look it up again next time.
|
|
1042
|
+
lsCache = undefined;
|
|
1043
|
+
throw RpcError.app("agent_unavailable", `连不上 Antigravity 桌面端(${error instanceof Error ? error.message : String(error)}),请确认电脑上的 Antigravity 已打开`);
|
|
1044
|
+
}
|
|
691
1045
|
if (!response.ok) {
|
|
1046
|
+
if (response.status === 401 || response.status === 403)
|
|
1047
|
+
lsCache = undefined;
|
|
692
1048
|
const text = await response.text().catch(() => "");
|
|
693
|
-
|
|
1049
|
+
let message = text.slice(0, 300);
|
|
1050
|
+
try {
|
|
1051
|
+
message = JSON.parse(text).message?.slice(0, 300) || message;
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
1054
|
+
// not a Connect error body
|
|
1055
|
+
}
|
|
1056
|
+
throw new Error(`Antigravity 桌面端接口 ${method} 调用失败(${response.status}):${message}`);
|
|
694
1057
|
}
|
|
695
1058
|
return (await response.json());
|
|
696
1059
|
}
|
|
@@ -729,21 +1092,77 @@ function extForMimeType(mimeType) {
|
|
|
729
1092
|
return "gif";
|
|
730
1093
|
return "png";
|
|
731
1094
|
}
|
|
1095
|
+
/** Teamwork orchestrators spawn workers that spawn reviewers; deeper than this is not followed. */
|
|
1096
|
+
const MAX_SUBAGENT_DEPTH = 4;
|
|
1097
|
+
function namespaced(update, prefix) {
|
|
1098
|
+
const next = { ...update };
|
|
1099
|
+
if (typeof next.messageId === "string")
|
|
1100
|
+
next.messageId = `${prefix}${next.messageId}`;
|
|
1101
|
+
if (typeof next.toolCallId === "string")
|
|
1102
|
+
next.toolCallId = `${prefix}${next.toolCallId}`;
|
|
1103
|
+
return next;
|
|
1104
|
+
}
|
|
1105
|
+
/** The sub-agents a conversation's items start: their conversation and the call (in `prefix`'s id space) that started each. */
|
|
1106
|
+
function spawnsIn(items, prefix) {
|
|
1107
|
+
const found = [];
|
|
1108
|
+
for (const item of items) {
|
|
1109
|
+
for (const update of item.updates) {
|
|
1110
|
+
if (update.sessionUpdate !== "tool_call" && update.sessionUpdate !== "tool_call_update")
|
|
1111
|
+
continue;
|
|
1112
|
+
const detail = update.detail;
|
|
1113
|
+
if (detail?.type !== "subagent" || (detail.action ?? "spawn") !== "spawn" || !detail.nativeSessionId)
|
|
1114
|
+
continue;
|
|
1115
|
+
found.push({ childId: detail.nativeSessionId, toolCallId: `${prefix}${update.toolCallId}` });
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
return found;
|
|
1119
|
+
}
|
|
1120
|
+
const RUNNING_STATUSES = new Set(["CASCADE_RUN_STATUS_RUNNING", "CASCADE_RUN_STATUS_BUSY", "CASCADE_RUN_STATUS_CANCELING"]);
|
|
1121
|
+
const GOAL_STOP = /^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i;
|
|
1122
|
+
const SLASH = /^\/([A-Za-z0-9_.:-]+)(?:\s+([\s\S]*))?$/;
|
|
1123
|
+
function firstUserTitle(transcriptPath) {
|
|
1124
|
+
// Only the head: the first request is near the top and transcripts grow to tens of MB.
|
|
1125
|
+
for (const line of readHead(transcriptPath, 256 * 1024).split("\n")) {
|
|
1126
|
+
if (!line.includes("USER_INPUT"))
|
|
1127
|
+
continue;
|
|
1128
|
+
try {
|
|
1129
|
+
const step = JSON.parse(line);
|
|
1130
|
+
if (step.type !== "USER_INPUT" || typeof step.content !== "string")
|
|
1131
|
+
continue;
|
|
1132
|
+
const title = cleanAntigravityUserInput(step.content).split("\n")[0]?.slice(0, 100).trim();
|
|
1133
|
+
if (title)
|
|
1134
|
+
return title;
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
// a line cut off by the head limit
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return undefined;
|
|
1141
|
+
}
|
|
732
1142
|
/**
|
|
733
1143
|
* Specialized driver for Google Antigravity:
|
|
734
1144
|
* - Drives `agy_acp_server.par` / `agy-acp` over ACP when installed.
|
|
735
1145
|
* - Also discovers and interacts with Antigravity Desktop (`/Applications/Antigravity.app`,
|
|
736
1146
|
* `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/transcript_full.jsonl`,
|
|
737
1147
|
* and its local `language_server` Connect-RPC service) so users with only the Desktop app
|
|
738
|
-
* get full session visibility, history, Goal Mode,
|
|
1148
|
+
* get full session visibility, history, Goal Mode, slash commands and skills, image messaging,
|
|
1149
|
+
* user questions/permissions, model selection, steering, fork, rename, archive and delete.
|
|
739
1150
|
*/
|
|
740
1151
|
export class AntigravityDriver extends AcpDriver {
|
|
741
1152
|
desktopMode = false;
|
|
742
1153
|
desktopSummaryTimer;
|
|
1154
|
+
/** The state last reported to the host for each desktop conversation. */
|
|
743
1155
|
desktopSessionStates = new Map();
|
|
744
1156
|
desktopWatchers = new Map();
|
|
1157
|
+
/** Conversations a device has open: their watchers outlive the turn. */
|
|
1158
|
+
watchedSessions = new Set();
|
|
745
1159
|
desktopModelsCache;
|
|
746
1160
|
desktopDefaultModel;
|
|
1161
|
+
desktopRowsCache;
|
|
1162
|
+
slashCache = new Map();
|
|
1163
|
+
generatorCache = new Map();
|
|
1164
|
+
archivedCache;
|
|
1165
|
+
untitledCache = new Map();
|
|
747
1166
|
constructor(options) {
|
|
748
1167
|
super(antigravitySpec(), options);
|
|
749
1168
|
}
|
|
@@ -755,11 +1174,103 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
755
1174
|
...resolved?.env,
|
|
756
1175
|
};
|
|
757
1176
|
}
|
|
1177
|
+
/**
|
|
1178
|
+
* The desktop app takes messages mid-turn (its message_delivery hook hands them
|
|
1179
|
+
* to the running turn), forks at any reply, and its transcript is the history.
|
|
1180
|
+
*/
|
|
1181
|
+
get behavior() {
|
|
1182
|
+
if (!this.desktopMode)
|
|
1183
|
+
return super.behavior;
|
|
1184
|
+
return {
|
|
1185
|
+
desktop: this.spec.tier,
|
|
1186
|
+
history: "native-snapshot",
|
|
1187
|
+
turnCompletion: "native-events",
|
|
1188
|
+
busyInput: "concurrent-prompt",
|
|
1189
|
+
fork: "native-turn",
|
|
1190
|
+
detach: "local-only",
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
desktop() {
|
|
1194
|
+
return resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
1195
|
+
}
|
|
1196
|
+
async languageServer() {
|
|
1197
|
+
return findRunningLanguageServer(this.desktop());
|
|
1198
|
+
}
|
|
1199
|
+
async requireConnection(action = "继续") {
|
|
1200
|
+
const conn = await this.languageServer();
|
|
1201
|
+
if (!conn)
|
|
1202
|
+
throw RpcError.app("agent_unavailable", `请先在电脑上打开 Antigravity 桌面端应用,再${action}`);
|
|
1203
|
+
return conn;
|
|
1204
|
+
}
|
|
1205
|
+
isDesktopSession(nativeId) {
|
|
1206
|
+
if (this.desktopMode || this.desktopWatchers.has(nativeId))
|
|
1207
|
+
return true;
|
|
1208
|
+
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
1209
|
+
return Boolean(transcriptPath && existsSync(transcriptPath));
|
|
1210
|
+
}
|
|
1211
|
+
/** Every conversation row, read at most every two seconds however many watchers ask. */
|
|
1212
|
+
desktopRows() {
|
|
1213
|
+
if (this.desktopRowsCache && Date.now() - this.desktopRowsCache.at < 2000)
|
|
1214
|
+
return this.desktopRowsCache.rows;
|
|
1215
|
+
const desktop = this.desktop();
|
|
1216
|
+
if (!desktop)
|
|
1217
|
+
return undefined;
|
|
1218
|
+
const dbPath = join(desktop.dataDir, "conversation_summaries.db");
|
|
1219
|
+
if (!existsSync(dbPath))
|
|
1220
|
+
return undefined;
|
|
1221
|
+
let db;
|
|
1222
|
+
try {
|
|
1223
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
1224
|
+
const rows = db
|
|
1225
|
+
.prepare(`SELECT conversation_id, title, last_modified_time, workspace_uris, status, parent_conversation_id, killed FROM conversation_summaries`)
|
|
1226
|
+
.all();
|
|
1227
|
+
const map = new Map();
|
|
1228
|
+
for (const row of rows) {
|
|
1229
|
+
if (!row.conversation_id)
|
|
1230
|
+
continue;
|
|
1231
|
+
map.set(row.conversation_id, {
|
|
1232
|
+
id: row.conversation_id,
|
|
1233
|
+
title: row.title?.trim() ?? "",
|
|
1234
|
+
lastModified: Date.parse(row.last_modified_time) || 0,
|
|
1235
|
+
workspaceUris: row.workspace_uris,
|
|
1236
|
+
status: row.status ?? "",
|
|
1237
|
+
parent: row.parent_conversation_id ?? "",
|
|
1238
|
+
killed: Boolean(row.killed),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
this.desktopRowsCache = { at: Date.now(), rows: map };
|
|
1242
|
+
return map;
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
// locked or mid-migration: keep the last good read
|
|
1246
|
+
return this.desktopRowsCache?.rows;
|
|
1247
|
+
}
|
|
1248
|
+
finally {
|
|
1249
|
+
try {
|
|
1250
|
+
db?.close();
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
// ignore close errors
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
sessionExists(nativeId, _cwd) {
|
|
1258
|
+
const desktop = this.desktop();
|
|
1259
|
+
if (!desktop)
|
|
1260
|
+
return true;
|
|
1261
|
+
if (this.desktopWatchers.has(nativeId))
|
|
1262
|
+
return true;
|
|
1263
|
+
const brainDir = join(desktop.dataDir, "brain", nativeId);
|
|
1264
|
+
const row = this.desktopRows()?.get(nativeId);
|
|
1265
|
+
if (row)
|
|
1266
|
+
return !row.killed && existsSync(brainDir);
|
|
1267
|
+
return existsSync(brainDir);
|
|
1268
|
+
}
|
|
758
1269
|
async authStatus() {
|
|
759
1270
|
const base = await super.authStatus();
|
|
760
1271
|
if (base.state !== "unknown")
|
|
761
1272
|
return base;
|
|
762
|
-
const desktop =
|
|
1273
|
+
const desktop = this.desktop();
|
|
763
1274
|
if (desktop && (existsSync(join(desktop.dataDir, "antigravity_state.pbtxt")) || existsSync(join(desktop.dataDir, "conversation_summaries.db")))) {
|
|
764
1275
|
return { state: "ok", method: "antigravity-desktop" };
|
|
765
1276
|
}
|
|
@@ -781,6 +1292,35 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
781
1292
|
}
|
|
782
1293
|
return undefined;
|
|
783
1294
|
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Reports conversations that started or stopped on the desktop. A running one
|
|
1297
|
+
* is followed (attached), and from then on its watcher reports its turns.
|
|
1298
|
+
*/
|
|
1299
|
+
syncDesktopSummaries() {
|
|
1300
|
+
// Keeps languageServerSeen() current (cached: a lookup at most every 15 s).
|
|
1301
|
+
void this.languageServer();
|
|
1302
|
+
for (const s of this.listDesktopSessions(30)) {
|
|
1303
|
+
const watcher = this.desktopWatchers.get(s.nativeId);
|
|
1304
|
+
const next = s.state === "running" || s.state === "waiting" ? s.state : "idle";
|
|
1305
|
+
const prev = this.desktopSessionStates.get(s.nativeId);
|
|
1306
|
+
if (prev === next)
|
|
1307
|
+
continue;
|
|
1308
|
+
this.desktopSessionStates.set(s.nativeId, next);
|
|
1309
|
+
// The first look only records where things stand; an idle conversation has nothing to say.
|
|
1310
|
+
if (prev === undefined && next === "idle")
|
|
1311
|
+
continue;
|
|
1312
|
+
this.host?.sessionSeen(this.id, { ...s, state: next });
|
|
1313
|
+
if (watcher)
|
|
1314
|
+
continue;
|
|
1315
|
+
if (next !== "idle") {
|
|
1316
|
+
void this.host?.follow(this.id, s.nativeId).catch(() => { });
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
this.host?.update(this.id, s.nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
1320
|
+
this.host?.update(this.id, s.nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
784
1324
|
async boot() {
|
|
785
1325
|
const env = this.options.env ?? process.env;
|
|
786
1326
|
const resolved = resolveAntigravityLaunch(env);
|
|
@@ -796,31 +1336,15 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
796
1336
|
this.desktopMode = true;
|
|
797
1337
|
this.capabilityState = {
|
|
798
1338
|
...this.capabilityState,
|
|
1339
|
+
permissions: true,
|
|
799
1340
|
images: true,
|
|
800
1341
|
models: true,
|
|
801
1342
|
};
|
|
802
1343
|
this.current = { installed: true, version: desktop.version };
|
|
803
1344
|
if (!this.desktopSummaryTimer) {
|
|
804
1345
|
this.desktopSummaryTimer = setInterval(() => {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const prev = this.desktopSessionStates.get(s.nativeId);
|
|
808
|
-
this.desktopSessionStates.set(s.nativeId, nextState);
|
|
809
|
-
if (prev !== undefined && prev !== nextState) {
|
|
810
|
-
if (nextState === "running") {
|
|
811
|
-
this.host?.update(this.id, s.nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
812
|
-
}
|
|
813
|
-
else {
|
|
814
|
-
this.host?.update(this.id, s.nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
815
|
-
}
|
|
816
|
-
this.host?.update(this.id, s.nativeId, {
|
|
817
|
-
sessionUpdate: "ls_status",
|
|
818
|
-
state: nextState,
|
|
819
|
-
turnActive: nextState === "running",
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}, 3000);
|
|
1346
|
+
this.syncDesktopSummaries();
|
|
1347
|
+
}, 2500);
|
|
824
1348
|
this.desktopSummaryTimer.unref?.();
|
|
825
1349
|
}
|
|
826
1350
|
return;
|
|
@@ -837,8 +1361,21 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
837
1361
|
this.desktopWatchers.clear();
|
|
838
1362
|
await super.stop();
|
|
839
1363
|
}
|
|
1364
|
+
watched(nativeId, open) {
|
|
1365
|
+
if (open) {
|
|
1366
|
+
this.watchedSessions.add(nativeId);
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
this.watchedSessions.delete(nativeId);
|
|
1370
|
+
// The last device left an idle conversation: stop polling it until it runs or is opened again.
|
|
1371
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1372
|
+
if (watcher && !watcher.running && !watcher.pendingAction) {
|
|
1373
|
+
this.stopWatching(nativeId);
|
|
1374
|
+
this.host?.detached(this.id, nativeId);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
840
1377
|
transcriptPathFor(nativeId) {
|
|
841
|
-
const desktop =
|
|
1378
|
+
const desktop = this.desktop();
|
|
842
1379
|
if (!desktop)
|
|
843
1380
|
return undefined;
|
|
844
1381
|
return resolveDesktopTranscriptPath(desktop.dataDir, nativeId);
|
|
@@ -862,128 +1399,101 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
862
1399
|
// ignore state write errors
|
|
863
1400
|
}
|
|
864
1401
|
}
|
|
865
|
-
isConversationRunningInDesktop(nativeId
|
|
866
|
-
const desktop =
|
|
1402
|
+
isConversationRunningInDesktop(nativeId) {
|
|
1403
|
+
const desktop = this.desktop();
|
|
867
1404
|
if (!desktop)
|
|
868
1405
|
return false;
|
|
869
|
-
|
|
870
|
-
|
|
1406
|
+
// With the app closed nothing runs, whatever status it saved last. (presence/<id>.lock only means a window
|
|
1407
|
+
// shows the conversation: sub-agents and conversations started from the phone never get one.)
|
|
1408
|
+
if (!languageServerSeen())
|
|
871
1409
|
return false;
|
|
872
|
-
|
|
873
|
-
if (status
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
.prepare("SELECT status FROM conversation_summaries WHERE conversation_id = ?")
|
|
881
|
-
.get(nativeId);
|
|
882
|
-
status = row?.status;
|
|
883
|
-
}
|
|
884
|
-
catch {
|
|
885
|
-
// ignore
|
|
886
|
-
}
|
|
887
|
-
finally {
|
|
888
|
-
try {
|
|
889
|
-
db?.close();
|
|
890
|
-
}
|
|
891
|
-
catch {
|
|
892
|
-
// ignore
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
1410
|
+
const status = this.desktopRows()?.get(nativeId)?.status;
|
|
1411
|
+
if (status)
|
|
1412
|
+
return RUNNING_STATUSES.has(status);
|
|
1413
|
+
// No saved status yet (a brand-new conversation): a transcript still being written is a running one.
|
|
1414
|
+
const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, nativeId);
|
|
1415
|
+
try {
|
|
1416
|
+
const ageMs = Date.now() - statSync(transcriptPath).mtimeMs;
|
|
1417
|
+
return ageMs >= 0 && ageMs < 15_000;
|
|
896
1418
|
}
|
|
897
|
-
|
|
898
|
-
|
|
1419
|
+
catch {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
untitledTitle(nativeId, transcriptPath) {
|
|
1424
|
+
try {
|
|
1425
|
+
const mtimeMs = statSync(transcriptPath).mtimeMs;
|
|
1426
|
+
const cached = this.untitledCache.get(nativeId);
|
|
1427
|
+
if (cached && (cached.title || cached.mtimeMs === mtimeMs))
|
|
1428
|
+
return cached.title;
|
|
1429
|
+
const title = firstUserTitle(transcriptPath);
|
|
1430
|
+
this.untitledCache.set(nativeId, { mtimeMs, title });
|
|
1431
|
+
return title;
|
|
1432
|
+
}
|
|
1433
|
+
catch {
|
|
1434
|
+
return undefined;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
899
1437
|
listDesktopSessions(limit) {
|
|
900
|
-
const desktop =
|
|
1438
|
+
const desktop = this.desktop();
|
|
901
1439
|
if (!desktop)
|
|
902
1440
|
return [];
|
|
903
|
-
const dbPath = join(desktop.dataDir, "conversation_summaries.db");
|
|
904
1441
|
const home = (this.options.env ?? process.env).HOME || homedir();
|
|
905
1442
|
const results = [];
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
this.desktopSessionStates.set(row.conversation_id, state);
|
|
938
|
-
}
|
|
939
|
-
results.push({
|
|
940
|
-
nativeId: row.conversation_id,
|
|
941
|
-
cwd,
|
|
942
|
-
title,
|
|
943
|
-
...(model ? { model } : {}),
|
|
944
|
-
createdAt: updatedAt,
|
|
945
|
-
updatedAt,
|
|
946
|
-
state,
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
catch {
|
|
951
|
-
// fallback to brain directory scan below if sqlite table is locked or unreadable
|
|
952
|
-
}
|
|
953
|
-
finally {
|
|
954
|
-
try {
|
|
955
|
-
db?.close();
|
|
956
|
-
}
|
|
957
|
-
catch {
|
|
958
|
-
// ignore close errors
|
|
959
|
-
}
|
|
1443
|
+
const rows = this.desktopRows();
|
|
1444
|
+
if (rows) {
|
|
1445
|
+
// Sub-agent conversations nest under their parent's tool calls rather than listing on their own.
|
|
1446
|
+
const top = [...rows.values()]
|
|
1447
|
+
.filter((row) => !row.parent && !row.killed)
|
|
1448
|
+
.sort((a, b) => b.lastModified - a.lastModified);
|
|
1449
|
+
for (const row of top) {
|
|
1450
|
+
if (results.length >= limit)
|
|
1451
|
+
break;
|
|
1452
|
+
const brainDir = join(desktop.dataDir, "brain", row.id);
|
|
1453
|
+
if (!existsSync(brainDir) && !this.desktopWatchers.has(row.id))
|
|
1454
|
+
continue;
|
|
1455
|
+
const cwd = parseWorkspaceCwd(row.workspaceUris, home);
|
|
1456
|
+
const updatedAt = row.lastModified || Date.now();
|
|
1457
|
+
const title = row.title || this.untitledTitle(row.id, resolveDesktopTranscriptPath(desktop.dataDir, row.id));
|
|
1458
|
+
const watcher = this.desktopWatchers.get(row.id);
|
|
1459
|
+
// A followed conversation's watcher knows best (it also covers a turn the phone just started).
|
|
1460
|
+
const state = watcher
|
|
1461
|
+
? watcher.pendingAction ? "waiting" : watcher.running ? "running" : "idle"
|
|
1462
|
+
: this.isConversationRunningInDesktop(row.id)
|
|
1463
|
+
? "running"
|
|
1464
|
+
: "idle";
|
|
1465
|
+
results.push({
|
|
1466
|
+
nativeId: row.id,
|
|
1467
|
+
cwd,
|
|
1468
|
+
...(title ? { title } : {}),
|
|
1469
|
+
...(watcher?.lastModelLabel ? { model: watcher.lastModelLabel } : {}),
|
|
1470
|
+
createdAt: updatedAt,
|
|
1471
|
+
updatedAt,
|
|
1472
|
+
state,
|
|
1473
|
+
});
|
|
960
1474
|
}
|
|
1475
|
+
return results;
|
|
961
1476
|
}
|
|
962
|
-
if (
|
|
1477
|
+
if (existsSync(join(desktop.dataDir, "brain"))) {
|
|
963
1478
|
try {
|
|
964
|
-
const
|
|
965
|
-
for (const id of
|
|
1479
|
+
const entries = [];
|
|
1480
|
+
for (const id of readdirSync(join(desktop.dataDir, "brain"))) {
|
|
966
1481
|
const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, id);
|
|
967
1482
|
if (!existsSync(transcriptPath))
|
|
968
1483
|
continue;
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const title =
|
|
974
|
-
? chunk.content.text.split("\n")[0]?.slice(0, 100).trim()
|
|
975
|
-
: undefined;
|
|
1484
|
+
entries.push({ id, path: transcriptPath, mtimeMs: statSync(transcriptPath).mtimeMs });
|
|
1485
|
+
}
|
|
1486
|
+
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1487
|
+
for (const entry of entries.slice(0, limit)) {
|
|
1488
|
+
const title = this.untitledTitle(entry.id, entry.path);
|
|
976
1489
|
results.push({
|
|
977
|
-
nativeId: id,
|
|
1490
|
+
nativeId: entry.id,
|
|
978
1491
|
cwd: home,
|
|
979
|
-
title,
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
state: "idle",
|
|
1492
|
+
...(title ? { title } : {}),
|
|
1493
|
+
createdAt: entry.mtimeMs,
|
|
1494
|
+
updatedAt: entry.mtimeMs,
|
|
1495
|
+
state: this.isConversationRunningInDesktop(entry.id) ? "running" : "idle",
|
|
984
1496
|
});
|
|
985
|
-
if (results.length >= limit)
|
|
986
|
-
break;
|
|
987
1497
|
}
|
|
988
1498
|
}
|
|
989
1499
|
catch {
|
|
@@ -994,21 +1504,49 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
994
1504
|
}
|
|
995
1505
|
async listSessions(limit) {
|
|
996
1506
|
const acpSessions = !this.desktopMode && this.connection?.alive ? await super.listSessions(limit).catch(() => []) : [];
|
|
1507
|
+
await this.languageServer();
|
|
997
1508
|
const desktopSessions = this.listDesktopSessions(limit);
|
|
998
1509
|
const merged = new Map();
|
|
999
1510
|
for (const session of [...acpSessions, ...desktopSessions]) {
|
|
1000
1511
|
if (!merged.has(session.nativeId))
|
|
1001
1512
|
merged.set(session.nativeId, session);
|
|
1002
1513
|
}
|
|
1003
|
-
|
|
1514
|
+
const sorted = [...merged.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
|
|
1515
|
+
for (const session of sorted) {
|
|
1516
|
+
// Running on the desktop before anyone opened it: follow it so its turn shows live.
|
|
1517
|
+
if ((session.state === "running" || session.state === "waiting") && !this.desktopWatchers.has(session.nativeId)) {
|
|
1518
|
+
void this.host?.follow(this.id, session.nativeId).catch(() => { });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return sorted;
|
|
1522
|
+
}
|
|
1523
|
+
async archivedSessions() {
|
|
1524
|
+
if (!this.desktopMode)
|
|
1525
|
+
return [];
|
|
1526
|
+
if (this.archivedCache && Date.now() - this.archivedCache.at < 30_000)
|
|
1527
|
+
return this.archivedCache.ids;
|
|
1528
|
+
const conn = await this.languageServer();
|
|
1529
|
+
if (!conn)
|
|
1530
|
+
return this.archivedCache?.ids ?? [];
|
|
1531
|
+
try {
|
|
1532
|
+
const data = await callLanguageServer(conn, "GetAllCascadeTrajectories", {});
|
|
1533
|
+
const ids = Object.entries(data.trajectorySummaries ?? {})
|
|
1534
|
+
.filter(([, summary]) => summary.annotations?.archived === true)
|
|
1535
|
+
.map(([id]) => id);
|
|
1536
|
+
this.archivedCache = { at: Date.now(), ids };
|
|
1537
|
+
return ids;
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
return this.archivedCache?.ids ?? [];
|
|
1541
|
+
}
|
|
1004
1542
|
}
|
|
1005
1543
|
async loadDesktopModels() {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const conn = await
|
|
1544
|
+
// New models ship with app updates; a running host picks them up within ten minutes.
|
|
1545
|
+
if (this.desktopModelsCache?.models.length && Date.now() - this.desktopModelsCache.at < 10 * 60_000)
|
|
1546
|
+
return this.desktopModelsCache.models;
|
|
1547
|
+
const conn = await this.languageServer();
|
|
1010
1548
|
if (!conn)
|
|
1011
|
-
return [];
|
|
1549
|
+
return this.desktopModelsCache?.models ?? [];
|
|
1012
1550
|
try {
|
|
1013
1551
|
const data = await callLanguageServer(conn, "GetCascadeModelConfigData", {});
|
|
1014
1552
|
const defaultOverride = data.defaultOverrideModelConfig?.modelOrAlias?.model ??
|
|
@@ -1042,13 +1580,76 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1042
1580
|
});
|
|
1043
1581
|
}
|
|
1044
1582
|
if (models.length > 0)
|
|
1045
|
-
this.desktopModelsCache = models;
|
|
1583
|
+
this.desktopModelsCache = { at: Date.now(), models };
|
|
1046
1584
|
return models;
|
|
1047
1585
|
}
|
|
1048
1586
|
catch {
|
|
1049
|
-
return [];
|
|
1587
|
+
return this.desktopModelsCache?.models ?? [];
|
|
1050
1588
|
}
|
|
1051
1589
|
}
|
|
1590
|
+
/**
|
|
1591
|
+
* The model and command auto-run policy the desktop app last used for a
|
|
1592
|
+
* conversation (or, for a new one, for the latest conversation): sending from
|
|
1593
|
+
* the phone must not quietly change either.
|
|
1594
|
+
*/
|
|
1595
|
+
generatorInfo(conn, nativeId) {
|
|
1596
|
+
const sourceId = nativeId || this.latestConversationId();
|
|
1597
|
+
if (!sourceId)
|
|
1598
|
+
return Promise.resolve({});
|
|
1599
|
+
const cached = this.generatorCache.get(sourceId);
|
|
1600
|
+
if (cached && Date.now() - cached.at < 5 * 60_000)
|
|
1601
|
+
return cached.info;
|
|
1602
|
+
// The metadata holds every generation's full config (MBs for a long conversation): one request at a time per conversation.
|
|
1603
|
+
const info = this.fetchGeneratorInfo(conn, sourceId);
|
|
1604
|
+
this.generatorCache.set(sourceId, { at: Date.now(), info });
|
|
1605
|
+
return info;
|
|
1606
|
+
}
|
|
1607
|
+
async fetchGeneratorInfo(conn, sourceId) {
|
|
1608
|
+
try {
|
|
1609
|
+
const meta = await callLanguageServer(conn, "GetCascadeTrajectoryGeneratorMetadata", { cascadeId: sourceId }, 20_000);
|
|
1610
|
+
const entries = meta.generatorMetadata ?? [];
|
|
1611
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1612
|
+
const planner = entries[i].plannerConfig;
|
|
1613
|
+
if (!planner)
|
|
1614
|
+
continue;
|
|
1615
|
+
return {
|
|
1616
|
+
model: planner.requestedModel?.model ?? planner.requestedModel?.alias ?? planner.planModel ?? entries[i].chatModel?.model,
|
|
1617
|
+
autoExecutionPolicy: planner.toolConfig?.runCommand?.autoCommandConfig?.autoExecutionPolicy,
|
|
1618
|
+
...(typeof planner.useAiCredits === "boolean" ? { useAiCredits: planner.useAiCredits } : {}),
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
catch {
|
|
1623
|
+
// metadata unavailable: the language server's defaults apply
|
|
1624
|
+
}
|
|
1625
|
+
return {};
|
|
1626
|
+
}
|
|
1627
|
+
/** A conversation the desktop hasn't generated in yet takes the settings the user last used anywhere. */
|
|
1628
|
+
async inheritedSettings(conn, nativeId) {
|
|
1629
|
+
const own = await this.generatorInfo(conn, nativeId);
|
|
1630
|
+
if (own.autoExecutionPolicy)
|
|
1631
|
+
return own;
|
|
1632
|
+
const latest = this.latestConversationId(nativeId);
|
|
1633
|
+
const fallback = latest ? await this.generatorInfo(conn, latest) : {};
|
|
1634
|
+
return {
|
|
1635
|
+
model: own.model ?? fallback.model,
|
|
1636
|
+
autoExecutionPolicy: fallback.autoExecutionPolicy,
|
|
1637
|
+
useAiCredits: own.useAiCredits ?? fallback.useAiCredits,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
latestConversationId(exclude) {
|
|
1641
|
+
const rows = this.desktopRows();
|
|
1642
|
+
if (!rows)
|
|
1643
|
+
return undefined;
|
|
1644
|
+
let latest;
|
|
1645
|
+
for (const row of rows.values()) {
|
|
1646
|
+
if (row.parent || row.killed || row.id === exclude)
|
|
1647
|
+
continue;
|
|
1648
|
+
if (!latest || row.lastModified > latest.lastModified)
|
|
1649
|
+
latest = row;
|
|
1650
|
+
}
|
|
1651
|
+
return latest?.id;
|
|
1652
|
+
}
|
|
1052
1653
|
async resolveSessionModel(nativeId, models, explicitModel) {
|
|
1053
1654
|
const matchByValueOrName = (candidate) => {
|
|
1054
1655
|
if (!candidate)
|
|
@@ -1056,37 +1657,23 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1056
1657
|
return models.find((m) => m.value === candidate || m.name === candidate);
|
|
1057
1658
|
};
|
|
1058
1659
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
1059
|
-
const persistedSelected = this.host?.state(this.id, nativeId).get("agy-selected-model");
|
|
1660
|
+
const persistedSelected = nativeId ? this.host?.state(this.id, nativeId).get("agy-selected-model") : undefined;
|
|
1060
1661
|
const chosen = matchByValueOrName(explicitModel) ??
|
|
1061
1662
|
matchByValueOrName(watcher?.selectedModel) ??
|
|
1062
|
-
matchByValueOrName(
|
|
1063
|
-
matchByValueOrName(
|
|
1663
|
+
matchByValueOrName(watcher?.lastModelLabel) ??
|
|
1664
|
+
matchByValueOrName(persistedSelected);
|
|
1064
1665
|
if (chosen)
|
|
1065
1666
|
return chosen;
|
|
1066
|
-
const
|
|
1067
|
-
const conn = await findRunningLanguageServer(desktop);
|
|
1667
|
+
const conn = nativeId ? await this.languageServer() : undefined;
|
|
1068
1668
|
if (conn) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1073
|
-
const entry = entries[i];
|
|
1074
|
-
const candidate = entry.plannerConfig?.requestedModel?.model ??
|
|
1075
|
-
entry.plannerConfig?.requestedModel?.alias ??
|
|
1076
|
-
entry.plannerConfig?.planModel ??
|
|
1077
|
-
entry.chatModel?.model;
|
|
1078
|
-
const found = matchByValueOrName(candidate);
|
|
1079
|
-
if (found)
|
|
1080
|
-
return found;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
catch {
|
|
1084
|
-
// ignore metadata RPC failure
|
|
1085
|
-
}
|
|
1669
|
+
const found = matchByValueOrName((await this.generatorInfo(conn, nativeId)).model);
|
|
1670
|
+
if (found)
|
|
1671
|
+
return found;
|
|
1086
1672
|
}
|
|
1087
1673
|
const fromDefault = matchByValueOrName(this.desktopDefaultModel);
|
|
1088
1674
|
if (fromDefault)
|
|
1089
1675
|
return fromDefault;
|
|
1676
|
+
const desktop = this.desktop();
|
|
1090
1677
|
if (desktop && existsSync(join(desktop.dataDir, "antigravity_state.pbtxt"))) {
|
|
1091
1678
|
try {
|
|
1092
1679
|
const stateText = readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8");
|
|
@@ -1110,36 +1697,69 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1110
1697
|
}
|
|
1111
1698
|
if (explicitModel?.startsWith("MODEL_"))
|
|
1112
1699
|
return explicitModel;
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const entries = meta.generatorMetadata ?? [];
|
|
1117
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1118
|
-
const entry = entries[i];
|
|
1119
|
-
const candidate = entry.plannerConfig?.planModel ??
|
|
1120
|
-
entry.plannerConfig?.requestedModel?.model ??
|
|
1121
|
-
entry.chatModel?.model;
|
|
1122
|
-
if (candidate?.startsWith("MODEL_"))
|
|
1123
|
-
return candidate;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
catch {
|
|
1127
|
-
// ignore metadata RPC failure
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1700
|
+
const fromMetadata = (await this.generatorInfo(conn, nativeId)).model;
|
|
1701
|
+
if (fromMetadata?.startsWith("MODEL_"))
|
|
1702
|
+
return fromMetadata;
|
|
1130
1703
|
if (this.desktopDefaultModel?.startsWith("MODEL_"))
|
|
1131
1704
|
return this.desktopDefaultModel;
|
|
1132
|
-
|
|
1705
|
+
const desktop = this.desktop();
|
|
1706
|
+
try {
|
|
1707
|
+
const stateText = desktop ? readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8") : "";
|
|
1708
|
+
const lastSelected = /last_selected_agent_model:\s*(MODEL_\S+)/.exec(stateText)?.[1];
|
|
1709
|
+
if (lastSelected)
|
|
1710
|
+
return lastSelected;
|
|
1711
|
+
}
|
|
1712
|
+
catch {
|
|
1713
|
+
// no saved selection
|
|
1714
|
+
}
|
|
1715
|
+
throw RpcError.app("agent_unavailable", "没能确定 Antigravity 要用的模型,请在电脑上的 Antigravity 里选一次模型后重试");
|
|
1716
|
+
}
|
|
1717
|
+
/** The system commands and skills the desktop offers for this conversation (its workspace adds skills). */
|
|
1718
|
+
async slashCommands(conn, nativeId, planModel) {
|
|
1719
|
+
const key = nativeId || "";
|
|
1720
|
+
const cached = this.slashCache.get(key);
|
|
1721
|
+
if (cached && Date.now() - cached.at < 60_000)
|
|
1722
|
+
return cached.commands;
|
|
1723
|
+
try {
|
|
1724
|
+
const data = await callLanguageServer(conn, "GetSlashCommands", {
|
|
1725
|
+
...(nativeId ? { cascadeId: nativeId } : {}),
|
|
1726
|
+
cascadeConfig: { plannerConfig: { planModel } },
|
|
1727
|
+
});
|
|
1728
|
+
const commands = (data.commands ?? []).flatMap((entry) => {
|
|
1729
|
+
const info = entry.info;
|
|
1730
|
+
const name = typeof info?.name === "string" ? info.name : undefined;
|
|
1731
|
+
if (!info || !name)
|
|
1732
|
+
return [];
|
|
1733
|
+
return [{ name, description: entry.description ?? "", type: typeof info.type === "string" ? info.type : undefined, info }];
|
|
1734
|
+
});
|
|
1735
|
+
if (commands.length > 0)
|
|
1736
|
+
this.slashCache.set(key, { at: Date.now(), commands });
|
|
1737
|
+
return commands;
|
|
1738
|
+
}
|
|
1739
|
+
catch {
|
|
1740
|
+
return cached?.commands ?? [];
|
|
1741
|
+
}
|
|
1133
1742
|
}
|
|
1134
1743
|
async emitDesktopConfig(nativeId, selectedModel) {
|
|
1135
|
-
this.host?.update(this.id, nativeId, {
|
|
1136
|
-
sessionUpdate: "available_commands_update",
|
|
1137
|
-
availableCommands: ANTIGRAVITY_DESKTOP_COMMANDS,
|
|
1138
|
-
});
|
|
1139
1744
|
const models = await this.loadDesktopModels();
|
|
1140
|
-
|
|
1745
|
+
const conn = await this.languageServer();
|
|
1746
|
+
let commands = ANTIGRAVITY_DESKTOP_COMMANDS;
|
|
1747
|
+
let matched;
|
|
1748
|
+
if (models.length > 0)
|
|
1749
|
+
matched = await this.resolveSessionModel(nativeId, models, selectedModel);
|
|
1750
|
+
if (conn && matched) {
|
|
1751
|
+
const live = await this.slashCommands(conn, nativeId, matched.value);
|
|
1752
|
+
if (live.length > 0) {
|
|
1753
|
+
commands = live.map((command) => {
|
|
1754
|
+
const known = SYSTEM_COMMAND_TEXT.get(command.name);
|
|
1755
|
+
const description = known?.description ?? command.description.split("\n")[0].slice(0, 160);
|
|
1756
|
+
return { name: command.name, description: description || command.name, ...(known && "hint" in known && known.hint ? { hint: known.hint } : {}) };
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "available_commands_update", availableCommands: commands });
|
|
1761
|
+
if (!matched)
|
|
1141
1762
|
return;
|
|
1142
|
-
const matched = await this.resolveSessionModel(nativeId, models, selectedModel);
|
|
1143
1763
|
const options = [
|
|
1144
1764
|
{
|
|
1145
1765
|
id: "model",
|
|
@@ -1152,111 +1772,419 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1152
1772
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_config", options });
|
|
1153
1773
|
this.host?.update(this.id, nativeId, { sessionUpdate: "session_info_update", model: matched.name });
|
|
1154
1774
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1775
|
+
emitDesktopPendingAction(nativeId, watcher, action) {
|
|
1776
|
+
watcher.pendingAction = action;
|
|
1777
|
+
this.desktopSessionStates.set(nativeId, "waiting");
|
|
1778
|
+
if (action.kind === "question") {
|
|
1779
|
+
const questions = action.questions.map((q, idx) => ({
|
|
1780
|
+
id: `q_${idx}`,
|
|
1781
|
+
text: q.question,
|
|
1782
|
+
kind: q.options?.length ? (q.isMultiSelect ? "choices" : "choice") : "text",
|
|
1783
|
+
...(q.options?.length
|
|
1784
|
+
? {
|
|
1785
|
+
options: q.options.map((o) => ({ value: o.id, label: o.text })),
|
|
1786
|
+
other: true,
|
|
1787
|
+
}
|
|
1788
|
+
: {}),
|
|
1789
|
+
}));
|
|
1790
|
+
this.host?.update(this.id, nativeId, {
|
|
1791
|
+
sessionUpdate: "ls_permission",
|
|
1792
|
+
requestId: action.requestId,
|
|
1793
|
+
toolCallId: action.toolCallId,
|
|
1794
|
+
title: action.title,
|
|
1795
|
+
options: QUESTION_OPTIONS,
|
|
1796
|
+
questions,
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
this.host?.update(this.id, nativeId, {
|
|
1801
|
+
sessionUpdate: "ls_permission",
|
|
1802
|
+
requestId: action.requestId,
|
|
1803
|
+
toolCallId: action.toolCallId,
|
|
1804
|
+
title: action.title,
|
|
1805
|
+
...(action.proposedCommandLine ? { detail: action.proposedCommandLine } : {}),
|
|
1806
|
+
tool: {
|
|
1807
|
+
title: action.title,
|
|
1808
|
+
kind: action.interactionType === "runCommand" ? "execute" : action.interactionType === "filePermission" ? "edit" : "other",
|
|
1809
|
+
...(action.proposedCommandLine ? { rawInput: { command: action.proposedCommandLine } } : {}),
|
|
1810
|
+
...(action.absolutePathUri ? { locations: [{ path: action.absolutePathUri }] } : {}),
|
|
1811
|
+
},
|
|
1812
|
+
options: [
|
|
1813
|
+
{ optionId: "allow", name: "允许", kind: "allow_once" },
|
|
1814
|
+
{ optionId: "reject", name: "拒绝", kind: "reject_once" },
|
|
1815
|
+
],
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
this.host?.update(this.id, nativeId, {
|
|
1819
|
+
sessionUpdate: "ls_status",
|
|
1820
|
+
state: "waiting",
|
|
1821
|
+
turnActive: true,
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
clearDesktopPendingAction(nativeId, watcher, resolution) {
|
|
1825
|
+
const prev = watcher.pendingAction;
|
|
1826
|
+
if (!prev)
|
|
1827
|
+
return;
|
|
1828
|
+
watcher.pendingAction = undefined;
|
|
1829
|
+
watcher.resolvedStepIndices.add(prev.stepIndex);
|
|
1830
|
+
this.host?.update(this.id, nativeId, {
|
|
1831
|
+
sessionUpdate: "ls_permission_resolved",
|
|
1832
|
+
requestId: prev.requestId,
|
|
1833
|
+
...(resolution?.optionId ? { optionId: resolution.optionId } : {}),
|
|
1834
|
+
...(resolution?.answers ? { answers: resolution.answers } : {}),
|
|
1835
|
+
});
|
|
1836
|
+
const nowRunning = this.isConversationRunningInDesktop(nativeId);
|
|
1837
|
+
const nextState = nowRunning ? "running" : "idle";
|
|
1838
|
+
this.desktopSessionStates.set(nativeId, nextState);
|
|
1839
|
+
this.host?.update(this.id, nativeId, {
|
|
1840
|
+
sessionUpdate: "ls_status",
|
|
1841
|
+
state: nextState,
|
|
1842
|
+
turnActive: nowRunning,
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
async pollDesktopInteractions(nativeId, watcher) {
|
|
1846
|
+
if (watcher.pollingInteractions)
|
|
1847
|
+
return;
|
|
1848
|
+
watcher.pollingInteractions = true;
|
|
1849
|
+
const transcriptPendingQuestion = watcher.transcriptPending;
|
|
1850
|
+
try {
|
|
1851
|
+
const conn = await this.languageServer();
|
|
1852
|
+
let foundAction;
|
|
1853
|
+
if (conn) {
|
|
1854
|
+
// Steps carry their full content (~20 KB each): ask only for the newest few, where a waiting one would be.
|
|
1855
|
+
const stepOffset = Math.max(0, watcher.lastStepIndex - 4);
|
|
1856
|
+
try {
|
|
1857
|
+
const resp = await callLanguageServer(conn, "GetCascadeTrajectorySteps", { cascadeId: nativeId, stepOffset });
|
|
1858
|
+
const steps = resp.steps ?? [];
|
|
1859
|
+
for (let idx = 0; idx < steps.length; idx++) {
|
|
1860
|
+
const step = steps[idx];
|
|
1861
|
+
const info = step.metadata?.sourceTrajectoryStepInfo;
|
|
1862
|
+
if (info?.trajectoryId)
|
|
1863
|
+
watcher.trajectoryId = info.trajectoryId;
|
|
1864
|
+
const stepIndex = info?.stepIndex ?? stepOffset + idx;
|
|
1865
|
+
if (watcher.resolvedStepIndices.has(stepIndex))
|
|
1866
|
+
continue;
|
|
1867
|
+
const isWaiting = step.status === "CORTEX_STEP_STATUS_WAITING" ||
|
|
1868
|
+
step.status === "CORTEX_STEP_STATUS_PENDING" ||
|
|
1869
|
+
(step.type === "CORTEX_STEP_TYPE_ASK_QUESTION" &&
|
|
1870
|
+
step.status !== "CORTEX_STEP_STATUS_DONE" &&
|
|
1871
|
+
step.status !== "CORTEX_STEP_STATUS_CANCELED" &&
|
|
1872
|
+
step.status !== "CORTEX_STEP_STATUS_ERROR" &&
|
|
1873
|
+
!step.askQuestion?.userResponse &&
|
|
1874
|
+
!(Array.isArray(step.completedInteractions) && step.completedInteractions.length > 0));
|
|
1875
|
+
if (!isWaiting)
|
|
1876
|
+
continue;
|
|
1877
|
+
if (step.type === "CORTEX_STEP_TYPE_ASK_QUESTION" || step.requestedInteraction?.askQuestion) {
|
|
1878
|
+
const rawQuestions = step.askQuestion?.questions ??
|
|
1879
|
+
step.requestedInteraction?.askQuestion?.questions;
|
|
1880
|
+
const askEntries = parseAskQuestionEntries({ questions: rawQuestions });
|
|
1881
|
+
const questions = askEntries.length > 0 ? askEntries : transcriptPendingQuestion?.questions ?? [];
|
|
1882
|
+
if (questions.length > 0) {
|
|
1883
|
+
foundAction = {
|
|
1884
|
+
kind: "question",
|
|
1885
|
+
requestId: `agy-ask:${nativeId}:${stepIndex}`,
|
|
1886
|
+
toolCallId: step.metadata?.toolCall?.id ||
|
|
1887
|
+
transcriptPendingQuestion?.toolCallId ||
|
|
1888
|
+
`agy:tc:${Math.max(0, stepIndex - 1)}:0`,
|
|
1889
|
+
stepIndex,
|
|
1890
|
+
trajectoryId: info?.trajectoryId ?? watcher.trajectoryId,
|
|
1891
|
+
title: transcriptPendingQuestion?.title || questions[0].question.slice(0, 80) || "Antigravity 有问题需要你回答",
|
|
1892
|
+
questions,
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
else if (step.requestedInteraction && (info?.trajectoryId || watcher.trajectoryId)) {
|
|
1897
|
+
const req = step.requestedInteraction;
|
|
1898
|
+
const trajectoryId = (info?.trajectoryId ?? watcher.trajectoryId);
|
|
1899
|
+
const toolCallId = step.metadata?.toolCall?.id || `agy:perm:${stepIndex}`;
|
|
1900
|
+
const base = { kind: "permission", requestId: `agy-perm:${nativeId}:${stepIndex}`, toolCallId, stepIndex, trajectoryId };
|
|
1901
|
+
if (req.runCommand) {
|
|
1902
|
+
const cmd = req.runCommand.proposedCommandLine || req.runCommand.commandLine || step.runCommand?.commandLine || "";
|
|
1903
|
+
foundAction = { ...base, title: cmd ? `确认执行命令:${cmd}` : "确认执行命令", interactionType: "runCommand", proposedCommandLine: cmd };
|
|
1904
|
+
}
|
|
1905
|
+
else if (req.filePermission) {
|
|
1906
|
+
const pathUri = req.filePermission.absolutePathUri || "";
|
|
1907
|
+
foundAction = { ...base, title: pathUri ? `允许访问文件:${pathUri}` : "允许访问文件", interactionType: "filePermission", absolutePathUri: pathUri };
|
|
1908
|
+
}
|
|
1909
|
+
else if (req.mcp) {
|
|
1910
|
+
foundAction = { ...base, title: "允许执行 MCP 工具调用", interactionType: "mcp" };
|
|
1911
|
+
}
|
|
1912
|
+
else if (req.openBrowserUrl) {
|
|
1913
|
+
foundAction = { ...base, title: req.openBrowserUrl.url ? `允许打开链接:${req.openBrowserUrl.url}` : "允许打开浏览器链接", interactionType: "openBrowserUrl" };
|
|
1914
|
+
}
|
|
1915
|
+
else if (req.readUrlContent) {
|
|
1916
|
+
foundAction = { ...base, title: req.readUrlContent.url ? `允许读取网页:${req.readUrlContent.url}` : "允许读取网页内容", interactionType: "readUrlContent" };
|
|
1186
1917
|
}
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1918
|
+
else if (req.approvalInteraction) {
|
|
1919
|
+
foundAction = { ...base, title: "需要你的确认以继续执行", interactionType: "approvalInteraction" };
|
|
1189
1920
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const pending = watcher.pendingClientMessages.shift();
|
|
1193
|
-
const aliases = this.desktopAliasesFor(nativeId);
|
|
1194
|
-
aliases.set(item.itemId, `local-${pending.clientMessageId}`);
|
|
1195
|
-
this.saveDesktopAliases(nativeId, aliases);
|
|
1196
|
-
// Emit only non-user_message_chunk updates (such as ls_goal) since the user message was already echoed with local-${clientMessageId}.
|
|
1197
|
-
const nonUserUpdates = item.updates.filter((u) => u.sessionUpdate !== "user_message_chunk");
|
|
1198
|
-
for (const update of nonUserUpdates) {
|
|
1199
|
-
this.host?.update(this.id, nativeId, update);
|
|
1200
|
-
}
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
item.updates.forEach((update, idx) => {
|
|
1204
|
-
this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
|
|
1205
|
-
});
|
|
1921
|
+
else if (req.permission) {
|
|
1922
|
+
foundAction = { ...base, title: "需要你的权限确认", interactionType: "permission" };
|
|
1206
1923
|
}
|
|
1207
1924
|
}
|
|
1208
1925
|
}
|
|
1209
|
-
catch {
|
|
1210
|
-
// ignore stat/read errors
|
|
1211
|
-
}
|
|
1212
1926
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1927
|
+
catch {
|
|
1928
|
+
// ignore GetCascadeTrajectorySteps failure and fall back to transcript question detection below
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
if (!foundAction && transcriptPendingQuestion && !watcher.resolvedStepIndices.has(transcriptPendingQuestion.stepIndex)) {
|
|
1932
|
+
foundAction = {
|
|
1933
|
+
kind: "question",
|
|
1934
|
+
requestId: `agy-ask:${nativeId}:${transcriptPendingQuestion.stepIndex}`,
|
|
1935
|
+
toolCallId: transcriptPendingQuestion.toolCallId,
|
|
1936
|
+
stepIndex: transcriptPendingQuestion.stepIndex,
|
|
1937
|
+
trajectoryId: watcher.trajectoryId,
|
|
1938
|
+
title: transcriptPendingQuestion.title,
|
|
1939
|
+
questions: transcriptPendingQuestion.questions,
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
if (foundAction) {
|
|
1943
|
+
if (watcher.pendingAction?.requestId !== foundAction.requestId) {
|
|
1944
|
+
if (watcher.pendingAction) {
|
|
1945
|
+
this.clearDesktopPendingAction(nativeId, watcher);
|
|
1220
1946
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1947
|
+
this.emitDesktopPendingAction(nativeId, watcher, foundAction);
|
|
1948
|
+
}
|
|
1949
|
+
else if (foundAction.trajectoryId && !watcher.pendingAction.trajectoryId) {
|
|
1950
|
+
watcher.pendingAction.trajectoryId = foundAction.trajectoryId;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
else if (watcher.pendingAction) {
|
|
1954
|
+
// Answered on the desktop, or the turn was stopped there.
|
|
1955
|
+
this.clearDesktopPendingAction(nativeId, watcher);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
finally {
|
|
1959
|
+
watcher.pollingInteractions = false;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
emitTranscriptItems(nativeId, watcher, items) {
|
|
1963
|
+
for (const item of items) {
|
|
1964
|
+
if (item.itemId.startsWith("agy:user:") && watcher.pendingClientMessages.length > 0) {
|
|
1965
|
+
const pending = watcher.pendingClientMessages.shift();
|
|
1966
|
+
const aliases = this.desktopAliasesFor(nativeId);
|
|
1967
|
+
aliases.set(item.itemId, `local-${pending.clientMessageId}`);
|
|
1968
|
+
this.saveDesktopAliases(nativeId, aliases);
|
|
1969
|
+
// The phone already shows this message (echoed as local-<clientMessageId>); only what came with it (ls_goal) is new.
|
|
1970
|
+
for (const update of item.updates.filter((u) => u.sessionUpdate !== "user_message_chunk")) {
|
|
1971
|
+
this.host?.update(this.id, nativeId, update);
|
|
1972
|
+
}
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
item.updates.forEach((update, idx) => {
|
|
1976
|
+
this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
/** A sub-agent's items as the parent session carries them: nested under the spawning call, with ids of their own. */
|
|
1981
|
+
nestChildItems(items, follow) {
|
|
1982
|
+
return items.flatMap((item) => {
|
|
1983
|
+
const updates = item.updates.flatMap((update) => nestUnder(namespaced(update, follow.prefix), follow.parentToolCallId) ?? []);
|
|
1984
|
+
return updates.length > 0 ? [{ itemId: `sub:${follow.parentToolCallId}:${item.itemId}`, updates }] : [];
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
childTurn(follow, running) {
|
|
1988
|
+
return {
|
|
1989
|
+
// Keyed by where the sub-agent's transcript stood, so attaching again doesn't record the same start or end twice.
|
|
1990
|
+
itemId: `sub:${follow.parentToolCallId}:turn:${running ? "started" : "ended"}:${follow.tail?.state.lastStepIndex ?? -1}`,
|
|
1991
|
+
updates: [{ sessionUpdate: "ls_turn", state: running ? "started" : "ended", ...(running ? {} : { stopReason: "end_turn" }), parentToolCallId: follow.parentToolCallId }],
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Starts following the sub-agents `items` spawned. With `collect`, what each
|
|
1996
|
+
* has done so far is added there (history, or a sub-agent just seen live);
|
|
1997
|
+
* without, only what it does from now on is shown.
|
|
1998
|
+
*/
|
|
1999
|
+
followChildren(watcher, items, prefix, depth, collect) {
|
|
2000
|
+
if (depth >= MAX_SUBAGENT_DEPTH)
|
|
2001
|
+
return;
|
|
2002
|
+
for (const spawn of spawnsIn(items, prefix)) {
|
|
2003
|
+
if (watcher.children.has(spawn.childId))
|
|
2004
|
+
continue;
|
|
2005
|
+
const follow = {
|
|
2006
|
+
id: spawn.childId,
|
|
2007
|
+
parentToolCallId: spawn.toolCallId,
|
|
2008
|
+
prefix: `${spawn.childId.slice(0, 8)}/`,
|
|
2009
|
+
depth: depth + 1,
|
|
2010
|
+
running: false,
|
|
2011
|
+
};
|
|
2012
|
+
watcher.children.set(spawn.childId, follow);
|
|
2013
|
+
const path = this.transcriptPathFor(spawn.childId);
|
|
2014
|
+
if (!path || !existsSync(path))
|
|
2015
|
+
continue;
|
|
2016
|
+
const parsed = parseAntigravityTranscript(path, 0);
|
|
2017
|
+
follow.tail = new TranscriptTail(path, parsed.state, parsed.endOffset);
|
|
2018
|
+
follow.running = this.isConversationRunningInDesktop(spawn.childId);
|
|
2019
|
+
if (collect) {
|
|
2020
|
+
collect.push(...this.nestChildItems(parsed.items, follow), this.childTurn(follow, follow.running));
|
|
2021
|
+
}
|
|
2022
|
+
this.followChildren(watcher, parsed.items, follow.prefix, follow.depth, collect);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
/** New work from every followed sub-agent, and when each starts or stops. */
|
|
2026
|
+
tickChildren(nativeId, watcher) {
|
|
2027
|
+
for (const follow of [...watcher.children.values()]) {
|
|
2028
|
+
const path = this.transcriptPathFor(follow.id);
|
|
2029
|
+
const out = [];
|
|
2030
|
+
if (path && existsSync(path)) {
|
|
2031
|
+
// A sub-agent whose transcript appeared after its spawn was seen: everything in it is new.
|
|
2032
|
+
follow.tail ??= new TranscriptTail(path, newTranscriptParseState(), 0);
|
|
2033
|
+
try {
|
|
2034
|
+
const read = follow.tail.read(path);
|
|
2035
|
+
if (read.items.length > 0) {
|
|
2036
|
+
out.push(...this.nestChildItems(read.items, follow));
|
|
2037
|
+
this.followChildren(watcher, read.items, follow.prefix, follow.depth, out);
|
|
1228
2038
|
}
|
|
1229
2039
|
}
|
|
1230
|
-
|
|
2040
|
+
catch {
|
|
2041
|
+
// ignore unreadable sub-agent transcript
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const running = this.isConversationRunningInDesktop(follow.id);
|
|
2045
|
+
if (running !== follow.running) {
|
|
2046
|
+
follow.running = running;
|
|
2047
|
+
out.push(this.childTurn(follow, running));
|
|
2048
|
+
}
|
|
2049
|
+
for (const item of out) {
|
|
2050
|
+
item.updates.forEach((update, idx) => {
|
|
2051
|
+
this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
stopWatching(nativeId) {
|
|
2057
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
2058
|
+
if (!watcher)
|
|
2059
|
+
return;
|
|
2060
|
+
clearInterval(watcher.timer);
|
|
2061
|
+
this.desktopWatchers.delete(nativeId);
|
|
2062
|
+
}
|
|
2063
|
+
startWatchingDesktopTranscript(nativeId, initial) {
|
|
2064
|
+
const existing = this.desktopWatchers.get(nativeId);
|
|
2065
|
+
if (existing)
|
|
2066
|
+
clearInterval(existing.timer);
|
|
2067
|
+
const path = initial?.path ?? this.transcriptPathFor(nativeId) ?? "";
|
|
2068
|
+
const state = initial?.state ?? newTranscriptParseState();
|
|
2069
|
+
const initialRunning = this.isConversationRunningInDesktop(nativeId);
|
|
2070
|
+
const watcher = {
|
|
2071
|
+
tail: new TranscriptTail(path, state, initial?.offset ?? 0),
|
|
2072
|
+
lastStepIndex: state.lastStepIndex ?? existing?.lastStepIndex ?? 0,
|
|
2073
|
+
running: initialRunning,
|
|
2074
|
+
detached: false,
|
|
2075
|
+
pollingInteractions: false,
|
|
2076
|
+
trajectoryId: existing?.trajectoryId,
|
|
2077
|
+
pendingAction: existing?.pendingAction,
|
|
2078
|
+
transcriptPending: initial?.pendingAskQuestion,
|
|
2079
|
+
resolvedStepIndices: existing?.resolvedStepIndices ?? new Set(),
|
|
2080
|
+
selectedModel: existing?.selectedModel ?? this.host?.state(this.id, nativeId).get("agy-selected-model") ?? undefined,
|
|
2081
|
+
lastModelLabel: state.lastModelLabel ?? existing?.lastModelLabel,
|
|
2082
|
+
goal: initial ? state.goal : existing?.goal,
|
|
2083
|
+
pendingClientMessages: existing?.pendingClientMessages ?? [],
|
|
2084
|
+
children: new Map(),
|
|
2085
|
+
timer: setInterval(() => this.tickWatcher(nativeId, watcher), 1500),
|
|
1231
2086
|
};
|
|
1232
2087
|
watcher.timer.unref?.();
|
|
1233
2088
|
this.desktopWatchers.set(nativeId, watcher);
|
|
2089
|
+
this.desktopSessionStates.set(nativeId, watcher.pendingAction ? "waiting" : initialRunning ? "running" : "idle");
|
|
2090
|
+
if (initialRunning || watcher.transcriptPending)
|
|
2091
|
+
void this.pollDesktopInteractions(nativeId, watcher);
|
|
1234
2092
|
return watcher;
|
|
1235
2093
|
}
|
|
2094
|
+
tickWatcher(nativeId, watcher) {
|
|
2095
|
+
void this.languageServer();
|
|
2096
|
+
let transcriptChanged = false;
|
|
2097
|
+
const currentPath = this.transcriptPathFor(nativeId);
|
|
2098
|
+
if (currentPath && existsSync(currentPath)) {
|
|
2099
|
+
try {
|
|
2100
|
+
const read = watcher.tail.read(currentPath);
|
|
2101
|
+
transcriptChanged = read.changed;
|
|
2102
|
+
const state = watcher.tail.state;
|
|
2103
|
+
if (state.lastStepIndex !== undefined)
|
|
2104
|
+
watcher.lastStepIndex = state.lastStepIndex;
|
|
2105
|
+
if (read.items.length > 0) {
|
|
2106
|
+
watcher.transcriptPending = read.pendingAskQuestion;
|
|
2107
|
+
watcher.goal = state.goal;
|
|
2108
|
+
if (state.lastModelLabel && state.lastModelLabel !== watcher.lastModelLabel) {
|
|
2109
|
+
watcher.lastModelLabel = state.lastModelLabel;
|
|
2110
|
+
watcher.selectedModel = state.lastModelLabel;
|
|
2111
|
+
this.host?.state(this.id, nativeId).set("agy-selected-model", state.lastModelLabel);
|
|
2112
|
+
void this.emitDesktopConfig(nativeId, state.lastModelLabel).catch(() => { });
|
|
2113
|
+
}
|
|
2114
|
+
this.emitTranscriptItems(nativeId, watcher, read.items);
|
|
2115
|
+
const spawned = [];
|
|
2116
|
+
this.followChildren(watcher, read.items, "", 0, spawned);
|
|
2117
|
+
this.emitTranscriptItems(nativeId, watcher, spawned);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
catch {
|
|
2121
|
+
// ignore stat/read errors
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
this.tickChildren(nativeId, watcher);
|
|
2125
|
+
let nowRunning = this.isConversationRunningInDesktop(nativeId);
|
|
2126
|
+
if (nowRunning) {
|
|
2127
|
+
watcher.sentAt = undefined;
|
|
2128
|
+
}
|
|
2129
|
+
else if (watcher.sentAt && Date.now() - watcher.sentAt < 15_000) {
|
|
2130
|
+
// Sent from the phone a moment ago: the desktop records the run a little later, so this isn't the end of it.
|
|
2131
|
+
nowRunning = true;
|
|
2132
|
+
}
|
|
2133
|
+
if (nowRunning || watcher.pendingAction || transcriptChanged || watcher.transcriptPending) {
|
|
2134
|
+
void this.pollDesktopInteractions(nativeId, watcher);
|
|
2135
|
+
}
|
|
2136
|
+
if (nowRunning === watcher.running)
|
|
2137
|
+
return;
|
|
2138
|
+
watcher.running = nowRunning;
|
|
2139
|
+
const nextState = watcher.pendingAction ? "waiting" : nowRunning ? "running" : "idle";
|
|
2140
|
+
this.desktopSessionStates.set(nativeId, nextState);
|
|
2141
|
+
if (nowRunning) {
|
|
2142
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
2143
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: nextState, turnActive: true });
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (watcher.pendingAction)
|
|
2147
|
+
return;
|
|
2148
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
2149
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
2150
|
+
// Followed only for its turn and nobody is looking: let go, and attach again on next use.
|
|
2151
|
+
if (watcher.detached || !this.watchedSessions.has(nativeId)) {
|
|
2152
|
+
this.stopWatching(nativeId);
|
|
2153
|
+
this.host?.detached(this.id, nativeId);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
1236
2156
|
async attach(nativeId, context) {
|
|
1237
2157
|
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
1238
2158
|
if (this.desktopMode || (transcriptPath && existsSync(transcriptPath))) {
|
|
1239
2159
|
await this.ensureStarted();
|
|
1240
|
-
const parsed = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) :
|
|
2160
|
+
const parsed = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : undefined;
|
|
1241
2161
|
const aliases = this.desktopAliasesFor(nativeId);
|
|
1242
|
-
const items = parsed
|
|
2162
|
+
const items = (parsed?.items ?? []).map((item) => {
|
|
1243
2163
|
const aliasedId = aliases.get(item.itemId);
|
|
1244
2164
|
return aliasedId ? { ...item, itemId: aliasedId } : item;
|
|
1245
2165
|
});
|
|
1246
|
-
this.startWatchingDesktopTranscript(nativeId, parsed
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
const
|
|
1251
|
-
this.
|
|
1252
|
-
|
|
2166
|
+
const watcher = this.startWatchingDesktopTranscript(nativeId, parsed && transcriptPath
|
|
2167
|
+
? { path: transcriptPath, state: parsed.state, offset: parsed.endOffset, pendingAskQuestion: parsed.pendingAskQuestion }
|
|
2168
|
+
: undefined);
|
|
2169
|
+
// Sub-agents' work so far, after the parent's (whose spawning calls it nests under).
|
|
2170
|
+
const childItems = [];
|
|
2171
|
+
this.followChildren(watcher, parsed?.items ?? [], "", 0, childItems);
|
|
2172
|
+
items.push(...childItems);
|
|
2173
|
+
const running = watcher.running;
|
|
2174
|
+
const state = watcher.pendingAction ? "waiting" : running ? "running" : "idle";
|
|
2175
|
+
this.desktopSessionStates.set(nativeId, state);
|
|
2176
|
+
if (state === "idle") {
|
|
1253
2177
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
1254
2178
|
}
|
|
1255
|
-
this.host?.update(this.id, nativeId, {
|
|
1256
|
-
|
|
2179
|
+
this.host?.update(this.id, nativeId, {
|
|
2180
|
+
sessionUpdate: "ls_status",
|
|
2181
|
+
state,
|
|
2182
|
+
turnActive: state === "running" || state === "waiting",
|
|
2183
|
+
});
|
|
2184
|
+
if (parsed?.goal !== undefined) {
|
|
1257
2185
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: parsed.goal });
|
|
1258
2186
|
}
|
|
1259
|
-
void this.emitDesktopConfig(nativeId,
|
|
2187
|
+
void this.emitDesktopConfig(nativeId, watcher.selectedModel).catch(() => { });
|
|
1260
2188
|
return items;
|
|
1261
2189
|
}
|
|
1262
2190
|
return super.attach(nativeId, context);
|
|
@@ -1264,26 +2192,155 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1264
2192
|
async detach(nativeId) {
|
|
1265
2193
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
1266
2194
|
if (watcher) {
|
|
1267
|
-
if (watcher.running) {
|
|
1268
|
-
// Keep watching until the active turn completes so turn end and final messages are recorded.
|
|
2195
|
+
if (watcher.running || watcher.pendingAction) {
|
|
2196
|
+
// Keep watching until the active turn completes so turn end, pending questions, and final messages are recorded.
|
|
1269
2197
|
watcher.detached = true;
|
|
1270
2198
|
}
|
|
1271
2199
|
else {
|
|
1272
|
-
|
|
1273
|
-
this.desktopWatchers.delete(nativeId);
|
|
2200
|
+
this.stopWatching(nativeId);
|
|
1274
2201
|
}
|
|
1275
2202
|
}
|
|
1276
2203
|
await super.detach(nativeId);
|
|
1277
2204
|
}
|
|
2205
|
+
async resolveDesktopTrajectory(conn, nativeId, watcher, fallbackStepIndex) {
|
|
2206
|
+
let trajectoryId = watcher.pendingAction?.trajectoryId ?? watcher.trajectoryId;
|
|
2207
|
+
let stepIndex = fallbackStepIndex;
|
|
2208
|
+
if (!trajectoryId) {
|
|
2209
|
+
const resp = await callLanguageServer(conn, "GetCascadeTrajectorySteps", { cascadeId: nativeId, stepOffset: Math.max(0, watcher.lastStepIndex - 10) });
|
|
2210
|
+
for (let idx = 0; idx < (resp.steps ?? []).length; idx++) {
|
|
2211
|
+
const s = resp.steps[idx];
|
|
2212
|
+
const info = s.metadata?.sourceTrajectoryStepInfo;
|
|
2213
|
+
if (info?.trajectoryId)
|
|
2214
|
+
trajectoryId = info.trajectoryId;
|
|
2215
|
+
if (s.type === "CORTEX_STEP_TYPE_ASK_QUESTION" && info?.stepIndex !== undefined) {
|
|
2216
|
+
stepIndex = info.stepIndex;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (!trajectoryId) {
|
|
2221
|
+
throw RpcError.app("agent_unavailable", "未能定位当前 Antigravity 交互轨迹,请在桌面端确认");
|
|
2222
|
+
}
|
|
2223
|
+
return { trajectoryId, stepIndex };
|
|
2224
|
+
}
|
|
2225
|
+
async answerQuestion(nativeId, requestId, answers) {
|
|
2226
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
2227
|
+
if (watcher?.pendingAction?.kind === "question" && watcher.pendingAction.requestId === requestId) {
|
|
2228
|
+
const pending = watcher.pendingAction;
|
|
2229
|
+
const conn = await this.requireConnection("回答");
|
|
2230
|
+
const { trajectoryId, stepIndex } = await this.resolveDesktopTrajectory(conn, nativeId, watcher, pending.stepIndex);
|
|
2231
|
+
const responses = pending.questions.map((q, idx) => {
|
|
2232
|
+
const qId = `q_${idx}`;
|
|
2233
|
+
const ans = answers.find((a) => a.id === qId) ?? answers[idx];
|
|
2234
|
+
const opts = q.options ?? [];
|
|
2235
|
+
const chosenValues = (ans?.values ?? []).map((v) => v.trim()).filter(Boolean);
|
|
2236
|
+
const selectedOptionIds = [];
|
|
2237
|
+
const writeInParts = [];
|
|
2238
|
+
for (const val of chosenValues) {
|
|
2239
|
+
const matchedOpt = opts.find((o) => o.id === val || o.text === val);
|
|
2240
|
+
if (matchedOpt) {
|
|
2241
|
+
selectedOptionIds.push(matchedOpt.id);
|
|
2242
|
+
}
|
|
2243
|
+
else {
|
|
2244
|
+
writeInParts.push(val);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
if (ans?.other?.trim()) {
|
|
2248
|
+
writeInParts.push(ans.other.trim());
|
|
2249
|
+
}
|
|
2250
|
+
const writeInResponse = writeInParts.join("\n");
|
|
2251
|
+
const skipped = selectedOptionIds.length === 0 && !writeInResponse;
|
|
2252
|
+
return {
|
|
2253
|
+
question: q.question,
|
|
2254
|
+
isMultiSelect: Boolean(q.isMultiSelect),
|
|
2255
|
+
options: opts.map((o) => ({ id: o.id, text: o.text })),
|
|
2256
|
+
selectedOptionIds,
|
|
2257
|
+
...(writeInResponse ? { writeInResponse } : {}),
|
|
2258
|
+
skipped,
|
|
2259
|
+
};
|
|
2260
|
+
});
|
|
2261
|
+
await callLanguageServer(conn, "HandleCascadeUserInteraction", {
|
|
2262
|
+
cascadeId: nativeId,
|
|
2263
|
+
interaction: {
|
|
2264
|
+
trajectoryId,
|
|
2265
|
+
stepIndex,
|
|
2266
|
+
askQuestion: {
|
|
2267
|
+
cancelled: false,
|
|
2268
|
+
responses,
|
|
2269
|
+
},
|
|
2270
|
+
},
|
|
2271
|
+
});
|
|
2272
|
+
this.clearDesktopPendingAction(nativeId, watcher, { answers });
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
await super.answerQuestion(nativeId, requestId, answers);
|
|
2276
|
+
}
|
|
2277
|
+
async respondPermission(nativeId, requestId, optionId) {
|
|
2278
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
2279
|
+
if (watcher?.pendingAction && watcher.pendingAction.requestId === requestId) {
|
|
2280
|
+
const pending = watcher.pendingAction;
|
|
2281
|
+
const conn = await this.requireConnection("确认");
|
|
2282
|
+
if (pending.kind === "question") {
|
|
2283
|
+
const { trajectoryId, stepIndex } = await this.resolveDesktopTrajectory(conn, nativeId, watcher, pending.stepIndex);
|
|
2284
|
+
const cancelled = optionId === "cancel";
|
|
2285
|
+
const responses = pending.questions.map((q) => ({
|
|
2286
|
+
question: q.question,
|
|
2287
|
+
isMultiSelect: Boolean(q.isMultiSelect),
|
|
2288
|
+
options: (q.options ?? []).map((o) => ({ id: o.id, text: o.text })),
|
|
2289
|
+
selectedOptionIds: [],
|
|
2290
|
+
skipped: true,
|
|
2291
|
+
}));
|
|
2292
|
+
await callLanguageServer(conn, "HandleCascadeUserInteraction", {
|
|
2293
|
+
cascadeId: nativeId,
|
|
2294
|
+
interaction: {
|
|
2295
|
+
trajectoryId,
|
|
2296
|
+
stepIndex,
|
|
2297
|
+
askQuestion: {
|
|
2298
|
+
cancelled,
|
|
2299
|
+
responses,
|
|
2300
|
+
},
|
|
2301
|
+
},
|
|
2302
|
+
});
|
|
2303
|
+
this.clearDesktopPendingAction(nativeId, watcher, { optionId });
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
const allow = optionId !== "reject" && optionId !== "cancel";
|
|
2307
|
+
const interactionPayload = {
|
|
2308
|
+
trajectoryId: pending.trajectoryId,
|
|
2309
|
+
stepIndex: pending.stepIndex,
|
|
2310
|
+
};
|
|
2311
|
+
if (pending.interactionType === "runCommand") {
|
|
2312
|
+
interactionPayload.runCommand = {
|
|
2313
|
+
confirm: allow,
|
|
2314
|
+
proposedCommandLine: pending.proposedCommandLine ?? "",
|
|
2315
|
+
submittedCommandLine: pending.proposedCommandLine ?? "",
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
else if (pending.interactionType === "filePermission") {
|
|
2319
|
+
interactionPayload.filePermission = {
|
|
2320
|
+
allow,
|
|
2321
|
+
...(pending.absolutePathUri ? { absolutePathUri: pending.absolutePathUri } : {}),
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
else if (pending.interactionType === "permission") {
|
|
2325
|
+
interactionPayload.permission = { allow };
|
|
2326
|
+
}
|
|
2327
|
+
else {
|
|
2328
|
+
interactionPayload[pending.interactionType] = { confirm: allow };
|
|
2329
|
+
}
|
|
2330
|
+
await callLanguageServer(conn, "HandleCascadeUserInteraction", {
|
|
2331
|
+
cascadeId: nativeId,
|
|
2332
|
+
interaction: interactionPayload,
|
|
2333
|
+
});
|
|
2334
|
+
this.clearDesktopPendingAction(nativeId, watcher, { optionId });
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
await super.respondPermission(nativeId, requestId, optionId);
|
|
2338
|
+
}
|
|
1278
2339
|
async createSession(options) {
|
|
1279
2340
|
if (!this.desktopMode)
|
|
1280
2341
|
return super.createSession(options);
|
|
1281
2342
|
await this.ensureStarted();
|
|
1282
|
-
const
|
|
1283
|
-
const conn = await findRunningLanguageServer(desktop);
|
|
1284
|
-
if (!conn) {
|
|
1285
|
-
throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用");
|
|
1286
|
-
}
|
|
2343
|
+
const conn = await this.requireConnection("新建会话");
|
|
1287
2344
|
const requestedModel = await this.resolvePlanModelEnum(conn, "", options.model);
|
|
1288
2345
|
const response = await callLanguageServer(conn, "StartCascade", {
|
|
1289
2346
|
source: "CORTEX_TRAJECTORY_SOURCE_CASCADE_CLIENT",
|
|
@@ -1293,16 +2350,99 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1293
2350
|
const nativeId = response.cascadeId;
|
|
1294
2351
|
if (!nativeId)
|
|
1295
2352
|
throw RpcError.app("agent_unavailable", "Antigravity 未返回新会话 ID");
|
|
1296
|
-
const watcher = this.startWatchingDesktopTranscript(nativeId
|
|
2353
|
+
const watcher = this.startWatchingDesktopTranscript(nativeId);
|
|
1297
2354
|
const selectedModel = options.model ?? requestedModel;
|
|
1298
2355
|
watcher.selectedModel = selectedModel;
|
|
1299
2356
|
this.host?.state(this.id, nativeId).set("agy-selected-model", selectedModel);
|
|
1300
|
-
void this.emitDesktopConfig(nativeId, selectedModel);
|
|
2357
|
+
void this.emitDesktopConfig(nativeId, selectedModel).catch(() => { });
|
|
1301
2358
|
const now = Date.now();
|
|
1302
2359
|
return { nativeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
|
|
1303
2360
|
}
|
|
2361
|
+
/** The transcript step an item of ours came from (the phone's own messages are aliased to theirs). */
|
|
2362
|
+
stepOfItem(nativeId, itemId) {
|
|
2363
|
+
let id = itemId;
|
|
2364
|
+
if (id.startsWith("local-"))
|
|
2365
|
+
id = [...this.desktopAliasesFor(nativeId)].find(([, alias]) => alias === itemId)?.[0] ?? id;
|
|
2366
|
+
const match = /^agy:[a-z-]+:(\d+)/.exec(id);
|
|
2367
|
+
if (!match)
|
|
2368
|
+
throw RpcError.app("not_supported", "Antigravity 只能从它自己记录的消息处分叉");
|
|
2369
|
+
return Number(match[1]);
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* The step a fork ends at: the end of the turn holding `upTo` (or of the
|
|
2373
|
+
* whole conversation). Antigravity forks only at a turn's last step, right
|
|
2374
|
+
* before the next user message; the trajectory (not the transcript, which
|
|
2375
|
+
* skips some steps) says which that is.
|
|
2376
|
+
*/
|
|
2377
|
+
async forkStepFor(conn, nativeId, upTo) {
|
|
2378
|
+
const from = upTo ? this.stepOfItem(nativeId, upTo.itemId) : undefined;
|
|
2379
|
+
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
2380
|
+
const lastKnown = this.desktopWatchers.get(nativeId)?.lastStepIndex
|
|
2381
|
+
?? (transcriptPath && existsSync(transcriptPath) ? parseAntigravityTranscript(transcriptPath, 0).lastStepIndex : undefined)
|
|
2382
|
+
?? 0;
|
|
2383
|
+
const resp = await callLanguageServer(conn, "GetCascadeTrajectorySteps", { cascadeId: nativeId, stepOffset: from ?? Math.max(0, lastKnown - 4) }, 30_000);
|
|
2384
|
+
const base = from ?? Math.max(0, lastKnown - 4);
|
|
2385
|
+
const steps = (resp.steps ?? []).map((step, idx) => ({
|
|
2386
|
+
index: step.metadata?.sourceTrajectoryStepInfo?.stepIndex ?? base + idx,
|
|
2387
|
+
user: step.type === "CORTEX_STEP_TYPE_USER_INPUT",
|
|
2388
|
+
}));
|
|
2389
|
+
const nextUser = from === undefined ? undefined : steps.find((step) => step.user && step.index > from);
|
|
2390
|
+
const end = nextUser ? steps.filter((step) => step.index < nextUser.index).at(-1) : steps.at(-1);
|
|
2391
|
+
if (!end || end.user)
|
|
2392
|
+
throw RpcError.app("not_supported", "这条消息之后还没有回复,无法从这里分叉");
|
|
2393
|
+
return end.index;
|
|
2394
|
+
}
|
|
2395
|
+
async fork(nativeId, options) {
|
|
2396
|
+
if (!this.isDesktopSession(nativeId))
|
|
2397
|
+
return super.fork(nativeId, options);
|
|
2398
|
+
// A fork into a worktree is made by replaying the conversation into a new session there.
|
|
2399
|
+
if (options.cwd !== options.sourceCwd)
|
|
2400
|
+
throw RpcError.app("not_supported", "Antigravity 的原生分叉只能留在同一目录");
|
|
2401
|
+
const conn = await this.requireConnection("分叉");
|
|
2402
|
+
if (!options.upTo && (this.desktopWatchers.get(nativeId)?.running || this.isConversationRunningInDesktop(nativeId))) {
|
|
2403
|
+
throw RpcError.app("busy", "这个会话正在运行:等这一轮结束再分叉");
|
|
2404
|
+
}
|
|
2405
|
+
const forkAtStepIndex = await this.forkStepFor(conn, nativeId, options.upTo);
|
|
2406
|
+
let response;
|
|
2407
|
+
try {
|
|
2408
|
+
response = await callLanguageServer(conn, "ForkConversation", { sourceCascadeId: nativeId, forkAtStepIndex });
|
|
2409
|
+
}
|
|
2410
|
+
catch (error) {
|
|
2411
|
+
throw RpcError.app("not_supported", error instanceof Error ? error.message : String(error));
|
|
2412
|
+
}
|
|
2413
|
+
if (!response.newCascadeId)
|
|
2414
|
+
throw RpcError.app("not_supported", "Antigravity 未返回分叉后的会话 ID");
|
|
2415
|
+
const now = Date.now();
|
|
2416
|
+
return { nativeId: response.newCascadeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
|
|
2417
|
+
}
|
|
2418
|
+
async rename(nativeId, title) {
|
|
2419
|
+
if (!this.isDesktopSession(nativeId))
|
|
2420
|
+
return;
|
|
2421
|
+
const conn = await this.requireConnection("改名");
|
|
2422
|
+
await callLanguageServer(conn, "UpdateConversationAnnotations", { cascadeId: nativeId, annotations: { title }, mergeAnnotations: true });
|
|
2423
|
+
this.desktopRowsCache = undefined;
|
|
2424
|
+
}
|
|
2425
|
+
async archive(nativeId, archived) {
|
|
2426
|
+
if (!this.isDesktopSession(nativeId))
|
|
2427
|
+
return;
|
|
2428
|
+
const conn = await this.languageServer();
|
|
2429
|
+
// Archiving still holds in LinkShell when the app is closed; the desktop catches up next time.
|
|
2430
|
+
if (!conn)
|
|
2431
|
+
return;
|
|
2432
|
+
await callLanguageServer(conn, "UpdateConversationAnnotations", { cascadeId: nativeId, annotations: { archived }, mergeAnnotations: true }).catch(() => { });
|
|
2433
|
+
this.archivedCache = undefined;
|
|
2434
|
+
}
|
|
2435
|
+
async delete(nativeId) {
|
|
2436
|
+
if (!this.isDesktopSession(nativeId))
|
|
2437
|
+
return super.delete(nativeId);
|
|
2438
|
+
const conn = await this.requireConnection("删除");
|
|
2439
|
+
await callLanguageServer(conn, "DeleteCascadeTrajectory", { cascadeId: nativeId });
|
|
2440
|
+
this.stopWatching(nativeId);
|
|
2441
|
+
this.desktopSessionStates.delete(nativeId);
|
|
2442
|
+
this.desktopRowsCache = undefined;
|
|
2443
|
+
}
|
|
1304
2444
|
async goal(nativeId, change) {
|
|
1305
|
-
if (!this.
|
|
2445
|
+
if (!this.isDesktopSession(nativeId)) {
|
|
1306
2446
|
throw RpcError.app("not_supported", "这个 Agent 通过 /goal 命令管理目标");
|
|
1307
2447
|
}
|
|
1308
2448
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
@@ -1310,77 +2450,61 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1310
2450
|
return watcher?.goal ?? null;
|
|
1311
2451
|
}
|
|
1312
2452
|
if (change.action === "clear") {
|
|
1313
|
-
|
|
1314
|
-
watcher.goal = null;
|
|
1315
|
-
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
2453
|
+
await this.prompt(nativeId, [{ type: "text", text: "/goal clear" }], `goal-${Date.now()}`);
|
|
1316
2454
|
return null;
|
|
1317
2455
|
}
|
|
1318
2456
|
if (change.action === "set") {
|
|
1319
2457
|
const nextGoal = { objective: change.objective, status: "active" };
|
|
1320
|
-
if (watcher)
|
|
1321
|
-
watcher.goal = nextGoal;
|
|
1322
2458
|
await this.prompt(nativeId, [{ type: "text", text: `/goal ${change.objective}` }], `goal-${Date.now()}`);
|
|
1323
2459
|
return nextGoal;
|
|
1324
2460
|
}
|
|
1325
2461
|
return watcher?.goal ?? null;
|
|
1326
2462
|
}
|
|
2463
|
+
echoDesktopMessage(nativeId, clientMessageId, blocks) {
|
|
2464
|
+
for (const block of blocks) {
|
|
2465
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "user_message_chunk", messageId: `local-${clientMessageId}`, content: block });
|
|
2466
|
+
}
|
|
2467
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_message_done", messageId: `local-${clientMessageId}`, role: "user" }, `local-${clientMessageId}`);
|
|
2468
|
+
}
|
|
1327
2469
|
async prompt(nativeId, content, clientMessageId, context) {
|
|
1328
|
-
|
|
1329
|
-
if (!this.desktopMode && !(transcriptPath && existsSync(transcriptPath))) {
|
|
2470
|
+
if (!this.isDesktopSession(nativeId)) {
|
|
1330
2471
|
return super.prompt(nativeId, content, clientMessageId, context);
|
|
1331
2472
|
}
|
|
1332
|
-
const desktop =
|
|
1333
|
-
const conn = await
|
|
1334
|
-
|
|
1335
|
-
throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用,再发送消息");
|
|
1336
|
-
}
|
|
1337
|
-
const text = content
|
|
2473
|
+
const desktop = this.desktop();
|
|
2474
|
+
const conn = await this.requireConnection("发送消息");
|
|
2475
|
+
let text = content
|
|
1338
2476
|
.map((block) => (block.type === "text" ? block.text : ""))
|
|
1339
2477
|
.join("\n")
|
|
1340
2478
|
.trim();
|
|
1341
2479
|
const imageBlocks = content.filter((block) => block.type === "image");
|
|
1342
2480
|
if (!text && imageBlocks.length === 0)
|
|
1343
2481
|
throw RpcError.app("invalid_params", "nothing to send");
|
|
1344
|
-
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(text) && imageBlocks.length === 0) {
|
|
1345
|
-
const watcher = this.desktopWatchers.get(nativeId);
|
|
1346
|
-
if (watcher)
|
|
1347
|
-
watcher.goal = null;
|
|
1348
|
-
this.host?.update(this.id, nativeId, {
|
|
1349
|
-
sessionUpdate: "user_message_chunk",
|
|
1350
|
-
messageId: `local-${clientMessageId}`,
|
|
1351
|
-
content: { type: "text", text },
|
|
1352
|
-
});
|
|
1353
|
-
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_message_done", messageId: `local-${clientMessageId}`, role: "user" }, `local-${clientMessageId}`);
|
|
1354
|
-
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1355
|
-
return "started";
|
|
1356
|
-
}
|
|
1357
2482
|
let watcher = this.desktopWatchers.get(nativeId);
|
|
1358
2483
|
if (!watcher) {
|
|
1359
|
-
const
|
|
1360
|
-
|
|
2484
|
+
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
2485
|
+
const parsed = transcriptPath && existsSync(transcriptPath) ? parseAntigravityTranscript(transcriptPath, 0) : undefined;
|
|
2486
|
+
watcher = this.startWatchingDesktopTranscript(nativeId, parsed && transcriptPath ? { path: transcriptPath, state: parsed.state, offset: parsed.endOffset, pendingAskQuestion: parsed.pendingAskQuestion } : undefined);
|
|
2487
|
+
this.followChildren(watcher, parsed?.items ?? [], "", 0);
|
|
1361
2488
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
2489
|
+
let displayBlocks = content;
|
|
2490
|
+
if (GOAL_STOP.test(text) && imageBlocks.length === 0) {
|
|
2491
|
+
const hadGoal = watcher.goal?.status === "active";
|
|
2492
|
+
watcher.goal = null;
|
|
2493
|
+
if (!hadGoal) {
|
|
2494
|
+
// Nothing for the agent to stop: only the phone's record changes.
|
|
2495
|
+
this.echoDesktopMessage(nativeId, clientMessageId, content);
|
|
2496
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
2497
|
+
return "started";
|
|
1368
2498
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
this.host?.update(this.id, nativeId, {
|
|
1373
|
-
sessionUpdate: "user_message_chunk",
|
|
1374
|
-
messageId: `local-${clientMessageId}`,
|
|
1375
|
-
content: block,
|
|
1376
|
-
});
|
|
2499
|
+
// The goal is enforced by the agent's stop hook: only the agent can end it (with <!-- GOAL_CANCELLED -->).
|
|
2500
|
+
text = "请停止并取消当前的 /goal 目标。";
|
|
2501
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1377
2502
|
}
|
|
1378
|
-
|
|
1379
|
-
if (/^\/goal(?:\s+|$)/i.test(text)) {
|
|
2503
|
+
else if (/^\/goal(?:\s+|$)/i.test(text)) {
|
|
1380
2504
|
const objective = text.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
1381
|
-
|
|
1382
|
-
watcher.goal
|
|
1383
|
-
|
|
2505
|
+
watcher.goal = { objective, status: "active" };
|
|
2506
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: watcher.goal });
|
|
2507
|
+
displayBlocks = content.map((block) => block.type === "text" && /^\/goal(?:\s+|$)/i.test(block.text.trim()) ? { type: "text", text: objective } : block);
|
|
1384
2508
|
}
|
|
1385
2509
|
else if (watcher.goal?.status === "complete") {
|
|
1386
2510
|
watcher.goal = null;
|
|
@@ -1412,55 +2536,108 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1412
2536
|
media.push({
|
|
1413
2537
|
mimeType: img.mimeType,
|
|
1414
2538
|
inlineData: img.data,
|
|
2539
|
+
thumbnail: img.data,
|
|
1415
2540
|
...(savedUri ? { uri: savedUri } : {}),
|
|
1416
2541
|
});
|
|
1417
2542
|
}
|
|
1418
2543
|
}
|
|
1419
|
-
const fullText = context ? (text ? `${context}\n\n${text}` : context) : text;
|
|
1420
2544
|
const planModel = await this.resolvePlanModelEnum(conn, nativeId, watcher.selectedModel);
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
2545
|
+
const inherited = await this.inheritedSettings(conn, nativeId);
|
|
2546
|
+
// A known command goes as the desktop sends it (a slash-command item), so the agent gets its instructions.
|
|
2547
|
+
const items = [];
|
|
2548
|
+
if (context)
|
|
2549
|
+
items.push({ text: `${context}\n\n` });
|
|
2550
|
+
const slash = SLASH.exec(text);
|
|
2551
|
+
const command = slash ? (await this.slashCommands(conn, nativeId, planModel)).find((entry) => entry.name === slash[1]) : undefined;
|
|
2552
|
+
if (command) {
|
|
2553
|
+
items.push({ item: { slashCommand: { info: command.info } } });
|
|
2554
|
+
const rest = slash?.[2]?.trim();
|
|
2555
|
+
if (rest)
|
|
2556
|
+
items.push({ text: ` ${rest}` });
|
|
2557
|
+
}
|
|
2558
|
+
else if (text) {
|
|
2559
|
+
items.push({ text });
|
|
2560
|
+
}
|
|
2561
|
+
else {
|
|
2562
|
+
items.push({ text: "请查看附图" });
|
|
2563
|
+
}
|
|
2564
|
+
watcher.pendingClientMessages.push({ clientMessageId, text });
|
|
2565
|
+
this.echoDesktopMessage(nativeId, clientMessageId, displayBlocks);
|
|
2566
|
+
const steering = watcher.running || this.isConversationRunningInDesktop(nativeId);
|
|
2567
|
+
if (!steering) {
|
|
2568
|
+
watcher.sentAt = Date.now();
|
|
2569
|
+
this.desktopRowsCache = undefined;
|
|
2570
|
+
watcher.running = true;
|
|
2571
|
+
this.desktopSessionStates.set(nativeId, "running");
|
|
2572
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
2573
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
|
|
2574
|
+
}
|
|
2575
|
+
try {
|
|
2576
|
+
await callLanguageServer(conn, "SendUserCascadeMessage", {
|
|
2577
|
+
cascadeId: nativeId,
|
|
2578
|
+
items,
|
|
2579
|
+
...(media.length > 0 ? { media } : {}),
|
|
2580
|
+
cascadeConfig: {
|
|
2581
|
+
plannerConfig: {
|
|
2582
|
+
supportsLatexRendering: true,
|
|
2583
|
+
...(inherited.autoExecutionPolicy
|
|
2584
|
+
? { toolConfig: { runCommand: { autoCommandConfig: { autoExecutionPolicy: inherited.autoExecutionPolicy } } } }
|
|
2585
|
+
: {}),
|
|
2586
|
+
planModel,
|
|
2587
|
+
useAiCredits: inherited.useAiCredits ?? false,
|
|
1439
2588
|
},
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
useCoreDirect: true,
|
|
2589
|
+
executorConfig: {
|
|
2590
|
+
useCoreDirect: true,
|
|
2591
|
+
},
|
|
2592
|
+
conversationHistoryConfig: {},
|
|
1445
2593
|
},
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
surface: "AGENT_SURFACE_HUB",
|
|
2594
|
+
customAgentSpec: {
|
|
2595
|
+
builtinAgent: {
|
|
2596
|
+
defaultAgent: {
|
|
2597
|
+
isGoogle: false,
|
|
2598
|
+
isInteractive: true,
|
|
2599
|
+
surface: "AGENT_SURFACE_HUB",
|
|
2600
|
+
},
|
|
1454
2601
|
},
|
|
1455
2602
|
},
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
catch (error) {
|
|
2606
|
+
const pendingIdx = watcher.pendingClientMessages.findIndex((p) => p.clientMessageId === clientMessageId);
|
|
2607
|
+
if (pendingIdx >= 0)
|
|
2608
|
+
watcher.pendingClientMessages.splice(pendingIdx, 1);
|
|
2609
|
+
this.host?.update(this.id, nativeId, {
|
|
2610
|
+
sessionUpdate: "ls_error",
|
|
2611
|
+
code: "send_failed",
|
|
2612
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2613
|
+
});
|
|
2614
|
+
if (!steering) {
|
|
2615
|
+
watcher.sentAt = undefined;
|
|
2616
|
+
watcher.running = false;
|
|
2617
|
+
this.desktopSessionStates.set(nativeId, "idle");
|
|
2618
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "error" });
|
|
2619
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
2620
|
+
}
|
|
2621
|
+
throw error;
|
|
2622
|
+
}
|
|
2623
|
+
return steering ? "steered" : "started";
|
|
2624
|
+
}
|
|
2625
|
+
async cancelSubagent(nativeId, childId) {
|
|
2626
|
+
if (!this.isDesktopSession(nativeId))
|
|
2627
|
+
return super.cancelSubagent(nativeId, childId);
|
|
2628
|
+
// Only a conversation this one spawned (directly or through its sub-agents) may be stopped from here.
|
|
2629
|
+
const rows = this.desktopRows();
|
|
2630
|
+
let parent = rows?.get(childId)?.parent;
|
|
2631
|
+
for (let depth = 0; parent && parent !== nativeId && depth < 8; depth++)
|
|
2632
|
+
parent = rows?.get(parent)?.parent;
|
|
2633
|
+
if (parent !== nativeId)
|
|
2634
|
+
throw RpcError.app("not_supported", "这个子代理不属于当前会话");
|
|
2635
|
+
const conn = await this.requireConnection("停止子代理");
|
|
2636
|
+
await callLanguageServer(conn, "CancelCascadeInvocation", { cascadeId: childId, killBackgroundTasks: false });
|
|
1459
2637
|
}
|
|
1460
2638
|
async cancel(nativeId) {
|
|
1461
|
-
if (this.
|
|
1462
|
-
const
|
|
1463
|
-
const conn = await findRunningLanguageServer(desktop);
|
|
2639
|
+
if (this.isDesktopSession(nativeId)) {
|
|
2640
|
+
const conn = await this.languageServer();
|
|
1464
2641
|
if (conn) {
|
|
1465
2642
|
await callLanguageServer(conn, "CancelCascadeInvocation", {
|
|
1466
2643
|
cascadeId: nativeId,
|
|
@@ -1468,8 +2645,13 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1468
2645
|
}).catch(() => { });
|
|
1469
2646
|
}
|
|
1470
2647
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
1471
|
-
if (watcher)
|
|
2648
|
+
if (watcher) {
|
|
2649
|
+
if (watcher.pendingAction) {
|
|
2650
|
+
this.clearDesktopPendingAction(nativeId, watcher, { optionId: "cancel" });
|
|
2651
|
+
}
|
|
2652
|
+
watcher.sentAt = undefined;
|
|
1472
2653
|
watcher.running = false;
|
|
2654
|
+
}
|
|
1473
2655
|
this.desktopSessionStates.set(nativeId, "idle");
|
|
1474
2656
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "cancelled" });
|
|
1475
2657
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
@@ -1478,7 +2660,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
1478
2660
|
await super.cancel(nativeId);
|
|
1479
2661
|
}
|
|
1480
2662
|
async setConfig(nativeId, optionId, value) {
|
|
1481
|
-
if (this.
|
|
2663
|
+
if (this.isDesktopSession(nativeId)) {
|
|
1482
2664
|
if (optionId !== "model")
|
|
1483
2665
|
throw RpcError.app("not_supported", `Antigravity 暂不支持设置 ${optionId}`);
|
|
1484
2666
|
const watcher = this.desktopWatchers.get(nativeId);
|