mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-16 01:12:49 +00:00
Merge branch 'main' into topics-debug-logs
This commit is contained in:
@@ -231,7 +231,8 @@ describe('Plan Mode', () => {
|
||||
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
|
||||
).toBe(true);
|
||||
});
|
||||
it('should switch from a pro model to a flash model after exiting plan mode', async () => {
|
||||
|
||||
it.skip('should switch from a pro model to a flash model after exiting plan mode', async () => {
|
||||
const plansDir = 'plans-folder';
|
||||
const planFilename = 'my-plan.md';
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ describe('parseArguments', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
it('should fail if both --resume and --session-id are provided', async () => {
|
||||
it('should fail if multiple session flags are provided', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
@@ -255,7 +255,7 @@ describe('parseArguments', () => {
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Cannot use both --resume (-r) and --session-id together',
|
||||
'The flags --resume, --session-id, and --session-file are mutually exclusive. Please provide only one.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface CliArgs {
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
resume: string | typeof RESUME_LATEST | undefined;
|
||||
sessionFile?: string | undefined;
|
||||
sessionId: string | undefined;
|
||||
listSessions: boolean | undefined;
|
||||
deleteSession: string | undefined;
|
||||
@@ -239,8 +240,14 @@ export async function parseArguments(
|
||||
? query.length > 0
|
||||
: !!query;
|
||||
|
||||
if (argv['resume'] !== undefined && argv['session-id'] !== undefined) {
|
||||
return 'Cannot use both --resume (-r) and --session-id together';
|
||||
const sessionFlags = [
|
||||
argv['resume'] !== undefined,
|
||||
argv['session-id'] !== undefined,
|
||||
argv['session-file'] !== undefined,
|
||||
].filter(Boolean).length;
|
||||
|
||||
if (sessionFlags > 1) {
|
||||
return 'The flags --resume, --session-id, and --session-file are mutually exclusive. Please provide only one.';
|
||||
}
|
||||
|
||||
if (argv['prompt'] && hasPositionalQuery) {
|
||||
@@ -412,6 +419,11 @@ export async function parseArguments(
|
||||
return trimmed;
|
||||
},
|
||||
})
|
||||
.option('session-file', {
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
description: 'Load a session from a JSON file',
|
||||
})
|
||||
.option('session-id', {
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
|
||||
@@ -120,6 +120,7 @@ describe('footerItems', () => {
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'session-id',
|
||||
'hostname',
|
||||
'code-changes',
|
||||
'token-count',
|
||||
]);
|
||||
|
||||
@@ -47,6 +47,11 @@ export const ALL_ITEMS = [
|
||||
header: 'session',
|
||||
description: 'Unique identifier for the current session',
|
||||
},
|
||||
{
|
||||
id: 'hostname',
|
||||
header: 'machine',
|
||||
description: 'Current machine hostname',
|
||||
},
|
||||
{
|
||||
id: 'auth',
|
||||
header: '/auth',
|
||||
@@ -75,6 +80,7 @@ export const DEFAULT_ORDER = [
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'session-id',
|
||||
'hostname',
|
||||
'auth',
|
||||
'code-changes',
|
||||
'token-count',
|
||||
|
||||
44
packages/cli/src/config/mutual-exclusivity.test.ts
Normal file
44
packages/cli/src/config/mutual-exclusivity.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { parseArguments } from './config.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
|
||||
describe('parseArguments mutual exclusivity', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const combinations = [
|
||||
['--resume', '--session-id', 'test-id'],
|
||||
['--resume', '--session-file', 'test.json'],
|
||||
['--session-id', 'test-id', '--session-file', 'test.json'],
|
||||
['--resume', '--session-id', 'test-id', '--session-file', 'test.json'],
|
||||
];
|
||||
|
||||
combinations.forEach((args) => {
|
||||
it(`should fail if ${args.filter((a) => a.startsWith('--')).join(' and ')} are provided`, async () => {
|
||||
process.argv = ['node', 'script.js', ...args];
|
||||
const mockConsoleError = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
|
||||
await expect(parseArguments(createTestMergedSettings())).rejects.toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
|
||||
expect(mockConsoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'The flags --resume, --session-id, and --session-file are mutually exclusive. Please provide only one.',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
type StartupWarning,
|
||||
type ConversationRecord,
|
||||
WarningPriority,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
@@ -828,14 +829,14 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
});
|
||||
|
||||
it('should handle session selector error', async () => {
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
resolveSession: vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Session not found')),
|
||||
}) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
);
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
vi.mocked(SessionSelector).mockImplementation(function () {
|
||||
return {
|
||||
resolveSession: vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Session not found')),
|
||||
} as unknown as InstanceType<typeof SessionSelector>;
|
||||
});
|
||||
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
@@ -884,14 +885,14 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
});
|
||||
|
||||
it('should start normally with a warning when no sessions found for resume', async () => {
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
resolveSession: vi
|
||||
.fn()
|
||||
.mockRejectedValue(SessionError.noSessionsFound()),
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
vi.mocked(SessionSelector).mockImplementation(function () {
|
||||
return {
|
||||
resolveSession: vi
|
||||
.fn()
|
||||
.mockRejectedValue(SessionError.noSessionsFound()),
|
||||
} as unknown as InstanceType<typeof SessionSelector>;
|
||||
});
|
||||
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
@@ -1068,13 +1069,88 @@ describe('resolveSessionId', () => {
|
||||
expect(resumedSessionData).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should import from session file when sessionFile is provided', async () => {
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
vi.mocked(SessionSelector).mockImplementation(function () {
|
||||
return {
|
||||
sessionExists: vi.fn().mockResolvedValue(false),
|
||||
} as unknown as InstanceType<typeof SessionSelector>;
|
||||
});
|
||||
|
||||
const coreModule = await import('@google/gemini-cli-core');
|
||||
vi.spyOn(coreModule, 'loadConversationRecord').mockResolvedValueOnce({
|
||||
sessionId: 'old-session-id',
|
||||
projectHash: 'hash',
|
||||
startTime: 'time',
|
||||
lastUpdated: 'time',
|
||||
messages: [
|
||||
{ type: 'info', content: 'Old info', id: '1' },
|
||||
{ type: 'user', content: 'Hello', id: '2' },
|
||||
{ type: 'gemini', content: 'Hi', id: '3' },
|
||||
{ type: 'error', content: 'Old error', id: '4' },
|
||||
{ type: 'user', id: '5' }, // Missing content
|
||||
null, // Null object
|
||||
{ type: 'unknown', content: 'Something', id: '6' }, // Unknown type
|
||||
],
|
||||
} as unknown as ConversationRecord);
|
||||
|
||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
try {
|
||||
const { sessionId, resumedSessionData } = await resolveSessionId(
|
||||
undefined,
|
||||
undefined,
|
||||
'dummy-session.json',
|
||||
);
|
||||
|
||||
expect(sessionId).toBeDefined();
|
||||
expect(sessionId).not.toBe('old-session-id'); // A new session ID should be created
|
||||
expect(resumedSessionData).toBeDefined();
|
||||
expect(resumedSessionData?.conversation.sessionId).toBe(sessionId); // Overwritten
|
||||
|
||||
// Verify messages: should have 1 info (the new import confirmation) + 2 valid conversation messages
|
||||
// Invalid messages (missing content, null, unknown type) and transient messages should be filtered out.
|
||||
expect(resumedSessionData?.conversation.messages).toHaveLength(3);
|
||||
expect(resumedSessionData?.conversation.messages![0]).toMatchObject({
|
||||
type: 'info',
|
||||
content: expect.stringContaining('Imported session from'),
|
||||
});
|
||||
expect(resumedSessionData?.conversation.messages![1]).toMatchObject({
|
||||
type: 'user',
|
||||
content: 'Hello',
|
||||
});
|
||||
expect(resumedSessionData?.conversation.messages![2]).toMatchObject({
|
||||
type: 'gemini',
|
||||
content: 'Hi',
|
||||
});
|
||||
|
||||
expect(resumedSessionData?.filePath).toContain(sessionId.slice(0, 8)); // New path
|
||||
} catch (e) {
|
||||
if (e instanceof MockProcessExitError) {
|
||||
throw new Error(
|
||||
'process.exit called with: ' +
|
||||
JSON.stringify(emitFeedbackSpy.mock.calls),
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
emitFeedbackSpy.mockRestore();
|
||||
processExitSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should exit with FATAL_INPUT_ERROR when sessionId already exists', async () => {
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
sessionExists: vi.fn().mockResolvedValue(true),
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
vi.mocked(SessionSelector).mockImplementation(function () {
|
||||
return {
|
||||
sessionExists: vi.fn().mockResolvedValue(true),
|
||||
} as unknown as InstanceType<typeof SessionSelector>;
|
||||
});
|
||||
|
||||
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
|
||||
const processExitSpy = vi
|
||||
@@ -1100,12 +1176,12 @@ describe('resolveSessionId', () => {
|
||||
});
|
||||
|
||||
it('should return provided sessionId when it does not exist', async () => {
|
||||
vi.mocked(SessionSelector).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
sessionExists: vi.fn().mockResolvedValue(false),
|
||||
}) as unknown as InstanceType<typeof SessionSelector>,
|
||||
);
|
||||
// eslint-disable-next-line prefer-arrow-callback
|
||||
vi.mocked(SessionSelector).mockImplementation(function () {
|
||||
return {
|
||||
sessionExists: vi.fn().mockResolvedValue(false),
|
||||
} as unknown as InstanceType<typeof SessionSelector>;
|
||||
});
|
||||
const { sessionId, resumedSessionData } = await resolveSessionId(
|
||||
undefined,
|
||||
'new-id',
|
||||
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
debugLogger,
|
||||
isHeadlessMode,
|
||||
Storage,
|
||||
getProjectHash,
|
||||
loadConversationRecord,
|
||||
type MessageRecord,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import { loadCliConfig, parseArguments } from './config/config.js';
|
||||
@@ -44,6 +47,8 @@ import { createHash } from 'node:crypto';
|
||||
import v8 from 'node:v8';
|
||||
import os from 'node:os';
|
||||
import dns from 'node:dns';
|
||||
import * as path from 'node:path';
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import { start_sandbox } from './utils/sandbox.js';
|
||||
import {
|
||||
loadSettings,
|
||||
@@ -194,11 +199,12 @@ ${reason.stack}`
|
||||
export async function resolveSessionId(
|
||||
resumeArg: string | undefined,
|
||||
sessionIdArg?: string | undefined,
|
||||
sessionFileArg?: string | undefined,
|
||||
): Promise<{
|
||||
sessionId: string;
|
||||
resumedSessionData?: ResumedSessionData;
|
||||
}> {
|
||||
if (!resumeArg && !sessionIdArg) {
|
||||
if (!resumeArg && !sessionIdArg && !sessionFileArg) {
|
||||
return { sessionId: createSessionId() };
|
||||
}
|
||||
|
||||
@@ -207,6 +213,80 @@ export async function resolveSessionId(
|
||||
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
|
||||
if (sessionFileArg) {
|
||||
try {
|
||||
const sessionData = await loadConversationRecord(sessionFileArg);
|
||||
if (!sessionData) {
|
||||
throw new Error(`File not found or invalid format: ${sessionFileArg}`);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const isoNow = new Date(now).toISOString();
|
||||
|
||||
// Filter out old system/info messages that are specific to the previous run
|
||||
// and only keep actual conversation messages (user/gemini).
|
||||
// Best effort parse: ensure message is an object and has required fields.
|
||||
sessionData.messages = (sessionData.messages || []).filter(
|
||||
(m) =>
|
||||
typeof m === 'object' &&
|
||||
m !== null &&
|
||||
(m.type === 'user' || m.type === 'gemini') &&
|
||||
m.content !== undefined,
|
||||
);
|
||||
|
||||
// Add a single info message to the history to confirm the import
|
||||
sessionData.messages.unshift({
|
||||
id: `import-${now}`,
|
||||
type: 'info',
|
||||
content: `Imported session from ${sessionFileArg}`,
|
||||
timestamp: isoNow,
|
||||
} as MessageRecord);
|
||||
|
||||
const newSessionId = createSessionId();
|
||||
sessionData.sessionId = newSessionId;
|
||||
sessionData.projectHash = getProjectHash(storage.getProjectRoot());
|
||||
sessionData.startTime = isoNow;
|
||||
sessionData.lastUpdated = isoNow;
|
||||
|
||||
const chatsDir = path.join(storage.getProjectTempDir(), 'chats');
|
||||
const newSessionPath = path.join(
|
||||
chatsDir,
|
||||
`session-${now}-${newSessionId.slice(0, 8)}.jsonl`,
|
||||
);
|
||||
|
||||
const { messages: _messages, ...initialMetadata } = sessionData;
|
||||
|
||||
const lines = [JSON.stringify(initialMetadata)];
|
||||
if (sessionData.messages) {
|
||||
for (const msg of sessionData.messages) {
|
||||
lines.push(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
await fsPromises.mkdir(chatsDir, { recursive: true });
|
||||
await fsPromises.writeFile(
|
||||
newSessionPath,
|
||||
lines.join('\n') + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
return {
|
||||
sessionId: newSessionId,
|
||||
resumedSessionData: {
|
||||
conversation: sessionData,
|
||||
filePath: newSessionPath,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Error importing session from file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.FATAL_INPUT_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionIdArg) {
|
||||
if (await sessionSelector.sessionExists(sessionIdArg)) {
|
||||
coreEvents.emitFeedback(
|
||||
@@ -340,6 +420,7 @@ export async function main() {
|
||||
const { sessionId, resumedSessionData } = await resolveSessionId(
|
||||
argv.resume,
|
||||
argv.sessionId,
|
||||
argv.sessionFile,
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -30,6 +30,7 @@ import { compressCommand } from '../ui/commands/compressCommand.js';
|
||||
import { copyCommand } from '../ui/commands/copyCommand.js';
|
||||
import { corgiCommand } from '../ui/commands/corgiCommand.js';
|
||||
import { docsCommand } from '../ui/commands/docsCommand.js';
|
||||
import { exportSessionCommand } from '../ui/commands/exportSessionCommand.js';
|
||||
import { directoryCommand } from '../ui/commands/directoryCommand.js';
|
||||
import { editorCommand } from '../ui/commands/editorCommand.js';
|
||||
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
|
||||
@@ -135,6 +136,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
copyCommand,
|
||||
corgiCommand,
|
||||
docsCommand,
|
||||
exportSessionCommand,
|
||||
directoryCommand,
|
||||
editorCommand,
|
||||
...(this.config?.getExtensionsEnabled() === false
|
||||
|
||||
135
packages/cli/src/ui/commands/exportSessionCommand.test.ts
Normal file
135
packages/cli/src/ui/commands/exportSessionCommand.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { exportSessionCommand } from './exportSessionCommand.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { SessionSelector } from '../../utils/sessionUtils.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
import { Storage, type ConversationRecord } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
vi.mock('../../utils/sessionUtils.js');
|
||||
|
||||
describe('exportSessionCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.spyOn(Storage.prototype, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(Storage.prototype, 'getProjectTempDir').mockReturnValue(
|
||||
path.join(path.sep, 'tmp', 'mock-dir'),
|
||||
);
|
||||
mockContext = {
|
||||
services: {
|
||||
agentContext: {
|
||||
config: {
|
||||
sessionId: 'test-session-id',
|
||||
getSessionId: () => 'test-session-id',
|
||||
storage: new Storage(process.cwd()),
|
||||
},
|
||||
},
|
||||
},
|
||||
invocation: {
|
||||
args: ' export.json ',
|
||||
name: 'export-session',
|
||||
raw: '/export-session export.json',
|
||||
},
|
||||
ui: {
|
||||
addItem: vi.fn(),
|
||||
setPendingItem: vi.fn(),
|
||||
pendingItem: null,
|
||||
},
|
||||
} as unknown as CommandContext;
|
||||
});
|
||||
|
||||
it('should return error if no path is provided', async () => {
|
||||
mockContext.invocation!.args = ' ';
|
||||
const result = await exportSessionCommand.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: expect.stringContaining('Please provide a file path'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error if sessionId is missing', async () => {
|
||||
mockContext.services.agentContext!.config.getSessionId = () =>
|
||||
undefined as unknown as string;
|
||||
const result = await exportSessionCommand.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No active session found to export.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should export the session successfully', async () => {
|
||||
const mockSessionData: ConversationRecord = {
|
||||
sessionId: 'test-session-id',
|
||||
messages: [],
|
||||
projectHash: 'hash',
|
||||
startTime: 'time',
|
||||
lastUpdated: 'time',
|
||||
};
|
||||
vi.mocked(SessionSelector.prototype.resolveSession).mockResolvedValue({
|
||||
sessionData: mockSessionData,
|
||||
sessionPath: path.join(
|
||||
path.sep,
|
||||
'tmp',
|
||||
'mock-dir',
|
||||
'chats',
|
||||
'session.jsonl',
|
||||
),
|
||||
displayInfo: 'test',
|
||||
});
|
||||
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const result = await exportSessionCommand.action!(mockContext, '');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
path.resolve(process.cwd(), 'export.json'),
|
||||
JSON.stringify(mockSessionData, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'export_session',
|
||||
exportSession: { isPending: true },
|
||||
}),
|
||||
);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'export_session',
|
||||
exportSession: {
|
||||
isPending: false,
|
||||
targetPath: expect.stringContaining('export.json'),
|
||||
},
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(mockContext.ui.setPendingItem).toHaveBeenLastCalledWith(null);
|
||||
});
|
||||
|
||||
it('should return error if resolveSession fails', async () => {
|
||||
vi.mocked(SessionSelector.prototype.resolveSession).mockRejectedValue(
|
||||
new Error('Session not found'),
|
||||
);
|
||||
|
||||
const result = await exportSessionCommand.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Failed to export session: Session not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
98
packages/cli/src/ui/commands/exportSessionCommand.ts
Normal file
98
packages/cli/src/ui/commands/exportSessionCommand.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
type SlashCommandActionReturn,
|
||||
CommandKind,
|
||||
} from './types.js';
|
||||
import { MessageType, type HistoryItemExportSession } from '../types.js';
|
||||
import { SessionSelector } from '../../utils/sessionUtils.js';
|
||||
|
||||
export const exportSessionCommand: SlashCommand = {
|
||||
name: 'export-session',
|
||||
description: 'Export the current session to a JSON file',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
): Promise<SlashCommandActionReturn | void> => {
|
||||
const { ui } = context;
|
||||
const args = context.invocation?.args.trim();
|
||||
if (!args) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content:
|
||||
'Please provide a file path to export the session to. Example: /export-session ./my-session.json',
|
||||
};
|
||||
}
|
||||
|
||||
const sessionId = context.services.agentContext?.config.getSessionId();
|
||||
if (!sessionId) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'No active session found to export.',
|
||||
};
|
||||
}
|
||||
|
||||
if (ui.pendingItem) {
|
||||
ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: 'Operation already in progress, please wait.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingMessage: HistoryItemExportSession = {
|
||||
type: MessageType.EXPORT_SESSION,
|
||||
exportSession: {
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
ui.setPendingItem(pendingMessage);
|
||||
const storage = context.services.agentContext!.config.storage;
|
||||
const sessionSelector = new SessionSelector(storage);
|
||||
const { sessionData } = await sessionSelector.resolveSession(sessionId);
|
||||
|
||||
const targetPath = path.resolve(process.cwd(), args);
|
||||
|
||||
await fs.writeFile(
|
||||
targetPath,
|
||||
JSON.stringify(sessionData, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
ui.addItem(
|
||||
{
|
||||
type: MessageType.EXPORT_SESSION,
|
||||
exportSession: {
|
||||
isPending: false,
|
||||
targetPath,
|
||||
},
|
||||
} as HistoryItemExportSession,
|
||||
Date.now(),
|
||||
);
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to export session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
} finally {
|
||||
ui.setPendingItem(null);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
import os from 'node:os';
|
||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { QuotaDisplay } from './QuotaDisplay.js';
|
||||
@@ -35,6 +36,8 @@ import {
|
||||
} from '../../config/footerItems.js';
|
||||
import { isDevelopment } from '../../utils/installationInfo.js';
|
||||
|
||||
const HOSTNAME = os.hostname();
|
||||
|
||||
interface CwdIndicatorProps {
|
||||
targetDir: string;
|
||||
maxWidth: number;
|
||||
@@ -386,6 +389,15 @@ export const Footer: React.FC = () => {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'hostname': {
|
||||
addCol(
|
||||
id,
|
||||
header,
|
||||
() => <Text color={itemColor}>{HOSTNAME}</Text>,
|
||||
HOSTNAME.length,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'auth': {
|
||||
if (!settings.merged.ui.showUserIdentity) break;
|
||||
if (!authType) break;
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('<FooterConfigDialog />', () => {
|
||||
expect(lastFrame()).toContain('~/project/path');
|
||||
|
||||
// Move focus down to 'code-changes' (which has colored elements)
|
||||
for (let i = 0; i < 9; i++) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
act(() => {
|
||||
stdin.write('\u001b[B'); // Down arrow
|
||||
});
|
||||
|
||||
@@ -249,6 +249,9 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||
'session-id': (
|
||||
<Text color={getColor('session-id', itemColor)}>769992f9</Text>
|
||||
),
|
||||
hostname: (
|
||||
<Text color={getColor('hostname', itemColor)}>dev-machine</Text>
|
||||
),
|
||||
'code-changes': (
|
||||
<Box flexDirection="row">
|
||||
<Text color={getColor('code-changes', theme.status.success)}>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as path from 'node:path';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||
import { MessageType, type HistoryItem } from '../types.js';
|
||||
@@ -198,6 +199,25 @@ describe('<HistoryItemDisplay />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders ExportSessionMessage for "export_session" type', async () => {
|
||||
const testPath = path.join(path.sep, 'test', 'path.json');
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'export_session',
|
||||
exportSession: {
|
||||
isPending: false,
|
||||
targetPath: testPath,
|
||||
},
|
||||
};
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
);
|
||||
expect(lastFrame()).toContain(
|
||||
`Successfully exported session to ${testPath}`,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should escape ANSI codes in text content', async () => {
|
||||
const historyItem: HistoryItem = {
|
||||
id: 1,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
|
||||
import { ToolGroupDisplay } from './messages/ToolGroupDisplay.js';
|
||||
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
|
||||
import { CompressionMessage } from './messages/CompressionMessage.js';
|
||||
import { ExportSessionMessage } from './messages/ExportSessionMessage.js';
|
||||
import { WarningMessage } from './messages/WarningMessage.js';
|
||||
import { SubagentHistoryMessage } from './messages/SubagentHistoryMessage.js';
|
||||
import { Box } from 'ink';
|
||||
@@ -211,6 +212,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'compression' && (
|
||||
<CompressionMessage compression={itemForDisplay.compression} />
|
||||
)}
|
||||
{itemForDisplay.type === 'export_session' && (
|
||||
<ExportSessionMessage exportSession={itemForDisplay.exportSession} />
|
||||
)}
|
||||
{itemForDisplay.type === 'extensions_list' && (
|
||||
<ExtensionsList extensions={itemForDisplay.extensions} />
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="734" viewBox="0 0 920 734">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="768" viewBox="0 0 920 768">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="734" fill="#000000" />
|
||||
<rect width="920" height="768" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -75,91 +75,98 @@
|
||||
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="376" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
|
||||
<text x="72" y="376" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> hostname</text>
|
||||
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="393" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
|
||||
<text x="45" y="393" fill="#afafaf" textLength="225" lengthAdjust="spacingAndGlyphs"> Current machine hostname</text>
|
||||
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="408" width="9" height="17" fill="#001a00" />
|
||||
<text x="27" y="410" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">></text>
|
||||
<rect x="36" y="408" width="9" height="17" fill="#001a00" />
|
||||
<rect x="45" y="408" width="27" height="17" fill="#001a00" />
|
||||
<text x="45" y="410" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
|
||||
<rect x="72" y="408" width="117" height="17" fill="#001a00" />
|
||||
<text x="72" y="410" fill="#00cd00" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
|
||||
<rect x="189" y="408" width="684" height="17" fill="#001a00" />
|
||||
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="410" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
|
||||
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="425" width="18" height="17" fill="#001a00" />
|
||||
<rect x="45" y="425" width="513" height="17" fill="#001a00" />
|
||||
<text x="45" y="427" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
|
||||
<rect x="558" y="425" width="315" height="17" fill="#001a00" />
|
||||
<text x="45" y="427" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
|
||||
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="444" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
|
||||
<rect x="27" y="442" width="9" height="17" fill="#001a00" />
|
||||
<text x="27" y="444" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">></text>
|
||||
<rect x="36" y="442" width="9" height="17" fill="#001a00" />
|
||||
<rect x="45" y="442" width="27" height="17" fill="#001a00" />
|
||||
<text x="45" y="444" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
|
||||
<rect x="72" y="442" width="117" height="17" fill="#001a00" />
|
||||
<text x="72" y="444" fill="#00cd00" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
|
||||
<rect x="189" y="442" width="684" height="17" fill="#001a00" />
|
||||
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="461" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
|
||||
<rect x="27" y="459" width="18" height="17" fill="#001a00" />
|
||||
<rect x="45" y="459" width="513" height="17" fill="#001a00" />
|
||||
<text x="45" y="461" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
|
||||
<rect x="558" y="459" width="315" height="17" fill="#001a00" />
|
||||
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="478" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
|
||||
<text x="45" y="478" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
|
||||
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="495" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
|
||||
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
|
||||
<text x="45" y="512" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
|
||||
<text x="72" y="512" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
|
||||
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="546" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
|
||||
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="563" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
|
||||
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="597" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
|
||||
<text x="27" y="597" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
|
||||
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="614" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
|
||||
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="631" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
|
||||
<text x="288" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
|
||||
<text x="396" y="631" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
|
||||
<text x="504" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
|
||||
<text x="684" y="631" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">quota</text>
|
||||
<rect x="801" y="629" width="36" height="17" fill="#001a00" />
|
||||
<text x="801" y="631" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">diff</text>
|
||||
<rect x="837" y="629" width="18" height="17" fill="#001a00" />
|
||||
<text x="864" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="631" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
|
||||
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
|
||||
<text x="288" y="648" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
|
||||
<text x="396" y="648" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
|
||||
<text x="504" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
|
||||
<text x="684" y="648" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">42% used</text>
|
||||
<rect x="801" y="646" width="27" height="17" fill="#001a00" />
|
||||
<text x="801" y="648" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">+12</text>
|
||||
<rect x="828" y="646" width="9" height="17" fill="#001a00" />
|
||||
<rect x="837" y="646" width="18" height="17" fill="#001a00" />
|
||||
<text x="837" y="648" fill="#ff87af" textLength="18" lengthAdjust="spacingAndGlyphs">-4</text>
|
||||
<text x="45" y="648" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
|
||||
<text x="864" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="665" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
|
||||
<text x="27" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="665" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
|
||||
<text x="288" y="665" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
|
||||
<text x="396" y="665" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
|
||||
<text x="504" y="665" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
|
||||
<text x="684" y="665" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">quota</text>
|
||||
<rect x="801" y="663" width="36" height="17" fill="#001a00" />
|
||||
<text x="801" y="665" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">diff</text>
|
||||
<rect x="837" y="663" width="18" height="17" fill="#001a00" />
|
||||
<text x="864" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="682" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
|
||||
<text x="288" y="682" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
|
||||
<text x="396" y="682" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
|
||||
<text x="504" y="682" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
|
||||
<text x="684" y="682" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">42% used</text>
|
||||
<rect x="801" y="680" width="27" height="17" fill="#001a00" />
|
||||
<text x="801" y="682" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">+12</text>
|
||||
<rect x="828" y="680" width="9" height="17" fill="#001a00" />
|
||||
<rect x="837" y="680" width="18" height="17" fill="#001a00" />
|
||||
<text x="837" y="682" fill="#ff87af" textLength="18" lengthAdjust="spacingAndGlyphs">-4</text>
|
||||
<text x="864" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="699" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="0" y="699" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="699" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
|
||||
<text x="891" y="699" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="716" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="716" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="733" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="734" viewBox="0 0 920 734">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="768" viewBox="0 0 920 768">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="734" fill="#000000" />
|
||||
<rect width="920" height="768" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -84,77 +84,84 @@
|
||||
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="376" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
|
||||
<text x="72" y="376" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> hostname</text>
|
||||
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="393" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
|
||||
<text x="45" y="393" fill="#afafaf" textLength="225" lengthAdjust="spacingAndGlyphs"> Current machine hostname</text>
|
||||
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="410" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
|
||||
<text x="72" y="410" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
|
||||
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="427" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
|
||||
<text x="45" y="427" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
|
||||
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="444" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
|
||||
<text x="72" y="444" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
|
||||
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="461" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
|
||||
<text x="45" y="461" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
|
||||
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="478" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
|
||||
<text x="45" y="478" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
|
||||
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="495" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
|
||||
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
|
||||
<text x="45" y="512" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
|
||||
<text x="72" y="512" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
|
||||
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="546" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
|
||||
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="563" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
|
||||
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="597" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
|
||||
<text x="27" y="597" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
|
||||
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="614" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
|
||||
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="45" y="629" width="198" height="17" fill="#001a00" />
|
||||
<text x="45" y="631" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
|
||||
<text x="315" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
|
||||
<text x="450" y="631" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
|
||||
<text x="585" y="631" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
|
||||
<text x="783" y="631" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">quota</text>
|
||||
<text x="864" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="631" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
|
||||
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="45" y="646" width="126" height="17" fill="#001a00" />
|
||||
<text x="45" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
|
||||
<rect x="171" y="646" width="72" height="17" fill="#001a00" />
|
||||
<text x="315" y="648" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
|
||||
<text x="450" y="648" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
|
||||
<text x="585" y="648" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
|
||||
<text x="783" y="648" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">42% used</text>
|
||||
<text x="45" y="648" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
|
||||
<text x="864" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="665" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
|
||||
<text x="27" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="45" y="663" width="198" height="17" fill="#001a00" />
|
||||
<text x="45" y="665" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
|
||||
<text x="315" y="665" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
|
||||
<text x="450" y="665" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
|
||||
<text x="585" y="665" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
|
||||
<text x="783" y="665" fill="#afafaf" textLength="45" lengthAdjust="spacingAndGlyphs">quota</text>
|
||||
<text x="864" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="45" y="680" width="126" height="17" fill="#001a00" />
|
||||
<text x="45" y="682" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
|
||||
<rect x="171" y="680" width="72" height="17" fill="#001a00" />
|
||||
<text x="315" y="682" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
|
||||
<text x="450" y="682" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
|
||||
<text x="585" y="682" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
|
||||
<text x="783" y="682" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">42% used</text>
|
||||
<text x="864" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="699" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="0" y="699" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="699" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
|
||||
<text x="891" y="699" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="716" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="716" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="733" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="717" viewBox="0 0 920 717">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="717" fill="#000000" />
|
||||
<rect width="920" height="751" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -75,76 +75,83 @@
|
||||
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="376" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
|
||||
<text x="72" y="376" fill="#ffffff" textLength="81" lengthAdjust="spacingAndGlyphs"> hostname</text>
|
||||
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="393" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
|
||||
<text x="45" y="393" fill="#afafaf" textLength="225" lengthAdjust="spacingAndGlyphs"> Current machine hostname</text>
|
||||
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="410" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
|
||||
<text x="72" y="410" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs"> auth</text>
|
||||
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="427" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
|
||||
<text x="45" y="427" fill="#afafaf" textLength="252" lengthAdjust="spacingAndGlyphs"> Current authentication info</text>
|
||||
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<text x="72" y="444" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
|
||||
<text x="72" y="444" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
|
||||
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="461" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
|
||||
<text x="45" y="461" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
|
||||
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="476" width="9" height="17" fill="#001a00" />
|
||||
<text x="27" y="478" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">></text>
|
||||
<rect x="36" y="476" width="9" height="17" fill="#001a00" />
|
||||
<rect x="45" y="476" width="27" height="17" fill="#001a00" />
|
||||
<text x="45" y="478" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<rect x="72" y="476" width="171" height="17" fill="#001a00" />
|
||||
<text x="72" y="478" fill="#00cd00" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
|
||||
<rect x="243" y="476" width="630" height="17" fill="#001a00" />
|
||||
<text x="72" y="478" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
|
||||
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="493" width="846" height="17" fill="#001a00" />
|
||||
<text x="45" y="495" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
|
||||
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="512" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
|
||||
<rect x="27" y="510" width="9" height="17" fill="#001a00" />
|
||||
<text x="27" y="512" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">></text>
|
||||
<rect x="36" y="510" width="9" height="17" fill="#001a00" />
|
||||
<rect x="45" y="510" width="27" height="17" fill="#001a00" />
|
||||
<text x="45" y="512" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
|
||||
<rect x="72" y="510" width="171" height="17" fill="#001a00" />
|
||||
<text x="72" y="512" fill="#00cd00" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
|
||||
<rect x="243" y="510" width="630" height="17" fill="#001a00" />
|
||||
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="527" width="846" height="17" fill="#001a00" />
|
||||
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="546" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
|
||||
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="563" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
|
||||
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="597" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
|
||||
<text x="27" y="597" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
|
||||
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="614" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
|
||||
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="631" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
|
||||
<text x="207" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="270" y="631" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
|
||||
<text x="342" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="405" y="631" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
|
||||
<text x="495" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="558" y="631" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
|
||||
<text x="720" y="631" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="783" y="631" fill="#afafaf" textLength="72" lengthAdjust="spacingAndGlyphs">42% used</text>
|
||||
<text x="864" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="631" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
|
||||
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="648" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
|
||||
<text x="27" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="648" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
|
||||
<text x="864" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="648" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="45" y="665" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
|
||||
<text x="207" y="665" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="270" y="665" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
|
||||
<text x="342" y="665" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="405" y="665" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
|
||||
<text x="495" y="665" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="558" y="665" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
|
||||
<text x="720" y="665" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
|
||||
<text x="783" y="665" fill="#afafaf" textLength="72" lengthAdjust="spacingAndGlyphs">42% used</text>
|
||||
<text x="864" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="665" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="682" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="0" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="682" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
|
||||
<text x="891" y="682" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="699" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="699" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="716" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -23,6 +23,8 @@ exports[`<FooterConfigDialog /> > highlights the active item in the preview 1`]
|
||||
│ Memory used by the application │
|
||||
│ [ ] session-id │
|
||||
│ Unique identifier for the current session │
|
||||
│ [ ] hostname │
|
||||
│ Current machine hostname │
|
||||
│ [ ] auth │
|
||||
│ Current authentication info │
|
||||
│ > [✓] code-changes │
|
||||
@@ -68,6 +70,8 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 1`] =
|
||||
│ Memory used by the application │
|
||||
│ [ ] session-id │
|
||||
│ Unique identifier for the current session │
|
||||
│ [ ] hostname │
|
||||
│ Current machine hostname │
|
||||
│ [ ] auth │
|
||||
│ Current authentication info │
|
||||
│ [ ] code-changes │
|
||||
@@ -114,6 +118,8 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 2`] =
|
||||
│ Memory used by the application │
|
||||
│ [ ] session-id │
|
||||
│ Unique identifier for the current session │
|
||||
│ [ ] hostname │
|
||||
│ Current machine hostname │
|
||||
│ [ ] auth │
|
||||
│ Current authentication info │
|
||||
│ [ ] code-changes │
|
||||
@@ -159,6 +165,8 @@ exports[`<FooterConfigDialog /> > updates the preview when Show footer labels is
|
||||
│ Memory used by the application │
|
||||
│ [ ] session-id │
|
||||
│ Unique identifier for the current session │
|
||||
│ [ ] hostname │
|
||||
│ Current machine hostname │
|
||||
│ [ ] auth │
|
||||
│ Current authentication info │
|
||||
│ [ ] code-changes │
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as path from 'node:path';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ExportSessionMessage } from './ExportSessionMessage.js';
|
||||
|
||||
vi.mock('../CliSpinner.js', () => ({
|
||||
CliSpinner: () => <Text>[spinner]</Text>,
|
||||
}));
|
||||
|
||||
describe('ExportSessionMessage', () => {
|
||||
it('renders pending state correctly', async () => {
|
||||
const { lastFrame } = await render(
|
||||
<ExportSessionMessage exportSession={{ isPending: true }} />,
|
||||
);
|
||||
expect(lastFrame()).toContain('[spinner]');
|
||||
expect(lastFrame()).toContain('Exporting session...');
|
||||
});
|
||||
|
||||
it('renders success state correctly', async () => {
|
||||
const testPath = path.join(path.sep, 'path', 'to', 'session.json');
|
||||
const { lastFrame } = await render(
|
||||
<ExportSessionMessage
|
||||
exportSession={{
|
||||
isPending: false,
|
||||
targetPath: testPath,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toContain('✓');
|
||||
expect(lastFrame()).toContain(
|
||||
`Successfully exported session to ${testPath}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { JSX } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { ExportSessionProps } from '../../types.js';
|
||||
import { CliSpinner } from '../CliSpinner.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
export interface ExportSessionDisplayProps {
|
||||
exportSession: ExportSessionProps;
|
||||
}
|
||||
|
||||
/*
|
||||
* Export session messages appear when the /export-session command is run, and show a loading spinner
|
||||
* while export is in progress, followed by a success message.
|
||||
*/
|
||||
export function ExportSessionMessage({
|
||||
exportSession,
|
||||
}: ExportSessionDisplayProps): JSX.Element {
|
||||
const { isPending, targetPath } = exportSession;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box marginRight={1}>
|
||||
{isPending ? (
|
||||
<CliSpinner type="dots" />
|
||||
) : (
|
||||
<Text color={theme.status.success}>✓</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={isPending ? theme.text.accent : theme.status.success}>
|
||||
{isPending
|
||||
? 'Exporting session...'
|
||||
: `Successfully exported session to ${targetPath}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -148,6 +148,11 @@ export interface CompressionProps {
|
||||
compressionStatus: CompressionStatus | null;
|
||||
}
|
||||
|
||||
export interface ExportSessionProps {
|
||||
isPending: boolean;
|
||||
targetPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* For use when you want no icon.
|
||||
*/
|
||||
@@ -284,6 +289,11 @@ export type HistoryItemCompression = HistoryItemBase & {
|
||||
compression: CompressionProps;
|
||||
};
|
||||
|
||||
export type HistoryItemExportSession = HistoryItemBase & {
|
||||
type: 'export_session';
|
||||
exportSession: ExportSessionProps;
|
||||
};
|
||||
|
||||
export type HistoryItemExtensionsList = HistoryItemBase & {
|
||||
type: 'extensions_list';
|
||||
extensions: GeminiCLIExtension[];
|
||||
@@ -427,6 +437,7 @@ export type HistoryItemWithoutId =
|
||||
| HistoryItemModel
|
||||
| HistoryItemQuit
|
||||
| HistoryItemCompression
|
||||
| HistoryItemExportSession
|
||||
| HistoryItemExtensionsList
|
||||
| HistoryItemToolsList
|
||||
| HistoryItemSkillsList
|
||||
@@ -454,6 +465,7 @@ export enum MessageType {
|
||||
QUIT = 'quit',
|
||||
GEMINI = 'gemini',
|
||||
COMPRESSION = 'compression',
|
||||
EXPORT_SESSION = 'export_session',
|
||||
EXTENSIONS_LIST = 'extensions_list',
|
||||
TOOLS_LIST = 'tools_list',
|
||||
SKILLS_LIST = 'skills_list',
|
||||
|
||||
Reference in New Issue
Block a user