linkshell-cli 0.10.32 → 0.10.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.
@@ -1,7 +1,13 @@
1
- import { accessSync, constants, existsSync, mkdirSync, readdirSync } from "node:fs";
1
+ import { execFile } from "node:child_process";
2
+ import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
2
3
  import { homedir, tmpdir } from "node:os";
3
4
  import { basename, delimiter, dirname, isAbsolute, join } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import { RpcError, } from "@linkshell/wire";
4
9
  import { AcpDriver } from "./driver.js";
10
+ const execFileAsync = promisify(execFile);
5
11
  /**
6
12
  * Official Google Antigravity ACP distribution metadata (from the ACP Registry:
7
13
  * `antigravity-acp/agent.json`).
@@ -74,7 +80,6 @@ function findInManagedToolRoots(roots, binaryName) {
74
80
  for (const root of roots) {
75
81
  if (!existsSync(root))
76
82
  continue;
77
- // Check `<root>/<version>/<platform>-<arch>/<binaryName>` and `<root>/<binaryName>`
78
83
  const direct = join(root, binaryName);
79
84
  if (isExecutable(direct))
80
85
  return { path: direct, version: ANTIGRAVITY_ACP_VERSION };
@@ -162,6 +167,63 @@ export function resolveAntigravityLaunch(env = process.env) {
162
167
  version,
163
168
  };
164
169
  }
170
+ /**
171
+ * Resolves an installed Antigravity Desktop app (`/Applications/Antigravity.app`)
172
+ * and its conversation store (`~/.gemini/antigravity`).
173
+ */
174
+ export function resolveAntigravityDesktop(env = process.env) {
175
+ if (env.PATH === "" && !env.ANTIGRAVITY_APP_DATA_DIR)
176
+ return undefined;
177
+ const home = env.HOME || homedir();
178
+ const dataCandidates = [
179
+ env.ANTIGRAVITY_APP_DATA_DIR?.trim(),
180
+ join(home, ".gemini", "antigravity"),
181
+ join(home, ".gemini", "antigravity-ide"),
182
+ ].filter((entry) => Boolean(entry));
183
+ const dataDir = dataCandidates.find((dir) => existsSync(join(dir, "conversation_summaries.db")) || existsSync(join(dir, "brain")));
184
+ const lsCandidates = process.platform === "darwin"
185
+ ? [
186
+ "/Applications/Antigravity.app/Contents/Resources/bin/language_server",
187
+ join(home, "Applications", "Antigravity.app", "Contents", "Resources", "bin", "language_server"),
188
+ ]
189
+ : [];
190
+ const languageServerPath = lsCandidates.find((candidate) => isExecutable(candidate));
191
+ if (!dataDir && !languageServerPath)
192
+ return undefined;
193
+ const logCandidates = process.platform === "darwin"
194
+ ? [join(home, "Library", "Logs", "Antigravity", "language_server.log")]
195
+ : [join(home, ".config", "Antigravity", "logs", "language_server.log")];
196
+ const logPath = logCandidates.find((candidate) => existsSync(candidate));
197
+ let version = "desktop";
198
+ if (logPath) {
199
+ try {
200
+ const head = readFileSync(logPath, "utf8").slice(0, 2048);
201
+ const match = /Language server version:\s*([^\s\r\n]+)/i.exec(head);
202
+ if (match?.[1])
203
+ version = match[1];
204
+ }
205
+ catch {
206
+ // ignore unreadable log
207
+ }
208
+ }
209
+ if (version === "desktop" && process.platform === "darwin") {
210
+ try {
211
+ const plist = readFileSync("/Applications/Antigravity.app/Contents/Info.plist", "utf8");
212
+ const match = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(plist);
213
+ if (match?.[1])
214
+ version = match[1].trim();
215
+ }
216
+ catch {
217
+ // ignore unreadable plist
218
+ }
219
+ }
220
+ return {
221
+ dataDir: dataDir ?? join(home, ".gemini", "antigravity"),
222
+ languageServerPath,
223
+ logPath,
224
+ version,
225
+ };
226
+ }
165
227
  const AUTH_URL_PATTERN = /Open the following link to authenticate the ACP server:\s*(https?:\/\/\S+)/i;
