@linkshell/host 0.4.32 → 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.
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
- import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
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 requestMatch = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/i.exec(content);
282
- if (requestMatch)
283
- return requestMatch[1].trim();
284
- return content
285
- .replace(/<(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY)>[\s\S]*?<\/(?:ADDITIONAL_METADATA|USER_SETTINGS_CHANGE|SYSTEM_MESSAGE|CONTEXT_SUMMARY)>/gi, "")
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
- ...(outputText ? { rawOutput: { output: outputText } } : {}),
448
+ ...(cleanedOutput ? { rawOutput: { output: cleanedOutput } } : {}),
340
449
  ...(locations ? { locations } : {}),
341
450
  };
342
451
  }
343
452
  /**
344
- * Converts an Antigravity Desktop `transcript.jsonl` file into LinkShell's
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
- const text = typeof step.content === "string" ? cleanAntigravityUserInput(step.content) : "";
375
- if (!text)
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" && step.thinking.trim()) {
392
- const thoughtId = `agy:thought:${stepIndex}`;
393
- updates.push({
394
- sessionUpdate: "agent_thought_chunk",
395
- messageId: thoughtId,
396
- content: { type: "text", text: step.thinking.trim() },
397
- }, {
398
- sessionUpdate: "ls_message_done",
399
- messageId: thoughtId,
400
- role: "thought",
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 messageId = `agy:msg:${stepIndex}`;
418
- updates.push({
419
- sessionUpdate: "agent_message_chunk",
420
- messageId,
421
- content: { type: "text", text: step.content.trim() },
422
- }, {
423
- sessionUpdate: "ls_message_done",
424
- messageId,
425
- role: "agent",
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,13 +710,30 @@ 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/transcript.jsonl`,
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, model selection, and messaging.
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;
@@ -514,6 +741,7 @@ export class AntigravityDriver extends AcpDriver {
514
741
  desktopSessionStates = new Map();
515
742
  desktopWatchers = new Map();
516
743
  desktopModelsCache;
744
+ desktopDefaultModel;
517
745
  constructor(options) {
518
746
  super(antigravitySpec(), options);
519
747
  }
@@ -577,6 +805,12 @@ export class AntigravityDriver extends AcpDriver {
577
805
  const prev = this.desktopSessionStates.get(s.nativeId);
578
806
  this.desktopSessionStates.set(s.nativeId, nextState);
579
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
+ }
580
814
  this.host?.update(this.id, s.nativeId, {
581
815
  sessionUpdate: "ls_status",
582
816
  state: nextState,
@@ -605,7 +839,26 @@ export class AntigravityDriver extends AcpDriver {
605
839
  const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
606
840
  if (!desktop)
607
841
  return undefined;
608
- return join(desktop.dataDir, "brain", nativeId, ".system_generated", "logs", "transcript.jsonl");
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
+ }
609
862
  }
610
863
  isConversationRunningInDesktop(nativeId, statusCol) {
611
864
  const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
@@ -665,11 +918,13 @@ export class AntigravityDriver extends AcpDriver {
665
918
  const cwd = parseWorkspaceCwd(row.workspace_uris, home);
666
919
  const updatedAt = Date.parse(row.last_modified_time) || Date.now();
667
920
  let title = row.title?.trim() || undefined;
668
- if (!title) {
669
- const transcriptPath = join(desktop.dataDir, "brain", row.conversation_id, ".system_generated", "logs", "transcript.jsonl");
670
- const { items } = parseAntigravityTranscript(transcriptPath, 0);
671
- const firstUser = items.find((entry) => entry.updates.some((u) => u.sessionUpdate === "user_message_chunk"));
672
- const chunk = firstUser?.updates.find((u) => u.sessionUpdate === "user_message_chunk");
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");
673
928
  if (chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text") {
674
929
  title = chunk.content.text.split("\n")[0]?.slice(0, 100).trim() || undefined;
675
930
  }
@@ -683,6 +938,7 @@ export class AntigravityDriver extends AcpDriver {
683
938
  nativeId: row.conversation_id,
684
939
  cwd,
685
940
  title,
941
+ ...(model ? { model } : {}),
686
942
  createdAt: updatedAt,
687
943
  updatedAt,
688
944
  state,
@@ -705,13 +961,13 @@ export class AntigravityDriver extends AcpDriver {
705
961
  try {
706
962
  const dirs = readdirSync(join(desktop.dataDir, "brain"));
707
963
  for (const id of dirs) {
708
- const transcriptPath = join(desktop.dataDir, "brain", id, ".system_generated", "logs", "transcript.jsonl");
964
+ const transcriptPath = resolveDesktopTranscriptPath(desktop.dataDir, id);
709
965
  if (!existsSync(transcriptPath))
710
966
  continue;
711
967
  const stat = statSync(transcriptPath);
712
- const { items } = parseAntigravityTranscript(transcriptPath, 0);
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");
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");
715
971
  const title = chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text"
716
972
  ? chunk.content.text.split("\n")[0]?.slice(0, 100).trim()
717
973
  : undefined;
@@ -719,6 +975,7 @@ export class AntigravityDriver extends AcpDriver {
719
975
  nativeId: id,
720
976
  cwd: home,
721
977
  title,
978
+ ...(parsed.lastModelLabel ? { model: parsed.lastModelLabel } : {}),
722
979
  createdAt: stat.mtimeMs,
723
980
  updatedAt: stat.mtimeMs,
724
981
  state: "idle",
@@ -752,6 +1009,16 @@ export class AntigravityDriver extends AcpDriver {
752
1009
  return [];
753
1010
  try {
754
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
+ });
755
1022
  const models = (data.clientModelConfigs ?? [])
756
1023
  .map((entry) => {
757
1024
  const value = entry.modelOrAlias?.model ?? entry.modelOrAlias?.alias;
@@ -765,6 +1032,13 @@ export class AntigravityDriver extends AcpDriver {
765
1032
  };
766
1033
  })
767
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
+ }
768
1042
  if (models.length > 0)
769
1043
  this.desktopModelsCache = models;
770
1044
  return models;
@@ -773,56 +1047,125 @@ export class AntigravityDriver extends AcpDriver {
773
1047
  return [];
774
1048
  }
775
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
+ }
776
1102
  async emitDesktopConfig(nativeId, selectedModel) {
1103
+ this.host?.update(this.id, nativeId, {
1104
+ sessionUpdate: "available_commands_update",
1105
+ availableCommands: ANTIGRAVITY_DESKTOP_COMMANDS,
1106
+ });
777
1107
  const models = await this.loadDesktopModels();
778
1108
  if (models.length === 0)
779
1109
  return;
780
- let defaultModel = selectedModel;
781
- if (!defaultModel) {
782
- const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
783
- if (desktop && existsSync(join(desktop.dataDir, "antigravity_state.pbtxt"))) {
784
- try {
785
- const stateText = readFileSync(join(desktop.dataDir, "antigravity_state.pbtxt"), "utf8");
786
- const match = /last_selected_agent_model:\s*(\S+)/.exec(stateText);
787
- if (match?.[1] && models.some((m) => m.value === match[1]))
788
- defaultModel = match[1];
789
- }
790
- catch {
791
- // ignore
792
- }
793
- }
794
- }
795
- const current = defaultModel && models.some((m) => m.value === defaultModel) ? defaultModel : models[0].value;
1110
+ const matched = await this.resolveSessionModel(nativeId, models, selectedModel);
796
1111
  const options = [
797
1112
  {
798
1113
  id: "model",
799
1114
  name: "Model",
800
1115
  category: "model",
801
- current,
1116
+ current: matched.value,
802
1117
  values: models,
803
1118
  },
804
1119
  ];
805
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 });
806
1122
  }
807
- startWatchingDesktopTranscript(nativeId, transcriptPath, initialLine) {
1123
+ startWatchingDesktopTranscript(nativeId, initialLine, initialMeta) {
808
1124
  const existing = this.desktopWatchers.get(nativeId);
809
1125
  if (existing)
810
1126
  clearInterval(existing.timer);
811
- const lastSize = existsSync(transcriptPath) ? statSync(transcriptPath).size : 0;
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");
812
1131
  const watcher = {
813
1132
  nextLine: initialLine,
814
1133
  lastSize,
815
- running: this.isConversationRunningInDesktop(nativeId),
816
- selectedModel: existing?.selectedModel,
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 ?? [],
817
1140
  timer: setInterval(() => {
818
- if (existsSync(transcriptPath)) {
1141
+ const currentPath = this.transcriptPathFor(nativeId);
1142
+ if (currentPath && existsSync(currentPath)) {
819
1143
  try {
820
- const stat = statSync(transcriptPath);
821
- if (stat.size > watcher.lastSize) {
1144
+ const stat = statSync(currentPath);
1145
+ if (stat.size !== watcher.lastSize) {
822
1146
  watcher.lastSize = stat.size;
823
- const { items, nextLine } = parseAntigravityTranscript(transcriptPath, watcher.nextLine);
824
- watcher.nextLine = nextLine;
825
- for (const item of items) {
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
+ }
826
1169
  item.updates.forEach((update, idx) => {
827
1170
  this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
828
1171
  });
@@ -836,6 +1179,7 @@ export class AntigravityDriver extends AcpDriver {
836
1179
  const nowRunning = this.isConversationRunningInDesktop(nativeId);
837
1180
  if (nowRunning !== watcher.running) {
838
1181
  watcher.running = nowRunning;
1182
+ this.desktopSessionStates.set(nativeId, nowRunning ? "running" : "idle");
839
1183
  if (nowRunning) {
840
1184
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
841
1185
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
@@ -843,23 +1187,41 @@ export class AntigravityDriver extends AcpDriver {
843
1187
  else {
844
1188
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
845
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
+ }
846
1194
  }
847
1195
  }
848
1196
  }, 1500),
849
1197
  };
850
1198
  watcher.timer.unref?.();
851
1199
  this.desktopWatchers.set(nativeId, watcher);
1200
+ return watcher;
852
1201
  }
853
1202
  async attach(nativeId, context) {
854
1203
  const transcriptPath = this.transcriptPathFor(nativeId);
855
1204
  if (this.desktopMode || (transcriptPath && existsSync(transcriptPath))) {
856
1205
  await this.ensureStarted();
857
- const { items, nextLine } = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : { items: [], nextLine: 0 };
858
- if (transcriptPath) {
859
- this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
860
- }
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
+ });
861
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" });
1220
+ }
862
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
+ }
863
1225
  void this.emitDesktopConfig(nativeId, this.desktopWatchers.get(nativeId)?.selectedModel);
864
1226
  return items;
865
1227
  }
@@ -868,8 +1230,14 @@ export class AntigravityDriver extends AcpDriver {
868
1230
  async detach(nativeId) {
869
1231
  const watcher = this.desktopWatchers.get(nativeId);
870
1232
  if (watcher) {
871
- clearInterval(watcher.timer);
872
- this.desktopWatchers.delete(nativeId);
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
+ }
873
1241
  }
874
1242
  await super.detach(nativeId);
875
1243
  }
@@ -890,18 +1258,38 @@ export class AntigravityDriver extends AcpDriver {
890
1258
  const nativeId = response.cascadeId;
891
1259
  if (!nativeId)
892
1260
  throw RpcError.app("agent_unavailable", "Antigravity 未返回新会话 ID");
893
- const transcriptPath = this.transcriptPathFor(nativeId);
894
- if (transcriptPath)
895
- this.startWatchingDesktopTranscript(nativeId, transcriptPath, 0);
1261
+ const watcher = this.startWatchingDesktopTranscript(nativeId, 0);
896
1262
  if (options.model) {
897
- const watcher = this.desktopWatchers.get(nativeId);
898
- if (watcher)
899
- watcher.selectedModel = options.model;
1263
+ watcher.selectedModel = options.model;
1264
+ this.host?.state(this.id, nativeId).set("agy-selected-model", options.model);
900
1265
  }
901
1266
  void this.emitDesktopConfig(nativeId, options.model);
902
1267
  const now = Date.now();
903
1268
  return { nativeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
904
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
+ }
905
1293
  async prompt(nativeId, content, clientMessageId, context) {
906
1294
  const transcriptPath = this.transcriptPathFor(nativeId);
907
1295
  if (!this.desktopMode && !(transcriptPath && existsSync(transcriptPath))) {
@@ -916,21 +1304,96 @@ export class AntigravityDriver extends AcpDriver {
916
1304
  .map((block) => (block.type === "text" ? block.text : ""))
917
1305
  .join("\n")
918
1306
  .trim();
919
- if (!text)
1307
+ const imageBlocks = content.filter((block) => block.type === "image");
1308
+ if (!text && imageBlocks.length === 0)
920
1309
  throw RpcError.app("invalid_params", "nothing to send");
921
- const fullText = context ? `${context}\n\n${text}` : text;
922
- const selectedModel = this.desktopWatchers.get(nativeId)?.selectedModel;
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");
923
1389
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
924
1390
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
925
1391
  await callLanguageServer(conn, "SendUserCascadeMessage", {
926
1392
  cascadeId: nativeId,
927
- items: [{ text: fullText }],
1393
+ items: [{ text: fullText || "请查看附图" }],
1394
+ ...(media.length > 0 ? { media } : {}),
928
1395
  ...(selectedModel ? { cascadeConfig: { plannerConfig: { requestedModel: { model: selectedModel } } } } : {}),
929
1396
  });
930
- if (transcriptPath && !this.desktopWatchers.has(nativeId)) {
931
- const { nextLine } = parseAntigravityTranscript(transcriptPath, 0);
932
- this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
933
- }
934
1397
  return "started";
935
1398
  }
936
1399
  async cancel(nativeId) {
@@ -943,6 +1406,10 @@ export class AntigravityDriver extends AcpDriver {
943
1406
  killBackgroundTasks: false,
944
1407
  }).catch(() => { });
945
1408
  }
1409
+ const watcher = this.desktopWatchers.get(nativeId);
1410
+ if (watcher)
1411
+ watcher.running = false;
1412
+ this.desktopSessionStates.set(nativeId, "idle");
946
1413
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "cancelled" });
947
1414
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
948
1415
  return;
@@ -956,6 +1423,7 @@ export class AntigravityDriver extends AcpDriver {
956
1423
  const watcher = this.desktopWatchers.get(nativeId);
957
1424
  if (watcher)
958
1425
  watcher.selectedModel = value;
1426
+ this.host?.state(this.id, nativeId).set("agy-selected-model", value);
959
1427
  await this.emitDesktopConfig(nativeId, value);
960
1428
  return;
961
1429
  }