@linkshell/host 0.4.31 → 0.4.33
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 +20 -5
- package/dist/host/src/drivers/acp/antigravity.js +646 -115
- 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,110 @@ 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
|
+
if (match[1]?.trim())
|
|
322
|
+
last = match[1].trim();
|
|
323
|
+
}
|
|
324
|
+
return last;
|
|
325
|
+
}
|
|
326
|
+
function parseGoalDurationSeconds(raw) {
|
|
327
|
+
if (!raw)
|
|
328
|
+
return undefined;
|
|
329
|
+
let total = 0;
|
|
330
|
+
let matched = false;
|
|
331
|
+
const hours = /(\d+)\s*h/i.exec(raw);
|
|
332
|
+
const mins = /(\d+)\s*m/i.exec(raw);
|
|
333
|
+
const secs = /(\d+)\s*s/i.exec(raw);
|
|
334
|
+
if (hours?.[1]) {
|
|
335
|
+
total += Number(hours[1]) * 3600;
|
|
336
|
+
matched = true;
|
|
337
|
+
}
|
|
338
|
+
if (mins?.[1]) {
|
|
339
|
+
total += Number(mins[1]) * 60;
|
|
340
|
+
matched = true;
|
|
341
|
+
}
|
|
342
|
+
if (secs?.[1]) {
|
|
343
|
+
total += Number(secs[1]);
|
|
344
|
+
matched = true;
|
|
345
|
+
}
|
|
346
|
+
return matched ? total : undefined;
|
|
347
|
+
}
|
|
348
|
+
function parseGoalSystemMessage(content, previousGoal) {
|
|
349
|
+
if (!content || !/Stop hook blocked termination:\s*You are still working toward the user's goal/i.test(content)) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
const iterMatch = /Continuation:\s*#(\d+)/i.exec(content);
|
|
353
|
+
const timeMatch = /Time spent so far:\s*([^\r\n]+)/i.exec(content);
|
|
354
|
+
const origMatch = /Original request:\s*([\s\S]*?)(?:<\/SYSTEM_MESSAGE>|$)/i.exec(content);
|
|
355
|
+
const rawObjective = origMatch?.[1]?.trim().replace(/^\/goal(?:\s+|$)/i, "").trim();
|
|
356
|
+
const objective = rawObjective || previousGoal?.objective || "持续目标";
|
|
357
|
+
const iterations = iterMatch?.[1] ? Number(iterMatch[1]) : previousGoal?.iterations;
|
|
358
|
+
const timeUsedSeconds = parseGoalDurationSeconds(timeMatch?.[1]) ?? previousGoal?.timeUsedSeconds;
|
|
359
|
+
return {
|
|
360
|
+
objective,
|
|
361
|
+
status: "active",
|
|
362
|
+
...(iterations !== undefined ? { iterations } : {}),
|
|
363
|
+
...(timeUsedSeconds !== undefined ? { timeUsedSeconds } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function readStepMediaBlocks(media) {
|
|
367
|
+
if (!Array.isArray(media) || media.length === 0)
|
|
368
|
+
return [];
|
|
369
|
+
const blocks = [];
|
|
370
|
+
for (const entry of media) {
|
|
371
|
+
const mimeType = entry.mime_type || entry.mimeType || "image/png";
|
|
372
|
+
const inline = entry.inline_data || entry.inlineData;
|
|
373
|
+
if (typeof inline === "string" && inline.length > 0) {
|
|
374
|
+
blocks.push({ type: "image", mimeType, data: inline });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const rawUri = entry.uri;
|
|
378
|
+
if (typeof rawUri === "string" && rawUri.length > 0) {
|
|
379
|
+
const filePath = rawUri.startsWith("file://") ? fileURLToPath(rawUri) : rawUri;
|
|
380
|
+
try {
|
|
381
|
+
if (existsSync(filePath)) {
|
|
382
|
+
const stat = statSync(filePath);
|
|
383
|
+
if (stat.isFile() && stat.size > 0 && stat.size <= 15 * 1024 * 1024) {
|
|
384
|
+
const data = readFileSync(filePath).toString("base64");
|
|
385
|
+
blocks.push({ type: "image", mimeType, data });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// ignore unreadable media file
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return blocks;
|
|
287
395
|
}
|
|
288
396
|
function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
289
397
|
const name = call.name ?? "tool";
|
|
@@ -328,6 +436,7 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
328
436
|
kind = "fetch";
|
|
329
437
|
rawInput = { url: typeof args.Url === "string" ? args.Url : summary };
|
|
330
438
|
}
|
|
439
|
+
const cleanedOutput = outputText ? stripTruncationMarkers(outputText) : undefined;
|
|
331
440
|
return {
|
|
332
441
|
sessionUpdate: "tool_call",
|
|
333
442
|
toolCallId,
|
|
@@ -336,13 +445,14 @@ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
|
|
|
336
445
|
kind,
|
|
337
446
|
status: "completed",
|
|
338
447
|
rawInput,
|
|
339
|
-
...(
|
|
448
|
+
...(cleanedOutput ? { rawOutput: { output: cleanedOutput } } : {}),
|
|
340
449
|
...(locations ? { locations } : {}),
|
|
341
450
|
};
|
|
342
451
|
}
|
|
343
452
|
/**
|
|
344
|
-
* Converts an Antigravity Desktop `
|
|
345
|
-
* `HistoryItem[]` list
|
|
453
|
+
* Converts an Antigravity Desktop `transcript_full.jsonl` (or `transcript.jsonl`)
|
|
454
|
+
* file into LinkShell's `HistoryItem[]` list, tracking Goal Mode (`ls_goal`),
|
|
455
|
+
* model changes (`<USER_SETTINGS_CHANGE>`), and uploaded media.
|
|
346
456
|
*/
|
|
347
457
|
export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
348
458
|
if (!existsSync(transcriptPath))
|
|
@@ -354,8 +464,50 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
354
464
|
catch {
|
|
355
465
|
return { items: [], nextLine: fromLine };
|
|
356
466
|
}
|
|
467
|
+
let activeGoal = null;
|
|
468
|
+
let lastModelLabel;
|
|
469
|
+
// Scan lines prior to `fromLine` so incremental parsing retains active goal & model state.
|
|
470
|
+
const scanLimit = Math.min(fromLine, lines.length);
|
|
471
|
+
for (let i = 0; i < scanLimit; i++) {
|
|
472
|
+
try {
|
|
473
|
+
const step = JSON.parse(lines[i]);
|
|
474
|
+
if (step.type === "USER_INPUT" && typeof step.content === "string") {
|
|
475
|
+
const modelLabel = extractModelSelectionFromContent(step.content);
|
|
476
|
+
if (modelLabel)
|
|
477
|
+
lastModelLabel = modelLabel;
|
|
478
|
+
const rawText = cleanAntigravityUserInput(step.content);
|
|
479
|
+
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(rawText)) {
|
|
480
|
+
activeGoal = null;
|
|
481
|
+
}
|
|
482
|
+
else if (/^\/goal(?:\s+|$)/i.test(rawText) || /\/goal is a \[Slash Command\]:/i.test(step.content)) {
|
|
483
|
+
const objective = rawText.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
484
|
+
activeGoal = { objective, status: "active" };
|
|
485
|
+
}
|
|
486
|
+
else if (activeGoal?.status === "complete") {
|
|
487
|
+
activeGoal = null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (step.type === "SYSTEM_MESSAGE") {
|
|
491
|
+
const nextGoal = parseGoalSystemMessage(step.content, activeGoal);
|
|
492
|
+
if (nextGoal)
|
|
493
|
+
activeGoal = nextGoal;
|
|
494
|
+
}
|
|
495
|
+
else if (step.type === "PLANNER_RESPONSE" && typeof step.content === "string") {
|
|
496
|
+
const { goalComplete, goalCancelled } = cleanAntigravityAgentContent(step.content);
|
|
497
|
+
if (goalCancelled) {
|
|
498
|
+
activeGoal = null;
|
|
499
|
+
}
|
|
500
|
+
else if (goalComplete) {
|
|
501
|
+
activeGoal = { ...(activeGoal ?? { objective: "持续目标" }), status: "complete" };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// ignore malformed historical line
|
|
507
|
+
}
|
|
508
|
+
}
|
|
357
509
|
if (fromLine >= lines.length)
|
|
358
|
-
return { items: [], nextLine: lines.length };
|
|
510
|
+
return { items: [], nextLine: lines.length, lastModelLabel, goal: activeGoal };
|
|
359
511
|
const parsed = [];
|
|
360
512
|
for (let i = fromLine; i < lines.length; i++) {
|
|
361
513
|
try {
|
|
@@ -364,41 +516,88 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
364
516
|
}
|
|
365
517
|
catch {
|
|
366
518
|
// skip partial trailing JSON line
|
|
367
|
-
return { items: [], nextLine: i };
|
|
519
|
+
return { items: [], nextLine: i, lastModelLabel, goal: activeGoal };
|
|
368
520
|
}
|
|
369
521
|
}
|
|
370
522
|
const items = [];
|
|
371
523
|
for (let i = 0; i < parsed.length; i++) {
|
|
372
524
|
const { index: stepIndex, step } = parsed[i];
|
|
373
525
|
if (step.type === "USER_INPUT") {
|
|
374
|
-
|
|
375
|
-
|
|
526
|
+
if (typeof step.content === "string") {
|
|
527
|
+
const modelLabel = extractModelSelectionFromContent(step.content);
|
|
528
|
+
if (modelLabel)
|
|
529
|
+
lastModelLabel = modelLabel;
|
|
530
|
+
}
|
|
531
|
+
const rawText = typeof step.content === "string" ? cleanAntigravityUserInput(step.content) : "";
|
|
532
|
+
const mediaBlocks = readStepMediaBlocks(step.media);
|
|
533
|
+
let displayText = rawText;
|
|
534
|
+
let goalUpdate;
|
|
535
|
+
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(rawText)) {
|
|
536
|
+
activeGoal = null;
|
|
537
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: null };
|
|
538
|
+
}
|
|
539
|
+
else if (/^\/goal(?:\s+|$)/i.test(rawText) || (typeof step.content === "string" && /\/goal is a \[Slash Command\]:/i.test(step.content))) {
|
|
540
|
+
const objective = rawText.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
541
|
+
displayText = objective;
|
|
542
|
+
activeGoal = { objective, status: "active" };
|
|
543
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: activeGoal };
|
|
544
|
+
}
|
|
545
|
+
else if (activeGoal?.status === "complete") {
|
|
546
|
+
activeGoal = null;
|
|
547
|
+
goalUpdate = { sessionUpdate: "ls_goal", goal: null };
|
|
548
|
+
}
|
|
549
|
+
if (!displayText && mediaBlocks.length === 0 && !goalUpdate)
|
|
376
550
|
continue;
|
|
377
551
|
const messageId = `agy:user:${stepIndex}`;
|
|
552
|
+
const updates = [];
|
|
553
|
+
for (const mediaBlock of mediaBlocks) {
|
|
554
|
+
updates.push({
|
|
555
|
+
sessionUpdate: "user_message_chunk",
|
|
556
|
+
messageId,
|
|
557
|
+
content: mediaBlock,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
if (displayText) {
|
|
561
|
+
updates.push({
|
|
562
|
+
sessionUpdate: "user_message_chunk",
|
|
563
|
+
messageId,
|
|
564
|
+
content: { type: "text", text: displayText },
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
if (goalUpdate) {
|
|
568
|
+
updates.push(goalUpdate);
|
|
569
|
+
}
|
|
378
570
|
items.push({
|
|
379
571
|
itemId: messageId,
|
|
380
|
-
updates
|
|
381
|
-
{
|
|
382
|
-
sessionUpdate: "user_message_chunk",
|
|
383
|
-
messageId,
|
|
384
|
-
content: { type: "text", text },
|
|
385
|
-
},
|
|
386
|
-
],
|
|
572
|
+
updates,
|
|
387
573
|
});
|
|
388
574
|
}
|
|
575
|
+
else if (step.type === "SYSTEM_MESSAGE") {
|
|
576
|
+
const nextGoal = parseGoalSystemMessage(step.content, activeGoal);
|
|
577
|
+
if (nextGoal) {
|
|
578
|
+
activeGoal = nextGoal;
|
|
579
|
+
items.push({
|
|
580
|
+
itemId: `agy:goal:${stepIndex}`,
|
|
581
|
+
updates: [{ sessionUpdate: "ls_goal", goal: activeGoal }],
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
389
585
|
else if (step.type === "PLANNER_RESPONSE") {
|
|
390
586
|
const updates = [];
|
|
391
|
-
if (typeof step.thinking === "string"
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
587
|
+
if (typeof step.thinking === "string") {
|
|
588
|
+
const cleanThought = stripTruncationMarkers(step.thinking).trim();
|
|
589
|
+
if (cleanThought) {
|
|
590
|
+
const thoughtId = `agy:thought:${stepIndex}`;
|
|
591
|
+
updates.push({
|
|
592
|
+
sessionUpdate: "agent_thought_chunk",
|
|
593
|
+
messageId: thoughtId,
|
|
594
|
+
content: { type: "text", text: cleanThought },
|
|
595
|
+
}, {
|
|
596
|
+
sessionUpdate: "ls_message_done",
|
|
597
|
+
messageId: thoughtId,
|
|
598
|
+
role: "thought",
|
|
599
|
+
});
|
|
600
|
+
}
|
|
402
601
|
}
|
|
403
602
|
const calls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
|
|
404
603
|
if (calls.length > 0) {
|
|
@@ -414,16 +613,27 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
414
613
|
});
|
|
415
614
|
}
|
|
416
615
|
if (typeof step.content === "string" && step.content.trim()) {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
616
|
+
const { text: cleanText, goalComplete, goalCancelled } = cleanAntigravityAgentContent(step.content);
|
|
617
|
+
if (cleanText) {
|
|
618
|
+
const messageId = `agy:msg:${stepIndex}`;
|
|
619
|
+
updates.push({
|
|
620
|
+
sessionUpdate: "agent_message_chunk",
|
|
621
|
+
messageId,
|
|
622
|
+
content: { type: "text", text: cleanText },
|
|
623
|
+
}, {
|
|
624
|
+
sessionUpdate: "ls_message_done",
|
|
625
|
+
messageId,
|
|
626
|
+
role: "agent",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (goalCancelled) {
|
|
630
|
+
activeGoal = null;
|
|
631
|
+
updates.push({ sessionUpdate: "ls_goal", goal: null });
|
|
632
|
+
}
|
|
633
|
+
else if (goalComplete) {
|
|
634
|
+
activeGoal = { ...(activeGoal ?? { objective: "持续目标" }), status: "complete" };
|
|
635
|
+
updates.push({ sessionUpdate: "ls_goal", goal: activeGoal });
|
|
636
|
+
}
|
|
427
637
|
}
|
|
428
638
|
if (updates.length > 0) {
|
|
429
639
|
items.push({
|
|
@@ -433,7 +643,7 @@ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
|
|
|
433
643
|
}
|
|
434
644
|
}
|
|
435
645
|
}
|
|
436
|
-
return { items, nextLine: lines.length };
|
|
646
|
+
return { items, nextLine: lines.length, lastModelLabel, goal: activeGoal };
|
|
437
647
|
}
|
|
438
648
|
async function findRunningLanguageServer(desktop) {
|
|
439
649
|
if (!desktop?.logPath || !existsSync(desktop.logPath))
|
|
@@ -500,18 +710,38 @@ function parseWorkspaceCwd(workspaceUrisJson, fallbackCwd) {
|
|
|
500
710
|
}
|
|
501
711
|
return fallbackCwd;
|
|
502
712
|
}
|
|
713
|
+
function resolveDesktopTranscriptPath(dataDir, nativeId) {
|
|
714
|
+
const logsDir = join(dataDir, "brain", nativeId, ".system_generated", "logs");
|
|
715
|
+
const fullPath = join(logsDir, "transcript_full.jsonl");
|
|
716
|
+
if (existsSync(fullPath))
|
|
717
|
+
return fullPath;
|
|
718
|
+
return join(logsDir, "transcript.jsonl");
|
|
719
|
+
}
|
|
720
|
+
function extForMimeType(mimeType) {
|
|
721
|
+
const sub = mimeType.split("/")[1]?.toLowerCase() ?? "png";
|
|
722
|
+
if (sub === "jpeg" || sub === "jpg")
|
|
723
|
+
return "jpg";
|
|
724
|
+
if (sub === "webp")
|
|
725
|
+
return "webp";
|
|
726
|
+
if (sub === "gif")
|
|
727
|
+
return "gif";
|
|
728
|
+
return "png";
|
|
729
|
+
}
|
|
503
730
|
/**
|
|
504
731
|
* Specialized driver for Google Antigravity:
|
|
505
732
|
* - Drives `agy_acp_server.par` / `agy-acp` over ACP when installed.
|
|
506
733
|
* - Also discovers and interacts with Antigravity Desktop (`/Applications/Antigravity.app`,
|
|
507
|
-
* `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/
|
|
734
|
+
* `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/transcript_full.jsonl`,
|
|
508
735
|
* and its local `language_server` Connect-RPC service) so users with only the Desktop app
|
|
509
|
-
* get full session visibility, history,
|
|
736
|
+
* get full session visibility, history, Goal Mode, image messaging, and model selection.
|
|
510
737
|
*/
|
|
511
738
|
export class AntigravityDriver extends AcpDriver {
|
|
512
739
|
desktopMode = false;
|
|
740
|
+
desktopSummaryTimer;
|
|
741
|
+
desktopSessionStates = new Map();
|
|
513
742
|
desktopWatchers = new Map();
|
|
514
743
|
desktopModelsCache;
|
|
744
|
+
desktopDefaultModel;
|
|
515
745
|
constructor(options) {
|
|
516
746
|
super(antigravitySpec(), options);
|
|
517
747
|
}
|
|
@@ -568,11 +798,38 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
568
798
|
models: true,
|
|
569
799
|
};
|
|
570
800
|
this.current = { installed: true, version: desktop.version };
|
|
801
|
+
if (!this.desktopSummaryTimer) {
|
|
802
|
+
this.desktopSummaryTimer = setInterval(() => {
|
|
803
|
+
for (const s of this.listDesktopSessions(30)) {
|
|
804
|
+
const nextState = s.state === "running" ? "running" : "idle";
|
|
805
|
+
const prev = this.desktopSessionStates.get(s.nativeId);
|
|
806
|
+
this.desktopSessionStates.set(s.nativeId, nextState);
|
|
807
|
+
if (prev !== undefined && prev !== nextState) {
|
|
808
|
+
if (nextState === "running") {
|
|
809
|
+
this.host?.update(this.id, s.nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
this.host?.update(this.id, s.nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
813
|
+
}
|
|
814
|
+
this.host?.update(this.id, s.nativeId, {
|
|
815
|
+
sessionUpdate: "ls_status",
|
|
816
|
+
state: nextState,
|
|
817
|
+
turnActive: nextState === "running",
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}, 3000);
|
|
822
|
+
this.desktopSummaryTimer.unref?.();
|
|
823
|
+
}
|
|
571
824
|
return;
|
|
572
825
|
}
|
|
573
826
|
await super.boot();
|
|
574
827
|
}
|
|
575
828
|
async stop() {
|
|
829
|
+
if (this.desktopSummaryTimer) {
|
|
830
|
+
clearInterval(this.desktopSummaryTimer);
|
|
831
|
+
this.desktopSummaryTimer = undefined;
|
|
832
|
+
}
|
|
576
833
|
for (const watcher of this.desktopWatchers.values())
|
|
577
834
|
clearInterval(watcher.timer);
|
|
578
835
|
this.desktopWatchers.clear();
|
|
@@ -582,16 +839,60 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
582
839
|
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
583
840
|
if (!desktop)
|
|
584
841
|
return undefined;
|
|
585
|
-
return
|
|
842
|
+
return resolveDesktopTranscriptPath(desktop.dataDir, nativeId);
|
|
843
|
+
}
|
|
844
|
+
desktopAliasesFor(nativeId) {
|
|
845
|
+
try {
|
|
846
|
+
const raw = this.host?.state(this.id, nativeId).get("agy-desktop-aliases");
|
|
847
|
+
if (!raw)
|
|
848
|
+
return new Map();
|
|
849
|
+
return new Map(JSON.parse(raw));
|
|
850
|
+
}
|
|
851
|
+
catch {
|
|
852
|
+
return new Map();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
saveDesktopAliases(nativeId, aliases) {
|
|
856
|
+
try {
|
|
857
|
+
this.host?.state(this.id, nativeId).set("agy-desktop-aliases", JSON.stringify([...aliases]));
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
// ignore state write errors
|
|
861
|
+
}
|
|
586
862
|
}
|
|
587
863
|
isConversationRunningInDesktop(nativeId, statusCol) {
|
|
588
864
|
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
589
865
|
if (!desktop)
|
|
590
866
|
return false;
|
|
591
867
|
const lockPath = join(desktop.dataDir, "presence", `${nativeId}.lock`);
|
|
592
|
-
if (
|
|
868
|
+
if (!existsSync(lockPath))
|
|
593
869
|
return false;
|
|
594
|
-
|
|
870
|
+
let status = statusCol;
|
|
871
|
+
if (status === undefined) {
|
|
872
|
+
const dbPath = join(desktop.dataDir, "conversation_summaries.db");
|
|
873
|
+
if (existsSync(dbPath)) {
|
|
874
|
+
let db;
|
|
875
|
+
try {
|
|
876
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
877
|
+
const row = db
|
|
878
|
+
.prepare("SELECT status FROM conversation_summaries WHERE conversation_id = ?")
|
|
879
|
+
.get(nativeId);
|
|
880
|
+
status = row?.status;
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
// ignore
|
|
884
|
+
}
|
|
885
|
+
finally {
|
|
886
|
+
try {
|
|
887
|
+
db?.close();
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
// ignore
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return status === "CASCADE_RUN_STATUS_RUNNING";
|
|
595
896
|
}
|
|
596
897
|
listDesktopSessions(limit) {
|
|
597
898
|
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
@@ -617,23 +918,30 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
617
918
|
const cwd = parseWorkspaceCwd(row.workspace_uris, home);
|
|
618
919
|
const updatedAt = Date.parse(row.last_modified_time) || Date.now();
|
|
619
920
|
let title = row.title?.trim() || undefined;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const
|
|
624
|
-
|
|
921
|
+
let model;
|
|
922
|
+
const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, row.conversation_id);
|
|
923
|
+
if (!title && existsSync(transcriptPath)) {
|
|
924
|
+
const parsed = parseAntigravityTranscript(transcriptPath, 0);
|
|
925
|
+
model = parsed.lastModelLabel;
|
|
926
|
+
const firstUser = parsed.items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
927
|
+
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk" && u.content.type === "text");
|
|
625
928
|
if (chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text") {
|
|
626
929
|
title = chunk.content.text.split("\n")[0]?.slice(0, 100).trim() || undefined;
|
|
627
930
|
}
|
|
628
931
|
}
|
|
629
932
|
const running = this.isConversationRunningInDesktop(row.conversation_id, row.status);
|
|
933
|
+
const state = running ? "running" : "idle";
|
|
934
|
+
if (!this.desktopSessionStates.has(row.conversation_id)) {
|
|
935
|
+
this.desktopSessionStates.set(row.conversation_id, state);
|
|
936
|
+
}
|
|
630
937
|
results.push({
|
|
631
938
|
nativeId: row.conversation_id,
|
|
632
939
|
cwd,
|
|
633
940
|
title,
|
|
941
|
+
...(model ? { model } : {}),
|
|
634
942
|
createdAt: updatedAt,
|
|
635
943
|
updatedAt,
|
|
636
|
-
state
|
|
944
|
+
state,
|
|
637
945
|
});
|
|
638
946
|
}
|
|
639
947
|
}
|
|
@@ -653,13 +961,13 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
653
961
|
try {
|
|
654
962
|
const dirs = readdirSync(join(desktop.dataDir, "brain"));
|
|
655
963
|
for (const id of dirs) {
|
|
656
|
-
const transcriptPath =
|
|
964
|
+
const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, id);
|
|
657
965
|
if (!existsSync(transcriptPath))
|
|
658
966
|
continue;
|
|
659
967
|
const stat = statSync(transcriptPath);
|
|
660
|
-
const
|
|
661
|
-
const firstUser = items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
662
|
-
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk");
|
|
968
|
+
const parsed = parseAntigravityTranscript(transcriptPath, 0);
|
|
969
|
+
const firstUser = parsed.items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
|
|
970
|
+
const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk" && u.content.type === "text");
|
|
663
971
|
const title = chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text"
|
|
664
972
|
? chunk.content.text.split("\n")[0]?.slice(0, 100).trim()
|
|
665
973
|
: undefined;
|
|
@@ -667,6 +975,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
667
975
|
nativeId: id,
|
|
668
976
|
cwd: home,
|
|
669
977
|
title,
|
|
978
|
+
...(parsed.lastModelLabel ? { model: parsed.lastModelLabel } : {}),
|
|
670
979
|
createdAt: stat.mtimeMs,
|
|
671
980
|
updatedAt: stat.mtimeMs,
|
|
672
981
|
state: "idle",
|
|
@@ -700,6 +1009,16 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
700
1009
|
return [];
|
|
701
1010
|
try {
|
|
702
1011
|
const data = await callLanguageServer(conn, "GetCascadeModelConfigData", {});
|
|
1012
|
+
const defaultOverride = data.defaultOverrideModelConfig?.modelOrAlias?.model ??
|
|
1013
|
+
data.defaultOverrideModelConfig?.modelOrAlias?.alias;
|
|
1014
|
+
if (defaultOverride)
|
|
1015
|
+
this.desktopDefaultModel = defaultOverride;
|
|
1016
|
+
const sortOrder = new Map();
|
|
1017
|
+
const sortedLabels = data.clientModelSorts?.[0]?.groups?.flatMap((g) => g.modelLabels ?? []) ?? [];
|
|
1018
|
+
sortedLabels.forEach((label, idx) => {
|
|
1019
|
+
if (!sortOrder.has(label))
|
|
1020
|
+
sortOrder.set(label, idx);
|
|
1021
|
+
});
|
|
703
1022
|
const models = (data.clientModelConfigs ?? [])
|
|
704
1023
|
.map((entry) => {
|
|
705
1024
|
const value = entry.modelOrAlias?.model ?? entry.modelOrAlias?.alias;
|
|
@@ -713,6 +1032,13 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
713
1032
|
};
|
|
714
1033
|
})
|
|
715
1034
|
.filter((entry) => Boolean(entry));
|
|
1035
|
+
if (sortOrder.size > 0) {
|
|
1036
|
+
models.sort((a, b) => {
|
|
1037
|
+
const rankA = sortOrder.get(a.name) ?? 9999;
|
|
1038
|
+
const rankB = sortOrder.get(b.name) ?? 9999;
|
|
1039
|
+
return rankA - rankB;
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
716
1042
|
if (models.length > 0)
|
|
717
1043
|
this.desktopModelsCache = models;
|
|
718
1044
|
return models;
|
|
@@ -721,82 +1047,181 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
721
1047
|
return [];
|
|
722
1048
|
}
|
|
723
1049
|
}
|
|
1050
|
+
async resolveSessionModel(nativeId, models, explicitModel) {
|
|
1051
|
+
const matchByValueOrName = (candidate) => {
|
|
1052
|
+
if (!candidate)
|
|
1053
|
+
return undefined;
|
|
1054
|
+
return models.find((m) => m.value === candidate || m.name === candidate);
|
|
1055
|
+
};
|
|
1056
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1057
|
+
const persistedSelected = this.host?.state(this.id, nativeId).get("agy-selected-model");
|
|
1058
|
+
const chosen = matchByValueOrName(explicitModel) ??
|
|
1059
|
+
matchByValueOrName(watcher?.selectedModel) ??
|
|
1060
|
+
matchByValueOrName(persistedSelected) ??
|
|
1061
|
+
matchByValueOrName(watcher?.lastModelLabel);
|
|
1062
|
+
if (chosen)
|
|
1063
|
+
return chosen;
|
|
1064
|
+
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
1065
|
+
const conn = await findRunningLanguageServer(desktop);
|
|
1066
|
+
if (conn) {
|
|
1067
|
+
try {
|
|
1068
|
+
const meta = await callLanguageServer(conn, "GetCascadeTrajectoryGeneratorMetadata", { cascadeId: nativeId });
|
|
1069
|
+
const entries = meta.generatorMetadata ?? [];
|
|
1070
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1071
|
+
const entry = entries[i];
|
|
1072
|
+
const candidate = entry.plannerConfig?.requestedModel?.model ??
|
|
1073
|
+
entry.plannerConfig?.requestedModel?.alias ??
|
|
1074
|
+
entry.plannerConfig?.planModel ??
|
|
1075
|
+
entry.chatModel?.model;
|
|
1076
|
+
const found = matchByValueOrName(candidate);
|
|
1077
|
+
if (found)
|
|
1078
|
+
return found;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
// ignore metadata RPC failure
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const fromDefault = matchByValueOrName(this.desktopDefaultModel);
|
|
1086
|
+
if (fromDefault)
|
|
1087
|
+
return fromDefault;
|
|
1088
|
+
if (desktop && existsSync(join(desktop.dataDir, "antigravity_state.pbtxt"))) {
|
|
1089
|
+
try {
|
|
1090
|
+
const stateText = readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8");
|
|
1091
|
+
const match = /last_selected_agent_model:\s*(\S+)/.exec(stateText);
|
|
1092
|
+
const found = matchByValueOrName(match?.[1]);
|
|
1093
|
+
if (found)
|
|
1094
|
+
return found;
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
// ignore
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return models[0];
|
|
1101
|
+
}
|
|
724
1102
|
async emitDesktopConfig(nativeId, selectedModel) {
|
|
1103
|
+
this.host?.update(this.id, nativeId, {
|
|
1104
|
+
sessionUpdate: "available_commands_update",
|
|
1105
|
+
availableCommands: ANTIGRAVITY_DESKTOP_COMMANDS,
|
|
1106
|
+
});
|
|
725
1107
|
const models = await this.loadDesktopModels();
|
|
726
1108
|
if (models.length === 0)
|
|
727
1109
|
return;
|
|
728
|
-
|
|
729
|
-
if (!defaultModel) {
|
|
730
|
-
const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
|
|
731
|
-
if (desktop && existsSync(join(desktop.dataDir, "antigravity_state.pbtxt"))) {
|
|
732
|
-
try {
|
|
733
|
-
const stateText = readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8");
|
|
734
|
-
const match = /last_selected_agent_model:\s*(\S+)/.exec(stateText);
|
|
735
|
-
if (match?.[1] && models.some((m) => m.value === match[1]))
|
|
736
|
-
defaultModel = match[1];
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
// ignore
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const current = defaultModel && models.some((m) => m.value === defaultModel) ? defaultModel : models[0].value;
|
|
1110
|
+
const matched = await this.resolveSessionModel(nativeId, models, selectedModel);
|
|
744
1111
|
const options = [
|
|
745
1112
|
{
|
|
746
1113
|
id: "model",
|
|
747
1114
|
name: "Model",
|
|
748
1115
|
category: "model",
|
|
749
|
-
current,
|
|
1116
|
+
current: matched.value,
|
|
750
1117
|
values: models,
|
|
751
1118
|
},
|
|
752
1119
|
];
|
|
753
1120
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_config", options });
|
|
1121
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "session_info_update", model: matched.name });
|
|
754
1122
|
}
|
|
755
|
-
startWatchingDesktopTranscript(nativeId,
|
|
1123
|
+
startWatchingDesktopTranscript(nativeId, initialLine, initialMeta) {
|
|
756
1124
|
const existing = this.desktopWatchers.get(nativeId);
|
|
757
1125
|
if (existing)
|
|
758
1126
|
clearInterval(existing.timer);
|
|
759
|
-
const
|
|
1127
|
+
const activePath = this.transcriptPathFor(nativeId);
|
|
1128
|
+
const lastSize = activePath && existsSync(activePath) ? statSync(activePath).size : 0;
|
|
1129
|
+
const initialRunning = this.isConversationRunningInDesktop(nativeId);
|
|
1130
|
+
this.desktopSessionStates.set(nativeId, initialRunning ? "running" : "idle");
|
|
760
1131
|
const watcher = {
|
|
761
1132
|
nextLine: initialLine,
|
|
762
1133
|
lastSize,
|
|
763
|
-
running:
|
|
764
|
-
|
|
1134
|
+
running: initialRunning,
|
|
1135
|
+
detached: false,
|
|
1136
|
+
selectedModel: existing?.selectedModel ?? this.host?.state(this.id, nativeId).get("agy-selected-model") ?? undefined,
|
|
1137
|
+
lastModelLabel: initialMeta?.lastModelLabel ?? existing?.lastModelLabel,
|
|
1138
|
+
goal: initialMeta?.goal !== undefined ? initialMeta.goal : existing?.goal,
|
|
1139
|
+
pendingClientMessages: existing?.pendingClientMessages ?? [],
|
|
765
1140
|
timer: setInterval(() => {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1141
|
+
const currentPath = this.transcriptPathFor(nativeId);
|
|
1142
|
+
if (currentPath && existsSync(currentPath)) {
|
|
1143
|
+
try {
|
|
1144
|
+
const stat = statSync(currentPath);
|
|
1145
|
+
if (stat.size !== watcher.lastSize) {
|
|
1146
|
+
watcher.lastSize = stat.size;
|
|
1147
|
+
const parsed = parseAntigravityTranscript(currentPath, watcher.nextLine);
|
|
1148
|
+
watcher.nextLine = parsed.nextLine;
|
|
1149
|
+
if (parsed.lastModelLabel && parsed.lastModelLabel !== watcher.lastModelLabel) {
|
|
1150
|
+
watcher.lastModelLabel = parsed.lastModelLabel;
|
|
1151
|
+
void this.emitDesktopConfig(nativeId);
|
|
1152
|
+
}
|
|
1153
|
+
if (parsed.goal !== undefined) {
|
|
1154
|
+
watcher.goal = parsed.goal;
|
|
1155
|
+
}
|
|
1156
|
+
for (const item of parsed.items) {
|
|
1157
|
+
if (item.itemId.startsWith("agy:user:") && watcher.pendingClientMessages.length > 0) {
|
|
1158
|
+
const pending = watcher.pendingClientMessages.shift();
|
|
1159
|
+
const aliases = this.desktopAliasesFor(nativeId);
|
|
1160
|
+
aliases.set(item.itemId, `local-${pending.clientMessageId}`);
|
|
1161
|
+
this.saveDesktopAliases(nativeId, aliases);
|
|
1162
|
+
// Emit only non-user_message_chunk updates (such as ls_goal) since the user message was already echoed with local-${clientMessageId}.
|
|
1163
|
+
const nonUserUpdates = item.updates.filter((u) => u.sessionUpdate !== "user_message_chunk");
|
|
1164
|
+
for (const update of nonUserUpdates) {
|
|
1165
|
+
this.host?.update(this.id, nativeId, update);
|
|
1166
|
+
}
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
item.updates.forEach((update, idx) => {
|
|
1170
|
+
this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
catch {
|
|
1176
|
+
// ignore stat/read errors
|
|
1177
|
+
}
|
|
774
1178
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1179
|
+
const nowRunning = this.isConversationRunningInDesktop(nativeId);
|
|
1180
|
+
if (nowRunning !== watcher.running) {
|
|
1181
|
+
watcher.running = nowRunning;
|
|
1182
|
+
this.desktopSessionStates.set(nativeId, nowRunning ? "running" : "idle");
|
|
1183
|
+
if (nowRunning) {
|
|
1184
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
1185
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
1189
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
1190
|
+
if (watcher.detached) {
|
|
1191
|
+
clearInterval(watcher.timer);
|
|
1192
|
+
this.desktopWatchers.delete(nativeId);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
784
1195
|
}
|
|
785
1196
|
}, 1500),
|
|
786
1197
|
};
|
|
787
1198
|
watcher.timer.unref?.();
|
|
788
1199
|
this.desktopWatchers.set(nativeId, watcher);
|
|
1200
|
+
return watcher;
|
|
789
1201
|
}
|
|
790
1202
|
async attach(nativeId, context) {
|
|
791
1203
|
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
792
1204
|
if (this.desktopMode || (transcriptPath && existsSync(transcriptPath))) {
|
|
793
1205
|
await this.ensureStarted();
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
1206
|
+
const parsed = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : { items: [], nextLine: 0 };
|
|
1207
|
+
const aliases = this.desktopAliasesFor(nativeId);
|
|
1208
|
+
const items = parsed.items.map((item) => {
|
|
1209
|
+
const aliasedId = aliases.get(item.itemId);
|
|
1210
|
+
return aliasedId ? { ...item, itemId: aliasedId } : item;
|
|
1211
|
+
});
|
|
1212
|
+
this.startWatchingDesktopTranscript(nativeId, parsed.nextLine, {
|
|
1213
|
+
lastModelLabel: parsed.lastModelLabel,
|
|
1214
|
+
goal: parsed.goal,
|
|
1215
|
+
});
|
|
1216
|
+
const running = this.isConversationRunningInDesktop(nativeId);
|
|
1217
|
+
this.desktopSessionStates.set(nativeId, running ? "running" : "idle");
|
|
1218
|
+
if (!running) {
|
|
1219
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
|
|
797
1220
|
}
|
|
798
|
-
const running = this.isConversationRunningInDesktop(nativeId, "CASCADE_RUN_STATUS_RUNNING");
|
|
799
1221
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: running ? "running" : "idle", turnActive: running });
|
|
1222
|
+
if (parsed.goal !== undefined) {
|
|
1223
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: parsed.goal });
|
|
1224
|
+
}
|
|
800
1225
|
void this.emitDesktopConfig(nativeId, this.desktopWatchers.get(nativeId)?.selectedModel);
|
|
801
1226
|
return items;
|
|
802
1227
|
}
|
|
@@ -805,8 +1230,14 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
805
1230
|
async detach(nativeId) {
|
|
806
1231
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
807
1232
|
if (watcher) {
|
|
808
|
-
|
|
809
|
-
|
|
1233
|
+
if (watcher.running) {
|
|
1234
|
+
// Keep watching until the active turn completes so turn end and final messages are recorded.
|
|
1235
|
+
watcher.detached = true;
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
clearInterval(watcher.timer);
|
|
1239
|
+
this.desktopWatchers.delete(nativeId);
|
|
1240
|
+
}
|
|
810
1241
|
}
|
|
811
1242
|
await super.detach(nativeId);
|
|
812
1243
|
}
|
|
@@ -827,18 +1258,38 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
827
1258
|
const nativeId = response.cascadeId;
|
|
828
1259
|
if (!nativeId)
|
|
829
1260
|
throw RpcError.app("agent_unavailable", "Antigravity 未返回新会话 ID");
|
|
830
|
-
const
|
|
831
|
-
if (transcriptPath)
|
|
832
|
-
this.startWatchingDesktopTranscript(nativeId, transcriptPath, 0);
|
|
1261
|
+
const watcher = this.startWatchingDesktopTranscript(nativeId, 0);
|
|
833
1262
|
if (options.model) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
watcher.selectedModel = options.model;
|
|
1263
|
+
watcher.selectedModel = options.model;
|
|
1264
|
+
this.host?.state(this.id, nativeId).set("agy-selected-model", options.model);
|
|
837
1265
|
}
|
|
838
1266
|
void this.emitDesktopConfig(nativeId, options.model);
|
|
839
1267
|
const now = Date.now();
|
|
840
1268
|
return { nativeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
|
|
841
1269
|
}
|
|
1270
|
+
async goal(nativeId, change) {
|
|
1271
|
+
if (!this.desktopMode && !this.desktopWatchers.has(nativeId)) {
|
|
1272
|
+
throw RpcError.app("not_supported", "这个 Agent 通过 /goal 命令管理目标");
|
|
1273
|
+
}
|
|
1274
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1275
|
+
if (change.action === "get") {
|
|
1276
|
+
return watcher?.goal ?? null;
|
|
1277
|
+
}
|
|
1278
|
+
if (change.action === "clear") {
|
|
1279
|
+
if (watcher)
|
|
1280
|
+
watcher.goal = null;
|
|
1281
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
if (change.action === "set") {
|
|
1285
|
+
const nextGoal = { objective: change.objective, status: "active" };
|
|
1286
|
+
if (watcher)
|
|
1287
|
+
watcher.goal = nextGoal;
|
|
1288
|
+
await this.prompt(nativeId, [{ type: "text", text: `/goal ${change.objective}` }], `goal-${Date.now()}`);
|
|
1289
|
+
return nextGoal;
|
|
1290
|
+
}
|
|
1291
|
+
return watcher?.goal ?? null;
|
|
1292
|
+
}
|
|
842
1293
|
async prompt(nativeId, content, clientMessageId, context) {
|
|
843
1294
|
const transcriptPath = this.transcriptPathFor(nativeId);
|
|
844
1295
|
if (!this.desktopMode && !(transcriptPath && existsSync(transcriptPath))) {
|
|
@@ -853,21 +1304,96 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
853
1304
|
.map((block) => (block.type === "text" ? block.text : ""))
|
|
854
1305
|
.join("\n")
|
|
855
1306
|
.trim();
|
|
856
|
-
|
|
1307
|
+
const imageBlocks = content.filter((block) => block.type === "image");
|
|
1308
|
+
if (!text && imageBlocks.length === 0)
|
|
857
1309
|
throw RpcError.app("invalid_params", "nothing to send");
|
|
858
|
-
|
|
859
|
-
|
|
1310
|
+
if (/^\/goal\s+(?:clear|stop|off|reset|none|cancel)\s*$/i.test(text) && imageBlocks.length === 0) {
|
|
1311
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1312
|
+
if (watcher)
|
|
1313
|
+
watcher.goal = null;
|
|
1314
|
+
this.host?.update(this.id, nativeId, {
|
|
1315
|
+
sessionUpdate: "user_message_chunk",
|
|
1316
|
+
messageId: `local-${clientMessageId}`,
|
|
1317
|
+
content: { type: "text", text },
|
|
1318
|
+
});
|
|
1319
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_message_done", messageId: `local-${clientMessageId}`, role: "user" }, `local-${clientMessageId}`);
|
|
1320
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1321
|
+
return "started";
|
|
1322
|
+
}
|
|
1323
|
+
let watcher = this.desktopWatchers.get(nativeId);
|
|
1324
|
+
if (!watcher) {
|
|
1325
|
+
const parsed = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : undefined;
|
|
1326
|
+
watcher = this.startWatchingDesktopTranscript(nativeId, parsed?.nextLine ?? 0, parsed);
|
|
1327
|
+
}
|
|
1328
|
+
// Echo user message with `local-${clientMessageId}` so the mobile client's optimistic bubble is confirmed in-place.
|
|
1329
|
+
watcher.pendingClientMessages.push({ clientMessageId, text });
|
|
1330
|
+
const displayBlocks = content.map((block) => {
|
|
1331
|
+
if (block.type === "text" && /^\/goal(?:\s+|$)/i.test(block.text.trim())) {
|
|
1332
|
+
const cleaned = block.text.trim().replace(/^\/goal(?:\s+|$)/i, "").trim() || block.text;
|
|
1333
|
+
return { type: "text", text: cleaned };
|
|
1334
|
+
}
|
|
1335
|
+
return block;
|
|
1336
|
+
});
|
|
1337
|
+
for (const block of displayBlocks) {
|
|
1338
|
+
this.host?.update(this.id, nativeId, {
|
|
1339
|
+
sessionUpdate: "user_message_chunk",
|
|
1340
|
+
messageId: `local-${clientMessageId}`,
|
|
1341
|
+
content: block,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_message_done", messageId: `local-${clientMessageId}`, role: "user" }, `local-${clientMessageId}`);
|
|
1345
|
+
if (/^\/goal(?:\s+|$)/i.test(text)) {
|
|
1346
|
+
const objective = text.replace(/^\/goal(?:\s+|$)/i, "").trim() || "持续目标";
|
|
1347
|
+
const nextGoal = { objective, status: "active" };
|
|
1348
|
+
watcher.goal = nextGoal;
|
|
1349
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: nextGoal });
|
|
1350
|
+
}
|
|
1351
|
+
else if (watcher.goal?.status === "complete") {
|
|
1352
|
+
watcher.goal = null;
|
|
1353
|
+
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_goal", goal: null });
|
|
1354
|
+
}
|
|
1355
|
+
const media = [];
|
|
1356
|
+
if (imageBlocks.length > 0 && desktop) {
|
|
1357
|
+
const uploadDir = join(desktop.dataDir, "brain", nativeId, ".user_uploaded");
|
|
1358
|
+
try {
|
|
1359
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// ignore mkdir error
|
|
1363
|
+
}
|
|
1364
|
+
for (const img of imageBlocks) {
|
|
1365
|
+
if (!img.data)
|
|
1366
|
+
continue;
|
|
1367
|
+
const ext = extForMimeType(img.mimeType);
|
|
1368
|
+
const fileName = `media_${Date.now()}_${randomBytes(4).toString("hex")}.${ext}`;
|
|
1369
|
+
const filePath = join(uploadDir, fileName);
|
|
1370
|
+
let savedUri;
|
|
1371
|
+
try {
|
|
1372
|
+
writeFileSync(filePath, Buffer.from(img.data, "base64"));
|
|
1373
|
+
savedUri = filePath;
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
// fallback to inlineData only
|
|
1377
|
+
}
|
|
1378
|
+
media.push({
|
|
1379
|
+
mimeType: img.mimeType,
|
|
1380
|
+
inlineData: img.data,
|
|
1381
|
+
...(savedUri ? { uri: savedUri } : {}),
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const fullText = context ? (text ? `${context}\n\n${text}` : context) : text;
|
|
1386
|
+
const selectedModel = watcher.selectedModel;
|
|
1387
|
+
watcher.running = true;
|
|
1388
|
+
this.desktopSessionStates.set(nativeId, "running");
|
|
860
1389
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
|
|
861
1390
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
|
|
862
1391
|
await callLanguageServer(conn, "SendUserCascadeMessage", {
|
|
863
1392
|
cascadeId: nativeId,
|
|
864
|
-
items: [{ text: fullText }],
|
|
1393
|
+
items: [{ text: fullText || "请查看附图" }],
|
|
1394
|
+
...(media.length > 0 ? { media } : {}),
|
|
865
1395
|
...(selectedModel ? { cascadeConfig: { plannerConfig: { requestedModel: { model: selectedModel } } } } : {}),
|
|
866
1396
|
});
|
|
867
|
-
if (transcriptPath && !this.desktopWatchers.has(nativeId)) {
|
|
868
|
-
const { nextLine } = parseAntigravityTranscript(transcriptPath, 0);
|
|
869
|
-
this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
|
|
870
|
-
}
|
|
871
1397
|
return "started";
|
|
872
1398
|
}
|
|
873
1399
|
async cancel(nativeId) {
|
|
@@ -880,6 +1406,10 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
880
1406
|
killBackgroundTasks: false,
|
|
881
1407
|
}).catch(() => { });
|
|
882
1408
|
}
|
|
1409
|
+
const watcher = this.desktopWatchers.get(nativeId);
|
|
1410
|
+
if (watcher)
|
|
1411
|
+
watcher.running = false;
|
|
1412
|
+
this.desktopSessionStates.set(nativeId, "idle");
|
|
883
1413
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "cancelled" });
|
|
884
1414
|
this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
|
|
885
1415
|
return;
|
|
@@ -893,6 +1423,7 @@ export class AntigravityDriver extends AcpDriver {
|
|
|
893
1423
|
const watcher = this.desktopWatchers.get(nativeId);
|
|
894
1424
|
if (watcher)
|
|
895
1425
|
watcher.selectedModel = value;
|
|
1426
|
+
this.host?.state(this.id, nativeId).set("agy-selected-model", value);
|
|
896
1427
|
await this.emitDesktopConfig(nativeId, value);
|
|
897
1428
|
return;
|
|
898
1429
|
}
|