feat(cli): add support for Antigravity (teleportation) sessions

This commit is contained in:
Sehoon Shon
2026-02-16 15:22:30 -05:00
parent 2009fbbd92
commit 71487c6fbe
8 changed files with 476 additions and 77 deletions
+155 -39
View File
@@ -5,15 +5,30 @@
{
"label": "Get started",
"items": [
{ "label": "Overview", "slug": "docs" },
{ "label": "Quickstart", "slug": "docs/get-started" },
{ "label": "Installation", "slug": "docs/get-started/installation" },
{
"label": "Overview",
"slug": "docs"
},
{
"label": "Quickstart",
"slug": "docs/get-started"
},
{
"label": "Installation",
"slug": "docs/get-started/installation"
},
{
"label": "Authentication",
"slug": "docs/get-started/authentication"
},
{ "label": "Examples", "slug": "docs/get-started/examples" },
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
{
"label": "Examples",
"slug": "docs/get-started/examples"
},
{
"label": "CLI cheatsheet",
"slug": "docs/cli/cli-reference"
},
{
"label": "Gemini 3 on Gemini CLI",
"slug": "docs/get-started/gemini-3"
@@ -60,12 +75,23 @@
"label": "Set up an MCP server",
"slug": "docs/cli/tutorials/mcp-setup"
},
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
{
"label": "Automate tasks",
"slug": "docs/cli/tutorials/automation"
}
]
},
{
"label": "Features",
"items": [
{
"label": "Agent Skills",
"slug": "docs/cli/skills"
},
{
"label": "Checkpointing",
"slug": "docs/cli/checkpointing"
},
{
"label": "Extensions",
"collapsed": true,
@@ -96,21 +122,40 @@
}
]
},
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{ "label": "Headless mode", "slug": "docs/cli/headless" },
{
"label": "Headless mode",
"slug": "docs/cli/headless"
},
{
"label": "Hooks",
"collapsed": true,
"items": [
{ "label": "Overview", "slug": "docs/hooks" },
{ "label": "Reference", "slug": "docs/hooks/reference" }
{
"label": "Overview",
"slug": "docs/hooks"
},
{
"label": "Reference",
"slug": "docs/hooks/reference"
}
]
},
{ "label": "IDE integration", "slug": "docs/ide-integration" },
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{
"label": "IDE integration",
"slug": "docs/ide-integration"
},
{
"label": "MCP servers",
"slug": "docs/tools/mcp-server"
},
{
"label": "Model routing",
"slug": "docs/cli/model-routing"
},
{
"label": "Model selection",
"slug": "docs/cli/model"
},
{
"label": "Model steering",
"badge": "🔬",
@@ -121,28 +166,54 @@
"badge": "🔬",
"slug": "docs/cli/notifications"
},
{ "label": "Plan mode", "slug": "docs/cli/plan-mode" },
{
"label": "Subagents",
"label": "Plan mode",
"badge": "🔬",
"slug": "docs/core/subagents"
"slug": "docs/cli/plan-mode"
},
{
"label": "Remote subagents",
"badge": "🔬",
"slug": "docs/core/remote-agents"
},
{ "label": "Rewind", "slug": "docs/cli/rewind" },
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
{ "label": "Settings", "slug": "docs/cli/settings" },
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
{ "label": "Token caching", "slug": "docs/cli/token-caching" }
{
"label": "Rewind",
"slug": "docs/cli/rewind"
},
{
"label": "Sandboxing",
"slug": "docs/cli/sandbox"
},
{
"label": "Settings",
"slug": "docs/cli/settings"
},
{
"label": "Subagents",
"badge": "🔬",
"slug": "docs/core/subagents"
},
{
"label": "Telemetry",
"slug": "docs/cli/telemetry"
},
{
"label": "Teleportation",
"slug": "docs/core/teleportation"
},
{
"label": "Token caching",
"slug": "docs/cli/token-caching"
}
]
},
{
"label": "Configuration",
"items": [
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
{
"label": "Custom commands",
"slug": "docs/cli/custom-commands"
},
{
"label": "Enterprise configuration",
"slug": "docs/cli/enterprise"
@@ -159,26 +230,47 @@
"label": "Project context (GEMINI.md)",
"slug": "docs/cli/gemini-md"
},
{ "label": "Settings", "slug": "docs/cli/settings" },
{
"label": "Settings",
"slug": "docs/cli/settings"
},
{
"label": "System prompt override",
"slug": "docs/cli/system-prompt"
},
{ "label": "Themes", "slug": "docs/cli/themes" },
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
{
"label": "Themes",
"slug": "docs/cli/themes"
},
{
"label": "Trusted folders",
"slug": "docs/cli/trusted-folders"
}
]
},
{
"label": "Development",
"items": [
{ "label": "Contribution guide", "slug": "docs/contributing" },
{ "label": "Integration testing", "slug": "docs/integration-tests" },
{
"label": "Contribution guide",
"slug": "docs/contributing"
},
{
"label": "Integration testing",
"slug": "docs/integration-tests"
},
{
"label": "Issue and PR automation",
"slug": "docs/issue-and-pr-automation"
},
{ "label": "Local development", "slug": "docs/local-development" },
{ "label": "NPM package structure", "slug": "docs/npm" }
{
"label": "Local development",
"slug": "docs/local-development"
},
{
"label": "NPM package structure",
"slug": "docs/npm"
}
]
}
]
@@ -189,7 +281,10 @@
{
"label": "Reference",
"items": [
{ "label": "Command reference", "slug": "docs/reference/commands" },
{
"label": "Command reference",
"slug": "docs/reference/commands"
},
{
"label": "Configuration reference",
"slug": "docs/reference/configuration"
@@ -202,8 +297,14 @@
"label": "Memory import processor",
"slug": "docs/reference/memport"
},
{ "label": "Policy engine", "slug": "docs/reference/policy-engine" },
{ "label": "Tools reference", "slug": "docs/reference/tools" }
{
"label": "Policy engine",
"slug": "docs/reference/policy-engine"
},
{
"label": "Tools reference",
"slug": "docs/reference/tools"
}
]
}
]
@@ -214,7 +315,10 @@
{
"label": "Resources",
"items": [
{ "label": "FAQ", "slug": "docs/resources/faq" },
{
"label": "FAQ",
"slug": "docs/resources/faq"
},
{
"label": "Quota and pricing",
"slug": "docs/resources/quota-and-pricing"
@@ -227,7 +331,10 @@
"label": "Troubleshooting",
"slug": "docs/resources/troubleshooting"
},
{ "label": "Uninstall", "slug": "docs/resources/uninstall" }
{
"label": "Uninstall",
"slug": "docs/resources/uninstall"
}
]
}
]
@@ -238,9 +345,18 @@
{
"label": "Releases",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
{
"label": "Release notes",
"slug": "docs/changelogs/"
},
{
"label": "Stable release",
"slug": "docs/changelogs/latest"
},
{
"label": "Preview release",
"slug": "docs/changelogs/preview"
}
]
}
]
+31 -3
View File
@@ -486,7 +486,8 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
"license": "(Apache-2.0 AND BSD-3-Clause)",
"peer": true
},
"node_modules/@bundled-es-modules/cookie": {
"version": "2.0.1",
@@ -1489,6 +1490,7 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -2195,6 +2197,7 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2375,6 +2378,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2424,6 +2428,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2798,6 +2803,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2831,6 +2837,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0"
@@ -2885,6 +2892,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0",
@@ -4121,6 +4129,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4395,6 +4404,7 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -5268,6 +5278,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7402,7 +7413,8 @@
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -7986,6 +7998,7 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8503,6 +8516,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -9815,6 +9829,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz",
"integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -10093,6 +10108,7 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -13850,6 +13866,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13860,6 +13877,7 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -16009,6 +16027,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16231,7 +16250,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -16239,6 +16259,7 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -16404,6 +16425,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16626,6 +16648,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -16739,6 +16762,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16751,6 +16775,7 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -17398,6 +17423,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -17841,6 +17867,7 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -17944,6 +17971,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+141 -9
View File
@@ -8,6 +8,7 @@ import * as fsPromises from 'node:fs/promises';
import React from 'react';
import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { Content, Part } from '@google/genai';
import type {
CommandContext,
SlashCommand,
@@ -18,6 +19,10 @@ import {
decodeTagName,
type MessageActionReturn,
INITIAL_HISTORY_LENGTH,
listAgySessions,
loadAgySession,
trajectoryToJson,
convertAgyToCliRecord,
} from '@google/gemini-cli-core';
import path from 'node:path';
import type {
@@ -64,6 +69,24 @@ const getSavedChatTags = async (
: a.mtime.localeCompare(b.mtime),
);
// Also look for Antigravity sessions
const agySessions = await listAgySessions();
for (const agy of agySessions) {
chatDetails.push({
name: `agy:${agy.id}`,
mtime: agy.mtime,
});
}
// Re-sort if we added AGY sessions
if (agySessions.length > 0) {
chatDetails.sort((a, b) =>
mtSortDesc
? b.mtime.localeCompare(a.mtime)
: a.mtime.localeCompare(b.mtime),
);
}
return chatDetails;
} catch (_err) {
return [];
@@ -174,8 +197,98 @@ const resumeCheckpointCommand: SlashCommand = {
const { logger, config } = context.services;
await logger.initialize();
let conversation: Content[] = [];
let authType: string | undefined;
const loadAgy = async (id: string): Promise<Content[] | null> => {
const data = await loadAgySession(id);
if (!data) return null;
const agyJson = trajectoryToJson(data);
const record = convertAgyToCliRecord(agyJson);
const conv: Content[] = [];
// Add a dummy system message so slice(INITIAL_HISTORY_LENGTH) works correctly
conv.push({ role: 'user', parts: [{ text: '' }] });
for (const m of record.messages) {
if (m.type === 'user') {
const parts = Array.isArray(m.content)
? m.content.map((c: string | Part) =>
typeof c === 'string' ? { text: c } : { text: c.text || '' },
)
: [{ text: '' }];
conv.push({ role: 'user', parts });
} else if (m.type === 'gemini') {
// 1. Model turn: Text + Function Calls
const modelParts: Part[] = [];
if (Array.isArray(m.content)) {
m.content.forEach((c: string | Part) => {
modelParts.push(
typeof c === 'string' ? { text: c } : { text: c.text || '' },
);
});
}
if (m.toolCalls) {
for (const tc of m.toolCalls) {
modelParts.push({
functionCall: {
name: tc.name,
args: tc.args,
},
});
}
}
conv.push({ role: 'model', parts: modelParts });
// 2. User turn: Function Responses (if any)
if (m.toolCalls && m.toolCalls.some((tc) => tc.result)) {
const responseParts: Part[] = [];
for (const tc of m.toolCalls) {
if (tc.result) {
responseParts.push({
functionResponse: {
name: tc.name,
response: { result: tc.result },
id: tc.id,
},
});
}
}
if (responseParts.length > 0) {
conv.push({ role: 'user', parts: responseParts });
}
}
}
}
return conv;
};
if (tag.startsWith('agy:')) {
const id = tag.slice(4);
const agyConv = await loadAgy(id);
if (!agyConv) {
return {
type: 'message',
messageType: 'error',
content: `No Antigravity session found with id: ${id}`,
};
}
conversation = agyConv;
} else {
const checkpoint = await logger.loadCheckpoint(tag);
const conversation = checkpoint.history;
if (checkpoint.history.length > 0) {
conversation = checkpoint.history;
authType = checkpoint.authType;
} else {
// Fallback: Try to load as AGY session even without prefix
const agyConv = await loadAgy(tag);
if (agyConv) {
conversation = agyConv;
}
}
}
if (conversation.length === 0) {
return {
@@ -186,15 +299,11 @@ const resumeCheckpointCommand: SlashCommand = {
}
const currentAuthType = config?.getContentGeneratorConfig()?.authType;
if (
checkpoint.authType &&
currentAuthType &&
checkpoint.authType !== currentAuthType
) {
if (authType && currentAuthType && authType !== currentAuthType) {
return {
type: 'message',
messageType: 'error',
content: `Cannot resume chat. It was saved with a different authentication method (${checkpoint.authType}) than the current one (${currentAuthType}).`,
content: `Cannot resume chat. It was saved with a different authentication method (${authType}) than the current one (${currentAuthType}).`,
};
}
@@ -206,11 +315,34 @@ const resumeCheckpointCommand: SlashCommand = {
const uiHistory: HistoryItemWithoutId[] = [];
for (const item of conversation.slice(INITIAL_HISTORY_LENGTH)) {
const text =
item.parts
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parts = item.parts as Array<{
text?: string;
functionCall?: { name: string };
functionResponse?: { name: string };
}>;
let text =
parts
?.filter((m) => !!m.text)
.map((m) => m.text)
.join('') || '';
const toolCalls = parts?.filter((p) => !!p.functionCall);
if (toolCalls?.length) {
const calls = toolCalls
.map((tc) => `[Tool Call: ${tc.functionCall?.name}]`)
.join('\n');
text = text ? `${text}\n${calls}` : calls;
}
const toolResponses = parts?.filter((p) => !!p.functionResponse);
if (toolResponses?.length) {
const responses = toolResponses
.map((tr) => `[Tool Result: ${tr.functionResponse?.name}]`)
.join('\n');
text = text ? `${text}\n${responses}` : responses;
}
if (!text) {
continue;
}
@@ -12,12 +12,18 @@ import { Colors } from '../colors.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { useKeypress } from '../hooks/useKeypress.js';
import path from 'node:path';
import type { Config } from '@google/gemini-cli-core';
import type { SessionInfo } from '../../utils/sessionUtils.js';
import {
listAgySessions,
type Config,
type AgySessionInfo,
} from '@google/gemini-cli-core';
import type { SessionInfo, TextMatch } from '../../utils/sessionUtils.js';
import {
formatRelativeTime,
getSessionFiles,
} from '../../utils/sessionUtils.js';
import { useTabbedNavigation } from '../hooks/useTabbedNavigation.js';
import { TabHeader, type Tab } from './shared/TabHeader.js';
/**
* Props for the main SessionBrowser component.
@@ -41,6 +47,8 @@ export interface SessionBrowserState {
// Data state
/** All loaded sessions */
sessions: SessionInfo[];
/** Antigravity sessions */
agySessions: SessionInfo[];
/** Sessions after filtering and sorting */
filteredAndSortedSessions: SessionInfo[];
@@ -55,6 +63,8 @@ export interface SessionBrowserState {
scrollOffset: number;
/** Terminal width for layout calculations */
terminalWidth: number;
/** Current active tab (0: CLI, 1: Antigravity) */
activeTab: number;
// Search state
/** Current search query string */
@@ -83,6 +93,8 @@ export interface SessionBrowserState {
// State setters
/** Update sessions array */
setSessions: React.Dispatch<React.SetStateAction<SessionInfo[]>>;
/** Update agySessions array */
setAgySessions: React.Dispatch<React.SetStateAction<SessionInfo[]>>;
/** Update loading state */
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
/** Update error state */
@@ -91,6 +103,8 @@ export interface SessionBrowserState {
setActiveIndex: React.Dispatch<React.SetStateAction<number>>;
/** Update scroll offset */
setScrollOffset: React.Dispatch<React.SetStateAction<number>>;
/** Update active tab */
setActiveTab: (index: number) => void;
/** Update search query */
setSearchQuery: React.Dispatch<React.SetStateAction<string>>;
/** Update search mode state */
@@ -352,6 +366,7 @@ export const useSessionBrowserState = (
): SessionBrowserState => {
const { columns: terminalWidth } = useTerminalSize();
const [sessions, setSessions] = useState<SessionInfo[]>(initialSessions);
const [agySessions, setAgySessions] = useState<SessionInfo[]>([]);
const [loading, setLoading] = useState(initialLoading);
const [error, setError] = useState<string | null>(initialError);
const [activeIndex, setActiveIndex] = useState(0);
@@ -365,10 +380,19 @@ export const useSessionBrowserState = (
const [hasLoadedFullContent, setHasLoadedFullContent] = useState(false);
const loadingFullContentRef = useRef(false);
const [activeTab, setActiveTabInternal] = useState(0);
const setActiveTab = useCallback((index: number) => {
setActiveTabInternal(index);
setActiveIndex(0);
setScrollOffset(0);
}, []);
const filteredAndSortedSessions = useMemo(() => {
const filtered = filterSessions(sessions, searchQuery);
const currentTabSessions = activeTab === 0 ? sessions : agySessions;
const filtered = filterSessions(currentTabSessions, searchQuery);
return sortSessions(filtered, sortOrder, sortReverse);
}, [sessions, searchQuery, sortOrder, sortReverse]);
}, [sessions, agySessions, activeTab, searchQuery, sortOrder, sortReverse]);
// Reset full content flag when search is cleared
useEffect(() => {
@@ -386,6 +410,8 @@ export const useSessionBrowserState = (
const state: SessionBrowserState = {
sessions,
setSessions,
agySessions,
setAgySessions,
loading,
setLoading,
error,
@@ -394,6 +420,8 @@ export const useSessionBrowserState = (
setActiveIndex,
scrollOffset,
setScrollOffset,
activeTab,
setActiveTab,
searchQuery,
setSearchQuery,
isSearchMode,
@@ -415,12 +443,34 @@ export const useSessionBrowserState = (
return state;
};
/**
* Converts Antigravity session info to CLI SessionInfo format.
*/
function convertAgyToSessionInfo(
agy: AgySessionInfo,
index: number,
): SessionInfo {
return {
id: agy.id,
file: agy.id,
fileName: agy.id + '.pb',
startTime: agy.mtime,
lastUpdated: agy.mtime,
messageCount: agy.messageCount || 0,
displayName: agy.displayName || 'Antigravity Session',
firstUserMessage: agy.displayName || '',
isCurrentSession: false,
index,
};
}
/**
* Hook to load sessions on mount.
*/
const useLoadSessions = (config: Config, state: SessionBrowserState) => {
const {
setSessions,
setAgySessions,
setLoading,
setError,
isSearchMode,
@@ -432,11 +482,20 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
const loadSessions = async () => {
try {
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
const sessionData = await getSessionFiles(
chatsDir,
config.getSessionId(),
);
const [sessionData, agyData] = await Promise.all([
getSessionFiles(chatsDir, config.getSessionId()),
listAgySessions(),
]);
setSessions(sessionData);
const normalizedAgy = agyData
.sort(
(a, b) => new Date(a.mtime).getTime() - new Date(b.mtime).getTime(),
)
.map((agy, i) => convertAgyToSessionInfo(agy, i + 1));
setAgySessions(normalizedAgy);
setLoading(false);
} catch (err) {
setError(
@@ -448,7 +507,7 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadSessions();
}, [config, setSessions, setLoading, setError]);
}, [config, setSessions, setAgySessions, setLoading, setError]);
useEffect(() => {
const loadFullContent = async () => {
@@ -610,6 +669,9 @@ export const useSessionBrowserInput = (
}
// Delete session control.
else if (key.sequence === 'x' || key.sequence === 'X') {
// Only allow deleting CLI sessions for now
if (state.activeTab !== 0) return true;
const selectedSession =
state.filteredAndSortedSessions[state.activeIndex];
if (selectedSession && !selectedSession.isCurrentSession) {
@@ -684,6 +746,11 @@ export function SessionBrowserView({
}: {
state: SessionBrowserState;
}): React.JSX.Element {
const tabs: Tab[] = [
{ key: 'cli', header: 'CLI Sessions' },
{ key: 'agy', header: 'Antigravity' },
];
if (state.loading) {
return <SessionBrowserLoading />;
}
@@ -692,11 +759,20 @@ export function SessionBrowserView({
return <SessionBrowserError state={state} />;
}
if (state.sessions.length === 0) {
if (state.sessions.length === 0 && state.agySessions.length === 0) {
return <SessionBrowserEmpty />;
}
return (
<Box flexDirection="column" paddingX={1}>
<Box marginBottom={1}>
<TabHeader
tabs={tabs}
currentIndex={state.activeTab}
showStatusIcons={false}
/>
</Box>
<SessionListHeader state={state} />
{state.isSearchMode && <SearchModeDisplay state={state} />}
@@ -721,6 +797,13 @@ export function SessionBrowser({
useLoadSessions(config, state);
const moveSelection = useMoveSelection(state);
const cycleSortOrder = useCycleSortOrder(state);
useTabbedNavigation({
tabCount: 2,
isActive: !state.isSearchMode,
onTabChange: (index) => state.setActiveTab(index),
});
useSessionBrowserInput(
state,
moveSelection,
+25 -7
View File
@@ -9,6 +9,9 @@ import type { HistoryItemWithoutId } from '../types.js';
import * as fs from 'node:fs/promises';
import path from 'node:path';
import {
loadAgySession,
trajectoryToJson,
convertAgyToCliRecord,
coreEvents,
convertSessionToClientHistory,
uiTelemetryService,
@@ -51,20 +54,35 @@ export const useSessionBrowser = (
handleResumeSession: useCallback(
async (session: SessionInfo) => {
try {
let conversation: ConversationRecord;
let filePath: string;
if (session.fileName.endsWith('.pb')) {
// Antigravity session
const data = await loadAgySession(session.id);
if (!data) {
throw new Error(
`Could not load Antigravity session ${session.id}`,
);
}
const json = trajectoryToJson(data);
conversation = convertAgyToCliRecord(json);
// Antigravity sessions don't have a local CLI file path yet,
// but we'll use the .pb path for reference in resumedSessionData
filePath = session.id + '.pb';
} else {
// Regular CLI session
const chatsDir = path.join(
config.storage.getProjectTempDir(),
'chats',
);
const fileName = session.fileName;
const originalFilePath = path.join(chatsDir, fileName);
filePath = path.join(chatsDir, fileName);
// Load up the conversation.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const conversation: ConversationRecord = JSON.parse(
await fs.readFile(originalFilePath, 'utf8'),
);
conversation = JSON.parse(await fs.readFile(filePath, 'utf8'));
}
// Use the old session's ID to continue it.
const existingSessionId = conversation.sessionId;
@@ -73,7 +91,7 @@ export const useSessionBrowser = (
const resumedSessionData = {
conversation,
filePath: originalFilePath,
filePath,
};
// We've loaded it; tell the UI about it.
+2
View File
@@ -221,6 +221,8 @@ export * from './telemetry/constants.js';
export { sessionId, createSessionId } from './utils/session.js';
export * from './utils/compatibility.js';
export * from './utils/browser.js';
export * from './teleportation/index.js';
export { Storage } from './config/storage.js';
// Export hooks system
+20
View File
@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export interface AgyTrajectory {
trajectoryId: string;
cascadeId: string;
trajectoryType: number;
steps: unknown[];
}
export * from './teleporter.js';
export { convertAgyToCliRecord } from './converter.js';
export {
loadAgySession,
listAgySessions,
type AgySessionInfo,
} from './discovery.js';
+1 -1
View File
@@ -26,7 +26,7 @@ import path from 'node:path';
const sourceDir = path.join('src');
const targetDir = path.join('dist', 'src');
const extensionsToCopy = ['.md', '.json', '.sb', '.toml'];
const extensionsToCopy = ['.md', '.json', '.sb', '.toml', '.js'];
function copyFilesRecursive(source, target) {
if (!fs.existsSync(target)) {