From 8b3aeb4550f4750db282cbf82de26f574ed0495c Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Wed, 11 Feb 2026 11:08:15 +0800 Subject: [PATCH 1/6] refactor: unify sandbox configuration naming and improve telemetry config Co-authored-by: Qwen-Coder --- .gcp/release-docker.yml | 8 +-- .vscode/launch.json | 6 +- docs/developers/examples/proxy-script.md | 4 +- docs/developers/tools/sandbox.md | 2 +- docs/users/configuration/settings.md | 34 +++++------ docs/users/features/sandbox.md | 18 +++--- integration-tests/acp-integration.test.ts | 4 +- integration-tests/globalSetup.ts | 2 +- integration-tests/run_shell_command.test.ts | 2 +- integration-tests/test-helper.ts | 8 +-- package.json | 14 ++--- packages/cli/src/config/config.test.ts | 56 +++++++++---------- packages/cli/src/config/sandboxConfig.ts | 10 ++-- .../cli/src/config/trustedFolders.test.ts | 6 +- packages/cli/src/config/trustedFolders.ts | 4 +- packages/cli/src/gemini.test.tsx | 8 +-- .../utils/sandbox-macos-permissive-proxied.sb | 2 +- .../sandbox-macos-restrictive-proxied.sb | 2 +- packages/cli/src/utils/sandbox.ts | 24 ++++---- packages/core/src/telemetry/config.test.ts | 18 +++--- packages/core/src/telemetry/config.ts | 16 +++--- scripts/build_sandbox.js | 2 +- scripts/sandbox_command.js | 30 +++++----- scripts/telemetry_utils.js | 4 +- 24 files changed, 141 insertions(+), 143 deletions(-) diff --git a/.gcp/release-docker.yml b/.gcp/release-docker.yml index 53e78b088..57f9e5880 100644 --- a/.gcp/release-docker.yml +++ b/.gcp/release-docker.yml @@ -42,11 +42,11 @@ steps: args: - '-c' - |- - export GEMINI_SANDBOX_IMAGE_TAG=$$(cat /workspace/image_tag.txt) - echo "Using Docker image tag for build: $$GEMINI_SANDBOX_IMAGE_TAG" + export QWEN_SANDBOX_IMAGE_TAG=$$(cat /workspace/image_tag.txt) + echo "Using Docker image tag for build: $$QWEN_SANDBOX_IMAGE_TAG" npm run build:sandbox -- --output-file /workspace/final_image_uri.txt env: - - 'GEMINI_SANDBOX=$_CONTAINER_TOOL' + - 'QWEN_SANDBOX=$_CONTAINER_TOOL' # Step 8: Publish sandbox container image - name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder' @@ -61,7 +61,7 @@ steps: echo "Pushing sandbox image: $${FINAL_IMAGE_URI}" $_CONTAINER_TOOL push "$${FINAL_IMAGE_URI}" env: - - 'GEMINI_SANDBOX=$_CONTAINER_TOOL' + - 'QWEN_SANDBOX=$_CONTAINER_TOOL' options: defaultLogsBucketBehavior: 'REGIONAL_USER_OWNED_BUCKET' diff --git a/.vscode/launch.json b/.vscode/launch.json index bab4f22e0..66a80fd90 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "cwd": "${workspaceFolder}", "console": "integratedTerminal", "env": { - "GEMINI_SANDBOX": "false" + "QWEN_SANDBOX": "false" } }, { @@ -86,7 +86,7 @@ "cwd": "${workspaceFolder}", "console": "integratedTerminal", "env": { - "GEMINI_SANDBOX": "false" + "QWEN_SANDBOX": "false" } }, { @@ -107,7 +107,7 @@ "internalConsoleOptions": "neverOpen", "skipFiles": ["/**"], "env": { - "GEMINI_SANDBOX": "false" + "QWEN_SANDBOX": "false" } }, { diff --git a/docs/developers/examples/proxy-script.md b/docs/developers/examples/proxy-script.md index 78299001b..cb8cfffd6 100644 --- a/docs/developers/examples/proxy-script.md +++ b/docs/developers/examples/proxy-script.md @@ -1,6 +1,6 @@ # Example Proxy Script -The following is an example of a proxy script that can be used with the `GEMINI_SANDBOX_PROXY_COMMAND` environment variable. This script only allows `HTTPS` connections to `example.com:443` and declines all other requests. +The following is an example of a proxy script that can be used with the `QWEN_SANDBOX_PROXY_COMMAND` environment variable. This script only allows `HTTPS` connections to `example.com:443` and declines all other requests. ```javascript #!/usr/bin/env node @@ -12,7 +12,7 @@ The following is an example of a proxy script that can be used with the `GEMINI_ */ // Example proxy server that listens on :::8877 and only allows HTTPS connections to example.com. -// Set `GEMINI_SANDBOX_PROXY_COMMAND=scripts/example-proxy.js` to run proxy alongside sandbox +// Set `QWEN_SANDBOX_PROXY_COMMAND=scripts/example-proxy.js` to run proxy alongside sandbox // Test via `curl https://example.com` inside sandbox (in shell mode or via shell tool) import http from 'node:http'; diff --git a/docs/developers/tools/sandbox.md b/docs/developers/tools/sandbox.md index 92550f164..38f81f6b2 100644 --- a/docs/developers/tools/sandbox.md +++ b/docs/developers/tools/sandbox.md @@ -59,7 +59,7 @@ RUN apt-get update && apt-get install -y \ #### 4、Create the first sandbox image under the root directory of your project ```bash -GEMINI_SANDBOX=docker BUILD_SANDBOX=1 qwen -s +QWEN_SANDBOX=docker BUILD_SANDBOX=1 qwen -s # Observe whether the sandbox version of the tool you launched is consistent with the version of your custom image. If they are consistent, the startup will be successful ``` diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 0094f411d..25d709b50 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -481,22 +481,22 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe ### Environment Variables Table -| Variable | Description | Notes | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `GEMINI_TELEMETRY_ENABLED` | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. | Overrides the `telemetry.enabled` setting. | -| `GEMINI_TELEMETRY_TARGET` | Sets the telemetry target (`local` or `gcp`). | Overrides the `telemetry.target` setting. | -| `GEMINI_TELEMETRY_OTLP_ENDPOINT` | Sets the OTLP endpoint for telemetry. | Overrides the `telemetry.otlpEndpoint` setting. | -| `GEMINI_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. | -| `GEMINI_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. | -| `GEMINI_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to when the target is `local`. | Overrides the `telemetry.outfile` setting. | -| `GEMINI_TELEMETRY_USE_COLLECTOR` | Set to `true` or `1` to enable or disable using an external OTLP collector. Any other value is treated as disabling it. | Overrides the `telemetry.useCollector` setting. | -| `GEMINI_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. | -| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. ``: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). | -| `DEBUG` or `DEBUG_MODE` | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting. | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically. | -| `NO_COLOR` | Set to any value to disable all color output in the CLI. | | -| `CLI_TITLE` | Set to a string to customize the title of the CLI. | | -| `CODE_ASSIST_ENDPOINT` | Specifies the endpoint for the code assist server. | This is useful for development and testing. | -| `TAVILY_API_KEY` | Your API key for the Tavily web search service. | Used to enable the `web_search` tool functionality. Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` | +| Variable | Description | Notes | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `QWEN_TELEMETRY_ENABLED` | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. | Overrides the `telemetry.enabled` setting. | +| `QWEN_TELEMETRY_TARGET` | Sets the telemetry target (`local` or `gcp`). | Overrides the `telemetry.target` setting. | +| `QWEN_TELEMETRY_OTLP_ENDPOINT` | Sets the OTLP endpoint for telemetry. | Overrides the `telemetry.otlpEndpoint` setting. | +| `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. | +| `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. | +| `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to when the target is `local`. | Overrides the `telemetry.outfile` setting. | +| `QWEN_TELEMETRY_USE_COLLECTOR` | Set to `true` or `1` to enable or disable using an external OTLP collector. Any other value is treated as disabling it. | Overrides the `telemetry.useCollector` setting. | +| `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. | +| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. ``: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). | +| `DEBUG` or `DEBUG_MODE` | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting. | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically. | +| `NO_COLOR` | Set to any value to disable all color output in the CLI. | | +| `CLI_TITLE` | Set to a string to customize the title of the CLI. | | +| `CODE_ASSIST_ENDPOINT` | Specifies the endpoint for the code assist server. | This is useful for development and testing. | +| `TAVILY_API_KEY` | Your API key for the Tavily web search service. | Used to enable the `web_search` tool functionality. Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` | ## Command-Line Arguments @@ -599,7 +599,7 @@ Qwen Code can execute potentially unsafe operations (like shell commands and fil [Sandbox](../features/sandbox) is disabled by default, but you can enable it in a few ways: - Using `--sandbox` or `-s` flag. -- Setting `GEMINI_SANDBOX` environment variable. +- Setting `QWEN_SANDBOX` environment variable. - Sandbox is enabled when using `--yolo` or `--approval-mode=yolo` by default. By default, it uses a pre-built `qwen-code-sandbox` Docker image. diff --git a/docs/users/features/sandbox.md b/docs/users/features/sandbox.md index 23ea89fe7..72005f959 100644 --- a/docs/users/features/sandbox.md +++ b/docs/users/features/sandbox.md @@ -29,7 +29,7 @@ The benefits of sandboxing include: > [!note] > -> **Naming note:** Some sandbox-related environment variables still use the `GEMINI_*` prefix for backwards compatibility. +> **Naming note:** Some sandbox-related environment variables may have used the `GEMINI_*` prefix historically. All new environment variables use the `QWEN_*` prefix. ## Sandboxing methods @@ -68,7 +68,7 @@ The container sandbox mounts your workspace and your `~/.qwen` directory into th qwen -s -p "analyze the code structure" # Or enable sandboxing for your shell session (recommended for CI / scripts) -export GEMINI_SANDBOX=true # true auto-picks a provider (see notes below) +export QWEN_SANDBOX=true # true auto-picks a provider (see notes below) qwen -p "run the test suite" # Configure in settings.json @@ -83,26 +83,26 @@ qwen -p "run the test suite" > > **Provider selection notes:** > -> - On **macOS**, `GEMINI_SANDBOX=true` typically selects `sandbox-exec` (Seatbelt) if available. -> - On **Linux/Windows**, `GEMINI_SANDBOX=true` requires `docker` or `podman` to be installed. -> - To force a provider, set `GEMINI_SANDBOX=docker|podman|sandbox-exec`. +> - On **macOS**, `QWEN_SANDBOX=true` typically selects `sandbox-exec` (Seatbelt) if available. +> - On **Linux/Windows**, `QWEN_SANDBOX=true` requires `docker` or `podman` to be installed. +> - To force a provider, set `QWEN_SANDBOX=docker|podman|sandbox-exec`. ## Configuration ### Enable sandboxing (in order of precedence) -1. **Environment variable**: `GEMINI_SANDBOX=true|false|docker|podman|sandbox-exec` +1. **Environment variable**: `QWEN_SANDBOX=true|false|docker|podman|sandbox-exec` 2. **Command flag / argument**: `-s`, `--sandbox`, or `--sandbox=` 3. **Settings file**: `tools.sandbox` in your `settings.json` (e.g., `{"tools": {"sandbox": true}}`). > [!important] > -> If `GEMINI_SANDBOX` is set, it **overrides** the CLI flag and `settings.json`. +> If `QWEN_SANDBOX` is set, it **overrides** the CLI flag and `settings.json`. ### Configure the sandbox image (Docker/Podman) - **CLI flag**: `--sandbox-image ` -- **Environment variable**: `GEMINI_SANDBOX_IMAGE=` +- **Environment variable**: `QWEN_SANDBOX_IMAGE=` If you don’t set either, Qwen Code uses the default image configured in the CLI package (for example `ghcr.io/qwenlm/qwen-code:`). @@ -150,7 +150,7 @@ export SANDBOX_FLAGS="--flag1 --flag2=value" If you want to restrict outbound network access to an allowlist, you can run a local proxy alongside the sandbox: -- Set `GEMINI_SANDBOX_PROXY_COMMAND=` +- Set `QWEN_SANDBOX_PROXY_COMMAND=` - The command must start a proxy server that listens on `:::8877` This is especially useful with `*-proxied` Seatbelt profiles. diff --git a/integration-tests/acp-integration.test.ts b/integration-tests/acp-integration.test.ts index 35397da26..448095fc8 100644 --- a/integration-tests/acp-integration.test.ts +++ b/integration-tests/acp-integration.test.ts @@ -14,8 +14,8 @@ import { TestRig } from './test-helper.js'; const REQUEST_TIMEOUT_MS = 60_000; const INITIAL_PROMPT = 'Create a quick note (smoke test).'; const IS_SANDBOX = - process.env['GEMINI_SANDBOX'] && - process.env['GEMINI_SANDBOX']!.toLowerCase() !== 'false'; + process.env['QWEN_SANDBOX'] && + process.env['QWEN_SANDBOX']!.toLowerCase() !== 'false'; type PendingRequest = { resolve: (value: unknown) => void; diff --git a/integration-tests/globalSetup.ts b/integration-tests/globalSetup.ts index a8a9877fe..02cea6859 100644 --- a/integration-tests/globalSetup.ts +++ b/integration-tests/globalSetup.ts @@ -94,7 +94,7 @@ export async function setup() { // Environment variables for CLI integration tests process.env['INTEGRATION_TEST_FILE_DIR'] = runDir; - process.env['GEMINI_CLI_INTEGRATION_TEST'] = 'true'; + process.env['QWEN_CODE_INTEGRATION_TEST'] = 'true'; process.env['TELEMETRY_LOG_FILE'] = join(runDir, 'telemetry.log'); // Environment variables for SDK E2E tests diff --git a/integration-tests/run_shell_command.test.ts b/integration-tests/run_shell_command.test.ts index cba8cb72a..4b0b99677 100644 --- a/integration-tests/run_shell_command.test.ts +++ b/integration-tests/run_shell_command.test.ts @@ -72,7 +72,7 @@ describe('run_shell_command', () => { const rig = new TestRig(); await rig.setup('should propagate environment variables'); - const varName = 'GEMINI_CLI_TEST_VAR'; + const varName = 'QWEN_CODE_TEST_VAR'; const varValue = `test-value-${Math.random().toString(36).substring(7)}`; process.env[varName] = varValue; diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts index a08b3df50..00cf77de4 100644 --- a/integration-tests/test-helper.ts +++ b/integration-tests/test-helper.ts @@ -154,7 +154,7 @@ export class TestRig { // Get timeout based on environment getDefaultTimeout() { if (env['CI']) return 60000; // 1 minute in CI - if (env['GEMINI_SANDBOX']) return 30000; // 30s in containers + if (env['QWEN_SANDBOX']) return 30000; // 30s in containers return 15000; // 15s locally } @@ -181,7 +181,7 @@ export class TestRig { otlpEndpoint: '', outfile: telemetryPath, }, - sandbox: env.GEMINI_SANDBOX !== 'false' ? env.GEMINI_SANDBOX : false, + sandbox: env.QWEN_SANDBOX !== 'false' ? env.QWEN_SANDBOX : false, ...options.settings, // Allow tests to override/add settings }; writeFileSync( @@ -301,7 +301,7 @@ export class TestRig { // Filter out telemetry output when running with Podman // Podman seems to output telemetry to stdout even when writing to file let result = stdout; - if (env['GEMINI_SANDBOX'] === 'podman') { + if (env['QWEN_SANDBOX'] === 'podman') { // Remove telemetry JSON objects from output // They are multi-line JSON objects that start with { and contain telemetry fields const lines = result.split(EOL); @@ -727,7 +727,7 @@ export class TestRig { readToolLogs() { // For Podman, first check if telemetry file exists and has content // If not, fall back to parsing from stdout - if (env['GEMINI_SANDBOX'] === 'podman') { + if (env['QWEN_SANDBOX'] === 'podman') { // Try reading from file first const logFilePath = join(this.testDir!, 'telemetry.log'); diff --git a/package.json b/package.json index 374dd32c6..8042af6d7 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,13 @@ "test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts", "test:e2e": "cross-env VERBOSE=true KEEP_OUTPUT=true npm run test:integration:sandbox:none", "test:integration:all": "npm run test:integration:sandbox:none && npm run test:integration:sandbox:docker && npm run test:integration:sandbox:podman", - "test:integration:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests", - "test:integration:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && GEMINI_SANDBOX=docker vitest run --root ./integration-tests", - "test:integration:sandbox:podman": "cross-env GEMINI_SANDBOX=podman vitest run --root ./integration-tests", - "test:integration:sdk:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests sdk-typescript", - "test:integration:sdk:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && GEMINI_SANDBOX=docker vitest run --root ./integration-tests sdk-typescript", - "test:integration:cli:sandbox:none": "cross-env GEMINI_SANDBOX=false vitest run --root ./integration-tests --exclude '**/sdk-typescript/**'", - "test:integration:cli:sandbox:docker": "cross-env GEMINI_SANDBOX=docker npm run build:sandbox && GEMINI_SANDBOX=docker vitest run --root ./integration-tests --exclude '**/sdk-typescript/**'", + "test:integration:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests", + "test:integration:sandbox:docker": "cross-env QWEN_SANDBOX=docker npm run build:sandbox && QWEN_SANDBOX=docker vitest run --root ./integration-tests", + "test:integration:sandbox:podman": "cross-env QWEN_SANDBOX=podman vitest run --root ./integration-tests", + "test:integration:sdk:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests sdk-typescript", + "test:integration:sdk:sandbox:docker": "cross-env QWEN_SANDBOX=docker npm run build:sandbox && QWEN_SANDBOX=docker vitest run --root ./integration-tests sdk-typescript", + "test:integration:cli:sandbox:none": "cross-env QWEN_SANDBOX=false vitest run --root ./integration-tests --exclude '**/sdk-typescript/**'", + "test:integration:cli:sandbox:docker": "cross-env QWEN_SANDBOX=docker npm run build:sandbox && QWEN_SANDBOX=docker vitest run --root ./integration-tests --exclude '**/sdk-typescript/**'", "test:terminal-bench": "cross-env VERBOSE=true KEEP_OUTPUT=true vitest run --config ./vitest.terminal-bench.config.ts --root ./integration-tests", "test:terminal-bench:oracle": "cross-env VERBOSE=true KEEP_OUTPUT=true vitest run --config ./vitest.terminal-bench.config.ts --root ./integration-tests -t 'oracle'", "test:terminal-bench:qwen": "cross-env VERBOSE=true KEEP_OUTPUT=true vitest run --config ./vitest.terminal-bench.config.ts --root ./integration-tests -t 'qwen'", diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index dbe57fd42..1d16d45fd 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -2173,8 +2173,8 @@ describe('parseArguments with positional prompt', () => { }); describe('Telemetry configuration via environment variables', () => { - it('should prioritize GEMINI_TELEMETRY_ENABLED over settings', async () => { - vi.stubEnv('GEMINI_TELEMETRY_ENABLED', 'true'); + it('should prioritize QWEN_TELEMETRY_ENABLED over settings', async () => { + vi.stubEnv('QWEN_TELEMETRY_ENABLED', 'true'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: false } }; @@ -2182,8 +2182,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryEnabled()).toBe(true); }); - it('should prioritize GEMINI_TELEMETRY_TARGET over settings', async () => { - vi.stubEnv('GEMINI_TELEMETRY_TARGET', 'gcp'); + it('should prioritize QWEN_TELEMETRY_TARGET over settings', async () => { + vi.stubEnv('QWEN_TELEMETRY_TARGET', 'gcp'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { @@ -2193,8 +2193,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryTarget()).toBe('gcp'); }); - it('should throw when GEMINI_TELEMETRY_TARGET is invalid', async () => { - vi.stubEnv('GEMINI_TELEMETRY_TARGET', 'bogus'); + it('should throw when QWEN_TELEMETRY_TARGET is invalid', async () => { + vi.stubEnv('QWEN_TELEMETRY_TARGET', 'bogus'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { @@ -2206,9 +2206,9 @@ describe('Telemetry configuration via environment variables', () => { vi.unstubAllEnvs(); }); - it('should prioritize GEMINI_TELEMETRY_OTLP_ENDPOINT over settings and default env var', async () => { + it('should prioritize QWEN_TELEMETRY_OTLP_ENDPOINT over settings and default env var', async () => { vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://default.env.com'); - vi.stubEnv('GEMINI_TELEMETRY_OTLP_ENDPOINT', 'http://gemini.env.com'); + vi.stubEnv('QWEN_TELEMETRY_OTLP_ENDPOINT', 'http://gemini.env.com'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { @@ -2218,8 +2218,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryOtlpEndpoint()).toBe('http://gemini.env.com'); }); - it('should prioritize GEMINI_TELEMETRY_OTLP_PROTOCOL over settings', async () => { - vi.stubEnv('GEMINI_TELEMETRY_OTLP_PROTOCOL', 'http'); + it('should prioritize QWEN_TELEMETRY_OTLP_PROTOCOL over settings', async () => { + vi.stubEnv('QWEN_TELEMETRY_OTLP_PROTOCOL', 'http'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { telemetry: { otlpProtocol: 'grpc' } }; @@ -2227,8 +2227,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryOtlpProtocol()).toBe('http'); }); - it('should prioritize GEMINI_TELEMETRY_LOG_PROMPTS over settings', async () => { - vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', 'false'); + it('should prioritize QWEN_TELEMETRY_LOG_PROMPTS over settings', async () => { + vi.stubEnv('QWEN_TELEMETRY_LOG_PROMPTS', 'false'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: true } }; @@ -2236,8 +2236,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); - it('should prioritize GEMINI_TELEMETRY_OUTFILE over settings', async () => { - vi.stubEnv('GEMINI_TELEMETRY_OUTFILE', '/gemini/env/telemetry.log'); + it('should prioritize QWEN_TELEMETRY_OUTFILE over settings', async () => { + vi.stubEnv('QWEN_TELEMETRY_OUTFILE', '/gemini/env/telemetry.log'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { @@ -2247,8 +2247,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryOutfile()).toBe('/gemini/env/telemetry.log'); }); - it('should prioritize GEMINI_TELEMETRY_USE_COLLECTOR over settings', async () => { - vi.stubEnv('GEMINI_TELEMETRY_USE_COLLECTOR', 'true'); + it('should prioritize QWEN_TELEMETRY_USE_COLLECTOR over settings', async () => { + vi.stubEnv('QWEN_TELEMETRY_USE_COLLECTOR', 'true'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { telemetry: { useCollector: false } }; @@ -2256,8 +2256,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryUseCollector()).toBe(true); }); - it('should use settings value when GEMINI_TELEMETRY_ENABLED is not set', async () => { - vi.stubEnv('GEMINI_TELEMETRY_ENABLED', undefined); + it('should use settings value when QWEN_TELEMETRY_ENABLED is not set', async () => { + vi.stubEnv('QWEN_TELEMETRY_ENABLED', undefined); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; @@ -2265,8 +2265,8 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryEnabled()).toBe(true); }); - it('should use settings value when GEMINI_TELEMETRY_TARGET is not set', async () => { - vi.stubEnv('GEMINI_TELEMETRY_TARGET', undefined); + it('should use settings value when QWEN_TELEMETRY_TARGET is not set', async () => { + vi.stubEnv('QWEN_TELEMETRY_TARGET', undefined); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const settings: Settings = { @@ -2276,16 +2276,16 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryTarget()).toBe('local'); }); - it("should treat GEMINI_TELEMETRY_ENABLED='1' as true", async () => { - vi.stubEnv('GEMINI_TELEMETRY_ENABLED', '1'); + it("should treat QWEN_TELEMETRY_ENABLED='1' as true", async () => { + vi.stubEnv('QWEN_TELEMETRY_ENABLED', '1'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const config = await loadCliConfig({}, argv, undefined, []); expect(config.getTelemetryEnabled()).toBe(true); }); - it("should treat GEMINI_TELEMETRY_ENABLED='0' as false", async () => { - vi.stubEnv('GEMINI_TELEMETRY_ENABLED', '0'); + it("should treat QWEN_TELEMETRY_ENABLED='0' as false", async () => { + vi.stubEnv('QWEN_TELEMETRY_ENABLED', '0'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const config = await loadCliConfig( @@ -2297,16 +2297,16 @@ describe('Telemetry configuration via environment variables', () => { expect(config.getTelemetryEnabled()).toBe(false); }); - it("should treat GEMINI_TELEMETRY_LOG_PROMPTS='1' as true", async () => { - vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', '1'); + it("should treat QWEN_TELEMETRY_LOG_PROMPTS='1' as true", async () => { + vi.stubEnv('QWEN_TELEMETRY_LOG_PROMPTS', '1'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const config = await loadCliConfig({}, argv, undefined, []); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); - it("should treat GEMINI_TELEMETRY_LOG_PROMPTS='false' as false", async () => { - vi.stubEnv('GEMINI_TELEMETRY_LOG_PROMPTS', 'false'); + it("should treat QWEN_TELEMETRY_LOG_PROMPTS='false' as false", async () => { + vi.stubEnv('QWEN_TELEMETRY_LOG_PROMPTS', 'false'); process.argv = ['node', 'script.js']; const argv = await parseArguments(); const config = await loadCliConfig( diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index 089fd85f1..f98e528fb 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -38,7 +38,7 @@ function getSandboxCommand( // note environment variable takes precedence over argument (from command line or settings) const environmentConfiguredSandbox = - process.env['GEMINI_SANDBOX']?.toLowerCase().trim() ?? ''; + process.env['QWEN_SANDBOX']?.toLowerCase().trim() ?? ''; sandbox = environmentConfiguredSandbox?.length > 0 ? environmentConfiguredSandbox @@ -63,7 +63,7 @@ function getSandboxCommand( return sandbox; } throw new FatalSandboxError( - `Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`, + `Missing sandbox command '${sandbox}' (from QWEN_SANDBOX)`, ); } @@ -80,8 +80,8 @@ function getSandboxCommand( // throw an error if user requested sandbox but no command was found if (sandbox === true) { throw new FatalSandboxError( - 'GEMINI_SANDBOX is true but failed to determine command for sandbox; ' + - 'install docker or podman or specify command in GEMINI_SANDBOX', + 'QWEN_SANDBOX is true but failed to determine command for sandbox; ' + + 'install docker or podman or specify command in QWEN_SANDBOX', ); } @@ -98,7 +98,7 @@ export async function loadSandboxConfig( const packageJson = await getPackageJson(); const image = argv.sandboxImage ?? - process.env['GEMINI_SANDBOX_IMAGE'] ?? + process.env['QWEN_SANDBOX_IMAGE'] ?? packageJson?.config?.sandboxImageUri; return command && image ? { command, image } : undefined; diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index 9d06dcf31..5b29969c2 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -158,9 +158,9 @@ describe('Trusted Folders Loading', () => { expect(errors[0].message).toContain('Unexpected token'); }); - it('should use GEMINI_CLI_TRUSTED_FOLDERS_PATH env var if set', () => { + it('should use QWEN_CODE_TRUSTED_FOLDERS_PATH env var if set', () => { const customPath = '/custom/path/to/trusted_folders.json'; - process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH'] = customPath; + process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH'] = customPath; (mockFsExistsSync as Mock).mockImplementation((p) => p === customPath); const userContent = { @@ -180,7 +180,7 @@ describe('Trusted Folders Loading', () => { ]); expect(errors).toEqual([]); - delete process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']; + delete process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']; }); it('setValue should update the user config and save it', () => { diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 355146025..1a243e537 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -22,8 +22,8 @@ export const SETTINGS_DIRECTORY_NAME = '.qwen'; export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME); export function getTrustedFoldersPath(): string { - if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) { - return process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']; + if (process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']) { + return process.env['QWEN_CODE_TRUSTED_FOLDERS_PATH']; } return path.join(USER_SETTINGS_DIR, TRUSTED_FOLDERS_FILENAME); } diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 039f0bef3..dfb3aa8a1 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -112,9 +112,9 @@ describe('gemini.tsx main function', () => { beforeEach(() => { // Store and clear sandbox-related env variables to ensure a consistent test environment - originalEnvGeminiSandbox = process.env['GEMINI_SANDBOX']; + originalEnvGeminiSandbox = process.env['QWEN_SANDBOX']; originalEnvSandbox = process.env['SANDBOX']; - delete process.env['GEMINI_SANDBOX']; + delete process.env['QWEN_SANDBOX']; delete process.env['SANDBOX']; initialUnhandledRejectionListeners = @@ -124,9 +124,9 @@ describe('gemini.tsx main function', () => { afterEach(() => { // Restore original env variables if (originalEnvGeminiSandbox !== undefined) { - process.env['GEMINI_SANDBOX'] = originalEnvGeminiSandbox; + process.env['QWEN_SANDBOX'] = originalEnvGeminiSandbox; } else { - delete process.env['GEMINI_SANDBOX']; + delete process.env['QWEN_SANDBOX']; } if (originalEnvSandbox !== undefined) { process.env['SANDBOX'] = originalEnvSandbox; diff --git a/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb b/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb index 70e1150c9..43f17062c 100644 --- a/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb +++ b/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb @@ -29,7 +29,7 @@ (allow network-inbound (local ip "localhost:9229")) ;; deny all outbound network traffic EXCEPT through proxy on localhost:8877 -;; set `GEMINI_SANDBOX_PROXY_COMMAND=` to run proxy alongside sandbox +;; set `QWEN_SANDBOX_PROXY_COMMAND=` to run proxy alongside sandbox ;; proxy must listen on :::8877 (see docs/examples/proxy-script.md) (deny network-outbound) (allow network-outbound (remote tcp "localhost:8877")) diff --git a/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb b/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb index 8affc94dc..d6a450c25 100644 --- a/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb +++ b/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb @@ -93,6 +93,6 @@ (allow network-inbound (local ip "localhost:9229")) ;; allow outbound network traffic through proxy on localhost:8877 -;; set `GEMINI_SANDBOX_PROXY_COMMAND=` to run proxy alongside sandbox +;; set `QWEN_SANDBOX_PROXY_COMMAND=` to run proxy alongside sandbox ;; proxy must listen on :::8877 (see docs/examples/proxy-script.md) (allow network-outbound (remote tcp "localhost:8877")) diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 23585dd3b..0ee5cf985 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -263,8 +263,8 @@ export async function start_sandbox( ...finalArgv.map((arg) => quote([arg])), ].join(' '), ); - // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set - const proxyCommand = process.env['GEMINI_SANDBOX_PROXY_COMMAND']; + // start and set up proxy if QWEN_SANDBOX_PROXY_COMMAND is set + const proxyCommand = process.env['QWEN_SANDBOX_PROXY_COMMAND']; let proxyProcess: ChildProcess | undefined = undefined; let sandboxProcess: ChildProcess | undefined = undefined; const sandboxEnv = { ...process.env }; @@ -378,7 +378,7 @@ export async function start_sandbox( stdio: 'inherit', env: { ...process.env, - GEMINI_SANDBOX: config.command, // in case sandbox is enabled via flags (see config.ts under cli package) + QWEN_SANDBOX: config.command, // in case sandbox is enabled via flags (see config.ts under cli package) }, }, ); @@ -498,8 +498,8 @@ export async function start_sandbox( // copy proxy environment variables, replacing localhost with SANDBOX_PROXY_NAME // copy as both upper-case and lower-case as is required by some utilities - // GEMINI_SANDBOX_PROXY_COMMAND implies HTTPS_PROXY unless HTTP_PROXY is set - const proxyCommand = process.env['GEMINI_SANDBOX_PROXY_COMMAND']; + // QWEN_SANDBOX_PROXY_COMMAND implies HTTPS_PROXY unless HTTP_PROXY is set + const proxyCommand = process.env['QWEN_SANDBOX_PROXY_COMMAND']; if (proxyCommand) { let proxy = @@ -541,10 +541,10 @@ export async function start_sandbox( // name container after image, plus random suffix to avoid conflicts const imageName = parseImageName(image); const isIntegrationTest = - process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true'; + process.env['QWEN_CODE_INTEGRATION_TEST'] === 'true'; let containerName; if (isIntegrationTest) { - containerName = `gemini-cli-integration-test-${randomBytes(4).toString( + containerName = `qwen-code-integration-test-${randomBytes(4).toString( 'hex', )}`; writeStderrLine(`ContainerName: ${containerName}`); @@ -563,11 +563,11 @@ export async function start_sandbox( } args.push('--name', containerName, '--hostname', containerName); - // copy GEMINI_CLI_TEST_VAR for integration tests - if (process.env['GEMINI_CLI_TEST_VAR']) { + // copy QWEN_CODE_TEST_VAR for integration tests + if (process.env['QWEN_CODE_TEST_VAR']) { args.push( '--env', - `GEMINI_CLI_TEST_VAR=${process.env['GEMINI_CLI_TEST_VAR']}`, + `QWEN_CODE_TEST_VAR=${process.env['QWEN_CODE_TEST_VAR']}`, ); } @@ -716,7 +716,7 @@ export async function start_sandbox( let userFlag = ''; const finalEntrypoint = entrypoint(workdir, cliArgs); - if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') { + if (process.env['QWEN_CODE_INTEGRATION_TEST'] === 'true') { args.push('--user', 'root'); userFlag = '--user root'; } else if (await shouldUseCurrentUserInSandbox()) { @@ -763,7 +763,7 @@ export async function start_sandbox( // push container entrypoint (including args) args.push(...finalEntrypoint); - // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set + // start and set up proxy if QWEN_SANDBOX_PROXY_COMMAND is set let proxyProcess: ChildProcess | undefined = undefined; let sandboxProcess: ChildProcess | undefined = undefined; diff --git a/packages/core/src/telemetry/config.test.ts b/packages/core/src/telemetry/config.test.ts index 1ded8d490..443282fd4 100644 --- a/packages/core/src/telemetry/config.test.ts +++ b/packages/core/src/telemetry/config.test.ts @@ -79,13 +79,13 @@ describe('telemetry/config helpers', () => { useCollector: false, }; const env = { - GEMINI_TELEMETRY_ENABLED: '1', - GEMINI_TELEMETRY_TARGET: 'gcp', - GEMINI_TELEMETRY_OTLP_ENDPOINT: 'http://env:4317', - GEMINI_TELEMETRY_OTLP_PROTOCOL: 'http', - GEMINI_TELEMETRY_LOG_PROMPTS: 'true', - GEMINI_TELEMETRY_OUTFILE: 'env.log', - GEMINI_TELEMETRY_USE_COLLECTOR: 'true', + QWEN_TELEMETRY_ENABLED: '1', + QWEN_TELEMETRY_TARGET: 'gcp', + QWEN_TELEMETRY_OTLP_ENDPOINT: 'http://env:4317', + QWEN_TELEMETRY_OTLP_PROTOCOL: 'http', + QWEN_TELEMETRY_LOG_PROMPTS: 'true', + QWEN_TELEMETRY_OUTFILE: 'env.log', + QWEN_TELEMETRY_USE_COLLECTOR: 'true', } as Record; const argv = { telemetry: false, @@ -133,7 +133,7 @@ describe('telemetry/config helpers', () => { }); it('throws on unknown protocol values', async () => { - const env = { GEMINI_TELEMETRY_OTLP_PROTOCOL: 'unknown' } as Record< + const env = { QWEN_TELEMETRY_OTLP_PROTOCOL: 'unknown' } as Record< string, string >; @@ -143,7 +143,7 @@ describe('telemetry/config helpers', () => { }); it('throws on unknown target values', async () => { - const env = { GEMINI_TELEMETRY_TARGET: 'unknown' } as Record< + const env = { QWEN_TELEMETRY_TARGET: 'unknown' } as Record< string, string >; diff --git a/packages/core/src/telemetry/config.ts b/packages/core/src/telemetry/config.ts index bfca365c8..f1037e742 100644 --- a/packages/core/src/telemetry/config.ts +++ b/packages/core/src/telemetry/config.ts @@ -57,12 +57,12 @@ export async function resolveTelemetrySettings(options: { const enabled = argv.telemetry ?? - parseBooleanEnvFlag(env['GEMINI_TELEMETRY_ENABLED']) ?? + parseBooleanEnvFlag(env['QWEN_TELEMETRY_ENABLED']) ?? settings.enabled; const rawTarget = (argv.telemetryTarget as string | TelemetryTarget | undefined) ?? - env['GEMINI_TELEMETRY_TARGET'] ?? + env['QWEN_TELEMETRY_TARGET'] ?? (settings.target as string | TelemetryTarget | undefined); const target = parseTelemetryTargetValue(rawTarget); if (rawTarget !== undefined && target === undefined) { @@ -75,13 +75,13 @@ export async function resolveTelemetrySettings(options: { const otlpEndpoint = argv.telemetryOtlpEndpoint ?? - env['GEMINI_TELEMETRY_OTLP_ENDPOINT'] ?? + env['QWEN_TELEMETRY_OTLP_ENDPOINT'] ?? env['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? settings.otlpEndpoint; const rawProtocol = (argv.telemetryOtlpProtocol as string | undefined) ?? - env['GEMINI_TELEMETRY_OTLP_PROTOCOL'] ?? + env['QWEN_TELEMETRY_OTLP_PROTOCOL'] ?? settings.otlpProtocol; const otlpProtocol = (['grpc', 'http'] as const).find( (p) => p === rawProtocol, @@ -96,16 +96,14 @@ export async function resolveTelemetrySettings(options: { const logPrompts = argv.telemetryLogPrompts ?? - parseBooleanEnvFlag(env['GEMINI_TELEMETRY_LOG_PROMPTS']) ?? + parseBooleanEnvFlag(env['QWEN_TELEMETRY_LOG_PROMPTS']) ?? settings.logPrompts; const outfile = - argv.telemetryOutfile ?? - env['GEMINI_TELEMETRY_OUTFILE'] ?? - settings.outfile; + argv.telemetryOutfile ?? env['QWEN_TELEMETRY_OUTFILE'] ?? settings.outfile; const useCollector = - parseBooleanEnvFlag(env['GEMINI_TELEMETRY_USE_COLLECTOR']) ?? + parseBooleanEnvFlag(env['QWEN_TELEMETRY_USE_COLLECTOR']) ?? settings.useCollector; return { diff --git a/scripts/build_sandbox.js b/scripts/build_sandbox.js index ac713ab1b..80133151b 100644 --- a/scripts/build_sandbox.js +++ b/scripts/build_sandbox.js @@ -173,7 +173,7 @@ function buildImage(imageName, dockerfile) { ).version; const imageTag = - process.env.GEMINI_SANDBOX_IMAGE_TAG || imageName.split(':')[1]; + process.env.QWEN_SANDBOX_IMAGE_TAG || imageName.split(':')[1]; const finalImageName = `${imageName.split(':')[0]}:${imageTag}`; try { diff --git a/scripts/sandbox_command.js b/scripts/sandbox_command.js index 75cc4127a..629e96f60 100644 --- a/scripts/sandbox_command.js +++ b/scripts/sandbox_command.js @@ -32,27 +32,27 @@ const argv = yargs(hideBin(process.argv)).option('q', { default: false, }).argv; -let geminiSandbox = process.env.GEMINI_SANDBOX; +let qwenSandbox = process.env.QWEN_SANDBOX; -if (!geminiSandbox) { +if (!qwenSandbox) { const userSettingsFile = join(os.homedir(), '.qwen', 'settings.json'); if (existsSync(userSettingsFile)) { const settings = JSON.parse( stripJsonComments(readFileSync(userSettingsFile, 'utf-8')), ); if (settings.sandbox) { - geminiSandbox = settings.sandbox; + qwenSandbox = settings.sandbox; } } } -if (!geminiSandbox) { +if (!qwenSandbox) { let currentDir = process.cwd(); while (true) { - const geminiEnv = join(currentDir, '.qwen', '.env'); + const qwenEnv = join(currentDir, '.qwen', '.env'); const regularEnv = join(currentDir, '.env'); - if (existsSync(geminiEnv)) { - dotenv.config({ path: geminiEnv, quiet: true }); + if (existsSync(qwenEnv)) { + dotenv.config({ path: qwenEnv, quiet: true }); break; } else if (existsSync(regularEnv)) { dotenv.config({ path: regularEnv, quiet: true }); @@ -64,10 +64,10 @@ if (!geminiSandbox) { } currentDir = parentDir; } - geminiSandbox = process.env.GEMINI_SANDBOX; + qwenSandbox = process.env.QWEN_SANDBOX; } -geminiSandbox = (geminiSandbox || '').toLowerCase(); +qwenSandbox = (qwenSandbox || '').toLowerCase(); const commandExists = (cmd) => { // Use 'where.exe' (not 'where') on Windows because PowerShell aliases @@ -90,23 +90,23 @@ const commandExists = (cmd) => { }; let command = ''; -if (['1', 'true'].includes(geminiSandbox)) { +if (['1', 'true'].includes(qwenSandbox)) { if (commandExists('docker')) { command = 'docker'; } else if (commandExists('podman')) { command = 'podman'; } else { console.error( - 'ERROR: install docker or podman or specify command in GEMINI_SANDBOX', + 'ERROR: install docker or podman or specify command in QWEN_SANDBOX', ); process.exit(1); } -} else if (geminiSandbox && !['0', 'false'].includes(geminiSandbox)) { - if (commandExists(geminiSandbox)) { - command = geminiSandbox; +} else if (qwenSandbox && !['0', 'false'].includes(qwenSandbox)) { + if (commandExists(qwenSandbox)) { + command = qwenSandbox; } else { console.error( - `ERROR: missing sandbox command '${geminiSandbox}' (from GEMINI_SANDBOX)`, + `ERROR: missing sandbox command '${qwenSandbox}' (from QWEN_SANDBOX)`, ); process.exit(1); } diff --git a/scripts/telemetry_utils.js b/scripts/telemetry_utils.js index cb2010d5b..cb30b2ee3 100644 --- a/scripts/telemetry_utils.js +++ b/scripts/telemetry_utils.js @@ -26,7 +26,7 @@ const projectHash = crypto // User-level .gemini directory in home const USER_GEMINI_DIR = path.join(os.homedir(), '.qwen'); // Project-level .gemini directory in the workspace -const WORKSPACE_GEMINI_DIR = path.join(projectRoot, '.qwen'); +const WORKSPACE_QWEN_DIR = path.join(projectRoot, '.qwen'); // Telemetry artifacts are stored in a hashed directory under the user's ~/.qwen/tmp export const OTEL_DIR = path.join(USER_GEMINI_DIR, 'tmp', projectHash, 'otel'); @@ -34,7 +34,7 @@ export const BIN_DIR = path.join(OTEL_DIR, 'bin'); // Workspace settings remain in the project's .gemini directory export const WORKSPACE_SETTINGS_FILE = path.join( - WORKSPACE_GEMINI_DIR, + WORKSPACE_QWEN_DIR, 'settings.json', ); From 396248e296b13fdb7163d3faca21e3661db2d9f6 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 4 Mar 2026 23:21:04 +0800 Subject: [PATCH 2/6] fix(core): remove LLM-based loop detection and enable skipLoopDetection by default The LLM-based loop detection was causing issues where the process would stop unexpectedly. This change removes the LLM-based loop detection feature entirely and sets skipLoopDetection to true by default to prevent false positives from interrupting user sessions. Co-authored-by: Qwen-Coder --- packages/cli/src/config/config.ts | 2 +- packages/cli/src/config/settingsSchema.ts | 2 +- packages/core/src/core/client.test.ts | 2 - packages/core/src/core/client.ts | 8 - .../src/services/loopDetectionService.test.ts | 144 +------------- .../core/src/services/loopDetectionService.ts | 175 ------------------ packages/core/src/telemetry/types.ts | 1 - 7 files changed, 3 insertions(+), 331 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 48961cdca..e66bed5cc 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1011,7 +1011,7 @@ export async function loadCliConfig( useBuiltinRipgrep: settings.tools?.useBuiltinRipgrep, shouldUseNodePtyShell: settings.tools?.shell?.enableInteractiveShell, skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck, - skipLoopDetection: settings.model?.skipLoopDetection ?? false, + skipLoopDetection: settings.model?.skipLoopDetection ?? true, skipStartupContext: settings.model?.skipStartupContext ?? false, truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold, truncateToolOutputLines: settings.tools?.truncateToolOutputLines, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index fd6c3e85b..f569598a4 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -589,7 +589,7 @@ const SETTINGS_SCHEMA = { label: 'Skip Loop Detection', category: 'Model', requiresRestart: false, - default: false, + default: true, description: 'Disable all loop detection checks (streaming and LLM).', showInDialog: false, }, diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index b5234045e..215c012a1 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -2270,7 +2270,6 @@ Other open files: // Replace loop detector with spies const ldMock = { - turnStarted: vi.fn().mockResolvedValue(false), addAndCheck: vi.fn().mockReturnValue(false), reset: vi.fn(), }; @@ -2301,7 +2300,6 @@ Other open files: } // Assert - loop detection methods should not be called when skipLoopDetection is true - expect(ldMock.turnStarted).not.toHaveBeenCalled(); expect(ldMock.addAndCheck).not.toHaveBeenCalled(); }); }); diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 9f3625c38..19d0a9c49 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -486,14 +486,6 @@ export class GeminiClient { const turn = new Turn(this.getChat(), prompt_id); - if (!this.config.getSkipLoopDetection()) { - const loopDetected = await this.loopDetector.turnStarted(signal); - if (loopDetected) { - yield { type: GeminiEventType.LoopDetected }; - return turn; - } - } - // append system reminders to the request let requestToSent = await flatMapTextParts(request, async (text) => [text]); if (!options?.isContinuation) { diff --git a/packages/core/src/services/loopDetectionService.test.ts b/packages/core/src/services/loopDetectionService.test.ts index c7629e134..31a8699dc 100644 --- a/packages/core/src/services/loopDetectionService.test.ts +++ b/packages/core/src/services/loopDetectionService.test.ts @@ -4,10 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Config } from '../config/config.js'; -import type { GeminiClient } from '../core/client.js'; -import type { BaseLlmClient } from '../core/baseLlmClient.js'; import type { ServerGeminiContentEvent, ServerGeminiStreamEvent, @@ -15,7 +13,6 @@ import type { } from '../core/turn.js'; import { GeminiEventType } from '../core/turn.js'; import * as loggers from '../telemetry/loggers.js'; -import { LoopType } from '../telemetry/types.js'; import { LoopDetectionService } from './loopDetectionService.js'; vi.mock('../telemetry/loggers.js', () => ({ @@ -623,142 +620,3 @@ describe('LoopDetectionService', () => { }); }); }); - -describe('LoopDetectionService LLM Checks', () => { - let service: LoopDetectionService; - let mockConfig: Config; - let mockGeminiClient: GeminiClient; - let mockBaseLlmClient: BaseLlmClient; - let abortController: AbortController; - - beforeEach(() => { - mockGeminiClient = { - getHistory: vi.fn().mockReturnValue([]), - } as unknown as GeminiClient; - - mockBaseLlmClient = { - generateJson: vi.fn(), - } as unknown as BaseLlmClient; - - mockConfig = { - getGeminiClient: () => mockGeminiClient, - getBaseLlmClient: () => mockBaseLlmClient, - getDebugMode: () => false, - getDebugLogger: () => ({ - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - }), - getTelemetryEnabled: () => true, - getModel: () => 'test-model', - } as unknown as Config; - - service = new LoopDetectionService(mockConfig); - abortController = new AbortController(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - const advanceTurns = async (count: number) => { - for (let i = 0; i < count; i++) { - await service.turnStarted(abortController.signal); - } - }; - - it('should not trigger LLM check before LLM_CHECK_AFTER_TURNS', async () => { - await advanceTurns(29); - expect(mockBaseLlmClient.generateJson).not.toHaveBeenCalled(); - }); - - it('should trigger LLM check on the 30th turn', async () => { - mockBaseLlmClient.generateJson = vi - .fn() - .mockResolvedValue({ confidence: 0.1 }); - await advanceTurns(30); - expect(mockBaseLlmClient.generateJson).toHaveBeenCalledTimes(1); - expect(mockBaseLlmClient.generateJson).toHaveBeenCalledWith( - expect.objectContaining({ - systemInstruction: expect.any(String), - contents: expect.any(Array), - model: expect.any(String), - schema: expect.any(Object), - promptId: expect.any(String), - }), - ); - }); - - it('should detect a cognitive loop when confidence is high', async () => { - // First check at turn 30 - mockBaseLlmClient.generateJson = vi - .fn() - .mockResolvedValue({ confidence: 0.85, reasoning: 'Repetitive actions' }); - await advanceTurns(30); - expect(mockBaseLlmClient.generateJson).toHaveBeenCalledTimes(1); - - // The confidence of 0.85 will result in a low interval. - // The interval will be: 5 + (15 - 5) * (1 - 0.85) = 5 + 10 * 0.15 = 6.5 -> rounded to 7 - await advanceTurns(6); // advance to turn 36 - - mockBaseLlmClient.generateJson = vi - .fn() - .mockResolvedValue({ confidence: 0.95, reasoning: 'Repetitive actions' }); - const finalResult = await service.turnStarted(abortController.signal); // This is turn 37 - - expect(finalResult).toBe(true); - expect(loggers.logLoopDetected).toHaveBeenCalledWith( - mockConfig, - expect.objectContaining({ - 'event.name': 'loop_detected', - loop_type: LoopType.LLM_DETECTED_LOOP, - }), - ); - }); - - it('should not detect a loop when confidence is low', async () => { - mockBaseLlmClient.generateJson = vi - .fn() - .mockResolvedValue({ confidence: 0.5, reasoning: 'Looks okay' }); - await advanceTurns(30); - const result = await service.turnStarted(abortController.signal); - expect(result).toBe(false); - expect(loggers.logLoopDetected).not.toHaveBeenCalled(); - }); - - it('should adjust the check interval based on confidence', async () => { - // Confidence is 0.0, so interval should be MAX_LLM_CHECK_INTERVAL (15) - mockBaseLlmClient.generateJson = vi - .fn() - .mockResolvedValue({ confidence: 0.0 }); - await advanceTurns(30); // First check at turn 30 - expect(mockBaseLlmClient.generateJson).toHaveBeenCalledTimes(1); - - await advanceTurns(14); // Advance to turn 44 - expect(mockBaseLlmClient.generateJson).toHaveBeenCalledTimes(1); - - await service.turnStarted(abortController.signal); // Turn 45 - expect(mockBaseLlmClient.generateJson).toHaveBeenCalledTimes(2); - }); - - it('should handle errors from generateJson gracefully', async () => { - mockBaseLlmClient.generateJson = vi - .fn() - .mockRejectedValue(new Error('API error')); - await advanceTurns(30); - const result = await service.turnStarted(abortController.signal); - expect(result).toBe(false); - expect(loggers.logLoopDetected).not.toHaveBeenCalled(); - }); - - it('should not trigger LLM check when disabled for session', async () => { - service.disableForSession(); - expect(loggers.logLoopDetectionDisabled).toHaveBeenCalledTimes(1); - await advanceTurns(30); - const result = await service.turnStarted(abortController.signal); - expect(result).toBe(false); - expect(mockBaseLlmClient.generateJson).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/core/src/services/loopDetectionService.ts b/packages/core/src/services/loopDetectionService.ts index 9117d0120..d14e4223e 100644 --- a/packages/core/src/services/loopDetectionService.ts +++ b/packages/core/src/services/loopDetectionService.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Content } from '@google/genai'; import { createHash } from 'node:crypto'; import type { ServerGeminiStreamEvent } from '../core/turn.js'; import { GeminiEventType } from '../core/turn.js'; @@ -18,59 +17,12 @@ import { LoopType, } from '../telemetry/types.js'; import type { Config } from '../config/config.js'; -import { - isFunctionCall, - isFunctionResponse, -} from '../utils/messageInspectors.js'; -import { DEFAULT_QWEN_MODEL } from '../config/models.js'; -import { createDebugLogger } from '../utils/debugLogger.js'; - -const debugLogger = createDebugLogger('LOOP_DETECTION'); const TOOL_CALL_LOOP_THRESHOLD = 5; const CONTENT_LOOP_THRESHOLD = 10; const CONTENT_CHUNK_SIZE = 50; const MAX_HISTORY_LENGTH = 1000; -/** - * The number of recent conversation turns to include in the history when asking the LLM to check for a loop. - */ -const LLM_LOOP_CHECK_HISTORY_COUNT = 20; - -/** - * The number of turns that must pass in a single prompt before the LLM-based loop check is activated. - */ -const LLM_CHECK_AFTER_TURNS = 30; - -/** - * The default interval, in number of turns, at which the LLM-based loop check is performed. - * This value is adjusted dynamically based on the LLM's confidence. - */ -const DEFAULT_LLM_CHECK_INTERVAL = 3; - -/** - * The minimum interval for LLM-based loop checks. - * This is used when the confidence of a loop is high, to check more frequently. - */ -const MIN_LLM_CHECK_INTERVAL = 5; - -/** - * The maximum interval for LLM-based loop checks. - * This is used when the confidence of a loop is low, to check less frequently. - */ -const MAX_LLM_CHECK_INTERVAL = 15; - -const LOOP_DETECTION_SYSTEM_PROMPT = `You are a sophisticated AI diagnostic agent specializing in identifying when a conversational AI is stuck in an unproductive state. Your task is to analyze the provided conversation history and determine if the assistant has ceased to make meaningful progress. - -An unproductive state is characterized by one or more of the following patterns over the last 5 or more assistant turns: - -Repetitive Actions: The assistant repeats the same tool calls or conversational responses a decent number of times. This includes simple loops (e.g., tool_A, tool_A, tool_A) and alternating patterns (e.g., tool_A, tool_B, tool_A, tool_B, ...). - -Cognitive Loop: The assistant seems unable to determine the next logical step. It might express confusion, repeatedly ask the same questions, or generate responses that don't logically follow from the previous turns, indicating it's stuck and not advancing the task. - -Crucially, differentiate between a true unproductive state and legitimate, incremental progress. -For example, a series of 'tool_A' or 'tool_B' tool calls that make small, distinct changes to the same file (like adding docstrings to functions one by one) is considered forward progress and is NOT a loop. A loop would be repeatedly replacing the same text with the same content, or cycling between a small set of files with no net change.`; - /** * Service for detecting and preventing infinite loops in AI responses. * Monitors tool call repetitions and content sentence repetitions. @@ -90,11 +42,6 @@ export class LoopDetectionService { private loopDetected = false; private inCodeBlock = false; - // LLM loop track tracking - private turnsInCurrentPrompt = 0; - private llmCheckInterval = DEFAULT_LLM_CHECK_INTERVAL; - private lastCheckTurn = 0; - // Session-level disable flag private disabledForSession = false; @@ -145,33 +92,6 @@ export class LoopDetectionService { return this.loopDetected; } - /** - * Signals the start of a new turn in the conversation. - * - * This method increments the turn counter and, if specific conditions are met, - * triggers an LLM-based check to detect potential conversation loops. The check - * is performed periodically based on the `llmCheckInterval`. - * - * @param signal - An AbortSignal to allow for cancellation of the asynchronous LLM check. - * @returns A promise that resolves to `true` if a loop is detected, and `false` otherwise. - */ - async turnStarted(signal: AbortSignal) { - if (this.disabledForSession) { - return false; - } - this.turnsInCurrentPrompt++; - - if ( - this.turnsInCurrentPrompt >= LLM_CHECK_AFTER_TURNS && - this.turnsInCurrentPrompt - this.lastCheckTurn >= this.llmCheckInterval - ) { - this.lastCheckTurn = this.turnsInCurrentPrompt; - return await this.checkForLoopWithLLM(signal); - } - - return false; - } - private checkToolCallLoop(toolCall: { name: string; args: object }): boolean { const key = this.getToolCallKey(toolCall); if (this.lastToolCallKey === key) { @@ -371,94 +291,6 @@ export class LoopDetectionService { return originalChunk === currentChunk; } - private trimRecentHistory(recentHistory: Content[]): Content[] { - // A function response must be preceded by a function call. - // Continuously removes dangling function calls from the end of the history - // until the last turn is not a function call. - while ( - recentHistory.length > 0 && - isFunctionCall(recentHistory[recentHistory.length - 1]) - ) { - recentHistory.pop(); - } - - // A function response should follow a function call. - // Continuously removes leading function responses from the beginning of history - // until the first turn is not a function response. - while (recentHistory.length > 0 && isFunctionResponse(recentHistory[0])) { - recentHistory.shift(); - } - - return recentHistory; - } - - private async checkForLoopWithLLM(signal: AbortSignal) { - const recentHistory = this.config - .getGeminiClient() - .getHistory() - .slice(-LLM_LOOP_CHECK_HISTORY_COUNT); - - const trimmedHistory = this.trimRecentHistory(recentHistory); - - const taskPrompt = `Please analyze the conversation history to determine the possibility that the conversation is stuck in a repetitive, non-productive state. Provide your response in the requested JSON format.`; - - const contents = [ - ...trimmedHistory, - { role: 'user', parts: [{ text: taskPrompt }] }, - ]; - const schema: Record = { - type: 'object', - properties: { - reasoning: { - type: 'string', - description: - 'Your reasoning on if the conversation is looping without forward progress.', - }, - confidence: { - type: 'number', - description: - 'A number between 0.0 and 1.0 representing your confidence that the conversation is in an unproductive state.', - }, - }, - required: ['reasoning', 'confidence'], - }; - let result; - try { - result = await this.config.getBaseLlmClient().generateJson({ - contents, - schema, - model: this.config.getModel() || DEFAULT_QWEN_MODEL, - systemInstruction: LOOP_DETECTION_SYSTEM_PROMPT, - abortSignal: signal, - promptId: this.promptId, - }); - } catch (e) { - // Do nothing, treat it as a non-loop. - this.config.getDebugLogger().error(e); - return false; - } - - if (typeof result['confidence'] === 'number') { - if (result['confidence'] > 0.9) { - if (typeof result['reasoning'] === 'string' && result['reasoning']) { - debugLogger.warn(result['reasoning']); - } - logLoopDetected( - this.config, - new LoopDetectedEvent(LoopType.LLM_DETECTED_LOOP, this.promptId), - ); - return true; - } else { - this.llmCheckInterval = Math.round( - MIN_LLM_CHECK_INTERVAL + - (MAX_LLM_CHECK_INTERVAL - MIN_LLM_CHECK_INTERVAL) * - (1 - result['confidence']), - ); - } - } - return false; - } - /** * Resets all loop detection state. */ @@ -466,7 +298,6 @@ export class LoopDetectionService { this.promptId = promptId; this.resetToolCallCount(); this.resetContentTracking(); - this.resetLlmCheckTracking(); this.loopDetected = false; } @@ -482,10 +313,4 @@ export class LoopDetectionService { this.contentStats.clear(); this.lastContentIndex = 0; } - - private resetLlmCheckTracking(): void { - this.turnsInCurrentPrompt = 0; - this.llmCheckInterval = DEFAULT_LLM_CHECK_INTERVAL; - this.lastCheckTurn = 0; - } } diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 98c8d5cac..d9c6b535d 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -362,7 +362,6 @@ export class RipgrepFallbackEvent implements BaseTelemetryEvent { export enum LoopType { CONSECUTIVE_IDENTICAL_TOOL_CALLS = 'consecutive_identical_tool_calls', CHANTING_IDENTICAL_SENTENCES = 'chanting_identical_sentences', - LLM_DETECTED_LOOP = 'llm_detected_loop', } export class LoopDetectedEvent implements BaseTelemetryEvent { From 659dd66840b1be73ec2b15e3211c493545cad949 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Thu, 5 Mar 2026 20:11:00 +0800 Subject: [PATCH 3/6] refactor(cli): consolidate and rename message components Consolidate individual message component files into two modules: - ConversationMessages.tsx for user/assistant/think messages - StatusMessages.tsx for info/warning/error/retry messages Rename Gemini* components to Assistant*/Think* for clarity. Add 'code' semantic color token for consistent code styling. Co-authored-by: Qwen-Coder --- .../src/ui/components/HistoryItemDisplay.tsx | 38 ++- .../HistoryItemDisplay.test.tsx.snap | 6 +- .../messages/ConversationMessages.tsx | 261 ++++++++++++++++++ .../ui/components/messages/ErrorMessage.tsx | 38 --- .../ui/components/messages/GeminiMessage.tsx | 46 --- .../messages/GeminiMessageContent.tsx | 43 --- .../messages/GeminiThoughtMessage.tsx | 48 ---- .../messages/GeminiThoughtMessageContent.tsx | 40 --- .../ui/components/messages/InfoMessage.tsx | 37 --- .../messages/RetryCountdownMessage.tsx | 41 --- .../ui/components/messages/StatusMessages.tsx | 105 +++++++ .../ui/components/messages/UserMessage.tsx | 38 --- .../components/messages/UserShellMessage.tsx | 25 -- .../ui/components/messages/WarningMessage.tsx | 33 --- packages/cli/src/ui/themes/no-color.ts | 1 + packages/cli/src/ui/themes/semantic-tokens.ts | 4 + packages/cli/src/ui/themes/theme.ts | 5 +- 17 files changed, 403 insertions(+), 406 deletions(-) create mode 100644 packages/cli/src/ui/components/messages/ConversationMessages.tsx delete mode 100644 packages/cli/src/ui/components/messages/ErrorMessage.tsx delete mode 100644 packages/cli/src/ui/components/messages/GeminiMessage.tsx delete mode 100644 packages/cli/src/ui/components/messages/GeminiMessageContent.tsx delete mode 100644 packages/cli/src/ui/components/messages/GeminiThoughtMessage.tsx delete mode 100644 packages/cli/src/ui/components/messages/GeminiThoughtMessageContent.tsx delete mode 100644 packages/cli/src/ui/components/messages/InfoMessage.tsx delete mode 100644 packages/cli/src/ui/components/messages/RetryCountdownMessage.tsx create mode 100644 packages/cli/src/ui/components/messages/StatusMessages.tsx delete mode 100644 packages/cli/src/ui/components/messages/UserMessage.tsx delete mode 100644 packages/cli/src/ui/components/messages/UserShellMessage.tsx delete mode 100644 packages/cli/src/ui/components/messages/WarningMessage.tsx diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 3bb6780ca..a82847cc8 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -8,19 +8,23 @@ import type React from 'react'; import { useMemo } from 'react'; import { escapeAnsiCtrlCodes } from '../utils/textUtils.js'; import type { HistoryItem } from '../types.js'; -import { UserMessage } from './messages/UserMessage.js'; -import { UserShellMessage } from './messages/UserShellMessage.js'; -import { GeminiMessage } from './messages/GeminiMessage.js'; -import { InfoMessage } from './messages/InfoMessage.js'; -import { ErrorMessage } from './messages/ErrorMessage.js'; +import { + UserMessage, + UserShellMessage, + AssistantMessage, + AssistantMessageContent, + ThinkMessage, + ThinkMessageContent, +} from './messages/ConversationMessages.js'; import { ToolGroupMessage } from './messages/ToolGroupMessage.js'; -import { GeminiMessageContent } from './messages/GeminiMessageContent.js'; -import { GeminiThoughtMessage } from './messages/GeminiThoughtMessage.js'; -import { GeminiThoughtMessageContent } from './messages/GeminiThoughtMessageContent.js'; import { CompressionMessage } from './messages/CompressionMessage.js'; import { SummaryMessage } from './messages/SummaryMessage.js'; -import { WarningMessage } from './messages/WarningMessage.js'; -import { RetryCountdownMessage } from './messages/RetryCountdownMessage.js'; +import { + InfoMessage, + WarningMessage, + ErrorMessage, + RetryCountdownMessage, +} from './messages/StatusMessages.js'; import { Box } from 'ink'; import { AboutBox } from './AboutBox.js'; import { StatsDisplay } from './StatsDisplay.js'; @@ -61,6 +65,11 @@ const HistoryItemDisplayComponent: React.FC = ({ embeddedShellFocused, availableTerminalHeightGemini, }) => { + const marginTop = + item.type === 'gemini_content' || item.type === 'gemini_thought_content' + ? 0 + : 1; + const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]); const contentWidth = terminalWidth - 4; const boxWidth = mainAreaWidth || contentWidth; @@ -69,6 +78,7 @@ const HistoryItemDisplayComponent: React.FC = ({ @@ -80,7 +90,7 @@ const HistoryItemDisplayComponent: React.FC = ({ )} {itemForDisplay.type === 'gemini' && ( - = ({ /> )} {itemForDisplay.type === 'gemini_content' && ( - = ({ /> )} {itemForDisplay.type === 'gemini_thought' && ( - = ({ /> )} {itemForDisplay.type === 'gemini_thought_content' && ( - > should render a full gemini item when using availableTerminalHeightGemini 1`] = ` -" ✦ Example code block: +" + ✦ Example code block: 1 Line 1 2 Line 2 3 Line 3 @@ -109,7 +110,8 @@ exports[` > should render a full gemini_content item when `; exports[` > should render a truncated gemini item 1`] = ` -" ✦ Example code block: +" + ✦ Example code block: ... first 41 lines hidden ... 42 Line 42 43 Line 43 diff --git a/packages/cli/src/ui/components/messages/ConversationMessages.tsx b/packages/cli/src/ui/components/messages/ConversationMessages.tsx new file mode 100644 index 000000000..526bc9cfe --- /dev/null +++ b/packages/cli/src/ui/components/messages/ConversationMessages.tsx @@ -0,0 +1,261 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import stringWidth from 'string-width'; +import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; +import { theme } from '../../semantic-colors.js'; +import { + SCREEN_READER_MODEL_PREFIX, + SCREEN_READER_USER_PREFIX, +} from '../../textConstants.js'; + +interface UserMessageProps { + text: string; +} + +interface UserShellMessageProps { + text: string; +} + +interface AssistantMessageProps { + text: string; + isPending: boolean; + availableTerminalHeight?: number; + contentWidth: number; +} + +interface AssistantMessageContentProps { + text: string; + isPending: boolean; + availableTerminalHeight?: number; + contentWidth: number; +} + +interface ThinkMessageProps { + text: string; + isPending: boolean; + availableTerminalHeight?: number; + contentWidth: number; +} + +interface ThinkMessageContentProps { + text: string; + isPending: boolean; + availableTerminalHeight?: number; + contentWidth: number; +} + +interface PrefixedTextMessageProps { + text: string; + prefix: string; + prefixColor: string; + textColor: string; + ariaLabel?: string; + marginTop?: number; + alignSelf?: 'auto' | 'flex-start' | 'center' | 'flex-end'; +} + +interface PrefixedMarkdownMessageProps { + text: string; + prefix: string; + prefixColor: string; + isPending: boolean; + availableTerminalHeight?: number; + contentWidth: number; + ariaLabel?: string; + textColor?: string; +} + +interface ContinuationMarkdownMessageProps { + text: string; + isPending: boolean; + availableTerminalHeight?: number; + contentWidth: number; + basePrefix: string; + textColor?: string; +} + +function getPrefixWidth(prefix: string): number { + // Reserve one extra column so text never touches the prefix glyph. + return stringWidth(prefix) + 1; +} + +const PrefixedTextMessage: React.FC = ({ + text, + prefix, + prefixColor, + textColor, + ariaLabel, + marginTop = 0, + alignSelf, +}) => { + const prefixWidth = getPrefixWidth(prefix); + + return ( + + + + {prefix} + + + + + {text} + + + + ); +}; + +const PrefixedMarkdownMessage: React.FC = ({ + text, + prefix, + prefixColor, + isPending, + availableTerminalHeight, + contentWidth, + ariaLabel, + textColor, +}) => { + const prefixWidth = getPrefixWidth(prefix); + + return ( + + + + {prefix} + + + + + + + ); +}; + +const ContinuationMarkdownMessage: React.FC< + ContinuationMarkdownMessageProps +> = ({ + text, + isPending, + availableTerminalHeight, + contentWidth, + basePrefix, + textColor, +}) => { + const prefixWidth = getPrefixWidth(basePrefix); + + return ( + + + + ); +}; + +export const UserMessage: React.FC = ({ text }) => ( + +); + +export const UserShellMessage: React.FC = ({ text }) => { + const commandToDisplay = text.startsWith('!') ? text.substring(1) : text; + + return ( + + ); +}; + +export const AssistantMessage: React.FC = ({ + text, + isPending, + availableTerminalHeight, + contentWidth, +}) => ( + +); + +export const AssistantMessageContent: React.FC< + AssistantMessageContentProps +> = ({ text, isPending, availableTerminalHeight, contentWidth }) => ( + +); + +export const ThinkMessage: React.FC = ({ + text, + isPending, + availableTerminalHeight, + contentWidth, +}) => ( + +); + +export const ThinkMessageContent: React.FC = ({ + text, + isPending, + availableTerminalHeight, + contentWidth, +}) => ( + +); diff --git a/packages/cli/src/ui/components/messages/ErrorMessage.tsx b/packages/cli/src/ui/components/messages/ErrorMessage.tsx deleted file mode 100644 index 14cb8a91f..000000000 --- a/packages/cli/src/ui/components/messages/ErrorMessage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Text, Box } from 'ink'; -import { theme } from '../../semantic-colors.js'; - -interface ErrorMessageProps { - text: string; - /** Optional inline hint displayed after the error text in secondary/dimmed color */ - hint?: string; -} - -/** - * Renders an error message with a "✕" prefix. - * When a hint is provided (e.g., retry countdown), it is displayed inline - * in parentheses with a dimmed secondary color, similar to the ESC hint - * style used in LoadingIndicator. - */ -export const ErrorMessage: React.FC = ({ text, hint }) => { - const prefix = '✕ '; - const prefixWidth = prefix.length; - - return ( - - - {prefix} - - - {text} - {hint && ({hint})} - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.tsx deleted file mode 100644 index 987cbf38a..000000000 --- a/packages/cli/src/ui/components/messages/GeminiMessage.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Text, Box } from 'ink'; -import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; -import { theme } from '../../semantic-colors.js'; -import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; - -interface GeminiMessageProps { - text: string; - isPending: boolean; - availableTerminalHeight?: number; - contentWidth: number; -} - -export const GeminiMessage: React.FC = ({ - text, - isPending, - availableTerminalHeight, - contentWidth, -}) => { - const prefix = '✦ '; - const prefixWidth = prefix.length; - - return ( - - - - {prefix} - - - - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx deleted file mode 100644 index 29a82298f..000000000 --- a/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Box } from 'ink'; -import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; - -interface GeminiMessageContentProps { - text: string; - isPending: boolean; - availableTerminalHeight?: number; - contentWidth: number; -} - -/* - * Gemini message content is a semi-hacked component. The intention is to represent a partial - * of GeminiMessage and is only used when a response gets too long. In that instance messages - * are split into multiple GeminiMessageContent's to enable the root component in - * App.tsx to be as performant as humanly possible. - */ -export const GeminiMessageContent: React.FC = ({ - text, - isPending, - availableTerminalHeight, - contentWidth, -}) => { - const originalPrefix = '✦ '; - const prefixWidth = originalPrefix.length; - - return ( - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/GeminiThoughtMessage.tsx b/packages/cli/src/ui/components/messages/GeminiThoughtMessage.tsx deleted file mode 100644 index b595c9d06..000000000 --- a/packages/cli/src/ui/components/messages/GeminiThoughtMessage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Text, Box } from 'ink'; -import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; -import { theme } from '../../semantic-colors.js'; - -interface GeminiThoughtMessageProps { - text: string; - isPending: boolean; - availableTerminalHeight?: number; - contentWidth: number; -} - -/** - * Displays model thinking/reasoning text with a softer, dimmed style - * to visually distinguish it from regular content output. - */ -export const GeminiThoughtMessage: React.FC = ({ - text, - isPending, - availableTerminalHeight, - contentWidth, -}) => { - const prefix = '✦ '; - const prefixWidth = prefix.length; - - return ( - - - {prefix} - - - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/GeminiThoughtMessageContent.tsx b/packages/cli/src/ui/components/messages/GeminiThoughtMessageContent.tsx deleted file mode 100644 index 0f20c45d2..000000000 --- a/packages/cli/src/ui/components/messages/GeminiThoughtMessageContent.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Box } from 'ink'; -import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; -import { theme } from '../../semantic-colors.js'; - -interface GeminiThoughtMessageContentProps { - text: string; - isPending: boolean; - availableTerminalHeight?: number; - contentWidth: number; -} - -/** - * Continuation component for thought messages, similar to GeminiMessageContent. - * Used when a thought response gets too long and needs to be split for performance. - */ -export const GeminiThoughtMessageContent: React.FC< - GeminiThoughtMessageContentProps -> = ({ text, isPending, availableTerminalHeight, contentWidth }) => { - const originalPrefix = '✦ '; - const prefixWidth = originalPrefix.length; - - return ( - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/InfoMessage.tsx b/packages/cli/src/ui/components/messages/InfoMessage.tsx deleted file mode 100644 index af036237a..000000000 --- a/packages/cli/src/ui/components/messages/InfoMessage.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Text, Box } from 'ink'; -import { theme } from '../../semantic-colors.js'; -import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; - -interface InfoMessageProps { - text: string; -} - -export const InfoMessage: React.FC = ({ text }) => { - // Don't render anything if text is empty - if (!text || text.trim() === '') { - return null; - } - - const prefix = 'ℹ '; - const prefixWidth = prefix.length; - - return ( - - - {prefix} - - - - - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/RetryCountdownMessage.tsx b/packages/cli/src/ui/components/messages/RetryCountdownMessage.tsx deleted file mode 100644 index 0f4727574..000000000 --- a/packages/cli/src/ui/components/messages/RetryCountdownMessage.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Text, Box } from 'ink'; -import { theme } from '../../semantic-colors.js'; - -interface RetryCountdownMessageProps { - text: string; -} - -/** - * Displays a retry countdown message in a dimmed/secondary style - * to visually distinguish it from error messages. - */ -export const RetryCountdownMessage: React.FC = ({ - text, -}) => { - if (!text || text.trim() === '') { - return null; - } - - const prefix = '↻ '; - const prefixWidth = prefix.length; - - return ( - - - {prefix} - - - - {text} - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/StatusMessages.tsx b/packages/cli/src/ui/components/messages/StatusMessages.tsx new file mode 100644 index 000000000..5bf63257e --- /dev/null +++ b/packages/cli/src/ui/components/messages/StatusMessages.tsx @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import stringWidth from 'string-width'; +import { theme } from '../../semantic-colors.js'; +import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; + +interface StatusMessageProps { + text: string; + prefix: string; + prefixColor: string; + textColor: string; + children?: React.ReactNode; +} + +interface StatusTextProps { + text: string; +} + +/** + * Shared renderer for status-like history messages (info/warning/error/retry). + * Keeps prefix spacing and wrapping behavior consistent across variants. + */ +export const StatusMessage: React.FC = ({ + text, + prefix, + prefixColor, + textColor, + children, +}) => { + if (!text || text.trim() === '') { + return null; + } + + const prefixWidth = stringWidth(prefix) + 1; + + return ( + + + {prefix} + + + + + {children} + + + + ); +}; + +export const InfoMessage: React.FC = ({ text }) => ( + +); + +export const SuccessMessage: React.FC = ({ text }) => ( + +); + +export const WarningMessage: React.FC = ({ text }) => ( + +); + +export const ErrorMessage: React.FC = ({ + text, + hint, +}) => ( + + {hint && ({hint})} + +); + +export const RetryCountdownMessage: React.FC = ({ text }) => ( + +); diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx deleted file mode 100644 index 5cc2b965c..000000000 --- a/packages/cli/src/ui/components/messages/UserMessage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Text, Box } from 'ink'; -import { theme } from '../../semantic-colors.js'; -import { SCREEN_READER_USER_PREFIX } from '../../textConstants.js'; -import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js'; - -interface UserMessageProps { - text: string; -} - -export const UserMessage: React.FC = ({ text }) => { - const prefix = '> '; - const prefixWidth = prefix.length; - const isSlashCommand = checkIsSlashCommand(text); - - const textColor = isSlashCommand ? theme.text.accent : theme.text.secondary; - - return ( - - - - {prefix} - - - - - {text} - - - - ); -}; diff --git a/packages/cli/src/ui/components/messages/UserShellMessage.tsx b/packages/cli/src/ui/components/messages/UserShellMessage.tsx deleted file mode 100644 index 3b7bc7724..000000000 --- a/packages/cli/src/ui/components/messages/UserShellMessage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Box, Text } from 'ink'; -import { theme } from '../../semantic-colors.js'; - -interface UserShellMessageProps { - text: string; -} - -export const UserShellMessage: React.FC = ({ text }) => { - // Remove leading '!' if present, as App.tsx adds it for the processor. - const commandToDisplay = text.startsWith('!') ? text.substring(1) : text; - - return ( - - $ - {commandToDisplay} - - ); -}; diff --git a/packages/cli/src/ui/components/messages/WarningMessage.tsx b/packages/cli/src/ui/components/messages/WarningMessage.tsx deleted file mode 100644 index 589ca4b07..000000000 --- a/packages/cli/src/ui/components/messages/WarningMessage.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { Box, Text } from 'ink'; -import { Colors } from '../../colors.js'; -import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; -import { theme } from '../../semantic-colors.js'; - -interface WarningMessageProps { - text: string; -} - -export const WarningMessage: React.FC = ({ text }) => { - const prefix = '⚠ '; - const prefixWidth = 3; - - return ( - - - {prefix} - - - - - - - - ); -}; diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts index 3d5b4d4e7..c3a7cbce4 100644 --- a/packages/cli/src/ui/themes/no-color.ts +++ b/packages/cli/src/ui/themes/no-color.ts @@ -33,6 +33,7 @@ const noColorSemanticColors: SemanticColors = { secondary: '', link: '', accent: '', + code: '', }, background: { primary: '', diff --git a/packages/cli/src/ui/themes/semantic-tokens.ts b/packages/cli/src/ui/themes/semantic-tokens.ts index 2aa27a09c..d3047f0f0 100644 --- a/packages/cli/src/ui/themes/semantic-tokens.ts +++ b/packages/cli/src/ui/themes/semantic-tokens.ts @@ -12,6 +12,7 @@ export interface SemanticColors { secondary: string; link: string; accent: string; + code: string; }; background: { primary: string; @@ -45,6 +46,7 @@ export const lightSemanticColors: SemanticColors = { secondary: lightTheme.Gray, link: lightTheme.AccentBlue, accent: lightTheme.AccentPurple, + code: lightTheme.LightBlue, }, background: { primary: lightTheme.Background, @@ -77,6 +79,7 @@ export const darkSemanticColors: SemanticColors = { secondary: darkTheme.Gray, link: darkTheme.AccentBlue, accent: darkTheme.AccentPurple, + code: darkTheme.LightBlue, }, background: { primary: darkTheme.Background, @@ -109,6 +112,7 @@ export const ansiSemanticColors: SemanticColors = { secondary: ansiTheme.Gray, link: ansiTheme.AccentBlue, accent: ansiTheme.AccentPurple, + code: ansiTheme.LightBlue, }, background: { primary: ansiTheme.Background, diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 3ae3bbead..5fee07729 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -40,6 +40,7 @@ export interface CustomTheme { secondary?: string; link?: string; accent?: string; + code?: string; }; background?: { primary?: string; @@ -174,6 +175,7 @@ export class Theme { secondary: this.colors.Gray, link: this.colors.AccentBlue, accent: this.colors.AccentPurple, + code: this.colors.LightBlue, }, background: { primary: this.colors.Background, @@ -269,7 +271,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { type: 'custom', Background: customTheme.background?.primary ?? customTheme.Background ?? '', Foreground: customTheme.text?.primary ?? customTheme.Foreground ?? '', - LightBlue: customTheme.text?.link ?? customTheme.LightBlue ?? '', + LightBlue: customTheme.text?.code ?? customTheme.LightBlue ?? '', AccentBlue: customTheme.text?.link ?? customTheme.AccentBlue ?? '', AccentPurple: customTheme.text?.accent ?? customTheme.AccentPurple ?? '', AccentCyan: customTheme.text?.link ?? customTheme.AccentCyan ?? '', @@ -433,6 +435,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { secondary: customTheme.text?.secondary ?? colors.Gray, link: customTheme.text?.link ?? colors.AccentBlue, accent: customTheme.text?.accent ?? colors.AccentPurple, + code: customTheme.text?.code ?? colors.LightBlue, }, background: { primary: customTheme.background?.primary ?? colors.Background, From 708d47683c0a26541cad78265747a5f0b94bf24d Mon Sep 17 00:00:00 2001 From: qwencoder Date: Thu, 5 Mar 2026 20:26:40 +0800 Subject: [PATCH 4/6] style(messages): use filled circle prefix for info messages --- packages/cli/src/ui/components/messages/StatusMessages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/components/messages/StatusMessages.tsx b/packages/cli/src/ui/components/messages/StatusMessages.tsx index 5bf63257e..e6e945bbd 100644 --- a/packages/cli/src/ui/components/messages/StatusMessages.tsx +++ b/packages/cli/src/ui/components/messages/StatusMessages.tsx @@ -57,7 +57,7 @@ export const StatusMessage: React.FC = ({ export const InfoMessage: React.FC = ({ text }) => ( From 01ed2a7b1f7b4b775bfbc655b5bb57eb1fb64fab Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Thu, 5 Mar 2026 21:13:13 +0800 Subject: [PATCH 5/6] test(terminal-capture): add message-components scenario for PR #2120 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test scenario to verify message component prefixes display correctly: - Info message prefix (● filled circle) - Error message prefix (✕) - User message prefix (>) - Assistant message prefix (✦) Also refactors GIF generation to scenario-level for cleaner output. Co-authored-by: Qwen-Coder --- .../terminal-capture/scenario-runner.ts | 56 ++++++++----------- .../scenarios/message-components.ts | 32 +++++++++++ .../scenarios/qc-code-review.ts | 1 - 3 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 integration-tests/terminal-capture/scenarios/message-components.ts diff --git a/integration-tests/terminal-capture/scenario-runner.ts b/integration-tests/terminal-capture/scenario-runner.ts index a7900e11c..93640694b 100644 --- a/integration-tests/terminal-capture/scenario-runner.ts +++ b/integration-tests/terminal-capture/scenario-runner.ts @@ -42,8 +42,6 @@ export interface FlowStep { intervalMs: number; /** Maximum number of captures */ count: number; - /** Generate animated GIF from captured frames (default: true) */ - gif?: boolean; }; } @@ -66,6 +64,8 @@ export interface ScenarioConfig { }; /** Screenshot output directory (relative to config file) */ outputDir?: string; + /** Generate animated GIF from all screenshots in order (default: true) */ + gif?: boolean; } // ───────────────────────────────────────────── @@ -195,12 +195,7 @@ export async function runScenario( // Streaming capture: capture multiple screenshots during execution if (step.streaming) { - const { - delayMs = 0, - intervalMs, - count, - gif = true, - } = step.streaming; + const { delayMs = 0, intervalMs, count } = step.streaming; console.log( ` 🎬 streaming capture: ${count} shots @ ${intervalMs}ms intervals${delayMs ? ` (delay ${delayMs}ms)` : ''}`, ); @@ -247,25 +242,7 @@ export async function runScenario( const resultName = step.capture ?? `${pad(seq)}-02.png`; console.log(` ${label} 📸 result: ${resultName}`); - const resultShot = await terminal.capture(resultName); - screenshots.push(resultShot); - - // Generate animated GIF: input -> streaming frames -> result - if (gif && streamingShots.length > 0) { - // Include input and result in the GIF for complete story - const inputShot = screenshots.find((s) => - s.endsWith(`${pad(seq)}-01.png`), - ); - const gifFrames = [ - ...(inputShot ? [inputShot] : []), - ...streamingShots, - resultShot, - ]; - const gifPath = generateGif(gifFrames, outputDir); - if (gifPath) { - console.log(` 🎞️ GIF: ${gifPath}`); - } - } + screenshots.push(await terminal.capture(resultName)); } else { console.log(` ⏳ waiting for output to settle...`); await terminal.idle(2000, 60000); @@ -342,6 +319,19 @@ export async function runScenario( } } + // Generate animated GIF from all screenshots (excluding full-flow captures) + if (config.gif !== false) { + const gifFrames = screenshots.filter( + (s) => !s.endsWith('full-flow.png') && !s.includes('-full-'), + ); + if (gifFrames.length > 0) { + const gifPath = generateGif(gifFrames, outputDir); + if (gifPath) { + console.log(` 🎞️ GIF: ${gifPath}`); + } + } + } + const duration = Date.now() - startTime; console.log( `\n ✅ ${config.name} — ${screenshots.length} screenshots, ${(duration / 1000).toFixed(1)}s`, @@ -404,8 +394,8 @@ function resolveKey(key: string): string { function generateGif(frames: string[], outputDir: string): string | null { if (frames.length === 0) return null; - const FRAME_DURATION = 0.3; // 300ms per frame - const EDGE_DURATION = 1.0; // 600ms for first/last frame + const STREAMING_DURATION = 0.3; // 300ms for streaming frames + const STATIC_DURATION = 1.0; // 1s for non-streaming and edge frames const gifPath = join(outputDir, 'streaming.gif'); const listFile = join(outputDir, 'frames.txt'); @@ -413,11 +403,9 @@ function generateGif(frames: string[], outputDir: string): string | null { try { const lines: string[] = []; for (let i = 0; i < frames.length; i++) { - const isEdge = i === 0 || i === frames.length - 1; - lines.push( - `file '${resolve(frames[i])}'`, - `duration ${isEdge ? EDGE_DURATION : FRAME_DURATION}`, - ); + const isStreaming = frames[i].includes('-streaming-'); + const duration = isStreaming ? STREAMING_DURATION : STATIC_DURATION; + lines.push(`file '${resolve(frames[i])}'`, `duration ${duration}`); } // Concat demuxer requires last frame repeated without duration lines.push(`file '${resolve(frames[frames.length - 1])}'`); diff --git a/integration-tests/terminal-capture/scenarios/message-components.ts b/integration-tests/terminal-capture/scenarios/message-components.ts new file mode 100644 index 000000000..621eb1ef8 --- /dev/null +++ b/integration-tests/terminal-capture/scenarios/message-components.ts @@ -0,0 +1,32 @@ +import type { ScenarioConfig } from '../scenario-runner.js'; + +/** + * Tests the message component refactoring for PR #2120. + * Captures info, warning, and error messages to verify proper icon/prefix display. + * + * This scenario tests: + * - Info message prefix (● filled circle) + * - Error message prefix (✕) + * - User message prefix (>) + * - Assistant message prefix (✦) + */ +export default { + name: 'message-components', + spawn: ['node', 'dist/cli.js', '--yolo'], + terminal: { title: 'qwen-code', cwd: '../../..' }, + flow: [ + // Test info message via /skills command (instant, no streaming) + { type: '/skills' }, + // Test error message via unknown skill (instant, no streaming) + { type: '/skills nonexistent-skill-xyz' }, + // Test user and assistant messages (streams from LLM) + { + type: 'Say "Hello, this is a test of message prefixes!" and nothing else.', + streaming: { + delayMs: 3000, + intervalMs: 1000, + count: 10, + }, + }, + ], +} satisfies ScenarioConfig; diff --git a/integration-tests/terminal-capture/scenarios/qc-code-review.ts b/integration-tests/terminal-capture/scenarios/qc-code-review.ts index caf50bffc..75b281539 100644 --- a/integration-tests/terminal-capture/scenarios/qc-code-review.ts +++ b/integration-tests/terminal-capture/scenarios/qc-code-review.ts @@ -11,7 +11,6 @@ export default { delayMs: 10000, // Wait for initial model thinking/approval intervalMs: 800, // Capture every 800ms count: 30, // Max 30 captures - gif: true, // Generate animated GIF }, }, ], From 8ad5838acb56e6fcf26418890799147269e1ac46 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Fri, 6 Mar 2026 14:44:07 +0800 Subject: [PATCH 6/6] chore: update settings.schema.json Co-authored-by: Qwen-Coder --- .../schemas/settings.schema.json | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index 8b5fca2b0..5f59d756b 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -24,7 +24,7 @@ } }, "env": { - "description": "Environment variables to set as fallback defaults. These are loaded with the lowest priority: system environment variables > .env files > settings.env.", + "description": "Environment variables to set as fallback defaults. These are loaded with the lowest priority: system environment variables > .env files > settings.json env field.", "type": "object", "additionalProperties": true }, @@ -151,7 +151,7 @@ "showLineNumbers": { "description": "Show line numbers in the code output.", "type": "boolean", - "default": false + "default": true }, "showCitations": { "description": "Show citations for generated text in the chat.", @@ -264,7 +264,7 @@ "skipLoopDetection": { "description": "Disable all loop detection checks (streaming and LLM).", "type": "boolean", - "default": false + "default": true }, "skipStartupContext": { "description": "Avoid sending the workspace startup context at the beginning of each session.", @@ -574,18 +574,41 @@ "type": "object", "additionalProperties": true }, - "experimental": { - "description": "Setting to enable experimental features", + "hooksConfig": { + "description": "Hook configurations for intercepting and customizing agent behavior.", "type": "object", "properties": { - "visionModelPreview": { - "description": "Enable vision model support and auto-switching functionality. When disabled, vision models like qwen-vl-max-latest will be hidden and auto-switching will not occur.", + "enabled": { + "description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.", "type": "boolean", "default": true }, - "vlmSwitchMode": { - "description": "Default behavior when images are detected in input. Values: once (one-time switch), session (switch for entire session), persist (continue with current model). If not set, user will be prompted each time. This is a temporary experimental feature.", - "type": "string" + "disabled": { + "description": "List of hook names (commands) that should be disabled. Hooks in this list will not execute even if configured.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "hooks": { + "description": "Hook event configurations for extending CLI behavior at various lifecycle points.", + "type": "object", + "properties": { + "UserPromptSubmit": { + "description": "Hooks that execute before agent processing. Can modify prompts or inject context.", + "type": "array", + "items": { + "type": "string" + } + }, + "Stop": { + "description": "Hooks that execute after agent processing. Can post-process responses or log interactions.", + "type": "array", + "items": { + "type": "string" + } } } },