Compare commits

...

11 Commits

Author SHA1 Message Date
Dmitry Lyalin 0c5594486c Merge branch 'main' into rename-directory-to-workspace 2026-03-10 09:23:12 -04:00
Dmitry Lyalin 1a340be449 Merge branch 'main' into rename-directory-to-workspace 2026-03-09 20:39:25 -04:00
Dmitry Lyalin b8fc6b6a57 Fix test fallout after merging main into workspace rename branch 2026-03-09 18:45:01 -04:00
Dmitry Lyalin 40c7ec1157 Merge remote-tracking branch 'origin/main' into rename-directory-to-workspace
# Conflicts:
#	docs/cli/settings.md
#	docs/reference/configuration.md
#	packages/cli/src/config/settings.test.ts
#	packages/cli/src/config/settingsSchema.ts
#	packages/cli/src/services/CommandService.test.ts
#	packages/cli/src/services/FileCommandLoader.ts
#	packages/cli/src/ui/components/RewindViewer.test.tsx
#	packages/cli/src/ui/constants/tips.ts
#	packages/cli/src/ui/utils/borderStyles.test.tsx
#	packages/cli/src/utils/processUtils.test.ts
#	packages/cli/src/utils/sessionUtils.ts
#	packages/core/src/commands/memory.test.ts
#	packages/core/src/config/config.ts
#	packages/core/src/config/storage.test.ts
#	packages/core/src/core/__snapshots__/prompts.test.ts.snap
#	packages/core/src/scheduler/tool-executor.ts
#	packages/core/src/services/contextManager.ts
#	packages/core/src/utils/memoryDiscovery.test.ts
#	packages/core/src/utils/memoryDiscovery.ts
#	schemas/settings.schema.json
2026-03-09 18:32:44 -04:00
Keith Guerin 84dfd8b277 trigger e2e 2026-03-02 01:39:11 -08:00
Keith Guerin 5fcfd972ed fix: update evals to use workspace terminology 2026-03-02 01:05:12 -08:00
Keith Guerin 71b4083e96 chore: regenerate settings schema and documentation 2026-03-02 00:20:49 -08:00
Keith Guerin 52f44516c5 fix: resolve build errors, update snapshots, and address review comments 2026-03-01 23:40:29 -08:00
Keith Guerin 9e2407993e address PR comments and linting 2026-03-01 14:30:08 -08:00
Keith Guerin db724097b0 chore: fix remaining lint error in tests 2026-03-01 13:12:14 -08:00
Keith Guerin 043a08807b feat: rename /directory to /workspace and unify terminology
Renames the /directory command to /workspace, adds aliases for backward compatibility, and updates UI strings, settings, and prompts to use 'workspace' terminology consistently.

