Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2162da30c8 | |||
| cc5a0dc3c9 | |||
| eeb8aae2ab | |||
| f59f9e9314 | |||
| 832e97cb76 | |||
| 6ee19e1f6b | |||
| aa02abe3e8 | |||
| 0d4d80eb53 | |||
| 0d27afcc7d | |||
| 8948a9075f | |||
| ca0db7ec4a | |||
| 524a84bfec | |||
| 6431ea8724 | |||
| 0db01a9d79 | |||
| 0af0fb6a50 | |||
| 78b0b3dd4d | |||
| c18aae398c | |||
| 94ccdc75c2 | |||
| 5e80faf389 | |||
| c2e92125c3 | |||
| 302b48a0b3 | |||
| 8fa4585ebf | |||
| 5d2983f12a | |||
| 9912ea8686 | |||
| 508aaeda89 | |||
| a6256cdabf | |||
| c316fc6c51 | |||
| 7e6e40c814 | |||
| b25c8137f6 | |||
| 03a8fc3113 | |||
| 393e9a171f | |||
| 59702f913d | |||
| b3439e1458 | |||
| b21b289e26 | |||
| 56a63a35e2 |
@@ -124,7 +124,9 @@ jobs:
|
||||
- name: 'Link Checker'
|
||||
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
|
||||
with:
|
||||
args: '--verbose --accept 200,503 ./**/*.md'
|
||||
# Exclude GEMINI.md because the absolute GitHub URL in CONTRIBUTING.md (which is symlinked)
|
||||
# causes intermittent 429 Too Many Requests errors from GitHub API rate limits.
|
||||
args: '--verbose --accept 200,503 --exclude "GEMINI\\.md" ./**/*.md'
|
||||
fail: true
|
||||
test_linux:
|
||||
name: 'Test (Linux) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
|
||||
|
||||
@@ -22,4 +22,6 @@ jobs:
|
||||
id: 'lychee'
|
||||
uses: 'lycheeverse/lychee-action@885c65f3dc543b57c898c8099f4e08c8afd178a2' # ratchet: lycheeverse/lychee-action@v2.6.1
|
||||
with:
|
||||
args: '--verbose --no-progress --accept 200,503 ./**/*.md'
|
||||
# Exclude GEMINI.md because the absolute GitHub URL in CONTRIBUTING.md (which is symlinked)
|
||||
# causes intermittent 429 Too Many Requests errors from GitHub API rate limits.
|
||||
args: '--verbose --no-progress --accept 200,503 --exclude "GEMINI\\.md" ./**/*.md'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Plan Mode (experimental)
|
||||
# Plan Mode
|
||||
|
||||
Plan Mode is a read-only environment for architecting robust solutions before
|
||||
implementation. It allows you to:
|
||||
@@ -8,65 +8,8 @@ implementation. It allows you to:
|
||||
- **Design:** Understand problems, evaluate trade-offs, and choose a solution.
|
||||
- **Plan:** Align on an execution strategy before any code is modified.
|
||||
|
||||
> **Note:** This is a preview feature currently under active development. Your
|
||||
> feedback is invaluable as we refine this feature. If you have ideas,
|
||||
> suggestions, or encounter issues:
|
||||
>
|
||||
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues) on
|
||||
> GitHub.
|
||||
> - Use the **/bug** command within Gemini CLI to file an issue.
|
||||
|
||||
- [Enabling Plan Mode](#enabling-plan-mode)
|
||||
- [How to use Plan Mode](#how-to-use-plan-mode)
|
||||
- [Entering Plan Mode](#entering-plan-mode)
|
||||
- [Planning Workflow](#planning-workflow)
|
||||
- [Exiting Plan Mode](#exiting-plan-mode)
|
||||
- [Commands](#commands)
|
||||
- [Tool Restrictions](#tool-restrictions)
|
||||
- [Customizing Planning with Skills](#customizing-planning-with-skills)
|
||||
- [Customizing Policies](#customizing-policies)
|
||||
- [Example: Allow git commands in Plan Mode](#example-allow-git-commands-in-plan-mode)
|
||||
- [Example: Enable custom subagents in Plan Mode](#example-enable-custom-subagents-in-plan-mode)
|
||||
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
|
||||
- [Automatic Model Routing](#automatic-model-routing)
|
||||
- [Cleanup](#cleanup)
|
||||
|
||||
## Enabling Plan Mode
|
||||
|
||||
To use Plan Mode, enable it via **/settings** (search for **Plan**) or add the
|
||||
following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"plan": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How to use Plan Mode
|
||||
|
||||
### Entering Plan Mode
|
||||
|
||||
You can configure Gemini CLI to start in Plan Mode by default or enter it
|
||||
manually during a session.
|
||||
|
||||
- **Configuration:** Configure Gemini CLI to start directly in Plan Mode by
|
||||
default:
|
||||
1. Type `/settings` in the CLI.
|
||||
2. Search for **Default Approval Mode**.
|
||||
3. Set the value to **Plan**.
|
||||
|
||||
Alternatively, use the `gemini --approval-mode=plan` CLI flag or manually
|
||||
update:
|
||||
|
||||
```json
|
||||
{
|
||||
"general": {
|
||||
"defaultApprovalMode": "plan"
|
||||
}
|
||||
}
|
||||
```
|
||||
Plan Mode is enabled by default. You can manage this setting using the
|
||||
`/settings` command.
|
||||
|
||||
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
|
||||
(`Default` -> `Auto-Edit` -> `Plan`).
|
||||
|
||||
@@ -144,7 +144,7 @@ they appear in the UI.
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
|
||||
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router. Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
|
||||
|
||||
@@ -268,8 +268,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
- **Description:** Switch to Plan Mode (read-only) and view the current plan if
|
||||
one has been generated.
|
||||
- **Note:** This feature requires the `experimental.plan` setting to be
|
||||
enabled in your configuration.
|
||||
- **Note:** This feature is enabled by default. It can be disabled via the
|
||||
`experimental.plan` setting in your configuration.
|
||||
- **Sub-commands:**
|
||||
- **`copy`**:
|
||||
- **Description:** Copy the currently approved plan to your clipboard.
|
||||
|
||||
@@ -1010,8 +1010,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.plan`** (boolean):
|
||||
- **Description:** Enable planning features (Plan Mode and tools).
|
||||
- **Default:** `false`
|
||||
- **Description:** Enable Plan Mode.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.modelSteering`** (boolean):
|
||||
|
||||
@@ -74,22 +74,12 @@ primary conditions are the tool's name and its arguments.
|
||||
|
||||
The `toolName` in the rule must match the name of the tool being called.
|
||||
|
||||
- **Qualified MCP Names**: Tools from MCP servers are internally prefixed with
|
||||
their server name (for example, `server__tool`). To match an MCP tool, you
|
||||
must use the qualified name or the `mcpName` field. Unqualified names (for
|
||||
example, `toolName = "my_tool"`) don't match MCP tools.
|
||||
- **Wildcards**: You can use wildcards to match multiple tools.
|
||||
- `*`: Matches **any tool** (built-in or MCP).
|
||||
- `server__*`: Matches any tool from a specific MCP server.
|
||||
- `*__toolName`: Matches a specific tool name across **all** MCP servers.
|
||||
- `*__*`: Matches **any tool from any MCP server**.
|
||||
|
||||
> **Note:** **Built-in Protection**: Unqualified rules for built-in tools (like
|
||||
> `read_file` or `run_shell_command`) are strictly scoped. A rule for
|
||||
> `read_file` will only match the built-in tool and won't automatically allow
|
||||
> `my_server__read_file`. This prevents malicious servers from hijacking
|
||||
> permissions of built-in tools.
|
||||
|
||||
#### Arguments pattern
|
||||
|
||||
If `argsPattern` is specified, the tool's arguments are converted to a stable
|
||||
@@ -175,20 +165,6 @@ A rule matches a tool call if all of its conditions are met:
|
||||
provided regular expression. If the arguments don't match the pattern, the
|
||||
rule does not apply.
|
||||
|
||||
### Name sanitization (The "Underscore Rule")
|
||||
|
||||
Gemini CLI sanitizes server and tool names to ensure compatibility with the
|
||||
Gemini API.
|
||||
|
||||
- **Invalid characters**: Any character that is not a letter, number, underscore
|
||||
(`_`), dot (`.`), colon (`:`), or hyphen (`-`) is replaced with an
|
||||
**underscore**.
|
||||
- **Spaces**: Spaces are always converted to underscores. For example, a server
|
||||
named `"Google Drive"` is registered as `"Google_Drive"`.
|
||||
- **Case sensitivity**: Matching is case-insensitive.
|
||||
|
||||
Your TOML rules must use these sanitized names to match correctly at runtime.
|
||||
|
||||
## Configuration
|
||||
|
||||
Policies are defined in `.toml` files. The CLI loads these files from Default,
|
||||
|
||||
@@ -559,9 +559,6 @@ Upon successful connection:
|
||||
with underscores
|
||||
- Names longer than 63 characters are truncated with middle replacement
|
||||
(`___`)
|
||||
- See
|
||||
[Name sanitization](/docs/reference/policy-engine.md#name-sanitization-the-underscore-rule)
|
||||
in the policy engine reference for more details.
|
||||
|
||||
### 3. Conflict resolution
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ describe.skip('ACP Environment and Auth', () => {
|
||||
|
||||
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
|
||||
|
||||
child = spawn('node', [bundlePath, '--experimental-acp'], {
|
||||
child = spawn('node', [bundlePath, '--acp'], {
|
||||
cwd: rig.homeDir!,
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
env: {
|
||||
@@ -120,7 +120,7 @@ describe.skip('ACP Environment and Auth', () => {
|
||||
|
||||
const bundlePath = join(import.meta.dirname, '..', 'bundle/gemini.js');
|
||||
|
||||
child = spawn('node', [bundlePath, '--experimental-acp'], {
|
||||
child = spawn('node', [bundlePath, '--acp'], {
|
||||
cwd: rig.homeDir!,
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
env: {
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('ACP telemetry', () => {
|
||||
'node',
|
||||
[
|
||||
bundlePath,
|
||||
'--experimental-acp',
|
||||
'--acp',
|
||||
'--fake-responses',
|
||||
join(rig.testDir!, 'fake-responses.json'),
|
||||
],
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('extension reloading', () => {
|
||||
return (
|
||||
output.includes(
|
||||
'test-server (from test-extension) - Ready (1 tool)',
|
||||
) && output.includes('- hello')
|
||||
) && output.includes('- mcp_test-server_hello')
|
||||
);
|
||||
},
|
||||
30000, // 30s timeout
|
||||
@@ -148,7 +148,7 @@ describe('extension reloading', () => {
|
||||
return (
|
||||
output.includes(
|
||||
'test-server (from test-extension) - Ready (1 tool)',
|
||||
) && output.includes('- goodbye')
|
||||
) && output.includes('- mcp_test-server_goodbye')
|
||||
);
|
||||
},
|
||||
30000,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260303.34f0c1538",
|
||||
"version": "0.33.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260303.34f0c1538"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.33.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.34.0-nightly.20260303.34f0c1538",
|
||||
"version": "0.33.2",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -36,7 +36,7 @@
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/genai": "^1.30.0",
|
||||
"@google/genai": "1.30.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/supertest": "^6.0.3",
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { CoderAgentExecutor } from './executor.js';
|
||||
import type {
|
||||
ExecutionEventBus,
|
||||
RequestContext,
|
||||
TaskStore,
|
||||
} from '@a2a-js/sdk/server';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { requestStorage } from '../http/requestStorage.js';
|
||||
|
||||
// Mocks for constructor dependencies
|
||||
vi.mock('../config/config.js', () => ({
|
||||
loadConfig: vi.fn().mockReturnValue({
|
||||
getSessionId: () => 'test-session',
|
||||
getTargetDir: () => '/tmp',
|
||||
getCheckpointingEnabled: () => false,
|
||||
}),
|
||||
loadEnvironment: vi.fn(),
|
||||
setTargetDir: vi.fn().mockReturnValue('/tmp'),
|
||||
}));
|
||||
|
||||
vi.mock('../config/settings.js', () => ({
|
||||
loadSettings: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
vi.mock('../config/extension.js', () => ({
|
||||
loadExtensions: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
|
||||
vi.mock('../http/requestStorage.js', () => ({
|
||||
requestStorage: {
|
||||
getStore: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./task.js', () => {
|
||||
const mockTaskInstance = (taskId: string, contextId: string) => ({
|
||||
id: taskId,
|
||||
contextId,
|
||||
taskState: 'working',
|
||||
acceptUserMessage: vi
|
||||
.fn()
|
||||
.mockImplementation(async function* (context, aborted) {
|
||||
const isConfirmation = (
|
||||
context.userMessage.parts as Array<{ kind: string }>
|
||||
).some((p) => p.kind === 'confirmation');
|
||||
// Hang only for main user messages (text), allow confirmations to finish quickly
|
||||
if (!isConfirmation && aborted) {
|
||||
await new Promise((resolve) => {
|
||||
aborted.addEventListener('abort', resolve, { once: true });
|
||||
});
|
||||
}
|
||||
yield { type: 'content', value: 'hello' };
|
||||
}),
|
||||
acceptAgentMessage: vi.fn().mockResolvedValue(undefined),
|
||||
scheduleToolCalls: vi.fn().mockResolvedValue(undefined),
|
||||
waitForPendingTools: vi.fn().mockResolvedValue(undefined),
|
||||
getAndClearCompletedTools: vi.fn().mockReturnValue([]),
|
||||
addToolResponsesToHistory: vi.fn(),
|
||||
sendCompletedToolsToLlm: vi.fn().mockImplementation(async function* () {}),
|
||||
cancelPendingTools: vi.fn(),
|
||||
setTaskStateAndPublishUpdate: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
getMetadata: vi.fn().mockResolvedValue({}),
|
||||
geminiClient: {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
toSDKTask: () => ({
|
||||
id: taskId,
|
||||
contextId,
|
||||
kind: 'task',
|
||||
status: { state: 'working', timestamp: new Date().toISOString() },
|
||||
metadata: {},
|
||||
history: [],
|
||||
artifacts: [],
|
||||
}),
|
||||
});
|
||||
|
||||
const MockTask = vi.fn().mockImplementation(mockTaskInstance);
|
||||
(MockTask as unknown as { create: Mock }).create = vi
|
||||
.fn()
|
||||
.mockImplementation(async (taskId: string, contextId: string) =>
|
||||
mockTaskInstance(taskId, contextId),
|
||||
);
|
||||
|
||||
return { Task: MockTask };
|
||||
});
|
||||
|
||||
describe('CoderAgentExecutor', () => {
|
||||
let executor: CoderAgentExecutor;
|
||||
let mockTaskStore: TaskStore;
|
||||
let mockEventBus: ExecutionEventBus;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockTaskStore = {
|
||||
save: vi.fn().mockResolvedValue(undefined),
|
||||
load: vi.fn().mockResolvedValue(undefined),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
list: vi.fn().mockResolvedValue([]),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
mockEventBus = new EventEmitter() as unknown as ExecutionEventBus;
|
||||
mockEventBus.publish = vi.fn();
|
||||
mockEventBus.finished = vi.fn();
|
||||
|
||||
executor = new CoderAgentExecutor(mockTaskStore);
|
||||
});
|
||||
|
||||
it('should distinguish between primary and secondary execution', async () => {
|
||||
const taskId = 'test-task';
|
||||
const contextId = 'test-context';
|
||||
|
||||
const mockSocket = new EventEmitter();
|
||||
const requestContext = {
|
||||
userMessage: {
|
||||
messageId: 'msg-1',
|
||||
taskId,
|
||||
contextId,
|
||||
parts: [{ kind: 'text', text: 'hi' }],
|
||||
metadata: {
|
||||
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
||||
},
|
||||
},
|
||||
} as unknown as RequestContext;
|
||||
|
||||
// Mock requestStorage for primary
|
||||
(requestStorage.getStore as Mock).mockReturnValue({
|
||||
req: { socket: mockSocket },
|
||||
});
|
||||
|
||||
// First execution (Primary)
|
||||
const primaryPromise = executor.execute(requestContext, mockEventBus);
|
||||
|
||||
// Give it enough time to reach line 490 in executor.ts
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(true);
|
||||
const wrapper = executor.getTask(taskId);
|
||||
expect(wrapper).toBeDefined();
|
||||
|
||||
// Mock requestStorage for secondary
|
||||
const secondarySocket = new EventEmitter();
|
||||
(requestStorage.getStore as Mock).mockReturnValue({
|
||||
req: { socket: secondarySocket },
|
||||
});
|
||||
|
||||
const secondaryRequestContext = {
|
||||
userMessage: {
|
||||
messageId: 'msg-2',
|
||||
taskId,
|
||||
contextId,
|
||||
parts: [{ kind: 'confirmation', callId: '1', outcome: 'proceed' }],
|
||||
metadata: {
|
||||
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
||||
},
|
||||
},
|
||||
} as unknown as RequestContext;
|
||||
|
||||
const secondaryPromise = executor.execute(
|
||||
secondaryRequestContext,
|
||||
mockEventBus,
|
||||
);
|
||||
|
||||
// Secondary execution should NOT add to executingTasks (already there)
|
||||
// and should return early after its loop
|
||||
await secondaryPromise;
|
||||
|
||||
// Task should still be in executingTasks and NOT disposed
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(true);
|
||||
expect(wrapper?.task.dispose).not.toHaveBeenCalled();
|
||||
|
||||
// Now simulate secondary socket closure - it should NOT affect primary
|
||||
secondarySocket.emit('end');
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(true);
|
||||
expect(wrapper?.task.dispose).not.toHaveBeenCalled();
|
||||
|
||||
// Set to terminal state to verify disposal on finish
|
||||
wrapper!.task.taskState = 'completed';
|
||||
|
||||
// Now close primary socket
|
||||
mockSocket.emit('end');
|
||||
|
||||
await primaryPromise;
|
||||
|
||||
expect(
|
||||
(
|
||||
executor as unknown as { executingTasks: Set<string> }
|
||||
).executingTasks.has(taskId),
|
||||
).toBe(false);
|
||||
expect(wrapper?.task.dispose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should evict task from cache when it reaches terminal state', async () => {
|
||||
const taskId = 'test-task-terminal';
|
||||
const contextId = 'test-context';
|
||||
|
||||
const mockSocket = new EventEmitter();
|
||||
(requestStorage.getStore as Mock).mockReturnValue({
|
||||
req: { socket: mockSocket },
|
||||
});
|
||||
|
||||
const requestContext = {
|
||||
userMessage: {
|
||||
messageId: 'msg-1',
|
||||
taskId,
|
||||
contextId,
|
||||
parts: [{ kind: 'text', text: 'hi' }],
|
||||
metadata: {
|
||||
coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
|
||||
},
|
||||
},
|
||||
} as unknown as RequestContext;
|
||||
|
||||
const primaryPromise = executor.execute(requestContext, mockEventBus);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const wrapper = executor.getTask(taskId)!;
|
||||
expect(wrapper).toBeDefined();
|
||||
// Simulate terminal state
|
||||
wrapper.task.taskState = 'completed';
|
||||
|
||||
// Finish primary execution
|
||||
mockSocket.emit('end');
|
||||
await primaryPromise;
|
||||
|
||||
expect(executor.getTask(taskId)).toBeUndefined();
|
||||
expect(wrapper.task.dispose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -252,6 +252,10 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
);
|
||||
await this.taskStore?.save(wrapper.toSDKTask());
|
||||
logger.info(`[CoderAgentExecutor] Task ${taskId} state CANCELED saved.`);
|
||||
|
||||
// Cleanup listener subscriptions to avoid memory leaks.
|
||||
wrapper.task.dispose();
|
||||
this.tasks.delete(taskId);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
@@ -320,23 +324,26 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
if (store) {
|
||||
// Grab the raw socket from the request object
|
||||
const socket = store.req.socket;
|
||||
const onClientEnd = () => {
|
||||
const onSocketEnd = () => {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Client socket closed for task ${taskId}. Cancelling execution.`,
|
||||
`[CoderAgentExecutor] Socket ended for message ${userMessage.messageId} (task ${taskId}). Aborting execution loop.`,
|
||||
);
|
||||
if (!abortController.signal.aborted) {
|
||||
abortController.abort();
|
||||
}
|
||||
// Clean up the listener to prevent memory leaks
|
||||
socket.removeListener('close', onClientEnd);
|
||||
socket.removeListener('end', onSocketEnd);
|
||||
};
|
||||
|
||||
// Listen on the socket's 'end' event (remote closed the connection)
|
||||
socket.on('end', onClientEnd);
|
||||
socket.on('end', onSocketEnd);
|
||||
socket.once('close', () => {
|
||||
socket.removeListener('end', onSocketEnd);
|
||||
});
|
||||
|
||||
// It's also good practice to remove the listener if the task completes successfully
|
||||
abortSignal.addEventListener('abort', () => {
|
||||
socket.removeListener('end', onClientEnd);
|
||||
socket.removeListener('end', onSocketEnd);
|
||||
});
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Socket close handler set up for task ${taskId}.`,
|
||||
@@ -457,6 +464,26 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is the primary/initial execution for this task
|
||||
const isPrimaryExecution = !this.executingTasks.has(taskId);
|
||||
|
||||
if (!isPrimaryExecution) {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Primary execution already active for task ${taskId}. Starting secondary loop for message ${userMessage.messageId}.`,
|
||||
);
|
||||
currentTask.eventBus = eventBus;
|
||||
for await (const _ of currentTask.acceptUserMessage(
|
||||
requestContext,
|
||||
abortController.signal,
|
||||
)) {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Processing user message ${userMessage.messageId} in secondary execution loop for task ${taskId}.`,
|
||||
);
|
||||
}
|
||||
// End this execution-- the original/source will be resumed.
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Starting main execution for message ${userMessage.messageId} for task ${taskId}.`,
|
||||
);
|
||||
@@ -598,18 +625,30 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.executingTasks.delete(taskId);
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Saving final state for task ${taskId}.`,
|
||||
);
|
||||
try {
|
||||
await this.taskStore?.save(wrapper.toSDKTask());
|
||||
logger.info(`[CoderAgentExecutor] Task ${taskId} state saved.`);
|
||||
} catch (saveError) {
|
||||
logger.error(
|
||||
`[CoderAgentExecutor] Failed to save task ${taskId} state in finally block:`,
|
||||
saveError,
|
||||
if (isPrimaryExecution) {
|
||||
this.executingTasks.delete(taskId);
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Saving final state for task ${taskId}.`,
|
||||
);
|
||||
try {
|
||||
await this.taskStore?.save(wrapper.toSDKTask());
|
||||
logger.info(`[CoderAgentExecutor] Task ${taskId} state saved.`);
|
||||
} catch (saveError) {
|
||||
logger.error(
|
||||
`[CoderAgentExecutor] Failed to save task ${taskId} state in finally block:`,
|
||||
saveError,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
['canceled', 'failed', 'completed'].includes(currentTask.taskState)
|
||||
) {
|
||||
logger.info(
|
||||
`[CoderAgentExecutor] Task ${taskId} reached terminal state ${currentTask.taskState}. Evicting and disposing.`,
|
||||
);
|
||||
wrapper.task.dispose();
|
||||
this.tasks.delete(taskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { Task } from './task.js';
|
||||
import {
|
||||
type Config,
|
||||
MessageBusType,
|
||||
ToolConfirmationOutcome,
|
||||
ApprovalMode,
|
||||
Scheduler,
|
||||
type MessageBus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
|
||||
describe('Task Event-Driven Scheduler', () => {
|
||||
let mockConfig: Config;
|
||||
let mockEventBus: ExecutionEventBus;
|
||||
let messageBus: MessageBus;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConfig = createMockConfig({
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
}) as Config;
|
||||
messageBus = mockConfig.getMessageBus();
|
||||
mockEventBus = {
|
||||
publish: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
once: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
finished: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should instantiate Scheduler when enabled', () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
expect(task.scheduler).toBeInstanceOf(Scheduler);
|
||||
});
|
||||
|
||||
it('should subscribe to TOOL_CALLS_UPDATE and map status changes', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'executing',
|
||||
};
|
||||
|
||||
// Simulate MessageBus event
|
||||
// Simulate MessageBus event
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
||||
}
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
});
|
||||
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: expect.objectContaining({
|
||||
state: 'submitted', // initial task state
|
||||
}),
|
||||
metadata: expect.objectContaining({
|
||||
coderAgent: expect.objectContaining({
|
||||
kind: 'tool-call-update',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle tool confirmations by publishing to MessageBus', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
|
||||
// Simulate MessageBus event to stash the correlationId
|
||||
// Simulate MessageBus event
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
||||
}
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
});
|
||||
|
||||
// Simulate A2A client confirmation
|
||||
const part = {
|
||||
kind: 'data',
|
||||
data: {
|
||||
callId: '1',
|
||||
outcome: 'proceed_once',
|
||||
},
|
||||
};
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart(part);
|
||||
expect(handled).toBe(true);
|
||||
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-1',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Rejection (Cancel) and Modification (ModifyWithEditor)', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// Simulate Rejection (Cancel)
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'cancel' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-1',
|
||||
confirmed: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const toolCall2 = {
|
||||
request: { callId: '2', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] });
|
||||
|
||||
// Simulate ModifyWithEditor
|
||||
const handled2 = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '2', outcome: 'modify_with_editor' },
|
||||
});
|
||||
expect(handled2).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-2',
|
||||
confirmed: false,
|
||||
outcome: ToolConfirmationOutcome.ModifyWithEditor,
|
||||
payload: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool operations correctly', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-1',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// Simulate ProceedOnce for MCP
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_once' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-1',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedOnce,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool ProceedAlwaysServer outcome', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-2',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_always_server' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-2',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedAlwaysServer,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool ProceedAlwaysTool outcome', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-3',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_always_tool' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-3',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedAlwaysTool,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle MCP Server tool ProceedAlwaysAndSave outcome', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'call_mcp_tool', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-mcp-4',
|
||||
confirmationDetails: {
|
||||
type: 'mcp',
|
||||
title: 'MCP Server Operation',
|
||||
prompt: 'test_mcp',
|
||||
},
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_always_and_save' },
|
||||
});
|
||||
expect(handled).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-mcp-4',
|
||||
confirmed: true,
|
||||
outcome: ToolConfirmationOutcome.ProceedAlwaysAndSave,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should execute without confirmation in YOLO mode and not transition to input-required', async () => {
|
||||
// Enable YOLO mode
|
||||
const yoloConfig = createMockConfig({
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
getApprovalMode: () => ApprovalMode.YOLO,
|
||||
}) as Config;
|
||||
const yoloMessageBus = yoloConfig.getMessageBus();
|
||||
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', yoloConfig, mockEventBus);
|
||||
task.setTaskStateAndPublishUpdate = vi.fn();
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test', prompt: 'test' },
|
||||
};
|
||||
|
||||
const handler = (yoloMessageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly
|
||||
expect(yoloMessageBus.publish).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
}),
|
||||
);
|
||||
|
||||
// Should NOT transition to input-required since it was auto-approved
|
||||
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith(
|
||||
'input-required',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle output updates via the message bus', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'executing',
|
||||
liveOutput: 'chunk1',
|
||||
};
|
||||
|
||||
// Simulate MessageBus event
|
||||
// Simulate MessageBus event
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
if (!handler) {
|
||||
throw new Error('TOOL_CALLS_UPDATE handler not found');
|
||||
}
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall],
|
||||
});
|
||||
|
||||
// Should publish artifact update for output
|
||||
expect(mockEventBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
kind: 'artifact-update',
|
||||
artifact: expect.objectContaining({
|
||||
artifactId: 'tool-1-output',
|
||||
parts: [{ kind: 'text', text: 'chunk1' }],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should complete artifact creation without hanging', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCallId = 'create-file-123';
|
||||
task['_registerToolCall'](toolCallId, 'executing');
|
||||
|
||||
const toolCall = {
|
||||
request: {
|
||||
callId: toolCallId,
|
||||
name: 'writeFile',
|
||||
args: { path: 'test.sh' },
|
||||
},
|
||||
status: 'success',
|
||||
result: { ok: true },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] });
|
||||
|
||||
// The tool should be complete and registered appropriately, eventually
|
||||
// triggering the toolCompletionPromise resolution when all clear.
|
||||
const internalTask = task as unknown as {
|
||||
completedToolCalls: unknown[];
|
||||
pendingToolCalls: Map<string, string>;
|
||||
};
|
||||
expect(internalTask.completedToolCalls.length).toBe(1);
|
||||
expect(internalTask.pendingToolCalls.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should preserve messageId across multiple text chunks to prevent UI duplication', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
// Initialize the ID for the first turn (happens internally upon LLM stream)
|
||||
task.currentAgentMessageId = 'test-id-123';
|
||||
|
||||
// Simulate sending multiple text chunks
|
||||
task._sendTextContent('chunk 1');
|
||||
task._sendTextContent('chunk 2');
|
||||
|
||||
// Both text contents should have been published with the same messageId
|
||||
const textCalls = (mockEventBus.publish as Mock).mock.calls.filter(
|
||||
(call) => call[0].status?.message?.kind === 'message',
|
||||
);
|
||||
expect(textCalls.length).toBe(2);
|
||||
expect(textCalls[0][0].status.message.messageId).toBe('test-id-123');
|
||||
expect(textCalls[1][0].status.message.messageId).toBe('test-id-123');
|
||||
|
||||
// Simulate starting a new turn by calling getAndClearCompletedTools
|
||||
// (which precedes sendCompletedToolsToLlm where a new ID is minted)
|
||||
task.getAndClearCompletedTools();
|
||||
|
||||
// sendCompletedToolsToLlm internally rolls the ID forward.
|
||||
// Simulate what sendCompletedToolsToLlm does:
|
||||
const internalTask = task as unknown as {
|
||||
setTaskStateAndPublishUpdate: (state: string, change: unknown) => void;
|
||||
};
|
||||
internalTask.setTaskStateAndPublishUpdate('working', {});
|
||||
|
||||
// Simulate what sendCompletedToolsToLlm does: generate a new UUID for the next turn
|
||||
task.currentAgentMessageId = 'test-id-456';
|
||||
|
||||
task._sendTextContent('chunk 3');
|
||||
|
||||
const secondTurnCalls = (mockEventBus.publish as Mock).mock.calls.filter(
|
||||
(call) => call[0].status?.message?.messageId === 'test-id-456',
|
||||
);
|
||||
expect(secondTurnCalls.length).toBe(1);
|
||||
expect(secondTurnCalls[0][0].status.message.parts[0].text).toBe('chunk 3');
|
||||
});
|
||||
|
||||
it('should handle parallel tool calls correctly', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const toolCall1 = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-1',
|
||||
confirmationDetails: { type: 'info', title: 'test 1', prompt: 'test 1' },
|
||||
};
|
||||
|
||||
const toolCall2 = {
|
||||
request: { callId: '2', name: 'pwd', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
// Publish update for both tool calls simultaneously
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1, toolCall2],
|
||||
});
|
||||
|
||||
// Confirm first tool call
|
||||
const handled1 = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '1', outcome: 'proceed_once' },
|
||||
});
|
||||
expect(handled1).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-1',
|
||||
confirmed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Confirm second tool call
|
||||
const handled2 = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: '2', outcome: 'cancel' },
|
||||
});
|
||||
expect(handled2).toBe(true);
|
||||
expect(messageBus.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId: 'corr-2',
|
||||
confirmed: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should wait for executing tools before transitioning to input-required state', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
task.setTaskStateAndPublishUpdate = vi.fn();
|
||||
|
||||
// Register tool 1 as executing
|
||||
task['_registerToolCall']('1', 'executing');
|
||||
|
||||
const toolCall1 = {
|
||||
request: { callId: '1', name: 'ls', args: {} },
|
||||
status: 'executing',
|
||||
};
|
||||
|
||||
const toolCall2 = {
|
||||
request: { callId: '2', name: 'pwd', args: {} },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'corr-2',
|
||||
confirmationDetails: { type: 'info', title: 'test 2', prompt: 'test 2' },
|
||||
};
|
||||
|
||||
const handler = (messageBus.subscribe as Mock).mock.calls.find(
|
||||
(call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE,
|
||||
)?.[1];
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1, toolCall2],
|
||||
});
|
||||
|
||||
// Should NOT transition to input-required yet
|
||||
expect(task.setTaskStateAndPublishUpdate).not.toHaveBeenCalledWith(
|
||||
'input-required',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
// Complete tool 1
|
||||
const toolCall1Complete = {
|
||||
...toolCall1,
|
||||
status: 'success',
|
||||
result: { ok: true },
|
||||
};
|
||||
|
||||
handler({
|
||||
type: MessageBusType.TOOL_CALLS_UPDATE,
|
||||
toolCalls: [toolCall1Complete, toolCall2],
|
||||
});
|
||||
|
||||
// Now it should transition
|
||||
expect(task.setTaskStateAndPublishUpdate).toHaveBeenCalledWith(
|
||||
'input-required',
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore confirmations for unknown tool calls', async () => {
|
||||
// @ts-expect-error - Calling private constructor
|
||||
const task = new Task('task-id', 'context-id', mockConfig, mockEventBus);
|
||||
|
||||
const handled = await (
|
||||
task as unknown as {
|
||||
_handleToolConfirmationPart: (part: unknown) => Promise<boolean>;
|
||||
}
|
||||
)._handleToolConfirmationPart({
|
||||
kind: 'data',
|
||||
data: { callId: 'unknown-id', outcome: 'proceed_once' },
|
||||
});
|
||||
|
||||
// Should return false for unhandled tool call
|
||||
expect(handled).toBe(false);
|
||||
|
||||
// Should not publish anything to the message bus
|
||||
expect(messageBus.publish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -504,13 +504,14 @@ describe('Task', () => {
|
||||
});
|
||||
|
||||
describe('auto-approval', () => {
|
||||
it('should auto-approve tool calls when autoExecute is true', () => {
|
||||
it('should NOT publish ToolCallConfirmationEvent when autoExecute is true', () => {
|
||||
task.autoExecute = true;
|
||||
const onConfirmSpy = vi.fn();
|
||||
const toolCalls = [
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'test-corr-id',
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
onConfirm: onConfirmSpy,
|
||||
@@ -524,9 +525,17 @@ describe('Task', () => {
|
||||
expect(onConfirmSpy).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
const calls = (mockEventBus.publish as Mock).mock.calls;
|
||||
// Search if ToolCallConfirmationEvent was published
|
||||
const confEvent = calls.find(
|
||||
(call) =>
|
||||
call[0].metadata?.coderAgent?.kind ===
|
||||
CoderAgentEvent.ToolCallConfirmationEvent,
|
||||
);
|
||||
expect(confEvent).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should auto-approve tool calls when approval mode is YOLO', () => {
|
||||
it('should NOT publish ToolCallConfirmationEvent when approval mode is YOLO', () => {
|
||||
(mockConfig.getApprovalMode as Mock).mockReturnValue(ApprovalMode.YOLO);
|
||||
task.autoExecute = false;
|
||||
const onConfirmSpy = vi.fn();
|
||||
@@ -534,6 +543,7 @@ describe('Task', () => {
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
correlationId: 'test-corr-id',
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
onConfirm: onConfirmSpy,
|
||||
@@ -547,6 +557,14 @@ describe('Task', () => {
|
||||
expect(onConfirmSpy).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
const calls = (mockEventBus.publish as Mock).mock.calls;
|
||||
// Search if ToolCallConfirmationEvent was published
|
||||
const confEvent = calls.find(
|
||||
(call) =>
|
||||
call[0].metadata?.coderAgent?.kind ===
|
||||
CoderAgentEvent.ToolCallConfirmationEvent,
|
||||
);
|
||||
expect(confEvent).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should NOT auto-approve when autoExecute is false and mode is not YOLO', () => {
|
||||
@@ -567,6 +585,14 @@ describe('Task', () => {
|
||||
task._schedulerToolCallsUpdate(toolCalls);
|
||||
|
||||
expect(onConfirmSpy).not.toHaveBeenCalled();
|
||||
const calls = (mockEventBus.publish as Mock).mock.calls;
|
||||
// Search if ToolCallConfirmationEvent was published
|
||||
const confEvent = calls.find(
|
||||
(call) =>
|
||||
call[0].metadata?.coderAgent?.kind ===
|
||||
CoderAgentEvent.ToolCallConfirmationEvent,
|
||||
);
|
||||
expect(confEvent).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
Scheduler,
|
||||
CoreToolScheduler,
|
||||
type GeminiClient,
|
||||
GeminiEventType,
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
isSubagentProgress,
|
||||
EDIT_TOOL_NAMES,
|
||||
processRestorableToolCalls,
|
||||
MessageBusType,
|
||||
type ToolCallsUpdateMessage,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type ExecutionEventBus,
|
||||
@@ -93,21 +96,30 @@ function isToolCallConfirmationDetails(
|
||||
export class Task {
|
||||
id: string;
|
||||
contextId: string;
|
||||
scheduler: CoreToolScheduler;
|
||||
scheduler: Scheduler | CoreToolScheduler;
|
||||
config: Config;
|
||||
geminiClient: GeminiClient;
|
||||
pendingToolConfirmationDetails: Map<string, ToolCallConfirmationDetails>;
|
||||
pendingCorrelationIds: Map<string, string> = new Map();
|
||||
taskState: TaskState;
|
||||
eventBus?: ExecutionEventBus;
|
||||
completedToolCalls: CompletedToolCall[];
|
||||
processedToolCallIds: Set<string> = new Set();
|
||||
skipFinalTrueAfterInlineEdit = false;
|
||||
modelInfo?: string;
|
||||
currentPromptId: string | undefined;
|
||||
currentAgentMessageId = uuidv4();
|
||||
promptCount = 0;
|
||||
autoExecute: boolean;
|
||||
private get isYoloMatch(): boolean {
|
||||
return (
|
||||
this.autoExecute || this.config.getApprovalMode() === ApprovalMode.YOLO
|
||||
);
|
||||
}
|
||||
|
||||
// For tool waiting logic
|
||||
private pendingToolCalls: Map<string, string> = new Map(); //toolCallId --> status
|
||||
private toolsAlreadyConfirmed: Set<string> = new Set();
|
||||
private toolCompletionPromise?: Promise<void>;
|
||||
private toolCompletionNotifier?: {
|
||||
resolve: () => void;
|
||||
@@ -124,7 +136,13 @@ export class Task {
|
||||
this.id = id;
|
||||
this.contextId = contextId;
|
||||
this.config = config;
|
||||
this.scheduler = this.createScheduler();
|
||||
|
||||
if (this.config.isEventDrivenSchedulerEnabled()) {
|
||||
this.scheduler = this.setupEventDrivenScheduler();
|
||||
} else {
|
||||
this.scheduler = this.createLegacyScheduler();
|
||||
}
|
||||
|
||||
this.geminiClient = this.config.getGeminiClient();
|
||||
this.pendingToolConfirmationDetails = new Map();
|
||||
this.taskState = 'submitted';
|
||||
@@ -224,7 +242,7 @@ export class Task {
|
||||
logger.info(
|
||||
`[Task] Waiting for ${this.pendingToolCalls.size} pending tool(s)...`,
|
||||
);
|
||||
return this.toolCompletionPromise;
|
||||
await this.toolCompletionPromise;
|
||||
}
|
||||
|
||||
cancelPendingTools(reason: string): void {
|
||||
@@ -237,6 +255,13 @@ export class Task {
|
||||
this.toolCompletionNotifier.reject(new Error(reason));
|
||||
}
|
||||
this.pendingToolCalls.clear();
|
||||
this.pendingCorrelationIds.clear();
|
||||
|
||||
if (this.scheduler instanceof Scheduler) {
|
||||
this.scheduler.cancelAll();
|
||||
} else {
|
||||
this.scheduler.cancelAll(new AbortController().signal);
|
||||
}
|
||||
// Reset the promise for any future operations, ensuring it's in a clean state.
|
||||
this._resetToolCompletionPromise();
|
||||
}
|
||||
@@ -249,7 +274,7 @@ export class Task {
|
||||
kind: 'message',
|
||||
role,
|
||||
parts: [{ kind: 'text', text }],
|
||||
messageId: uuidv4(),
|
||||
messageId: role === 'agent' ? this.currentAgentMessageId : uuidv4(),
|
||||
taskId: this.id,
|
||||
contextId: this.contextId,
|
||||
};
|
||||
@@ -417,26 +442,34 @@ export class Task {
|
||||
|
||||
// Only send an update if the status has actually changed.
|
||||
if (hasChanged) {
|
||||
const coderAgentMessage: CoderAgentMessage =
|
||||
tc.status === 'awaiting_approval'
|
||||
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
||||
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
// Skip sending confirmation event if we are going to auto-approve it anyway
|
||||
if (
|
||||
tc.status === 'awaiting_approval' &&
|
||||
tc.confirmationDetails &&
|
||||
this.isYoloMatch
|
||||
) {
|
||||
logger.info(
|
||||
`[Task] Skipping ToolCallConfirmationEvent for ${tc.request.callId} due to YOLO mode.`,
|
||||
);
|
||||
} else {
|
||||
const coderAgentMessage: CoderAgentMessage =
|
||||
tc.status === 'awaiting_approval'
|
||||
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
||||
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
|
||||
const event = this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
coderAgentMessage,
|
||||
message,
|
||||
false, // Always false for these continuous updates
|
||||
);
|
||||
this.eventBus?.publish(event);
|
||||
const event = this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
coderAgentMessage,
|
||||
message,
|
||||
false, // Always false for these continuous updates
|
||||
);
|
||||
this.eventBus?.publish(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
this.autoExecute ||
|
||||
this.config.getApprovalMode() === ApprovalMode.YOLO
|
||||
) {
|
||||
if (this.isYoloMatch) {
|
||||
logger.info(
|
||||
'[Task] ' +
|
||||
(this.autoExecute ? '' : 'YOLO mode enabled. ') +
|
||||
@@ -484,7 +517,7 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
private createScheduler(): CoreToolScheduler {
|
||||
private createLegacyScheduler(): CoreToolScheduler {
|
||||
const scheduler = new CoreToolScheduler({
|
||||
outputUpdateHandler: this._schedulerOutputUpdate.bind(this),
|
||||
onAllToolCallsComplete: this._schedulerAllToolCallsComplete.bind(this),
|
||||
@@ -495,6 +528,171 @@ export class Task {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
private messageBusListener?: (message: ToolCallsUpdateMessage) => void;
|
||||
|
||||
private setupEventDrivenScheduler(): Scheduler {
|
||||
const messageBus = this.config.getMessageBus();
|
||||
const scheduler = new Scheduler({
|
||||
schedulerId: this.id,
|
||||
config: this.config,
|
||||
messageBus,
|
||||
getPreferredEditor: () => DEFAULT_GUI_EDITOR,
|
||||
});
|
||||
|
||||
this.messageBusListener = this.handleEventDrivenToolCallsUpdate.bind(this);
|
||||
messageBus.subscribe<ToolCallsUpdateMessage>(
|
||||
MessageBusType.TOOL_CALLS_UPDATE,
|
||||
this.messageBusListener,
|
||||
);
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.messageBusListener) {
|
||||
this.config
|
||||
.getMessageBus()
|
||||
.unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, this.messageBusListener);
|
||||
this.messageBusListener = undefined;
|
||||
}
|
||||
|
||||
if (this.scheduler instanceof Scheduler) {
|
||||
this.scheduler.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private handleEventDrivenToolCallsUpdate(
|
||||
event: ToolCallsUpdateMessage,
|
||||
): void {
|
||||
if (event.type !== MessageBusType.TOOL_CALLS_UPDATE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolCalls = event.toolCalls;
|
||||
|
||||
toolCalls.forEach((tc) => {
|
||||
this.handleEventDrivenToolCall(tc);
|
||||
});
|
||||
|
||||
this.checkInputRequiredState();
|
||||
}
|
||||
|
||||
private handleEventDrivenToolCall(tc: ToolCall): void {
|
||||
const callId = tc.request.callId;
|
||||
|
||||
// Do not process events for tools that have already been finalized.
|
||||
// This prevents duplicate completions if the state manager emits a snapshot containing
|
||||
// already resolved tools whose IDs were removed from pendingToolCalls.
|
||||
if (
|
||||
this.processedToolCallIds.has(callId) ||
|
||||
this.completedToolCalls.some((c) => c.request.callId === callId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousStatus = this.pendingToolCalls.get(callId);
|
||||
const hasChanged = previousStatus !== tc.status;
|
||||
|
||||
// 1. Handle Output
|
||||
if (tc.status === 'executing' && tc.liveOutput) {
|
||||
this._schedulerOutputUpdate(callId, tc.liveOutput);
|
||||
}
|
||||
|
||||
// 2. Handle terminal states
|
||||
if (
|
||||
tc.status === 'success' ||
|
||||
tc.status === 'error' ||
|
||||
tc.status === 'cancelled'
|
||||
) {
|
||||
this.toolsAlreadyConfirmed.delete(callId);
|
||||
if (hasChanged) {
|
||||
logger.info(
|
||||
`[Task] Tool call ${callId} completed with status: ${tc.status}`,
|
||||
);
|
||||
this.completedToolCalls.push(tc);
|
||||
this._resolveToolCall(callId);
|
||||
}
|
||||
} else {
|
||||
// Keep track of pending tools
|
||||
this._registerToolCall(callId, tc.status);
|
||||
}
|
||||
|
||||
// 3. Handle Confirmation Stash
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
const details = tc.confirmationDetails;
|
||||
|
||||
if (tc.correlationId) {
|
||||
this.pendingCorrelationIds.set(callId, tc.correlationId);
|
||||
}
|
||||
|
||||
this.pendingToolConfirmationDetails.set(callId, {
|
||||
...details,
|
||||
onConfirm: async () => {},
|
||||
} as ToolCallConfirmationDetails);
|
||||
}
|
||||
|
||||
// 4. Publish Status Updates to A2A event bus
|
||||
if (hasChanged) {
|
||||
const coderAgentMessage: CoderAgentMessage =
|
||||
tc.status === 'awaiting_approval'
|
||||
? { kind: CoderAgentEvent.ToolCallConfirmationEvent }
|
||||
: { kind: CoderAgentEvent.ToolCallUpdateEvent };
|
||||
|
||||
const message = this.toolStatusMessage(tc, this.id, this.contextId);
|
||||
const statusUpdate = this._createStatusUpdateEvent(
|
||||
this.taskState,
|
||||
coderAgentMessage,
|
||||
message,
|
||||
false,
|
||||
);
|
||||
this.eventBus?.publish(statusUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
private checkInputRequiredState(): void {
|
||||
if (this.isYoloMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Handle Input Required State
|
||||
let isAwaitingApproval = false;
|
||||
let isExecuting = false;
|
||||
|
||||
for (const [callId, status] of this.pendingToolCalls.entries()) {
|
||||
if (status === 'executing' || status === 'scheduled') {
|
||||
isExecuting = true;
|
||||
} else if (
|
||||
status === 'awaiting_approval' &&
|
||||
!this.toolsAlreadyConfirmed.has(callId)
|
||||
) {
|
||||
isAwaitingApproval = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isAwaitingApproval &&
|
||||
!isExecuting &&
|
||||
!this.skipFinalTrueAfterInlineEdit
|
||||
) {
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
const wasAlreadyInputRequired = this.taskState === 'input-required';
|
||||
|
||||
this.setTaskStateAndPublishUpdate(
|
||||
'input-required',
|
||||
{ kind: CoderAgentEvent.StateChangeEvent },
|
||||
undefined,
|
||||
undefined,
|
||||
/*final*/ true,
|
||||
);
|
||||
|
||||
// Unblock waitForPendingTools to correctly end the executor loop and release the HTTP response stream.
|
||||
// The IDE client will open a new stream with the confirmation reply.
|
||||
if (!wasAlreadyInputRequired && this.toolCompletionNotifier) {
|
||||
this.toolCompletionNotifier.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _pickFields<
|
||||
T extends ToolCall | AnyDeclarativeTool,
|
||||
K extends UnionKeys<T>,
|
||||
@@ -705,7 +903,16 @@ export class Task {
|
||||
};
|
||||
this.setTaskStateAndPublishUpdate('working', stateChange);
|
||||
|
||||
await this.scheduler.schedule(updatedRequests, abortSignal);
|
||||
// Pre-register tools to ensure waitForPendingTools sees them as pending
|
||||
// before the async scheduler enqueues them and fires the event bus update.
|
||||
for (const req of updatedRequests) {
|
||||
if (!this.pendingToolCalls.has(req.callId)) {
|
||||
this._registerToolCall(req.callId, 'scheduled');
|
||||
}
|
||||
}
|
||||
|
||||
// Fire and forget so we don't block the executor loop before waitForPendingTools can be called
|
||||
void this.scheduler.schedule(updatedRequests, abortSignal);
|
||||
}
|
||||
|
||||
async acceptAgentMessage(event: ServerGeminiStreamEvent): Promise<void> {
|
||||
@@ -829,9 +1036,15 @@ export class Task {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!part.data['outcome']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callId = part.data['callId'];
|
||||
const outcomeString = part.data['outcome'];
|
||||
|
||||
this.toolsAlreadyConfirmed.add(callId);
|
||||
|
||||
let confirmationOutcome: ToolConfirmationOutcome | undefined;
|
||||
|
||||
if (outcomeString === 'proceed_once') {
|
||||
@@ -844,6 +1057,8 @@ export class Task {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysServer;
|
||||
} else if (outcomeString === 'proceed_always_tool') {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysTool;
|
||||
} else if (outcomeString === 'proceed_always_and_save') {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ProceedAlwaysAndSave;
|
||||
} else if (outcomeString === 'modify_with_editor') {
|
||||
confirmationOutcome = ToolConfirmationOutcome.ModifyWithEditor;
|
||||
} else {
|
||||
@@ -854,8 +1069,9 @@ export class Task {
|
||||
}
|
||||
|
||||
const confirmationDetails = this.pendingToolConfirmationDetails.get(callId);
|
||||
const correlationId = this.pendingCorrelationIds.get(callId);
|
||||
|
||||
if (!confirmationDetails) {
|
||||
if (!confirmationDetails && !correlationId) {
|
||||
logger.warn(
|
||||
`[Task] Received tool confirmation for unknown or already processed callId: ${callId}`,
|
||||
);
|
||||
@@ -877,24 +1093,35 @@ export class Task {
|
||||
// This will trigger the scheduler to continue or cancel the specific tool.
|
||||
// The scheduler's onToolCallsUpdate will then reflect the new state (e.g., executing or cancelled).
|
||||
|
||||
// If `edit` tool call, pass updated payload if presesent
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
const newContent = part.data['newContent'];
|
||||
const payload =
|
||||
typeof newContent === 'string'
|
||||
? ({ newContent } as ToolConfirmationPayload)
|
||||
: undefined;
|
||||
this.skipFinalTrueAfterInlineEdit = !!payload;
|
||||
try {
|
||||
// If `edit` tool call, pass updated payload if present
|
||||
const newContent = part.data['newContent'];
|
||||
const payload =
|
||||
confirmationDetails?.type === 'edit' && typeof newContent === 'string'
|
||||
? ({ newContent } as ToolConfirmationPayload)
|
||||
: undefined;
|
||||
this.skipFinalTrueAfterInlineEdit = !!payload;
|
||||
|
||||
try {
|
||||
if (correlationId) {
|
||||
await this.config.getMessageBus().publish({
|
||||
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
|
||||
correlationId,
|
||||
confirmed:
|
||||
confirmationOutcome !== ToolConfirmationOutcome.Cancel &&
|
||||
confirmationOutcome !==
|
||||
ToolConfirmationOutcome.ModifyWithEditor,
|
||||
outcome: confirmationOutcome,
|
||||
payload,
|
||||
});
|
||||
} else if (confirmationDetails?.onConfirm) {
|
||||
// Fallback for legacy callback-based confirmation
|
||||
await confirmationDetails.onConfirm(confirmationOutcome, payload);
|
||||
} finally {
|
||||
// Once confirmationDetails.onConfirm finishes (or fails) with a payload,
|
||||
// reset skipFinalTrueAfterInlineEdit so that external callers receive
|
||||
// their call has been completed.
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
}
|
||||
} else {
|
||||
await confirmationDetails.onConfirm(confirmationOutcome);
|
||||
} finally {
|
||||
// Once confirmation payload is sent or callback finishes,
|
||||
// reset skipFinalTrueAfterInlineEdit so that external callers receive
|
||||
// their call has been completed.
|
||||
this.skipFinalTrueAfterInlineEdit = false;
|
||||
}
|
||||
} finally {
|
||||
if (gcpProject) {
|
||||
@@ -910,6 +1137,7 @@ export class Task {
|
||||
// Note !== ToolConfirmationOutcome.ModifyWithEditor does not work!
|
||||
if (confirmationOutcome !== 'modify_with_editor') {
|
||||
this.pendingToolConfirmationDetails.delete(callId);
|
||||
this.pendingCorrelationIds.delete(callId);
|
||||
}
|
||||
|
||||
// If outcome is Cancel, scheduler should update status to 'cancelled', which then resolves the tool.
|
||||
@@ -943,6 +1171,9 @@ export class Task {
|
||||
|
||||
getAndClearCompletedTools(): CompletedToolCall[] {
|
||||
const tools = [...this.completedToolCalls];
|
||||
for (const tool of tools) {
|
||||
this.processedToolCallIds.add(tool.request.callId);
|
||||
}
|
||||
this.completedToolCalls = [];
|
||||
return tools;
|
||||
}
|
||||
@@ -1003,6 +1234,7 @@ export class Task {
|
||||
};
|
||||
// Set task state to working as we are about to call LLM
|
||||
this.setTaskStateAndPublishUpdate('working', stateChange);
|
||||
this.currentAgentMessageId = uuidv4();
|
||||
yield* this.geminiClient.sendMessageStream(
|
||||
llmParts,
|
||||
aborted,
|
||||
@@ -1024,6 +1256,10 @@ export class Task {
|
||||
if (confirmationHandled) {
|
||||
anyConfirmationHandled = true;
|
||||
// If a confirmation was handled, the scheduler will now run the tool (or cancel it).
|
||||
// We resolve the toolCompletionPromise manually in checkInputRequiredState
|
||||
// to break the original execution loop, so we must reset it here so the
|
||||
// new loop correctly awaits the tool's final execution.
|
||||
this._resetToolCompletionPromise();
|
||||
// We don't send anything to the LLM for this part.
|
||||
// The subsequent tool execution will eventually lead to resolveToolCall.
|
||||
continue;
|
||||
@@ -1038,6 +1274,7 @@ export class Task {
|
||||
if (hasContentForLlm) {
|
||||
this.currentPromptId =
|
||||
this.config.getSessionId() + '########' + this.promptCount++;
|
||||
this.currentAgentMessageId = uuidv4();
|
||||
logger.info('[Task] Sending new parts to LLM.');
|
||||
const stateChange: StateChange = {
|
||||
kind: CoderAgentEvent.StateChangeEvent,
|
||||
@@ -1083,7 +1320,6 @@ export class Task {
|
||||
if (content === '') {
|
||||
return;
|
||||
}
|
||||
logger.info('[Task] Sending text content to event bus.');
|
||||
const message = this._createTextMessage(content);
|
||||
const textContent: TextContent = {
|
||||
kind: CoderAgentEvent.TextContentEvent,
|
||||
@@ -1115,7 +1351,7 @@ export class Task {
|
||||
data: content,
|
||||
} as Part,
|
||||
],
|
||||
messageId: uuidv4(),
|
||||
messageId: this.currentAgentMessageId,
|
||||
taskId: this.id,
|
||||
contextId: this.contextId,
|
||||
};
|
||||
|
||||
@@ -105,6 +105,8 @@ export async function loadConfig(
|
||||
checkpointing,
|
||||
interactive: true,
|
||||
enableInteractiveShell: true,
|
||||
enableEventDrivenScheduler:
|
||||
settings.experimental?.enableEventDrivenScheduler ?? true,
|
||||
ptyInfo: 'auto',
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ export interface Settings {
|
||||
showMemoryUsage?: boolean;
|
||||
checkpointing?: CheckpointingSettings;
|
||||
folderTrust?: boolean;
|
||||
general?: {
|
||||
previewFeatures?: boolean;
|
||||
};
|
||||
experimental?: {
|
||||
enableEventDrivenScheduler?: boolean;
|
||||
};
|
||||
|
||||
// Git-aware file filtering settings
|
||||
fileFiltering?: {
|
||||
|
||||
@@ -64,6 +64,7 @@ export function createMockConfig(
|
||||
getEmbeddingModel: vi.fn().mockReturnValue('text-embedding-004'),
|
||||
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||
getUserTier: vi.fn(),
|
||||
isEventDrivenSchedulerEnabled: vi.fn().mockReturnValue(false),
|
||||
getMessageBus: vi.fn(),
|
||||
getPolicyEngine: vi.fn(),
|
||||
getEnableExtensionReloading: vi.fn().mockReturnValue(false),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260303.34f0c1538",
|
||||
"version": "0.33.2",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,12 +26,12 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260303.34f0c1538"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.33.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.41.0",
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"ansi-escapes": "^7.3.0",
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
type Mocked,
|
||||
} from 'vitest';
|
||||
import { GeminiAgent, Session } from './zedIntegration.js';
|
||||
import { GeminiAgent, Session } from './acpClient.js';
|
||||
import type { CommandHandler } from './commandHandler.js';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
import {
|
||||
@@ -172,7 +172,7 @@ describe('GeminiAgent', () => {
|
||||
unsubscribe: vi.fn(),
|
||||
}),
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
@@ -208,7 +208,16 @@ describe('GeminiAgent', () => {
|
||||
});
|
||||
|
||||
expect(response.protocolVersion).toBe(acp.PROTOCOL_VERSION);
|
||||
expect(response.authMethods).toHaveLength(3);
|
||||
expect(response.authMethods).toHaveLength(4);
|
||||
const gatewayAuth = response.authMethods?.find(
|
||||
(m) => m.id === AuthType.GATEWAY,
|
||||
);
|
||||
expect(gatewayAuth?._meta).toEqual({
|
||||
gateway: {
|
||||
protocol: 'google',
|
||||
restartRequired: 'false',
|
||||
},
|
||||
});
|
||||
const geminiAuth = response.authMethods?.find(
|
||||
(m) => m.id === AuthType.USE_GEMINI,
|
||||
);
|
||||
@@ -228,6 +237,8 @@ describe('GeminiAgent', () => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.LOGIN_WITH_GOOGLE,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
@@ -247,6 +258,8 @@ describe('GeminiAgent', () => {
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.USE_GEMINI,
|
||||
'test-api-key',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
@@ -255,6 +268,45 @@ describe('GeminiAgent', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should authenticate correctly with gateway method', async () => {
|
||||
await agent.authenticate({
|
||||
methodId: AuthType.GATEWAY,
|
||||
_meta: {
|
||||
gateway: {
|
||||
baseUrl: 'https://example.com',
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
},
|
||||
},
|
||||
} as unknown as acp.AuthenticateRequest);
|
||||
|
||||
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
|
||||
AuthType.GATEWAY,
|
||||
undefined,
|
||||
'https://example.com',
|
||||
{ Authorization: 'Bearer token' },
|
||||
);
|
||||
expect(mockSettings.setValue).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'security.auth.selectedType',
|
||||
AuthType.GATEWAY,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw acp.RequestError when gateway payload is malformed', async () => {
|
||||
await expect(
|
||||
agent.authenticate({
|
||||
methodId: AuthType.GATEWAY,
|
||||
_meta: {
|
||||
gateway: {
|
||||
// Invalid baseUrl
|
||||
baseUrl: 123,
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
},
|
||||
},
|
||||
} as unknown as acp.AuthenticateRequest),
|
||||
).rejects.toThrow(/Malformed gateway payload/);
|
||||
});
|
||||
|
||||
it('should create a new session', async () => {
|
||||
vi.useFakeTimers();
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
@@ -598,7 +650,7 @@ describe('Session', () => {
|
||||
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
|
||||
setApprovalMode: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
|
||||
getGitService: vi.fn().mockResolvedValue({} as GitService),
|
||||
waitForMcpInit: vi.fn(),
|
||||
@@ -70,7 +70,7 @@ import { runExitCleanup } from '../utils/cleanup.js';
|
||||
import { SessionSelector } from '../utils/sessionUtils.js';
|
||||
|
||||
import { CommandHandler } from './commandHandler.js';
|
||||
export async function runZedIntegration(
|
||||
export async function runAcpClient(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
argv: CliArgs,
|
||||
@@ -98,6 +98,8 @@ export class GeminiAgent {
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private clientCapabilities: acp.ClientCapabilities | undefined;
|
||||
private apiKey: string | undefined;
|
||||
private baseUrl: string | undefined;
|
||||
private customHeaders: Record<string, string> | undefined;
|
||||
|
||||
constructor(
|
||||
private config: Config,
|
||||
@@ -131,6 +133,17 @@ export class GeminiAgent {
|
||||
name: 'Vertex AI',
|
||||
description: 'Use an API key with Vertex AI GenAI API',
|
||||
},
|
||||
{
|
||||
id: AuthType.GATEWAY,
|
||||
name: 'AI API Gateway',
|
||||
description: 'Use a custom AI API Gateway',
|
||||
_meta: {
|
||||
gateway: {
|
||||
protocol: 'google',
|
||||
restartRequired: 'false',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await this.config.initialize();
|
||||
@@ -179,7 +192,38 @@ export class GeminiAgent {
|
||||
if (apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
await this.config.refreshAuth(method, apiKey ?? this.apiKey);
|
||||
|
||||
// Extract gateway details if present
|
||||
const gatewaySchema = z.object({
|
||||
baseUrl: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
let baseUrl: string | undefined;
|
||||
let headers: Record<string, string> | undefined;
|
||||
|
||||
if (meta?.['gateway']) {
|
||||
const result = gatewaySchema.safeParse(meta['gateway']);
|
||||
if (result.success) {
|
||||
baseUrl = result.data.baseUrl;
|
||||
headers = result.data.headers;
|
||||
} else {
|
||||
throw new acp.RequestError(
|
||||
-32602,
|
||||
`Malformed gateway payload: ${result.error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.baseUrl = baseUrl;
|
||||
this.customHeaders = headers;
|
||||
|
||||
await this.config.refreshAuth(
|
||||
method,
|
||||
apiKey ?? this.apiKey,
|
||||
baseUrl,
|
||||
headers,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new acp.RequestError(-32000, getAcpErrorMessage(e));
|
||||
}
|
||||
@@ -209,7 +253,12 @@ export class GeminiAgent {
|
||||
let isAuthenticated = false;
|
||||
let authErrorMessage = '';
|
||||
try {
|
||||
await config.refreshAuth(authType, this.apiKey);
|
||||
await config.refreshAuth(
|
||||
authType,
|
||||
this.apiKey,
|
||||
this.baseUrl,
|
||||
this.customHeaders,
|
||||
);
|
||||
isAuthenticated = true;
|
||||
|
||||
// Extra validation for Gemini API key
|
||||
@@ -371,7 +420,12 @@ export class GeminiAgent {
|
||||
// This satisfies the security requirement to verify the user before executing
|
||||
// potentially unsafe server definitions.
|
||||
try {
|
||||
await config.refreshAuth(selectedAuthType, this.apiKey);
|
||||
await config.refreshAuth(
|
||||
selectedAuthType,
|
||||
this.apiKey,
|
||||
this.baseUrl,
|
||||
this.customHeaders,
|
||||
);
|
||||
} catch (e) {
|
||||
debugLogger.error(`Authentication failed: ${e}`);
|
||||
throw acp.RequestError.authRequired();
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type Mocked,
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { GeminiAgent } from './zedIntegration.js';
|
||||
import { GeminiAgent } from './acpClient.js';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
import {
|
||||
ApprovalMode,
|
||||
@@ -92,7 +92,7 @@ describe('GeminiAgent Session Resume', () => {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
},
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getModel: vi.fn().mockReturnValue('gemini-pro'),
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
|
||||
@@ -204,6 +204,11 @@ describe('GeminiAgent Session Resume', () => {
|
||||
name: 'YOLO',
|
||||
description: 'Auto-approves all tools',
|
||||
},
|
||||
{
|
||||
id: ApprovalMode.PLAN,
|
||||
name: 'Plan',
|
||||
description: 'Read-only mode',
|
||||
},
|
||||
],
|
||||
currentModeId: ApprovalMode.DEFAULT,
|
||||
},
|
||||
@@ -2630,13 +2630,13 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => {
|
||||
it('should allow plan approval mode by default when --approval-mode=plan is used', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({});
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should pass planSettings.directory from settings to config', async () => {
|
||||
|
||||
@@ -81,7 +81,8 @@ export interface CliArgs {
|
||||
policy: string[] | undefined;
|
||||
allowedMcpServerNames: string[] | undefined;
|
||||
allowedTools: string[] | undefined;
|
||||
experimentalAcp: boolean | undefined;
|
||||
acp?: boolean;
|
||||
experimentalAcp?: boolean;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
resume: string | typeof RESUME_LATEST | undefined;
|
||||
@@ -177,10 +178,15 @@ export async function parseArguments(
|
||||
.filter(Boolean),
|
||||
),
|
||||
})
|
||||
.option('experimental-acp', {
|
||||
.option('acp', {
|
||||
type: 'boolean',
|
||||
description: 'Starts the agent in ACP mode',
|
||||
})
|
||||
.option('experimental-acp', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Starts the agent in ACP mode (deprecated, use --acp instead)',
|
||||
})
|
||||
.option('allowed-mcp-server-names', {
|
||||
type: 'array',
|
||||
string: true,
|
||||
@@ -632,6 +638,7 @@ export async function loadCliConfig(
|
||||
// -i/--prompt-interactive forces interactive mode with an initial prompt
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.acp ||
|
||||
!!argv.experimentalAcp ||
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
@@ -758,6 +765,7 @@ export async function loadCliConfig(
|
||||
}
|
||||
|
||||
return new Config({
|
||||
acpMode: !!argv.acp || !!argv.experimentalAcp,
|
||||
sessionId,
|
||||
clientVersion: await getVersion(),
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
@@ -821,7 +829,7 @@ export async function loadCliConfig(
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: resolvedModel,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns,
|
||||
experimentalZedIntegration: argv.experimentalAcp || false,
|
||||
|
||||
listExtensions: argv.listExtensions || false,
|
||||
listSessions: argv.listSessions || false,
|
||||
deleteSession: argv.deleteSession,
|
||||
@@ -867,6 +875,7 @@ export async function loadCliConfig(
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors,
|
||||
billing: settings.billing,
|
||||
maxAttempts: settings.general?.maxAttempts,
|
||||
ptyInfo: ptyInfo?.name,
|
||||
disableLLMCorrection: settings.tools?.disableLLMCorrection,
|
||||
|
||||
@@ -10,15 +10,11 @@
|
||||
<text x="0" y="53" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * server2 (remote): https://remote.com </text>
|
||||
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will append info to your gemini.md context using my-context.md </text>
|
||||
<text x="0" y="87" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will exclude the following core tools: tool1,tool2 </text>
|
||||
<text x="0" y="121" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-weight="bold">Agent Skills:</text>
|
||||
<text x="0" y="121" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Agent Skills: </text>
|
||||
<text x="0" y="155" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will install the following agent skills: </text>
|
||||
<text x="0" y="189" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
|
||||
<text x="36" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">skill1</text>
|
||||
<text x="90" y="189" fill="#ffffff" textLength="810" lengthAdjust="spacingAndGlyphs">: desc1 </text>
|
||||
<text x="0" y="189" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * skill1: desc1 </text>
|
||||
<text x="0" y="206" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory) </text>
|
||||
<text x="0" y="240" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
|
||||
<text x="36" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">skill2</text>
|
||||
<text x="90" y="240" fill="#ffffff" textLength="810" lengthAdjust="spacingAndGlyphs">: desc2 </text>
|
||||
<text x="0" y="240" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * skill2: desc2 </text>
|
||||
<text x="0" y="257" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory) </text>
|
||||
<text x="0" y="308" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">The extension you are about to install may have been created by a third-party developer and sourced</text>
|
||||
<text x="0" y="325" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">from a public repository. Google does not vet, endorse, or guarantee the functionality or security</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -5,11 +5,9 @@
|
||||
<rect width="920" height="343" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing extension "test-ext". </text>
|
||||
<text x="0" y="36" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-weight="bold">Agent Skills:</text>
|
||||
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Agent Skills: </text>
|
||||
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will install the following agent skills: </text>
|
||||
<text x="0" y="104" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
|
||||
<text x="36" y="104" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs" font-weight="bold">locked-skill</text>
|
||||
<text x="144" y="104" fill="#ffffff" textLength="756" lengthAdjust="spacingAndGlyphs">: A skill in a locked dir </text>
|
||||
<text x="0" y="104" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * locked-skill: A skill in a locked dir </text>
|
||||
<text x="0" y="121" fill="#ffffff" textLength="405" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/locked/SKILL.md) </text>
|
||||
<text x="405" y="121" fill="#cd0000" textLength="342" lengthAdjust="spacingAndGlyphs">⚠️ (Could not count items in directory)</text>
|
||||
<text x="0" y="172" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">The extension you are about to install may have been created by a third-party developer and sourced</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -6,9 +6,7 @@
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing agent skill(s) from "https://example.com/repo.git". </text>
|
||||
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">The following agent skill(s) will be installing: </text>
|
||||
<text x="0" y="70" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs"> * </text>
|
||||
<text x="36" y="70" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">skill1</text>
|
||||
<text x="90" y="70" fill="#ffffff" textLength="810" lengthAdjust="spacingAndGlyphs">: desc1 </text>
|
||||
<text x="0" y="70" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * skill1: desc1 </text>
|
||||
<text x="0" y="87" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> (Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory) </text>
|
||||
<text x="0" y="121" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Install Destination: /mock/target/dir </text>
|
||||
<text x="0" y="155" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">Agent skills inject specialized instructions and domain-specific knowledge into the agent's system</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -89,13 +89,13 @@ describe('Policy Engine Integration Tests', () => {
|
||||
// Tools from allowed server should be allowed
|
||||
// Tools from allowed server should be allowed
|
||||
expect(
|
||||
(await engine.check({ name: 'allowed-server__tool1' }, undefined))
|
||||
(await engine.check({ name: 'mcp_allowed-server_tool1' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(
|
||||
await engine.check(
|
||||
{ name: 'allowed-server__another_tool' },
|
||||
{ name: 'mcp_allowed-server_another_tool' },
|
||||
undefined,
|
||||
)
|
||||
).decision,
|
||||
@@ -103,13 +103,13 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Tools from trusted server should be allowed
|
||||
expect(
|
||||
(await engine.check({ name: 'trusted-server__tool1' }, undefined))
|
||||
(await engine.check({ name: 'mcp_trusted-server_tool1' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(
|
||||
await engine.check(
|
||||
{ name: 'trusted-server__special_tool' },
|
||||
{ name: 'mcp_trusted-server_special_tool' },
|
||||
undefined,
|
||||
)
|
||||
).decision,
|
||||
@@ -117,17 +117,17 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Tools from blocked server should be denied
|
||||
expect(
|
||||
(await engine.check({ name: 'blocked-server__tool1' }, undefined))
|
||||
(await engine.check({ name: 'mcp_blocked-server_tool1' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
expect(
|
||||
(await engine.check({ name: 'blocked-server__any_tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_blocked-server_any_tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
|
||||
// Tools from unknown servers should use default
|
||||
expect(
|
||||
(await engine.check({ name: 'unknown-server__tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_unknown-server_tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
@@ -147,12 +147,16 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// ANY tool with a server name should be allowed
|
||||
expect(
|
||||
(await engine.check({ name: 'mcp-server__tool' }, 'mcp-server'))
|
||||
(await engine.check({ name: 'mcp_mcp-server_tool' }, 'mcp-server'))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'another-server__tool' }, 'another-server'))
|
||||
.decision,
|
||||
(
|
||||
await engine.check(
|
||||
{ name: 'mcp_another-server_tool' },
|
||||
'another-server',
|
||||
)
|
||||
).decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
|
||||
// Built-in tools should NOT be allowed by the MCP wildcard
|
||||
@@ -167,7 +171,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
allowed: ['my-server'],
|
||||
},
|
||||
tools: {
|
||||
exclude: ['my-server__dangerous-tool'],
|
||||
exclude: ['mcp_my-server_dangerous-tool'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,20 +184,24 @@ describe('Policy Engine Integration Tests', () => {
|
||||
// MCP server allowed (priority 4.1) provides general allow for server
|
||||
// MCP server allowed (priority 4.1) provides general allow for server
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__safe-tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_my-server_safe-tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
// But specific tool exclude (priority 4.4) wins over server allow
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__dangerous-tool' }, undefined))
|
||||
.decision,
|
||||
(
|
||||
await engine.check(
|
||||
{ name: 'mcp_my-server_dangerous-tool' },
|
||||
undefined,
|
||||
)
|
||||
).decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should handle complex mixed configurations', async () => {
|
||||
const settings: Settings = {
|
||||
tools: {
|
||||
allowed: ['custom-tool', 'my-server__special-tool'],
|
||||
allowed: ['custom-tool', 'mcp_my-server_special-tool'],
|
||||
exclude: ['glob', 'dangerous-tool'],
|
||||
},
|
||||
mcp: {
|
||||
@@ -238,21 +246,21 @@ describe('Policy Engine Integration Tests', () => {
|
||||
(await engine.check({ name: 'custom-tool' }, undefined)).decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__special-tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_my-server_special-tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
|
||||
// MCP server tools
|
||||
expect(
|
||||
(await engine.check({ name: 'allowed-server__tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_allowed-server_tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'trusted-server__tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_trusted-server_tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'blocked-server__tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_blocked-server_tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
|
||||
@@ -479,7 +487,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
expect(blockedToolRule?.priority).toBe(4.4); // Command line exclude
|
||||
|
||||
const blockedServerRule = rules.find(
|
||||
(r) => r.toolName === 'blocked-server__*',
|
||||
(r) => r.toolName === 'mcp_blocked-server_*',
|
||||
);
|
||||
expect(blockedServerRule?.priority).toBe(4.9); // MCP server exclude
|
||||
|
||||
@@ -489,11 +497,13 @@ describe('Policy Engine Integration Tests', () => {
|
||||
expect(specificToolRule?.priority).toBe(4.3); // Command line allow
|
||||
|
||||
const trustedServerRule = rules.find(
|
||||
(r) => r.toolName === 'trusted-server__*',
|
||||
(r) => r.toolName === 'mcp_trusted-server_*',
|
||||
);
|
||||
expect(trustedServerRule?.priority).toBe(4.2); // MCP trusted server
|
||||
|
||||
const mcpServerRule = rules.find((r) => r.toolName === 'mcp-server__*');
|
||||
const mcpServerRule = rules.find(
|
||||
(r) => r.toolName === 'mcp_mcp-server_*',
|
||||
);
|
||||
expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server
|
||||
|
||||
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
|
||||
@@ -505,18 +515,19 @@ describe('Policy Engine Integration Tests', () => {
|
||||
(await engine.check({ name: 'blocked-tool' }, undefined)).decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
expect(
|
||||
(await engine.check({ name: 'blocked-server__any' }, undefined))
|
||||
(await engine.check({ name: 'mcp_blocked-server_any' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
expect(
|
||||
(await engine.check({ name: 'specific-tool' }, undefined)).decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'trusted-server__any' }, undefined))
|
||||
(await engine.check({ name: 'mcp_trusted-server_any' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'mcp-server__any' }, undefined)).decision,
|
||||
(await engine.check({ name: 'mcp_mcp-server_any' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect((await engine.check({ name: 'glob' }, undefined)).decision).toBe(
|
||||
PolicyDecision.ALLOW,
|
||||
@@ -545,7 +556,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Exclusion (195) should win over trust (90)
|
||||
expect(
|
||||
(await engine.check({ name: 'conflicted-server__tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_conflicted-server_tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
@@ -556,7 +567,7 @@ describe('Policy Engine Integration Tests', () => {
|
||||
excluded: ['my-server'], // Priority 195 - DENY
|
||||
},
|
||||
tools: {
|
||||
allowed: ['my-server__special-tool'], // Priority 100 - ALLOW
|
||||
allowed: ['mcp_my-server_special-tool'], // Priority 100 - ALLOW
|
||||
},
|
||||
};
|
||||
|
||||
@@ -569,11 +580,11 @@ describe('Policy Engine Integration Tests', () => {
|
||||
// Server exclusion (195) wins over specific tool allow (100)
|
||||
// This might be counterintuitive but follows the priority system
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__special-tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_my-server_special-tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__other-tool' }, undefined))
|
||||
(await engine.check({ name: 'mcp_my-server_other-tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
@@ -643,13 +654,13 @@ describe('Policy Engine Integration Tests', () => {
|
||||
const tool3Rule = rules.find((r) => r.toolName === 'tool3');
|
||||
expect(tool3Rule?.priority).toBe(4.4); // Excluded tools (user tier)
|
||||
|
||||
const server2Rule = rules.find((r) => r.toolName === 'server2__*');
|
||||
const server2Rule = rules.find((r) => r.toolName === 'mcp_server2_*');
|
||||
expect(server2Rule?.priority).toBe(4.9); // Excluded servers (user tier)
|
||||
|
||||
const tool1Rule = rules.find((r) => r.toolName === 'tool1');
|
||||
expect(tool1Rule?.priority).toBe(4.3); // Allowed tools (user tier)
|
||||
|
||||
const server1Rule = rules.find((r) => r.toolName === 'server1__*');
|
||||
const server1Rule = rules.find((r) => r.toolName === 'mcp_server1_*');
|
||||
expect(server1Rule?.priority).toBe(4.1); // Allowed servers (user tier)
|
||||
|
||||
const globRule = rules.find((r) => r.toolName === 'glob');
|
||||
|
||||
@@ -424,12 +424,10 @@ describe('SettingsSchema', () => {
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting.type).toBe('boolean');
|
||||
expect(setting.category).toBe('Experimental');
|
||||
expect(setting.default).toBe(false);
|
||||
expect(setting.default).toBe(true);
|
||||
expect(setting.requiresRestart).toBe(true);
|
||||
expect(setting.showInDialog).toBe(true);
|
||||
expect(setting.description).toBe(
|
||||
'Enable planning features (Plan Mode and tools).',
|
||||
);
|
||||
expect(setting.description).toBe('Enable Plan Mode.');
|
||||
});
|
||||
|
||||
it('should have hooksConfig.notifications setting in schema', () => {
|
||||
|
||||
@@ -1802,8 +1802,8 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Plan',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable planning features (Plan Mode and tools).',
|
||||
default: true,
|
||||
description: 'Enable Plan Mode.',
|
||||
showInDialog: true,
|
||||
},
|
||||
modelSteering: {
|
||||
|
||||
@@ -79,7 +79,7 @@ import {
|
||||
type InitializationResult,
|
||||
} from './core/initializer.js';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { runZedIntegration } from './zed-integration/zedIntegration.js';
|
||||
import { runAcpClient } from './acp/acpClient.js';
|
||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||
import { checkForUpdates } from './ui/utils/updateCheck.js';
|
||||
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
|
||||
@@ -672,8 +672,8 @@ export async function main() {
|
||||
await getOauthClient(settings.merged.security.auth.selectedType, config);
|
||||
}
|
||||
|
||||
if (config.getExperimentalZedIntegration()) {
|
||||
return runZedIntegration(config, settings, argv);
|
||||
if (config.getAcpMode()) {
|
||||
return runAcpClient(config, settings, argv);
|
||||
}
|
||||
|
||||
let input = config.getQuestion();
|
||||
|
||||
@@ -179,7 +179,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should log error when cleanupExpiredSessions fails', async () => {
|
||||
it.skip('should log error when cleanupExpiredSessions fails', async () => {
|
||||
const { loadCliConfig, parseArguments } = await import(
|
||||
'./config/config.js'
|
||||
);
|
||||
@@ -216,7 +216,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getMcpServers: () => ({}),
|
||||
getMcpClientManager: vi.fn(),
|
||||
getIdeMode: vi.fn(() => false),
|
||||
getExperimentalZedIntegration: vi.fn(() => true),
|
||||
getAcpMode: vi.fn(() => true),
|
||||
getScreenReader: vi.fn(() => false),
|
||||
getGeminiMdFileCount: vi.fn(() => 0),
|
||||
getProjectRoot: vi.fn(() => '/'),
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('BuiltinCommandLoader', () => {
|
||||
vi.clearAllMocks();
|
||||
mockConfig = {
|
||||
getFolderTrust: vi.fn().mockReturnValue(true),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getEnableExtensionReloading: () => false,
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
@@ -287,7 +287,7 @@ describe('BuiltinCommandLoader profile', () => {
|
||||
vi.resetModules();
|
||||
mockConfig = {
|
||||
getFolderTrust: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(true),
|
||||
getCheckpointingEnabled: () => false,
|
||||
getEnableExtensionReloading: () => false,
|
||||
getEnableHooks: () => false,
|
||||
|
||||
@@ -42,7 +42,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
setSessionId: vi.fn(),
|
||||
getSessionId: vi.fn().mockReturnValue('mock-session-id'),
|
||||
getContentGeneratorConfig: vi.fn(() => ({ authType: 'google' })),
|
||||
getExperimentalZedIntegration: vi.fn(() => false),
|
||||
getAcpMode: vi.fn(() => false),
|
||||
isBrowserLaunchSuppressed: vi.fn(() => false),
|
||||
setRemoteAdminSettings: vi.fn(),
|
||||
isYoloModeDisabled: vi.fn(() => false),
|
||||
|
||||
@@ -89,7 +89,6 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentRows === 0) contentRows = 1; // Minimum 1 row
|
||||
|
||||
const width = terminal.cols * charWidth + padding * 2;
|
||||
@@ -114,9 +113,6 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
||||
|
||||
let currentFgHex: string | null = null;
|
||||
let currentBgHex: string | null = null;
|
||||
let currentIsBold = false;
|
||||
let currentIsItalic = false;
|
||||
let currentIsUnderline = false;
|
||||
let currentBlockStartCol = -1;
|
||||
let currentBlockText = '';
|
||||
let currentBlockNumCells = 0;
|
||||
@@ -132,20 +128,12 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
||||
svg += ` <rect x="${xPos}" y="${yPos}" width="${rectWidth}" height="${charHeight}" fill="${currentBgHex}" />
|
||||
`;
|
||||
}
|
||||
if (currentBlockText.trim().length > 0 || currentIsUnderline) {
|
||||
if (currentBlockText.trim().length > 0) {
|
||||
const fill = currentFgHex || '#ffffff'; // Default text color
|
||||
const textWidth = currentBlockNumCells * charWidth;
|
||||
|
||||
let extraAttrs = '';
|
||||
if (currentIsBold) extraAttrs += ' font-weight="bold"';
|
||||
if (currentIsItalic) extraAttrs += ' font-style="italic"';
|
||||
if (currentIsUnderline)
|
||||
extraAttrs += ' text-decoration="underline"';
|
||||
|
||||
// Use textLength to ensure the block fits exactly into its designated cells
|
||||
const textElement = `<text x="${xPos}" y="${yPos + 2}" fill="${fill}" textLength="${textWidth}" lengthAdjust="spacingAndGlyphs"${extraAttrs}>${escapeXml(currentBlockText)}</text>`;
|
||||
|
||||
svg += ` ${textElement}\n`;
|
||||
svg += ` <text x="${xPos}" y="${yPos + 2}" fill="${fill}" textLength="${textWidth}" lengthAdjust="spacingAndGlyphs">${escapeXml(currentBlockText)}</text>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,27 +164,17 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
||||
bgHex = tempFgHex || '#ffffff';
|
||||
}
|
||||
|
||||
const isBold = !!cell.isBold();
|
||||
const isItalic = !!cell.isItalic();
|
||||
const isUnderline = !!cell.isUnderline();
|
||||
|
||||
let chars = cell.getChars();
|
||||
if (chars === '') chars = ' '.repeat(cellWidth);
|
||||
|
||||
if (
|
||||
fgHex !== currentFgHex ||
|
||||
bgHex !== currentBgHex ||
|
||||
isBold !== currentIsBold ||
|
||||
isItalic !== currentIsItalic ||
|
||||
isUnderline !== currentIsUnderline ||
|
||||
currentBlockStartCol === -1
|
||||
) {
|
||||
finalizeBlock(x);
|
||||
currentFgHex = fgHex;
|
||||
currentBgHex = bgHex;
|
||||
currentIsBold = isBold;
|
||||
currentIsItalic = isItalic;
|
||||
currentIsUnderline = isUnderline;
|
||||
currentBlockStartCol = x;
|
||||
currentBlockText = chars;
|
||||
currentBlockNumCells = cellWidth;
|
||||
@@ -207,7 +185,6 @@ export const generateSvgForTerminal = (terminal: Terminal): string => {
|
||||
}
|
||||
finalizeBlock(line.length);
|
||||
}
|
||||
|
||||
svg += ` </g>\n</svg>`;
|
||||
return svg;
|
||||
};
|
||||
|
||||
@@ -2544,136 +2544,6 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Expansion Persistence', () => {
|
||||
let rerender: () => void;
|
||||
let unmount: () => void;
|
||||
let stdin: ReturnType<typeof render>['stdin'];
|
||||
|
||||
const setupExpansionPersistenceTest = async (
|
||||
HighPriorityChild?: React.FC,
|
||||
) => {
|
||||
const getTree = () => (
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<KeypressProvider config={mockConfig}>
|
||||
<OverflowProvider>
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>
|
||||
{HighPriorityChild && <HighPriorityChild />}
|
||||
</OverflowProvider>
|
||||
</KeypressProvider>
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
|
||||
const renderResult = render(getTree());
|
||||
stdin = renderResult.stdin;
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
rerender = () => renderResult.rerender(getTree());
|
||||
unmount = () => renderResult.unmount();
|
||||
};
|
||||
|
||||
const writeStdin = async (sequence: string) => {
|
||||
await act(async () => {
|
||||
stdin.write(sequence);
|
||||
// Advance timers to allow escape sequence parsing and broadcasting
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
rerender();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should reset expansion when a key is NOT handled by anyone', async () => {
|
||||
await setupExpansionPersistenceTest();
|
||||
|
||||
// Expand first
|
||||
act(() => capturedUIActions.setConstrainHeight(false));
|
||||
rerender();
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// Press a random key that no one handles (hits Low priority fallback)
|
||||
await writeStdin('x');
|
||||
|
||||
// Should be reset to true (collapsed)
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should toggle expansion when Ctrl+O is pressed', async () => {
|
||||
await setupExpansionPersistenceTest();
|
||||
|
||||
// Initial state is collapsed
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
|
||||
// Press Ctrl+O to expand (Ctrl+O is sequence \x0f)
|
||||
await writeStdin('\x0f');
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// Press Ctrl+O again to collapse
|
||||
await writeStdin('\x0f');
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT collapse when a high-priority component handles the key (e.g., up/down arrows)', async () => {
|
||||
const NavigationHandler = () => {
|
||||
// use real useKeypress
|
||||
useKeypress(
|
||||
(key: Key) => {
|
||||
if (key.name === 'up' || key.name === 'down') {
|
||||
return true; // Handle navigation
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ isActive: true, priority: true }, // High priority
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
await setupExpansionPersistenceTest(NavigationHandler);
|
||||
|
||||
// Expand first
|
||||
act(() => capturedUIActions.setConstrainHeight(false));
|
||||
rerender();
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// 1. Simulate Up arrow (handled by high priority child)
|
||||
// CSI A is Up arrow
|
||||
await writeStdin('\u001b[A');
|
||||
|
||||
// Should STILL be expanded
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// 2. Simulate Down arrow (handled by high priority child)
|
||||
// CSI B is Down arrow
|
||||
await writeStdin('\u001b[B');
|
||||
|
||||
// Should STILL be expanded
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// 3. Sanity check: press an unhandled key
|
||||
await writeStdin('x');
|
||||
|
||||
// Should finally collapse
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shortcuts Help Visibility', () => {
|
||||
let handleGlobalKeypress: (key: Key) => boolean;
|
||||
let mockedUseKeypress: Mock;
|
||||
|
||||
@@ -1382,11 +1382,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
// Compute available terminal height based on controls measurement
|
||||
const availableTerminalHeight = Math.max(
|
||||
0,
|
||||
terminalHeight -
|
||||
controlsHeight -
|
||||
staticExtraHeight -
|
||||
2 -
|
||||
backgroundShellHeight,
|
||||
terminalHeight - controlsHeight - backgroundShellHeight - 1,
|
||||
);
|
||||
|
||||
config.setShellExecutionConfig({
|
||||
@@ -1873,10 +1869,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
],
|
||||
);
|
||||
|
||||
useKeypress(handleGlobalKeypress, {
|
||||
isActive: true,
|
||||
priority: KeypressPriority.Low,
|
||||
});
|
||||
useKeypress(handleGlobalKeypress, { isActive: true, priority: true });
|
||||
|
||||
useKeypress(
|
||||
() => {
|
||||
|
||||
@@ -98,7 +98,7 @@ export function ApiAuthDialog({
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.ui.focus}
|
||||
borderColor={theme.border.focused}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
|
||||
@@ -193,7 +193,7 @@ export function AuthDialog({
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.ui.focus}
|
||||
borderColor={theme.border.focused}
|
||||
flexDirection="row"
|
||||
padding={1}
|
||||
width="100%"
|
||||
@@ -209,7 +209,7 @@ export function AuthDialog({
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.ui.focus}
|
||||
borderColor={theme.border.focused}
|
||||
flexDirection="row"
|
||||
padding={1}
|
||||
width="100%"
|
||||
|
||||
@@ -786,16 +786,21 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
const TITLE_MARGIN = 1;
|
||||
const FOOTER_HEIGHT = 2; // DialogFooter + margin
|
||||
const overhead = HEADER_HEIGHT + TITLE_MARGIN + FOOTER_HEIGHT;
|
||||
|
||||
const listHeight = availableHeight
|
||||
? Math.max(1, availableHeight - overhead)
|
||||
: undefined;
|
||||
const questionHeight =
|
||||
|
||||
const questionHeightLimit =
|
||||
listHeight && !isAlternateBuffer
|
||||
? Math.min(15, Math.max(1, listHeight - DIALOG_PADDING))
|
||||
? question.unconstrainedHeight
|
||||
? Math.max(1, listHeight - selectionItems.length * 2)
|
||||
: Math.min(15, Math.max(1, listHeight - DIALOG_PADDING))
|
||||
: undefined;
|
||||
|
||||
const maxItemsToShow =
|
||||
listHeight && questionHeight
|
||||
? Math.max(1, Math.floor((listHeight - questionHeight) / 2))
|
||||
listHeight && questionHeightLimit
|
||||
? Math.max(1, Math.floor((listHeight - questionHeightLimit) / 2))
|
||||
: selectionItems.length;
|
||||
|
||||
return (
|
||||
@@ -803,7 +808,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
{progressHeader}
|
||||
<Box marginBottom={TITLE_MARGIN}>
|
||||
<MaxSizedBox
|
||||
maxHeight={questionHeight}
|
||||
maxHeight={questionHeightLimit}
|
||||
maxWidth={availableWidth}
|
||||
overflowDirection="bottom"
|
||||
>
|
||||
|
||||
@@ -427,7 +427,7 @@ export const BackgroundShellDisplay = ({
|
||||
height="100%"
|
||||
width="100%"
|
||||
borderStyle="single"
|
||||
borderColor={isFocused ? theme.ui.focus : undefined}
|
||||
borderColor={isFocused ? theme.border.focused : undefined}
|
||||
>
|
||||
<Box
|
||||
flexDirection="row"
|
||||
@@ -438,7 +438,7 @@ export const BackgroundShellDisplay = ({
|
||||
borderRight={false}
|
||||
borderTop={false}
|
||||
paddingX={1}
|
||||
borderColor={isFocused ? theme.ui.focus : undefined}
|
||||
borderColor={isFocused ? theme.border.focused : undefined}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
{renderTabs()}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { ColorsDisplay } from './ColorsDisplay.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { themeManager } from '../themes/theme-manager.js';
|
||||
import type { Theme, ColorsTheme } from '../themes/theme.js';
|
||||
import type { SemanticColors } from '../themes/semantic-tokens.js';
|
||||
|
||||
describe('ColorsDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(themeManager, 'getSemanticColors').mockReturnValue({
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#cccccc',
|
||||
link: '#0000ff',
|
||||
accent: '#ff00ff',
|
||||
response: '#ffffff',
|
||||
},
|
||||
background: {
|
||||
primary: '#000000',
|
||||
message: '#111111',
|
||||
input: '#222222',
|
||||
focus: '#333333',
|
||||
diff: {
|
||||
added: '#003300',
|
||||
removed: '#330000',
|
||||
},
|
||||
},
|
||||
border: {
|
||||
default: '#555555',
|
||||
},
|
||||
ui: {
|
||||
comment: '#666666',
|
||||
symbol: '#cccccc',
|
||||
active: '#0000ff',
|
||||
dark: '#333333',
|
||||
focus: '#0000ff',
|
||||
gradient: undefined,
|
||||
},
|
||||
status: {
|
||||
error: '#ff0000',
|
||||
success: '#00ff00',
|
||||
warning: '#ffff00',
|
||||
},
|
||||
});
|
||||
|
||||
vi.spyOn(themeManager, 'getActiveTheme').mockReturnValue({
|
||||
name: 'Test Theme',
|
||||
type: 'dark',
|
||||
colors: {} as unknown as ColorsTheme,
|
||||
semanticColors: {
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: '#cccccc',
|
||||
link: '#0000ff',
|
||||
accent: '#ff00ff',
|
||||
response: '#ffffff',
|
||||
},
|
||||
background: {
|
||||
primary: '#000000',
|
||||
message: '#111111',
|
||||
input: '#222222',
|
||||
diff: {
|
||||
added: '#003300',
|
||||
removed: '#330000',
|
||||
},
|
||||
},
|
||||
border: {
|
||||
default: '#555555',
|
||||
},
|
||||
ui: {
|
||||
comment: '#666666',
|
||||
symbol: '#cccccc',
|
||||
active: '#0000ff',
|
||||
dark: '#333333',
|
||||
focus: '#0000ff',
|
||||
gradient: undefined,
|
||||
},
|
||||
status: {
|
||||
error: '#ff0000',
|
||||
success: '#00ff00',
|
||||
warning: '#ffff00',
|
||||
},
|
||||
} as unknown as SemanticColors,
|
||||
} as unknown as Theme);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
const mockTheme = themeManager.getActiveTheme();
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ColorsDisplay activeTheme={mockTheme} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
// Check for title and description
|
||||
expect(output).toContain('How do colors get applied?');
|
||||
expect(output).toContain('Hex:');
|
||||
|
||||
// Check for some color names and values expect(output).toContain('text.primary');
|
||||
expect(output).toContain('#ffffff');
|
||||
expect(output).toContain('background.diff.added');
|
||||
expect(output).toContain('#003300');
|
||||
expect(output).toContain('border.default');
|
||||
expect(output).toContain('#555555');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -1,277 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { Theme } from '../themes/theme.js';
|
||||
|
||||
interface StandardColorRow {
|
||||
type: 'standard';
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface GradientColorRow {
|
||||
type: 'gradient';
|
||||
name: string;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
interface BackgroundColorRow {
|
||||
type: 'background';
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type ColorRow = StandardColorRow | GradientColorRow | BackgroundColorRow;
|
||||
|
||||
const VALUE_COLUMN_WIDTH = 10;
|
||||
|
||||
const COLOR_DESCRIPTIONS: Record<string, string> = {
|
||||
'text.primary': 'Primary text color (uses terminal default if blank)',
|
||||
'text.secondary': 'Secondary/dimmed text color',
|
||||
'text.link': 'Hyperlink and highlighting color',
|
||||
'text.accent': 'Accent color for emphasis',
|
||||
'text.response':
|
||||
'Color for model response text (uses terminal default if blank)',
|
||||
'background.primary': 'Main terminal background color',
|
||||
'background.message': 'Subtle background for message blocks',
|
||||
'background.input': 'Background for the input prompt',
|
||||
'background.focus': 'Background highlight for selected/focused items',
|
||||
'background.diff.added': 'Background for added lines in diffs',
|
||||
'background.diff.removed': 'Background for removed lines in diffs',
|
||||
'border.default': 'Standard border color',
|
||||
'ui.comment': 'Color for code comments and metadata',
|
||||
'ui.symbol': 'Color for technical symbols and UI icons',
|
||||
'ui.active': 'Border color for active or running elements',
|
||||
'ui.dark': 'Deeply dimmed color for subtle UI elements',
|
||||
'ui.focus':
|
||||
'Color for focused elements (e.g. selected menu items, focused borders)',
|
||||
'status.error': 'Color for error messages and critical status',
|
||||
'status.success': 'Color for success messages and positive status',
|
||||
'status.warning': 'Color for warnings and cautionary status',
|
||||
};
|
||||
|
||||
interface ColorsDisplayProps {
|
||||
activeTheme: Theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines a contrasting text color (black or white) based on the background color's luminance.
|
||||
*/
|
||||
function getContrastingTextColor(hex: string): string {
|
||||
if (!hex || !hex.startsWith('#') || hex.length < 7) {
|
||||
// Fallback for invalid hex codes or named colors
|
||||
return theme.text.primary;
|
||||
}
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
// Using YIQ formula to determine luminance
|
||||
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
return yiq >= 128 ? '#000000' : '#FFFFFF';
|
||||
}
|
||||
|
||||
export const ColorsDisplay: React.FC<ColorsDisplayProps> = ({
|
||||
activeTheme,
|
||||
}) => {
|
||||
const semanticColors = activeTheme.semanticColors;
|
||||
|
||||
const backgroundRows: BackgroundColorRow[] = [];
|
||||
const standardRows: StandardColorRow[] = [];
|
||||
let gradientRow: GradientColorRow | null = null;
|
||||
|
||||
if (semanticColors.ui.gradient && semanticColors.ui.gradient.length > 0) {
|
||||
gradientRow = {
|
||||
type: 'gradient',
|
||||
name: 'ui.gradient',
|
||||
value: semanticColors.ui.gradient,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively flattens the semanticColors object.
|
||||
*/
|
||||
const flattenColors = (obj: object, path: string = '') => {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
const newPath = path ? `${path}.${key}` : key;
|
||||
|
||||
if (key === 'gradient' && Array.isArray(value)) {
|
||||
// Gradient handled separately
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
flattenColors(value, newPath);
|
||||
} else if (typeof value === 'string') {
|
||||
if (newPath.startsWith('background.')) {
|
||||
backgroundRows.push({
|
||||
type: 'background',
|
||||
name: newPath,
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
standardRows.push({
|
||||
type: 'standard',
|
||||
name: newPath,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
flattenColors(semanticColors);
|
||||
|
||||
// Final order: Backgrounds first, then Standards, then Gradient
|
||||
const allRows: ColorRow[] = [
|
||||
...backgroundRows,
|
||||
...standardRows,
|
||||
...(gradientRow ? [gradientRow] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
>
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
<Text bold color={theme.text.accent}>
|
||||
DEVELOPER TOOLS (Not visible to users)
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold>How do colors get applied?</Text>
|
||||
</Text>
|
||||
<Box marginLeft={2} flexDirection="column">
|
||||
<Text color={theme.text.primary}>
|
||||
• <Text bold>Hex:</Text> Rendered exactly by modern terminals. Not
|
||||
overridden by app themes.
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
• <Text bold>Blank:</Text> Uses your terminal's default
|
||||
foreground/background.
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
• <Text bold>Compatibility:</Text> On older terminals, hex is
|
||||
approximated to the nearest ANSI color.
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
• <Text bold>ANSI Names:</Text> 'red',
|
||||
'green', etc. are mapped to your terminal app's
|
||||
palette.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Header */}
|
||||
<Box flexDirection="row" marginBottom={0} paddingX={1}>
|
||||
<Box width={VALUE_COLUMN_WIDTH}>
|
||||
<Text bold color={theme.text.link} dimColor>
|
||||
Value
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text bold color={theme.text.link} dimColor>
|
||||
Name
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* All Rows */}
|
||||
<Box flexDirection="column">
|
||||
{allRows.map((row) => {
|
||||
if (row.type === 'standard') return renderStandardRow(row);
|
||||
if (row.type === 'gradient') return renderGradientRow(row);
|
||||
if (row.type === 'background') return renderBackgroundRow(row);
|
||||
return null;
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
function renderStandardRow({ name, value }: StandardColorRow) {
|
||||
const isHex = value.startsWith('#');
|
||||
const displayColor = isHex ? value : theme.text.primary;
|
||||
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||
|
||||
return (
|
||||
<Box key={name} flexDirection="row" paddingX={1}>
|
||||
<Box width={VALUE_COLUMN_WIDTH}>
|
||||
<Text color={displayColor}>{value || '(blank)'}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="row">
|
||||
<Box width="30%">
|
||||
<Text color={displayColor}>{name}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} paddingLeft={1}>
|
||||
<Text color={theme.text.secondary}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function renderGradientRow({ name, value }: GradientColorRow) {
|
||||
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||
|
||||
return (
|
||||
<Box key={name} flexDirection="row" paddingX={1}>
|
||||
<Box width={VALUE_COLUMN_WIDTH} flexDirection="column">
|
||||
{value.map((c, i) => (
|
||||
<Text key={i} color={c}>
|
||||
{c}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="row">
|
||||
<Box width="30%">
|
||||
<Gradient colors={value}>
|
||||
<Text>{name}</Text>
|
||||
</Gradient>
|
||||
</Box>
|
||||
<Box flexGrow={1} paddingLeft={1}>
|
||||
<Text color={theme.text.secondary}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBackgroundRow({ name, value }: BackgroundColorRow) {
|
||||
const description = COLOR_DESCRIPTIONS[name] || '';
|
||||
|
||||
return (
|
||||
<Box key={name} flexDirection="row" paddingX={1}>
|
||||
<Box
|
||||
width={VALUE_COLUMN_WIDTH}
|
||||
backgroundColor={value}
|
||||
justifyContent="center"
|
||||
paddingX={1}
|
||||
>
|
||||
<Text color={getContrastingTextColor(value)} bold wrap="truncate">
|
||||
{value || 'default'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="row" paddingLeft={1}>
|
||||
<Box width="30%">
|
||||
<Text color={theme.text.primary}>{name}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} paddingLeft={1}>
|
||||
<Text color={theme.text.secondary}>{description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -231,7 +231,7 @@ const createMockConfig = (overrides = {}): Config =>
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getAccessibility: vi.fn(() => ({})),
|
||||
getMcpServers: vi.fn(() => ({})),
|
||||
isPlanEnabled: vi.fn(() => false),
|
||||
isPlanEnabled: vi.fn(() => true),
|
||||
getToolRegistry: () => ({
|
||||
getTool: vi.fn(),
|
||||
}),
|
||||
|
||||
@@ -247,6 +247,7 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
|
||||
],
|
||||
placeholder: 'Type your feedback...',
|
||||
multiSelect: false,
|
||||
unconstrainedHeight: false,
|
||||
},
|
||||
]}
|
||||
onSubmit={(answers) => {
|
||||
|
||||
@@ -22,13 +22,8 @@ vi.mock('../semantic-colors.js', async (importOriginal) => {
|
||||
...original,
|
||||
theme: {
|
||||
...original.theme,
|
||||
background: {
|
||||
...original.theme.background,
|
||||
focus: '#004000',
|
||||
},
|
||||
ui: {
|
||||
...original.theme.ui,
|
||||
focus: '#00ff00',
|
||||
gradient: [], // Empty array to potentially trigger the crash
|
||||
},
|
||||
},
|
||||
|
||||
@@ -98,18 +98,16 @@ describe('<Header />', () => {
|
||||
primary: '',
|
||||
message: '',
|
||||
input: '',
|
||||
focus: '',
|
||||
diff: { added: '', removed: '' },
|
||||
},
|
||||
border: {
|
||||
default: '',
|
||||
focused: '',
|
||||
},
|
||||
ui: {
|
||||
comment: '',
|
||||
symbol: '',
|
||||
active: '',
|
||||
dark: '',
|
||||
focus: '',
|
||||
gradient: undefined,
|
||||
},
|
||||
status: {
|
||||
|
||||
@@ -1427,7 +1427,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
const borderColor =
|
||||
isShellFocused && !isEmbeddedShellFocused
|
||||
? (statusColor ?? theme.ui.focus)
|
||||
? (statusColor ?? theme.border.focused)
|
||||
: theme.border.default;
|
||||
|
||||
return (
|
||||
|
||||
@@ -79,18 +79,10 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
/>
|
||||
</Box>
|
||||
{primaryText && (
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
{thinkingIndicator}
|
||||
{primaryText}
|
||||
</Text>
|
||||
{primaryText === INTERACTIVE_SHELL_WAITING_PHRASE && (
|
||||
<Text color={theme.ui.active} italic>
|
||||
{' '}
|
||||
(press tab to focus)
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
{thinkingIndicator}
|
||||
{primaryText}
|
||||
</Text>
|
||||
)}
|
||||
{cancelAndTimerContent && (
|
||||
<>
|
||||
@@ -121,18 +113,10 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
/>
|
||||
</Box>
|
||||
{primaryText && (
|
||||
<Box flexShrink={1}>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
{thinkingIndicator}
|
||||
{primaryText}
|
||||
</Text>
|
||||
{primaryText === INTERACTIVE_SHELL_WAITING_PHRASE && (
|
||||
<Text color={theme.ui.active} italic>
|
||||
{' '}
|
||||
(press tab to focus)
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
{thinkingIndicator}
|
||||
{primaryText}
|
||||
</Text>
|
||||
)}
|
||||
{!isNarrow && cancelAndTimerContent && (
|
||||
<>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const LogoutConfirmationDialog: React.FC<
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.ui.focus}
|
||||
borderColor={theme.border.focused}
|
||||
padding={1}
|
||||
flexGrow={1}
|
||||
marginLeft={1}
|
||||
|
||||
@@ -203,7 +203,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns active border for executing shell commands', () => {
|
||||
it('returns symbol border for executing shell commands', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
@@ -219,37 +219,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
],
|
||||
id: 1,
|
||||
};
|
||||
// While executing shell commands, it's dim false, border active
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
activeShellPtyId,
|
||||
false,
|
||||
[],
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.active,
|
||||
borderDimColor: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns focus border for focused executing shell commands', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
{
|
||||
callId: '1',
|
||||
name: SHELL_COMMAND_NAME,
|
||||
description: '',
|
||||
status: CoreToolCallStatus.Executing,
|
||||
ptyId: activeShellPtyId,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: undefined,
|
||||
} as IndividualToolCallDisplay,
|
||||
],
|
||||
id: 1,
|
||||
};
|
||||
// When focused, it's dim false, border focus
|
||||
// While executing shell commands, it's dim false, border symbol
|
||||
const result = getToolGroupBorderAppearance(
|
||||
item,
|
||||
activeShellPtyId,
|
||||
@@ -258,12 +228,12 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.focus,
|
||||
borderColor: theme.ui.symbol,
|
||||
borderDimColor: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns active border and dims color for background executing shell command when another shell is active', () => {
|
||||
it('returns symbol border and dims color for background executing shell command when another shell is active', () => {
|
||||
const item = {
|
||||
type: 'tool_group' as const,
|
||||
tools: [
|
||||
@@ -287,7 +257,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
mockBackgroundShells,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
borderColor: theme.ui.active,
|
||||
borderColor: theme.ui.symbol,
|
||||
borderDimColor: true,
|
||||
});
|
||||
});
|
||||
@@ -305,7 +275,7 @@ describe('getToolGroupBorderAppearance', () => {
|
||||
);
|
||||
// Since there are no tools to inspect, it falls back to empty pending, but isCurrentlyInShellTurn=true
|
||||
// so it counts as pending shell.
|
||||
expect(result.borderColor).toEqual(theme.ui.focus);
|
||||
expect(result.borderColor).toEqual(theme.ui.symbol);
|
||||
// It shouldn't be dim because there are no tools to say it isEmbeddedShellFocused = false
|
||||
expect(result.borderDimColor).toBe(false);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
AuthType,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -28,8 +30,9 @@ const mockGetDisplayString = vi.fn();
|
||||
const mockLogModelSlashCommand = vi.fn();
|
||||
const mockModelSlashCommandEvent = vi.fn();
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
getDisplayString: (val: string) => mockGetDisplayString(val),
|
||||
@@ -40,6 +43,7 @@ vi.mock('@google/gemini-cli-core', async () => {
|
||||
mockModelSlashCommandEvent(model);
|
||||
}
|
||||
},
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL: 'gemini-3.1-flash-lite-preview',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -49,6 +53,9 @@ describe('<ModelDialog />', () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const mockGetHasAccessToPreviewModel = vi.fn();
|
||||
const mockGetGemini31LaunchedSync = vi.fn();
|
||||
const mockGetProModelNoAccess = vi.fn();
|
||||
const mockGetProModelNoAccessSync = vi.fn();
|
||||
const mockGetUserTier = vi.fn();
|
||||
|
||||
interface MockConfig extends Partial<Config> {
|
||||
setModel: (model: string, isTemporary?: boolean) => void;
|
||||
@@ -56,6 +63,9 @@ describe('<ModelDialog />', () => {
|
||||
getHasAccessToPreviewModel: () => boolean;
|
||||
getIdeMode: () => boolean;
|
||||
getGemini31LaunchedSync: () => boolean;
|
||||
getProModelNoAccess: () => Promise<boolean>;
|
||||
getProModelNoAccessSync: () => boolean;
|
||||
getUserTier: () => UserTierId | undefined;
|
||||
}
|
||||
|
||||
const mockConfig: MockConfig = {
|
||||
@@ -64,6 +74,9 @@ describe('<ModelDialog />', () => {
|
||||
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
|
||||
getIdeMode: () => false,
|
||||
getGemini31LaunchedSync: mockGetGemini31LaunchedSync,
|
||||
getProModelNoAccess: mockGetProModelNoAccess,
|
||||
getProModelNoAccessSync: mockGetProModelNoAccessSync,
|
||||
getUserTier: mockGetUserTier,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,6 +84,9 @@ describe('<ModelDialog />', () => {
|
||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(false);
|
||||
mockGetGemini31LaunchedSync.mockReturnValue(false);
|
||||
mockGetProModelNoAccess.mockResolvedValue(false);
|
||||
mockGetProModelNoAccessSync.mockReturnValue(false);
|
||||
mockGetUserTier.mockReturnValue(UserTierId.STANDARD);
|
||||
|
||||
// Default implementation for getDisplayString
|
||||
mockGetDisplayString.mockImplementation((val: string) => {
|
||||
@@ -109,6 +125,55 @@ describe('<ModelDialog />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders the "manual" view initially for users with no pro access and filters Pro models with correct order', async () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(true);
|
||||
mockGetProModelNoAccess.mockResolvedValue(true);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetUserTier.mockReturnValue(UserTierId.FREE);
|
||||
mockGetDisplayString.mockImplementation((val: string) => val);
|
||||
|
||||
const { lastFrame, unmount } = await renderComponent();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Select Model');
|
||||
expect(output).not.toContain(DEFAULT_GEMINI_MODEL);
|
||||
expect(output).not.toContain(PREVIEW_GEMINI_MODEL);
|
||||
|
||||
// Verify order: Flash Preview -> Flash Lite Preview -> Flash -> Flash Lite
|
||||
const flashPreviewIdx = output.indexOf(PREVIEW_GEMINI_FLASH_MODEL);
|
||||
const flashLitePreviewIdx = output.indexOf(
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
);
|
||||
const flashIdx = output.indexOf(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
const flashLiteIdx = output.indexOf(DEFAULT_GEMINI_FLASH_LITE_MODEL);
|
||||
|
||||
expect(flashPreviewIdx).toBeLessThan(flashLitePreviewIdx);
|
||||
expect(flashLitePreviewIdx).toBeLessThan(flashIdx);
|
||||
expect(flashIdx).toBeLessThan(flashLiteIdx);
|
||||
|
||||
expect(output).not.toContain('Auto');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('closes dialog on escape in "manual" view for users with no pro access', async () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(true);
|
||||
mockGetProModelNoAccess.mockResolvedValue(true);
|
||||
const { stdin, waitUntilReady, unmount } = await renderComponent();
|
||||
|
||||
// Already in manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B'); // Escape
|
||||
});
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('switches to "manual" view when "Manual" is selected and uses getDisplayString for models', async () => {
|
||||
mockGetDisplayString.mockImplementation((val: string) => {
|
||||
if (val === DEFAULT_GEMINI_MODEL) return 'Formatted Pro Model';
|
||||
@@ -369,5 +434,50 @@ describe('<ModelDialog />', () => {
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides Flash Lite Preview model for users with pro access', async () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(false);
|
||||
mockGetProModelNoAccess.mockResolvedValue(false);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } =
|
||||
await renderComponent();
|
||||
|
||||
// Go to manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Manual
|
||||
});
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows Flash Lite Preview model for free tier users', async () => {
|
||||
mockGetProModelNoAccessSync.mockReturnValue(false);
|
||||
mockGetProModelNoAccess.mockResolvedValue(false);
|
||||
mockGetHasAccessToPreviewModel.mockReturnValue(true);
|
||||
mockGetUserTier.mockReturnValue(UserTierId.FREE);
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } =
|
||||
await renderComponent();
|
||||
|
||||
// Go to manual view
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Manual
|
||||
});
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useCallback, useContext, useMemo, useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
PREVIEW_GEMINI_MODEL_AUTO,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
getDisplayString,
|
||||
AuthType,
|
||||
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
|
||||
isProModel,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
@@ -35,9 +38,26 @@ interface ModelDialogProps {
|
||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
const settings = useSettings();
|
||||
const [view, setView] = useState<'main' | 'manual'>('main');
|
||||
const [hasAccessToProModel, setHasAccessToProModel] = useState<boolean>(
|
||||
() => !(config?.getProModelNoAccessSync() ?? false),
|
||||
);
|
||||
const [view, setView] = useState<'main' | 'manual'>(() =>
|
||||
config?.getProModelNoAccessSync() ? 'manual' : 'main',
|
||||
);
|
||||
const [persistMode, setPersistMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAccess() {
|
||||
if (!config) return;
|
||||
const noAccess = await config.getProModelNoAccess();
|
||||
setHasAccessToProModel(!noAccess);
|
||||
if (noAccess) {
|
||||
setView('manual');
|
||||
}
|
||||
}
|
||||
void checkAccess();
|
||||
}, [config]);
|
||||
|
||||
// Determine the Preferred Model (read once when the dialog opens).
|
||||
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
|
||||
|
||||
@@ -66,7 +86,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
if (view === 'manual') {
|
||||
if (view === 'manual' && hasAccessToProModel) {
|
||||
setView('main');
|
||||
} else {
|
||||
onClose();
|
||||
@@ -115,6 +135,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
}, [shouldShowPreviewModels, manualModelSelected, useGemini31]);
|
||||
|
||||
const manualOptions = useMemo(() => {
|
||||
const isFreeTier = config?.getUserTier() === UserTierId.FREE;
|
||||
const list = [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL,
|
||||
@@ -142,7 +163,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
|
||||
: previewProModel;
|
||||
|
||||
list.unshift(
|
||||
const previewOptions = [
|
||||
{
|
||||
value: previewProValue,
|
||||
title: getDisplayString(previewProModel),
|
||||
@@ -153,10 +174,32 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
|
||||
key: PREVIEW_GEMINI_FLASH_MODEL,
|
||||
},
|
||||
);
|
||||
];
|
||||
|
||||
if (isFreeTier) {
|
||||
previewOptions.push({
|
||||
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
|
||||
key: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
|
||||
});
|
||||
}
|
||||
|
||||
list.unshift(...previewOptions);
|
||||
}
|
||||
|
||||
if (!hasAccessToProModel) {
|
||||
// Filter out all Pro models for free tier
|
||||
return list.filter((option) => !isProModel(option.value));
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [shouldShowPreviewModels, useGemini31, useCustomToolModel]);
|
||||
}, [
|
||||
shouldShowPreviewModels,
|
||||
useGemini31,
|
||||
useCustomToolModel,
|
||||
hasAccessToProModel,
|
||||
config,
|
||||
]);
|
||||
|
||||
const options = view === 'main' ? mainOptions : manualOptions;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import type React from 'react';
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
@@ -437,7 +436,7 @@ const SessionItem = ({
|
||||
if (isDisabled) {
|
||||
return Colors.Gray;
|
||||
}
|
||||
return isActive ? theme.ui.focus : c;
|
||||
return isActive ? Colors.AccentPurple : c;
|
||||
};
|
||||
|
||||
const prefix = isActive ? '❯ ' : ' ';
|
||||
@@ -484,10 +483,7 @@ const SessionItem = ({
|
||||
));
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
backgroundColor={isActive ? theme.background.focus : undefined}
|
||||
>
|
||||
<Box flexDirection="row">
|
||||
<Text color={textColor()} dimColor={isDisabled}>
|
||||
{prefix}
|
||||
</Text>
|
||||
|
||||
@@ -5,23 +5,11 @@
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
ToolCallDecision,
|
||||
getShellConfiguration,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
getShellConfiguration: vi.fn(),
|
||||
};
|
||||
});
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof SessionContext>();
|
||||
@@ -31,16 +19,12 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const getShellConfigurationMock = vi.mocked(getShellConfiguration);
|
||||
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||
|
||||
const renderWithMockedStats = async (
|
||||
metrics: SessionMetrics,
|
||||
sessionId = 'test-session',
|
||||
) => {
|
||||
const renderWithMockedStats = async (metrics: SessionMetrics) => {
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId,
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
@@ -62,38 +46,8 @@ const renderWithMockedStats = async (
|
||||
};
|
||||
|
||||
describe('<SessionSummaryDisplay />', () => {
|
||||
const emptyMetrics: SessionMetrics = {
|
||||
models: {},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getShellConfigurationMock.mockReturnValue({
|
||||
executable: 'bash',
|
||||
argsPrefix: ['-c'],
|
||||
shell: 'bash',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the summary display with a title', async () => {
|
||||
const metrics: SessionMetrics = {
|
||||
...emptyMetrics,
|
||||
models: {
|
||||
'gemini-2.5-pro': {
|
||||
api: { totalRequests: 10, totalErrors: 1, totalLatencyMs: 50234 },
|
||||
@@ -109,6 +63,19 @@ describe('<SessionSummaryDisplay />', () => {
|
||||
roles: {},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
totalCalls: 0,
|
||||
totalSuccess: 0,
|
||||
totalFail: 0,
|
||||
totalDurationMs: 0,
|
||||
totalDecisions: {
|
||||
accept: 0,
|
||||
reject: 0,
|
||||
modify: 0,
|
||||
[ToolCallDecision.AUTO_ACCEPT]: 0,
|
||||
},
|
||||
byName: {},
|
||||
},
|
||||
files: {
|
||||
totalLinesAdded: 42,
|
||||
totalLinesRemoved: 15,
|
||||
@@ -122,70 +89,4 @@ describe('<SessionSummaryDisplay />', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('Session ID escaping', () => {
|
||||
it('renders a standard UUID-formatted session ID in the footer (bash)', async () => {
|
||||
const uuidSessionId = '1234-abcd-5678-efgh';
|
||||
const { lastFrame, unmount } = await renderWithMockedStats(
|
||||
emptyMetrics,
|
||||
uuidSessionId,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// Standard UUID characters should not be escaped/quoted by default for bash.
|
||||
expect(output).toContain('gemini --resume 1234-abcd-5678-efgh');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('sanitizes a malicious session ID in the footer (bash)', async () => {
|
||||
const maliciousSessionId = "'; rm -rf / #";
|
||||
const { lastFrame, unmount } = await renderWithMockedStats(
|
||||
emptyMetrics,
|
||||
maliciousSessionId,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// escapeShellArg (using shell-quote for bash) will wrap special characters in double quotes.
|
||||
expect(output).toContain('gemini --resume "\'; rm -rf / #"');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders a standard UUID-formatted session ID in the footer (powershell)', async () => {
|
||||
getShellConfigurationMock.mockReturnValue({
|
||||
executable: 'powershell.exe',
|
||||
argsPrefix: ['-NoProfile', '-Command'],
|
||||
shell: 'powershell',
|
||||
});
|
||||
|
||||
const uuidSessionId = '1234-abcd-5678-efgh';
|
||||
const { lastFrame, unmount } = await renderWithMockedStats(
|
||||
emptyMetrics,
|
||||
uuidSessionId,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// PowerShell wraps strings in single quotes
|
||||
expect(output).toContain("gemini --resume '1234-abcd-5678-efgh'");
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('sanitizes a malicious session ID in the footer (powershell)', async () => {
|
||||
getShellConfigurationMock.mockReturnValue({
|
||||
executable: 'powershell.exe',
|
||||
argsPrefix: ['-NoProfile', '-Command'],
|
||||
shell: 'powershell',
|
||||
});
|
||||
|
||||
const maliciousSessionId = "'; rm -rf / #";
|
||||
const { lastFrame, unmount } = await renderWithMockedStats(
|
||||
emptyMetrics,
|
||||
maliciousSessionId,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// PowerShell wraps in single quotes and escapes internal single quotes by doubling them
|
||||
expect(output).toContain("gemini --resume '''; rm -rf / #'");
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import { escapeShellArg, getShellConfiguration } from '@google/gemini-cli-core';
|
||||
|
||||
interface SessionSummaryDisplayProps {
|
||||
duration: string;
|
||||
@@ -15,16 +13,10 @@ interface SessionSummaryDisplayProps {
|
||||
|
||||
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
|
||||
duration,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { shell } = getShellConfiguration();
|
||||
const footer = `To resume this session: gemini --resume ${escapeShellArg(stats.sessionId, shell)}`;
|
||||
|
||||
return (
|
||||
<StatsDisplay
|
||||
title="Agent powering down. Goodbye!"
|
||||
duration={duration}
|
||||
footer={footer}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<StatsDisplay
|
||||
title="Agent powering down. Goodbye!"
|
||||
duration={duration}
|
||||
footer="Tip: Resume a previous session using gemini --resume or /resume"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ export function SuggestionsDisplay({
|
||||
const originalIndex = startIndex + index;
|
||||
const isActive = originalIndex === activeIndex;
|
||||
const isExpanded = originalIndex === expandedIndex;
|
||||
const textColor = isActive ? theme.ui.focus : theme.text.secondary;
|
||||
const textColor = isActive ? theme.text.accent : theme.text.secondary;
|
||||
const isLong = suggestion.value.length >= MAX_WIDTH;
|
||||
const labelElement = (
|
||||
<ExpandableText
|
||||
@@ -97,11 +97,7 @@ export function SuggestionsDisplay({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`${suggestion.value}-${originalIndex}`}
|
||||
flexDirection="row"
|
||||
backgroundColor={isActive ? theme.background.focus : undefined}
|
||||
>
|
||||
<Box key={`${suggestion.value}-${originalIndex}`} flexDirection="row">
|
||||
<Box
|
||||
{...(mode === 'slash'
|
||||
? { width: commandColumnWidth, flexShrink: 0 as const }
|
||||
|
||||
@@ -8,22 +8,6 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { ThemeDialog } from './ThemeDialog.js';
|
||||
|
||||
const { mockIsDevelopment } = vi.hoisted(() => ({
|
||||
mockIsDevelopment: { value: false },
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/installationInfo.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../../utils/installationInfo.js')>();
|
||||
return {
|
||||
...actual,
|
||||
get isDevelopment() {
|
||||
return mockIsDevelopment.value;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import { DEFAULT_THEME, themeManager } from '../themes/theme-manager.js';
|
||||
import { act } from 'react';
|
||||
@@ -46,21 +30,17 @@ describe('ThemeDialog Snapshots', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
'should render correctly in theme selection mode (isDevelopment: %s)',
|
||||
async (isDev) => {
|
||||
mockIsDevelopment.value = isDev;
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThemeDialog {...baseProps} settings={settings} />,
|
||||
{ settings },
|
||||
);
|
||||
await waitUntilReady();
|
||||
it('should render correctly in theme selection mode', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThemeDialog {...baseProps} settings={settings} />,
|
||||
{ settings },
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render correctly in scope selector mode', async () => {
|
||||
const settings = createMockSettings();
|
||||
|
||||
@@ -23,8 +23,6 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { ScopeSelector } from './shared/ScopeSelector.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { ColorsDisplay } from './ColorsDisplay.js';
|
||||
import { isDevelopment } from '../../utils/installationInfo.js';
|
||||
|
||||
interface ThemeDialogProps {
|
||||
/** Callback function when a theme is selected */
|
||||
@@ -247,11 +245,6 @@ export function ThemeDialog({
|
||||
// The code block is slightly longer than the diff, so give it more space.
|
||||
const codeBlockHeight = Math.ceil(availableHeightForPanes * 0.6);
|
||||
const diffHeight = Math.floor(availableHeightForPanes * 0.4);
|
||||
|
||||
const previewTheme =
|
||||
themeManager.getTheme(highlightedThemeName || DEFAULT_THEME.name) ||
|
||||
DEFAULT_THEME;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -335,48 +328,53 @@ export function ThemeDialog({
|
||||
<Text bold color={theme.text.primary}>
|
||||
Preview
|
||||
</Text>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={theme.border.default}
|
||||
paddingTop={includePadding ? 1 : 0}
|
||||
paddingBottom={includePadding ? 1 : 0}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{colorizeCode({
|
||||
code: `# function
|
||||
{/* Get the Theme object for the highlighted theme, fall back to default if not found */}
|
||||
{(() => {
|
||||
const previewTheme =
|
||||
themeManager.getTheme(
|
||||
highlightedThemeName || DEFAULT_THEME.name,
|
||||
) || DEFAULT_THEME;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={theme.border.default}
|
||||
paddingTop={includePadding ? 1 : 0}
|
||||
paddingBottom={includePadding ? 1 : 0}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{colorizeCode({
|
||||
code: `# function
|
||||
def fibonacci(n):
|
||||
a, b = 0, 1
|
||||
for _ in range(n):
|
||||
a, b = b, a + b
|
||||
return a`,
|
||||
language: 'python',
|
||||
availableHeight:
|
||||
isAlternateBuffer === false ? codeBlockHeight : undefined,
|
||||
maxWidth: colorizeCodeWidth,
|
||||
settings,
|
||||
})}
|
||||
<Box marginTop={1} />
|
||||
<DiffRenderer
|
||||
diffContent={`--- a/util.py
|
||||
language: 'python',
|
||||
availableHeight:
|
||||
isAlternateBuffer === false ? codeBlockHeight : undefined,
|
||||
maxWidth: colorizeCodeWidth,
|
||||
settings,
|
||||
})}
|
||||
<Box marginTop={1} />
|
||||
<DiffRenderer
|
||||
diffContent={`--- a/util.py
|
||||
+++ b/util.py
|
||||
@@ -1,2 +1,2 @@
|
||||
- print("Hello, " + name)
|
||||
+ print(f"Hello, {name}!")
|
||||
`}
|
||||
availableTerminalHeight={
|
||||
isAlternateBuffer === false ? diffHeight : undefined
|
||||
}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
theme={previewTheme}
|
||||
/>
|
||||
</Box>
|
||||
{isDevelopment && (
|
||||
<Box marginTop={1}>
|
||||
<ColorsDisplay activeTheme={previewTheme} />
|
||||
</Box>
|
||||
)}
|
||||
availableTerminalHeight={
|
||||
isAlternateBuffer === false ? diffHeight : undefined
|
||||
}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
theme={previewTheme}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -13,10 +13,6 @@ vi.mock('../semantic-colors.js', () => ({
|
||||
theme: {
|
||||
ui: {
|
||||
gradient: ['red', 'blue'],
|
||||
focus: 'green',
|
||||
},
|
||||
background: {
|
||||
focus: 'darkgreen',
|
||||
},
|
||||
text: {
|
||||
accent: 'cyan',
|
||||
|
||||
@@ -282,7 +282,10 @@ describe('ToolConfirmationQueue', () => {
|
||||
// hideToolIdentity is true for ask_user -> subtracts 4 instead of 6
|
||||
// availableContentHeight = 19 - 4 = 15
|
||||
// ToolConfirmationMessage handlesOwnUI=true -> returns full 15
|
||||
// AskUserDialog uses 15 lines to render its multi-line question and options.
|
||||
// AskUserDialog allocates questionHeight = availableHeight - overhead - DIALOG_PADDING.
|
||||
// listHeight = 15 - overhead (Header:0, Margin:1, Footer:2) = 12.
|
||||
// maxQuestionHeight = listHeight - 4 = 8.
|
||||
// 8 lines is enough for the 6-line question.
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Line 6');
|
||||
expect(lastFrame()).not.toContain('lines hidden');
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 1`] = `
|
||||
"Select your preferred language:
|
||||
|
||||
1. TypeScript
|
||||
2. JavaScript
|
||||
● 3. Enter a custom value
|
||||
|
||||
Enter to submit · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 2`] = `
|
||||
"Select your preferred language:
|
||||
|
||||
1. TypeScript
|
||||
@@ -23,17 +12,6 @@ Enter to submit · Esc to cancel
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = `
|
||||
"Select your preferred language:
|
||||
|
||||
1. TypeScript
|
||||
2. JavaScript
|
||||
● 3. Type another language...
|
||||
|
||||
Enter to submit · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 2`] = `
|
||||
"Select your preferred language:
|
||||
|
||||
1. TypeScript
|
||||
@@ -47,20 +25,6 @@ Enter to submit · Esc to cancel
|
||||
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = `
|
||||
"Choose an option
|
||||
|
||||
▲
|
||||
● 1. Option 1
|
||||
Description 1
|
||||
2. Option 2
|
||||
Description 2
|
||||
▼
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 2`] = `
|
||||
"Choose an option
|
||||
|
||||
▲
|
||||
● 1. Option 1
|
||||
Description 1
|
||||
@@ -75,45 +39,6 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
|
||||
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 1`] = `
|
||||
"Choose an option
|
||||
|
||||
● 1. Option 1
|
||||
Description 1
|
||||
2. Option 2
|
||||
Description 2
|
||||
3. Option 3
|
||||
Description 3
|
||||
4. Option 4
|
||||
Description 4
|
||||
5. Option 5
|
||||
Description 5
|
||||
6. Option 6
|
||||
Description 6
|
||||
7. Option 7
|
||||
Description 7
|
||||
8. Option 8
|
||||
Description 8
|
||||
9. Option 9
|
||||
Description 9
|
||||
10. Option 10
|
||||
Description 10
|
||||
11. Option 11
|
||||
Description 11
|
||||
12. Option 12
|
||||
Description 12
|
||||
13. Option 13
|
||||
Description 13
|
||||
14. Option 14
|
||||
Description 14
|
||||
15. Option 15
|
||||
Description 15
|
||||
16. Enter a custom value
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 2`] = `
|
||||
"Choose an option
|
||||
|
||||
● 1. Option 1
|
||||
Description 1
|
||||
2. Option 2
|
||||
@@ -197,8 +122,8 @@ Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
|
||||
exports[`AskUserDialog > hides progress header for single question 1`] = `
|
||||
"Which authentication method should we use?
|
||||
|
||||
● 1. OAuth 2.0
|
||||
Industry standard, supports SSO
|
||||
● 1. OAuth 2.0
|
||||
Industry standard, supports SSO
|
||||
2. JWT tokens
|
||||
Stateless, good for APIs
|
||||
3. Enter a custom value
|
||||
@@ -210,8 +135,8 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
|
||||
exports[`AskUserDialog > renders question and options 1`] = `
|
||||
"Which authentication method should we use?
|
||||
|
||||
● 1. OAuth 2.0
|
||||
Industry standard, supports SSO
|
||||
● 1. OAuth 2.0
|
||||
Industry standard, supports SSO
|
||||
2. JWT tokens
|
||||
Stateless, good for APIs
|
||||
3. Enter a custom value
|
||||
@@ -225,8 +150,8 @@ exports[`AskUserDialog > shows Review tab in progress header for multiple questi
|
||||
|
||||
Which framework?
|
||||
|
||||
● 1. React
|
||||
Component library
|
||||
● 1. React
|
||||
Component library
|
||||
2. Vue
|
||||
Progressive framework
|
||||
3. Enter a custom value
|
||||
@@ -238,8 +163,8 @@ Enter to select · ←/→ to switch questions · Esc to cancel
|
||||
exports[`AskUserDialog > shows keyboard hints 1`] = `
|
||||
"Which authentication method should we use?
|
||||
|
||||
● 1. OAuth 2.0
|
||||
Industry standard, supports SSO
|
||||
● 1. OAuth 2.0
|
||||
Industry standard, supports SSO
|
||||
2. JWT tokens
|
||||
Stateless, good for APIs
|
||||
3. Enter a custom value
|
||||
@@ -253,8 +178,8 @@ exports[`AskUserDialog > shows progress header for multiple questions 1`] = `
|
||||
|
||||
Which database should we use?
|
||||
|
||||
● 1. PostgreSQL
|
||||
Relational database
|
||||
● 1. PostgreSQL
|
||||
Relational database
|
||||
2. MongoDB
|
||||
Document database
|
||||
3. Enter a custom value
|
||||
|
||||
@@ -19,41 +19,14 @@ Files to Modify
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
● 2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
|
||||
"Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
Implementation Steps
|
||||
|
||||
1. Create src/auth/AuthService.ts with login/logout methods
|
||||
2. Add session storage in src/storage/SessionStore.ts
|
||||
3. Update src/commands/index.ts to check auth status
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
● 3. Type your feedback...
|
||||
|
||||
Enter to submit · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = `
|
||||
"Overview
|
||||
|
||||
@@ -71,8 +44,8 @@ Files to Modify
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
@@ -81,33 +54,6 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 2`] = `
|
||||
"Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
Implementation Steps
|
||||
|
||||
1. Create src/auth/AuthService.ts with login/logout methods
|
||||
2. Add session storage in src/storage/SessionStore.ts
|
||||
3. Update src/commands/index.ts to check auth status
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
● 3. Add tests
|
||||
|
||||
Enter to submit · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = `
|
||||
" Error reading plan: File not found
|
||||
"
|
||||
@@ -130,8 +76,8 @@ Implementation Steps
|
||||
8. Add multi-factor authentication in src/auth/MFAService.ts
|
||||
... last 22 lines hidden (Ctrl+O to show) ...
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
@@ -157,8 +103,8 @@ Files to Modify
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
@@ -186,41 +132,14 @@ Files to Modify
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
● 2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
|
||||
"Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
Implementation Steps
|
||||
|
||||
1. Create src/auth/AuthService.ts with login/logout methods
|
||||
2. Add session storage in src/storage/SessionStore.ts
|
||||
3. Update src/commands/index.ts to check auth status
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
● 3. Type your feedback...
|
||||
|
||||
Enter to submit · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = `
|
||||
"Overview
|
||||
|
||||
@@ -238,8 +157,8 @@ Files to Modify
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
@@ -248,33 +167,6 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 2`] = `
|
||||
"Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
Implementation Steps
|
||||
|
||||
1. Create src/auth/AuthService.ts with login/logout methods
|
||||
2. Add session storage in src/storage/SessionStore.ts
|
||||
3. Update src/commands/index.ts to check auth status
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
● 3. Add tests
|
||||
|
||||
Enter to submit · Ctrl+X to edit plan · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = `
|
||||
" Error reading plan: File not found
|
||||
"
|
||||
@@ -318,8 +210,8 @@ Testing Strategy
|
||||
- Security penetration testing
|
||||
- Load testing for session management
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
@@ -345,8 +237,8 @@ Files to Modify
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
3. Type your feedback...
|
||||
|
||||
@@ -12,8 +12,8 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
|
||||
(r:) Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
...
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
...
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -22,8 +22,8 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
|
||||
(r:) Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
llllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
llllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -31,7 +31,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) commit
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
git commit -m "feat: add search" in src/app
|
||||
git commit -m "feat: add search" in src/app
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -39,7 +39,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) commit
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
git commit -m "feat: add search" in src/app
|
||||
git commit -m "feat: add search" in src/app
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -78,27 +78,6 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<LoadingIndicator /> > should truncate long primary text instead of wrapping 1`] = `
|
||||
"MockRespondin This is an extremely long loading phrase that shoul…(esc to
|
||||
"MockRespondin This is an extremely long loading phrase that shoul… (esc to
|
||||
gSpinner cancel, 5s)
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -4,7 +4,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focuse
|
||||
"ScrollableList
|
||||
AppHeader(full)
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊶ Shell Command Running a long command... │
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 10 │
|
||||
│ Line 11 │
|
||||
@@ -26,7 +26,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocu
|
||||
"ScrollableList
|
||||
AppHeader(full)
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊶ Shell Command Running a long command... │
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 10 │
|
||||
│ Line 11 │
|
||||
@@ -47,7 +47,7 @@ ShowMoreLines
|
||||
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
|
||||
"AppHeader(full)
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊶ Shell Command Running a long command... │
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ ... first 11 lines hidden (Ctrl+O to show) ... │
|
||||
│ Line 12 │
|
||||
@@ -67,7 +67,7 @@ ShowMoreLines
|
||||
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
|
||||
"AppHeader(full)
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊶ Shell Command Running a long command... │
|
||||
│ ⊷ Shell Command Running a long command... │
|
||||
│ │
|
||||
│ Line 1 │
|
||||
│ Line 2 │
|
||||
|
||||
@@ -6,7 +6,7 @@ exports[`SessionBrowser component > enters search mode, filters sessions, and re
|
||||
Search: query (Esc to cancel)
|
||||
|
||||
Index │ Msgs │ Age │ Match
|
||||
❯ #1 │ 1 │ 10mo │ You: Query is here a… (+1 more)
|
||||
❯ #1 │ 1 │ 10mo │ You: Query is here a… (+1 more)
|
||||
▼
|
||||
"
|
||||
`;
|
||||
@@ -17,7 +17,7 @@ exports[`SessionBrowser component > renders a list of sessions and marks current
|
||||
Sort: s Reverse: r First/Last: g/G
|
||||
|
||||
Index │ Msgs │ Age │ Name
|
||||
❯ #1 │ 5 │ 10mo │ Second conversation about dogs (current)
|
||||
❯ #1 │ 5 │ 10mo │ Second conversation about dogs (current)
|
||||
#2 │ 2 │ 10mo │ First conversation about cats
|
||||
▼
|
||||
"
|
||||
|
||||
@@ -24,7 +24,7 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
|
||||
│ │
|
||||
│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
│ To resume this session: gemini --resume test-session │
|
||||
│ Tip: Resume a previous session using gemini --resume or /resume │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="711" height="17" fill="#394545" />
|
||||
<rect x="828" y="153" width="45" height="17" fill="#394545" />
|
||||
<text x="828" y="155" fill="#a6e3a1" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -120,12 +112,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="711" height="17" fill="#394545" />
|
||||
<rect x="828" y="153" width="45" height="17" fill="#394545" />
|
||||
<text x="828" y="155" fill="#a6e3a1" textLength="45" lengthAdjust="spacingAndGlyphs">true*</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -120,12 +112,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="702" height="17" fill="#394545" />
|
||||
<rect x="819" y="153" width="54" height="17" fill="#394545" />
|
||||
<text x="819" y="155" fill="#a6e3a1" textLength="54" lengthAdjust="spacingAndGlyphs">false*</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -118,12 +110,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="711" height="17" fill="#394545" />
|
||||
<rect x="828" y="153" width="45" height="17" fill="#394545" />
|
||||
<text x="828" y="155" fill="#a6e3a1" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -120,12 +112,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="711" height="17" fill="#394545" />
|
||||
<rect x="828" y="153" width="45" height="17" fill="#394545" />
|
||||
<text x="828" y="155" fill="#a6e3a1" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -120,12 +112,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -106,18 +106,12 @@
|
||||
<text x="0" y="580" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="597" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Apply To</text>
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="18" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
|
||||
<rect x="63" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="72" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="72" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="189" y="612" width="684" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> 2. Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="702" height="17" fill="#394545" />
|
||||
<rect x="819" y="153" width="54" height="17" fill="#394545" />
|
||||
<text x="819" y="155" fill="#a6e3a1" textLength="54" lengthAdjust="spacingAndGlyphs">false*</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -119,12 +111,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="711" height="17" fill="#394545" />
|
||||
<rect x="828" y="153" width="45" height="17" fill="#394545" />
|
||||
<text x="828" y="155" fill="#a6e3a1" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -120,12 +112,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -8,22 +8,22 @@
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="19" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs" font-weight="bold">> Settings </text>
|
||||
<text x="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> > Settings </text>
|
||||
<text x="891" y="36" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="53" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="18" y="70" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="891" y="70" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="85" width="9" height="17" fill="#ffffff" />
|
||||
<text x="36" y="87" fill="#000000" textLength="9" lengthAdjust="spacingAndGlyphs">S</text>
|
||||
<text x="45" y="87" fill="#6c7086" textLength="135" lengthAdjust="spacingAndGlyphs">earch to filter</text>
|
||||
<text x="873" y="87" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="873" y="87" fill="#89b4fa" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="87" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#a6e3a1" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="18" y="104" fill="#89b4fa" textLength="864" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="104" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="121" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -31,20 +31,12 @@
|
||||
<text x="27" y="138" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs">▲</text>
|
||||
<text x="891" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="153" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="153" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="153" width="72" height="17" fill="#394545" />
|
||||
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
|
||||
<rect x="117" y="153" width="711" height="17" fill="#394545" />
|
||||
<rect x="828" y="153" width="45" height="17" fill="#394545" />
|
||||
<text x="828" y="155" fill="#a6e3a1" textLength="45" lengthAdjust="spacingAndGlyphs">true*</text>
|
||||
<text x="891" y="155" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="170" width="18" height="17" fill="#394545" />
|
||||
<rect x="45" y="170" width="198" height="17" fill="#394545" />
|
||||
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
|
||||
<rect x="243" y="170" width="630" height="17" fill="#394545" />
|
||||
<text x="891" y="172" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
@@ -118,12 +110,8 @@
|
||||
<text x="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Apply To </text>
|
||||
<text x="891" y="597" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="27" y="612" width="9" height="17" fill="#394545" />
|
||||
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="36" y="612" width="9" height="17" fill="#394545" />
|
||||
<rect x="45" y="612" width="117" height="17" fill="#394545" />
|
||||
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
|
||||
<rect x="162" y="612" width="711" height="17" fill="#394545" />
|
||||
<text x="891" y="614" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="631" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Workspace Settings </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
@@ -7,7 +7,7 @@ exports[`SuggestionsDisplay > handles scrolling 1`] = `
|
||||
Cmd 7 Description 7
|
||||
Cmd 8 Description 8
|
||||
Cmd 9 Description 9
|
||||
Cmd 10 Description 10
|
||||
Cmd 10 Description 10
|
||||
Cmd 11 Description 11
|
||||
Cmd 12 Description 12
|
||||
▼
|
||||
@@ -17,13 +17,13 @@ exports[`SuggestionsDisplay > handles scrolling 1`] = `
|
||||
|
||||
exports[`SuggestionsDisplay > highlights active item 1`] = `
|
||||
" command1 Description 1
|
||||
command2 Description 2
|
||||
command2 Description 2
|
||||
command3 Description 3
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`SuggestionsDisplay > renders MCP tag for MCP prompts 1`] = `
|
||||
" mcp-tool [MCP]
|
||||
" mcp-tool [MCP]
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -33,7 +33,7 @@ exports[`SuggestionsDisplay > renders loading state 1`] = `
|
||||
`;
|
||||
|
||||
exports[`SuggestionsDisplay > renders suggestions list 1`] = `
|
||||
" command1 Description 1
|
||||
" command1 Description 1
|
||||
command2 Description 2
|
||||
command3 Description 3
|
||||
"
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs" font-weight="bold">ID</text>
|
||||
<text x="45" y="2" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs" font-weight="bold">Name</text>
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">ID Name </text>
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">1 Alice </text>
|
||||
<text x="0" y="53" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">2 Bob </text>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -4,7 +4,7 @@
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs" font-weight="bold">Value</text>
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Value </text>
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<text x="0" y="36" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">20</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 948 B After Width: | Height: | Size: 1.0 KiB |
@@ -4,7 +4,7 @@
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">Status</text>
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Status </text>
|
||||
<text x="0" y="19" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">────────────────────────────────────────────────────────────────────────────────────────────────────</text>
|
||||
<rect x="0" y="34" width="54" height="17" fill="#ffffff" />
|
||||
<text x="0" y="36" fill="#000000" textLength="54" lengthAdjust="spacingAndGlyphs">Active</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 1017 B After Width: | Height: | Size: 1.1 KiB |
@@ -89,7 +89,7 @@ exports[`ThemeDialog Snapshots > should render correctly in scope selector mode
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThemeDialog Snapshots > should render correctly in theme selection mode (isDevelopment: false) 1`] = `
|
||||
exports[`ThemeDialog Snapshots > should render correctly in theme selection mode 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Select Theme Preview │
|
||||
@@ -113,90 +113,3 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThemeDialog Snapshots > should render correctly in theme selection mode (isDevelopment: true) 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Select Theme Preview │
|
||||
│ ▲ ┌─────────────────────────────────────────────────┐ │
|
||||
│ ● 1. ANSI Dark (Matches terminal) │ │ │
|
||||
│ 2. Atom One Dark │ 1 # function │ │
|
||||
│ 3. Ayu Dark │ 2 def fibonacci(n): │ │
|
||||
│ 4. Default Dark │ 3 a, b = 0, 1 │ │
|
||||
│ 5. Dracula Dark │ 4 for _ in range(n): │ │
|
||||
│ 6. GitHub Dark │ 5 a, b = b, a + b │ │
|
||||
│ 7. Holiday Dark │ 6 return a │ │
|
||||
│ 8. Shades Of Purple Dark │ │ │
|
||||
│ 9. Solarized Dark │ 1 - print("Hello, " + name) │ │
|
||||
│ 10. ANSI Light │ 1 + print(f"Hello, {name}!") │ │
|
||||
│ 11. Ayu Light │ │ │
|
||||
│ 12. Default Light └─────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ╭─────────────────────────────────────────────────╮ │
|
||||
│ │ DEVELOPER TOOLS (Not visible to users) │ │
|
||||
│ │ │ │
|
||||
│ │ How do colors get applied? │ │
|
||||
│ │ • Hex: Rendered exactly by modern terminals. │ │
|
||||
│ │ Not overridden by app themes. │ │
|
||||
│ │ • Blank: Uses your terminal's default │ │
|
||||
│ │ foreground/background. │ │
|
||||
│ │ • Compatibility: On older terminals, hex is │ │
|
||||
│ │ approximated to the nearest ANSI color. │ │
|
||||
│ │ • ANSI Names: 'red', 'green', etc. are mapped │ │
|
||||
│ │ to your terminal app's palette. │ │
|
||||
│ │ │ │
|
||||
│ │ Value Name │ │
|
||||
│ │ #1E1E… backgroun Main terminal background │ │
|
||||
│ │ d.primary color │ │
|
||||
│ │ #313… backgroun Subtle background for │ │
|
||||
│ │ d.message message blocks │ │
|
||||
│ │ #313… backgroun Background for the input │ │
|
||||
│ │ d.input prompt │ │
|
||||
│ │ #39… background. Background highlight for │ │
|
||||
│ │ focus selected/focused items │ │
|
||||
│ │ #283… backgrou Background for added lines │ │
|
||||
│ │ nd.diff. in diffs │ │
|
||||
│ │ added │ │
|
||||
│ │ #430… backgroun Background for removed │ │
|
||||
│ │ d.diff.re lines in diffs │ │
|
||||
│ │ moved │ │
|
||||
│ │ (blank text.prim Primary text color (uses │ │
|
||||
│ │ ) ary terminal default if blank) │ │
|
||||
│ │ #6C7086 text.secon Secondary/dimmed text │ │
|
||||
│ │ dary color │ │
|
||||
│ │ #89B4FA text.link Hyperlink and highlighting │ │
|
||||
│ │ color │ │
|
||||
│ │ #CBA6F7 text.accen Accent color for │ │
|
||||
│ │ t emphasis │ │
|
||||
│ │ (blank) text.res Color for model response │ │
|
||||
│ │ ponse text (uses terminal default │ │
|
||||
│ │ if blank) │ │
|
||||
│ │ #3d3f51 border.def Standard border color │ │
|
||||
│ │ ault │ │
|
||||
│ │ #6C7086ui.comme Color for code comments and │ │
|
||||
│ │ nt metadata │ │
|
||||
│ │ #6C708 ui.symbol Color for technical symbols │ │
|
||||
│ │ 6 and UI icons │ │
|
||||
│ │ #89B4F ui.active Border color for active or │ │
|
||||
│ │ A running elements │ │
|
||||
│ │ #3d3f5 ui.dark Deeply dimmed color for │ │
|
||||
│ │ 1 subtle UI elements │ │
|
||||
│ │ #A6E3A ui.focus Color for focused elements │ │
|
||||
│ │ 1 (e.g. selected menu items, │ │
|
||||
│ │ focused borders) │ │
|
||||
│ │ #F38BA8status.err Color for error messages │ │
|
||||
│ │ or and critical status │ │
|
||||
│ │ #A6E3A1status.suc Color for success messages │ │
|
||||
│ │ cess and positive status │ │
|
||||
│ │ #F9E2A status.wa Color for warnings and │ │
|
||||
│ │ F rning cautionary status │ │
|
||||
│ │ #4796E4 ui.gradien │ │
|
||||
│ │ #847ACE t │ │
|
||||
│ │ #C3677F │ │
|
||||
│ ╰─────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to configure scope, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('<ShellToolMessage />', () => {
|
||||
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
|
||||
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
|
||||
])('clicks inside the shell area sets focus for %s', async (_, name) => {
|
||||
const { lastFrame, simulateClick, unmount } = renderShell(
|
||||
const { lastFrame, simulateClick } = renderShell(
|
||||
{ name },
|
||||
{ mouseEventsEnabled: true },
|
||||
);
|
||||
@@ -79,7 +79,6 @@ describe('<ShellToolMessage />', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
it('resets focus when shell finishes', async () => {
|
||||
let updateStatus: (s: CoreToolCallStatus) => void = () => {};
|
||||
@@ -92,7 +91,7 @@ describe('<ShellToolMessage />', () => {
|
||||
return <ShellToolMessage {...baseProps} status={status} ptyId={1} />;
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(<Wrapper />, {
|
||||
const { lastFrame } = renderWithProviders(<Wrapper />, {
|
||||
uiActions,
|
||||
uiState: {
|
||||
streamingState: StreamingState.Idle,
|
||||
@@ -116,7 +115,6 @@ describe('<ShellToolMessage />', () => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
|
||||
expect(lastFrame()).not.toContain('(Shift+Tab to unfocus)');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,13 +164,9 @@ describe('<ShellToolMessage />', () => {
|
||||
},
|
||||
],
|
||||
])('%s', async (_, props, options) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderShell(
|
||||
props,
|
||||
options,
|
||||
);
|
||||
const { lastFrame, waitUntilReady } = renderShell(props, options);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,7 +197,7 @@ describe('<ShellToolMessage />', () => {
|
||||
false,
|
||||
],
|
||||
])('%s', async (_, availableTerminalHeight, expectedMaxLines, focused) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderShell(
|
||||
const { lastFrame, waitUntilReady } = renderShell(
|
||||
{
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
@@ -224,11 +218,10 @@ describe('<ShellToolMessage />', () => {
|
||||
const frame = lastFrame();
|
||||
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||
expect(frame).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('fully expands in standard mode when availableTerminalHeight is undefined', async () => {
|
||||
const { lastFrame, unmount } = renderShell(
|
||||
const { lastFrame } = renderShell(
|
||||
{
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
@@ -243,11 +236,10 @@ describe('<ShellToolMessage />', () => {
|
||||
// Should show all 100 lines
|
||||
expect(frame.match(/Line \d+/g)?.length).toBe(100);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('fully expands in alternate buffer mode when constrainHeight is false and isExpandable is true', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderShell(
|
||||
const { lastFrame, waitUntilReady } = renderShell(
|
||||
{
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
@@ -270,11 +262,10 @@ describe('<ShellToolMessage />', () => {
|
||||
expect(frame.match(/Line \d+/g)?.length).toBe(100);
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('stays constrained in alternate buffer mode when isExpandable is false even if constrainHeight is false', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderShell(
|
||||
const { lastFrame, waitUntilReady } = renderShell(
|
||||
{
|
||||
resultDisplay: LONG_OUTPUT,
|
||||
renderOutputAsMarkdown: false,
|
||||
@@ -297,7 +288,6 @@ describe('<ShellToolMessage />', () => {
|
||||
expect(frame.match(/Line \d+/g)?.length).toBe(15);
|
||||
});
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,11 +125,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
borderDimColor={borderDimColor}
|
||||
containerRef={headerRef}
|
||||
>
|
||||
<ToolStatusIndicator
|
||||
status={status}
|
||||
name={name}
|
||||
isFocused={isThisShellFocused}
|
||||
/>
|
||||
<ToolStatusIndicator status={status} name={name} />
|
||||
|
||||
<ToolInfo
|
||||
name={name}
|
||||
|
||||