diff --git a/docs/sidebar.json b/docs/sidebar.json index 6cac5ec9fd..5d0d21d14f 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -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,11 +345,20 @@ { "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" + } ] } ] } -] +] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 914d66d3ac..05c7e4676f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 8b38204aa2..845de8d05f 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -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(); - const checkpoint = await logger.loadCheckpoint(tag); - const conversation = checkpoint.history; + + let conversation: Content[] = []; + let authType: string | undefined; + + const loadAgy = async (id: string): Promise => { + 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); + 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; } diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx index ac9b2c2b00..816164be69 100644 --- a/packages/cli/src/ui/components/SessionBrowser.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.tsx @@ -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>; + /** Update agySessions array */ + setAgySessions: React.Dispatch>; /** Update loading state */ setLoading: React.Dispatch>; /** Update error state */ @@ -91,6 +103,8 @@ export interface SessionBrowserState { setActiveIndex: React.Dispatch>; /** Update scroll offset */ setScrollOffset: React.Dispatch>; + /** Update active tab */ + setActiveTab: (index: number) => void; /** Update search query */ setSearchQuery: React.Dispatch>; /** Update search mode state */ @@ -352,6 +366,7 @@ export const useSessionBrowserState = ( ): SessionBrowserState => { const { columns: terminalWidth } = useTerminalSize(); const [sessions, setSessions] = useState(initialSessions); + const [agySessions, setAgySessions] = useState([]); const [loading, setLoading] = useState(initialLoading); const [error, setError] = useState(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 ; } @@ -692,11 +759,20 @@ export function SessionBrowserView({ return ; } - if (state.sessions.length === 0) { + if (state.sessions.length === 0 && state.agySessions.length === 0) { return ; } + return ( + + + + {state.isSearchMode && } @@ -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, diff --git a/packages/cli/src/ui/hooks/useSessionBrowser.ts b/packages/cli/src/ui/hooks/useSessionBrowser.ts index 9a34f68e0b..3d1d376035 100644 --- a/packages/cli/src/ui/hooks/useSessionBrowser.ts +++ b/packages/cli/src/ui/hooks/useSessionBrowser.ts @@ -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 { - const chatsDir = path.join( - config.storage.getProjectTempDir(), - 'chats', - ); + let conversation: ConversationRecord; + let filePath: string; - const fileName = session.fileName; + 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; + filePath = path.join(chatsDir, fileName); - const originalFilePath = 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'), - ); + // Load up the conversation. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + 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. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 47412dd73c..d2318fc5aa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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 diff --git a/packages/core/src/teleportation/index.ts b/packages/core/src/teleportation/index.ts new file mode 100644 index 0000000000..d08d42b771 --- /dev/null +++ b/packages/core/src/teleportation/index.ts @@ -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'; diff --git a/scripts/copy_files.js b/scripts/copy_files.js index fc612fd144..950a7b7e61 100644 --- a/scripts/copy_files.js +++ b/scripts/copy_files.js @@ -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)) {