@linkshell/host 0.4.32 → 0.4.34
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 +19 -5
- package/dist/host/src/drivers/acp/antigravity.js +636 -107
- package/dist/host/src/drivers/acp/antigravity.js.map +1 -1
- package/dist/host/src/hub.js +7 -1
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { homedir, tmpdir } from "node:os";
|
|
4
5
|
import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
|
|
5
6
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -249,6 +250,16 @@ export function antigravitySpec() {
|
|
|
249
250
|
authStatus: antigravityAuthStatus,
|
|
250
251
|
};
|
|
251
252
|
}
|
|
253
|
+
const ANTIGRAVITY_DESKTOP_COMMANDS = [
|
|
254
|
+
{ name: "goal", description: "设定持续目标,让 Antigravity 持续推进直到完成", hint: "<目标描述>" },
|
|
255
|
+
{ name: "plan", description: "先制定分步执行计划,再动手实现", hint: "<任务描述>" },
|
|
256
|
+
{ name: "boost", description: "深度思考、多视角分析与严格验证模式", hint: "<复杂问题或任务>" },
|
|
257
|
+
{ name: "teamwork-preview", description: "启动多 Agent 协作团队共同完成大型项目", hint: "<项目任务>" },
|
|
258
|
+
{ name: "grill-me", description: "通过互动提问对齐方案细节与设计决策", hint: "<主题>" },
|
|
259
|
+
{ name: "compact", description: "压缩并总结当前对话上下文以释放窗口空间" },
|
|
260
|
+
{ name: "schedule", description: "创建定时任务或后台周期自动化", hint: "<定时指令>" },
|
|
261
|
+
{ name: "learn", description: "总结并持久化当前经验与约定供后续会话使用" },
|
|
262
|
+
];
|
|
252
263
|
function unquoteArg(value) {
|
|
253
264
|
if (typeof value !== "string")
|
|
254
265
|
return value;
|
|
@@ -277,13 +288,112 @@ function parseToolArgs(raw) {
|
|
|
277
288
|
}
|
|
278
289
|
return result;
|
|
279
290
|
}
|
|
291
|
+
function stripTruncationMarkers(text) {
|
|
292
|
+
return text.replace(/\n?\.\.\.\s*<truncated\s+\d+\s+bytes>\s*\.\.\.\n?/gi, "\n").replace(/<truncated\s+\d+\s+bytes>/gi, "");
|
|
293
|
+
}
|
|
280
294
|
export function cleanAntigravityUserInput(content) {
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
const closedMatch = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/i.exec(content);
|
|
296
|
+
if (closedMatch)
|
|
297
|
+
return stripTruncationMarkers(closedMatch[1]).trim();
|
|
298
|
+
const openMatch = /<USER_REQUEST>\s*([\s\S]*?)(?:<(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY|EPILOGUE|GOAL_MODE|PLAN|TEAMWORK|BOOST)>|$)/i.exec(content);
|
|
299
|
+
if (openMatch)
|
|
300
|
+
return stripTruncationMarkers(openMatch[1]).trim();
|
|
301
|
+
return stripTruncationMarkers(content.replace(/<(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY|EPILOGUE|GOAL_MODE|PLAN|TEAMWORK|BOOST|GOAL)>[\s\S]*?<\/(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY|EPILOGUE|GOAL_MODE|PLAN|TEAMWORK|BOOST|GOAL)>/gi, ""))
|
|
302
|
+
.replace(/<!--\s*GOAL_(?:COMPLETE|CANCELLED)\s*-->/gi, "")
|
|
303
|
+
.trim();
|
|
304
|
+
}
|
|
305
|
+
export function cleanAntigravityAgentContent(content) {
|
|
306
|
+
const goalComplete = /<!--\s*GOAL_COMPLETE\s*-->/i.test(content);
|
|
307
|
+
const goalCancelled = /<!--\s*GOAL_CANCELLED\s*-->/i.test(content);
|
|
308
|
+
const text = stripTruncationMarkers(content)
|
|
309
|
+
.replace(/<!--\s*GOAL_(?:COMPLETE|CANCELLED)\s*-->/gi, "")
|
|
310
|
+
.replace(/<(?:SYSTEM_MESSAGE|EPILOGUE|GOAL_MODE|ADDITIONAL_METADATA|USER_SETTINGS_CHANGE)>[\s\S]*?<\/(?:SYSTEM_MESSAGE|EPILOGUE|GOAL_MODE|ADDITIONAL_METADATA|USER_SETTINGS_CHANGE)>/gi, "")
|
|
286
311
|
.trim();
|
|
312
|
+
return { text, goalComplete, goalCancelled };
|
|
313
|
+
}
|
|
314
|
+
function extractModelSelectionFromContent(content) {
|
|
315
|
+
if (!content)
|
|
316
|
+
return undefined;
|
|
317
|
+
const regex = /changed setting `?Model Selection`? from .*? to ([^\r\n.<]+?)(?:\.(?:\s|$)|<\/USER_SETTINGS_CHANGE>|\r|\n|$)/gi;
|
|
318
|
+
let last;
|
|
319
|
+
let match;
|
|
320
|
+
while ((match = regex.exec(content)) !== null) {
|
|
321
|
+
const candidate = match[1]?.trim();
|
|
322
|
+
if (candidate && candidate.toLowerCase() !== "none") {
|
|
323
|
+
last = candidate;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return last;
|
|
327
|
+
}
|
|
328
|
+
function parseGoalDurationSeconds(raw) {
|
|
329
|
+
if (!raw)
|
|
330
|
+
return undefined;
|
|
331
|
+
let total = 0;
|
|
332
|
+
let matched = false;
|
|
333
|
+
const hours = /(\d+)\s*h/i.exec(raw);
|
|
334
|
+
const mins = /(\d+)\s*m/i.exec(raw);
|
|
335
|
+
const secs = /(\d+)\s*s/i.exec(raw);
|
|
336
|
+
if (hours?.[1]) {
|
|
337
|
+
total += Number(hours[1]) * 3600;
|
|
338
|
+
matched = true;
|
|
339
|
+
}
|
|
340
|
+
if (mins?.[1]) {
|
|
341
|
+
total += Number(mins[1]) * 60;
|
|
342
|
+
matched = true;
|
|
343
|
+
}
|
|
344
|
+
if (secs?.[1]) {
|
|
345
|
+
total += Number(secs[1]);
|
|
346
|
+
matched = true;
|
|
347
|
+
}
|
|
348
|
+
return matched ? total : undefined;
|
|
349
|
+
}
|
|
350
|
+
function parseGoalSystemMessage(content, previousGoal) {
|
|
351
|
+
if (!content || !/Stop hook blocked termination:\s*You are still working toward the user's goal/i.test(content)) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const iterMatch = /Continuation:\s*#(\d+)/i.exec(content);
|
|
355
|
+
const timeMatch = /Time spent so far:\s*([^\r\n]+)/i.exec(content);
|
|
356
|
+
const origMatch = /Original request:\s*([\s\S]*?)(?:<\/SYSTEM_MESSAGE>|$)/i.exec(content);
|
|
357
|
+
const rawObjective = origMatch?.[1]?.trim().replace(/^\/goal(?:\s+|$)/i, "").trim();
|
|
358
|
+
const objective = rawObjective || previousGoal?.objective || "持续目标";
|
|
359
|
+
const iterations = iterMatch?.[1] ? Number(iterMatch[1]) : previousGoal?.iterations;
|
|
360
|
+
const timeUsedSeconds = parseGoalDurationSeconds(timeMatch?.[1]) ?? previousGoal?.timeUsedSeconds;
|
|
361
|
+
return {
|
|
362
|
+
objective,
|
|
363
|
+
status: "active",
|
|
364
|
+
...(iterations !== undefined ? { iterations } : {}),
|
|
365
|
+
...(timeUsedSeconds !== undefined ? { timeUsedSeconds } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function readStepMediaBlocks(media) {
|
|
369
|
+
if (!Array.isArray(media) || media.length === 0)
|
|
370
|
+
return [];
|
|
371
|
+
const blocks = [];
|
|
372
|
+
for (const entry of media) {
|
|
373
|
+
const mimeType = entry.mime_type || entry.mimeType || "image/png";
|
|
374
|
+
const inline = entry.inline_data || entry.inlineData;
|
|
375
|
+
if (typeof inline === "string" && inline.length > 0) {
|
|
376
|
+
blocks.push({ type: "image", mimeType, data: inline });
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const rawUri = entry.uri;
|
|
380
|
+
if (typeof rawUri === "string" && rawUri.length > 0) {
|
|
381
|
+
const filePath = rawUri.startsWith("file://") ? fileURLToPath(rawUri) : rawUri;
|
|
382
|
+
try {
|
|
383
|
+
if (existsSync(filePath)) {
|
|
384
|
+
const stat = statSync(filePath);
|
|
385
|
+
if (stat.isFile() && stat.size > 0 && stat.size <= 15 * 1024 * 1024) {
|
|
386
|
+
const data = readFileSync(filePath).toString("base64");
|
|
387
|
+
blocks.push({ type: "image", mimeType, data });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// ignore unreadable media file
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return blocks;
|
|
287
397
|
}
|
|
288
398
|
function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
289
399
|
const name = call.name ?? "tool";
|
|
@@ -328,6 +438,7 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
328
438
|
kind = "fetch";
|
|
329
439
|
rawInput = { url: typeof args.Url === "string" ? args.Url : summary };
|
|
330
440
|
}
|
|
441
|
+
const cleanedOutput = outputText ? stripTruncationMarkers(outputText) : undefined;
|
|
331
442
|
return {
|
|
332
443
|
sessionUpdate: "tool_call",
|
|
333
444
|
toolCallId,
|
|
@@ -336,13 +447,14 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
336
447
|
kind,
|
|
337
448
|
status: "completed",
|
|
338
449
|
rawInput,
|
|
339
|
-
...(
|
|
450
|
+
...(cleanedOutput ? { rawOutput: { output: cleanedOutput } } : {}),
|
|
340
451
|
...(locations ? { locations } : {}),
|
|
341
452
|
};
|
|
342
453
|
}
|
|
343
454
|
/**
|
|
344
|
-
* Converts an Antigravity Desktop `
|
|
345
|
-
* `HistoryItem[]` list
|
|
455
|
+
* Converts an Antigravity Desktop `transcript_full.jsonl` (or `transcript.jsonl`)
|
|
456
|
+
* file into LinkShell's `HistoryItem[]` list, tracking Goal Mode (`ls_goal`),
|
|
457
|
+
* model changes (`<USER_SETTINGS_CHANGE>`), and uploaded media.
|
|
346
458
|
*/
|
|
347
459
|
export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
348
460
|
if (!existsSync(transcriptPath))
|
|
@@ -354,8 +466,50 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
354
466
|
catch {
|
|
355
467
|
return { items: [], nextLine: fromLine };
|
|
356
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
|
+
}
|
|
357
511
|
if (fromLine >= lines.length)
|
|
358
|
-
return { items: [], nextLine: lines.length };
|
|
512
|
+
return { items: [], nextLine: lines.length, lastModelLabel, goal: activeGoal };
|
|
359
513
|
const parsed = [];
|
|
360
514
|
for (let i = fromLine; i < lines.length; i++) {
|
|
361
515
|
try {
|
|
@@ -364,41 +518,88 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
364
518
|
}
|
|
365
519
|
catch {
|
|
366
520
|
// skip partial trailing JSON line
|
|
367
|
-
return { items: [], nextLine: i };
|
|
521
|
+
return { items: [], nextLine: i, lastModelLabel, goal: activeGoal };
|
|
368
522
|
}
|
|
369
523
|
}
|
|
370
524
|
const items = [];
|
|
371
525
|
for (let i = 0; i < parsed.length; i++) {
|
|
372
526
|
const { index: stepIndex, step } = parsed[i];
|
|
373
527
|
if (step.type === "USER_INPUT") {
|
|
374
|
-
|
|
375
|
-
|
|
528
|
+
if (typeof step.content === "string") {
|
|
529
|
+
const modelLabel = extractModelSelectionFromContent(step.content);
|
|
530
|
+
if (modelLabel)
|
|
531
|
+
lastModelLabel = modelLabel;
|
|
532
|
+
}
|
|
533
|
+
const rawText = typeof step.content === "string" ? cleanAntigravityUserInput(step.content) : "";
|
|
534
|
+
const mediaBlocks = readStepMediaBlocks(step.media);
|
|
535
|
+
let displayText = rawText;
|
|
536
|
+
let goalUpdate;
|
|
537
|
+
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(rawText)) {
|
|
538
|
+
activeGoal = null;
|
|
539
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: null };
|
|
540
|
+
}
|
|
541
|
+
else if (/^\/goal(?:\s+|$)/i.test(rawText) || (typeof step.content === "string" && /\/goal is a \[Slash Command\]:/i.test(step.content))) {
|
|
542
|
+
const objective = rawText.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
543
|
+
displayText = objective;
|
|
544
|
+
activeGoal = { objective, status: "active" };
|
|
545
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: activeGoal };
|
|
546
|
+
}
|
|
547
|
+
else if (activeGoal?.status === "complete") {
|
|
548
|
+
activeGoal = null;
|
|
549
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: null };
|
|
550
|
+
}
|
|
551
|
+
if (!displayText && mediaBlocks.length === 0 && !goalUpdate)
|
|
376
552
|
continue;
|
|
377
553
|
const messageId = `agy:user:${stepIndex}`;
|
|
554
|
+
const updates = [];
|
|
555
|
+
for (const mediaBlock of mediaBlocks) {
|
|
556
|
+
updates.push({
|
|
557
|
+
sessionUpdate: "user_message_chunk",
|
|
558
|
+
messageId,
|
|
559
|
+
content: mediaBlock,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (displayText) {
|
|
563
|
+
updates.push({
|
|
564
|
+
sessionUpdate: "user_message_chunk",
|
|
565
|
+
messageId,
|
|
566
|
+
content: { type: "text", text: displayText },
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (goalUpdate) {
|
|
570
|
+
updates.push(goalUpdate);
|
|
571
|
+
}
|
|
378
572
|
items.push({
|
|
379
573
|
itemId: messageId,
|
|
380
|
-
updates
|
|
381
|
-
{
|
|
382
|
-
sessionUpdate: "user_message_chunk",
|
|
383
|
-
messageId,
|
|
384
|
-
content: { type: "text", text },
|
|
385
|
-
},
|
|
386
|
-
],
|
|
574
|
+
updates,
|
|
387
575
|
});
|
|
388
576
|
}
|
|
577
|
+
else if (step.type === "SYSTEM_MESSAGE") {
|
|
578
|
+
const nextGoal = parseGoalSystemMessage(step.content, activeGoal);
|
|
579
|
+
if (nextGoal) {
|
|
580
|
+
activeGoal = nextGoal;
|
|
581
|
+
items.push({
|
|
582
|
+
itemId: `agy:goal:${stepIndex}`,
|
|
583
|
+
updates: [{ sessionUpdate: "ls_goal", goal: activeGoal }],
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
389
587
|
else if (step.type === "PLANNER_RESPONSE") {
|
|
390
588
|
const updates = [];
|
|
391
|
-
if (typeof step.thinking === "string"
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
589
|
+
if (typeof step.thinking === "string") {
|
|
590
|
+
const cleanThought = stripTruncationMarkers(step.thinking).trim();
|
|
591
|
+
if (cleanThought) {
|
|
592
|
+
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
|
+
});
|
|
602
|
+
}
|
|
402
603
|
}
|
|
403
604
|
const calls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
|
|
404
605
|
if (calls.length > 0) {
|
|
@@ -414,16 +615,27 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
414
615
|
});
|
|
415
616
|
}
|
|
416
617
|
if (typeof step.content === "string" && step.content.trim()) {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
618
|
+
const { text: cleanText, goalComplete, goalCancelled } = cleanAntigravityAgentContent(step.content);
|
|
619
|
+
if (cleanText) {
|
|
620
|
+
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
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (goalCancelled) {
|
|
632
|
+
activeGoal = null;
|
|
633
|
+
updates.push({ sessionUpdate: "ls_goal", goal: null });
|
|
634
|
+
}
|
|
635
|
+
else if (goalComplete) {
|
|
636
|
+
activeGoal = { ...(activeGoal ?? { objective: "持续目标" }), status: "complete" };
|
|
637
|
+
updates.push({ sessionUpdate: "ls_goal", goal: activeGoal });
|
|
638
|
+
}
|
|
427
639
|
}
|
|
428
640
|
if (updates.length > 0) {
|
|
429
641
|
items.push({
|
|
@@ -433,7 +645,7 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
433
645
|
}
|
|
434
646
|
}
|
|
435
647
|
}
|
|
436
|
-
return { items, nextLine: lines.length };
|
|
648
|
+
return { items, nextLine: lines.length, lastModelLabel, goal: activeGoal };
|
|
437
649
|
}
|
|
438
650
|
async function findRunningLanguageServer(desktop) {
|
|
439
651
|
if (!desktop?.logPath || !existsSync(desktop.logPath))
|
|
@@ -500,13 +712,30 @@ function parseWorkspaceCwd(workspaceUrisJson, fallbackCwd) {
|
|
|
500
712
|
}
|
|
501
713
|
return fallbackCwd;
|
|
502
714
|
}
|
|
715
|
+
function resolveDesktopTranscriptPath(dataDir, nativeId) {
|
|
716
|
+
const logsDir = join(dataDir, "brain", nativeId, ".system_generated", "logs");
|
|
717
|
+
const fullPath = join(logsDir, "transcript_full.jsonl");
|
|
718
|
+
if (existsSync(fullPath))
|
|
719
|
+
return fullPath;
|
|
720
|
+
return join(logsDir, "transcript.jsonl");
|
|
721
|
+
}
|
|
722
|
+
function extForMimeType(mimeType) {
|
|
723
|
+
const sub = mimeType.split("/")[1]?.toLowerCase() ?? "png";
|
|
724
|
+
if (sub === "jpeg" || sub === "jpg")
|
|
725
|
+
return "jpg";
|
|
726
|
+
if (sub === "webp")
|
|
727
|
+
return "webp";
|
|
728
|
+
if (sub === "gif")
|
|
729
|
+
return "gif";
|
|
730
|
+
return "png";
|
|
731
|
+
}
|
|
503
732
|
/**
|
|
504
733
|
* Specialized driver for Google Antigravity:
|
|
505
734
|
* - Drives `agy_acp_server.par` / `agy-acp` over ACP when installed.
|
|
506
735
|
* - Also discovers and interacts with Antigravity Desktop (`/Applications/Antigravity.app`,
|
|
507
|
-
* `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/
|
|
736
|
+
* `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/transcript_full.jsonl`,
|
|
508
737
|
* and its local `language_server` Connect-RPC service) so users with only the Desktop app
|
|
509
|
-
* get full session visibility, history,
|
|
738
|
+
* get full session visibility, history, Goal Mode, image messaging, and model selection.
|
|
510
739
|
*/
|
|
511
740
|
export class AntigravityDriver extends AcpDriver {
|
|
512
741
|
desktopMode = false;
|
|
@@ -514,6 +743,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
514
743
|
desktopSessionStates = new Map();
|
|
515
744
|
desktopWatchers = new Map();
|
|
516
745
|
desktopModelsCache;
|
|
746
|
+
desktopDefaultModel;
|
|
517
747
|
constructor(options) {
|
|
518
748
|
super(antigravitySpec(), options);
|
|
519
749
|
}
|
|
@@ -577,6 +807,12 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
577
807
|
const prev = this.desktopSessionStates.get(s.nativeId);
|
|
578
808
|
this.desktopSessionStates.set(s.nativeId, nextState);
|
|
579
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
|
+
}
|
|
580
816
|
this.host?.update(this.id, s.nativeId, {
|
|
581
817
|
sessionUpdate: "ls_status",
|
|
582
818
|
state: nextState,
|
|
@@ -605,7 +841,26 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
605
841
|
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
606
842
|
if (!desktop)
|
|
607
843
|
return undefined;
|
|
608
|
-
return
|
|
844
|
+
return resolveDesktopTranscriptPath(desktop.dataDir, nativeId);
|
|
845
|
+
}
|
|
846
|
+
desktopAliasesFor(nativeId) {
|
|
847
|
+
try {
|
|
848
|
+
const raw = this.host?.state(this.id, nativeId).get("agy-desktop-aliases");
|
|
849
|
+
if (!raw)
|
|
850
|
+
return new Map();
|
|
851
|
+
return new Map(JSON.parse(raw));
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
return new Map();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
saveDesktopAliases(nativeId, aliases) {
|
|
858
|
+
try {
|
|
859
|
+
this.host?.state(this.id, nativeId).set("agy-desktop-aliases", JSON.stringify([...aliases]));
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// ignore state write errors
|
|
863
|
+
}
|
|
609
864
|
}
|
|
610
865
|
isConversationRunningInDesktop(nativeId, statusCol) {
|
|
611
866
|
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
@@ -665,11 +920,13 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
665
920
|
const cwd = parseWorkspaceCwd(row.workspace_uris, home);
|
|
666
921
|
const updatedAt = Date.parse(row.last_modified_time) || Date.now();
|
|
667
922
|
let title = row.title?.trim() || undefined;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
923
|
+
let model;
|
|
924
|
+
const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, row.conversation_id);
|
|
925
|
+
if (!title && existsSync(transcriptPath)) {
|
|
926
|
+
const parsed = parseAntigravityTranscript(transcriptPath, 0);
|
|
927
|
+
model = parsed.lastModelLabel;
|
|
928
|
+
const firstUser = parsed.items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
929
|
+
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk" && u.content.type === "text");
|
|
673
930
|
if (chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text") {
|
|
674
931
|
title = chunk.content.text.split("\n")[0]?.slice(0, 100).trim() || undefined;
|
|
675
932
|
}
|
|
@@ -683,6 +940,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
683
940
|
nativeId: row.conversation_id,
|
|
684
941
|
cwd,
|
|
685
942
|
title,
|
|
943
|
+
...(model ? { model } : {}),
|
|
686
944
|
createdAt: updatedAt,
|
|
687
945
|
updatedAt,
|
|
688
946
|
state,
|
|
@@ -705,13 +963,13 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
705
963
|
try {
|
|
706
964
|
const dirs = readdirSync(join(desktop.dataDir, "brain"));
|
|
707
965
|
for (const id of dirs) {
|
|
708
|
-
const transcriptPath =
|
|
966
|
+
const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, id);
|
|
709
967
|
if (!existsSync(transcriptPath))
|
|
710
968
|
continue;
|
|
711
969
|
const stat = statSync(transcriptPath);
|
|
712
|
-
const
|
|
713
|
-
const firstUser = items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
714
|
-
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk");
|
|
970
|
+
const parsed = parseAntigravityTranscript(transcriptPath, 0);
|
|
971
|
+
const firstUser = parsed.items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
972
|
+
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk" && u.content.type === "text");
|
|
715
973
|
const title = chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text"
|
|
716
974
|
? chunk.content.text.split("\n")[0]?.slice(0, 100).trim()
|
|
717
975
|
: undefined;
|
|
@@ -719,6 +977,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
719
977
|
nativeId: id,
|
|
720
978
|
cwd: home,
|
|
721
979
|
title,
|
|
980
|
+
...(parsed.lastModelLabel ? { model: parsed.lastModelLabel } : {}),
|
|
722
981
|
createdAt: stat.mtimeMs,
|
|
723
982
|
updatedAt: stat.mtimeMs,
|
|
724
983
|
state: "idle",
|
|
@@ -752,6 +1011,16 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
752
1011
|
return [];
|
|
753
1012
|
try {
|
|
754
1013
|
const data = await callLanguageServer(conn, "GetCascadeModelConfigData", {});
|
|
1014
|
+
const defaultOverride = data.defaultOverrideModelConfig?.modelOrAlias?.model ??
|
|
1015
|
+
data.defaultOverrideModelConfig?.modelOrAlias?.alias;
|
|
1016
|
+
if (defaultOverride)
|
|
1017
|
+
this.desktopDefaultModel = defaultOverride;
|
|
1018
|
+
const sortOrder = new Map();
|
|
1019
|
+
const sortedLabels = data.clientModelSorts?.[0]?.groups?.flatMap((g) => g.modelLabels ?? []) ?? [];
|
|
1020
|
+
sortedLabels.forEach((label, idx) => {
|
|
1021
|
+
if (!sortOrder.has(label))
|
|
1022
|
+
sortOrder.set(label, idx);
|
|
1023
|
+
});
|
|
755
1024
|
const models = (data.clientModelConfigs ?? [])
|
|
756
1025
|
.map((entry) => {
|
|
757
1026
|
const value = entry.modelOrAlias?.model ?? entry.modelOrAlias?.alias;
|
|
@@ -765,6 +1034,13 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
765
1034
|
};
|
|
766
1035
|
})
|
|
767
1036
|
.filter((entry) => Boolean(entry));
|
|
1037
|
+
if (sortOrder.size > 0) {
|
|
1038
|
+
models.sort((a, b) => {
|
|
1039
|
+
const rankA = sortOrder.get(a.name) ?? 9999;
|
|
1040
|
+
const rankB = sortOrder.get(b.name) ?? 9999;
|
|
1041
|
+
return rankA - rankB;
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
768
1044
|
if (models.length > 0)
|
|
769
1045
|
this.desktopModelsCache = models;
|
|
770
1046
|
return models;
|
|
@@ -773,56 +1049,157 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
773
1049
|
return [];
|
|
774
1050
|
}
|
|
775
1051
|
}
|
|
776
|
-
async
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1052
|
+
async resolveSessionModel(nativeId, models, explicitModel) {
|
|
1053
|
+
const matchByValueOrName = (candidate) => {
|
|
1054
|
+
if (!candidate)
|
|
1055
|
+
return undefined;
|
|
1056
|
+
return models.find((m) => m.value === candidate || m.name === candidate);
|
|
1057
|
+
};
|
|
1058
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1059
|
+
const persistedSelected = this.host?.state(this.id, nativeId).get("agy-selected-model");
|
|
1060
|
+
const chosen = matchByValueOrName(explicitModel) ??
|
|
1061
|
+
matchByValueOrName(watcher?.selectedModel) ??
|
|
1062
|
+
matchByValueOrName(persistedSelected) ??
|
|
1063
|
+
matchByValueOrName(watcher?.lastModelLabel);
|
|
1064
|
+
if (chosen)
|
|
1065
|
+
return chosen;
|
|
1066
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
1067
|
+
const conn = await findRunningLanguageServer(desktop);
|
|
1068
|
+
if (conn) {
|
|
1069
|
+
try {
|
|
1070
|
+
const meta = await callLanguageServer(conn, "GetCascadeTrajectoryGeneratorMetadata", { cascadeId: nativeId });
|
|
1071
|
+
const entries = meta.generatorMetadata ?? [];
|
|
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;
|
|
789
1081
|
}
|
|
790
|
-
|
|
791
|
-
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
// ignore metadata RPC failure
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const fromDefault = matchByValueOrName(this.desktopDefaultModel);
|
|
1088
|
+
if (fromDefault)
|
|
1089
|
+
return fromDefault;
|
|
1090
|
+
if (desktop && existsSync(join(desktop.dataDir, "antigravity_state.pbtxt"))) {
|
|
1091
|
+
try {
|
|
1092
|
+
const stateText = readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8");
|
|
1093
|
+
const match = /last_selected_agent_model:\s*(\S+)/.exec(stateText);
|
|
1094
|
+
const found = matchByValueOrName(match?.[1]);
|
|
1095
|
+
if (found)
|
|
1096
|
+
return found;
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
// ignore
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return models[0];
|
|
1103
|
+
}
|
|
1104
|
+
async resolvePlanModelEnum(conn, nativeId, explicitModel) {
|
|
1105
|
+
const models = await this.loadDesktopModels();
|
|
1106
|
+
if (models.length > 0) {
|
|
1107
|
+
const matched = await this.resolveSessionModel(nativeId, models, explicitModel);
|
|
1108
|
+
if (matched?.value)
|
|
1109
|
+
return matched.value;
|
|
1110
|
+
}
|
|
1111
|
+
if (explicitModel?.startsWith("MODEL_"))
|
|
1112
|
+
return explicitModel;
|
|
1113
|
+
if (nativeId) {
|
|
1114
|
+
try {
|
|
1115
|
+
const meta = await callLanguageServer(conn, "GetCascadeTrajectoryGeneratorMetadata", { cascadeId: nativeId });
|
|
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;
|
|
792
1124
|
}
|
|
793
1125
|
}
|
|
1126
|
+
catch {
|
|
1127
|
+
// ignore metadata RPC failure
|
|
1128
|
+
}
|
|
794
1129
|
}
|
|
795
|
-
|
|
1130
|
+
if (this.desktopDefaultModel?.startsWith("MODEL_"))
|
|
1131
|
+
return this.desktopDefaultModel;
|
|
1132
|
+
return "MODEL_PLACEHOLDER_M418";
|
|
1133
|
+
}
|
|
1134
|
+
async emitDesktopConfig(nativeId, selectedModel) {
|
|
1135
|
+
this.host?.update(this.id, nativeId, {
|
|
1136
|
+
sessionUpdate: "available_commands_update",
|
|
1137
|
+
availableCommands: ANTIGRAVITY_DESKTOP_COMMANDS,
|
|
1138
|
+
});
|
|
1139
|
+
const models = await this.loadDesktopModels();
|
|
1140
|
+
if (models.length === 0)
|
|
1141
|
+
return;
|
|
1142
|
+
const matched = await this.resolveSessionModel(nativeId, models, selectedModel);
|
|
796
1143
|
const options = [
|
|
797
1144
|
{
|
|
798
1145
|
id: "model",
|
|
799
1146
|
name: "Model",
|
|
800
1147
|
category: "model",
|
|
801
|
-
current,
|
|
1148
|
+
current: matched.value,
|
|
802
1149
|
values: models,
|
|
803
1150
|
},
|
|
804
1151
|
];
|
|
805
1152
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_config", options });
|
|
1153
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "session_info_update", model: matched.name });
|
|
806
1154
|
}
|
|
807
|
-
startWatchingDesktopTranscript(nativeId,
|
|
1155
|
+
startWatchingDesktopTranscript(nativeId, initialLine, initialMeta) {
|
|
808
1156
|
const existing = this.desktopWatchers.get(nativeId);
|
|
809
1157
|
if (existing)
|
|
810
1158
|
clearInterval(existing.timer);
|
|
811
|
-
const
|
|
1159
|
+
const activePath = this.transcriptPathFor(nativeId);
|
|
1160
|
+
const lastSize = activePath && existsSync(activePath) ? statSync(activePath).size : 0;
|
|
1161
|
+
const initialRunning = this.isConversationRunningInDesktop(nativeId);
|
|
1162
|
+
this.desktopSessionStates.set(nativeId, initialRunning ? "running" : "idle");
|
|
812
1163
|
const watcher = {
|
|
813
1164
|
nextLine: initialLine,
|
|
814
1165
|
lastSize,
|
|
815
|
-
running:
|
|
816
|
-
|
|
1166
|
+
running: initialRunning,
|
|
1167
|
+
detached: false,
|
|
1168
|
+
selectedModel: existing?.selectedModel ?? this.host?.state(this.id, nativeId).get("agy-selected-model") ?? undefined,
|
|
1169
|
+
lastModelLabel: initialMeta?.lastModelLabel ?? existing?.lastModelLabel,
|
|
1170
|
+
goal: initialMeta?.goal !== undefined ? initialMeta.goal : existing?.goal,
|
|
1171
|
+
pendingClientMessages: existing?.pendingClientMessages ?? [],
|
|
817
1172
|
timer: setInterval(() => {
|
|
818
|
-
|
|
1173
|
+
const currentPath = this.transcriptPathFor(nativeId);
|
|
1174
|
+
if (currentPath && existsSync(currentPath)) {
|
|
819
1175
|
try {
|
|
820
|
-
const stat = statSync(
|
|
821
|
-
if (stat.size
|
|
1176
|
+
const stat = statSync(currentPath);
|
|
1177
|
+
if (stat.size !== watcher.lastSize) {
|
|
822
1178
|
watcher.lastSize = stat.size;
|
|
823
|
-
const
|
|
824
|
-
watcher.nextLine = nextLine;
|
|
825
|
-
|
|
1179
|
+
const parsed = parseAntigravityTranscript(currentPath, watcher.nextLine);
|
|
1180
|
+
watcher.nextLine = parsed.nextLine;
|
|
1181
|
+
if (parsed.lastModelLabel && parsed.lastModelLabel !== watcher.lastModelLabel) {
|
|
1182
|
+
watcher.lastModelLabel = parsed.lastModelLabel;
|
|
1183
|
+
watcher.selectedModel = parsed.lastModelLabel;
|
|
1184
|
+
this.host?.state(this.id, nativeId).set("agy-selected-model", parsed.lastModelLabel);
|
|
1185
|
+
void this.emitDesktopConfig(nativeId, parsed.lastModelLabel);
|
|
1186
|
+
}
|
|
1187
|
+
if (parsed.goal !== undefined) {
|
|
1188
|
+
watcher.goal = parsed.goal;
|
|
1189
|
+
}
|
|
1190
|
+
for (const item of parsed.items) {
|
|
1191
|
+
if (item.itemId.startsWith("agy:user:") && watcher.pendingClientMessages.length > 0) {
|
|
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
|
+
}
|
|
826
1203
|
item.updates.forEach((update, idx) => {
|
|
827
1204
|
this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
|
|
828
1205
|
});
|
|
@@ -836,6 +1213,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
836
1213
|
const nowRunning = this.isConversationRunningInDesktop(nativeId);
|
|
837
1214
|
if (nowRunning !== watcher.running) {
|
|
838
1215
|
watcher.running = nowRunning;
|
|
1216
|
+
this.desktopSessionStates.set(nativeId, nowRunning ? "running" : "idle");
|
|
839
1217
|
if (nowRunning) {
|
|
840
1218
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
841
1219
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
|
|
@@ -843,23 +1221,41 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
843
1221
|
else {
|
|
844
1222
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
845
1223
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
1224
|
+
if (watcher.detached) {
|
|
1225
|
+
clearInterval(watcher.timer);
|
|
1226
|
+
this.desktopWatchers.delete(nativeId);
|
|
1227
|
+
}
|
|
846
1228
|
}
|
|
847
1229
|
}
|
|
848
1230
|
}, 1500),
|
|
849
1231
|
};
|
|
850
1232
|
watcher.timer.unref?.();
|
|
851
1233
|
this.desktopWatchers.set(nativeId, watcher);
|
|
1234
|
+
return watcher;
|
|
852
1235
|
}
|
|
853
1236
|
async attach(nativeId, context) {
|
|
854
1237
|
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
855
1238
|
if (this.desktopMode || (transcriptPath && existsSync(transcriptPath))) {
|
|
856
1239
|
await this.ensureStarted();
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1240
|
+
const parsed = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : { items: [], nextLine: 0 };
|
|
1241
|
+
const aliases = this.desktopAliasesFor(nativeId);
|
|
1242
|
+
const items = parsed.items.map((item) => {
|
|
1243
|
+
const aliasedId = aliases.get(item.itemId);
|
|
1244
|
+
return aliasedId ? { ...item, itemId: aliasedId } : item;
|
|
1245
|
+
});
|
|
1246
|
+
this.startWatchingDesktopTranscript(nativeId, parsed.nextLine, {
|
|
1247
|
+
lastModelLabel: parsed.lastModelLabel,
|
|
1248
|
+
goal: parsed.goal,
|
|
1249
|
+
});
|
|
861
1250
|
const running = this.isConversationRunningInDesktop(nativeId);
|
|
1251
|
+
this.desktopSessionStates.set(nativeId, running ? "running" : "idle");
|
|
1252
|
+
if (!running) {
|
|
1253
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
1254
|
+
}
|
|
862
1255
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: running ? "running" : "idle", turnActive: running });
|
|
1256
|
+
if (parsed.goal !== undefined) {
|
|
1257
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: parsed.goal });
|
|
1258
|
+
}
|
|
863
1259
|
void this.emitDesktopConfig(nativeId, this.desktopWatchers.get(nativeId)?.selectedModel);
|
|
864
1260
|
return items;
|
|
865
1261
|
}
|
|
@@ -868,8 +1264,14 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
868
1264
|
async detach(nativeId) {
|
|
869
1265
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
870
1266
|
if (watcher) {
|
|
871
|
-
|
|
872
|
-
|
|
1267
|
+
if (watcher.running) {
|
|
1268
|
+
// Keep watching until the active turn completes so turn end and final messages are recorded.
|
|
1269
|
+
watcher.detached = true;
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
clearInterval(watcher.timer);
|
|
1273
|
+
this.desktopWatchers.delete(nativeId);
|
|
1274
|
+
}
|
|
873
1275
|
}
|
|
874
1276
|
await super.detach(nativeId);
|
|
875
1277
|
}
|
|
@@ -882,26 +1284,46 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
882
1284
|
if (!conn) {
|
|
883
1285
|
throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用");
|
|
884
1286
|
}
|
|
1287
|
+
const requestedModel = await this.resolvePlanModelEnum(conn, "", options.model);
|
|
885
1288
|
const response = await callLanguageServer(conn, "StartCascade", {
|
|
1289
|
+
source: "CORTEX_TRAJECTORY_SOURCE_CASCADE_CLIENT",
|
|
886
1290
|
workspaceUris: [pathToFileURL(options.cwd).href],
|
|
887
|
-
|
|
888
|
-
...(options.model ? { requestedModel: options.model } : {}),
|
|
1291
|
+
requestedModel,
|
|
889
1292
|
});
|
|
890
1293
|
const nativeId = response.cascadeId;
|
|
891
1294
|
if (!nativeId)
|
|
892
1295
|
throw RpcError.app("agent_unavailable", "Antigravity 未返回新会话 ID");
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if (watcher)
|
|
899
|
-
watcher.selectedModel = options.model;
|
|
900
|
-
}
|
|
901
|
-
void this.emitDesktopConfig(nativeId, options.model);
|
|
1296
|
+
const watcher = this.startWatchingDesktopTranscript(nativeId, 0);
|
|
1297
|
+
const selectedModel = options.model ?? requestedModel;
|
|
1298
|
+
watcher.selectedModel = selectedModel;
|
|
1299
|
+
this.host?.state(this.id, nativeId).set("agy-selected-model", selectedModel);
|
|
1300
|
+
void this.emitDesktopConfig(nativeId, selectedModel);
|
|
902
1301
|
const now = Date.now();
|
|
903
1302
|
return { nativeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
|
|
904
1303
|
}
|
|
1304
|
+
async goal(nativeId, change) {
|
|
1305
|
+
if (!this.desktopMode && !this.desktopWatchers.has(nativeId)) {
|
|
1306
|
+
throw RpcError.app("not_supported", "这个 Agent 通过 /goal 命令管理目标");
|
|
1307
|
+
}
|
|
1308
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1309
|
+
if (change.action === "get") {
|
|
1310
|
+
return watcher?.goal ?? null;
|
|
1311
|
+
}
|
|
1312
|
+
if (change.action === "clear") {
|
|
1313
|
+
if (watcher)
|
|
1314
|
+
watcher.goal = null;
|
|
1315
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
if (change.action === "set") {
|
|
1319
|
+
const nextGoal = { objective: change.objective, status: "active" };
|
|
1320
|
+
if (watcher)
|
|
1321
|
+
watcher.goal = nextGoal;
|
|
1322
|
+
await this.prompt(nativeId, [{ type: "text", text: `/goal ${change.objective}` }], `goal-${Date.now()}`);
|
|
1323
|
+
return nextGoal;
|
|
1324
|
+
}
|
|
1325
|
+
return watcher?.goal ?? null;
|
|
1326
|
+
}
|
|
905
1327
|
async prompt(nativeId, content, clientMessageId, context) {
|
|
906
1328
|
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
907
1329
|
if (!this.desktopMode && !(transcriptPath && existsSync(transcriptPath))) {
|
|
@@ -916,21 +1338,123 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
916
1338
|
.map((block) => (block.type === "text" ? block.text : ""))
|
|
917
1339
|
.join("\n")
|
|
918
1340
|
.trim();
|
|
919
|
-
|
|
1341
|
+
const imageBlocks = content.filter((block) => block.type === "image");
|
|
1342
|
+
if (!text && imageBlocks.length === 0)
|
|
920
1343
|
throw RpcError.app("invalid_params", "nothing to send");
|
|
921
|
-
|
|
922
|
-
|
|
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
|
+
let watcher = this.desktopWatchers.get(nativeId);
|
|
1358
|
+
if (!watcher) {
|
|
1359
|
+
const parsed = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : undefined;
|
|
1360
|
+
watcher = this.startWatchingDesktopTranscript(nativeId, parsed?.nextLine ?? 0, parsed);
|
|
1361
|
+
}
|
|
1362
|
+
// Echo user message with `local-${clientMessageId}` so the mobile client's optimistic bubble is confirmed in-place.
|
|
1363
|
+
watcher.pendingClientMessages.push({ clientMessageId, text });
|
|
1364
|
+
const displayBlocks = content.map((block) => {
|
|
1365
|
+
if (block.type === "text" && /^\/goal(?:\s+|$)/i.test(block.text.trim())) {
|
|
1366
|
+
const cleaned = block.text.trim().replace(/^\/goal(?:\s+|$)/i, "").trim() || block.text;
|
|
1367
|
+
return { type: "text", text: cleaned };
|
|
1368
|
+
}
|
|
1369
|
+
return block;
|
|
1370
|
+
});
|
|
1371
|
+
for (const block of displayBlocks) {
|
|
1372
|
+
this.host?.update(this.id, nativeId, {
|
|
1373
|
+
sessionUpdate: "user_message_chunk",
|
|
1374
|
+
messageId: `local-${clientMessageId}`,
|
|
1375
|
+
content: block,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_message_done", messageId: `local-${clientMessageId}`, role: "user" }, `local-${clientMessageId}`);
|
|
1379
|
+
if (/^\/goal(?:\s+|$)/i.test(text)) {
|
|
1380
|
+
const objective = text.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
1381
|
+
const nextGoal = { objective, status: "active" };
|
|
1382
|
+
watcher.goal = nextGoal;
|
|
1383
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: nextGoal });
|
|
1384
|
+
}
|
|
1385
|
+
else if (watcher.goal?.status === "complete") {
|
|
1386
|
+
watcher.goal = null;
|
|
1387
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1388
|
+
}
|
|
1389
|
+
const media = [];
|
|
1390
|
+
if (imageBlocks.length > 0 && desktop) {
|
|
1391
|
+
const uploadDir = join(desktop.dataDir, "brain", nativeId, ".user_uploaded");
|
|
1392
|
+
try {
|
|
1393
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
// ignore mkdir error
|
|
1397
|
+
}
|
|
1398
|
+
for (const img of imageBlocks) {
|
|
1399
|
+
if (!img.data)
|
|
1400
|
+
continue;
|
|
1401
|
+
const ext = extForMimeType(img.mimeType);
|
|
1402
|
+
const fileName = `media_${Date.now()}_${randomBytes(4).toString("hex")}.${ext}`;
|
|
1403
|
+
const filePath = join(uploadDir, fileName);
|
|
1404
|
+
let savedUri;
|
|
1405
|
+
try {
|
|
1406
|
+
writeFileSync(filePath, Buffer.from(img.data, "base64"));
|
|
1407
|
+
savedUri = filePath;
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
// fallback to inlineData only
|
|
1411
|
+
}
|
|
1412
|
+
media.push({
|
|
1413
|
+
mimeType: img.mimeType,
|
|
1414
|
+
inlineData: img.data,
|
|
1415
|
+
...(savedUri ? { uri: savedUri } : {}),
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
const fullText = context ? (text ? `${context}\n\n${text}` : context) : text;
|
|
1420
|
+
const planModel = await this.resolvePlanModelEnum(conn, nativeId, watcher.selectedModel);
|
|
1421
|
+
watcher.running = true;
|
|
1422
|
+
this.desktopSessionStates.set(nativeId, "running");
|
|
923
1423
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
924
1424
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
|
|
925
1425
|
await callLanguageServer(conn, "SendUserCascadeMessage", {
|
|
926
1426
|
cascadeId: nativeId,
|
|
927
|
-
items: [{ text: fullText }],
|
|
928
|
-
...(
|
|
1427
|
+
items: [{ text: fullText || "请查看附图" }],
|
|
1428
|
+
...(media.length > 0 ? { media } : {}),
|
|
1429
|
+
cascadeConfig: {
|
|
1430
|
+
plannerConfig: {
|
|
1431
|
+
supportsLatexRendering: true,
|
|
1432
|
+
toolConfig: {
|
|
1433
|
+
runCommand: {
|
|
1434
|
+
autoCommandConfig: {
|
|
1435
|
+
autoExecutionPolicy: "CASCADE_COMMANDS_AUTO_EXECUTION_EAGER",
|
|
1436
|
+
},
|
|
1437
|
+
},
|
|
1438
|
+
notifyUser: {},
|
|
1439
|
+
},
|
|
1440
|
+
planModel,
|
|
1441
|
+
useAiCredits: false,
|
|
1442
|
+
},
|
|
1443
|
+
executorConfig: {
|
|
1444
|
+
useCoreDirect: true,
|
|
1445
|
+
},
|
|
1446
|
+
conversationHistoryConfig: {},
|
|
1447
|
+
},
|
|
1448
|
+
customAgentSpec: {
|
|
1449
|
+
builtinAgent: {
|
|
1450
|
+
defaultAgent: {
|
|
1451
|
+
isGoogle: false,
|
|
1452
|
+
isInteractive: true,
|
|
1453
|
+
surface: "AGENT_SURFACE_HUB",
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
929
1457
|
});
|
|
930
|
-
if (transcriptPath && !this.desktopWatchers.has(nativeId)) {
|
|
931
|
-
const { nextLine } = parseAntigravityTranscript(transcriptPath, 0);
|
|
932
|
-
this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
|
|
933
|
-
}
|
|
934
1458
|
return "started";
|
|
935
1459
|
}
|
|
936
1460
|
async cancel(nativeId) {
|
|
@@ -943,6 +1467,10 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
943
1467
|
killBackgroundTasks: false,
|
|
944
1468
|
}).catch(() => { });
|
|
945
1469
|
}
|
|
1470
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1471
|
+
if (watcher)
|
|
1472
|
+
watcher.running = false;
|
|
1473
|
+
this.desktopSessionStates.set(nativeId, "idle");
|
|
946
1474
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "cancelled" });
|
|
947
1475
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
948
1476
|
return;
|
|
@@ -956,6 +1484,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
956
1484
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
957
1485
|
if (watcher)
|
|
958
1486
|
watcher.selectedModel = value;
|
|
1487
|
+
this.host?.state(this.id, nativeId).set("agy-selected-model", value);
|
|
959
1488
|
await this.emitDesktopConfig(nativeId, value);
|
|
960
1489
|
return;
|
|
961
1490
|
}
|