Compare commits

..

21 Commits

Author SHA1 Message Date
Adam Weidman 6f1c6ee4b8 fix: stop old services before rebuild to prevent TUI noise flooding serial 2026-03-04 10:06:45 -05:00
Adam Weidman ff8384ab29 debug: bridge logs to serial, suppress agent TUI noise 2026-03-04 09:55:35 -05:00
Adam Weidman 5819b67273 debug: enable agent stderr on serial for troubleshooting 2026-03-04 09:44:49 -05:00
Adam Weidman 444e42f6bf feat: one-command deployment script and parameterized startup
- deploy-forever-agent.sh: creates Pub/Sub, static IP, IAM, VM in one command
- teardown-forever-agent.sh: cleans up all resources
- gce-startup.sh: reads config from instance metadata instead of hardcoding
2026-03-04 09:36:14 -05:00
Adam Weidman 6d120edaf4 fix: use systemctl restart to pick up newly built code on VM boot 2026-03-04 09:27:51 -05:00
Adam Weidman 1b313d6475 feat: switch chat bridge from HTTP webhook to Pub/Sub
Google Chat publishes to Pub/Sub topic, bridge pulls messages.
Eliminates all inbound firewall/networking issues on GCE VM.
2026-03-04 09:19:41 -05:00
Adam Weidman 7bc0515c7c feat: GCE forever agent with Cloud Run bridge support
- Add gce-startup.sh: self-contained VM startup (install, build, configure, run)
- Add start-forever.js: dual-process launcher for local development
- Add npm scripts: start:forever, start:bridge
- Bind A2A listener to 0.0.0.0 (was 127.0.0.1) for remote bridge access
- Pre-configure all headless settings to bypass interactive dialogs
- Use .env file + script pseudo-TTY for systemd service
2026-03-03 23:45:17 -05:00
Adam Weidman 8caf7d5690 feat: add Google Chat bridge for forever mode
- Minimal Express bridge that receives Google Chat webhooks, forwards
  to the forever agent's external listener via JSON-RPC, and pushes
  responses back via Chat API
- Chat API client for async message delivery (Google Chat webhooks
  have a 30s timeout, agent responses take longer)
- JWT verification for Google Chat requests (skippable for local dev)
- External listener default port changed to 3100 (configurable via
  A2A_PORT env var), blocking timeout increased to 30min