166
228
  export function extractAntigravityAuthUrl(line) {
167
229
  const match = AUTH_URL_PATTERN.exec(line.trim());
@@ -187,12 +249,271 @@ export function antigravitySpec() {
187
249
  authStatus: antigravityAuthStatus,
188
250
  };
189
251
  }
252
+ function unquoteArg(value) {
253
+ if (typeof value !== "string")
254
+ return value;
255
+ const trimmed = value.trim();
256
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
257
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
258
+ (trimmed.startsWith("[") && trimmed.endsWith("]")) ||
259
+ trimmed === "true" ||
260
+ trimmed === "false" ||
261
+ /^-?\d+(?:\.\d+)?$/.test(trimmed)) {
262
+ try {
263
+ return JSON.parse(trimmed);
264
+ }
265
+ catch {
266
+ return value;
267
+ }
268
+ }
269
+ return value;
270
+ }
271
+ function parseToolArgs(raw) {
272
+ if (!raw || typeof raw !== "object")
273
+ return {};
274
+ const result = {};
275
+ for (const [k, v] of Object.entries(raw)) {
276
+ result[k] = unquoteArg(v);
277
+ }
278
+ return result;
279
+ }
280
+ 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, "")
286
+ .trim();
287
+ }
288
+ function mapAntigravityToolCall(stepIndex, callIndex, call, outputText) {
289
+ const name = call.name ?? "tool";
290
+ const args = parseToolArgs(call.args);
291
+ const summary = (typeof args.toolSummary === "string" && args.toolSummary.trim()) ||
292
+ (typeof args.toolAction === "string" && args.toolAction.trim()) ||
293
+ name;
294
+ const toolCallId = call.id || `agy:tc:${stepIndex}:${callIndex}`;
295
+ let kind = "other";
296
+ let rawInput = args;
297
+ let locations;
298
+ if (name === "run_command") {
299
+ kind = "execute";
300
+ rawInput = {
301
+ command: typeof args.CommandLine === "string" ? args.CommandLine : summary,
302
+ cwd: typeof args.Cwd === "string" ? args.Cwd : undefined,
303
+ };
304
+ }
305
+ else if (name === "view_file") {
306
+ kind = "read";
307
+ const path = typeof args.AbsolutePath === "string" ? args.AbsolutePath : undefined;
308
+ const line = typeof args.StartLine === "number" ? args.StartLine : undefined;
309
+ if (path) {
310
+ rawInput = { file_path: path };
311
+ locations = [{ path, ...(line ? { line } : {}) }];
312
+ }
313
+ }
314
+ else if (name === "replace_file_content" || name === "multi_replace_file_content" || name === "write_to_file") {
315
+ kind = "edit";
316
+ const path = typeof args.TargetFile === "string" ? args.TargetFile : undefined;
317
+ const line = typeof args.StartLine === "number" ? args.StartLine : undefined;
318
+ if (path) {
319
+ rawInput = { file_path: path };
320
+ locations = [{ path, ...(line ? { line } : {}) }];
321
+ }
322
+ }
323
+ else if (name === "search_web") {
324
+ kind = "search";
325
+ rawInput = { query: typeof args.query === "string" ? args.query : summary };
326
+ }
327
+ else if (name === "read_url_content") {
328
+ kind = "fetch";
329
+ rawInput = { url: typeof args.Url === "string" ? args.Url : summary };
330
+ }
331
+ return {
332
+ sessionUpdate: "tool_call",
333
+ toolCallId,
334
+ title: summary,
335
+ name,
336
+ kind,
337
+ status: "completed",
338
+ rawInput,
339
+ ...(outputText ? { rawOutput: { output: outputText } } : {}),
340
+ ...(locations ? { locations } : {}),
341
+ };
342
+ }
343
+ /**
344
+ * Converts an Antigravity Desktop `transcript.jsonl` file into LinkShell's
345
+ * `HistoryItem[]` list.
346
+ */
347
+ export function parseAntigravityTranscript(transcriptPath, fromLine = 0) {
348
+ if (!existsSync(transcriptPath))
349
+ return { items: [], nextLine: fromLine };
350
+ let lines;
351
+ try {
352
+ lines = readFileSync(transcriptPath, "utf8").split("\n").filter((line) => line.trim().length > 0);
353
+ }
354
+ catch {
355
+ return { items: [], nextLine: fromLine };
356
+ }
357
+ if (fromLine >= lines.length)
358
+ return { items: [], nextLine: lines.length };
359
+ const parsed = [];
360
+ for (let i = fromLine; i < lines.length; i++) {
361
+ try {
362
+ const step = JSON.parse(lines[i]);
363
+ parsed.push({ index: step.step_index ?? i, step });
364
+ }
365
+ catch {
366
+ // skip partial trailing JSON line
367
+ return { items: [], nextLine: i };
368
+ }
369
+ }
370
+ const items = [];
371
+ for (let i = 0; i < parsed.length; i++) {
372
+ const { index: stepIndex, step } = parsed[i];
373
+ if (step.type === "USER_INPUT") {
374
+ const text = typeof step.content === "string" ? cleanAntigravityUserInput(step.content) : "";
375
+ if (!text)
376
+ continue;
377
+ const messageId = `agy:user:${stepIndex}`;
378
+ items.push({
379
+ itemId: messageId,
380
+ updates: [
381
+ {
382
+ sessionUpdate: "user_message_chunk",
383
+ messageId,
384
+ content: { type: "text", text },
385
+ },
386
+ ],
387
+ });
388
+ }
389
+ else if (step.type === "PLANNER_RESPONSE") {
390
+ 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
+ });
402
+ }
403
+ const calls = Array.isArray(step.tool_calls) ? step.tool_calls : [];
404
+ if (calls.length > 0) {
405
+ // Subsequent GENERIC steps hold the outputs of these tool calls in order.
406
+ const outputs = [];
407
+ let lookahead = i + 1;
408
+ while (lookahead < parsed.length && parsed[lookahead].step.type === "GENERIC" && outputs.length < calls.length) {
409
+ outputs.push(typeof parsed[lookahead].step.content === "string" ? parsed[lookahead].step.content : "");
410
+ lookahead++;
411
+ }
412
+ calls.forEach((call, callIdx) => {
413
+ updates.push(mapAntigravityToolCall(stepIndex, callIdx, call, outputs[callIdx]));
414
+ });
415
+ }
416
+ 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
+ });
427
+ }
428
+ if (updates.length > 0) {
429
+ items.push({
430
+ itemId: `agy:step:${stepIndex}`,
431
+ updates,
432
+ });
433
+ }
434
+ }
435
+ }
436
+ return { items, nextLine: lines.length };
437
+ }
438
+ async function findRunningLanguageServer(desktop) {
439
+ if (!desktop?.logPath || !existsSync(desktop.logPath))
440
+ return undefined;
441
+ let httpPort;
442
+ try {
443
+ const head = readFileSync(desktop.logPath, "utf8").slice(0, 4096);
444
+ const match = /listening on \w+ port at (\d+) for HTTP\b/i.exec(head);
445
+ if (match?.[1])
446
+ httpPort = Number(match[1]);
447
+ }
448
+ catch {
449
+ return undefined;
450
+ }
451
+ if (!httpPort)
452
+ return undefined;
453
+ try {
454
+ const { stdout } = await execFileAsync("ps", ["-ax", "-o", "args="], { timeout: 3000 });
455
+ for (const line of stdout.split("\n")) {
456
+ if (!line.includes("language_server") || !line.includes("--csrf_token"))
457
+ continue;
458
+ const tokenMatch = /--csrf_token[=\s]+([^\s]+)/.exec(line);
459
+ if (tokenMatch?.[1]) {
460
+ return { httpPort, csrfToken: tokenMatch[1] };
461
+ }
462
+ }
463
+ }
464
+ catch {
465
+ // ignore ps errors
466
+ }
467
+ return undefined;
468
+ }
469
+ async function callLanguageServer(conn, method, body) {
470
+ const response = await fetch(`http://127.0.0.1:${conn.httpPort}/exa.language_server_pb.LanguageServerService/${method}`, {
471
+ method: "POST",
472
+ headers: {
473
+ "Content-Type": "application/json",
474
+ "x-codeium-csrf-token": conn.csrfToken,
475
+ },
476
+ body: JSON.stringify(body),
477
+ signal: AbortSignal.timeout(10_000),
478
+ });
479
+ if (!response.ok) {
480
+ const text = await response.text().catch(() => "");
481
+ throw new Error(`Antigravity Desktop RPC ${method} failed (${response.status}): ${text.slice(0, 200)}`);
482
+ }
483
+ return (await response.json());
484
+ }
485
+ function parseWorkspaceCwd(workspaceUrisJson, fallbackCwd) {
486
+ if (!workspaceUrisJson)
487
+ return fallbackCwd;
488
+ try {
489
+ const uris = JSON.parse(workspaceUrisJson);
490
+ if (Array.isArray(uris) && typeof uris[0] === "string") {
491
+ const first = uris[0];
492
+ if (first.startsWith("file://"))
493
+ return fileURLToPath(first);
494
+ if (isAbsolute(first))
495
+ return first;
496
+ }
497
+ }
498
+ catch {
499
+ // ignore invalid JSON
500
+ }
501
+ return fallbackCwd;
502
+ }
190
503
  /**
191
- * Specialized ACP driver for Google Antigravity (`agy_acp_server.par` / `agy-acp`).
192
- * Dynamically resolves the binary + `localharness_external`, sets up `.par` file-storage
193
- * environment variables, and surfaces stdout Google OAuth links as session/driver notices.
504
+ * Specialized driver for Google Antigravity:
505
+ * - Drives `agy_acp_server.par` / `agy-acp` over ACP when installed.
506
+ * - Also discovers and interacts with Antigravity Desktop (`/Applications/Antigravity.app`,
507
+ * `~/.gemini/antigravity/conversation_summaries.db`, `brain/<id>/.system_generated/logs/transcript.jsonl`,
508
+ * 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.
194
510
  */
