mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 13:53:02 -07:00
feat(plan): support configuring custom plans storage directory (#19577)
This commit is contained in:
@@ -127,6 +127,47 @@ To use a skill in Plan Mode, you can explicitly ask the agent to "use the
|
|||||||
[skill-name] skill to plan..." or the agent may autonomously activate it based
|
[skill-name] skill to plan..." or the agent may autonomously activate it based
|
||||||
on the task description.
|
on the task description.
|
||||||
|
|
||||||
|
### Custom Plan Directory and Policies
|
||||||
|
|
||||||
|
By default, planning artifacts are stored in a managed temporary directory
|
||||||
|
outside your project: `~/.gemini/tmp/<project>/<session-id>/plans/`.
|
||||||
|
|
||||||
|
You can configure a custom directory for plans in your `settings.json`. For
|
||||||
|
example, to store plans in a `.gemini/plans` directory within your project:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"plan": {
|
||||||
|
"directory": ".gemini/plans"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To maintain the safety of Plan Mode, user-configured paths for the plans
|
||||||
|
directory are restricted to the project root. This ensures that custom planning
|
||||||
|
locations defined within a project's workspace cannot be used to escape and
|
||||||
|
overwrite sensitive files elsewhere. Any user-configured directory must reside
|
||||||
|
within the project boundary.
|
||||||
|
|
||||||
|
Because Plan Mode is read-only by default, using a custom directory requires
|
||||||
|
updating your [Policy Engine] configurations to allow `write_file` and `replace`
|
||||||
|
in that specific location. For example, to allow writing to the `.gemini/plans`
|
||||||
|
directory within your project, create a policy file at
|
||||||
|
`~/.gemini/policies/plan-custom-directory.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[rule]]
|
||||||
|
toolName = ["write_file", "replace"]
|
||||||
|
decision = "allow"
|
||||||
|
priority = 100
|
||||||
|
modes = ["plan"]
|
||||||
|
# Adjust the pattern to match your custom directory.
|
||||||
|
# This example matches any .md file in a .gemini/plans directory within the project.
|
||||||
|
argsPattern = "\"file_path\":\".*\\\\.gemini/plans/.*\\\\.md\""
|
||||||
|
```
|
||||||
|
|
||||||
### Customizing Policies
|
### Customizing Policies
|
||||||
|
|
||||||
Plan Mode is designed to be read-only by default to ensure safety during the
|
Plan Mode is designed to be read-only by default to ensure safety during the
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ they appear in the UI.
|
|||||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
|
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
|
||||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||||
|
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
|
||||||
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
||||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`general.plan.directory`** (string):
|
||||||
|
- **Description:** The directory where planning artifacts are stored. If not
|
||||||
|
specified, defaults to the system temporary directory.
|
||||||
|
- **Default:** `undefined`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`general.enablePromptCompletion`** (boolean):
|
- **`general.enablePromptCompletion`** (boolean):
|
||||||
- **Description:** Enable AI-powered prompt completion suggestions while
|
- **Description:** Enable AI-powered prompt completion suggestions while
|
||||||
typing.
|
typing.
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import {
|
|||||||
type MCPServerConfig,
|
type MCPServerConfig,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||||
import { type Settings, createTestMergedSettings } from './settings.js';
|
import {
|
||||||
|
type Settings,
|
||||||
|
type MergedSettings,
|
||||||
|
createTestMergedSettings,
|
||||||
|
} from './settings.js';
|
||||||
import * as ServerConfig from '@google/gemini-cli-core';
|
import * as ServerConfig from '@google/gemini-cli-core';
|
||||||
|
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||||
@@ -2599,6 +2603,21 @@ describe('loadCliConfig approval mode', () => {
|
|||||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass planSettings.directory from settings to config', async () => {
|
||||||
|
process.argv = ['node', 'script.js'];
|
||||||
|
const settings = createTestMergedSettings({
|
||||||
|
general: {
|
||||||
|
plan: {
|
||||||
|
directory: '.custom-plans',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as MergedSettings);
|
||||||
|
const argv = await parseArguments(settings);
|
||||||
|
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||||
|
const plansDir = config.storage.getPlansDir();
|
||||||
|
expect(plansDir).toContain('.custom-plans');
|
||||||
|
});
|
||||||
|
|
||||||
// --- Untrusted Folder Scenarios ---
|
// --- Untrusted Folder Scenarios ---
|
||||||
describe('when folder is NOT trusted', () => {
|
describe('when folder is NOT trusted', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -814,6 +814,7 @@ export async function loadCliConfig(
|
|||||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||||
enableAgents: settings.experimental?.enableAgents,
|
enableAgents: settings.experimental?.enableAgents,
|
||||||
plan: settings.experimental?.plan,
|
plan: settings.experimental?.plan,
|
||||||
|
planSettings: settings.general.plan,
|
||||||
enableEventDrivenScheduler: true,
|
enableEventDrivenScheduler: true,
|
||||||
skillsSupport: settings.skills?.enabled ?? true,
|
skillsSupport: settings.skills?.enabled ?? true,
|
||||||
disabledSkills: settings.skills?.disabled,
|
disabledSkills: settings.skills?.disabled,
|
||||||
|
|||||||
@@ -107,6 +107,16 @@ describe('SettingsSchema', () => {
|
|||||||
).toBe('boolean');
|
).toBe('boolean');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have plan nested properties', () => {
|
||||||
|
expect(
|
||||||
|
getSettingsSchema().general?.properties?.plan?.properties?.directory,
|
||||||
|
).toBeDefined();
|
||||||
|
expect(
|
||||||
|
getSettingsSchema().general?.properties?.plan?.properties?.directory
|
||||||
|
.type,
|
||||||
|
).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
it('should have fileFiltering nested properties', () => {
|
it('should have fileFiltering nested properties', () => {
|
||||||
expect(
|
expect(
|
||||||
getSettingsSchema().context.properties.fileFiltering.properties
|
getSettingsSchema().context.properties.fileFiltering.properties
|
||||||
|
|||||||
@@ -266,6 +266,27 @@ const SETTINGS_SCHEMA = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plan: {
|
||||||
|
type: 'object',
|
||||||
|
label: 'Plan',
|
||||||
|
category: 'General',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: {},
|
||||||
|
description: 'Planning features configuration.',
|
||||||
|
showInDialog: false,
|
||||||
|
properties: {
|
||||||
|
directory: {
|
||||||
|
type: 'string',
|
||||||
|
label: 'Plan Directory',
|
||||||
|
category: 'General',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: undefined as string | undefined,
|
||||||
|
description:
|
||||||
|
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
enablePromptCompletion: {
|
enablePromptCompletion: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Enable Prompt Completion',
|
label: 'Enable Prompt Completion',
|
||||||
@@ -1313,6 +1334,7 @@ const SETTINGS_SCHEMA = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
useWriteTodos: {
|
useWriteTodos: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Use WriteTodos',
|
label: 'Use WriteTodos',
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ describe('planCommand', () => {
|
|||||||
getApprovalMode: vi.fn(),
|
getApprovalMode: vi.fn(),
|
||||||
getFileSystemService: vi.fn(),
|
getFileSystemService: vi.fn(),
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
|
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const planCommand: SlashCommand = {
|
|||||||
try {
|
try {
|
||||||
const content = await processSingleFileContent(
|
const content = await processSingleFileContent(
|
||||||
approvedPlanPath,
|
approvedPlanPath,
|
||||||
config.storage.getProjectTempPlansDir(),
|
config.storage.getPlansDir(),
|
||||||
config.getFileSystemService(),
|
config.getFileSystemService(),
|
||||||
);
|
);
|
||||||
const fileName = path.basename(approvedPlanPath);
|
const fileName = path.basename(approvedPlanPath);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ Implement a comprehensive authentication system with multiple providers.
|
|||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempPlansDir: () => mockPlansDir,
|
getPlansDir: () => mockPlansDir,
|
||||||
},
|
},
|
||||||
getFileSystemService: (): FileSystemService => ({
|
getFileSystemService: (): FileSystemService => ({
|
||||||
readTextFile: vi.fn(),
|
readTextFile: vi.fn(),
|
||||||
@@ -429,7 +429,7 @@ Implement a comprehensive authentication system with multiple providers.
|
|||||||
getIdeMode: () => false,
|
getIdeMode: () => false,
|
||||||
isTrustedFolder: () => true,
|
isTrustedFolder: () => true,
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempPlansDir: () => mockPlansDir,
|
getPlansDir: () => mockPlansDir,
|
||||||
},
|
},
|
||||||
getFileSystemService: (): FileSystemService => ({
|
getFileSystemService: (): FileSystemService => ({
|
||||||
readTextFile: vi.fn(),
|
readTextFile: vi.fn(),
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
|
|||||||
try {
|
try {
|
||||||
const pathError = await validatePlanPath(
|
const pathError = await validatePlanPath(
|
||||||
planPath,
|
planPath,
|
||||||
config.storage.getProjectTempPlansDir(),
|
config.storage.getPlansDir(),
|
||||||
config.getTargetDir(),
|
config.getTargetDir(),
|
||||||
);
|
);
|
||||||
if (ignore) return;
|
if (ignore) return;
|
||||||
@@ -83,7 +83,7 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
|
|||||||
|
|
||||||
const result = await processSingleFileContent(
|
const result = await processSingleFileContent(
|
||||||
planPath,
|
planPath,
|
||||||
config.storage.getProjectTempPlansDir(),
|
config.storage.getPlansDir(),
|
||||||
config.getFileSystemService(),
|
config.getFileSystemService(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('ToolConfirmationQueue', () => {
|
|||||||
readFile: vi.fn().mockResolvedValue('Plan content'),
|
readFile: vi.fn().mockResolvedValue('Plan content'),
|
||||||
}),
|
}),
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempPlansDir: () => '/mock/temp/plans',
|
getPlansDir: () => '/mock/temp/plans',
|
||||||
},
|
},
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -31,9 +34,6 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -69,6 +69,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -78,9 +81,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -116,6 +116,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false* │
|
│ Enable Prompt Completion false* │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -125,9 +128,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -163,6 +163,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -172,9 +175,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -210,6 +210,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -219,9 +222,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -257,6 +257,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -266,9 +269,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ > Apply To │
|
│ > Apply To │
|
||||||
@@ -304,6 +304,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -313,9 +316,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -351,6 +351,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion false │
|
│ Enable Prompt Completion false │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -360,9 +363,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
@@ -398,6 +398,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
|||||||
│ Enable Notifications false │
|
│ Enable Notifications false │
|
||||||
│ Enable run-event notifications for action-required prompts and session completion. … │
|
│ Enable run-event notifications for action-required prompts and session completion. … │
|
||||||
│ │
|
│ │
|
||||||
|
│ Plan Directory undefined │
|
||||||
|
│ The directory where planning artifacts are stored. If not specified, defaults t… │
|
||||||
|
│ │
|
||||||
│ Enable Prompt Completion true* │
|
│ Enable Prompt Completion true* │
|
||||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||||
│ │
|
│ │
|
||||||
@@ -407,9 +410,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
|||||||
│ Enable Session Cleanup false │
|
│ Enable Session Cleanup false │
|
||||||
│ Enable automatic session cleanup │
|
│ Enable automatic session cleanup │
|
||||||
│ │
|
│ │
|
||||||
│ Keep chat history undefined │
|
|
||||||
│ Automatically delete chats older than this time period (e.g., "30d", "7d", "24h… │
|
|
||||||
│ │
|
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ │
|
│ │
|
||||||
│ Apply To │
|
│ Apply To │
|
||||||
|
|||||||
@@ -737,6 +737,42 @@ describe('Server Config (config.ts)', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Plan Settings', () => {
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
name: 'should pass custom plan directory to storage',
|
||||||
|
planSettings: { directory: 'custom-plans' },
|
||||||
|
expected: 'custom-plans',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should call setCustomPlansDir with undefined if directory is not provided',
|
||||||
|
planSettings: {},
|
||||||
|
expected: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'should call setCustomPlansDir with undefined if planSettings is not provided',
|
||||||
|
planSettings: undefined,
|
||||||
|
expected: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ name, planSettings, expected }) => {
|
||||||
|
it(`${name}`, () => {
|
||||||
|
const setCustomPlansDirSpy = vi.spyOn(
|
||||||
|
Storage.prototype,
|
||||||
|
'setCustomPlansDir',
|
||||||
|
);
|
||||||
|
new Config({
|
||||||
|
...baseParams,
|
||||||
|
planSettings,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setCustomPlansDirSpy).toHaveBeenCalledWith(expected);
|
||||||
|
setCustomPlansDirSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Telemetry Settings', () => {
|
describe('Telemetry Settings', () => {
|
||||||
it('should return default telemetry target if not provided', () => {
|
it('should return default telemetry target if not provided', () => {
|
||||||
const params: ConfigParameters = {
|
const params: ConfigParameters = {
|
||||||
@@ -2501,7 +2537,7 @@ describe('Plans Directory Initialization', () => {
|
|||||||
|
|
||||||
await config.initialize();
|
await config.initialize();
|
||||||
|
|
||||||
const plansDir = config.storage.getProjectTempPlansDir();
|
const plansDir = config.storage.getPlansDir();
|
||||||
expect(fs.promises.mkdir).toHaveBeenCalledWith(plansDir, {
|
expect(fs.promises.mkdir).toHaveBeenCalledWith(plansDir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
@@ -2518,7 +2554,7 @@ describe('Plans Directory Initialization', () => {
|
|||||||
|
|
||||||
await config.initialize();
|
await config.initialize();
|
||||||
|
|
||||||
const plansDir = config.storage.getProjectTempPlansDir();
|
const plansDir = config.storage.getPlansDir();
|
||||||
expect(fs.promises.mkdir).not.toHaveBeenCalledWith(plansDir, {
|
expect(fs.promises.mkdir).not.toHaveBeenCalledWith(plansDir, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ export interface SummarizeToolOutputSettings {
|
|||||||
tokenBudget?: number;
|
tokenBudget?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanSettings {
|
||||||
|
directory?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TelemetrySettings {
|
export interface TelemetrySettings {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
target?: TelemetryTarget;
|
target?: TelemetryTarget;
|
||||||
@@ -483,6 +487,7 @@ export interface ConfigParameters {
|
|||||||
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
||||||
disableLLMCorrection?: boolean;
|
disableLLMCorrection?: boolean;
|
||||||
plan?: boolean;
|
plan?: boolean;
|
||||||
|
planSettings?: PlanSettings;
|
||||||
modelSteering?: boolean;
|
modelSteering?: boolean;
|
||||||
onModelChange?: (model: string) => void;
|
onModelChange?: (model: string) => void;
|
||||||
mcpEnabled?: boolean;
|
mcpEnabled?: boolean;
|
||||||
@@ -836,6 +841,7 @@ export class Config {
|
|||||||
this.extensionManagement = params.extensionManagement ?? true;
|
this.extensionManagement = params.extensionManagement ?? true;
|
||||||
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
|
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
|
||||||
this.storage = new Storage(this.targetDir, this.sessionId);
|
this.storage = new Storage(this.targetDir, this.sessionId);
|
||||||
|
this.storage.setCustomPlansDir(params.planSettings?.directory);
|
||||||
|
|
||||||
this.fakeResponses = params.fakeResponses;
|
this.fakeResponses = params.fakeResponses;
|
||||||
this.recordResponses = params.recordResponses;
|
this.recordResponses = params.recordResponses;
|
||||||
@@ -949,7 +955,7 @@ export class Config {
|
|||||||
|
|
||||||
// Add plans directory to workspace context for plan file storage
|
// Add plans directory to workspace context for plan file storage
|
||||||
if (this.planEnabled) {
|
if (this.planEnabled) {
|
||||||
const plansDir = this.storage.getProjectTempPlansDir();
|
const plansDir = this.storage.getPlansDir();
|
||||||
await fs.promises.mkdir(plansDir, { recursive: true });
|
await fs.promises.mkdir(plansDir, { recursive: true });
|
||||||
this.workspaceContext.addDirectory(plansDir);
|
this.workspaceContext.addDirectory(plansDir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ vi.unmock('./storageMigration.js');
|
|||||||
|
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
|
||||||
vi.mock('fs', async (importOriginal) => {
|
vi.mock('fs', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('fs')>();
|
const actual = await importOriginal<typeof import('fs')>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
mkdirSync: vi.fn(),
|
mkdirSync: vi.fn(),
|
||||||
|
realpathSync: vi.fn(actual.realpathSync),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,12 +63,11 @@ describe('Storage – initialize', () => {
|
|||||||
).toHaveBeenCalledWith(projectRoot);
|
).toHaveBeenCalledWith(projectRoot);
|
||||||
|
|
||||||
// Verify migration calls
|
// Verify migration calls
|
||||||
const shortId = 'project-slug';
|
|
||||||
// We can't easily get the hash here without repeating logic, but we can verify it's called twice
|
// We can't easily get the hash here without repeating logic, but we can verify it's called twice
|
||||||
expect(StorageMigration.migrateDirectory).toHaveBeenCalledTimes(2);
|
expect(StorageMigration.migrateDirectory).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
// Verify identifier is set by checking a path
|
// Verify identifier is set by checking a path
|
||||||
expect(storage.getProjectTempDir()).toContain(shortId);
|
expect(storage.getProjectTempDir()).toContain(PROJECT_SLUG);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,6 +106,12 @@ describe('Storage – additional helpers', () => {
|
|||||||
const projectRoot = '/tmp/project';
|
const projectRoot = '/tmp/project';
|
||||||
const storage = new Storage(projectRoot);
|
const storage = new Storage(projectRoot);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ProjectRegistry.prototype.getShortId = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue(PROJECT_SLUG);
|
||||||
|
});
|
||||||
|
|
||||||
it('getWorkspaceSettingsPath returns project/.gemini/settings.json', () => {
|
it('getWorkspaceSettingsPath returns project/.gemini/settings.json', () => {
|
||||||
const expected = path.join(projectRoot, GEMINI_DIR, 'settings.json');
|
const expected = path.join(projectRoot, GEMINI_DIR, 'settings.json');
|
||||||
expect(storage.getWorkspaceSettingsPath()).toBe(expected);
|
expect(storage.getWorkspaceSettingsPath()).toBe(expected);
|
||||||
@@ -172,6 +179,101 @@ describe('Storage – additional helpers', () => {
|
|||||||
const expected = path.join(tempDir, sessionId, 'plans');
|
const expected = path.join(tempDir, sessionId, 'plans');
|
||||||
expect(storageWithSession.getProjectTempPlansDir()).toBe(expected);
|
expect(storageWithSession.getProjectTempPlansDir()).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getPlansDir', () => {
|
||||||
|
interface TestCase {
|
||||||
|
name: string;
|
||||||
|
customDir: string | undefined;
|
||||||
|
expected: string | (() => string);
|
||||||
|
expectedError?: string;
|
||||||
|
setup?: () => () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCases: TestCase[] = [
|
||||||
|
{
|
||||||
|
name: 'custom relative path',
|
||||||
|
customDir: '.my-plans',
|
||||||
|
expected: path.resolve(projectRoot, '.my-plans'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'custom absolute path outside throws',
|
||||||
|
customDir: '/absolute/path/to/plans',
|
||||||
|
expected: '',
|
||||||
|
expectedError:
|
||||||
|
"Custom plans directory '/absolute/path/to/plans' resolves to '/absolute/path/to/plans', which is outside the project root '/tmp/project'.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'absolute path that happens to be inside project root',
|
||||||
|
customDir: path.join(projectRoot, 'internal-plans'),
|
||||||
|
expected: path.join(projectRoot, 'internal-plans'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relative path that stays within project root',
|
||||||
|
customDir: 'subdir/../plans',
|
||||||
|
expected: path.resolve(projectRoot, 'plans'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dot path',
|
||||||
|
customDir: '.',
|
||||||
|
expected: projectRoot,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'default behavior when customDir is undefined',
|
||||||
|
customDir: undefined,
|
||||||
|
expected: () => storage.getProjectTempPlansDir(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'escaping relative path throws',
|
||||||
|
customDir: '../escaped-plans',
|
||||||
|
expected: '',
|
||||||
|
expectedError:
|
||||||
|
"Custom plans directory '../escaped-plans' resolves to '/tmp/escaped-plans', which is outside the project root '/tmp/project'.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hidden directory starting with ..',
|
||||||
|
customDir: '..plans',
|
||||||
|
expected: path.resolve(projectRoot, '..plans'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'security escape via symbolic link throws',
|
||||||
|
customDir: 'symlink-to-outside',
|
||||||
|
setup: () => {
|
||||||
|
vi.mocked(fs.realpathSync).mockImplementation((p: fs.PathLike) => {
|
||||||
|
if (p.toString().includes('symlink-to-outside')) {
|
||||||
|
return '/outside/project/root';
|
||||||
|
}
|
||||||
|
return p.toString();
|
||||||
|
});
|
||||||
|
return () => vi.mocked(fs.realpathSync).mockRestore();
|
||||||
|
},
|
||||||
|
expected: '',
|
||||||
|
expectedError:
|
||||||
|
"Custom plans directory 'symlink-to-outside' resolves to '/outside/project/root', which is outside the project root '/tmp/project'.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ name, customDir, expected, expectedError, setup }) => {
|
||||||
|
it(`should handle ${name}`, async () => {
|
||||||
|
const cleanup = setup?.();
|
||||||
|
try {
|
||||||
|
if (name.includes('default behavior')) {
|
||||||
|
await storage.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setCustomPlansDir(customDir);
|
||||||
|
if (expectedError) {
|
||||||
|
expect(() => storage.getPlansDir()).toThrow(expectedError);
|
||||||
|
} else {
|
||||||
|
const expectedValue =
|
||||||
|
typeof expected === 'function' ? expected() : expected;
|
||||||
|
expect(storage.getPlansDir()).toBe(expectedValue);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cleanup?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Storage - System Paths', () => {
|
describe('Storage - System Paths', () => {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
GEMINI_DIR,
|
GEMINI_DIR,
|
||||||
homedir,
|
homedir,
|
||||||
GOOGLE_ACCOUNTS_FILENAME,
|
GOOGLE_ACCOUNTS_FILENAME,
|
||||||
|
isSubpath,
|
||||||
|
resolveToRealPath,
|
||||||
} from '../utils/paths.js';
|
} from '../utils/paths.js';
|
||||||
import { ProjectRegistry } from './projectRegistry.js';
|
import { ProjectRegistry } from './projectRegistry.js';
|
||||||
import { StorageMigration } from './storageMigration.js';
|
import { StorageMigration } from './storageMigration.js';
|
||||||
@@ -26,12 +28,17 @@ export class Storage {
|
|||||||
private readonly sessionId: string | undefined;
|
private readonly sessionId: string | undefined;
|
||||||
private projectIdentifier: string | undefined;
|
private projectIdentifier: string | undefined;
|
||||||
private initPromise: Promise<void> | undefined;
|
private initPromise: Promise<void> | undefined;
|
||||||
|
private customPlansDir: string | undefined;
|
||||||
|
|
||||||
constructor(targetDir: string, sessionId?: string) {
|
constructor(targetDir: string, sessionId?: string) {
|
||||||
this.targetDir = targetDir;
|
this.targetDir = targetDir;
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomPlansDir(dir: string | undefined): void {
|
||||||
|
this.customPlansDir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
static getGlobalGeminiDir(): string {
|
static getGlobalGeminiDir(): string {
|
||||||
const homeDir = homedir();
|
const homeDir = homedir();
|
||||||
if (!homeDir) {
|
if (!homeDir) {
|
||||||
@@ -253,6 +260,26 @@ export class Storage {
|
|||||||
return path.join(this.getProjectTempDir(), 'plans');
|
return path.join(this.getProjectTempDir(), 'plans');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlansDir(): string {
|
||||||
|
if (this.customPlansDir) {
|
||||||
|
const resolvedPath = path.resolve(
|
||||||
|
this.getProjectRoot(),
|
||||||
|
this.customPlansDir,
|
||||||
|
);
|
||||||
|
const realProjectRoot = resolveToRealPath(this.getProjectRoot());
|
||||||
|
const realResolvedPath = resolveToRealPath(resolvedPath);
|
||||||
|
|
||||||
|
if (!isSubpath(realProjectRoot, realResolvedPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Custom plans directory '${this.customPlansDir}' resolves to '${realResolvedPath}', which is outside the project root '${realProjectRoot}'.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
return this.getProjectTempPlansDir();
|
||||||
|
}
|
||||||
|
|
||||||
getProjectTempTasksDir(): string {
|
getProjectTempTasksDir(): string {
|
||||||
if (this.sessionId) {
|
if (this.sessionId) {
|
||||||
return path.join(this.getProjectTempDir(), this.sessionId, 'tasks');
|
return path.join(this.getProjectTempDir(), this.sessionId, 'tasks');
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ describe('Core System Prompt (prompts.ts)', () => {
|
|||||||
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
|
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
|
||||||
getProjectTempPlansDir: vi
|
getPlansDir: vi.fn().mockReturnValue('/tmp/project-temp/plans'),
|
||||||
.fn()
|
|
||||||
.mockReturnValue('/tmp/project-temp/plans'),
|
|
||||||
},
|
},
|
||||||
isInteractive: vi.fn().mockReturnValue(true),
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
||||||
@@ -509,9 +507,7 @@ describe('Core System Prompt (prompts.ts)', () => {
|
|||||||
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(
|
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(
|
||||||
ApprovalMode.PLAN,
|
ApprovalMode.PLAN,
|
||||||
);
|
);
|
||||||
vi.mocked(mockConfig.storage.getProjectTempPlansDir).mockReturnValue(
|
vi.mocked(mockConfig.storage.getPlansDir).mockReturnValue('/tmp/plans');
|
||||||
'/tmp/plans',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include approved plan path when set in config', () => {
|
it('should include approved plan path when set in config', () => {
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ describe('PromptProvider', () => {
|
|||||||
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
|
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
|
||||||
getProjectTempPlansDir: vi
|
getPlansDir: vi.fn().mockReturnValue('/tmp/project-temp/plans'),
|
||||||
.fn()
|
|
||||||
.mockReturnValue('/tmp/project-temp/plans'),
|
|
||||||
},
|
},
|
||||||
isInteractive: vi.fn().mockReturnValue(true),
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export class PromptProvider {
|
|||||||
'planningWorkflow',
|
'planningWorkflow',
|
||||||
() => ({
|
() => ({
|
||||||
planModeToolsList,
|
planModeToolsList,
|
||||||
plansDir: config.storage.getProjectTempPlansDir(),
|
plansDir: config.storage.getPlansDir(),
|
||||||
approvedPlanPath: config.getApprovedPlanPath(),
|
approvedPlanPath: config.getApprovedPlanPath(),
|
||||||
}),
|
}),
|
||||||
isPlanMode,
|
isPlanMode,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ describe('EnterPlanModeTool', () => {
|
|||||||
mockConfig = {
|
mockConfig = {
|
||||||
setApprovalMode: vi.fn(),
|
setApprovalMode: vi.fn(),
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
|
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
|
||||||
} as unknown as Config['storage'],
|
} as unknown as Config['storage'],
|
||||||
};
|
};
|
||||||
tool = new EnterPlanModeTool(
|
tool = new EnterPlanModeTool(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('ExitPlanModeTool', () => {
|
|||||||
setApprovalMode: vi.fn(),
|
setApprovalMode: vi.fn(),
|
||||||
setApprovedPlanPath: vi.fn(),
|
setApprovedPlanPath: vi.fn(),
|
||||||
storage: {
|
storage: {
|
||||||
getProjectTempPlansDir: vi.fn().mockReturnValue(mockPlansDir),
|
getPlansDir: vi.fn().mockReturnValue(mockPlansDir),
|
||||||
} as unknown as Config['storage'],
|
} as unknown as Config['storage'],
|
||||||
};
|
};
|
||||||
tool = new ExitPlanModeTool(
|
tool = new ExitPlanModeTool(
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
|
|||||||
private config: Config,
|
private config: Config,
|
||||||
messageBus: MessageBus,
|
messageBus: MessageBus,
|
||||||
) {
|
) {
|
||||||
const plansDir = config.storage.getProjectTempPlansDir();
|
const plansDir = config.storage.getPlansDir();
|
||||||
const definition = getExitPlanModeDefinition(plansDir);
|
const definition = getExitPlanModeDefinition(plansDir);
|
||||||
super(
|
super(
|
||||||
EXIT_PLAN_MODE_TOOL_NAME,
|
EXIT_PLAN_MODE_TOOL_NAME,
|
||||||
@@ -78,9 +78,7 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
|
|||||||
|
|
||||||
// Since validateToolParamValues is synchronous, we use a basic synchronous check
|
// Since validateToolParamValues is synchronous, we use a basic synchronous check
|
||||||
// for path traversal safety. High-level async validation is deferred to shouldConfirmExecute.
|
// for path traversal safety. High-level async validation is deferred to shouldConfirmExecute.
|
||||||
const plansDir = resolveToRealPath(
|
const plansDir = resolveToRealPath(this.config.storage.getPlansDir());
|
||||||
this.config.storage.getProjectTempPlansDir(),
|
|
||||||
);
|
|
||||||
const resolvedPath = path.resolve(
|
const resolvedPath = path.resolve(
|
||||||
this.config.getTargetDir(),
|
this.config.getTargetDir(),
|
||||||
params.plan_path,
|
params.plan_path,
|
||||||
@@ -111,7 +109,7 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getSchema(modelId?: string) {
|
override getSchema(modelId?: string) {
|
||||||
const plansDir = this.config.storage.getProjectTempPlansDir();
|
const plansDir = this.config.storage.getPlansDir();
|
||||||
return resolveToolDeclaration(getExitPlanModeDefinition(plansDir), modelId);
|
return resolveToolDeclaration(getExitPlanModeDefinition(plansDir), modelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +139,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
const pathError = await validatePlanPath(
|
const pathError = await validatePlanPath(
|
||||||
this.params.plan_path,
|
this.params.plan_path,
|
||||||
this.config.storage.getProjectTempPlansDir(),
|
this.config.storage.getPlansDir(),
|
||||||
this.config.getTargetDir(),
|
this.config.getTargetDir(),
|
||||||
);
|
);
|
||||||
if (pathError) {
|
if (pathError) {
|
||||||
|
|||||||
@@ -105,6 +105,22 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"plan": {
|
||||||
|
"title": "Plan",
|
||||||
|
"description": "Planning features configuration.",
|
||||||
|
"markdownDescription": "Planning features configuration.\n\n- Category: `General`\n- Requires restart: `yes`\n- Default: `{}`",
|
||||||
|
"default": {},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"directory": {
|
||||||
|
"title": "Plan Directory",
|
||||||
|
"description": "The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.",
|
||||||
|
"markdownDescription": "The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.\n\n- Category: `General`\n- Requires restart: `yes`",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"enablePromptCompletion": {
|
"enablePromptCompletion": {
|
||||||
"title": "Enable Prompt Completion",
|
"title": "Enable Prompt Completion",
|
||||||
"description": "Enable AI-powered prompt completion suggestions while typing.",
|
"description": "Enable AI-powered prompt completion suggestions while typing.",
|
||||||
|
|||||||
Reference in New Issue
Block a user