Linked to #20737. Per Core/UX chat on 2/18.
2026-03-01 09:02:32 -08:00
177 changed files with 2103 additions and 1199 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ they appear in the UI.
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
| Hide Workspace Path | `ui.footer.hideCWD` | Hide the workspace path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window usage percentage. | `true` |
+7 -7
View File
@@ -90,8 +90,8 @@ Slash commands provide meta-level control over the CLI itself.
- **Sub-commands:**
- **`reload`**:
- **Description:** Reload custom command definitions from all sources
(user-level `~/.gemini/commands/`, project-level
`<project>/.gemini/commands/`, MCP prompts, and extensions). Use this to
(user-level `~/.gemini/commands/`, workspace-level
`<workspace>/.gemini/commands/`, MCP prompts, and extensions). Use this to
pick up new or modified `.toml` files without restarting the CLI.
- **Usage:** `/commands reload`
@@ -115,7 +115,7 @@ Slash commands provide meta-level control over the CLI itself.
- On macOS, it requires `pbcopy`, and on Windows, it requires `clip`. These
tools are typically pre-installed on their respective systems.
### `/directory` (or `/dir`)
### `/workspace` (or `/dir`, `/directory`)
- **Description:** Manage workspace directories for multi-directory support.
- **Sub-commands:**
@@ -123,13 +123,13 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Add a directory to the workspace. The path can be
absolute or relative to the current working directory. Moreover, the
reference from home directory is supported as well.
- **Usage:** `/directory add <path1>,<path2>`
- **Usage:** `/workspace add <path1>,<path2>`
- **Note:** Disabled in restrictive sandbox profiles. If you're using that,
use `--include-directories` when starting the session instead.
- **`show`**:
- **Description:** Display all directories added by `/directory add` and
- **Description:** Display all directories added by `/workspace add` and
`--include-directories`.
- **Usage:** `/directory show`
- **Usage:** `/workspace show`
### `/docs`
@@ -303,7 +303,7 @@ Slash commands provide meta-level control over the CLI itself.
### `/restore`
- **Description:** Restores the project files to the state they were in just
- **Description:** Restores the workspace files to the state they were in just
before a tool was executed. This is particularly useful for undoing file edits
made by a tool. If run without a tool call ID, it will list available
checkpoints to restore from.
+15 -13
View File
@@ -13,7 +13,7 @@ overridden by higher numbers):
2. **System defaults file:** System-wide default settings that can be
overridden by other settings files.
3. **User settings file:** Global settings for the current user.
4. **Project settings file:** Project-specific settings.
4. **Workspace settings file:** Workspace-specific settings.
5. **System settings file:** System-wide settings that override all other
settings files.
6. **Environment variables:** System-wide or session-specific variables,
@@ -43,10 +43,11 @@ locations for these files:
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
- **Scope:** Applies to all Gemini CLI sessions for the current user. User
settings override system defaults.
- **Project settings file:**
- **Location:** `.gemini/settings.json` within your project's root directory.
- **Scope:** Applies only when running Gemini CLI from that specific project.
Project settings override user settings and system defaults.
- **Workspace settings file:**
- **Location:** `.gemini/settings.json` within your workspace's root
directory.
- **Scope:** Applies only when running Gemini CLI from that specific
workspace. Workspace settings override user settings and system defaults.
- **System settings file:**
- **Location:** `/etc/gemini-cli/settings.json` (Linux),
`C:\ProgramData\gemini-cli\settings.json` (Windows) or
@@ -70,10 +71,11 @@ this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
> CLI in a corporate environment, please see the
> [Enterprise Configuration](../cli/enterprise.md) documentation.
### The `.gemini` directory in your project
### The `.gemini` directory in your workspace
In addition to a project settings file, a project's `.gemini` directory can
contain other project-specific files related to Gemini CLI's operation, such as:
In addition to a workspace settings file, a workspace's `.gemini` directory can
contain other workspace-specific files related to Gemini CLI's operation, such
as:
- [Custom sandbox profiles](#sandboxing) (e.g.,
`.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
@@ -261,7 +263,7 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **`ui.footer.hideCWD`** (boolean):
- **Description:** Hide the current working directory in the footer.
- **Description:** Hide the workspace path in the footer.
- **Default:** `false`
- **`ui.footer.hideSandboxStatus`** (boolean):
@@ -1303,12 +1305,12 @@ of v0.3.0:
## Shell history
The CLI keeps a history of shell commands you run. To avoid conflicts between
different projects, this history is stored in a project-specific directory
different workspaces, this history is stored in a workspace-specific directory
within your user's home folder.
- **Location:** `~/.gemini/tmp/<project_hash>/shell_history`
- `<project_hash>` is a unique identifier generated from your project's root
path.
- **Location:** `~/.gemini/tmp/<workspace_hash>/shell_history`
- `<workspace_hash>` is a unique identifier generated from your workspace's
root path.
- The history is stored in a file named `shell_history`.
## Environment variables and `.env` files
+9 -9
View File
@@ -21,7 +21,7 @@ describe('Hierarchical Memory', () => {
},
},
// We simulate the hierarchical memory by including the tags in the prompt
// since setting up real global/extension/project files in the eval rig is complex.
// since setting up real global/extension/workspace files in the eval rig is complex.
// The system prompt logic will append these tags when it finds them in userMemory.
prompt: `
<global_context>
@@ -32,9 +32,9 @@ When asked for my favorite fruit, always say "Apple".
When asked for my favorite fruit, always say "Banana".
</extension_context>
<project_context>
<workspace_context>
When asked for my favorite fruit, always say "Cherry".
</project_context>
</workspace_context>
What is my favorite fruit? Tell me just the name of the fruit.`,
assert: async (rig) => {
@@ -65,23 +65,23 @@ Instruction A: Always be helpful.
Instruction B: Use a professional tone.
</extension_context>
<project_context>
Instruction C: Adhere to the project's coding style.
</project_context>
<workspace_context>
Instruction C: Adhere to the workspace's coding style.
</workspace_context>
Which instruction came from the global context, which from the extension context, and which from the project context?
Which instruction came from the global context, which from the extension context, and which from the workspace context?
Provide the answer as an XML block like this:
<results>
<global>Instruction ...</global>
<extension>Instruction ...</extension>
<project>Instruction ...</project>
<workspace>Instruction ...</workspace>
</results>`,
assert: async (rig) => {
const stdout = rig._lastRunStdout!;
assertModelHasOutput(stdout);
expect(stdout).toMatch(/<global>.*Instruction A/i);
expect(stdout).toMatch(/<extension>.*Instruction B/i);
expect(stdout).toMatch(/<project>.*Instruction C/i);
expect(stdout).toMatch(/<workspace>.*Instruction C/i);
},
});
+2 -2
View File
@@ -95,10 +95,10 @@ export function loadSettings(workspaceDir: string): Settings {
// Load workspace settings
try {
if (fs.existsSync(workspaceSettingsPath)) {
const projectContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
const workspaceContent = fs.readFileSync(workspaceSettingsPath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parsedWorkspaceSettings = JSON.parse(
stripJsonComments(projectContent),
stripJsonComments(workspaceContent),
) as Settings;
workspaceSettings = resolveEnvVarsInObject(parsedWorkspaceSettings);
}
+1 -1
View File
@@ -1160,7 +1160,7 @@ export class Session {
resolvedSuccessfully = true;
} else {
this.debug(
`Path ${pathName} is outside the project directory. Skipping.`,
`Path ${pathName} is outside the workspace directory. Skipping.`,
);
}
} catch (error) {
+1 -1
View File
@@ -89,7 +89,7 @@ describe('GeminiAgent Session Resume', () => {
getChat: vi.fn().mockReturnValue({}),
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/workspace'),
},
getApprovalMode: vi.fn().mockReturnValue('default'),
isPlanEnabled: vi.fn().mockReturnValue(true),
+4 -4
View File
@@ -41,7 +41,7 @@ async function addMcpServer(
const settings = loadSettings(process.cwd());
const inHome = settings.workspace.path === settings.user.path;
if (scope === 'project' && inHome) {
if ((scope === 'workspace' || scope === 'project') && inHome) {
debugLogger.error(
'Error: Please use --scope user to edit settings in the home directory.',
);
@@ -159,10 +159,10 @@ export const addCommand: CommandModule = {
})
.option('scope', {
alias: 's',
describe: 'Configuration scope (user or project)',
describe: 'Configuration scope (user or workspace)',
type: 'string',
default: 'project',
choices: ['user', 'project'],
default: 'workspace',
choices: ['user', 'workspace', 'project'],
})
.option('transport', {
alias: ['t', 'type'],
+2 -2
View File
@@ -99,7 +99,7 @@ describe('mcp remove command', () => {
expect(mockSetValue).not.toHaveBeenCalled();
expect(debugLogSpy).toHaveBeenCalledWith(
'Server "non-existent-server" not found in project settings.',
'Server "non-existent-server" not found in workspace settings.',
);
debugLogSpy.mockRestore();
});
@@ -159,7 +159,7 @@ describe('mcp remove command', () => {
expect(updatedContent).not.toContain('"server-to-remove"');
expect(debugLogSpy).toHaveBeenCalledWith(
'Server "server-to-remove" removed from project settings.',
'Server "server-to-remove" removed from workspace settings.',
);
debugLogSpy.mockRestore();
+3 -3
View File
@@ -49,10 +49,10 @@ export const removeCommand: CommandModule = {
})
.option('scope', {
alias: 's',
describe: 'Configuration scope (user or project)',
describe: 'Configuration scope (user or workspace)',
type: 'string',
default: 'project',
choices: ['user', 'project'],
default: 'workspace',
choices: ['user', 'workspace', 'project'],
}),
handler: async (argv) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+1 -1
View File
@@ -60,7 +60,7 @@ describe('validateAuthMethod', () => {
'should return null for USE_VERTEX_AI if GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION are set',
authType: AuthType.USE_VERTEX_AI,
envs: {
GOOGLE_CLOUD_PROJECT: 'test-project',
GOOGLE_CLOUD_PROJECT: 'test-workspace',
GOOGLE_CLOUD_LOCATION: 'test-location',
},
expected: null,
+3 -3
View File
@@ -51,7 +51,7 @@ vi.mock('fs', async (importOriginal) => {
const pathMod = await import('node:path');
const mockHome = pathMod.resolve(pathMod.sep, 'mock', 'home', 'user');
const MOCK_CWD1 = process.cwd();
const MOCK_CWD2 = pathMod.resolve(pathMod.sep, 'home', 'user', 'project');
const MOCK_CWD2 = pathMod.resolve(pathMod.sep, 'home', 'user', 'workspace');
const mockPaths = new Set([
MOCK_CWD1,
@@ -1890,7 +1890,7 @@ describe('loadCliConfig with includeDirectories', () => {
);
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
vi.spyOn(process, 'cwd').mockReturnValue(
path.resolve(path.sep, 'home', 'user', 'project'),
path.resolve(path.sep, 'home', 'user', 'workspace'),
);
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
});
@@ -1900,7 +1900,7 @@ describe('loadCliConfig with includeDirectories', () => {
});
it.skip('should combine and resolve paths from settings and CLI arguments', async () => {
const mockCwd = path.resolve(path.sep, 'home', 'user', 'project');
const mockCwd = path.resolve(path.sep, 'home', 'user', 'workspace');
process.argv = [
'node',
+1 -1
View File
@@ -243,7 +243,7 @@ export async function parseArguments(
.option('list-sessions', {
type: 'boolean',
description:
'List available sessions for the current project and exit.',
'List available sessions for the current workspace and exit.',
})
.option('delete-session', {
type: 'string',
@@ -7,6 +7,7 @@
import React from 'react';
import { Text } from 'ink';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import '../../test-utils/customMatchers.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
@@ -412,8 +412,8 @@ describe('Policy Engine Integration Tests', () => {
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
'/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
'C:\\Users\\user\\.gemini\\tmp\\project-id\\session-id\\plans\\plan.md',
'D:\\gemini-cli\\.gemini\\tmp\\project-id\\session-1\\plans\\plan.md', // no session ID
'C:\\Users\\user\\.gemini\\tmp\\workspace-id\\session-id\\plans\\plan.md',
'D:\\gemini-cli\\.gemini\\tmp\\workspace-id\\session-1\\plans\\plan.md', // no session ID
];
for (const file_path of validPaths) {
@@ -437,7 +437,7 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
const invalidPaths = [
'/project/src/file.ts', // Workspace
'/workspace/src/file.ts', // Workspace
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal (Unix)
'C:\\Users\\user\\.gemini\\tmp\\id\\session\\plans\\..\\..\\..\\Windows\\System32\\config\\SAM', // Path traversal (Windows)
+9 -7
View File
@@ -1666,7 +1666,7 @@ describe('Settings Loading and Merging', () => {
process.env = originalEnv;
});
it('should exclude DEBUG and DEBUG_MODE from project .env files by default', () => {
it('should exclude DEBUG and DEBUG_MODE from workspace .env files by default', () => {
// Create a workspace settings file with excludedProjectEnvVars
const workspaceSettingsContent = {
general: {},
@@ -1686,18 +1686,18 @@ describe('Settings Loading and Merging', () => {
},
);
// Mock findEnvFile to return a project .env file
// Mock findEnvFile to return a workspace .env file
const originalFindEnvFile = (
loadSettings as unknown as { findEnvFile: () => string }
).findEnvFile;
(loadSettings as unknown as { findEnvFile: () => string }).findEnvFile =
() => path.resolve('/mock/project/.env');
() => '/mock/workspace/.env';
// Mock fs.readFileSync for .env file content
const originalReadFileSync = fs.readFileSync;
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === path.resolve('/mock/project/.env')) {
if (p === '/mock/workspace/.env') {
return 'DEBUG=true\nDEBUG_MODE=1\nGEMINI_API_KEY=test-key';
}
if (
@@ -3080,7 +3080,9 @@ MALICIOUS_VAR=allowed-because-trusted
it('should sanitize value in sanitizeEnvVar helper', () => {
expect(sanitizeEnvVar('$(calc)')).toBe('calc');
expect(sanitizeEnvVar('`rm -rf /`')).toBe('rm-rf/');
expect(sanitizeEnvVar('normal-project-123')).toBe('normal-project-123');
expect(sanitizeEnvVar('normal-workspace-123')).toBe(
'normal-workspace-123',
);
expect(sanitizeEnvVar('us-central1')).toBe('us-central1');
});
});
@@ -3114,7 +3116,7 @@ MALICIOUS_VAR=allowed-because-trusted
});
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
'GOOGLE_CLOUD_PROJECT=attacker-project;inject',
'GOOGLE_CLOUD_PROJECT=attacker-workspace;inject',
);
loadEnvironment(
@@ -3123,7 +3125,7 @@ MALICIOUS_VAR=allowed-because-trusted
);
expect(process.env['GOOGLE_CLOUD_PROJECT']).toBe(
'attacker-projectinject',
'attacker-workspaceinject',
);
});
});
+2 -2
View File
@@ -588,11 +588,11 @@ const SETTINGS_SCHEMA = {
},
hideCWD: {
type: 'boolean',
label: 'Hide CWD',
label: 'Hide Workspace Path',
category: 'UI',
requiresRestart: false,
default: false,
description: 'Hide the current working directory in the footer.',
description: 'Hide the workspace path in the footer.',
showInDialog: true,
},
hideSandboxStatus: {
+24 -22
View File
@@ -272,30 +272,30 @@ describe('Trusted Folders', () => {
};
it('should return true for a directly trusted folder', () => {
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
const config = { '/workspaceA': TrustLevel.TRUST_FOLDER };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
isTrusted: true,
source: 'file',
});
});
it('should return true for a child of a trusted folder', () => {
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
const config = { '/workspaceA': TrustLevel.TRUST_FOLDER };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
expect(isWorkspaceTrusted(mockSettings, '/projectA/src')).toEqual({
expect(isWorkspaceTrusted(mockSettings, '/workspaceA/src')).toEqual({
isTrusted: true,
source: 'file',
});
});
it('should return true for a child of a trusted parent folder', () => {
const config = { '/projectB/somefile.txt': TrustLevel.TRUST_PARENT };
const config = { '/workspaceB/somefile.txt': TrustLevel.TRUST_PARENT };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
expect(isWorkspaceTrusted(mockSettings, '/projectB')).toEqual({
expect(isWorkspaceTrusted(mockSettings, '/workspaceB')).toEqual({
isTrusted: true,
source: 'file',
});
@@ -329,20 +329,22 @@ describe('Trusted Folders', () => {
it('should prioritize specific distrust over parent trust', () => {
const config = {
'/projectA': TrustLevel.TRUST_FOLDER,
'/projectA/untrusted': TrustLevel.DO_NOT_TRUST,
'/workspaceA': TrustLevel.TRUST_FOLDER,
'/workspaceA/untrusted': TrustLevel.DO_NOT_TRUST,
};
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
expect(isWorkspaceTrusted(mockSettings, '/projectA/untrusted')).toEqual({
isTrusted: false,
source: 'file',
});
expect(isWorkspaceTrusted(mockSettings, '/workspaceA/untrusted')).toEqual(
{
isTrusted: false,
source: 'file',
},
);
});
it('should use workspaceDir instead of process.cwd() when provided', () => {
const config = {
'/projectA': TrustLevel.TRUST_FOLDER,
'/workspaceA': TrustLevel.TRUST_FOLDER,
'/untrusted': TrustLevel.DO_NOT_TRUST,
};
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
@@ -350,18 +352,18 @@ describe('Trusted Folders', () => {
vi.spyOn(process, 'cwd').mockImplementation(() => '/untrusted');
// process.cwd() is untrusted, but workspaceDir is trusted
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
isTrusted: true,
source: 'file',
});
});
it('should handle path normalization', () => {
const config = { '/home/user/projectA': TrustLevel.TRUST_FOLDER };
const config = { '/home/user/workspaceA': TrustLevel.TRUST_FOLDER };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
expect(
isWorkspaceTrusted(mockSettings, '/home/user/../user/projectA'),
isWorkspaceTrusted(mockSettings, '/home/user/../user/workspaceA'),
).toEqual({
isTrusted: true,
source: 'file',
@@ -369,13 +371,13 @@ describe('Trusted Folders', () => {
});
it('should prioritize IDE override over file config', () => {
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
const config = { '/workspaceA': TrustLevel.DO_NOT_TRUST };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
ideContextStore.set({ workspaceState: { isTrusted: true } });
try {
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
isTrusted: true,
source: 'ide',
});
@@ -385,13 +387,13 @@ describe('Trusted Folders', () => {
});
it('should return false when IDE override is false', () => {
const config = { '/projectA': TrustLevel.TRUST_FOLDER };
const config = { '/workspaceA': TrustLevel.TRUST_FOLDER };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
ideContextStore.set({ workspaceState: { isTrusted: false } });
try {
expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
expect(isWorkspaceTrusted(mockSettings, '/workspaceA')).toEqual({
isTrusted: false,
source: 'ide',
});
@@ -442,10 +444,10 @@ describe('Trusted Folders', () => {
const geminiCore = await import('@google/gemini-cli-core');
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(false);
const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
const config = { '/workspaceA': TrustLevel.DO_NOT_TRUST };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
expect(isWorkspaceTrusted(mockSettings, '/projectA').isTrusted).toBe(
expect(isWorkspaceTrusted(mockSettings, '/workspaceA').isTrusted).toBe(
false,
);
});
+8 -2
View File
@@ -4,7 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
beforeEach,
describe,
expect,
it,
type MockInstance,
vi,
} from 'vitest';
import {
runDeferredCommand,
defer,
@@ -14,7 +21,6 @@ import {
import { ExitCodes } from '@google/gemini-cli-core';
import type { ArgumentsCamelCase, CommandModule } from 'yargs';
import { createMockSettings } from './test-utils/settings.js';
import type { MockInstance } from 'vitest';
const { mockRunExitCleanup, mockCoreEvents } = vi.hoisted(() => ({
mockRunExitCleanup: vi.fn(),
+1 -1
View File
@@ -1646,7 +1646,7 @@ describe('runNonInteractive', () => {
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
firstUserMessage: 'Previous message',
projectHash: 'test-hash',
workspaceHash: 'test-hash',
},
filePath: '/path/to/session.json',
};
@@ -29,7 +29,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 { directoryCommand } from '../ui/commands/directoryCommand.js';
import { workspaceCommand } from '../ui/commands/workspaceCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { footerCommand } from '../ui/commands/footerCommand.js';
@@ -131,7 +131,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
copyCommand,
corgiCommand,
docsCommand,
directoryCommand,
workspaceCommand,
editorCommand,
...(this.config?.getExtensionsEnabled() === false
? [
@@ -10,7 +10,15 @@ import type { Config } from '@google/gemini-cli-core';
import { GEMINI_DIR, Storage } from '@google/gemini-cli-core';
import mock from 'mock-fs';
import { FileCommandLoader } from './FileCommandLoader.js';
import { assert, vi } from 'vitest';
import {
assert,
vi,
describe,
it,
expect,
beforeEach,
afterEach,
} from 'vitest';
import { createMockCommandContext } from '../test-utils/mockCommandContext.js';
import {
SHELL_INJECTION_TRIGGER,
@@ -230,7 +238,7 @@ describe('FileCommandLoader', () => {
},
});
const mockConfig = {
getProjectRoot: vi.fn(() => '/path/to/project'),
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => false),
isTrustedFolder: vi.fn(() => false),
@@ -260,22 +268,22 @@ describe('FileCommandLoader', () => {
expect(command.name).toBe('git:commit');
});
it('returns both user and project commands in order', async () => {
it('returns both user and workspace commands in order', async () => {
const userCommandsDir = Storage.getUserCommandsDir();
const projectCommandsDir = new Storage(
const workspaceCommandsDir = new Storage(
process.cwd(),
).getProjectCommandsDir();
mock({
[userCommandsDir]: {
'test.toml': 'prompt = "User prompt"',
},
[projectCommandsDir]: {
[workspaceCommandsDir]: {
'test.toml': 'prompt = "Project prompt"',
},
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => false),
isTrustedFolder: vi.fn(() => false),
@@ -299,7 +307,7 @@ describe('FileCommandLoader', () => {
} else {
assert.fail('Incorrect action type for user command');
}
const projectResult = await commands[1].action?.(
const workspaceResult = await commands[1].action?.(
createMockCommandContext({
invocation: {
raw: '/test',
@@ -309,10 +317,10 @@ describe('FileCommandLoader', () => {
}),
'',
);
if (projectResult?.type === 'submit_prompt') {
expect(projectResult.content).toEqual([{ text: 'Project prompt' }]);
if (workspaceResult?.type === 'submit_prompt') {
expect(workspaceResult.content).toEqual([{ text: 'Project prompt' }]);
} else {
assert.fail('Incorrect action type for project command');
assert.fail('Incorrect action type for workspace command');
}
});
@@ -532,7 +540,7 @@ describe('FileCommandLoader', () => {
describe('Extension Command Loading', () => {
it('loads commands from active extensions', async () => {
const userCommandsDir = Storage.getUserCommandsDir();
const projectCommandsDir = new Storage(
const workspaceCommandsDir = new Storage(
process.cwd(),
).getProjectCommandsDir();
const extensionDir = path.join(
@@ -546,8 +554,8 @@ describe('FileCommandLoader', () => {
[userCommandsDir]: {
'user.toml': 'prompt = "User command"',
},
[projectCommandsDir]: {
'project.toml': 'prompt = "Project command"',
[workspaceCommandsDir]: {
'workspace.toml': 'prompt = "Project command"',
},
[extensionDir]: {
'gemini-extension.json': JSON.stringify({
@@ -561,7 +569,7 @@ describe('FileCommandLoader', () => {
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'test-ext',
@@ -578,7 +586,7 @@ describe('FileCommandLoader', () => {
expect(commands).toHaveLength(3);
const commandNames = commands.map((cmd) => cmd.name);
expect(commandNames).toEqual(['user', 'project', 'ext']);
expect(commandNames).toEqual(['user', 'workspace', 'ext']);
const extCommand = commands.find((cmd) => cmd.name === 'ext');
expect(extCommand?.extensionName).toBe('test-ext');
@@ -587,7 +595,7 @@ describe('FileCommandLoader', () => {
it('extension commands have extensionName metadata for conflict resolution', async () => {
const userCommandsDir = Storage.getUserCommandsDir();
const projectCommandsDir = new Storage(
const workspaceCommandsDir = new Storage(
process.cwd(),
).getProjectCommandsDir();
const extensionDir = path.join(
@@ -610,13 +618,13 @@ describe('FileCommandLoader', () => {
[userCommandsDir]: {
'deploy.toml': 'prompt = "User deploy command"',
},
[projectCommandsDir]: {
[workspaceCommandsDir]: {
'deploy.toml': 'prompt = "Project deploy command"',
},
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'test-ext',
@@ -723,7 +731,7 @@ describe('FileCommandLoader', () => {
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'active-ext',
@@ -769,7 +777,7 @@ describe('FileCommandLoader', () => {
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'no-commands',
@@ -813,7 +821,7 @@ describe('FileCommandLoader', () => {
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{ name: 'a', version: '1.0.0', isActive: true, path: extensionDir },
]),
@@ -874,7 +882,7 @@ describe('FileCommandLoader', () => {
});
const mockConfig = {
getProjectRoot: vi.fn(() => process.cwd()),
getWorkspaceRoot: vi.fn(() => process.cwd()),
getExtensions: vi.fn(() => [
{
name: 'my-test-ext',
@@ -1256,7 +1264,7 @@ describe('FileCommandLoader', () => {
describe('with folder trust enabled', () => {
it('loads multiple commands', async () => {
const mockConfig = {
getProjectRoot: vi.fn(() => '/path/to/project'),
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => true),
isTrustedFolder: vi.fn(() => true),
@@ -1277,7 +1285,7 @@ describe('FileCommandLoader', () => {
it('does not load when folder is not trusted', async () => {
const mockConfig = {
getProjectRoot: vi.fn(() => '/path/to/project'),
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => true),
isTrustedFolder: vi.fn(() => false),
@@ -1307,7 +1315,7 @@ describe('FileCommandLoader', () => {
.mockImplementation(() => {});
const mockConfig = {
getProjectRoot: vi.fn(() => '/path/to/project'),
getWorkspaceRoot: vi.fn(() => '/path/to/workspace'),
getExtensions: vi.fn(() => []),
getFolderTrust: vi.fn(() => false),
isTrustedFolder: vi.fn(() => false),
@@ -56,7 +56,7 @@ const TomlCommandDefSchema = z.object({
/**
* Discovers and loads custom slash commands from .toml files in both the
* user's global config directory and the current project's directory.
* user's global config directory and the current workspace's directory.
*
* This loader is responsible for:
* - Recursively scanning command directories.
@@ -65,22 +65,22 @@ const TomlCommandDefSchema = z.object({
* - Handling file system errors and malformed files gracefully.
*/
export class FileCommandLoader implements ICommandLoader {
private readonly projectRoot: string;
private readonly workspaceRoot: string;
private readonly folderTrustEnabled: boolean;
private readonly isTrustedFolder: boolean;
constructor(private readonly config: Config | null) {
this.folderTrustEnabled = !!config?.getFolderTrust();
this.isTrustedFolder = !!config?.isTrustedFolder();
this.projectRoot = config?.getProjectRoot() || process.cwd();
this.workspaceRoot = config?.getWorkspaceRoot() || process.cwd();
}
/**
* Loads all commands from user, project, and extension directories.
* Returns commands in order: user project extensions (alphabetically).
* Loads all commands from user, workspace, and extension directories.
* Returns commands in order: user workspace extensions (alphabetically).
*
* Order is important for conflict resolution in CommandService:
* - User/project commands (without extensionName) use "last wins" strategy
* - User/workspace commands (without extensionName) use "last wins" strategy
* - Extension commands (with extensionName) get renamed if conflicts exist
*
* @param signal An AbortSignal to cancel the loading process.
@@ -150,7 +150,7 @@ export class FileCommandLoader implements ICommandLoader {
private getCommandDirectories(): CommandDirectory[] {
const dirs: CommandDirectory[] = [];
const storage = this.config?.storage ?? new Storage(this.projectRoot);
const storage = this.config?.storage ?? new Storage(this.workspaceRoot);
// 1. User commands
dirs.push({
@@ -158,9 +158,9 @@ export class FileCommandLoader implements ICommandLoader {
kind: CommandKind.USER_FILE,
});
// 2. Project commands
// 2. Workspace commands (override user commands)
dirs.push({
path: storage.getProjectCommandsDir(),
path: storage.getWorkspaceCommandsDir(),
kind: CommandKind.WORKSPACE_FILE,
});
@@ -40,7 +40,7 @@ describe('createMockCommandContext', () => {
it('should apply deeply nested overrides correctly', () => {
// This is the most important test for factory's logic.
const mockConfig = {
getProjectRoot: () => '/test/project',
getWorkspaceRoot: () => '/test/workspace',
getModel: () => 'gemini-pro',
};
@@ -54,7 +54,7 @@ describe('createMockCommandContext', () => {
expect(context.services.config).toBeDefined();
expect(context.services.config?.getModel()).toBe('gemini-pro');
expect(context.services.config?.getProjectRoot()).toBe('/test/project');
expect(context.services.config?.getWorkspaceRoot()).toBe('/test/workspace');
// Verify a default property on the same nested object is still there
expect(context.services.logger).toBeDefined();
+1 -1
View File
@@ -21,7 +21,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
isInitialized: vi.fn(() => true),
setTerminalBackground: vi.fn(),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
initialize: vi.fn().mockResolvedValue(undefined),
},
getDebugMode: vi.fn(() => false),
+3 -3
View File
@@ -1107,7 +1107,7 @@ describe('AppContainer State Management', () => {
const mockResumedSessionData = {
conversation: {
sessionId: 'test-session-123',
projectHash: 'test-project-hash',
workspaceHash: 'test-workspace-hash',
startTime: '2024-01-01T00:00:00Z',
lastUpdated: '2024-01-01T00:00:01Z',
messages: [
@@ -1296,7 +1296,7 @@ describe('AppContainer State Management', () => {
const resumedData = {
conversation: {
sessionId: 'resumed-session-456',
projectHash: 'project-hash',
workspaceHash: 'workspace-hash',
startTime: '2024-01-01T00:00:00Z',
lastUpdated: '2024-01-01T00:01:00Z',
messages: [
@@ -1352,7 +1352,7 @@ describe('AppContainer State Management', () => {
const resumedData = {
conversation: {
sessionId: 'test-session',
projectHash: 'project-hash',
workspaceHash: 'workspace-hash',
startTime: '2024-01-01T00:00:00Z',
lastUpdated: '2024-01-01T00:01:00Z',
messages: [],
+1 -1
View File
@@ -218,7 +218,7 @@ export function AuthDialog({
</Text>
<Box marginTop={1}>
<Text color={theme.text.primary}>
How would you like to authenticate for this project?
How would you like to authenticate for this workspace?
</Text>
</Box>
<Box marginTop={1}>
@@ -5,7 +5,7 @@ exports[`AuthDialog > Snapshots > renders correctly with auth error 1`] = `
│ │
│ ? Get started │
│ │
│ How would you like to authenticate for this project?
│ How would you like to authenticate for this workspace?
│ │
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
│ │
@@ -26,7 +26,7 @@ exports[`AuthDialog > Snapshots > renders correctly with default props 1`] = `
│ │
│ ? Get started │
│ │
│ How would you like to authenticate for this project?
│ How would you like to authenticate for this workspace?
│ │
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
│ │
@@ -45,7 +45,7 @@ exports[`AuthDialog > Snapshots > renders correctly with enforced auth type 1`]
│ │
│ ? Get started │
│ │
│ How would you like to authenticate for this project?
│ How would you like to authenticate for this workspace?
│ │
│ (selected) Use Gemini API Key │
│ │
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { clearCommand } from './clearCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { copyCommand } from './copyCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { MockInstance } from 'vitest';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type MockInstance,
vi,
} from 'vitest';
import { ideCommand } from './ideCommand.js';
import { type CommandContext } from './types.js';
import { IDE_DEFINITIONS } from '@google/gemini-cli-core';
@@ -78,7 +78,7 @@ describe('initCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Empty GEMINI.md created. Now analyzing the project to populate it.',
text: 'Empty GEMINI.md created. Now analyzing the workspace to populate it.',
},
expect.any(Number),
);
+2 -2
View File
@@ -16,7 +16,7 @@ import { performInit } from '@google/gemini-cli-core';
export const initCommand: SlashCommand = {
name: 'init',
description: 'Analyzes the project and creates a tailored GEMINI.md file',
description: 'Analyzes the workspace and creates a tailored GEMINI.md file',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (
@@ -42,7 +42,7 @@ export const initCommand: SlashCommand = {
context.ui.addItem(
{
type: 'info',
text: 'Empty GEMINI.md created. Now analyzing the project to populate it.',
text: 'Empty GEMINI.md created. Now analyzing the workspace to populate it.',
},
Date.now(),
);
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { memoryCommand } from './memoryCommand.js';
import type { SlashCommand, CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
@@ -153,7 +153,7 @@ describe('restoreCommand', () => {
});
});
it('should restore a tool call and project state', async () => {
it('should restore a tool call and workspace state', async () => {
const toolCallData = {
history: [{ type: 'user', text: 'do a thing', id: 123 }],
clientHistory: [{ role: 'user', parts: [{ text: 'do a thing' }] }],
@@ -181,7 +181,7 @@ describe('restoreCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Restored project to the state before the tool call.',
text: 'Restored workspace to the state before the tool call.',
},
expect.any(Number),
);
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { describe, expect, it, type vi } from 'vitest';
import { toolsCommand } from './toolsCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
@@ -4,9 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import { directoryCommand } from './directoryCommand.js';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { workspaceCommand } from './workspaceCommand.js';
import {
expandHomeDir,
getDirectorySuggestions,
@@ -38,14 +45,14 @@ vi.mock('../utils/directoryUtils.js', async (importOriginal) => {
};
});
describe('directoryCommand', () => {
describe('workspaceCommand', () => {
let mockContext: CommandContext;
let mockConfig: Config;
let mockWorkspaceContext: WorkspaceContext;
const addCommand = directoryCommand.subCommands?.find(
const addCommand = workspaceCommand.subCommands?.find(
(c) => c.name === 'add',
);
const showCommand = directoryCommand.subCommands?.find(
const showCommand = workspaceCommand.subCommands?.find(
(c) => c.name === 'show',
);
@@ -126,7 +133,7 @@ describe('directoryCommand', () => {
type: 'message',
messageType: 'error',
content:
'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
'The /workspace add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
});
});
@@ -77,9 +77,9 @@ async function finishAddingDirectories(
}
}
export const directoryCommand: SlashCommand = {
name: 'directory',
altNames: ['dir'],
export const workspaceCommand: SlashCommand = {
name: 'workspace',
altNames: ['directory', 'dir'],
description: 'Manage workspace directories',
kind: CommandKind.BUILT_IN,
subCommands: [
@@ -156,7 +156,7 @@ export const directoryCommand: SlashCommand = {
type: 'message' as const,
messageType: 'error' as const,
content:
'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
'The /workspace add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.',
};
}
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { render } from '../../test-utils/render.js';
import { AnsiOutputText } from './AnsiOutput.js';
import type { AnsiOutput, AnsiToken } from '@google/gemini-cli-core';
@@ -296,17 +296,17 @@ describe('FolderTrustDialog', () => {
describe('directory display', () => {
it('should correctly display the folder name for a nested directory', async () => {
mockedCwd.mockReturnValue('/home/user/project');
mockedCwd.mockReturnValue('/home/user/workspace');
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<FolderTrustDialog onSelect={vi.fn()} />,
);
await waitUntilReady();
expect(lastFrame()).toContain('Trust folder (project)');
expect(lastFrame()).toContain('Trust folder (workspace)');
unmount();
});
it('should correctly display the parent folder name for a nested directory', async () => {
mockedCwd.mockReturnValue('/home/user/project');
mockedCwd.mockReturnValue('/home/user/workspace');
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<FolderTrustDialog onSelect={vi.fn()} />,
);
@@ -1,16 +1,15 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it, vi } from 'vitest';
import React, { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
import { Text } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js';
import { StreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js';
import { vi } from 'vitest';
import * as useTerminalSize from '../hooks/useTerminalSize.js';
// Mock GeminiRespondingSpinner
@@ -72,7 +72,8 @@ export const NewAgentsNotification = ({
New Agents Discovered
</Text>
<Text color={theme.text.primary}>
The following agents were found in this project. Please review them:
The following agents were found in this workspace. Please review
them:
</Text>
<Box
flexDirection="column"
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
import { RewindViewer } from './RewindViewer.js';
@@ -60,7 +60,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const createConversation = (messages: MessageRecord[]): ConversationRecord => ({
sessionId: 'test-session',
projectHash: 'hash',
workspaceHash: 'hash',
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
messages,
@@ -1,3 +1,8 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @license
* Copyright 2025 Google LLC
@@ -22,7 +27,8 @@
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import '../../test-utils/customMatchers.js';
import { SettingsDialog } from './SettingsDialog.js';
import { SettingScope } from '../../config/settings.js';
import { createMockSettings } from '../../test-utils/settings.js';
@@ -1,9 +1,16 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../test-utils/customMatchers.js';
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { render } from '../../test-utils/render.js';
import { Table } from './Table.js';
import { Text } from 'ink';
@@ -4,7 +4,7 @@ exports[`NewAgentsNotification > renders agent list 1`] = `
" ╭────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ New Agents Discovered │
│ The following agents were found in this project. Please review them:
│ The following agents were found in this workspace. Please review them: │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
@@ -24,7 +24,7 @@ exports[`NewAgentsNotification > truncates list if more than 5 agents 1`] = `
" ╭────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ New Agents Discovered │
│ The following agents were found in this project. Please review them:
│ The following agents were found in this workspace. Please review them: │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
@@ -4,12 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import { waitFor } from '../../../test-utils/async.js';
import { DiffRenderer } from './DiffRenderer.js';
import * as CodeColorizer from '../../utils/CodeColorizer.js';
import { vi } from 'vitest';
describe('<OverflowProvider><DiffRenderer /></OverflowProvider>', () => {
const mockColorizeCode = vi.spyOn(CodeColorizer, 'colorizeCode');
@@ -1,9 +1,9 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { GeminiMessage } from './GeminiMessage.js';
import { StreamingState } from '../../types.js';
import { renderWithProviders } from '../../../test-utils/render.js';
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { describe, it, expect, beforeAll, vi } from 'vitest';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import type {
SerializableConfirmationDetails,
@@ -5,8 +5,8 @@
*/
import type React from 'react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
import { describe, it, expect, vi } from 'vitest';
import { StreamingState } from '../../types.js';
import { Text } from 'ink';
import {
@@ -5,6 +5,7 @@
*/
import { describe, it, expect } from 'vitest';
import '../../../test-utils/customMatchers.js';
import { render } from '../../../test-utils/render.js';
import { ExpandableText, MAX_WIDTH } from './ExpandableText.js';
@@ -5,6 +5,7 @@
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import '../../../test-utils/customMatchers.js';
import stripAnsi from 'strip-ansi';
import { act } from 'react';
import * as fs from 'node:fs';
@@ -5,6 +5,7 @@
*/
import { describe, it, expect } from 'vitest';
import '../../../test-utils/customMatchers.js';
import { handleVimAction } from './vim-buffer-actions.js';
import type { TextBufferState, VisualLayout } from './text-buffer.js';
+7 -7
View File
@@ -19,7 +19,7 @@ export const INFORMATIVE_TIPS = [
'Hide the startup banner for a cleaner launch (/settings)…',
'Hide the context summary above the input (/settings)…',
'Reclaim vertical space by hiding the footer (/settings)…',
'Hide individual footer elements like CWD or sandbox status (/settings)…',
'Hide individual footer elements like Workspace Path or sandbox status (/settings)…',
'Hide the context window percentage in the footer (/settings)…',
'Show memory usage for performance monitoring (/settings)…',
'Show line numbers in the chat for easier reference (/settings)…',
@@ -33,7 +33,7 @@ export const INFORMATIVE_TIPS = [
'Control when chat history gets compressed based on context compression threshold (settings.json)…',
'Define custom context file names, like CONTEXT.md (settings.json)…',
'Set max directories to scan for context files (/settings)…',
'Expand your workspace with additional directories (/directory)…',
'Expand your workspace with additional directories (/workspace)…',
'Control how /memory reload loads context files (/settings)…',
'Toggle respect for .gitignore files in context (/settings)…',
'Toggle respect for .geminiignore files in context (/settings)…',
@@ -131,15 +131,15 @@ export const INFORMATIVE_TIPS = [
'Save tokens by summarizing the context with /compress…',
'Copy the last response to your clipboard with /copy…',
'Open the full documentation in your browser with /docs…',
'Add directories to your workspace with /directory add <path>…',
'Show all directories in your workspace with /directory show…',
'Use /dir as a shortcut for /directory…',
'Add directories to your workspace with /workspace add <path>…',
'Show all directories in your workspace with /workspace show…',
'Use /dir or /directory as a shortcut for /workspace…',
'Set your preferred external editor with /editor…',
'List all active extensions with /extensions list…',
'Update all or specific extensions with /extensions update…',
'Get help on commands with /help…',
'Manage IDE integration with /ide…',
'Create a project-specific GEMINI.md file with /init…',
'Create a workspace-specific GEMINI.md file with /init…',
'List configured MCP servers and tools with /mcp list…',
'Authenticate with an OAuth-enabled MCP server with /mcp auth…',
'Reload MCP servers with /mcp reload…',
@@ -149,7 +149,7 @@ export const INFORMATIVE_TIPS = [
'List the paths of the GEMINI.md files in use with /memory list…',
'Choose your Gemini model with /model…',
'Display the privacy notice with /privacy…',
'Restore project files to a previous state with /restore…',
'Restore workspace files to a previous state with /restore…',
'Exit the CLI with /quit or /exit…',
'Check model-specific usage stats with /stats model…',
'Check tool-specific usage stats with /stats tools…',
@@ -7,10 +7,19 @@
import { debugLogger } from '@google/gemini-cli-core';
import type React from 'react';
import { act } from 'react';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import type { Mock } from 'vitest';
import { vi, afterAll, beforeAll } from 'vitest';
import type { Key } from './KeypressContext.js';
import {
KeypressProvider,
@@ -4,10 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { act } from 'react';
import { MouseProvider, useMouseContext, useMouse } from './MouseContext.js';
import { vi, type Mock } from 'vitest';
import type React from 'react';
import { useStdin } from 'ink';
import { EventEmitter } from 'node:events';
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { handleAtCommand } from './atCommandProcessor.js';
import type { Config, DiscoveredMCPResource } from '@google/gemini-cli-core';
import {
@@ -108,7 +115,7 @@ describe('handleAtCommand', () => {
const workspaceDirs = this.getWorkspaceContext().getDirectories();
const projectTempDir = this.storage.getProjectTempDir();
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the workspace temp directory: ${projectTempDir}`;
},
getMcpServers: () => ({}),
getMcpServerCommand: () => undefined,
@@ -108,7 +108,7 @@ describe('handleAtCommand with Agents', () => {
const workspaceDirs = this.getWorkspaceContext().getDirectories();
const projectTempDir = this.storage.getProjectTempDir();
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
return `Path validation failed: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the workspace temp directory: ${projectTempDir}`;
},
getMcpServers: () => ({}),
getMcpServerCommand: () => undefined,
@@ -5,7 +5,7 @@
*/
import { act, useCallback } from 'react';
import { vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from '../../test-utils/render.js';
import { useConsoleMessages } from './useConsoleMessages.js';
import { CoreEvent, type ConsoleLogPayload } from '@google/gemini-cli-core';
@@ -4,8 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { vi, type Mock } from 'vitest';
import { useFlickerDetector } from './useFlickerDetector.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { recordFlickerFrame } from '@google/gemini-cli-core';
+9 -1
View File
@@ -4,10 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { render } from '../../test-utils/render.js';
import { EventEmitter } from 'node:events';
import { useFocus } from './useFocus.js';
import { vi, type Mock } from 'vitest';
import { useStdin, useStdout } from 'ink';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
@@ -154,7 +154,7 @@ describe('useFolderTrust', () => {
renderHook(() => useFolderTrust(mockSettings, onTrustChange, addItem));
expect(addItem).toHaveBeenCalledWith(
{
text: 'This folder is untrusted, project settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
text: 'This folder is untrusted, workspace settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
type: 'info',
},
expect.any(Number),
+1 -1
View File
@@ -59,7 +59,7 @@ export const useFolderTrust = (
addItem(
{
type: MessageType.INFO,
text: 'This folder is untrusted, project settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
text: 'This folder is untrusted, workspace settings, hooks, MCPs, and GEMINI.md files will not be applied for this folder.\nUse the `/permissions` command to change the trust level.',
},
Date.now(),
);
@@ -5,8 +5,15 @@
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Mock, MockInstance } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
beforeEach,
describe,
expect,
it,
type Mock,
type MockInstance,
vi,
} from 'vitest';
import { act } from 'react';
import { renderHookWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { MockedFunction } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type MockedFunction,
vi,
} from 'vitest';
import { act } from 'react';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { Mock } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { useIncludeDirsTrust } from './useIncludeDirsTrust.js';
@@ -5,12 +5,20 @@
*/
import { act } from 'react';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { render } from '../../test-utils/render.js';
import { useKeypress } from './useKeypress.js';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import { useStdin } from 'ink';
import { EventEmitter } from 'node:events';
import type { Mock } from 'vitest';
// Mock the 'ink' module to control stdin
vi.mock('ink', async (importOriginal) => {
@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from '../../test-utils/render.js';
import { vi } from 'vitest';
import {
useMemoryMonitor,
MEMORY_CHECK_INTERVAL,
+1 -1
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import { useMouse } from './useMouse.js';
import { MouseProvider, useMouseContext } from '../contexts/MouseContext.js';
+1 -1
View File
@@ -38,7 +38,7 @@ describe('useRewindLogic', () => {
const mockConversation: ConversationRecord = {
sessionId: 'conv-1',
projectHash: 'hash-1',
workspaceHash: 'hash-1',
startTime: new Date(1000).toISOString(),
lastUpdated: new Date(1001).toISOString(),
messages: [mockUserMessage, mockModelMessage],
@@ -78,7 +78,7 @@ describe('useSessionResume', () => {
const resumedData: ResumedSessionData = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [] as MessageRecord[],
@@ -133,7 +133,7 @@ describe('useSessionResume', () => {
const resumedData: ResumedSessionData = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [] as MessageRecord[],
@@ -160,7 +160,7 @@ describe('useSessionResume', () => {
const resumedData: ResumedSessionData = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [] as MessageRecord[],
@@ -200,7 +200,7 @@ describe('useSessionResume', () => {
const resumedData: ResumedSessionData = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [] as MessageRecord[],
@@ -240,7 +240,7 @@ describe('useSessionResume', () => {
const resumedData: ResumedSessionData = {
conversation: {
sessionId: 'test-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [] as MessageRecord[],
@@ -306,7 +306,7 @@ describe('useSessionResume', () => {
it('should not resume when user is authenticating', () => {
const conversation: ConversationRecord = {
sessionId: 'auto-resume-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [
@@ -338,7 +338,7 @@ describe('useSessionResume', () => {
it('should not resume when Gemini client is not initialized', () => {
const conversation: ConversationRecord = {
sessionId: 'auto-resume-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [
@@ -370,7 +370,7 @@ describe('useSessionResume', () => {
it('should automatically resume session when resumedSessionData is provided', async () => {
const conversation: ConversationRecord = {
sessionId: 'auto-resume-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [
@@ -425,7 +425,7 @@ describe('useSessionResume', () => {
it('should only resume once even if props change', async () => {
const conversation: ConversationRecord = {
sessionId: 'auto-resume-123',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [
@@ -480,7 +480,7 @@ describe('useSessionResume', () => {
it('should convert session messages correctly during auto-resume', async () => {
const conversation: ConversationRecord = {
sessionId: 'auto-resume-with-tools',
projectHash: 'project-123',
workspaceHash: 'project-123',
startTime: '2025-01-01T00:00:00Z',
lastUpdated: '2025-01-01T01:00:00Z',
messages: [
+9 -1
View File
@@ -4,7 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import type React from 'react';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import '../../test-utils/customMatchers.js';
import { TableRenderer } from './TableRenderer.js';
import { renderWithProviders } from '../../test-utils/render.js';
@@ -5,6 +5,7 @@
*/
import { describe, expect, it, vi } from 'vitest';
import '../../test-utils/customMatchers.js';
import { getToolGroupBorderAppearance } from './borderStyles.js';
import { CoreToolCallStatus } from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
@@ -52,7 +52,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
warn: vi.fn(),
},
Storage: class {
getProjectTempDir = vi.fn(() => '/tmp/global');
getWorkspaceTempDir = vi.fn(() => '/tmp/global');
initialize = vi.fn(() => Promise.resolve(undefined));
},
};
+10 -10
View File
@@ -245,22 +245,22 @@ const saveFileWithXclip = async (tempFilePath: string) => {
};
/**
* Gets the directory where clipboard images should be stored for a specific project.
* Gets the directory where clipboard images should be stored for a specific workspace.
*
* This uses the global temporary directory but creates a project-specific subdirectory
* based on the hash of the project path (via `Storage.getProjectTempDir()`).
* This prevents path conflicts between different projects while keeping the images
* outside of the user's project directory.
* This uses the global temporary directory but creates a workspace-specific subdirectory
* based on the hash of the workspace path (via `Storage.getProjectTempDir()`).
* This prevents path conflicts between different workspaces while keeping the images
* outside of the user's workspace directory.
*
* @param targetDir The root directory of the current project.
* @param targetDir The root directory of the current workspace.
* @returns The absolute path to the images directory.
*/
async function getProjectClipboardImagesDir(
async function getWorkspaceClipboardImagesDir(
targetDir: string,
): Promise<string> {
const storage = new Storage(targetDir);
await storage.initialize();
const baseDir = storage.getProjectTempDir();
const baseDir = storage.getWorkspaceTempDir();
return path.join(baseDir, 'images');
}
@@ -273,7 +273,7 @@ export async function saveClipboardImage(
targetDir: string,
): Promise<string | null> {
try {
const tempDir = await getProjectClipboardImagesDir(targetDir);
const tempDir = await getWorkspaceClipboardImagesDir(targetDir);
await fs.mkdir(tempDir, { recursive: true });
// Generate a unique filename with timestamp
@@ -398,7 +398,7 @@ export async function cleanupOldClipboardImages(
targetDir: string,
): Promise<void> {
try {
const tempDir = await getProjectClipboardImagesDir(targetDir);
const tempDir = await getWorkspaceClipboardImagesDir(targetDir);
const files = await fs.readdir(tempDir);
const oneHourAgo = Date.now() - 60 * 60 * 1000;
@@ -17,7 +17,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
...actual,
spawnAsync: vi.fn(),
Storage: class {
getProjectTempDir = vi.fn(() => "C:\\User's Files");
getWorkspaceTempDir = vi.fn(() => "C:\\User's Files");
initialize = vi.fn(() => Promise.resolve(undefined));
},
};
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { EventEmitter } from 'node:events';
import clipboardy from 'clipboardy';
import {
+1
View File
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect, it } from 'vitest';
import { couldBeSGRMouseSequence, SGR_MOUSE_REGEX, ESC } from './input.js';
describe('input utils', () => {
+1 -2
View File
@@ -6,8 +6,7 @@
/// <reference types="vitest/globals" />
import type { MockInstance } from 'vitest';
import { vi } from 'vitest';
import { describe, it, expect, vi, type MockInstance } from 'vitest';
import { TextOutput } from './textOutput.js';
describe('TextOutput', () => {
+2 -2
View File
@@ -108,7 +108,7 @@ describe('agentUtils', () => {
],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" enabled by setting it to enabled in [user:/path/to/user/settings] and [project:/path/to/workspace/settings] settings.',
'Agent "my-agent" enabled by setting it to enabled in [user:/path/to/user/settings] and [workspace:/path/to/workspace/settings] settings.',
);
});
@@ -143,7 +143,7 @@ describe('agentUtils', () => {
],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" is now disabled in both [user:/path/to/user/settings] and [project:/path/to/workspace/settings] settings.',
'Agent "my-agent" is now disabled in both [user:/path/to/user/settings] and [workspace:/path/to/workspace/settings] settings.',
);
});
});
+1 -1
View File
@@ -40,7 +40,7 @@ export function renderAgentActionFeedback(
const formatScopeItem = (s: { scope: SettingScope; path: string }) => {
const label =
s.scope === SettingScope.Workspace ? 'project' : s.scope.toLowerCase();
s.scope === SettingScope.Workspace ? 'workspace' : s.scope.toLowerCase();
return formatScope(label, s.path);
};
+2 -2
View File
@@ -10,7 +10,7 @@ import * as path from 'node:path';
vi.mock('@google/gemini-cli-core', () => ({
Storage: vi.fn().mockImplementation(() => ({
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/workspace'),
initialize: vi.fn().mockResolvedValue(undefined),
})),
shutdownTelemetry: vi.fn(),
@@ -112,7 +112,7 @@ describe('cleanup', () => {
it('should remove checkpoints directory', async () => {
await cleanupCheckpoints();
expect(fs.rm).toHaveBeenCalledWith(
path.join('/tmp/project', 'checkpoints'),
path.join('/tmp/workspace', 'checkpoints'),
{
recursive: true,
force: true,
+1 -1
View File
@@ -165,7 +165,7 @@ export function setupTtyCheck(): () => void {
export async function cleanupCheckpoints() {
const storage = new Storage(process.cwd());
await storage.initialize();
const tempDir = storage.getProjectTempDir();
const tempDir = storage.getWorkspaceTempDir();
const checkpointsDir = join(tempDir, 'checkpoints');
try {
await fs.rm(checkpointsDir, { recursive: true, force: true });
@@ -4,8 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Mock } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from 'vitest';
import { getInstallationInfo, PackageManager } from './installationInfo.js';
import { updateEventEmitter } from './updateEventEmitter.js';
import type { UpdateObject } from '../ui/utils/updateCheck.js';
+2 -2
View File
@@ -64,7 +64,7 @@ export async function waitForUpdateCompletion(
export function handleAutoUpdate(
info: UpdateObject | null,
settings: LoadedSettings,
projectRoot: string,
workspaceRoot: string,
spawnFn: typeof spawn = spawnWrapper,
) {
if (!info) {
@@ -83,7 +83,7 @@ export function handleAutoUpdate(
}
const installationInfo = getInstallationInfo(
projectRoot,
workspaceRoot,
settings.merged.general.enableAutoUpdate,
);
+32 -32
View File
@@ -43,14 +43,14 @@ const mockedExistsSync = vi.mocked(fs.existsSync);
const mockedExecSync = vi.mocked(childProcess.execSync);
describe('getInstallationInfo', () => {
const projectRoot = '/path/to/project';
const workspaceRoot = '/path/to/workspace';
let originalArgv: string[];
beforeEach(() => {
vi.resetAllMocks();
originalArgv = [...process.argv];
// Mock process.cwd() for isGitRepository
vi.spyOn(process, 'cwd').mockReturnValue(projectRoot);
vi.spyOn(process, 'cwd').mockReturnValue(workspaceRoot);
vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
});
@@ -60,7 +60,7 @@ describe('getInstallationInfo', () => {
it('should return UNKNOWN when cliPath is not available', () => {
process.argv[1] = '';
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
});
@@ -71,20 +71,20 @@ describe('getInstallationInfo', () => {
throw error;
});
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
expect(debugLogger.log).toHaveBeenCalledWith(error);
});
it('should detect running from a local git clone', () => {
process.argv[1] = `${projectRoot}/packages/cli/dist/index.js`;
process.argv[1] = `${workspaceRoot}/packages/cli/dist/index.js`;
mockedRealPathSync.mockReturnValue(
`${projectRoot}/packages/cli/dist/index.js`,
`${workspaceRoot}/packages/cli/dist/index.js`,
);
mockedIsGitRepository.mockReturnValue(true);
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.UNKNOWN);
expect(info.isGlobal).toBe(false);
@@ -98,7 +98,7 @@ describe('getInstallationInfo', () => {
process.argv[1] = npxPath;
mockedRealPathSync.mockReturnValue(npxPath);
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.NPX);
expect(info.isGlobal).toBe(false);
@@ -110,7 +110,7 @@ describe('getInstallationInfo', () => {
process.argv[1] = pnpxPath;
mockedRealPathSync.mockReturnValue(pnpxPath);
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.PNPX);
expect(info.isGlobal).toBe(false);
@@ -125,7 +125,7 @@ describe('getInstallationInfo', () => {
throw new Error('Command failed');
});
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.BUNX);
expect(info.isGlobal).toBe(false);
@@ -155,7 +155,7 @@ describe('getInstallationInfo', () => {
return String(p);
});
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining('brew --prefix gemini-cli'),
@@ -179,7 +179,7 @@ describe('getInstallationInfo', () => {
throw new Error('Command failed');
});
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining('brew --prefix gemini-cli'),
@@ -199,14 +199,14 @@ describe('getInstallationInfo', () => {
});
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.PNPM);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe('pnpm add -g @google/gemini-cli@latest');
expect(info.updateMessage).toContain('Attempting to automatically update');
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
const infoDisabled = getInstallationInfo(workspaceRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run pnpm add');
});
@@ -219,7 +219,7 @@ describe('getInstallationInfo', () => {
});
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.YARN);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe(
@@ -228,7 +228,7 @@ describe('getInstallationInfo', () => {
expect(info.updateMessage).toContain('Attempting to automatically update');
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
const infoDisabled = getInstallationInfo(workspaceRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run yarn global add');
});
@@ -241,29 +241,29 @@ describe('getInstallationInfo', () => {
});
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.BUN);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe('bun add -g @google/gemini-cli@latest');
expect(info.updateMessage).toContain('Attempting to automatically update');
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
const infoDisabled = getInstallationInfo(workspaceRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run bun add');
});
it('should detect local installation and identify yarn from lockfile', () => {
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
process.argv[1] = localPath;
mockedRealPathSync.mockReturnValue(localPath);
mockedExecSync.mockImplementation(() => {
throw new Error('Command failed');
});
mockedExistsSync.mockImplementation(
(p) => p === path.join(projectRoot, 'yarn.lock'),
(p) => p === path.join(workspaceRoot, 'yarn.lock'),
);
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.YARN);
expect(info.isGlobal).toBe(false);
@@ -271,41 +271,41 @@ describe('getInstallationInfo', () => {
});
it('should detect local installation and identify pnpm from lockfile', () => {
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
process.argv[1] = localPath;
mockedRealPathSync.mockReturnValue(localPath);
mockedExecSync.mockImplementation(() => {
throw new Error('Command failed');
});
mockedExistsSync.mockImplementation(
(p) => p === path.join(projectRoot, 'pnpm-lock.yaml'),
(p) => p === path.join(workspaceRoot, 'pnpm-lock.yaml'),
);
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.PNPM);
expect(info.isGlobal).toBe(false);
});
it('should detect local installation and identify bun from lockfile', () => {
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
process.argv[1] = localPath;
mockedRealPathSync.mockReturnValue(localPath);
mockedExecSync.mockImplementation(() => {
throw new Error('Command failed');
});
mockedExistsSync.mockImplementation(
(p) => p === path.join(projectRoot, 'bun.lockb'),
(p) => p === path.join(workspaceRoot, 'bun.lockb'),
);
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.BUN);
expect(info.isGlobal).toBe(false);
});
it('should default to local npm installation if no lockfile is found', () => {
const localPath = `${projectRoot}/node_modules/.bin/gemini`;
const localPath = `${workspaceRoot}/node_modules/.bin/gemini`;
process.argv[1] = localPath;
mockedRealPathSync.mockReturnValue(localPath);
mockedExecSync.mockImplementation(() => {
@@ -313,7 +313,7 @@ describe('getInstallationInfo', () => {
});
mockedExistsSync.mockReturnValue(false); // No lockfiles
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.NPM);
expect(info.isGlobal).toBe(false);
@@ -328,14 +328,14 @@ describe('getInstallationInfo', () => {
});
// isAutoUpdateEnabled = true -> "Attempting to automatically update"
const info = getInstallationInfo(projectRoot, true);
const info = getInstallationInfo(workspaceRoot, true);
expect(info.packageManager).toBe(PackageManager.NPM);
expect(info.isGlobal).toBe(true);
expect(info.updateCommand).toBe('npm install -g @google/gemini-cli@latest');
expect(info.updateMessage).toContain('Attempting to automatically update');
// isAutoUpdateEnabled = false -> "Please run..."
const infoDisabled = getInstallationInfo(projectRoot, false);
const infoDisabled = getInstallationInfo(workspaceRoot, false);
expect(infoDisabled.updateMessage).toContain('Please run npm install');
});
@@ -368,7 +368,7 @@ describe('getInstallationInfo', () => {
return String(p);
});
const info = getInstallationInfo(projectRoot, false);
const info = getInstallationInfo(workspaceRoot, false);
expect(info.packageManager).not.toBe(PackageManager.HOMEBREW);
expect(info.packageManager).toBe(PackageManager.NPM);
+5 -5
View File
@@ -32,7 +32,7 @@ export interface InstallationInfo {
}
export function getInstallationInfo(
projectRoot: string,
workspaceRoot: string,
isAutoUpdateEnabled: boolean,
): InstallationInfo {
const cliPath = process.argv[1];
@@ -43,7 +43,7 @@ export function getInstallationInfo(
try {
// Normalize path separators to forward slashes for consistent matching.
const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/');
const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/');
const normalizedProjectRoot = workspaceRoot?.replace(/\\/g, '/');
const isGit = isGitRepository(process.cwd());
// Check for local git clone first
@@ -160,11 +160,11 @@ export function getInstallationInfo(
realPath.startsWith(`${normalizedProjectRoot}/node_modules`)
) {
let pm = PackageManager.NPM;
if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {
if (fs.existsSync(path.join(workspaceRoot, 'yarn.lock'))) {
pm = PackageManager.YARN;
} else if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {
} else if (fs.existsSync(path.join(workspaceRoot, 'pnpm-lock.yaml'))) {
pm = PackageManager.PNPM;
} else if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) {
} else if (fs.existsSync(path.join(workspaceRoot, 'bun.lockb'))) {
pm = PackageManager.BUN;
}
return {
+1 -1
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
RELAUNCH_EXIT_CODE,
relaunchApp,
+1 -1
View File
@@ -69,7 +69,7 @@ describe('sandboxUtils', () => {
});
it('should handle registry path', () => {
expect(parseImageName('gcr.io/my-project/my-image:v1')).toBe(
expect(parseImageName('gcr.io/my-workspace/my-image:v1')).toBe(
'my-image-v1',
);
});
@@ -17,7 +17,7 @@ import {
function createTestConfig(): Config {
return {
storage: {
getProjectTempDir: () => '/tmp/nonexistent-test-dir',
getWorkspaceTempDir: () => '/tmp/nonexistent-test-dir',
},
getSessionId: () => 'test-session-id',
getDebugMode: () => false,
@@ -93,7 +93,7 @@ describe('Session Cleanup Integration', () => {
);
const config = createTestConfig();
config.storage.getProjectTempDir = vi.fn().mockReturnValue(tempDir);
config.storage.getWorkspaceTempDir = vi.fn().mockReturnValue(tempDir);
const settings: Settings = {};
@@ -209,7 +209,7 @@ describe('Session Cleanup Integration', () => {
// Configure test with real temp directory
const config: Config = {
storage: {
getProjectTempDir: () => tempDir,
getWorkspaceTempDir: () => tempDir,
},
getSessionId: () => 'current123',
getDebugMode: () => false,
+18 -18
View File
@@ -28,8 +28,8 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...actual,
Storage: class MockStorage {
getProjectTempDir() {
return '/tmp/test-project';
getWorkspaceTempDir() {
return '/tmp/test-workspace';
}
},
};
@@ -42,7 +42,7 @@ const mockGetAllSessionFiles = vi.mocked(getAllSessionFiles);
function createMockConfig(overrides: Partial<Config> = {}): Config {
return {
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
},
getSessionId: vi.fn().mockReturnValue('current123'),
getDebugMode: vi.fn().mockReturnValue(false),
@@ -245,7 +245,7 @@ describe('Session Cleanup', () => {
// Verify that unlink was never called with the current session file
const unlinkCalls = mockFs.unlink.mock.calls;
const currentSessionPath = path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}2025-01-20T10-30-00-current12.json`,
);
@@ -525,21 +525,21 @@ describe('Session Cleanup', () => {
const unlinkCalls = mockFs.unlink.mock.calls.map((call) => call[0]);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}8d.json`,
),
);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}15d.json`,
),
);
expect(unlinkCalls).not.toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}5d.json`,
),
@@ -724,21 +724,21 @@ describe('Session Cleanup', () => {
const unlinkCalls = mockFs.unlink.mock.calls.map((call) => call[0]);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}3d.json`,
),
);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}4d.json`,
),
);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}5d.json`,
),
@@ -747,21 +747,21 @@ describe('Session Cleanup', () => {
// Verify which files were NOT deleted
expect(unlinkCalls).not.toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}current.json`,
),
);
expect(unlinkCalls).not.toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}1d.json`,
),
);
expect(unlinkCalls).not.toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}2d.json`,
),
@@ -883,21 +883,21 @@ describe('Session Cleanup', () => {
const unlinkCalls = mockFs.unlink.mock.calls.map((call) => call[0]);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}5d.json`,
),
);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}7d.json`,
),
);
expect(unlinkCalls).toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}12d.json`,
),
@@ -906,14 +906,14 @@ describe('Session Cleanup', () => {
// Verify which files were NOT deleted
expect(unlinkCalls).not.toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}current.json`,
),
);
expect(unlinkCalls).not.toContain(
path.join(
'/tmp/test-project',
'/tmp/test-workspace',
'chats',
`${SESSION_FILE_PREFIX}3d.json`,
),
+7 -4
View File
@@ -59,7 +59,7 @@ export async function cleanupExpiredSessions(
}
const retentionConfig = settings.general.sessionRetention;
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
const chatsDir = path.join(config.storage.getWorkspaceTempDir(), 'chats');
// Validate retention configuration
const validationErrorMessage = validateRetentionConfig(
@@ -95,7 +95,10 @@ export async function cleanupExpiredSessions(
// ALSO cleanup Activity logs in the project logs directory
const sessionId = sessionToDelete.sessionInfo?.id;
if (sessionId) {
const logsDir = path.join(config.storage.getProjectTempDir(), 'logs');
const logsDir = path.join(
config.storage.getWorkspaceTempDir(),
'logs',
);
const logPath = path.join(logsDir, `session-${sessionId}.jsonl`);
try {
await fs.unlink(logPath);
@@ -106,7 +109,7 @@ export async function cleanupExpiredSessions(
// ALSO cleanup tool outputs for this session
const safeSessionId = sanitizeFilenamePart(sessionId);
const toolOutputDir = path.join(
config.storage.getProjectTempDir(),
config.storage.getWorkspaceTempDir(),
TOOL_OUTPUTS_DIR,
`session-${safeSessionId}`,
);
@@ -382,7 +385,7 @@ export async function cleanupToolOutputFiles(
if (!tempDir) {
const storage = new Storage(process.cwd());
await storage.initialize();
tempDir = storage.getProjectTempDir();
tempDir = storage.getWorkspaceTempDir();
}
const toolOutputDir = path.join(tempDir, TOOL_OUTPUTS_DIR);
+16 -16
View File
@@ -30,7 +30,7 @@ describe('SessionSelector', () => {
// Mock config
config = {
storage: {
getProjectTempDir: () => tmpDir,
getWorkspaceTempDir: () => tmpDir,
},
getSessionId: () => 'current-session-id',
} as Partial<Config> as Config;
@@ -55,7 +55,7 @@ describe('SessionSelector', () => {
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -70,7 +70,7 @@ describe('SessionSelector', () => {
const session2 = {
sessionId: sessionId2,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
@@ -121,7 +121,7 @@ describe('SessionSelector', () => {
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -136,7 +136,7 @@ describe('SessionSelector', () => {
const session2 = {
sessionId: sessionId2,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
@@ -185,7 +185,7 @@ describe('SessionSelector', () => {
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -200,7 +200,7 @@ describe('SessionSelector', () => {
const session2 = {
sessionId: sessionId2,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
@@ -245,7 +245,7 @@ describe('SessionSelector', () => {
const sessionOriginal = {
sessionId,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -260,7 +260,7 @@ describe('SessionSelector', () => {
const sessionDuplicate = {
sessionId,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T11:00:00.000Z', // Newer
messages: [
@@ -309,7 +309,7 @@ describe('SessionSelector', () => {
const session1 = {
sessionId: sessionId1,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -348,7 +348,7 @@ describe('SessionSelector', () => {
const emptyConfig = {
storage: {
getProjectTempDir: () => tmpDir,
getWorkspaceTempDir: () => tmpDir,
},
getSessionId: () => 'current-session-id',
} as Partial<Config> as Config;
@@ -375,7 +375,7 @@ describe('SessionSelector', () => {
// Session with user message - should be listed
const sessionWithUser = {
sessionId: sessionIdWithUser,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -391,7 +391,7 @@ describe('SessionSelector', () => {
// Session with only system messages - should NOT be listed
const sessionSystemOnly = {
sessionId: sessionIdSystemOnly,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
@@ -444,7 +444,7 @@ describe('SessionSelector', () => {
// Session with only gemini message - should be listed
const sessionGeminiOnly = {
sessionId: sessionIdGeminiOnly,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -484,7 +484,7 @@ describe('SessionSelector', () => {
// Main session - should be listed
const mainSession = {
sessionId: mainSessionId,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T10:00:00.000Z',
lastUpdated: '2024-01-01T10:30:00.000Z',
messages: [
@@ -501,7 +501,7 @@ describe('SessionSelector', () => {
// Subagent session - should NOT be listed
const subagentSession = {
sessionId: subagentSessionId,
projectHash: 'test-hash',
workspaceHash: 'test-hash',
startTime: '2024-01-01T11:00:00.000Z',
lastUpdated: '2024-01-01T11:30:00.000Z',
messages: [
+3 -3
View File
@@ -50,7 +50,7 @@ export class SessionError extends Error {
static noSessionsFound(): SessionError {
return new SessionError(
'NO_SESSIONS_FOUND',
'No previous sessions found for this project.',
'No previous sessions found for this workspace.',
);
}
@@ -402,7 +402,7 @@ export class SessionSelector {
*/
async listSessions(): Promise<SessionInfo[]> {
const chatsDir = path.join(
this.config.storage.getProjectTempDir(),
this.config.storage.getWorkspaceTempDir(),
'chats',
);
return getSessionFiles(chatsDir, this.config.getSessionId());
@@ -498,7 +498,7 @@ export class SessionSelector {
sessionInfo: SessionInfo,
): Promise<SessionSelectionResult> {
const chatsDir = path.join(
this.config.storage.getProjectTempDir(),
this.config.storage.getWorkspaceTempDir(),
'chats',
);
const sessionPath = path.join(chatsDir, sessionInfo.fileName);
+6 -6
View File
@@ -40,7 +40,7 @@ describe('listSessions', () => {
// Create mock config
mockConfig = {
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
},
getSessionId: vi.fn().mockReturnValue('current-session-id'),
} as unknown as Config;
@@ -73,7 +73,7 @@ describe('listSessions', () => {
// Assert
expect(mockListSessions).toHaveBeenCalledOnce();
expect(mocks.writeToStdout).toHaveBeenCalledWith(
'No previous sessions found for this project.',
'No previous sessions found for this workspace.',
);
});
@@ -132,7 +132,7 @@ describe('listSessions', () => {
// Check that the header was displayed
expect(mocks.writeToStdout).toHaveBeenCalledWith(
'\nAvailable sessions for this project (3):\n',
'\nAvailable sessions for this workspace (3):\n',
);
// Check that each session was logged
@@ -286,7 +286,7 @@ describe('listSessions', () => {
// Assert
expect(mocks.writeToStdout).toHaveBeenCalledWith(
'\nAvailable sessions for this project (1):\n',
'\nAvailable sessions for this workspace (1):\n',
);
expect(mocks.writeToStdout).toHaveBeenCalledWith(
expect.stringContaining('1. Only session'),
@@ -340,7 +340,7 @@ describe('deleteSession', () => {
// Create mock config
mockConfig = {
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/test-project'),
getWorkspaceTempDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
},
getSessionId: vi.fn().mockReturnValue('current-session-id'),
} as unknown as Config;
@@ -380,7 +380,7 @@ describe('deleteSession', () => {
// Assert
expect(mockListSessions).toHaveBeenCalledOnce();
expect(mocks.writeToStderr).toHaveBeenCalledWith(
'No sessions found for this project.',
'No sessions found for this workspace.',
);
expect(mockDeleteSession).not.toHaveBeenCalled();
});
+3 -3
View File
@@ -25,12 +25,12 @@ export async function listSessions(config: Config): Promise<void> {
const sessions = await sessionSelector.listSessions();
if (sessions.length === 0) {
writeToStdout('No previous sessions found for this project.');
writeToStdout('No previous sessions found for this workspace.');
return;
}
writeToStdout(
`\nAvailable sessions for this project (${sessions.length}):\n`,
`\nAvailable sessions for this workspace (${sessions.length}):\n`,
);
sessions
@@ -59,7 +59,7 @@ export async function deleteSession(
const sessions = await sessionSelector.listSessions();
if (sessions.length === 0) {
writeToStderr('No sessions found for this project.');
writeToStderr('No sessions found for this workspace.');
return;
}
+2 -2
View File
@@ -12,7 +12,7 @@ import { installSkill, linkSkill } from './skillUtils.js';
describe('skillUtils', () => {
let tempDir: string;
const projectRoot = path.resolve(__dirname, '../../../../../');
const workspaceRoot = path.resolve(__dirname, '../../../../../');
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-utils-test-'));
@@ -130,7 +130,7 @@ describe('skillUtils', () => {
});
it('should successfully install from a .skill file', async () => {
const skillPath = path.join(projectRoot, 'weather-skill.skill');
const skillPath = path.join(workspaceRoot, 'weather-skill.skill');
// Ensure the file exists
const exists = await fs.stat(skillPath).catch(() => null);
@@ -82,10 +82,10 @@ describe('getUserStartupWarnings', () => {
);
});
it('should not return a warning when running in a project directory', async () => {
const projectDir = path.join(testRootDir, 'project');
await fs.mkdir(projectDir);
const warnings = await getUserStartupWarnings({}, projectDir);
it('should not return a warning when running in a workspace directory', async () => {
const workspaceDir = path.join(testRootDir, 'workspace');
await fs.mkdir(workspaceDir);
const warnings = await getUserStartupWarnings({}, workspaceDir);
expect(warnings.find((w) => w.id === 'home-directory')).toBeUndefined();
});
@@ -123,9 +123,9 @@ describe('getUserStartupWarnings', () => {
});
it('should not return a warning when running in a non-root directory', async () => {
const projectDir = path.join(testRootDir, 'project');
await fs.mkdir(projectDir);
const warnings = await getUserStartupWarnings({}, projectDir);
const workspaceDir = path.join(testRootDir, 'workspace');
await fs.mkdir(workspaceDir);
const warnings = await getUserStartupWarnings({}, workspaceDir);
expect(warnings.find((w) => w.id === 'root-directory')).toBeUndefined();
});
});
@@ -151,10 +151,10 @@ describe('getUserStartupWarnings', () => {
priority: WarningPriority.High,
};
vi.mocked(getCompatibilityWarnings).mockReturnValue([compWarning]);
const projectDir = path.join(testRootDir, 'project');
await fs.mkdir(projectDir);
const workspaceDir = path.join(testRootDir, 'workspace');
await fs.mkdir(workspaceDir);
const warnings = await getUserStartupWarnings({}, projectDir);
const warnings = await getUserStartupWarnings({}, workspaceDir);
expect(warnings).toContainEqual(compWarning);
});
@@ -165,12 +165,12 @@ describe('getUserStartupWarnings', () => {
priority: WarningPriority.High,
};
vi.mocked(getCompatibilityWarnings).mockReturnValue([compWarning]);
const projectDir = path.join(testRootDir, 'project');
await fs.mkdir(projectDir);
const workspaceDir = path.join(testRootDir, 'workspace');
await fs.mkdir(workspaceDir);
const warnings = await getUserStartupWarnings(
{ ui: { showCompatibilityWarnings: false } },
projectDir,
workspaceDir,
);
expect(warnings).not.toContainEqual(compWarning);
});

Some files were not shown because too many files have changed in this diff Show More