195
511
  export class AntigravityDriver extends AcpDriver {
512
+ desktopMode = false;
513
+ desktopSummaryTimer;
514
+ desktopSessionStates = new Map();
515
+ desktopWatchers = new Map();
516
+ desktopModelsCache;
196
517
  constructor(options) {
197
518
  super(antigravitySpec(), options);
198
519
  }
@@ -204,21 +525,441 @@ export class AntigravityDriver extends AcpDriver {
204
525
  ...resolved?.env,
205
526
  };
206
527
  }
528
+ async authStatus() {
529
+ const base = await super.authStatus();
530
+ if (base.state !== "unknown")
531
+ return base;
532
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
533
+ if (desktop && (existsSync(join(desktop.dataDir, "antigravity_state.pbtxt")) || existsSync(join(desktop.dataDir, "conversation_summaries.db")))) {
534
+ return { state: "ok", method: "antigravity-desktop" };
535
+ }
536
+ return base;
537
+ }
207
538
  async detectVersion() {
208
- const resolved = resolveAntigravityLaunch(this.options.env ?? process.env);
209
- if (!resolved)
210
- return undefined;
211
- this.spec.command = resolved.command;
212
- this.spec.args = resolved.args;
213
- return resolved.version;
539
+ const env = this.options.env ?? process.env;
540
+ const resolved = resolveAntigravityLaunch(env);
541
+ if (resolved) {
542
+ this.desktopMode = false;
543
+ this.spec.command = resolved.command;
544
+ this.spec.args = resolved.args;
545
+ return resolved.version;
546
+ }
547
+ const desktop = resolveAntigravityDesktop(env);
548
+ if (desktop) {
549
+ this.desktopMode = true;
550
+ return desktop.version;
551
+ }
552
+ return undefined;
214
553
  }