- Startup script (scripts/start-forever.sh) launches both processes
- FOREVER.md with full setup instructions (local dev + GCE VM)
2026-03-03 15:26:31 -05:00
Sandy Tao 89d62f387f listen 2026-02-25 18:52:13 -08:00
Sandy Tao 1ca7c35027 feat: introduce Forever Mode (Sisyphus, Confucius, and Bicameral Voice) 2026-02-25 17:27:05 -08:00
Abhi 28c9a907de feat(core): introduce Kind.Agent for sub-agent classification (#20369) 2026-02-26 01:04:20 +00:00
Jacob Richman f9f916e1dc test: support tests that include color information (#20220) 2026-02-25 23:31:35 +00:00
anj-s 78dfe9dea8 feat(core): implement task tracker foundation and service (#19464) 2026-02-25 23:17:17 +00:00
nityam c9d07b62f2 fix: remove trailing comma in issue triage workflow settings json (#20265) 2026-02-25 22:05:21 +00:00
joshualitt 189ab6c637 feat(core): Enable model steering in workspace. (#20343) 2026-02-25 21:18:40 +00:00
Tommaso Sciortino 4aec3cdb24 docs: remove legacy cmd examples in favor of powershell (#20323)
avoid flakes
2026-02-25 12:09:49 -08:00
Achal Jhawar b765fb8af4 fix(cli): expose model.name setting in settings dialog for persistence (#19605) 2026-02-25 19:56:05 +00:00
Himanshu Soni fb0c7d268f fix(cli): skip 404 errors in setup-github file downloads (#20287) 2026-02-25 18:45:02 +00:00
kevinjwang1 be12371380 Plumb the maxAttempts setting through Config args (#20239) 2026-02-25 17:58:07 +00:00
Tommaso Sciortino a037b961b1 feat(cli): load extensions in parallel (#20229) 2026-02-25 17:15:00 +00:00
Adam Weidman 6c739955c0 feat(core): implement robust A2A streaming reassembly and fix task continuity (#20091) 2026-02-25 16:51:08 +00:00
147 changed files with 8090 additions and 1489 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
{
"experimental": {
"plan": true,
"extensionReloading": true
"extensionReloading": true,
"modelSteering": true
},
"general": {
"devtools": true
@@ -158,7 +158,7 @@ jobs:
},
"coreTools": [
"run_shell_command(echo)"
],
]
}
prompt: |-
## Role
+1 -1
View File
@@ -61,4 +61,4 @@ gemini-debug.log
.genkit
.gemini-clipboard/
.eslintcache
evals/logs/
evals/logs/
+255
View File
@@ -0,0 +1,255 @@
# Forever Agent + Google Chat Bridge
Run Gemini CLI as an autonomous forever agent, accessible via Google Chat.
## Architecture
```
Google Chat Space
↓ webhook (HTTPS)
Chat Bridge (port 8081, public)
↓ JSON-RPC (localhost)
gemini-cli --forever (port 3100, localhost only)
Gemini API (LLM + tools)
```
**Two processes on one VM:**
- **gemini-cli --forever** — the agent, runs continuously with Sisyphus
auto-resume
- **Chat bridge** — receives Google Chat webhooks, forwards to agent, pushes
responses back via Chat API
One agent per Google Chat space. YOLO mode (auto-approve all tools).
## Prerequisites
1. **Google Cloud project** with:
- Chat API enabled
- A service account with `Chat Bot` role
- Service account key JSON file
2. **Gemini API key** from
[Google AI Studio](https://aistudio.google.com/apikey)
3. **Node.js 20+**
## Local Development (with ngrok)
### 1. Install and build
```bash
git clone -b st/forever https://github.com/google-gemini/gemini-cli.git gemini-cli-forever
cd gemini-cli-forever
npm install --ignore-scripts
npm run build
```
### 2. Set env vars
```bash
export GOOGLE_API_KEY="your-gemini-api-key"
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"
export A2A_PORT=3100
export BRIDGE_PORT=8081
# Optional: set for JWT verification (your GCP project number, not project ID)
# export CHAT_PROJECT_NUMBER="123456789"
```
### 3. Start ngrok (separate terminal)
```bash
ngrok http 8081
```
Copy the HTTPS URL (e.g., `https://abc123.ngrok.io`).
### 4. Configure Google Chat App
1. Go to
[Google Cloud Console → APIs & Services → Google Chat API](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat)
2. Click **Configuration**
3. Set:
- **App name:** Forever Agent
- **App URL:** `https://abc123.ngrok.io/chat/webhook`
- **Visibility:** People and groups in your domain (or specific people)
- **Functionality:** Spaces and group conversations (check), Direct messages
(check)
- **Connection settings:** HTTP endpoint URL
- **Permissions:** Everyone in your Workspace domain (or specific people)
4. Save
### 5. Start the agent
```bash
./scripts/start-forever.sh
```
The onboarding dialog will ask for a mission and Sisyphus config on first run.
### 6. Test
1. Open Google Chat
2. Create a new space and add the "Forever Agent" app
3. Send a message — the agent will process it and respond
## GCE VM Deployment
### 1. Create the VM
```bash
gcloud compute instances create forever-agent \
--zone=us-central1-a \
--machine-type=e2-small \
--image-family=debian-12 \
--image-project=debian-cloud \
--boot-disk-size=20GB \
--tags=forever-agent
```
### 2. Allow inbound traffic on bridge port
```bash
gcloud compute firewall-rules create allow-chat-bridge \
--allow=tcp:8081 \
--target-tags=forever-agent \
--source-ranges=0.0.0.0/0 \
--description="Allow Google Chat webhooks to reach the bridge"
```
### 3. SSH and install Node.js
```bash
gcloud compute ssh forever-agent --zone=us-central1-a
# On the VM:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs git
```
### 4. Clone and build
```bash
git clone -b st/forever https://github.com/google-gemini/gemini-cli.git
cd gemini-cli
npm install --ignore-scripts
npm run build
```
### 5. Configure
```bash
# Create env file
cat > ~/.forever-agent.env << 'EOF'
GOOGLE_API_KEY=your-gemini-api-key
GOOGLE_APPLICATION_CREDENTIALS=/home/$USER/service-account-key.json
A2A_PORT=3100
BRIDGE_PORT=8081
CHAT_PROJECT_NUMBER=your-project-number
EOF
# Upload service account key
# (from your local machine):
# gcloud compute scp service-account-key.json forever-agent:~/service-account-key.json --zone=us-central1-a
```
### 6. Create systemd service
```bash
sudo tee /etc/systemd/system/forever-agent.service << EOF
[Unit]
Description=Gemini CLI Forever Agent
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=/home/$USER/gemini-cli
EnvironmentFile=/home/$USER/.forever-agent.env
ExecStart=/home/$USER/gemini-cli/scripts/start-forever.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable forever-agent
sudo systemctl start forever-agent
```
### 7. Check logs
```bash
sudo journalctl -u forever-agent -f
```
### 8. Configure Google Chat App
Same as local dev, but use the VM's external IP:
- **App URL:** `http://EXTERNAL_IP:8081/chat/webhook`
> **Note:** Google Chat requires HTTPS in production. For HTTPS, either:
>
> - Put nginx + Let's Encrypt in front
> (`sudo apt install nginx certbot python3-certbot-nginx`)
> - Use a Cloud Load Balancer with managed cert
> - For testing, HTTP works with some Chat API configurations
## Env Vars Reference
| Variable | Required | Default | Description |
| -------------------------------- | -------- | ----------------------- | --------------------------------------- |
| `GOOGLE_API_KEY` | Yes | — | Gemini API key |
| `GOOGLE_APPLICATION_CREDENTIALS` | Yes | — | Path to service account key JSON |
| `A2A_PORT` | No | `3100` | External listener port (agent) |
| `BRIDGE_PORT` | No | `8081` | Chat bridge port (public-facing) |
| `A2A_URL` | No | `http://127.0.0.1:3100` | Agent URL (for bridge to connect to) |
| `CHAT_PROJECT_NUMBER` | No | — | GCP project number for JWT verification |
## How It Works
1. User sends message in Google Chat space
2. Google Chat POSTs webhook to the bridge (`/chat/webhook`)
3. Bridge immediately returns `{}` (Google Chat has a 30s webhook timeout)
4. Bridge asynchronously sends the message to the forever agent via JSON-RPC
5. Agent processes the message (may take minutes — tools, thinking, etc.)
6. Agent returns response to bridge
7. Bridge pushes response to Google Chat via Chat REST API
The forever agent runs with:
- **Sisyphus** — auto-resumes after idle timeout (configurable)
- **Confucius** — reflects and consolidates knowledge at ~80% context
- **Hippocampus** — extracts key facts after each turn
- **Bicameral Voice** — proactively captures knowledge from user messages
- **YOLO mode** — auto-approves all tool calls (no human-in-the-loop)
## Troubleshooting
### Bridge returns 401/403
- Check `CHAT_PROJECT_NUMBER` matches your GCP project number (not project ID)
- Unset `CHAT_PROJECT_NUMBER` to disable JWT verification for testing
### Agent doesn't respond
- Check the agent is running:
`curl http://localhost:3100/.well-known/agent-card.json`
- Check bridge health: `curl http://localhost:8081/health`
- Check bridge logs for errors
### Google Chat shows no response
- The bridge returns `{}` immediately — responses are pushed async via Chat API
- Ensure `GOOGLE_APPLICATION_CREDENTIALS` points to a valid service account key
- Ensure the service account has `Chat Bot` role
### Webhook not reaching bridge
- Check firewall rules allow inbound on port 8081
- For HTTPS requirement: use ngrok for testing, nginx + Let's Encrypt for
production
+7 -6
View File
@@ -82,12 +82,13 @@ they appear in the UI.
### Model
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- |
| Model | `model.name` | The Gemini model to use for conversations. | `undefined` |
| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` |
| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` |
| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` |
### Context
+1 -10
View File
@@ -19,16 +19,7 @@ can find your npm cache path by running `npm config get cache`.
rm -rf "$(npm config get cache)/_npx"
```
**For Windows**
_Command Prompt_
```cmd
:: The path is typically %LocalAppData%\npm-cache\_npx
rmdir /s /q "%LocalAppData%\npm-cache\_npx"
```
_PowerShell_
**For Windows (PowerShell)**
```powershell
# The path is typically $env:LocalAppData\npm-cache\_npx
+1
View File
@@ -17086,6 +17086,7 @@
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"google-auth-library": "^9.15.1",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.8",
"uuid": "^13.0.0",
+2
View File
@@ -19,6 +19,8 @@
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
"start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server",
"start:forever": "node scripts/start-forever.js",
"start:bridge": "node packages/a2a-server/dist/src/chat-bridge/bridge.js",
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
"deflake": "node scripts/deflake.js",
"deflake:test:integration:sandbox:none": "npm run deflake -- --command=\"npm run test:integration:sandbox:none -- --retry=0\"",
+2
View File
@@ -26,9 +26,11 @@
],
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/pubsub": "^4.9.0",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"google-auth-library": "^9.15.1",
"fs-extra": "^11.3.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.8",
@@ -0,0 +1,290 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Google Chat bridge for the Gemini CLI forever mode via Cloud Pub/Sub.
*
* Architecture:
* Google Chat → Pub/Sub topic → this bridge (pull subscriber) → agent (localhost:3100)
* Response comes back → bridge pushes to Google Chat via Chat API
*
* One agent per VM. Messages are forwarded as-is to the running
* gemini-cli --forever session via its JSON-RPC external listener.
*/
import http from 'node:http';
import { PubSub } from '@google-cloud/pubsub';
import { ChatApiClient } from './chat-api-client.js';
import { logger } from '../utils/logger.js';
// --- Config from env vars ---
const A2A_URL = process.env['A2A_URL'] ?? 'http://127.0.0.1:3100';
const GOOGLE_CLOUD_PROJECT = process.env['GOOGLE_CLOUD_PROJECT'] ?? '';
const PUBSUB_SUBSCRIPTION =
process.env['PUBSUB_SUBSCRIPTION'] ?? 'forever-agent-chat-sub';
const HEALTH_PORT = parseInt(process.env['BRIDGE_PORT'] ?? '8081', 10);
// --- Types ---
interface ChatMessagePart {
kind?: string;
text?: string;
}
interface JsonRpcResponse {
jsonrpc: string;
id: string | number | null;
result?: {
kind: string;
id: string;
status: {
state: string;
message?: {
parts: ChatMessagePart[];
};
};
};
error?: { code: number; message: string };
}
// --- Event normalization ---
function isObj(v: unknown): v is Record<string, unknown> {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function str(obj: Record<string, unknown>, key: string): string {
const v = obj[key];
return typeof v === 'string' ? v : '';
}
interface NormalizedEvent {
type: string;
text: string;
spaceName: string;
threadName: string;
}
/**
* Extract the essentials from a Google Chat event.
* Handles both legacy and Workspace Add-ons format.
*/
function normalizeEvent(raw: Record<string, unknown>): NormalizedEvent | null {
// Legacy format
if (typeof raw['type'] === 'string') {
const message = isObj(raw['message']) ? raw['message'] : {};
const space = isObj(raw['space'])
? raw['space']
: isObj(message['space'])
? message['space']
: {};
const thread = isObj(message['thread']) ? message['thread'] : {};
return {
type: raw['type'],
text: str(message, 'text'),
spaceName: str(space, 'name'),
threadName: str(thread, 'name'),
};
}
// Workspace Add-ons format
const chat = raw['chat'];
if (!isObj(chat)) return null;
if (isObj(chat['messagePayload'])) {
const payload = chat['messagePayload'];
const message = isObj(payload['message']) ? payload['message'] : {};
const space = isObj(payload['space'])
? payload['space']
: isObj(message['space'])
? message['space']
: {};
const thread = isObj(message['thread']) ? message['thread'] : {};
return {
type: 'MESSAGE',
text: str(message, 'text'),
spaceName: str(space, 'name'),
threadName: str(thread, 'name'),
};
}
if (isObj(chat['addedToSpacePayload'])) {
const payload = chat['addedToSpacePayload'];
const space = isObj(payload['space']) ? payload['space'] : {};
return {
type: 'ADDED_TO_SPACE',
text: '',
spaceName: str(space, 'name'),
threadName: '',
};
}
return null;
}
// --- JSON-RPC call to external listener ---
async function sendToAgent(text: string): Promise<string> {
const body = JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'message/send',
params: {
message: {
role: 'user',
parts: [{ kind: 'text', text }],
},
},
});
const response = await fetch(A2A_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
if (!response.ok) {
throw new Error(
`Agent returned ${response.status}: ${await response.text()}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = (await response.json()) as JsonRpcResponse;
if (result.error) {
throw new Error(`Agent error: ${result.error.message}`);
}
// Extract text from the response
const parts = result.result?.status?.message?.parts ?? [];
const texts = parts
.filter((p) => p.kind === 'text' && p.text)
.map((p) => p.text!);
return texts.join('\n') || '(no response)';
}
// --- Async message processing ---
async function processMessageAsync(
chatApi: ChatApiClient,
event: NormalizedEvent,
): Promise<void> {
const { text, spaceName, threadName } = event;
try {
logger.info(`[Bridge] Sending to agent: "${text.substring(0, 100)}"`);
const responseText = await sendToAgent(text);
logger.info(
`[Bridge] Agent response (${responseText.length} chars): "${responseText.substring(0, 100)}..."`,
);
// Push response back to Google Chat
await chatApi.sendMessage(spaceName, threadName, { text: responseText });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.error(`[Bridge] Error: ${msg}`);
await chatApi.sendMessage(spaceName, threadName, {
text: `Error: ${msg}`,
});
}
}
// --- Pub/Sub subscriber ---
function startSubscriber(): void {
if (!GOOGLE_CLOUD_PROJECT) {
logger.error(
'[Bridge] GOOGLE_CLOUD_PROJECT not set — cannot start Pub/Sub subscriber',
);
process.exit(1);
}
const pubsub = new PubSub({ projectId: GOOGLE_CLOUD_PROJECT });
const subscription = pubsub.subscription(PUBSUB_SUBSCRIPTION);
const chatApi = new ChatApiClient();
logger.info(
`[Bridge] Subscribing to ${PUBSUB_SUBSCRIPTION} in project ${GOOGLE_CLOUD_PROJECT}`,
);
logger.info(`[Bridge] Forwarding to agent at ${A2A_URL}`);
subscription.on('message', (message) => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const raw = JSON.parse(message.data.toString()) as Record<
string,
unknown
>;
message.ack();
const event = normalizeEvent(raw);
if (!event) {
logger.warn(
`[Bridge] Unknown event format: ${Object.keys(raw).join(',')}`,
);
return;
}
logger.info(
`[Bridge] ${event.type}: space=${event.spaceName} text="${event.text.substring(0, 100)}"`,
);
if (event.type === 'ADDED_TO_SPACE') {
chatApi
.sendMessage(event.spaceName, '', {
text: 'Gemini CLI forever agent connected. Send me a task!',
})
.catch((err) =>
logger.error(`[Bridge] Welcome message failed: ${err}`),
);
return;
}
if (event.type !== 'MESSAGE' || !event.text) return;
processMessageAsync(chatApi, event).catch((err) => {
logger.error(`[Bridge] Async processing failed: ${err}`);
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.error(`[Bridge] Failed to parse Pub/Sub message: ${msg}`);
message.ack(); // ack to avoid redelivery of bad messages
}
});
subscription.on('error', (err) => {
logger.error(`[Bridge] Pub/Sub subscription error: ${err.message}`);
});
// Health check for systemd
http
.createServer((_req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
status: 'ok',
mode: 'pubsub',
subscription: PUBSUB_SUBSCRIPTION,
a2aUrl: A2A_URL,
}),
);
})
.listen(HEALTH_PORT, '127.0.0.1', () => {
logger.info(`[Bridge] Health check on port ${HEALTH_PORT}`);
});
}
// --- Standalone entrypoint ---
if (
process.argv[1]?.endsWith('bridge.js') ||
process.argv[1]?.endsWith('bridge.ts')
) {
startSubscriber();
}
@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Google Chat REST API client for pushing messages to spaces.
*/
import { GoogleAuth } from 'google-auth-library';
import { logger } from '../utils/logger.js';
const CHAT_API_BASE = 'https://chat.googleapis.com/v1';
const MAX_TEXT_LENGTH = 4000;
export class ChatApiClient {
private auth: GoogleAuth;
private initialized = false;
constructor() {
this.auth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/chat.bot'],
});
}
private async init(): Promise<void> {
if (this.initialized) return;
await this.auth.getClient();
this.initialized = true;
logger.info('[ChatApi] Initialized');
}
async sendMessage(
spaceName: string,
threadName: string,
options: { text?: string },
): Promise<void> {
await this.init();
const chunks = options.text ? splitText(options.text) : [''];
for (const chunk of chunks) {
const message: Record<string, unknown> = {};
if (chunk) message['text'] = chunk;
if (threadName) {
message['thread'] = { name: threadName };
}
await this.postMessage(spaceName, message);
}
}
private async postMessage(
spaceName: string,
message: Record<string, unknown>,
): Promise<void> {
try {
const url =
`${CHAT_API_BASE}/${spaceName}/messages` +
`?messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD`;
const client = await this.auth.getClient();
const headers = await client.getRequestHeaders();
const response = await fetch(url, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(message),
});
if (!response.ok) {
const body = await response.text();
logger.error(`[ChatApi] Send failed: ${response.status} ${body}`);
} else {
logger.info(`[ChatApi] Message sent to ${spaceName}`);
}
} catch (error) {
const msg = error instanceof Error ? error.message : 'Unknown error';
logger.error(`[ChatApi] Error: ${msg}`);
}
}
}
function splitText(text: string): string[] {
if (text.length <= MAX_TEXT_LENGTH) return [text];
const chunks: string[] = [];
let remaining = text;
while (remaining.length > MAX_TEXT_LENGTH) {
let splitAt = remaining.lastIndexOf('\n\n', MAX_TEXT_LENGTH);
if (splitAt < MAX_TEXT_LENGTH * 0.3) {
splitAt = remaining.lastIndexOf('\n', MAX_TEXT_LENGTH);
}
if (splitAt < MAX_TEXT_LENGTH * 0.3) {
splitAt = MAX_TEXT_LENGTH;
}
chunks.push(remaining.substring(0, splitAt));
remaining = remaining.substring(splitAt);
}
if (remaining) chunks.push(remaining);
return chunks;
}
@@ -13,34 +13,21 @@ import {
afterEach,
type Mock,
} from 'vitest';
import { format } from 'node:util';
import { coreEvents } from '@google/gemini-cli-core';
import { type Argv } from 'yargs';
import { handleLink, linkCommand } from './link.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
},
debugLogger,
};
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{ stripAnsi: true },
);
});
vi.mock('../../config/extension-manager.js');
@@ -95,7 +82,7 @@ describe('extensions link command', () => {
source: '/local/path/to/extension',
type: 'link',
});
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
'Extension "my-linked-extension" linked successfully and enabled.',
);
@@ -116,7 +103,7 @@ describe('extensions link command', () => {
await handleLink({ path: '/local/path/to/extension' });
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'error',
'Link failed message',
);
@@ -5,33 +5,22 @@
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { coreEvents } from '@google/gemini-cli-core';
import { handleList, listCommand } from './list.js';
import { ExtensionManager } from '../../config/extension-manager.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { getErrorMessage } from '../../utils/errors.js';
// Mock dependencies
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{
stripAnsi: false,
},
debugLogger,
};
);
});
vi.mock('../../config/extension-manager.js');
@@ -71,7 +60,7 @@ describe('extensions list command', () => {
.mockResolvedValue([]);
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
'No extensions installed.',
);
@@ -85,7 +74,7 @@ describe('extensions list command', () => {
.mockResolvedValue([]);
await handleList({ outputFormat: 'json' });
expect(emitConsoleLog).toHaveBeenCalledWith('log', '[]');
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith('log', '[]');
mockCwd.mockRestore();
});
@@ -103,7 +92,7 @@ describe('extensions list command', () => {
);
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
'ext1@1.0.0\n\next2@2.0.0',
);
@@ -121,7 +110,7 @@ describe('extensions list command', () => {
.mockResolvedValue(extensions);
await handleList({ outputFormat: 'json' });
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
JSON.stringify(extensions, null, 2),
);
@@ -142,7 +131,7 @@ describe('extensions list command', () => {
await handleList();
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'error',
'List failed message',
);
@@ -5,7 +5,6 @@
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { handleDisable, disableCommand } from './disable.js';
import {
loadSettings,
@@ -14,12 +13,12 @@ import {
type LoadableSettingScope,
} from '../../config/settings.js';
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
}));
const { emitConsoleLog, debugLogger } = await vi.hoisted(async () => {
const { createMockDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return createMockDebugLogger({ stripAnsi: true });
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -5,7 +5,6 @@
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { handleEnable, enableCommand } from './enable.js';
import {
loadSettings,
@@ -13,12 +12,12 @@ import {
type LoadedSettings,
} from '../../config/settings.js';
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
}));
const { emitConsoleLog, debugLogger } = await vi.hoisted(async () => {
const { createMockDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return createMockDebugLogger({ stripAnsi: true });
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { vi, describe, it, expect, beforeEach } from 'vitest';
const mockInstallSkill = vi.hoisted(() => vi.fn());
const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());
@@ -19,11 +19,17 @@ vi.mock('../../config/extensions/consent.js', () => ({
skillsConsentString: mockSkillsConsentString,
}));
const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => {
const { createMockDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return createMockDebugLogger({ stripAnsi: true });
});
vi.mock('@google/gemini-cli-core', () => ({
debugLogger: { log: vi.fn(), error: vi.fn() },
debugLogger,
}));
import { debugLogger } from '@google/gemini-cli-core';
import { handleInstall, installCommand } from './install.js';
describe('skill install command', () => {
@@ -63,10 +69,12 @@ describe('skill install command', () => {
expect.any(Function),
expect.any(Function),
);
expect(debugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('Successfully installed skill: test-skill'),
);
expect(debugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('location: /mock/user/skills/test-skill'),
);
expect(mockRequestConsentNonInteractive).toHaveBeenCalledWith(
@@ -86,10 +94,11 @@ describe('skill install command', () => {
});
expect(mockRequestConsentNonInteractive).not.toHaveBeenCalled();
expect(debugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
'You have consented to the following:',
);
expect(debugLogger.log).toHaveBeenCalledWith('Mock Consent String');
expect(emitConsoleLog).toHaveBeenCalledWith('log', 'Mock Consent String');
expect(mockInstallSkill).toHaveBeenCalled();
});
@@ -106,7 +115,8 @@ describe('skill install command', () => {
source: 'https://example.com/repo.git',
});
expect(debugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Skill installation cancelled by user.',
);
expect(process.exit).toHaveBeenCalledWith(1);
@@ -137,7 +147,7 @@ describe('skill install command', () => {
await handleInstall({ source: '/local/path' });
expect(debugLogger.error).toHaveBeenCalledWith('Install failed');
expect(emitConsoleLog).toHaveBeenCalledWith('error', 'Install failed');
expect(process.exit).toHaveBeenCalledWith(1);
});
});
@@ -15,8 +15,15 @@ vi.mock('../../utils/skillUtils.js', () => ({
linkSkill: mockLinkSkill,
}));
const { debugLogger } = await vi.hoisted(async () => {
const { createMockDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return createMockDebugLogger({ stripAnsi: false });
});
vi.mock('@google/gemini-cli-core', () => ({
debugLogger: { log: vi.fn(), error: vi.fn() },
debugLogger,
}));
vi.mock('../../config/extensions/consent.js', () => ({
@@ -24,8 +31,6 @@ vi.mock('../../config/extensions/consent.js', () => ({
skillsConsentString: mockSkillsConsentString,
}));
import { debugLogger } from '@google/gemini-cli-core';
describe('skills link command', () => {
beforeEach(() => {
vi.clearAllMocks();
+20 -30
View File
@@ -5,33 +5,23 @@
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { format } from 'node:util';
import { coreEvents } from '@google/gemini-cli-core';
import { handleList, listCommand } from './list.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { loadCliConfig } from '../../config/config.js';
import type { Config } from '@google/gemini-cli-core';
import chalk from 'chalk';
const emitConsoleLog = vi.hoisted(() => vi.fn());
const debugLogger = vi.hoisted(() => ({
log: vi.fn((message, ...args) => {
emitConsoleLog('log', format(message, ...args));
}),
error: vi.fn((message, ...args) => {
emitConsoleLog('error', format(message, ...args));
}),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitConsoleLog,
const { mockCoreDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return mockCoreDebugLogger(
await importOriginal<typeof import('@google/gemini-cli-core')>(),
{
stripAnsi: false,
},
debugLogger,
};
);
});
vi.mock('../../config/settings.js');
@@ -67,7 +57,7 @@ describe('skills list command', () => {
await handleList({});
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
'No skills discovered.',
);
@@ -98,23 +88,23 @@ describe('skills list command', () => {
await handleList({});
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
chalk.bold('Discovered Agent Skills:'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('skill1'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining(chalk.green('[Enabled]')),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('skill2'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining(chalk.red('[Disabled]')),
);
@@ -146,11 +136,11 @@ describe('skills list command', () => {
// Default
await handleList({ all: false });
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('regular'),
);
expect(emitConsoleLog).not.toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).not.toHaveBeenCalledWith(
'log',
expect.stringContaining('builtin'),
);
@@ -159,15 +149,15 @@ describe('skills list command', () => {
// With all: true
await handleList({ all: true });
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('regular'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('builtin'),
);
expect(emitConsoleLog).toHaveBeenCalledWith(
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining(chalk.gray(' [Built-in]')),
);
@@ -12,11 +12,17 @@ vi.mock('../../utils/skillUtils.js', () => ({
uninstallSkill: mockUninstallSkill,
}));
const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => {
const { createMockDebugLogger } = await import(
'../../test-utils/mockDebugLogger.js'
);
return createMockDebugLogger({ stripAnsi: true });
});
vi.mock('@google/gemini-cli-core', () => ({
debugLogger: { log: vi.fn(), error: vi.fn() },
debugLogger,
}));
import { debugLogger } from '@google/gemini-cli-core';
import { handleUninstall, uninstallCommand } from './uninstall.js';
describe('skill uninstall command', () => {
@@ -45,10 +51,12 @@ describe('skill uninstall command', () => {
});
expect(mockUninstallSkill).toHaveBeenCalledWith('test-skill', 'user');
expect(debugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('Successfully uninstalled skill: test-skill'),
);
expect(debugLogger.log).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'log',
expect.stringContaining('location: /mock/user/skills/test-skill'),
);
});
@@ -71,7 +79,8 @@ describe('skill uninstall command', () => {
await handleUninstall({ name: 'test-skill' });
expect(debugLogger.error).toHaveBeenCalledWith(
expect(emitConsoleLog).toHaveBeenCalledWith(
'error',
'Skill "test-skill" is not installed in the user scope.',
);
});
@@ -81,7 +90,7 @@ describe('skill uninstall command', () => {
await handleUninstall({ name: 'test-skill' });
expect(debugLogger.error).toHaveBeenCalledWith('Uninstall failed');
expect(emitConsoleLog).toHaveBeenCalledWith('error', 'Uninstall failed');
expect(process.exit).toHaveBeenCalledWith(1);
});
});
+16
View File
@@ -667,6 +667,22 @@ describe('parseArguments', () => {
const argv = await parseArguments(settings);
expect(argv.isCommand).toBe(true);
});
it('should correctly parse the --forever flag and set default a2aPort to 0', async () => {
process.argv = ['node', 'script.js', '--forever'];
const settings = createTestMergedSettings({});
const argv = await parseArguments(settings);
expect(argv.forever).toBe(true);
expect(argv.a2aPort).toBe(0);
});
it('should not override explicit a2aPort when --forever is specified', async () => {
process.argv = ['node', 'script.js', '--forever', '--a2a-port', '8080'];
const settings = createTestMergedSettings({});
const argv = await parseArguments(settings);
expect(argv.forever).toBe(true);
expect(argv.a2aPort).toBe(8080);
});
});
describe('loadCliConfig', () => {
+112 -5
View File
@@ -5,8 +5,11 @@
*/
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import process from 'node:process';
import * as path from 'node:path';
import * as fsPromises from 'node:fs/promises';
import { mcpCommand } from '../commands/mcp.js';
import { extensionsCommand } from '../commands/extensions.js';
import { skillsCommand } from '../commands/skills.js';
@@ -43,6 +46,8 @@ import {
type HookDefinition,
type HookEventName,
type OutputFormat,
type SisyphusModeSettings,
GEMINI_DIR,
} from '@google/gemini-cli-core';
import {
type Settings,
@@ -72,6 +77,7 @@ export interface CliArgs {
query: string | undefined;
model: string | undefined;
sandbox: boolean | string | undefined;
forever: boolean | undefined;
debug: boolean | undefined;
prompt: string | undefined;
promptInteractive: string | undefined;
@@ -97,6 +103,7 @@ export interface CliArgs {
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
isCommand: boolean | undefined;
a2aPort: number | undefined;
}
export async function parseArguments(
@@ -147,7 +154,12 @@ export async function parseArguments(
type: 'boolean',
description: 'Run in sandbox?',
})
.option('forever', {
type: 'boolean',
description:
'Enable forever (long-running agent) mode. Uses GEMINI.md frontmatter for sisyphus engine config.',
default: false,
})
.option('yolo', {
alias: 'y',
type: 'boolean',
@@ -288,6 +300,12 @@ export async function parseArguments(
.option('accept-raw-output-risk', {
type: 'boolean',
description: 'Suppress the security warning when using --raw-output.',
})
.option('a2a-port', {
type: 'number',
nargs: 1,
description:
'Enable the embedded A2A HTTP listener on the specified port (0 for random). Implies --a2a enabled.',
}),
)
// Register MCP subcommands
@@ -389,8 +407,16 @@ export async function parseArguments(
(result as Record<string, unknown>)['query'] = q || undefined;
(result as Record<string, unknown>)['startupMessages'] = startupMessages;
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
// Enable A2A listener by default in Forever Mode
if (
result['forever'] &&
result['a2aPort'] === undefined &&
result['a2a-port'] === undefined
) {
(result as Record<string, unknown>)['a2aPort'] =
parseInt(process.env['A2A_PORT'] ?? '0', 10) || 3100;
}
// The import format is now only controlled by settings.memoryImportFormat // We no longer accept it as a CLI argument
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return result as unknown as CliArgs;
}
@@ -513,6 +539,66 @@ export async function loadCliConfig(
const experimentalJitContext = settings.experimental?.jitContext ?? false;
let sisyphusMode: SisyphusModeSettings | undefined;
let isForeverModeConfigured = false;
const isForeverMode = argv.forever ?? false;
if (isForeverMode) {
try {
const yaml = await import('js-yaml');
const fsPromises = await import('node:fs/promises');
const path = await import('node:path');
const { FRONTMATTER_REGEX } = await import('@google/gemini-cli-core');
const { GEMINI_DIR } = await import('@google/gemini-cli-core');
const { DEFAULT_CONTEXT_FILENAME } = await import(
'@google/gemini-cli-core'
);
const geminiMdPath = path.default.join(
cwd,
GEMINI_DIR,
DEFAULT_CONTEXT_FILENAME,
);
const mdContent = await fsPromises.default.readFile(
geminiMdPath,
'utf-8',
);
const match = mdContent.match(FRONTMATTER_REGEX);
if (match) {
const parsed = yaml.default.load(match[1]);
if (parsed && typeof parsed === 'object') {
isForeverModeConfigured = true;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const frontmatter = parsed as Record<string, unknown>;
if (frontmatter['sisyphus']) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const sisyphusSettings = frontmatter['sisyphus'] as Record<
string,
unknown
>;
sisyphusMode = {
enabled:
typeof sisyphusSettings['enabled'] === 'boolean'
? sisyphusSettings['enabled']
: false,
idleTimeout:
typeof sisyphusSettings['idleTimeout'] === 'number'
? sisyphusSettings['idleTimeout']
: undefined,
prompt:
typeof sisyphusSettings['prompt'] === 'string'
? sisyphusSettings['prompt']
: undefined,
};
}
}
}
} catch (_e) {
// Ignored
}
}
let memoryContent: string | HierarchicalMemory = '';
let fileCount = 0;
let filePaths: string[] = [];
@@ -537,8 +623,24 @@ export async function loadCliConfig(
filePaths = result.filePaths;
}
const question = argv.promptInteractive || argv.prompt || '';
let onboardingPrompt = '';
const onboardingPromptPath = path.join(cwd, GEMINI_DIR, '.onboarding_prompt');
try {
onboardingPrompt = await fsPromises.readFile(onboardingPromptPath, 'utf-8');
if (onboardingPrompt) {
await fsPromises.unlink(onboardingPromptPath).catch(() => {});
process.env['GEMINI_CLI_INITIAL_PROMPT'] = onboardingPrompt;
}
} catch (_e) {
// Ignored
}
const question =
argv.promptInteractive ||
argv.prompt ||
onboardingPrompt ||
process.env['GEMINI_CLI_INITIAL_PROMPT'] ||
'';
// Determine approval mode with backward compatibility
let approvalMode: ApprovalMode;
const rawApprovalMode =
@@ -630,7 +732,8 @@ export async function loadCliConfig(
!!argv.promptInteractive ||
!!argv.experimentalAcp ||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.isCommand);
!argv.isCommand) ||
!!argv.forever;
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools);
@@ -829,6 +932,9 @@ export async function loadCliConfig(
directWebFetch: settings.experimental?.directWebFetch,
planSettings: settings.general?.plan,
enableEventDrivenScheduler: true,
isForeverMode,
isForeverModeConfigured,
sisyphusMode,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
@@ -859,6 +965,7 @@ export async function loadCliConfig(
fakeResponses: argv.fakeResponses,
recordResponses: argv.recordResponses,
retryFetchErrors: settings.general?.retryFetchErrors,
maxAttempts: settings.general?.maxAttempts,
ptyInfo: ptyInfo?.name,
disableLLMCorrection: settings.tools?.disableLLMCorrection,
rawOutput: argv.rawOutput,
@@ -0,0 +1,188 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { ExtensionManager } from './extension-manager.js';
import { createTestMergedSettings } from './settings.js';
import { createExtension } from '../test-utils/createExtension.js';
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
vi.mock('os', async (importOriginal) => {
const mockedOs = await importOriginal<typeof os>();
return {
...mockedOs,
homedir: mockHomedir,
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: mockHomedir,
};
});
describe('ExtensionManager', () => {
let tempHomeDir: string;
let tempWorkspaceDir: string;
let userExtensionsDir: string;
let extensionManager: ExtensionManager;
beforeEach(() => {
vi.clearAllMocks();
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
tempWorkspaceDir = fs.mkdtempSync(
path.join(tempHomeDir, 'gemini-cli-test-workspace-'),
);
mockHomedir.mockReturnValue(tempHomeDir);
userExtensionsDir = path.join(tempHomeDir, EXTENSIONS_DIRECTORY_NAME);
fs.mkdirSync(userExtensionsDir, { recursive: true });
extensionManager = new ExtensionManager({
settings: createTestMergedSettings(),
workspaceDir: tempWorkspaceDir,
requestConsent: vi.fn().mockResolvedValue(true),
requestSetting: null,
});
});
afterEach(() => {
try {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
} catch (_e) {
// Ignore
}
});
describe('loadExtensions parallel loading', () => {
it('should prevent concurrent loading and return the same promise', async () => {
createExtension({
extensionsDir: userExtensionsDir,
name: 'ext1',
version: '1.0.0',
});
createExtension({
extensionsDir: userExtensionsDir,
name: 'ext2',
version: '1.0.0',
});
// Call loadExtensions twice concurrently
const promise1 = extensionManager.loadExtensions();
const promise2 = extensionManager.loadExtensions();
// They should resolve to the exact same array
const [extensions1, extensions2] = await Promise.all([
promise1,
promise2,
]);
expect(extensions1).toBe(extensions2);
expect(extensions1).toHaveLength(2);
const names = extensions1.map((ext) => ext.name).sort();
expect(names).toEqual(['ext1', 'ext2']);
});
it('should throw an error if loadExtensions is called after it has already resolved', async () => {
createExtension({
extensionsDir: userExtensionsDir,
name: 'ext1',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await expect(extensionManager.loadExtensions()).rejects.toThrow(
'Extensions already loaded, only load extensions once.',
);
});
it('should not throw if extension directory does not exist', async () => {
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
const extensions = await extensionManager.loadExtensions();
expect(extensions).toEqual([]);
});
it('should throw if there are duplicate extension names', async () => {
// We manually create two extensions with different dirs but same name in config
const ext1Dir = path.join(userExtensionsDir, 'ext1-dir');
const ext2Dir = path.join(userExtensionsDir, 'ext2-dir');
fs.mkdirSync(ext1Dir, { recursive: true });
fs.mkdirSync(ext2Dir, { recursive: true });
const config = JSON.stringify({
name: 'duplicate-ext',
version: '1.0.0',
});
fs.writeFileSync(path.join(ext1Dir, 'gemini-extension.json'), config);
fs.writeFileSync(
path.join(ext1Dir, 'metadata.json'),
JSON.stringify({ type: 'local', source: ext1Dir }),
);
fs.writeFileSync(path.join(ext2Dir, 'gemini-extension.json'), config);
fs.writeFileSync(
path.join(ext2Dir, 'metadata.json'),
JSON.stringify({ type: 'local', source: ext2Dir }),
);
await expect(extensionManager.loadExtensions()).rejects.toThrow(
'Extension with name duplicate-ext already was loaded.',
);
});
it('should wait for loadExtensions to finish when loadExtension is called concurrently', async () => {
// Create an initial extension that loadExtensions will find
createExtension({
extensionsDir: userExtensionsDir,
name: 'ext1',
version: '1.0.0',
});
// Start the parallel load (it will read ext1)
const loadAllPromise = extensionManager.loadExtensions();
// Create a second extension dynamically in a DIFFERENT directory
// so that loadExtensions (which scans userExtensionsDir) doesn't find it.
const externalDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'external-ext-'),
);
fs.writeFileSync(
path.join(externalDir, 'gemini-extension.json'),
JSON.stringify({ name: 'ext2', version: '1.0.0' }),
);
fs.writeFileSync(
path.join(externalDir, 'metadata.json'),
JSON.stringify({ type: 'local', source: externalDir }),
);
// Concurrently call loadExtension (simulating an install or update)
const loadSinglePromise = extensionManager.loadExtension(externalDir);
// Wait for both to complete
await Promise.all([loadAllPromise, loadSinglePromise]);
// Both extensions should now be present in the loadedExtensions array
const extensions = extensionManager.getExtensions();
expect(extensions).toHaveLength(2);
const names = extensions.map((ext) => ext.name).sort();
expect(names).toEqual(['ext1', 'ext2']);
fs.rmSync(externalDir, { recursive: true, force: true });
});
});
});
+89 -27
View File
@@ -102,6 +102,7 @@ export class ExtensionManager extends ExtensionLoader {
private telemetryConfig: Config;
private workspaceDir: string;
private loadedExtensions: GeminiCLIExtension[] | undefined;
private loadingPromise: Promise<GeminiCLIExtension[]> | null = null;
constructor(options: ExtensionManagerParams) {
super(options.eventEmitter);
@@ -519,31 +520,103 @@ Would you like to attempt to install via "git clone" instead?`,
throw new Error('Extensions already loaded, only load extensions once.');
}
if (this.settings.admin.extensions.enabled === false) {
this.loadedExtensions = [];
return this.loadedExtensions;
if (this.loadingPromise) {
return this.loadingPromise;
}
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
this.loadedExtensions = [];
if (!fs.existsSync(extensionsDir)) {
return this.loadedExtensions;
}
for (const subdir of fs.readdirSync(extensionsDir)) {
const extensionDir = path.join(extensionsDir, subdir);
await this.loadExtension(extensionDir);
}
return this.loadedExtensions;
this.loadingPromise = (async () => {
try {
if (this.settings.admin.extensions.enabled === false) {
this.loadedExtensions = [];
return this.loadedExtensions;
}
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
if (!fs.existsSync(extensionsDir)) {
this.loadedExtensions = [];
return this.loadedExtensions;
}
const subdirs = await fs.promises.readdir(extensionsDir);
const extensionPromises = subdirs.map((subdir) => {
const extensionDir = path.join(extensionsDir, subdir);
return this._buildExtension(extensionDir);
});
const builtExtensionsOrNull = await Promise.all(extensionPromises);
const builtExtensions = builtExtensionsOrNull.filter(
(ext): ext is GeminiCLIExtension => ext !== null,
);
const seenNames = new Set<string>();
for (const ext of builtExtensions) {
if (seenNames.has(ext.name)) {
throw new Error(
`Extension with name ${ext.name} already was loaded.`,
);
}
seenNames.add(ext.name);
}
this.loadedExtensions = builtExtensions;
await Promise.all(
this.loadedExtensions.map((ext) => this.maybeStartExtension(ext)),
);
return this.loadedExtensions;
} finally {
this.loadingPromise = null;
}
})();
return this.loadingPromise;
}
/**
* Adds `extension` to the list of extensions and starts it if appropriate.
*
* @internal visible for testing only
*/
private async loadExtension(
async loadExtension(
extensionDir: string,
): Promise<GeminiCLIExtension | null> {
if (this.loadingPromise) {
await this.loadingPromise;
}
this.loadedExtensions ??= [];
if (!fs.statSync(extensionDir).isDirectory()) {
const extension = await this._buildExtension(extensionDir);
if (!extension) {
return null;
}
if (
this.getExtensions().find(
(installed) => installed.name === extension.name,
)
) {
throw new Error(
`Extension with name ${extension.name} already was loaded.`,
);
}
this.loadedExtensions = [...this.loadedExtensions, extension];
await this.maybeStartExtension(extension);
return extension;
}
/**
* Builds an extension without side effects (does not mutate loadedExtensions or start it).
*/
private async _buildExtension(
extensionDir: string,
): Promise<GeminiCLIExtension | null> {
try {
const stats = await fs.promises.stat(extensionDir);
if (!stats.isDirectory()) {
return null;
}
} catch {
return null;
}
@@ -592,13 +665,6 @@ Would you like to attempt to install via "git clone" instead?`,
try {
let config = await this.loadExtensionConfig(effectiveExtensionPath);
if (
this.getExtensions().find((extension) => extension.name === config.name)
) {
throw new Error(
`Extension with name ${config.name} already was loaded.`,
);
}
const extensionId = getExtensionId(config, installMetadata);
@@ -768,7 +834,7 @@ Would you like to attempt to install via "git clone" instead?`,
);
}
const extension: GeminiCLIExtension = {
return {
name: config.name,
version: config.version,
path: effectiveExtensionPath,
@@ -788,10 +854,6 @@ Would you like to attempt to install via "git clone" instead?`,
agents: agentLoadResult.agents,
themes: config.themes,
};
this.loadedExtensions = [...this.loadedExtensions, extension];
await this.maybeStartExtension(extension);
return extension;
} catch (e) {
debugLogger.error(
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="207" viewBox="0 0 920 207">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="207" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing extension &quot;test-ext&quot;. </text>
<text x="0" y="19" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will run the following MCP servers: </text>
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * server1 (local): npm start </text>
<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="#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="138" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">from a public repository. Google does not vet, endorse, or guarantee the functionality or security</text>
<text x="0" y="155" fill="#cdcd00" textLength="846" lengthAdjust="spacingAndGlyphs">of extensions. Please carefully inspect any extension and its source code before installing to</text>
<text x="0" y="172" fill="#cdcd00" textLength="630" lengthAdjust="spacingAndGlyphs">understand the permissions it requires and the actions it may perform.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="139" viewBox="0 0 920 139">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="139" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing extension &quot;test-ext&quot;. </text>
<text x="0" y="19" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">⚠️ This extension contains Hooks which can automatically execute commands. </text>
<text x="0" y="53" 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="70" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">from a public repository. Google does not vet, endorse, or guarantee the functionality or security</text>
<text x="0" y="87" fill="#cdcd00" textLength="846" lengthAdjust="spacingAndGlyphs">of extensions. Please carefully inspect any extension and its source code before installing to</text>
<text x="0" y="104" fill="#cdcd00" textLength="630" lengthAdjust="spacingAndGlyphs">understand the permissions it requires and the actions it may perform.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="479" viewBox="0 0 920 479">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="479" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing extension &quot;test-ext&quot;. </text>
<text x="0" y="19" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">This extension will run the following MCP servers: </text>
<text x="0" y="36" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs"> * server1 (local): npm start </text>
<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="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="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="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>
<text x="0" y="342" fill="#cdcd00" textLength="846" lengthAdjust="spacingAndGlyphs">of extensions. Please carefully inspect any extension and its source code before installing to</text>
<text x="0" y="359" fill="#cdcd00" textLength="630" lengthAdjust="spacingAndGlyphs">understand the permissions it requires and the actions it may perform.</text>
<text x="0" y="393" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">Agent skills inject specialized instructions and domain-specific knowledge into the agent&apos;s system</text>
<text x="0" y="410" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">prompt. This can change how the agent interprets your requests and interacts with your environment.</text>
<text x="0" y="427" fill="#cdcd00" textLength="864" lengthAdjust="spacingAndGlyphs">Review the skill definitions at the location(s) provided below to ensure they meet your security</text>
<text x="0" y="444" fill="#cdcd00" textLength="90" lengthAdjust="spacingAndGlyphs">standards.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="343" viewBox="0 0 920 343">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<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 &quot;test-ext&quot;. </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="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>
<text x="0" y="189" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">from a public repository. Google does not vet, endorse, or guarantee the functionality or security</text>
<text x="0" y="206" fill="#cdcd00" textLength="846" lengthAdjust="spacingAndGlyphs">of extensions. Please carefully inspect any extension and its source code before installing to</text>
<text x="0" y="223" fill="#cdcd00" textLength="630" lengthAdjust="spacingAndGlyphs">understand the permissions it requires and the actions it may perform.</text>
<text x="0" y="257" fill="#cdcd00" textLength="882" lengthAdjust="spacingAndGlyphs">Agent skills inject specialized instructions and domain-specific knowledge into the agent&apos;s system</text>
<text x="0" y="274" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">prompt. This can change how the agent interprets your requests and interacts with your environment.</text>
<text x="0" y="291" fill="#cdcd00" textLength="864" lengthAdjust="spacingAndGlyphs">Review the skill definitions at the location(s) provided below to ensure they meet your security</text>
<text x="0" y="308" fill="#cdcd00" textLength="90" lengthAdjust="spacingAndGlyphs">standards.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="241" viewBox="0 0 920 241">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="241" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">Installing agent skill(s) from &quot;https://example.com/repo.git&quot;. </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="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&apos;s system</text>
<text x="0" y="172" fill="#cdcd00" textLength="891" lengthAdjust="spacingAndGlyphs">prompt. This can change how the agent interprets your requests and interacts with your environment.</text>
<text x="0" y="189" fill="#cdcd00" textLength="864" lengthAdjust="spacingAndGlyphs">Review the skill definitions at the location(s) provided below to ensure they meet your security</text>
<text x="0" y="206" fill="#cdcd00" textLength="90" lengthAdjust="spacingAndGlyphs">standards.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,93 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`consent > maybeRequestConsentOrFail > consent string generation > should generate a consent string with all fields 1`] = `
"Installing extension "test-ext".
This extension will run the following MCP servers:
* server1 (local): npm start
* server2 (remote): https://remote.com
This extension will append info to your gemini.md context using my-context.md
This extension will exclude the following core tools: tool1,tool2
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform."
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should include warning when hooks are present 1`] = `
"Installing extension "test-ext".
⚠️ This extension contains Hooks which can automatically execute commands.
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform."
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if skills change 1`] = `
"Installing extension "test-ext".
This extension will run the following MCP servers:
* server1 (local): npm start
* server2 (remote): https://remote.com
This extension will append info to your gemini.md context using my-context.md
This extension will exclude the following core tools: tool1,tool2
Agent Skills:
This extension will install the following agent skills:
* skill1: desc1
(Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory)
* skill2: desc2
(Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory)
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform.
Agent skills inject specialized instructions and domain-specific knowledge into the agent's system
prompt. This can change how the agent interprets your requests and interacts with your environment.
Review the skill definitions at the location(s) provided below to ensure they meet your security
standards."
`;
exports[`consent > maybeRequestConsentOrFail > consent string generation > should show a warning if the skill directory cannot be read 1`] = `
"Installing extension "test-ext".
Agent Skills:
This extension will install the following agent skills:
* locked-skill: A skill in a locked dir
(Source: /mock/temp/dir/locked/SKILL.md) ⚠️ (Could not count items in directory)
The extension you are about to install may have been created by a third-party developer and sourced
from a public repository. Google does not vet, endorse, or guarantee the functionality or security
of extensions. Please carefully inspect any extension and its source code before installing to
understand the permissions it requires and the actions it may perform.
Agent skills inject specialized instructions and domain-specific knowledge into the agent's system
prompt. This can change how the agent interprets your requests and interacts with your environment.
Review the skill definitions at the location(s) provided below to ensure they meet your security
standards."
`;
exports[`consent > skillsConsentString > should generate a consent string for skills 1`] = `
"Installing agent skill(s) from "https://example.com/repo.git".
The following agent skill(s) will be installing:
* skill1: desc1
(Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory)
Install Destination: /mock/target/dir
Agent skills inject specialized instructions and domain-specific knowledge into the agent's system
prompt. This can change how the agent interprets your requests and interacts with your environment.
Review the skill definitions at the location(s) provided below to ensure they meet your security
standards."
`;
@@ -4,17 +4,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { Text } from 'ink';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import chalk from 'chalk';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { render, cleanup } from '../../test-utils/render.js';
import {
requestConsentNonInteractive,
requestConsentInteractive,
maybeRequestConsentOrFail,
INSTALL_WARNING_MESSAGE,
SKILLS_WARNING_MESSAGE,
} from './consent.js';
import type { ConfirmationRequest } from '../../ui/types.js';
import type { ExtensionConfig } from '../extension.js';
@@ -58,6 +58,21 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
};
});
async function expectConsentSnapshot(consentString: string) {
const renderResult = render(React.createElement(Text, null, consentString));
await renderResult.waitUntilReady();
await expect(renderResult).toMatchSvgSnapshot();
}
/**
* Normalizes a consent string for snapshot testing by:
* 1. Replacing the dynamic temp directory path with a static placeholder.
* 2. Converting Windows backslashes to forward slashes for platform-agnosticism.
*/
function normalizePathsForSnapshot(str: string, tempDir: string): string {
return str.replaceAll(tempDir, '/mock/temp/dir').replaceAll('\\', '/');
}
describe('consent', () => {
let tempDir: string;
@@ -75,6 +90,7 @@ describe('consent', () => {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
cleanup();
});
describe('requestConsentNonInteractive', () => {
@@ -189,18 +205,9 @@ describe('consent', () => {
undefined,
);
const expectedConsentString = [
'Installing extension "test-ext".',
'This extension will run the following MCP servers:',
' * server1 (local): npm start',
' * server2 (remote): https://remote.com',
'This extension will append info to your gemini.md context using my-context.md',
'This extension will exclude the following core tools: tool1,tool2',
'',
INSTALL_WARNING_MESSAGE,
].join('\n');
expect(requestConsent).toHaveBeenCalledWith(expectedConsentString);
expect(requestConsent).toHaveBeenCalledTimes(1);
const consentString = requestConsent.mock.calls[0][0] as string;
await expectConsentSnapshot(consentString);
});
it('should request consent if mcpServers change', async () => {
@@ -263,11 +270,9 @@ describe('consent', () => {
undefined,
);
expect(requestConsent).toHaveBeenCalledWith(
expect.stringContaining(
'⚠️ This extension contains Hooks which can automatically execute commands.',
),
);
expect(requestConsent).toHaveBeenCalledTimes(1);
const consentString = requestConsent.mock.calls[0][0] as string;
await expectConsentSnapshot(consentString);
});
it('should request consent if hooks status changes', async () => {
@@ -323,29 +328,10 @@ describe('consent', () => {
[skill1, skill2],
);
const expectedConsentString = [
'Installing extension "test-ext".',
'This extension will run the following MCP servers:',
' * server1 (local): npm start',
' * server2 (remote): https://remote.com',
'This extension will append info to your gemini.md context using my-context.md',
'This extension will exclude the following core tools: tool1,tool2',
'',
chalk.bold('Agent Skills:'),
'\nThis extension will install the following agent skills:\n',
` * ${chalk.bold('skill1')}: desc1`,
chalk.dim(` (Source: ${skill1.location}) (2 items in directory)`),
'',
` * ${chalk.bold('skill2')}: desc2`,
chalk.dim(` (Source: ${skill2.location}) (1 items in directory)`),
'',
'',
INSTALL_WARNING_MESSAGE,
'',
SKILLS_WARNING_MESSAGE,
].join('\n');
expect(requestConsent).toHaveBeenCalledWith(expectedConsentString);
expect(requestConsent).toHaveBeenCalledTimes(1);
let consentString = requestConsent.mock.calls[0][0] as string;
consentString = normalizePathsForSnapshot(consentString, tempDir);
await expectConsentSnapshot(consentString);
});
it('should show a warning if the skill directory cannot be read', async () => {
@@ -377,11 +363,10 @@ describe('consent', () => {
[skill],
);
expect(requestConsent).toHaveBeenCalledWith(
expect.stringContaining(
` (Source: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`,
),
);
expect(requestConsent).toHaveBeenCalledTimes(1);
let consentString = requestConsent.mock.calls[0][0] as string;
consentString = normalizePathsForSnapshot(consentString, tempDir);
await expectConsentSnapshot(consentString);
});
});
});
@@ -400,21 +385,14 @@ describe('consent', () => {
};
const { skillsConsentString } = await import('./consent.js');
const consentString = await skillsConsentString(
let consentString = await skillsConsentString(
[skill1],
'https://example.com/repo.git',
'/mock/target/dir',
);
expect(consentString).toContain(
'Installing agent skill(s) from "https://example.com/repo.git".',
);
expect(consentString).toContain('Install Destination: /mock/target/dir');
expect(consentString).toContain('\n' + SKILLS_WARNING_MESSAGE);
expect(consentString).toContain(` * ${chalk.bold('skill1')}: desc1`);
expect(consentString).toContain(
chalk.dim(`(Source: ${skill1.location}) (1 items in directory)`),
);
consentString = normalizePathsForSnapshot(consentString, tempDir);
await expectConsentSnapshot(consentString);
});
});
});
+1 -1
View File
@@ -844,7 +844,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: false,
default: undefined as string | undefined,
description: 'The Gemini model to use for conversations.',
showInDialog: false,
showInDialog: true,
},
maxSessionTurns: {
type: 'number',
+426
View File
@@ -0,0 +1,426 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import http from 'node:http';
import { writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
import { appEvents, AppEvent } from './utils/events.js';
// --- A2A Task management ---
interface A2AResponseMessage {
kind: 'message';
role: 'agent';
parts: Array<{ kind: 'text'; text: string }>;
messageId: string;
}
interface A2ATask {
id: string;
contextId: string;
status: {
state: 'submitted' | 'working' | 'completed' | 'failed';
timestamp: string;
message?: A2AResponseMessage;
};
}
const tasks = new Map<string, A2ATask>();
const TASK_CLEANUP_DELAY_MS = 10 * 60 * 1000; // 10 minutes
const DEFAULT_BLOCKING_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes (forever mode tasks can run long)
interface ResponseWaiter {
taskId: string;
resolve: (text: string) => void;
}
const responseWaiters: ResponseWaiter[] = [];
/**
* Called by AppContainer when streaming transitions from non-Idle to Idle.
* Resolves the oldest blocking waiter (FIFO) and completes its task.
*/
export function notifyResponse(responseText: string): void {
const waiter = responseWaiters.shift();
if (!waiter) return;
const task = tasks.get(waiter.taskId);
if (task) {
task.status = {
state: 'completed',
timestamp: new Date().toISOString(),
message: {
kind: 'message',
role: 'agent',
parts: [{ kind: 'text', text: responseText }],
messageId: crypto.randomUUID(),
},
};
scheduleTaskCleanup(task.id);
}
waiter.resolve(responseText);
}
/**
* Returns true if there are any in-flight tasks waiting for a response.
*/
export function hasPendingTasks(): boolean {
return responseWaiters.length > 0;
}
/**
* Called when streaming starts (Idle -> non-Idle) to mark the oldest
* submitted task as "working".
*/
export function markTasksWorking(): void {
const waiter = responseWaiters[0];
if (!waiter) return;
const task = tasks.get(waiter.taskId);
if (task && task.status.state === 'submitted') {
task.status = {
state: 'working',
timestamp: new Date().toISOString(),
};
}
}
function scheduleTaskCleanup(taskId: string): void {
setTimeout(() => {
tasks.delete(taskId);
}, TASK_CLEANUP_DELAY_MS);
}
function createTask(): A2ATask {
const task: A2ATask = {
id: crypto.randomUUID(),
contextId: `session-${process.pid}`,
status: {
state: 'submitted',
timestamp: new Date().toISOString(),
},
};
tasks.set(task.id, task);
return task;
}
function formatTaskResult(task: A2ATask): object {
return {
kind: 'task',
id: task.id,
contextId: task.contextId,
status: task.status,
};
}
// --- JSON-RPC helpers ---
interface JsonRpcRequest {
jsonrpc?: string;
id?: string | number | null;
method?: string;
params?: Record<string, unknown>;
}
function jsonRpcSuccess(id: string | number | null, result: object): object {
return { jsonrpc: '2.0', id, result };
}
function jsonRpcError(
id: string | number | null,
code: number,
message: string,
): object {
return { jsonrpc: '2.0', id, error: { code, message } };
}
// --- HTTP utilities ---
function getSessionsDir(): string {
return join(os.homedir(), '.gemini', 'sessions');
}
function getPortFilePath(): string {
return join(getSessionsDir(), `interactive-${process.pid}.port`);
}
function buildAgentCard(port: number): object {
return {
name: 'Gemini CLI Interactive Session',
url: `http://localhost:${port}/`,
protocolVersion: '0.3.0',
provider: { organization: 'Google', url: 'https://google.com' },
capabilities: { streaming: false, pushNotifications: false },
defaultInputModes: ['text'],
defaultOutputModes: ['text'],
skills: [
{
id: 'interactive_session',
name: 'Interactive Session',
description: 'Send messages to the live interactive Gemini CLI session',
},
],
};
}
interface A2AMessagePart {
kind?: string;
text?: string;
}
function extractTextFromParts(
parts: A2AMessagePart[] | undefined,
): string | null {
if (!Array.isArray(parts)) {
return null;
}
const texts: string[] = [];
for (const part of parts) {
if (part.kind === 'text' && typeof part.text === 'string') {
texts.push(part.text);
}
}
return texts.length > 0 ? texts.join('\n') : null;
}
function sendJson(
res: http.ServerResponse,
statusCode: number,
data: object,
): void {
const body = JSON.stringify(data);
res.writeHead(statusCode, {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
});
res.end(body);
}
function readBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let size = 0;
const maxSize = 1024 * 1024; // 1MB limit
req.on('data', (chunk: Buffer) => {
size += chunk.length;
if (size > maxSize) {
req.destroy();
reject(new Error('Request body too large'));
return;
}
chunks.push(chunk);
});
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
req.on('error', reject);
});
}
// --- JSON-RPC request handlers ---
function handleMessageSend(
rpcId: string | number | null,
params: Record<string, unknown>,
res: http.ServerResponse,
): void {
const messageVal = params['message'];
const message =
messageVal && typeof messageVal === 'object'
? (messageVal as { role?: string; parts?: A2AMessagePart[] })
: undefined;
const text = extractTextFromParts(message?.parts);
if (!text) {
sendJson(
res,
200,
jsonRpcError(
rpcId,
-32602,
'Missing or empty text. Expected: params.message.parts with kind "text".',
),
);
return;
}
const task = createTask();
// Inject message into the session
appEvents.emit(AppEvent.ExternalMessage, text);
// Block until response (standard A2A message/send semantics)
const timer = setTimeout(() => {
const idx = responseWaiters.findIndex((w) => w.taskId === task.id);
if (idx !== -1) {
responseWaiters.splice(idx, 1);
}
task.status = {
state: 'failed',
timestamp: new Date().toISOString(),
};
scheduleTaskCleanup(task.id);
sendJson(res, 200, jsonRpcError(rpcId, -32000, 'Request timed out'));
}, DEFAULT_BLOCKING_TIMEOUT_MS);
responseWaiters.push({
taskId: task.id,
resolve: () => {
clearTimeout(timer);
// Task is already updated in notifyResponse
const updatedTask = tasks.get(task.id);
sendJson(
res,
200,
jsonRpcSuccess(rpcId, formatTaskResult(updatedTask ?? task)),
);
},
});
}
function handleTasksGet(
rpcId: string | number | null,
params: Record<string, unknown>,
res: http.ServerResponse,
): void {
const taskId = params['id'];
if (typeof taskId !== 'string') {
sendJson(
res,
200,
jsonRpcError(rpcId, -32602, 'Missing or invalid params.id'),
);
return;
}
const task = tasks.get(taskId);
if (!task) {
sendJson(res, 200, jsonRpcError(rpcId, -32001, 'Task not found'));
return;
}
sendJson(res, 200, jsonRpcSuccess(rpcId, formatTaskResult(task)));
}
// --- Server ---
export interface ExternalListenerResult {
port: number;
cleanup: () => void;
}
/**
* Start an embedded HTTP server that accepts A2A-format JSON-RPC messages
* and bridges them into the interactive session's message queue.
*/
export function startExternalListener(options?: {
port?: number;
}): Promise<ExternalListenerResult> {
const port = options?.port ?? 0;
return new Promise((resolve, reject) => {
const server = http.createServer(
(req: http.IncomingMessage, res: http.ServerResponse) => {
const url = new URL(req.url ?? '/', `http://localhost`);
// GET /.well-known/agent-card.json
if (
req.method === 'GET' &&
url.pathname === '/.well-known/agent-card.json'
) {
const address = server.address();
const actualPort =
typeof address === 'object' && address ? address.port : port;
sendJson(res, 200, buildAgentCard(actualPort));
return;
}
// POST / — JSON-RPC 2.0 routing
if (req.method === 'POST' && url.pathname === '/') {
readBody(req)
.then((rawBody) => {
let parsed: JsonRpcRequest;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
parsed = JSON.parse(rawBody) as JsonRpcRequest;
} catch {
sendJson(
res,
200,
jsonRpcError(null, -32700, 'Parse error: invalid JSON'),
);
return;
}
const rpcId = parsed.id ?? null;
const method = parsed.method;
const params = parsed.params ?? {};
switch (method) {
case 'message/send':
handleMessageSend(rpcId, params, res);
break;
case 'tasks/get':
handleTasksGet(rpcId, params, res);
break;
default:
sendJson(
res,
200,
jsonRpcError(
rpcId,
-32601,
`Method not found: ${method ?? '(none)'}`,
),
);
}
})
.catch(() => {
sendJson(
res,
200,
jsonRpcError(null, -32603, 'Failed to read request body'),
);
});
return;
}
// 404 for everything else
sendJson(res, 404, { error: 'Not found' });
},
);
server.listen(port, '0.0.0.0', () => {
const address = server.address();
const actualPort =
typeof address === 'object' && address ? address.port : port;
// Write port file
try {
const sessionsDir = getSessionsDir();
mkdirSync(sessionsDir, { recursive: true });
writeFileSync(getPortFilePath(), String(actualPort), 'utf-8');
} catch {
// Non-fatal: port file is a convenience, not a requirement
}
const cleanup = () => {
server.close();
try {
unlinkSync(getPortFilePath());
} catch {
// Ignore: file may already be deleted
}
};
resolve({ port: actualPort, cleanup });
});
server.on('error', (err) => {
reject(err);
});
});
}
+2
View File
@@ -479,6 +479,7 @@ describe('gemini.tsx main function kitty protocol', () => {
promptInteractive: undefined,
query: undefined,
yolo: undefined,
forever: undefined,
approvalMode: undefined,
policy: undefined,
allowedMcpServerNames: undefined,
@@ -498,6 +499,7 @@ describe('gemini.tsx main function kitty protocol', () => {
rawOutput: undefined,
acceptRawOutputRisk: undefined,
isCommand: undefined,
a2aPort: undefined,
});
await act(async () => {
+20
View File
@@ -82,6 +82,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from './utils/events.js';
import { startExternalListener } from './external-listener.js';
import { SessionSelector } from './utils/sessionUtils.js';
import { SettingsContext } from './ui/contexts/SettingsContext.js';
import { MouseProvider } from './ui/contexts/MouseContext.js';
@@ -188,6 +189,7 @@ export async function startInteractiveUI(
workspaceRoot: string = process.cwd(),
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
a2aPort?: number,
) {
// Never enter Ink alternate buffer mode when screen reader mode is enabled
// as there is no benefit of alternate buffer mode when using a screen reader
@@ -319,6 +321,23 @@ export async function startInteractiveUI(
});
registerCleanup(() => instance.unmount());
// Start embedded A2A HTTP listener if enabled via --a2a-port
if (a2aPort !== undefined) {
try {
const listener = await startExternalListener({ port: a2aPort });
registerCleanup(listener.cleanup);
coreEvents.emitFeedback(
'info',
`A2A endpoint listening on port ${listener.port}`,
);
} catch (err) {
coreEvents.emitFeedback(
'warning',
`Failed to start A2A listener: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
}
export async function main() {
@@ -722,6 +741,7 @@ export async function main() {
process.cwd(),
resumedSessionData,
initializationResult,
argv.a2aPort,
);
return;
}
+77 -12
View File
@@ -6,20 +6,78 @@
/// <reference types="vitest/globals" />
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Assertion } from 'vitest';
import { expect } from 'vitest';
import { expect, type Assertion } from 'vitest';
import path from 'node:path';
import stripAnsi from 'strip-ansi';
import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
// RegExp to detect invalid characters: backspace, and ANSI escape codes
// eslint-disable-next-line no-control-regex
const invalidCharsRegex = /[\b\x1b]/;
const callCountByTest = new Map<string, number>();
export async function toMatchSvgSnapshot(
this: Assertion,
renderInstance: {
lastFrameRaw?: (options?: { allowEmpty?: boolean }) => string;
lastFrame?: (options?: { allowEmpty?: boolean }) => string;
generateSvg: () => string;
},
options?: { allowEmpty?: boolean; name?: string },
) {
const currentTestName = expect.getState().currentTestName;
if (!currentTestName) {
throw new Error('toMatchSvgSnapshot must be called within a test');
}
const testPath = expect.getState().testPath;
if (!testPath) {
throw new Error('toMatchSvgSnapshot requires testPath');
}
let textContent: string;
if (renderInstance.lastFrameRaw) {
textContent = renderInstance.lastFrameRaw({
allowEmpty: options?.allowEmpty,
});
} else if (renderInstance.lastFrame) {
textContent = renderInstance.lastFrame({ allowEmpty: options?.allowEmpty });
} else {
throw new Error(
'toMatchSvgSnapshot requires a renderInstance with either lastFrameRaw or lastFrame',
);
}
const svgContent = renderInstance.generateSvg();
const sanitize = (name: string) =>
name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-');
const testId = testPath + ':' + currentTestName;
let count = callCountByTest.get(testId) ?? 0;
count++;
callCountByTest.set(testId, count);
const snapshotName =
options?.name ??
(count > 1 ? `${currentTestName}-${count}` : currentTestName);
const svgFileName =
sanitize(path.basename(testPath).replace(/\.test\.tsx?$/, '')) +
'-' +
sanitize(snapshotName) +
'.snap.svg';
const svgDir = path.join(path.dirname(testPath), '__snapshots__');
const svgFilePath = path.join(svgDir, svgFileName);
// Assert the text matches standard snapshot, stripping ANSI for stability
expect(stripAnsi(textContent)).toMatchSnapshot();
// Assert the SVG matches the file snapshot
await expect(svgContent).toMatchFileSnapshot(svgFilePath);
return { pass: true, message: () => '' };
}
function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
const { isNot } = this as any;
@@ -53,15 +111,22 @@ function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
expect.extend({
toHaveOnlyValidCharacters,
toMatchSvgSnapshot,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
// Extend Vitest's `expect` interface with the custom matcher's type definition.
declare module 'vitest' {
interface Assertion<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type
interface Assertion<T = any> extends CustomMatchers<T> {}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface AsymmetricMatchersContaining extends CustomMatchers {}
interface CustomMatchers<T = unknown> {
toHaveOnlyValidCharacters(): T;
}
interface AsymmetricMatchersContaining {
toHaveOnlyValidCharacters(): void;
toMatchSvgSnapshot(options?: {
allowEmpty?: boolean;
name?: string;
}): Promise<void>;
}
}
@@ -0,0 +1,76 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
import stripAnsi from 'strip-ansi';
import { format } from 'node:util';
export function createMockDebugLogger(options: { stripAnsi?: boolean } = {}) {
const emitConsoleLog = vi.fn();
const debugLogger = {
log: vi.fn((message: unknown, ...args: unknown[]) => {
let formatted =
typeof message === 'string' ? format(message, ...args) : message;
if (options.stripAnsi && typeof formatted === 'string') {
formatted = stripAnsi(formatted);
}
emitConsoleLog('log', formatted);
}),
error: vi.fn((message: unknown, ...args: unknown[]) => {
let formatted =
typeof message === 'string' ? format(message, ...args) : message;
if (options.stripAnsi && typeof formatted === 'string') {
formatted = stripAnsi(formatted);
}
emitConsoleLog('error', formatted);
}),
warn: vi.fn((message: unknown, ...args: unknown[]) => {
let formatted =
typeof message === 'string' ? format(message, ...args) : message;
if (options.stripAnsi && typeof formatted === 'string') {
formatted = stripAnsi(formatted);
}
emitConsoleLog('warn', formatted);
}),
debug: vi.fn(),
info: vi.fn(),
};
return { emitConsoleLog, debugLogger };
}
/**
* A helper specifically designed for `vi.mock('@google/gemini-cli-core', ...)` to easily
* mock both `debugLogger` and `coreEvents.emitConsoleLog`.
*
* Example:
* ```typescript
* vi.mock('@google/gemini-cli-core', async (importOriginal) => {
* const { mockCoreDebugLogger } = await import('../../test-utils/mockDebugLogger.js');
* return mockCoreDebugLogger(
* await importOriginal<typeof import('@google/gemini-cli-core')>(),
* { stripAnsi: true }
* );
* });
* ```
*/
export function mockCoreDebugLogger<T extends Record<string, unknown>>(
actual: T,
options?: { stripAnsi?: boolean },
): T {
const { emitConsoleLog, debugLogger } = createMockDebugLogger(options);
return {
...actual,
coreEvents: {
...(typeof actual['coreEvents'] === 'object' &&
actual['coreEvents'] !== null
? actual['coreEvents']
: {}),
emitConsoleLog,
},
debugLogger,
} as T;
}
+46 -7
View File
@@ -51,6 +51,7 @@ import { SessionStatsProvider } from '../ui/contexts/SessionContext.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
import { generateSvgForTerminal } from './svg.js';
export const persistentStateMock = new FakePersistentState();
@@ -105,7 +106,12 @@ class XtermStdout extends EventEmitter {
private queue: { promise: Promise<void> };
isTTY = true;
getColorDepth(): number {
return 24;
}
private lastRenderOutput: string | undefined = undefined;
private lastRenderStaticContent: string | undefined = undefined;
constructor(state: TerminalState, queue: { promise: Promise<void> }) {
super();
@@ -138,6 +144,7 @@ class XtermStdout extends EventEmitter {
clear = () => {
this.state.terminal.reset();
this.lastRenderOutput = undefined;
this.lastRenderStaticContent = undefined;
};
dispose = () => {
@@ -146,10 +153,32 @@ class XtermStdout extends EventEmitter {
onRender = (staticContent: string, output: string) => {
this.renderCount++;
this.lastRenderStaticContent = staticContent;
this.lastRenderOutput = output;
this.emit('render');
};
private normalizeFrame = (text: string): string =>
text.replace(/\r\n/g, '\n');
generateSvg = (): string => generateSvgForTerminal(this.state.terminal);
lastFrameRaw = (options: { allowEmpty?: boolean } = {}) => {
const result =
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? '');
const normalized = this.normalizeFrame(result);
if (normalized === '' && !options.allowEmpty) {
throw new Error(
'lastFrameRaw() returned an empty string. If this is intentional, use lastFrameRaw({ allowEmpty: true }). ' +
'Otherwise, ensure you are calling await waitUntilReady() and that the component is rendering correctly.',
);
}
return normalized;
};
lastFrame = (options: { allowEmpty?: boolean } = {}) => {
const buffer = this.state.terminal.buffer.active;
const allLines: string[] = [];
@@ -163,9 +192,7 @@ class XtermStdout extends EventEmitter {
}
const result = trimmed.join('\n');
// Normalize for cross-platform snapshot stability:
// Normalize any \r\n to \n
const normalized = result.replace(/\r\n/g, '\n');
const normalized = this.normalizeFrame(result);
if (normalized === '' && !options.allowEmpty) {
throw new Error(
@@ -213,9 +240,11 @@ class XtermStdout extends EventEmitter {
const currentFrame = stripAnsi(
this.lastFrame({ allowEmpty: true }),
).trim();
const expectedFrame = stripAnsi(this.lastRenderOutput ?? '')
.trim()
.replace(/\r\n/g, '\n');
const expectedFrame = this.normalizeFrame(
stripAnsi(
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''),
),
).trim();
lastCurrent = currentFrame;
lastExpected = expectedFrame;
@@ -340,6 +369,8 @@ export type RenderInstance = {
stdin: XtermStdin;
frames: string[];
lastFrame: (options?: { allowEmpty?: boolean }) => string;
lastFrameRaw: (options?: { allowEmpty?: boolean }) => string;
generateSvg: () => string;
terminal: Terminal;
waitUntilReady: () => Promise<void>;
capturedOverflowState: OverflowState | undefined;
@@ -424,6 +455,8 @@ export const render = (
stdin,
frames: stdout.frames,
lastFrame: stdout.lastFrame,
lastFrameRaw: stdout.lastFrameRaw,
generateSvg: stdout.generateSvg,
terminal: state.terminal,
waitUntilReady: () => stdout.waitUntilReady(),
};
@@ -524,6 +557,7 @@ export const mockAppState: AppState = {
const mockUIActions: UIActions = {
handleThemeSelect: vi.fn(),
closeThemeDialog: vi.fn(),
setIsOnboardingForeverMode: vi.fn(),
handleThemeHighlight: vi.fn(),
handleAuthSelect: vi.fn(),
setAuthState: vi.fn(),
@@ -767,6 +801,7 @@ export function renderHook<Result, Props>(
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
generateSvg: () => string;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
@@ -789,6 +824,7 @@ export function renderHook<Result, Props>(
let inkRerender: (tree: React.ReactElement) => void = () => {};
let unmount: () => void = () => {};
let waitUntilReady: () => Promise<void> = async () => {};
let generateSvg: () => string = () => '';
act(() => {
const renderResult = render(
@@ -799,6 +835,7 @@ export function renderHook<Result, Props>(
inkRerender = renderResult.rerender;
unmount = renderResult.unmount;
waitUntilReady = renderResult.waitUntilReady;
generateSvg = renderResult.generateSvg;
});
function rerender(props?: Props) {
@@ -815,7 +852,7 @@ export function renderHook<Result, Props>(
});
}
return { result, rerender, unmount, waitUntilReady };
return { result, rerender, unmount, waitUntilReady, generateSvg };
}
export function renderHookWithProviders<Result, Props>(
@@ -837,6 +874,7 @@ export function renderHookWithProviders<Result, Props>(
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
generateSvg: () => string;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
@@ -887,5 +925,6 @@ export function renderHookWithProviders<Result, Props>(
});
},
waitUntilReady: () => renderResult.waitUntilReady(),
generateSvg: () => renderResult.generateSvg(),
};
}
+190
View File
@@ -0,0 +1,190 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Terminal } from '@xterm/headless';
export const generateSvgForTerminal = (terminal: Terminal): string => {
const activeBuffer = terminal.buffer.active;
const getHexColor = (
isRGB: boolean,
isPalette: boolean,
isDefault: boolean,
colorCode: number,
): string | null => {
if (isDefault) return null;
if (isRGB) {
return `#${colorCode.toString(16).padStart(6, '0')}`;
}
if (isPalette) {
if (colorCode >= 0 && colorCode <= 15) {
return (
[
'#000000',
'#cd0000',
'#00cd00',
'#cdcd00',
'#0000ee',
'#cd00cd',
'#00cdcd',
'#e5e5e5',
'#7f7f7f',
'#ff0000',
'#00ff00',
'#ffff00',
'#5c5cff',
'#ff00ff',
'#00ffff',
'#ffffff',
][colorCode] || null
);
} else if (colorCode >= 16 && colorCode <= 231) {
const v = [0, 95, 135, 175, 215, 255];
const c = colorCode - 16;
const b = v[c % 6];
const g = v[Math.floor(c / 6) % 6];
const r = v[Math.floor(c / 36) % 6];
return `#${[r, g, b].map((x) => x?.toString(16).padStart(2, '0')).join('')}`;
} else if (colorCode >= 232 && colorCode <= 255) {
const gray = 8 + (colorCode - 232) * 10;
const hex = gray.toString(16).padStart(2, '0');
return `#${hex}${hex}${hex}`;
}
}
return null;
};
const escapeXml = (unsafe: string): string =>
// eslint-disable-next-line no-control-regex
unsafe.replace(/[<>&'"\x00-\x08\x0B-\x0C\x0E-\x1F]/g, (c) => {
switch (c) {
case '<':
return '&lt;';
case '>':
return '&gt;';
case '&':
return '&amp;';
case "'":
return '&apos;';
case '"':
return '&quot;';
default:
return '';
}
});
const charWidth = 9;
const charHeight = 17;
const padding = 10;
// Find the actual number of rows with content to avoid rendering trailing blank space.
let contentRows = terminal.rows;
for (let y = terminal.rows - 1; y >= 0; y--) {
const line = activeBuffer.getLine(y);
if (line && line.translateToString(true).trim().length > 0) {
contentRows = y + 1;
break;
}
}
if (contentRows === 0) contentRows = 1; // Minimum 1 row
const width = terminal.cols * charWidth + padding * 2;
const height = contentRows * charHeight + padding * 2;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
`;
svg += ` <style>
`;
svg += ` text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
`;
svg += ` </style>
`;
svg += ` <rect width="${width}" height="${height}" fill="#000000" />
`; // Terminal background
svg += ` <g transform="translate(${padding}, ${padding})">
`;
for (let y = 0; y < contentRows; y++) {
const line = activeBuffer.getLine(y);
if (!line) continue;
let currentFgHex: string | null = null;
let currentBgHex: string | null = null;
let currentBlockStartCol = -1;
let currentBlockText = '';
let currentBlockNumCells = 0;
const finalizeBlock = (_endCol: number) => {
if (currentBlockStartCol !== -1) {
if (currentBlockText.length > 0) {
const xPos = currentBlockStartCol * charWidth;
const yPos = y * charHeight;
if (currentBgHex) {
const rectWidth = currentBlockNumCells * charWidth;
svg += ` <rect x="${xPos}" y="${yPos}" width="${rectWidth}" height="${charHeight}" fill="${currentBgHex}" />
`;
}
if (currentBlockText.trim().length > 0) {
const fill = currentFgHex || '#ffffff'; // Default text color
const textWidth = currentBlockNumCells * charWidth;
// Use textLength to ensure the block fits exactly into its designated cells
svg += ` <text x="${xPos}" y="${yPos + 2}" fill="${fill}" textLength="${textWidth}" lengthAdjust="spacingAndGlyphs">${escapeXml(currentBlockText)}</text>
`;
}
}
}
};
for (let x = 0; x < line.length; x++) {
const cell = line.getCell(x);
if (!cell) continue;
const cellWidth = cell.getWidth();
if (cellWidth === 0) continue; // Skip continuation cells of wide characters
let fgHex = getHexColor(
cell.isFgRGB(),
cell.isFgPalette(),
cell.isFgDefault(),
cell.getFgColor(),
);
let bgHex = getHexColor(
cell.isBgRGB(),
cell.isBgPalette(),
cell.isBgDefault(),
cell.getBgColor(),
);
if (cell.isInverse()) {
const tempFgHex = fgHex;
fgHex = bgHex || '#000000';
bgHex = tempFgHex || '#ffffff';
}
let chars = cell.getChars();
if (chars === '') chars = ' '.repeat(cellWidth);
if (
fgHex !== currentFgHex ||
bgHex !== currentBgHex ||
currentBlockStartCol === -1
) {
finalizeBlock(x);
currentFgHex = fgHex;
currentBgHex = bgHex;
currentBlockStartCol = x;
currentBlockText = chars;
currentBlockNumCells = cellWidth;
} else {
currentBlockText += chars;
currentBlockNumCells += cellWidth;
}
}
finalizeBlock(line.length);
}
svg += ` </g>\n</svg>`;
return svg;
};
+3 -2
View File
@@ -328,6 +328,7 @@ describe('AppContainer State Management', () => {
backgroundShells: new Map(),
registerBackgroundShell: vi.fn(),
dismissBackgroundShell: vi.fn(),
sisyphusSecondsRemaining: null,
};
beforeEach(() => {
@@ -2185,7 +2186,7 @@ describe('AppContainer State Management', () => {
const mockedMeasureElement = measureElement as Mock;
const mockedUseTerminalSize = useTerminalSize as Mock;
it('should prevent terminal height from being less than 1', async () => {
it.skip('should prevent terminal height from being less than 1', async () => {
const resizePtySpy = vi.spyOn(ShellExecutionService, 'resizePty');
// Arrange: Simulate a small terminal and a large footer
mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 5 });
@@ -3122,7 +3123,7 @@ describe('AppContainer State Management', () => {
});
describe('Shell Interaction', () => {
it('should not crash if resizing the pty fails', async () => {
it.skip('should not crash if resizing the pty fails', async () => {
const resizePtySpy = vi
.spyOn(ShellExecutionService, 'resizePty')
.mockImplementation(() => {
+65 -28
View File
@@ -123,6 +123,11 @@ import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { type IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
import {
notifyResponse,
hasPendingTasks,
markTasksWorking,
} from '../external-listener.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
@@ -230,6 +235,9 @@ export const AppContainer = (props: AppContainerProps) => {
useMemoryMonitor(historyManager);
const isAlternateBuffer = useAlternateBuffer();
const [corgiMode, setCorgiMode] = useState(false);
const [isOnboardingForeverMode, setIsOnboardingForeverMode] = useState(
() => config.getIsForeverMode() && !config.getIsForeverModeConfigured(),
);
const [forceRerenderKey, setForceRerenderKey] = useState(0);
const [debugMessage, setDebugMessage] = useState<string>('');
const [quittingMessages, setQuittingMessages] = useState<
@@ -1104,6 +1112,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
dismissBackgroundShell,
retryStatus,
sisyphusSecondsRemaining,
} = useGeminiStream(
config.getGeminiClient(),
historyManager.history,
@@ -1195,6 +1204,49 @@ Logging in with Google... Restarting Gemini CLI to continue.
isMcpReady,
});
// Bridge external messages from A2A HTTP listener to message queue
useEffect(() => {
const handler = (text: string) => {
addMessage(text);
};
appEvents.on(AppEvent.ExternalMessage, handler);
return () => {
appEvents.off(AppEvent.ExternalMessage, handler);
};
}, [addMessage]);
// Track streaming state transitions for A2A response capture
const prevStreamingStateRef = useRef(streamingState);
useEffect(() => {
const prev = prevStreamingStateRef.current;
prevStreamingStateRef.current = streamingState;
// Mark tasks as "working" when streaming starts
if (
prev === StreamingState.Idle &&
streamingState !== StreamingState.Idle
) {
markTasksWorking();
}
// Capture response when streaming ends
if (
prev !== StreamingState.Idle &&
streamingState === StreamingState.Idle &&
hasPendingTasks()
) {
const lastResponse = historyManager.history
.slice()
.reverse()
.find((item) => item.type === 'gemini');
notifyResponse(
typeof lastResponse?.text === 'string' ? lastResponse.text : '',
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [streamingState]);
cancelHandlerRef.current = useCallback(
(shouldRestorePrompt: boolean = true) => {
const pendingHistoryItems = [
@@ -1417,32 +1469,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
const initialPromptSubmitted = useRef(false);
const geminiClient = config.getGeminiClient();
useEffect(() => {
if (activePtyId) {
try {
ShellExecutionService.resizePty(
activePtyId,
Math.floor(terminalWidth * SHELL_WIDTH_FRACTION),
Math.max(
Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING),
1,
),
);
} catch (e) {
// This can happen in a race condition where the pty exits
// right before we try to resize it.
if (
!(
e instanceof Error &&
e.message.includes('Cannot resize a pty that has already exited')
)
) {
throw e;
}
}
}
}, [terminalWidth, availableTerminalHeight, activePtyId]);
useEffect(() => {
if (
initialPrompt &&
@@ -1453,7 +1479,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
!isThemeDialogOpen &&
!isEditorDialogOpen &&
!showPrivacyNotice &&
geminiClient?.isInitialized?.()
!isOnboardingForeverMode &&
geminiClient?.isInitialized?.() &&
isMcpReady
) {
void handleFinalSubmit(initialPrompt);
initialPromptSubmitted.current = true;
@@ -1467,7 +1495,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
isThemeDialogOpen,
isEditorDialogOpen,
showPrivacyNotice,
isOnboardingForeverMode,
geminiClient,
isMcpReady,
]);
const [idePromptAnswered, setIdePromptAnswered] = useState(false);
@@ -2034,8 +2064,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
const dialogsVisible =
(shouldShowRetentionWarning && retentionCheckComplete) ||
isOnboardingForeverMode ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
(!isOnboardingForeverMode && isFolderTrustDialogOpen) ||
isPolicyUpdateDialogOpen ||
adminSettingsChanged ||
!!commandConfirmationRequest ||
@@ -2213,6 +2244,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
const uiState: UIState = useMemo(
() => ({
isOnboardingForeverMode,
history: historyManager.history,
historyManager,
isThemeDialogOpen,
@@ -2341,6 +2373,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
...pendingGeminiHistoryItems,
]),
hintBuffer: '',
sisyphusSecondsRemaining,
}),
[
isThemeDialogOpen,
@@ -2461,6 +2494,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
adminSettingsChanged,
newAgents,
showIsExpandableHint,
sisyphusSecondsRemaining,
isOnboardingForeverMode,
],
);
@@ -2471,6 +2506,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
const uiActions: UIActions = useMemo(
() => ({
setIsOnboardingForeverMode,
handleThemeSelect,
closeThemeDialog,
handleThemeHighlight,
@@ -2577,6 +2613,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleFolderTrustSelect,
setIsPolicyUpdateDialogOpen,
setConstrainHeight,
setIsOnboardingForeverMode,
handleEscapePromptChange,
refreshStatic,
handleFinalSubmit,
@@ -2,14 +2,14 @@
exports[`App > Snapshots > renders default layout correctly 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -47,14 +47,14 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = `
"Notifications
Footer
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -67,14 +67,14 @@ Composer
exports[`App > Snapshots > renders with dialogs visible 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
@@ -110,14 +110,14 @@ DialogManager
exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -53,6 +53,7 @@ export const compressCommand: SlashCommand = {
originalTokenCount: compressed.originalTokenCount,
newTokenCount: compressed.newTokenCount,
compressionStatus: compressed.compressionStatus,
archivePath: compressed.archivePath,
},
} as HistoryItemCompression,
Date.now(),
@@ -23,11 +23,11 @@ import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
import { debugLogger } from '@google/gemini-cli-core';
export const GITHUB_WORKFLOW_PATHS = [
'gemini-dispatch/gemini-dispatch.yml',
'gemini-assistant/gemini-invoke.yml',
'gemini-assistant/gemini-plan-execute.yml',
'gemini-dispatch/gemini-dispatch.yml',
'issue-triage/gemini-scheduled-triage.yml',
'issue-triage/gemini-triage.yml',
'issue-triage/gemini-scheduled-triage.yml',
'pr-review/gemini-review.yml',
];
@@ -33,7 +33,7 @@ describe('<AnsiOutputText />', () => {
<AnsiOutputText data={data} width={80} />,
);
await waitUntilReady();
expect(lastFrame()).toBe('Hello, world!\n');
expect(lastFrame().trim()).toBe('Hello, world!');
unmount();
});
@@ -51,7 +51,7 @@ describe('<AnsiOutputText />', () => {
<AnsiOutputText data={data} width={80} />,
);
await waitUntilReady();
expect(lastFrame()).toBe(text + '\n');
expect(lastFrame().trim()).toBe(text);
unmount();
});
@@ -65,7 +65,7 @@ describe('<AnsiOutputText />', () => {
<AnsiOutputText data={data} width={80} />,
);
await waitUntilReady();
expect(lastFrame()).toBe(text + '\n');
expect(lastFrame().trim()).toBe(text);
unmount();
});
@@ -10,7 +10,6 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { AskUserDialog } from './AskUserDialog.js';
import { QuestionType, type Question } from '@google/gemini-cli-core';
import chalk from 'chalk';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
// Helper to write to stdin with proper act() wrapping
@@ -1104,7 +1103,7 @@ describe('AskUserDialog', () => {
await waitUntilReady();
const frame = lastFrame();
// Plain text should be rendered as bold
expect(frame).toContain(chalk.bold('Which option do you prefer?'));
expect(frame).toContain('Which option do you prefer?');
});
});
@@ -1136,7 +1135,7 @@ describe('AskUserDialog', () => {
// Should NOT have double-bold (the whole question bolded AND "this" bolded)
// "Is " should not be bold, only "this" should be bold
expect(frame).toContain('Is ');
expect(frame).toContain(chalk.bold('this'));
expect(frame).toContain('this');
expect(frame).not.toContain('**this**');
});
});
@@ -1166,8 +1165,8 @@ describe('AskUserDialog', () => {
await waitFor(async () => {
await waitUntilReady();
const frame = lastFrame();
// Check for chalk.bold('this') - asterisks should be gone, text should be bold
expect(frame).toContain(chalk.bold('this'));
// Check for 'this' - asterisks should be gone
expect(frame).toContain('this');
expect(frame).not.toContain('**this**');
});
});
@@ -1198,8 +1197,8 @@ describe('AskUserDialog', () => {
await waitUntilReady();
const frame = lastFrame();
// Backticks should be removed
expect(frame).toContain('npm start');
expect(frame).not.toContain('`npm start`');
expect(frame).toContain('Run npm start?');
expect(frame).not.toContain('`');
});
});
});
@@ -207,6 +207,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
proQuotaRequest: null,
validationRequest: null,
},
sisyphusSecondsRemaining: null,
...overrides,
}) as UIState;
@@ -6,7 +6,7 @@
import { act } from 'react';
import type { EventEmitter } from 'node:events';
import { render } from '../../test-utils/render.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
import {
@@ -27,7 +27,7 @@ import {
import { Text } from 'ink';
// Mock GeminiSpinner
vi.mock('./GeminiRespondingSpinner.js', () => ({
vi.mock('./GeminiSpinner.js', () => ({
GeminiSpinner: () => <Text>Spinner</Text>,
}));
@@ -43,7 +43,9 @@ describe('ConfigInitDisplay', () => {
});
it('renders initial state', async () => {
const { lastFrame, waitUntilReady } = render(<ConfigInitDisplay />);
const { lastFrame, waitUntilReady } = renderWithProviders(
<ConfigInitDisplay />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
@@ -57,7 +59,7 @@ describe('ConfigInitDisplay', () => {
return coreEvents;
});
const { lastFrame } = render(<ConfigInitDisplay />);
const { lastFrame } = renderWithProviders(<ConfigInitDisplay />);
// Wait for listener to be registered
await waitFor(() => {
@@ -95,7 +97,7 @@ describe('ConfigInitDisplay', () => {
return coreEvents;
});
const { lastFrame } = render(<ConfigInitDisplay />);
const { lastFrame } = renderWithProviders(<ConfigInitDisplay />);
await waitFor(() => {
if (!listener) throw new Error('Listener not registered yet');
@@ -131,7 +133,7 @@ describe('ConfigInitDisplay', () => {
return coreEvents;
});
const { lastFrame } = render(<ConfigInitDisplay />);
const { lastFrame } = renderWithProviders(<ConfigInitDisplay />);
await waitFor(() => {
if (!listener) throw new Error('Listener not registered yet');
@@ -12,7 +12,7 @@ import {
type McpClient,
MCPServerStatus,
} from '@google/gemini-cli-core';
import { GeminiSpinner } from './GeminiRespondingSpinner.js';
import { GeminiSpinner } from './GeminiSpinner.js';
import { theme } from '../semantic-colors.js';
export const ConfigInitDisplay = ({
@@ -38,6 +38,7 @@ import { SessionRetentionWarningDialog } from './SessionRetentionWarningDialog.j
import { useCallback } from 'react';
import { SettingScope } from '../../config/settings.js';
import { PolicyUpdateDialog } from './PolicyUpdateDialog.js';
import { ForeverModeOnboardingDialog } from './ForeverModeOnboardingDialog.js';
interface DialogManagerProps {
addItem: UseHistoryManagerReturn['addItem'];
@@ -109,6 +110,13 @@ export const DialogManager = ({
);
}
if (uiState.isOnboardingForeverMode) {
return (
<ForeverModeOnboardingDialog
onComplete={() => uiActions.setIsOnboardingForeverMode(false)}
/>
);
}
if (uiState.adminSettingsChanged) {
return <AdminSettingsChangedDialog />;
}
@@ -7,7 +7,7 @@
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { act } from 'react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { FolderTrustDialog } from './FolderTrustDialog.js';
import { ExitCodes } from '@google/gemini-cli-core';
import * as processUtils from '../../utils/processUtils.js';
@@ -0,0 +1,323 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text } from 'ink';
import { useState } from 'react';
import { theme } from '../semantic-colors.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { relaunchApp } from '../../utils/processUtils.js';
import { GEMINI_DIR, DEFAULT_CONTEXT_FILENAME } from '@google/gemini-cli-core';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import { useTextBuffer } from './shared/text-buffer.js';
import { TextInput } from './shared/TextInput.js';
enum Step {
MISSION,
FIRST_STEPS,
SISYPHUS_CONFIG,
SAVING,
ERROR,
}
export const ForeverModeOnboardingDialog = ({
onComplete,
}: {
onComplete: () => void;
}) => {
const config = useConfig();
const [step, setStep] = useState(Step.MISSION);
const [sisyphusFocus, setSisyphusFocus] = useState<'timeout' | 'prompt'>(
'timeout',
);
const [error, setError] = useState<string | null>(null);
const missionBuffer = useTextBuffer({
initialText: '',
viewport: { width: 80, height: 3 },
singleLine: false,
});
const firstStepsBuffer = useTextBuffer({
initialText: '',
viewport: { width: 80, height: 5 },
singleLine: false,
});
const sisyphusTimeoutBuffer = useTextBuffer({
initialText: '',
viewport: { width: 50, height: 1 },
singleLine: true,
});
const sisyphusPromptBuffer = useTextBuffer({
initialText: 'continue',
viewport: { width: 50, height: 1 },
singleLine: true,
});
const handleMissionSubmit = () => {
if (missionBuffer.text.trim()) setStep(Step.FIRST_STEPS);
};
const handleFirstStepsSubmit = () => {
if (firstStepsBuffer.text.trim()) setStep(Step.SISYPHUS_CONFIG);
};
const handleSisyphusTimeoutSubmit = (value: string) => {
const num = parseInt(value, 10);
if (!isNaN(num) && num > 0) {
setSisyphusFocus('prompt');
} else {
void handleSaveSettings();
}
};
const handleSisyphusPromptSubmit = () => {
void handleSaveSettings();
};
const handleSaveSettings = async () => {
setStep(Step.SAVING);
try {
const timeoutNum = parseInt(sisyphusTimeoutBuffer.text, 10);
const hasSisyphus = !isNaN(timeoutNum) && timeoutNum > 0;
let frontmatter = '---\n';
frontmatter += 'sisyphus:\n';
frontmatter += ` enabled: ${hasSisyphus}\n`;
if (hasSisyphus) {
frontmatter += ` idleTimeout: ${timeoutNum}\n`;
if (sisyphusPromptBuffer.text.trim()) {
frontmatter += ` prompt: "${sisyphusPromptBuffer.text.trim()}"\n`;
}
}
frontmatter += '---\n\n';
let content = frontmatter;
if (missionBuffer.text.trim()) {
content += `# Mission\n${missionBuffer.text.trim()}\n\n`;
}
const geminiDir = path.join(config.getTargetDir(), GEMINI_DIR);
await fs.mkdir(geminiDir, { recursive: true });
await fs.writeFile(
path.join(geminiDir, DEFAULT_CONTEXT_FILENAME),
content,
'utf-8',
);
if (firstStepsBuffer.text.trim()) {
await fs.writeFile(
path.join(geminiDir, '.onboarding_prompt'),
firstStepsBuffer.text.trim(),
'utf-8',
);
}
try {
execSync('git init', { cwd: geminiDir, stdio: 'ignore' });
execSync('git add .', { cwd: geminiDir, stdio: 'ignore' });
execSync('git commit -m "chore(memory): initialize gemini memory"', {
cwd: geminiDir,
stdio: 'ignore',
});
} catch (_e) {
// Ignore git errors if git is not installed or user has no git config
}
onComplete(); // Before relaunch
await relaunchApp();
} catch (e: unknown) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(String(e));
}
setStep(Step.ERROR);
}
};
if (step === Step.ERROR) {
return (
<Box
flexDirection="column"
padding={1}
borderStyle="round"
borderColor={theme.border.default}
>
<Text color={theme.status.error} bold>
Failed to generate config
</Text>
<Text>{error}</Text>
<Text color={theme.text.secondary}>
Please create the .gemini/GEMINI.md file manually and try again.
</Text>
</Box>
);
}
if (step === Step.SAVING) {
return (
<Box padding={1} borderStyle="round" borderColor={theme.border.default}>
<Text color={theme.text.accent}>
Saving your configuration... please wait.
</Text>
</Box>
);
}
if (step === Step.MISSION) {
return (
<Box
flexDirection="column"
padding={1}
borderStyle="round"
borderColor={theme.border.default}
>
<Text color={theme.status.success} bold>
Welcome to Forever Mode!
</Text>
<Text>
You launched the CLI with <Text bold>--forever</Text>, which runs the
agent continuously.
</Text>
<Text>
To get started, we need to set up your{' '}
<Text bold>.gemini/GEMINI.md</Text> configuration file.
</Text>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.primary} bold>
What is the primary mission of the agent?
</Text>
<Text color={theme.text.secondary}>
(e.g. &quot;Refactor the authentication module to use OAuth2&quot;)
</Text>
<Box marginTop={1}>
<Text color={theme.text.primary}> </Text>
<TextInput
buffer={missionBuffer}
onSubmit={handleMissionSubmit}
focus={true}
/>
</Box>
</Box>
</Box>
);
}
if (step === Step.FIRST_STEPS) {
return (
<Box
flexDirection="column"
padding={1}
borderStyle="round"
borderColor={theme.border.default}
>
<Text color={theme.text.primary} bold>
What are the immediate first steps?
</Text>
<Text color={theme.text.secondary}>
(e.g. &quot;Investigate src/auth.ts and propose changes&quot;)
</Text>
<Box marginTop={1}>
<Text color={theme.text.primary}> </Text>
<TextInput
buffer={firstStepsBuffer}
onSubmit={handleFirstStepsSubmit}
focus={true}
/>
</Box>
</Box>
);
}
if (step === Step.SISYPHUS_CONFIG) {
return (
<Box
flexDirection="column"
padding={1}
borderStyle="round"
borderColor={theme.border.default}
>
<Text color={theme.text.primary} bold>
Sisyphus Mode (Auto-resume)
</Text>
<Text>
If the agent completes a task and remains idle, it can automatically
resume itself by sending a specific prompt.
</Text>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>
Enter idle timeout in minutes before the agent automatically resumes
(leave blank to disable):
</Text>
<Box>
<Text
color={
sisyphusFocus === 'timeout'
? theme.text.primary
: theme.text.secondary
}
>
{' '}
</Text>
<TextInput
buffer={sisyphusTimeoutBuffer}
onSubmit={handleSisyphusTimeoutSubmit}
focus={sisyphusFocus === 'timeout'}
/>
</Box>
</Box>
{sisyphusFocus === 'prompt' && (
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>
What prompt should be sent when Sisyphus triggers?
</Text>
<Box>
<Text color={theme.text.primary}> </Text>
<TextInput
buffer={sisyphusPromptBuffer}
onSubmit={handleSisyphusPromptSubmit}
focus={sisyphusFocus === 'prompt'}
/>
</Box>
</Box>
)}
</Box>
);
}
if (step === Step.SAVING) {
return (
<Box padding={1} borderStyle="round" borderColor={theme.border.default}>
<Text>Saving your settings and launching the agent...</Text>
</Box>
);
}
if (step === Step.ERROR && error) {
return (
<Box
flexDirection="column"
padding={1}
borderStyle="round"
borderColor={theme.status.error}
>
<Text color={theme.status.error} bold>
Error
</Text>
<Text>{error}</Text>
<Text color={theme.text.secondary}>
Press Ctrl+C to exit and try again.
</Text>
</Box>
);
}
return null;
};
@@ -8,7 +8,7 @@ import { render } from '../../test-utils/render.js';
import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useStreamingContext } from '../contexts/StreamingContext.js';
import { useIsScreenReaderEnabled } from 'ink';
import { Text, useIsScreenReaderEnabled } from 'ink';
import { StreamingState } from '../types.js';
import {
SCREEN_READER_LOADING,
@@ -24,8 +24,10 @@ vi.mock('ink', async (importOriginal) => {
};
});
vi.mock('./CliSpinner.js', () => ({
CliSpinner: () => 'Spinner',
vi.mock('./GeminiSpinner.js', () => ({
GeminiSpinner: ({ altText }: { altText?: string }) => (
<Text>GeminiSpinner {altText}</Text>
),
}));
describe('GeminiRespondingSpinner', () => {
@@ -33,23 +35,17 @@ describe('GeminiRespondingSpinner', () => {
const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled);
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
mockUseIsScreenReaderEnabled.mockReturnValue(false);
});
afterEach(() => {
vi.useRealTimers();
});
it('renders spinner when responding', async () => {
mockUseStreamingContext.mockReturnValue(StreamingState.Responding);
const { lastFrame, waitUntilReady, unmount } = render(
<GeminiRespondingSpinner />,
);
await waitUntilReady();
// Spinner output varies, but it shouldn't be empty
expect(lastFrame()).not.toBe('');
expect(lastFrame()).toContain('GeminiSpinner');
unmount();
});
@@ -5,9 +5,7 @@
*/
import type React from 'react';
import { useState, useEffect, useMemo } from 'react';
import { Text, useIsScreenReaderEnabled } from 'ink';
import { CliSpinner } from './CliSpinner.js';
import type { SpinnerName } from 'cli-spinners';
import { useStreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js';
@@ -16,10 +14,7 @@ import {
SCREEN_READER_RESPONDING,
} from '../textConstants.js';
import { theme } from '../semantic-colors.js';
import { Colors } from '../colors.js';
import tinygradient from 'tinygradient';
const COLOR_CYCLE_DURATION_MS = 4000;
import { GeminiSpinner } from './GeminiSpinner.js';
interface GeminiRespondingSpinnerProps {
/**
@@ -54,51 +49,3 @@ export const GeminiRespondingSpinner: React.FC<
return null;
};
interface GeminiSpinnerProps {
spinnerType?: SpinnerName;
altText?: string;
}
export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
spinnerType = 'dots',
altText,
}) => {
const isScreenReaderEnabled = useIsScreenReaderEnabled();
const [time, setTime] = useState(0);
const googleGradient = useMemo(() => {
const brandColors = [
Colors.AccentPurple,
Colors.AccentBlue,
Colors.AccentCyan,
Colors.AccentGreen,
Colors.AccentYellow,
Colors.AccentRed,
];
return tinygradient([...brandColors, brandColors[0]]);
}, []);
useEffect(() => {
if (isScreenReaderEnabled) {
return;
}
const interval = setInterval(() => {
setTime((prevTime) => prevTime + 30);
}, 30); // ~33fps for smooth color transitions
return () => clearInterval(interval);
}, [isScreenReaderEnabled]);
const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS;
const currentColor = googleGradient.rgbAt(progress).toHexString();
return isScreenReaderEnabled ? (
<Text>{altText}</Text>
) : (
<Text color={currentColor}>
<CliSpinner type={spinnerType} />
</Text>
);
};
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useState, useEffect, useMemo } from 'react';
import { Text, useIsScreenReaderEnabled } from 'ink';
import { CliSpinner } from './CliSpinner.js';
import type { SpinnerName } from 'cli-spinners';
import { Colors } from '../colors.js';
import tinygradient from 'tinygradient';
const COLOR_CYCLE_DURATION_MS = 4000;
interface GeminiSpinnerProps {
spinnerType?: SpinnerName;
altText?: string;
}
export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
spinnerType = 'dots',
altText,
}) => {
const isScreenReaderEnabled = useIsScreenReaderEnabled();
const [time, setTime] = useState(0);
const googleGradient = useMemo(() => {
const brandColors = [
Colors.AccentPurple,
Colors.AccentBlue,
Colors.AccentCyan,
Colors.AccentGreen,
Colors.AccentYellow,
Colors.AccentRed,
];
return tinygradient([...brandColors, brandColors[0]]);
}, []);
useEffect(() => {
if (isScreenReaderEnabled) {
return;
}
const interval = setInterval(() => {
setTime((prevTime) => prevTime + 30);
}, 30); // ~33fps for smooth color transitions
return () => clearInterval(interval);
}, [isScreenReaderEnabled]);
const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS;
const currentColor = googleGradient.rgbAt(progress).toHexString();
return isScreenReaderEnabled ? (
<Text>{altText}</Text>
) : (
<Text color={currentColor}>
<CliSpinner type={spinnerType} />
</Text>
);
};
@@ -1537,7 +1537,7 @@ describe('InputPrompt', () => {
const { stdout, unmount } = renderWithProviders(<InputPrompt {...props} />);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
// In plan mode it uses '>' but with success color.
// We check that it contains '>' and not '*' or '!'.
expect(frame).toContain('>');
@@ -1593,7 +1593,7 @@ describe('InputPrompt', () => {
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
expect(frame).toContain('▀');
expect(frame).toContain('▄');
});
@@ -1626,7 +1626,7 @@ describe('InputPrompt', () => {
const expectedBgColor = isWhite ? '#eeeeee' : '#1c1c1c';
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
// Use chalk to get the expected background color escape sequence
const bgCheck = chalk.bgHex(expectedBgColor)(' ');
@@ -1658,7 +1658,7 @@ describe('InputPrompt', () => {
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
expect(frame).not.toContain('▀');
expect(frame).not.toContain('▄');
// It SHOULD have horizontal fallback lines
@@ -1681,7 +1681,7 @@ describe('InputPrompt', () => {
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
expect(frame).toContain('▀');
@@ -1705,7 +1705,7 @@ describe('InputPrompt', () => {
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
// Should NOT have background characters
@@ -1734,7 +1734,7 @@ describe('InputPrompt', () => {
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
expect(frame).not.toContain('▀');
expect(frame).not.toContain('▄');
// Check for Box borders (round style uses unicode box chars)
@@ -1974,7 +1974,7 @@ describe('InputPrompt', () => {
name: 'at the end of a line with unicode characters',
text: 'hello 👍',
visualCursor: [0, 8],
expected: `hello 👍${chalk.inverse(' ')}`,
expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug
},
{
name: 'at the end of a short line with unicode characters',
@@ -1996,7 +1996,7 @@ describe('InputPrompt', () => {
},
])(
'should display cursor correctly $name',
async ({ text, visualCursor, expected }) => {
async ({ name, text, visualCursor, expected }) => {
mockBuffer.text = text;
mockBuffer.lines = [text];
mockBuffer.viewportVisualLines = [text];
@@ -2007,8 +2007,14 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrame();
expect(frame).toContain(expected);
const frame = stdout.lastFrameRaw();
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
if (
name !== 'at the end of a line with unicode characters' &&
name !== 'on a highlighted token'
) {
expect(frame).toContain('\u001b[7m');
}
});
unmount();
},
@@ -2050,7 +2056,7 @@ describe('InputPrompt', () => {
},
])(
'should display cursor correctly $name in a multiline block',
async ({ text, visualCursor, expected, visualToLogicalMap }) => {
async ({ name, text, visualCursor, expected, visualToLogicalMap }) => {
mockBuffer.text = text;
mockBuffer.lines = text.split('\n');
mockBuffer.viewportVisualLines = text.split('\n');
@@ -2064,8 +2070,14 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrame();
expect(frame).toContain(expected);
const frame = stdout.lastFrameRaw();
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
if (
name !== 'at the end of a line with unicode characters' &&
name !== 'on a highlighted token'
) {
expect(frame).toContain('\u001b[7m');
}
});
unmount();
},
@@ -2088,7 +2100,7 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
const lines = frame.split('\n');
// The line with the cursor should just be an inverted space inside the box border
expect(
@@ -2120,7 +2132,7 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
);
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
// Check that all lines, including the empty one, are rendered.
// This implicitly tests that the Box wrapper provides height for the empty line.
expect(frame).toContain('hello');
@@ -2655,7 +2667,7 @@ describe('InputPrompt', () => {
});
await waitFor(() => {
const frame = stdout.lastFrame();
const frame = stdout.lastFrameRaw();
expect(frame).toContain('(r:)');
expect(frame).toContain('echo hello');
expect(frame).toContain('echo world');
@@ -2926,7 +2938,7 @@ describe('InputPrompt', () => {
});
await waitFor(() => {
const frame = stdout.lastFrame() ?? '';
const frame = stdout.lastFrameRaw() ?? '';
expect(frame).toContain('(r:)');
expect(frame).toContain('git commit');
expect(frame).toContain('git push');
@@ -263,16 +263,11 @@ describe('SettingsDialog', () => {
const settings = createMockSettings();
const onSelect = vi.fn();
const { lastFrame, waitUntilReady, unmount } = renderDialog(
settings,
onSelect,
);
await waitUntilReady();
const renderResult = renderDialog(settings, onSelect);
await renderResult.waitUntilReady();
const output = lastFrame();
// Use snapshot to capture visual layout including indicators
expect(output).toMatchSnapshot();
unmount();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
});
it('should use almost full height of the window but no more when the window height is 25 rows', async () => {
@@ -1830,18 +1825,15 @@ describe('SettingsDialog', () => {
});
const onSelect = vi.fn();
const { lastFrame, stdin, waitUntilReady, unmount } = renderDialog(
settings,
onSelect,
);
await waitUntilReady();
const renderResult = renderDialog(settings, onSelect);
await renderResult.waitUntilReady();
if (stdinActions) {
await stdinActions(stdin, waitUntilReady);
await stdinActions(renderResult.stdin, renderResult.waitUntilReady);
}
expect(lastFrame()).toMatchSnapshot();
unmount();
await expect(renderResult).toMatchSvgSnapshot();
renderResult.unmount();
},
);
});
@@ -54,6 +54,7 @@ const createMockUIState = (overrides: UIStateOverrides = {}): UIState =>
backgroundShellCount: 0,
buffer: { text: '' },
history: [{ id: 1, type: 'user', text: 'test' }],
sisyphusSecondsRemaining: null,
...overrides,
}) as UIState;
@@ -170,4 +171,16 @@ describe('StatusDisplay', () => {
expect(lastFrame()).toContain('Shells: 3');
unmount();
});
it('renders Sisyphus countdown timer when active', async () => {
const uiState = createMockUIState({
sisyphusSecondsRemaining: 65, // 01:05
});
const { lastFrame, unmount } = await renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toContain('✦ Resuming work in 01:05');
unmount();
});
});
@@ -5,7 +5,7 @@
*/
import type React from 'react';
import { Text } from 'ink';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
@@ -24,18 +24,36 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
const settings = useSettings();
const config = useConfig();
const items: React.ReactNode[] = [];
if (process.env['GEMINI_SYSTEM_MD']) {
return <Text color={theme.status.error}>|_|</Text>;
items.push(<Text color={theme.status.error}>|_|</Text>);
}
if (
uiState.activeHooks.length > 0 &&
settings.merged.hooksConfig.notifications
) {
return <HookStatusDisplay activeHooks={uiState.activeHooks} />;
items.push(<HookStatusDisplay activeHooks={uiState.activeHooks} />);
}
if (!settings.merged.ui.hideContextSummary && !hideContextSummary) {
if (uiState.sisyphusSecondsRemaining !== null) {
const mins = Math.floor(uiState.sisyphusSecondsRemaining / 60);
const secs = uiState.sisyphusSecondsRemaining % 60;
const timerStr = `${mins.toString().padStart(2, '0')}:${secs
.toString()
.padStart(2, '0')}`;
items.push(
<Text color={theme.text.accent}> Resuming work in {timerStr}</Text>,
);
}
if (
items.length === 0 &&
uiState.sisyphusSecondsRemaining === null &&
!settings.merged.ui.hideContextSummary &&
!hideContextSummary
) {
return (
<ContextSummaryDisplay
ideContext={uiState.ideContextState}
@@ -51,5 +69,17 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
);
}
return null;
if (items.length === 0) {
return null;
}
return (
<Box flexDirection="row">
{items.map((item, index) => (
<Box key={index} marginRight={index < items.length - 1 ? 1 : 0}>
{item}
</Box>
))}
</Box>
);
};
+28 -10
View File
@@ -19,10 +19,8 @@ describe('Table', () => {
{ id: 2, name: 'Bob' },
];
const { lastFrame, waitUntilReady } = render(
<Table columns={columns} data={data} />,
100,
);
const renderResult = render(<Table columns={columns} data={data} />, 100);
const { lastFrame, waitUntilReady } = renderResult;
await waitUntilReady?.();
const output = lastFrame();
@@ -32,7 +30,7 @@ describe('Table', () => {
expect(output).toContain('Alice');
expect(output).toContain('2');
expect(output).toContain('Bob');
expect(lastFrame()).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
});
it('should support custom cell rendering', async () => {
@@ -48,15 +46,13 @@ describe('Table', () => {
];
const data = [{ value: 10 }];
const { lastFrame, waitUntilReady } = render(
<Table columns={columns} data={data} />,
100,
);
const renderResult = render(<Table columns={columns} data={data} />, 100);
const { lastFrame, waitUntilReady } = renderResult;
await waitUntilReady?.();
const output = lastFrame();
expect(output).toContain('20');
expect(lastFrame()).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
});
it('should handle undefined values gracefully', async () => {
@@ -70,4 +66,26 @@ describe('Table', () => {
const output = lastFrame();
expect(output).toContain('undefined');
});
it('should support inverse text rendering', async () => {
const columns = [
{
key: 'status',
header: 'Status',
flexGrow: 1,
renderCell: (item: { status: string }) => (
<Text inverse>{item.status}</Text>
),
},
];
const data = [{ status: 'Active' }];
const renderResult = render(<Table columns={columns} data={data} />, 100);
const { lastFrame, waitUntilReady } = renderResult;
await waitUntilReady?.();
const output = lastFrame();
expect(output).toContain('Active');
await expect(renderResult).toMatchSvgSnapshot();
});
});
@@ -255,7 +255,11 @@ describe('ToolConfirmationQueue', () => {
total: 1,
};
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
const {
lastFrame,
waitUntilReady,
unmount = vi.fn(),
} = renderWithProviders(
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>,
@@ -2,14 +2,14 @@
exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -25,14 +25,14 @@ Action Required (was prompted):
exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -52,14 +52,14 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -71,14 +71,14 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with history but no pending items > with_history_no_pending 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -98,14 +98,14 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -117,14 +117,14 @@ Tips for getting started:
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -132,7 +132,7 @@ Tips for getting started:
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello Gemini
> Hello Gemini
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
✦ Hello User!
"
@@ -2,14 +2,14 @@
exports[`<AppHeader /> > should not render the banner when no flags are set 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -21,14 +21,14 @@ Tips for getting started:
exports[`<AppHeader /> > should not render the default banner if shown count is 5 or more 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
Tips for getting started:
1. Ask questions, edit files, or run commands.
@@ -40,14 +40,14 @@ Tips for getting started:
exports[`<AppHeader /> > should render the banner with default text 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ This is the default banner │
@@ -62,14 +62,14 @@ Tips for getting started:
exports[`<AppHeader /> > should render the banner with warning text 1`] = `
"
███ █████████
███ █████████
░░░███ ███░░░░░███
░░░███ ███ ░░░
░░░███░███
░░░███ ███ ░░░
░░░███░███
███░ ░███ █████
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
███░ ░░███ ░░███
███░ ░░█████████
░░░ ░░░░░░░░░
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ There are capacity issues │
@@ -2,16 +2,16 @@
exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> second message
> second message
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(r:) Type your message or @path/to/file
(r:) Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
...
"
@@ -19,9 +19,9 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(r:) Type your message or @path/to/file
(r:) Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
llllllllllllllllllllllllllllllllllllllllllllllllll
"
@@ -29,7 +29,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(r:) commit
(r:) commit
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
git commit -m "feat: add search" in src/app
"
@@ -37,7 +37,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(r:) commit
(r:) commit
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
git commit -m "feat: add search" in src/app
"
@@ -45,63 +45,63 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match
exports[`InputPrompt > image path transformation snapshots > should snapshot collapsed image path 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Image ...reenshot2x.png]
> [Image ...reenshot2x.png]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > image path transformation snapshots > should snapshot expanded image path when cursor is on it 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> @/path/to/screenshots/screenshot2x.png
> @/path/to/screenshots/screenshot2x.png
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 2`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 3`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file
> Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
! Type your message or @path/to/file
! Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
* Type your message or @path/to/file
* Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file
> Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
@@ -0,0 +1,133 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Enable Auto Update </text>
<text x="837" y="257" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,133 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Enable Auto Update </text>
<text x="837" y="257" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,131 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Enable Auto Update true* </text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging false* </text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,133 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Enable Auto Update </text>
<text x="837" y="257" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,133 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Enable Auto Update </text>
<text x="837" y="257" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,131 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="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="#3d3f51" 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="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="36" y="87" fill="#6c7086" textLength="144" lengthAdjust="spacingAndGlyphs">Search to filter</text>
<text x="873" y="87" fill="#3d3f51" 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="#3d3f51" 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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="9" y="155" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Vim Mode </text>
<text x="828" y="155" fill="#6c7086" 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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Enable Auto Update </text>
<text x="837" y="257" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="9" y="597" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
<text x="72" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> 3. System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,132 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Enable Auto Update false* </text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,133 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Enable Auto Update </text>
<text x="837" y="257" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging </text>
<text x="828" y="512" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,131 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="751" viewBox="0 0 920 751">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="751" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<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="9" y="36" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> &gt; 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="#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="#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="#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="#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>
<text x="0" y="138" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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>
<text x="27" y="155" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#a6e3a1" textLength="72" lengthAdjust="spacingAndGlyphs">Vim Mode</text>
<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>
<text x="45" y="172" fill="#6c7086" textLength="198" lengthAdjust="spacingAndGlyphs">Enable Vim keybindings</text>
<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>
<text x="0" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="206" fill="#ffffff" textLength="801" lengthAdjust="spacingAndGlyphs"> Default Approval Mode </text>
<text x="810" y="206" fill="#6c7086" textLength="63" lengthAdjust="spacingAndGlyphs">Default</text>
<text x="891" y="206" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#6c7086" textLength="738" lengthAdjust="spacingAndGlyphs">The default approval mode for tool execution. &apos;default&apos; prompts for approval, &apos;au…</text>
<text x="891" y="223" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="240" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="257" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Enable Auto Update false* </text>
<text x="891" y="257" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#6c7086" textLength="225" lengthAdjust="spacingAndGlyphs">Enable automatic updates.</text>
<text x="891" y="274" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="291" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="308" fill="#ffffff" textLength="819" lengthAdjust="spacingAndGlyphs"> Enable Notifications </text>
<text x="828" y="308" fill="#6c7086" textLength="45" lengthAdjust="spacingAndGlyphs">false</text>
<text x="891" y="308" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#6c7086" textLength="756" lengthAdjust="spacingAndGlyphs">Enable run-event notifications for action-required prompts and session completion. …</text>
<text x="891" y="325" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="342" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="359" fill="#ffffff" textLength="783" lengthAdjust="spacingAndGlyphs"> Plan Directory </text>
<text x="792" y="359" fill="#6c7086" textLength="81" lengthAdjust="spacingAndGlyphs">undefined</text>
<text x="891" y="359" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#6c7086" textLength="720" lengthAdjust="spacingAndGlyphs">The directory where planning artifacts are stored. If not specified, defaults t…</text>
<text x="891" y="376" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="393" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="410" fill="#ffffff" textLength="828" lengthAdjust="spacingAndGlyphs"> Plan Model Routing </text>
<text x="837" y="410" fill="#6c7086" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
<text x="891" y="410" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#6c7086" textLength="765" lengthAdjust="spacingAndGlyphs">Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr…</text>
<text x="891" y="427" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="444" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="461" fill="#ffffff" textLength="846" lengthAdjust="spacingAndGlyphs"> Max Chat Model Attempts </text>
<text x="855" y="461" fill="#6c7086" textLength="18" lengthAdjust="spacingAndGlyphs">10</text>
<text x="891" y="461" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#6c7086" textLength="729" lengthAdjust="spacingAndGlyphs">Maximum number of attempts for requests to the main chat model. Cannot exceed 10.</text>
<text x="891" y="478" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="512" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> Debug Keystroke Logging true* </text>
<text x="891" y="512" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="529" fill="#6c7086" textLength="450" lengthAdjust="spacingAndGlyphs">Enable debug logging of keystrokes to the console.</text>
<text x="891" y="529" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#6c7086" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="563" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<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="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>
<text x="27" y="614" fill="#a6e3a1" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#a6e3a1" textLength="117" lengthAdjust="spacingAndGlyphs">User Settings</text>
<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>
<text x="891" y="631" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="9" y="648" fill="#ffffff" textLength="882" lengthAdjust="spacingAndGlyphs"> System Settings </text>
<text x="891" y="648" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="665" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="682" fill="#6c7086" textLength="657" lengthAdjust="spacingAndGlyphs">(Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close)</text>
<text x="891" y="682" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="699" fill="#3d3f51" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="716" fill="#3d3f51" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -43,8 +43,7 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = `
@@ -90,8 +89,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings disabled' correctly 1`] = `
@@ -137,8 +135,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'default state' correctly 1`] = `
@@ -184,8 +181,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'file filtering settings configured' correctly 1`] = `
@@ -231,8 +227,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = `
@@ -278,8 +273,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and number settings' correctly 1`] = `
@@ -325,8 +319,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'tools and security settings' correctly 1`] = `
@@ -372,8 +365,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settings enabled' correctly 1`] = `
@@ -419,6 +411,5 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
│ │
│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="88" fill="#000000" />
<g transform="translate(10, 10)">
<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>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<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>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="71" fill="#000000" />
<g transform="translate(10, 10)">
<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>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -4,13 +4,17 @@ exports[`Table > should render headers and data correctly 1`] = `
"ID Name
────────────────────────────────────────────────────────────────────────────────────────────────────
1 Alice
2 Bob
"
2 Bob"
`;
exports[`Table > should support custom cell rendering 1`] = `
"Value
────────────────────────────────────────────────────────────────────────────────────────────────────
20
"
20"
`;
exports[`Table > should support inverse text rendering 1`] = `
"Status
────────────────────────────────────────────────────────────────────────────────────────────────────
Active"
`;
@@ -27,6 +27,7 @@ export function CompressionMessage({
const originalTokens = originalTokenCount ?? 0;
const newTokens = newTokenCount ?? 0;
const archivePath = compression.archivePath;
const getCompressionText = () => {
if (isPending) {
@@ -36,6 +37,8 @@ export function CompressionMessage({
switch (compressionStatus) {
case CompressionStatus.COMPRESSED:
return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`;
case CompressionStatus.ARCHIVED:
return `Chat history archived to ${archivePath} (${originalTokens} to ${newTokens} tokens).`;
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
// For smaller histories (< 50k tokens), compression overhead likely exceeds benefits
if (originalTokens < 50000) {
@@ -2,29 +2,29 @@
exports[`UserMessage > renders multiline user message 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Line 1
Line 2
> Line 1
Line 2
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`UserMessage > renders normal user message with correct prefix 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello Gemini
> Hello Gemini
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`UserMessage > renders slash command message 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> /help
> /help
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`UserMessage > transforms image paths in user message 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Check out this image: [Image my-image.png]
> Check out this image: [Image my-image.png]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import chalk from 'chalk';
import { describe, it, expect } from 'vitest';
import { render } from '../../../test-utils/render.js';
import { ExpandableText, MAX_WIDTH } from './ExpandableText.js';
@@ -14,7 +13,7 @@ describe('ExpandableText', () => {
const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, '');
it('renders plain label when no match (short label)', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label="simple command"
userInput=""
@@ -23,14 +22,15 @@ describe('ExpandableText', () => {
isExpanded={false}
/>,
);
const { waitUntilReady, unmount } = renderResult;
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
it('truncates long label when collapsed and no match', async () => {
const long = 'x'.repeat(MAX_WIDTH + 25);
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label={long}
userInput=""
@@ -38,18 +38,19 @@ describe('ExpandableText', () => {
isExpanded={false}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const out = lastFrame();
const f = flat(out);
expect(f.endsWith('...')).toBe(true);
expect(f.length).toBe(MAX_WIDTH + 3);
expect(out).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
it('shows full long label when expanded and no match', async () => {
const long = 'y'.repeat(MAX_WIDTH + 25);
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label={long}
userInput=""
@@ -57,11 +58,12 @@ describe('ExpandableText', () => {
isExpanded={true}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const out = lastFrame();
const f = flat(out);
expect(f.length).toBe(long.length);
expect(out).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
@@ -69,7 +71,7 @@ describe('ExpandableText', () => {
const label = 'run: git commit -m "feat: add search"';
const userInput = 'commit';
const matchedIndex = label.indexOf(userInput);
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label={label}
userInput={userInput}
@@ -79,9 +81,9 @@ describe('ExpandableText', () => {
/>,
100,
);
const { waitUntilReady, unmount } = renderResult;
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).toContain(chalk.inverse(userInput));
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
@@ -91,7 +93,7 @@ describe('ExpandableText', () => {
const suffix = '/and/then/some/more/components/'.repeat(3);
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label={label}
userInput={core}
@@ -101,13 +103,14 @@ describe('ExpandableText', () => {
/>,
100,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const out = lastFrame();
const f = flat(out);
expect(f.includes(core)).toBe(true);
expect(f.startsWith('...')).toBe(true);
expect(f.endsWith('...')).toBe(true);
expect(out).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
@@ -117,7 +120,7 @@ describe('ExpandableText', () => {
const suffix = ' in this text';
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label={label}
userInput={core}
@@ -126,6 +129,7 @@ describe('ExpandableText', () => {
isExpanded={false}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const out = lastFrame();
const f = flat(out);
@@ -133,14 +137,14 @@ describe('ExpandableText', () => {
expect(f.startsWith('...')).toBe(false);
expect(f.endsWith('...')).toBe(true);
expect(f.length).toBe(MAX_WIDTH + 2);
expect(out).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
it('respects custom maxWidth', async () => {
const customWidth = 50;
const long = 'z'.repeat(100);
const { lastFrame, waitUntilReady, unmount } = render(
const renderResult = render(
<ExpandableText
label={long}
userInput=""
@@ -149,12 +153,13 @@ describe('ExpandableText', () => {
maxWidth={customWidth}
/>,
);
const { lastFrame, waitUntilReady, unmount } = renderResult;
await waitUntilReady();
const out = lastFrame();
const f = flat(out);
expect(f.endsWith('...')).toBe(true);
expect(f.length).toBe(customWidth + 3);
expect(out).toMatchSnapshot();
await expect(renderResult).toMatchSvgSnapshot();
unmount();
});
});
@@ -11,7 +11,7 @@ exports[`<EnumSelector /> > renders with numeric options and matches snapshot 1`
`;
exports[`<EnumSelector /> > renders with single option and matches snapshot 1`] = `
" Only Option
" Only Option
"
`;
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="54" viewBox="0 0 920 54">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="54" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#e5e5e5" textLength="621" lengthAdjust="spacingAndGlyphs">...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/</text>
<rect x="621" y="0" width="99" height="17" fill="#e5e5e5" />
<text x="621" y="2" fill="#000000" textLength="99" lengthAdjust="spacingAndGlyphs">search-here</text>
<text x="720" y="2" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs">/and/then/some/more/</text>
<text x="0" y="19" fill="#e5e5e5" textLength="450" lengthAdjust="spacingAndGlyphs">components//and/then/some/more/components//and/...</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 935 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="37" viewBox="0 0 920 37">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="37" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#e5e5e5" textLength="81" lengthAdjust="spacingAndGlyphs">run: git </text>
<rect x="81" y="0" width="54" height="17" fill="#e5e5e5" />
<text x="81" y="2" fill="#000000" textLength="54" lengthAdjust="spacingAndGlyphs">commit</text>
<text x="135" y="2" fill="#e5e5e5" textLength="198" lengthAdjust="spacingAndGlyphs"> -m &quot;feat: add search&quot;</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 734 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="37" viewBox="0 0 920 37">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="37" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#e5e5e5" textLength="126" lengthAdjust="spacingAndGlyphs">simple command</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 448 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="37" viewBox="0 0 920 37">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="37" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#e5e5e5" textLength="477" lengthAdjust="spacingAndGlyphs">zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 487 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="54" viewBox="0 0 920 54">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="54" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#e5e5e5" textLength="900" lengthAdjust="spacingAndGlyphs">yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</text>
<text x="0" y="19" fill="#e5e5e5" textLength="675" lengthAdjust="spacingAndGlyphs">yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 704 B

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="54" viewBox="0 0 920 54">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="54" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#e5e5e5" textLength="900" lengthAdjust="spacingAndGlyphs">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</text>
<text x="0" y="19" fill="#e5e5e5" textLength="477" lengthAdjust="spacingAndGlyphs">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 682 B

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="54" viewBox="0 0 920 54">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="54" fill="#000000" />
<g transform="translate(10, 10)">
<rect x="0" y="0" width="900" height="17" fill="#e5e5e5" />
<text x="0" y="2" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</text>
<rect x="0" y="17" width="468" height="17" fill="#e5e5e5" />
<text x="0" y="19" fill="#000000" textLength="468" lengthAdjust="spacingAndGlyphs">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 810 B

@@ -2,39 +2,26 @@
exports[`ExpandableText > creates centered window around match when collapsed 1`] = `
"...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/search-here/and/then/some/more/
components//and/then/some/more/components//and/...
"
components//and/then/some/more/components//and/..."
`;
exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = `
"run: git commit -m "feat: add search"
"
`;
exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = `"run: git commit -m "feat: add search""`;
exports[`ExpandableText > renders plain label when no match (short label) 1`] = `
"simple command
"
`;
exports[`ExpandableText > renders plain label when no match (short label) 1`] = `"simple command"`;
exports[`ExpandableText > respects custom maxWidth 1`] = `
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz...
"
`;
exports[`ExpandableText > respects custom maxWidth 1`] = `"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."`;
exports[`ExpandableText > shows full long label when expanded and no match 1`] = `
"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
"
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
`;
exports[`ExpandableText > truncates long label when collapsed and no match 1`] = `
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
"
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
`;
exports[`ExpandableText > truncates match itself when match is very long 1`] = `
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...
"
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
`;
@@ -2,7 +2,7 @@
exports[`<HalfLinePaddedBox /> > renders iTerm2-specific blocks when iTerm2 is detected 1`] = `
"▄▄▄▄▄▄▄▄▄▄
Content
Content
▀▀▀▀▀▀▀▀▀▀
"
`;
@@ -19,7 +19,7 @@ exports[`<HalfLinePaddedBox /> > renders nothing when useBackgroundColor is fals
exports[`<HalfLinePaddedBox /> > renders standard background and blocks when not iTerm2 1`] = `
"▀▀▀▀▀▀▀▀▀▀
Content
Content
▄▄▄▄▄▄▄▄▄▄
"
`;
@@ -7,7 +7,7 @@ exports[`SearchableList > should match snapshot 1`] = `
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
● Item One
● Item One
Description for item one
Item Two
@@ -28,7 +28,7 @@ exports[`SearchableList > should reset selection to top when items change if res
Item One
Description for item one
● Item Two
● Item Two
Description for item two
Item Three
@@ -43,7 +43,7 @@ exports[`SearchableList > should reset selection to top when items change if res
│ One │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
● Item One
● Item One
Description for item one
"
`;
@@ -55,7 +55,7 @@ exports[`SearchableList > should reset selection to top when items change if res
│ Search... │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
● Item One
● Item One
Description for item one
Item Two
@@ -20,6 +20,7 @@ import type { SessionInfo } from '../../utils/sessionUtils.js';
import { type NewAgentsChoice } from '../components/NewAgentsNotification.js';
export interface UIActions {
setIsOnboardingForeverMode: (value: boolean) => void;
handleThemeSelect: (
themeName: string,
scope: LoadableSettingScope,
@@ -65,6 +65,7 @@ export interface QuotaState {
}
export interface UIState {
isOnboardingForeverMode: boolean;
history: HistoryItem[];
historyManager: UseHistoryManagerReturn;
isThemeDialogOpen: boolean;
@@ -189,6 +190,7 @@ export interface UIState {
text: string;
type: TransientMessageType;
} | null;
sisyphusSecondsRemaining: number | null;
}
export const UIStateContext = createContext<UIState | null>(null);
@@ -279,8 +279,14 @@ describe('useGeminiStream', () => {
})),
getIdeMode: vi.fn(() => false),
getEnableHooks: vi.fn(() => false),
getIsForeverMode: vi.fn(() => false),
getIsForeverModeConfigured: vi.fn(() => false),
getSisyphusMode: vi.fn(() => ({
enabled: false,
idleTimeout: 1,
prompt: 'continue workflow',
})),
} as unknown as Config;
beforeEach(() => {
vi.clearAllMocks(); // Clear mocks before each test
mockAddItem = vi.fn();
+182 -5
View File
@@ -37,6 +37,9 @@ import {
buildUserSteeringHintPrompt,
generateSteeringAckMessage,
getPlanModeExitMessage,
CompressionStatus,
SCHEDULE_WORK_TOOL_NAME,
CONFUCIUS_PROMPT,
} from '@google/gemini-cli-core';
import type {
Config,
@@ -220,6 +223,28 @@ export const useGeminiStream = (
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
useStateAndRef<boolean>(true);
const processedMemoryToolsRef = useRef<Set<string>>(new Set());
// Sisyphus Mode States
const activeSisyphusScheduleRef = useRef<{
breakTime?: number;
prompt?: string;
isExplicitSchedule?: boolean;
} | null>(null);
const sisyphusTargetTimestampRef = useRef<number | null>(null);
const [sisyphusSecondsRemaining, setSisyphusSecondsRemaining] = useState<
number | null
>(null);
const [, setSisyphusTick] = useState<number>(0);
const submitQueryRef = useRef<
(
query: PartListUnion,
options?: { isContinuation: boolean },
prompt_id?: string,
) => Promise<void>
>(() => Promise.resolve());
const pendingQueryRef = useRef<PartListUnion | null>(null);
const hasForcedConfuciusRef = useRef<boolean>(false);
const { startNewPrompt, getPromptCount } = useSessionStats();
const storage = config.storage;
const logger = useLogger(storage);
@@ -996,17 +1021,34 @@ export const useGeminiStream = (
eventValue: ServerGeminiChatCompressedEvent['value'],
userMessageTimestamp: number,
) => {
// Reset the force flag so Confucius can trigger again before the NEXT compression cycle
hasForcedConfuciusRef.current = false;
if (pendingHistoryItemRef.current) {
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
const isArchived =
eventValue?.compressionStatus === CompressionStatus.ARCHIVED;
const archivePath = eventValue?.archivePath;
let text =
`IMPORTANT: This conversation exceeded the compress threshold. ` +
`A compressed context will be sent for future messages (compressed from: ` +
`${eventValue?.originalTokenCount ?? 'unknown'} to ` +
`${eventValue?.newTokenCount ?? 'unknown'} tokens).`;
if (isArchived && archivePath) {
text =
`IMPORTANT: This conversation exceeded the compress threshold. ` +
`History has been archived to: ${archivePath} (compressed from: ` +
`${eventValue?.originalTokenCount ?? 'unknown'} to ` +
`${eventValue?.newTokenCount ?? 'unknown'} tokens).`;
}
return addItem({
type: 'info',
text:
`IMPORTANT: This conversation exceeded the compress threshold. ` +
`A compressed context will be sent for future messages (compressed from: ` +
`${eventValue?.originalTokenCount ?? 'unknown'} to ` +
`${eventValue?.newTokenCount ?? 'unknown'} tokens).`,
text,
});
},
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
@@ -1160,6 +1202,17 @@ export const useGeminiStream = (
);
break;
case ServerGeminiEventType.ToolCallRequest:
if (event.value.name === SCHEDULE_WORK_TOOL_NAME) {
const args = event.value.args;
const inMinutes = Number(args?.['inMinutes'] ?? 0);
activeSisyphusScheduleRef.current = {
breakTime: inMinutes,
isExplicitSchedule: true,
};
setSisyphusSecondsRemaining(inMinutes * 60);
// Do NOT intercept and manually resolve it here.
// Push it to toolCallRequests so it is executed properly by the backend tool registry.
}
toolCallRequests.push(event.value);
break;
case ServerGeminiEventType.UserCancelled:
@@ -1277,6 +1330,10 @@ export const useGeminiStream = (
const userMessageTimestamp = Date.now();
// Reset Sisyphus timer on any activity but preserve the active schedule override if it exists
setSisyphusSecondsRemaining(null);
sisyphusTargetTimestampRef.current = null;
// Reset quota error flag when starting a new query (not a continuation)
if (!options?.isContinuation) {
setModelSwitchedFromQuotaError(false);
@@ -1290,6 +1347,24 @@ export const useGeminiStream = (
if (!prompt_id) {
prompt_id = config.getSessionId() + '########' + getPromptCount();
}
if (config.getIsForeverMode() && query !== CONFUCIUS_PROMPT) {
const currentTokens = geminiClient
.getChat()
.getLastPromptTokenCount();
const threshold = (await config.getCompressionThreshold()) ?? 0.8;
const limit = tokenLimit(config.getActiveModel());
if (
currentTokens >= limit * threshold * 0.9 &&
!hasForcedConfuciusRef.current
) {
hasForcedConfuciusRef.current = true;
pendingQueryRef.current = query;
query = CONFUCIUS_PROMPT;
}
}
return promptIdContext.run(prompt_id, async () => {
const { queryToSend, shouldProceed } = await prepareQueryForGemini(
query,
@@ -1350,6 +1425,7 @@ export const useGeminiStream = (
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
setPendingHistoryItem(null);
}
if (loopDetectedRef.current) {
loopDetectedRef.current = false;
// Show the confirmation dialog to choose whether to disable loop detection
@@ -1772,6 +1848,106 @@ export const useGeminiStream = (
storage,
]);
// Handle Sisyphus countdown and automatic trigger
useEffect(() => {
submitQueryRef.current = submitQuery;
}, [submitQuery]);
// Handle Sisyphus activation and automatic trigger
useEffect(() => {
const sisyphusSettings = config.getSisyphusMode();
const isExplicitlyScheduled =
activeSisyphusScheduleRef.current?.isExplicitSchedule;
if (!sisyphusSettings.enabled && !isExplicitlyScheduled) {
setSisyphusSecondsRemaining(null);
sisyphusTargetTimestampRef.current = null;
activeSisyphusScheduleRef.current = null;
return;
}
if (streamingState !== StreamingState.Idle) {
setSisyphusSecondsRemaining(null);
sisyphusTargetTimestampRef.current = null;
return;
}
// Now we are IDLE. If no target is set, set one.
if (sisyphusTargetTimestampRef.current === null) {
if (
!activeSisyphusScheduleRef.current &&
sisyphusSettings.idleTimeout !== undefined
) {
activeSisyphusScheduleRef.current = {
breakTime: sisyphusSettings.idleTimeout,
prompt: sisyphusSettings.prompt,
};
}
if (activeSisyphusScheduleRef.current?.breakTime !== undefined) {
const delayMs = activeSisyphusScheduleRef.current.breakTime * 60 * 1000;
sisyphusTargetTimestampRef.current = Date.now() + delayMs;
setSisyphusSecondsRemaining(Math.ceil(delayMs / 1000));
}
}
if (
streamingState === StreamingState.Idle &&
sisyphusSecondsRemaining !== null &&
sisyphusSecondsRemaining <= 0
) {
const isExplicitSchedule =
activeSisyphusScheduleRef.current?.isExplicitSchedule;
const promptToUse = isExplicitSchedule
? 'System: The scheduled break has ended. Please resume your work.'
: (activeSisyphusScheduleRef.current?.prompt ??
sisyphusSettings.prompt ??
'continue workflow');
// Clear for next time so it reverts to default
activeSisyphusScheduleRef.current = null;
sisyphusTargetTimestampRef.current = null;
setSisyphusSecondsRemaining(null);
void submitQueryRef.current(promptToUse);
}
}, [streamingState, sisyphusSecondsRemaining, config]);
// Handle Sisyphus countdown timers independently to ensure UI updates
const isTimerActive =
(streamingState === StreamingState.Idle &&
sisyphusTargetTimestampRef.current !== null) ||
config.getSisyphusMode().enabled ||
activeSisyphusScheduleRef.current?.isExplicitSchedule;
useEffect(() => {
if (streamingState === StreamingState.Idle && pendingQueryRef.current) {
const queryToRun = pendingQueryRef.current;
pendingQueryRef.current = null;
void submitQueryRef.current(queryToRun);
}
}, [streamingState]);
useEffect(() => {
if (!isTimerActive) {
return;
}
const updateTimer = () => {
// Sisyphus countdown
if (sisyphusTargetTimestampRef.current !== null) {
const remainingMs = sisyphusTargetTimestampRef.current - Date.now();
const remainingSecs = Math.max(0, Math.ceil(remainingMs / 1000));
setSisyphusSecondsRemaining(remainingSecs);
}
setSisyphusTick((t) => t + 1); // Force a re-render
};
const timer = setInterval(updateTimer, 100); // Update frequently for high responsiveness
return () => clearInterval(timer);
}, [isTimerActive, config]);
const lastOutputTime = Math.max(
lastToolOutputTime,
lastShellOutputTime,
@@ -1797,5 +1973,6 @@ export const useGeminiStream = (
backgroundShells,
dismissBackgroundShell,
retryStatus,
sisyphusSecondsRemaining,
};
};
@@ -337,7 +337,7 @@ describe('usePhraseCycler', () => {
await act(async () => {
setStateExternally?.({
isActive: true,
customPhrases: [],
customPhrases: [] as string[],
});
});
await waitUntilReady();
+1
View File
@@ -119,6 +119,7 @@ export interface CompressionProps {
originalTokenCount: number | null;
newTokenCount: number | null;
compressionStatus: CompressionStatus | null;
archivePath?: string;
}
/**
@@ -235,7 +235,7 @@ Another paragraph.
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).not.toContain(' 1 ');
expect(lastFrame()).not.toContain('1 const x = 1;');
unmount();
});
@@ -246,7 +246,7 @@ Another paragraph.
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).toContain(' 1 ');
expect(lastFrame()).toContain('1 const x = 1;');
unmount();
});
});
+2
View File
@@ -23,6 +23,7 @@ export enum AppEvent {
PasteTimeout = 'paste-timeout',
TerminalBackground = 'terminal-background',
TransientMessage = 'transient-message',
ExternalMessage = 'external-message',
}
export interface AppEvents {
@@ -32,6 +33,7 @@ export interface AppEvents {
[AppEvent.PasteTimeout]: never[];
[AppEvent.TerminalBackground]: [string];
[AppEvent.TransientMessage]: [TransientMessagePayload];
[AppEvent.ExternalMessage]: [string];
}
export const appEvents = new EventEmitter<AppEvents>();
@@ -1335,6 +1335,8 @@ function toAcpToolKind(kind: Kind): acp.ToolKind {
case Kind.SwitchMode:
case Kind.Other:
return kind as acp.ToolKind;
case Kind.Agent:
return 'think';
case Kind.Plan:
case Kind.Communicate:
default:

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