215
554
  async boot() {
216
- const resolved = resolveAntigravityLaunch(this.options.env ?? process.env);
555
+ const env = this.options.env ?? process.env;
556
+ const resolved = resolveAntigravityLaunch(env);
217
557
  if (resolved) {
558
+ this.desktopMode = false;
218
559
  this.spec.command = resolved.command;
219
560
  this.spec.args = resolved.args;
561
+ await super.boot();
562
+ return;
563
+ }
564
+ const desktop = resolveAntigravityDesktop(env);
565
+ if (desktop) {
566
+ this.desktopMode = true;
567
+ this.capabilityState = {
568
+ ...this.capabilityState,
569
+ images: true,
570
+ models: true,
571
+ };
572
+ this.current = { installed: true, version: desktop.version };
573
+ if (!this.desktopSummaryTimer) {
574
+ this.desktopSummaryTimer = setInterval(() => {
575
+ for (const s of this.listDesktopSessions(30)) {
576
+ const nextState = s.state === "running" ? "running" : "idle";
577
+ const prev = this.desktopSessionStates.get(s.nativeId);
578
+ this.desktopSessionStates.set(s.nativeId, nextState);
579
+ if (prev !== undefined && prev !== nextState) {
580
+ this.host?.update(this.id, s.nativeId, {
581
+ sessionUpdate: "ls_status",
582
+ state: nextState,
583
+ turnActive: nextState === "running",
584
+ });
585
+ }
586
+ }
587
+ }, 3000);
588
+ this.desktopSummaryTimer.unref?.();
589
+ }
590
+ return;
220
591
  }
221
592
  await super.boot();
222
593
  }
594
+ async stop() {
595
+ if (this.desktopSummaryTimer) {
596
+ clearInterval(this.desktopSummaryTimer);
597
+ this.desktopSummaryTimer = undefined;
598
+ }
599
+ for (const watcher of this.desktopWatchers.values())
600
+ clearInterval(watcher.timer);
601
+ this.desktopWatchers.clear();
602
+ await super.stop();
603
+ }
604
+ transcriptPathFor(nativeId) {
605
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
606
+ if (!desktop)
607
+ return undefined;
608
+ return join(desktop.dataDir, "brain", nativeId, ".system_generated", "logs", "transcript.jsonl");
609
+ }
610
+ isConversationRunningInDesktop(nativeId, statusCol) {
611
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
612
+ if (!desktop)
613
+ return false;
614
+ const lockPath = join(desktop.dataDir, "presence", `${nativeId}.lock`);
615
+ if (!existsSync(lockPath))
616
+ return false;
617
+ let status = statusCol;
618
+ if (status === undefined) {
619
+ const dbPath = join(desktop.dataDir, "conversation_summaries.db");
620
+ if (existsSync(dbPath)) {
621
+ let db;
622
+ try {
623
+ db = new DatabaseSync(dbPath, { readOnly: true });
624
+ const row = db
625
+ .prepare("SELECT status FROM conversation_summaries WHERE conversation_id = ?")
626
+ .get(nativeId);
627
+ status = row?.status;
628
+ }
629
+ catch {
630
+ // ignore
631
+ }
632
+ finally {
633
+ try {
634
+ db?.close();
635
+ }
636
+ catch {
637
+ // ignore
638
+ }
639
+ }
640
+ }
641
+ }
642
+ return status === "CASCADE_RUN_STATUS_RUNNING";
643
+ }
644
+ listDesktopSessions(limit) {
645
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
646
+ if (!desktop)
647
+ return [];
648
+ const dbPath = join(desktop.dataDir, "conversation_summaries.db");
649
+ const home = (this.options.env ?? process.env).HOME || homedir();
650
+ const results = [];
651
+ if (existsSync(dbPath)) {
652
+ let db;
653
+ try {
654
+ db = new DatabaseSync(dbPath, { readOnly: true });
655
+ const rows = db
656
+ .prepare(`SELECT conversation_id, title, last_modified_time, workspace_uris, status, parent_conversation_id, killed
657
+ FROM conversation_summaries
658
+ WHERE (parent_conversation_id = '' OR parent_conversation_id IS NULL) AND killed = 0
659
+ ORDER BY last_modified_time DESC
660
+ LIMIT ?`)
661
+ .all(limit);
662
+ for (const row of rows) {
663
+ if (!row.conversation_id)
664
+ continue;
665
+ const cwd = parseWorkspaceCwd(row.workspace_uris, home);
666
+ const updatedAt = Date.parse(row.last_modified_time) || Date.now();
667
+ 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");
673
+ if (chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text") {
674
+ title = chunk.content.text.split("\n")[0]?.slice(0, 100).trim() || undefined;
675
+ }
676
+ }
677
+ const running = this.isConversationRunningInDesktop(row.conversation_id, row.status);
678
+ const state = running ? "running" : "idle";
679
+ if (!this.desktopSessionStates.has(row.conversation_id)) {
680
+ this.desktopSessionStates.set(row.conversation_id, state);
681
+ }
682
+ results.push({
683
+ nativeId: row.conversation_id,
684
+ cwd,
685
+ title,
686
+ createdAt: updatedAt,
687
+ updatedAt,
688
+ state,
689
+ });
690
+ }
691
+ }
692
+ catch {
693
+ // fallback to brain directory scan below if sqlite table is locked or unreadable
694
+ }
695
+ finally {
696
+ try {
697
+ db?.close();
698
+ }
699
+ catch {
700
+ // ignore close errors
701
+ }
702
+ }
703
+ }
704
+ if (results.length === 0 && existsSync(join(desktop.dataDir, "brain"))) {
705
+ try {
706
+ const dirs = readdirSync(join(desktop.dataDir, "brain"));
707
+ for (const id of dirs) {
708
+ const transcriptPath = join(desktop.dataDir, "brain", id, ".system_generated", "logs", "transcript.jsonl");
709
+ if (!existsSync(transcriptPath))
710
+ continue;
711
+ 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");
715
+ const title = chunk?.sessionUpdate === "user_message_chunk" && chunk.content.type === "text"
716
+ ? chunk.content.text.split("\n")[0]?.slice(0, 100).trim()
717
+ : undefined;
718
+ results.push({
719
+ nativeId: id,
720
+ cwd: home,
721
+ title,
722
+ createdAt: stat.mtimeMs,
723
+ updatedAt: stat.mtimeMs,
724
+ state: "idle",
725
+ });
726
+ if (results.length >= limit)
727
+ break;
728
+ }
729
+ }
730
+ catch {
731
+ // ignore unreadable brain dir
732
+ }
733
+ }
734
+ return results;
735
+ }
736
+ async listSessions(limit) {
737
+ const acpSessions = !this.desktopMode && this.connection?.alive ? await super.listSessions(limit).catch(() => []) : [];
738
+ const desktopSessions = this.listDesktopSessions(limit);
739
+ const merged = new Map();
740
+ for (const session of [...acpSessions, ...desktopSessions]) {
741
+ if (!merged.has(session.nativeId))
742
+ merged.set(session.nativeId, session);
743
+ }
744
+ return [...merged.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
745
+ }
746
+ async loadDesktopModels() {
747
+ if (this.desktopModelsCache?.length)
748
+ return this.desktopModelsCache;
749
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
750
+ const conn = await findRunningLanguageServer(desktop);
751
+ if (!conn)
752
+ return [];
753
+ try {
754
+ const data = await callLanguageServer(conn, "GetCascadeModelConfigData", {});
755
+ const models = (data.clientModelConfigs ?? [])
756
+ .map((entry) => {
757
+ const value = entry.modelOrAlias?.model ?? entry.modelOrAlias?.alias;
758
+ const name = entry.label?.trim();
759
+ if (!value || !name)
760
+ return undefined;
761
+ return {
762
+ value,
763
+ name,
764
+ ...(entry.tagTitle ? { description: entry.tagTitle } : {}),
765
+ };
766
+ })
767
+ .filter((entry) => Boolean(entry));
768
+ if (models.length > 0)
769
+ this.desktopModelsCache = models;
770
+ return models;
771
+ }
772
+ catch {
773
+ return [];
774
+ }
775
+ }
776
+ async emitDesktopConfig(nativeId, selectedModel) {
777
+ const models = await this.loadDesktopModels();
778
+ if (models.length === 0)
779
+ 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;
796
+ const options = [
797
+ {
798
+ id: "model",
799
+ name: "Model",
800
+ category: "model",
801
+ current,
802
+ values: models,
803
+ },
804
+ ];
805
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_config", options });
806
+ }
807
+ startWatchingDesktopTranscript(nativeId, transcriptPath, initialLine) {
808
+ const existing = this.desktopWatchers.get(nativeId);
809
+ if (existing)
810
+ clearInterval(existing.timer);
811
+ const lastSize = existsSync(transcriptPath) ? statSync(transcriptPath).size : 0;
812
+ const watcher = {
813
+ nextLine: initialLine,
814
+ lastSize,
815
+ running: this.isConversationRunningInDesktop(nativeId),
816
+ selectedModel: existing?.selectedModel,
817
+ timer: setInterval(() => {
818
+ if (existsSync(transcriptPath)) {
819
+ try {
820
+ const stat = statSync(transcriptPath);
821
+ if (stat.size > watcher.lastSize) {
822
+ watcher.lastSize = stat.size;
823
+ const { items, nextLine } = parseAntigravityTranscript(transcriptPath, watcher.nextLine);
824
+ watcher.nextLine = nextLine;
825
+ for (const item of items) {
826
+ item.updates.forEach((update, idx) => {
827
+ this.host?.update(this.id, nativeId, update, idx === item.updates.length - 1 ? item.itemId : undefined);
828
+ });
829
+ }
830
+ }
831
+ }
832
+ catch {
833
+ // ignore stat/read errors
834
+ }
835
+ }
836
+ const nowRunning = this.isConversationRunningInDesktop(nativeId);
837
+ if (nowRunning !== watcher.running) {
838
+ watcher.running = nowRunning;
839
+ if (nowRunning) {
840
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
841
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
842
+ }
843
+ else {
844
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "end_turn" });
845
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
846
+ }
847
+ }
848
+ }, 1500),
849
+ };
850
+ watcher.timer.unref?.();
851
+ this.desktopWatchers.set(nativeId, watcher);
852
+ }
853
+ async attach(nativeId, context) {
854
+ const transcriptPath = this.transcriptPathFor(nativeId);
855
+ if (this.desktopMode || (transcriptPath && existsSync(transcriptPath))) {
856
+ await this.ensureStarted();
857
+ const { items, nextLine } = transcriptPath ? parseAntigravityTranscript(transcriptPath, 0) : { items: [], nextLine: 0 };
858
+ if (transcriptPath) {
859
+ this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
860
+ }
861
+ const running = this.isConversationRunningInDesktop(nativeId);
862
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: running ? "running" : "idle", turnActive: running });
863
+ void this.emitDesktopConfig(nativeId, this.desktopWatchers.get(nativeId)?.selectedModel);
864
+ return items;
865
+ }
866
+ return super.attach(nativeId, context);
867
+ }
868
+ async detach(nativeId) {
869
+ const watcher = this.desktopWatchers.get(nativeId);
870
+ if (watcher) {
871
+ clearInterval(watcher.timer);
872
+ this.desktopWatchers.delete(nativeId);
873
+ }
874
+ await super.detach(nativeId);
875
+ }
876
+ async createSession(options) {
877
+ if (!this.desktopMode)
878
+ return super.createSession(options);
879
+ await this.ensureStarted();
880
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
881
+ const conn = await findRunningLanguageServer(desktop);
882
+ if (!conn) {
883
+ throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用");
884
+ }
885
+ const response = await callLanguageServer(conn, "StartCascade", {
886
+ workspaceUris: [pathToFileURL(options.cwd).href],
887
+ overrideWorkspaceUris: true,
888
+ ...(options.model ? { requestedModel: options.model } : {}),
889
+ });
890
+ const nativeId = response.cascadeId;
891
+ if (!nativeId)
892
+ throw RpcError.app("agent_unavailable", "Antigravity 未返回新会话 ID");
893
+ const transcriptPath = this.transcriptPathFor(nativeId);
894
+ if (transcriptPath)
895
+ this.startWatchingDesktopTranscript(nativeId, transcriptPath, 0);
896
+ if (options.model) {
897
+ const watcher = this.desktopWatchers.get(nativeId);
898
+ if (watcher)
899
+ watcher.selectedModel = options.model;
900
+ }
901
+ void this.emitDesktopConfig(nativeId, options.model);
902
+ const now = Date.now();
903
+ return { nativeId, cwd: options.cwd, createdAt: now, updatedAt: now, state: "idle" };
904
+ }
905
+ async prompt(nativeId, content, clientMessageId, context) {
906
+ const transcriptPath = this.transcriptPathFor(nativeId);
907
+ if (!this.desktopMode && !(transcriptPath && existsSync(transcriptPath))) {
908
+ return super.prompt(nativeId, content, clientMessageId, context);
909
+ }
910
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
911
+ const conn = await findRunningLanguageServer(desktop);
912
+ if (!conn) {
913
+ throw RpcError.app("agent_unavailable", "请先在电脑上打开 Antigravity 桌面端应用,再发送消息");
914
+ }
915
+ const text = content
916
+ .map((block) => (block.type === "text" ? block.text : ""))
917
+ .join("\n")
918
+ .trim();
919
+ if (!text)
920
+ 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;
923
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "started" });
924
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "running", turnActive: true });
925
+ await callLanguageServer(conn, "SendUserCascadeMessage", {
926
+ cascadeId: nativeId,
927
+ items: [{ text: fullText }],
928
+ ...(selectedModel ? { cascadeConfig: { plannerConfig: { requestedModel: { model: selectedModel } } } } : {}),
929
+ });
930
+ if (transcriptPath && !this.desktopWatchers.has(nativeId)) {
931
+ const { nextLine } = parseAntigravityTranscript(transcriptPath, 0);
932
+ this.startWatchingDesktopTranscript(nativeId, transcriptPath, nextLine);
933
+ }
934
+ return "started";
935
+ }
936
+ async cancel(nativeId) {
937
+ if (this.desktopMode || this.desktopWatchers.has(nativeId)) {
938
+ const desktop = resolveAntigravityDesktop(this.options.env ?? process.env);
939
+ const conn = await findRunningLanguageServer(desktop);
940
+ if (conn) {
941
+ await callLanguageServer(conn, "CancelCascadeInvocation", {
942
+ cascadeId: nativeId,
943
+ killBackgroundTasks: false,
944
+ }).catch(() => { });
945
+ }
946
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_turn", state: "ended", stopReason: "cancelled" });
947
+ this.host?.update(this.id, nativeId, { sessionUpdate: "ls_status", state: "idle", turnActive: false });
948
+ return;
949
+ }
950
+ await super.cancel(nativeId);
951
+ }
952
+ async setConfig(nativeId, optionId, value) {
953
+ if (this.desktopMode || this.desktopWatchers.has(nativeId)) {
954
+ if (optionId !== "model")
955
+ throw RpcError.app("not_supported", `Antigravity 暂不支持设置 ${optionId}`);
956
+ const watcher = this.desktopWatchers.get(nativeId);
957
+ if (watcher)
958
+ watcher.selectedModel = value;
959
+ await this.emitDesktopConfig(nativeId, value);
960
+ return;
961
+ }
962
+ await super.setConfig(nativeId, optionId, value);
963
+ }
223
964
  }
224
965
  //# sourceMappingURL=antigravity.js.map