mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-18 14:56:20 +00:00
Compare commits
69 Commits
sdk-typesc
...
feat/cli-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87197d420 | ||
|
|
28f6c161da | ||
|
|
d5683886c6 | ||
|
|
758e5c0992 | ||
|
|
881e7d038b | ||
|
|
5c6c3b2cf6 | ||
|
|
f4d4844364 | ||
|
|
b804b1f48a | ||
|
|
ff5ea3c6d7 | ||
|
|
0faaac8fa4 | ||
|
|
c2e62b9122 | ||
|
|
f54b62cda3 | ||
|
|
9521987a09 | ||
|
|
d20f2a41a2 | ||
|
|
e3eccb5987 | ||
|
|
22916457cd | ||
|
|
28bc4e6467 | ||
|
|
50bf65b10b | ||
|
|
47c8bc5303 | ||
|
|
e70ecdf3a8 | ||
|
|
117af05122 | ||
|
|
557e6397bb | ||
|
|
f762a62a2e | ||
|
|
ca12772a28 | ||
|
|
cec4b831b6 | ||
|
|
74bf72877d | ||
|
|
b60ae42d10 | ||
|
|
54fd4c22a9 | ||
|
|
f3b7c63cd1 | ||
|
|
e4dee3a2b2 | ||
|
|
996b9df947 | ||
|
|
64291db926 | ||
|
|
a8e3b9ebe7 | ||
|
|
5cfc9f4686 | ||
|
|
85473210e5 | ||
|
|
c0c94bd4fc | ||
|
|
8111511a89 | ||
|
|
a8eb858f99 | ||
|
|
52d6d1ff13 | ||
|
|
c845049d26 | ||
|
|
299b7de030 | ||
|
|
b93bb8bff6 | ||
|
|
adb53a6dc6 | ||
|
|
09196c6e19 | ||
|
|
6917031128 | ||
|
|
b33525183f | ||
|
|
ec8cccafd7 | ||
|
|
b0e561ca73 | ||
|
|
563d68ad5b | ||
|
|
82c524f87d | ||
|
|
df75aa06b6 | ||
|
|
8ea9871d23 | ||
|
|
2d1934bf2f | ||
|
|
1b7418f91f | ||
|
|
b7828ac765 | ||
|
|
0bd17a2406 | ||
|
|
59be5163fd | ||
|
|
95efe89ac0 | ||
|
|
d86903ced5 | ||
|
|
a47bdc0b06 | ||
|
|
0e769e100b | ||
|
|
b5bcc07223 | ||
|
|
9653dc90d5 | ||
|
|
052337861b | ||
|
|
f8aecb2631 | ||
|
|
361492247e | ||
|
|
824ca056a4 | ||
|
|
4f664d00ac | ||
|
|
7fdebe8fe6 |
10
README.md
10
README.md
@@ -25,7 +25,7 @@ Qwen Code is an open-source AI agent for the terminal, optimized for [Qwen3-Code
|
||||
- **OpenAI-compatible, OAuth free tier**: use an OpenAI-compatible API, or sign in with Qwen OAuth to get 2,000 free requests/day.
|
||||
- **Open-source, co-evolving**: both the framework and the Qwen3-Coder model are open-source—and they ship and evolve together.
|
||||
- **Agentic workflow, feature-rich**: rich built-in tools (Skills, SubAgents, Plan Mode) for a full agentic workflow and a Claude Code-like experience.
|
||||
- **Terminal-first, IDE-friendly**: built for developers who live in the command line, with optional integration for VS Code and Zed.
|
||||
- **Terminal-first, IDE-friendly**: built for developers who live in the command line, with optional integration for VS Code, Zed, and JetBrains IDEs.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -137,10 +137,11 @@ Use `-p` to run Qwen Code without the interactive UI—ideal for scripts, automa
|
||||
|
||||
#### IDE integration
|
||||
|
||||
Use Qwen Code inside your editor (VS Code and Zed):
|
||||
Use Qwen Code inside your editor (VS Code, Zed, and JetBrains IDEs):
|
||||
|
||||
- [Use in VS Code](https://qwenlm.github.io/qwen-code-docs/en/users/integration-vscode/)
|
||||
- [Use in Zed](https://qwenlm.github.io/qwen-code-docs/en/users/integration-zed/)
|
||||
- [Use in JetBrains IDEs](https://qwenlm.github.io/qwen-code-docs/en/users/integration-jetbrains/)
|
||||
|
||||
#### TypeScript SDK
|
||||
|
||||
@@ -200,6 +201,11 @@ If you encounter issues, check the [troubleshooting guide](https://qwenlm.github
|
||||
|
||||
To report a bug from within the CLI, run `/bug` and include a short title and repro steps.
|
||||
|
||||
## Connect with Us
|
||||
|
||||
- Discord: https://discord.gg/ycKBjdNd
|
||||
- Dingtalk: https://qr.dingtalk.com/action/joingroup?code=v1,k1,+FX6Gf/ZDlTahTIRi8AEQhIaBlqykA0j+eBKKdhLeAE=&_dt_no_comment=1&origin=1
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This project is based on [Google Gemini CLI](https://github.com/google-gemini/gemini-cli). We acknowledge and appreciate the excellent work of the Gemini CLI team. Our main contribution focuses on parser-level adaptations to better support Qwen-Coder models.
|
||||
|
||||
@@ -10,4 +10,5 @@ export default {
|
||||
'web-search': 'Web Search',
|
||||
memory: 'Memory',
|
||||
'mcp-server': 'MCP Servers',
|
||||
sandbox: 'Sandboxing',
|
||||
};
|
||||
|
||||
90
docs/developers/tools/sandbox.md
Normal file
90
docs/developers/tools/sandbox.md
Normal file
@@ -0,0 +1,90 @@
|
||||
## Customizing the sandbox environment (Docker/Podman)
|
||||
|
||||
### Currently, the project does not support the use of the BUILD_SANDBOX function after installation through the npm package
|
||||
|
||||
1. To build a custom sandbox, you need to access the build scripts (scripts/build_sandbox.js) in the source code repository.
|
||||
2. These build scripts are not included in the packages released by npm.
|
||||
3. The code contains hard-coded path checks that explicitly reject build requests from non-source code environments.
|
||||
|
||||
If you need extra tools inside the container (e.g., `git`, `python`, `rg`), create a custom Dockerfile, The specific operation is as follows
|
||||
|
||||
#### 1、Clone qwen code project first, https://github.com/QwenLM/qwen-code.git
|
||||
|
||||
#### 2、Make sure you perform the following operation in the source code repository directory
|
||||
|
||||
```bash
|
||||
# 1. First, install the dependencies of the project
|
||||
npm install
|
||||
|
||||
# 2. Build the Qwen Code project
|
||||
npm run build
|
||||
|
||||
# 3. Verify that the dist directory has been generated
|
||||
ls -la packages/cli/dist/
|
||||
|
||||
# 4. Create a global link in the CLI package directory
|
||||
cd packages/cli
|
||||
npm link
|
||||
|
||||
# 5. Verification link (it should now point to the source code)
|
||||
which qwen
|
||||
# Expected output: /xxx/xxx/.nvm/versions/node/v24.11.1/bin/qwen
|
||||
# Or similar paths, but it should be a symbolic link
|
||||
|
||||
# 6. For details of the symbolic link, you can see the specific source code path
|
||||
ls -la $(dirname $(which qwen))/../lib/node_modules/@qwen-code/qwen-code
|
||||
# It should show that this is a symbolic link pointing to your source code directory
|
||||
|
||||
# 7.Test the version of qwen
|
||||
qwen -v
|
||||
# npm link will overwrite the global qwen. To avoid being unable to distinguish the same version number, you can uninstall the global CLI first
|
||||
```
|
||||
|
||||
#### 3、Create your sandbox Dockerfile under the root directory of your own project
|
||||
|
||||
- Path: `.qwen/sandbox.Dockerfile`
|
||||
|
||||
- Official mirror image address:https://github.com/QwenLM/qwen-code/pkgs/container/qwen-code
|
||||
|
||||
```bash
|
||||
# Based on the official Qwen sandbox image (It is recommended to explicitly specify the version)
|
||||
FROM ghcr.io/qwenlm/qwen-code:sha-570ec43
|
||||
# Add your extra tools here
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
python3 \
|
||||
ripgrep
|
||||
```
|
||||
|
||||
#### 4、Create the first sandbox image under the root directory of your project
|
||||
|
||||
```bash
|
||||
GEMINI_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
|
||||
```
|
||||
|
||||
This builds a project-specific image based on the default sandbox image.
|
||||
|
||||
#### Remove npm link
|
||||
|
||||
- If you want to restore the official CLI of qwen, please remove the npm link
|
||||
|
||||
```bash
|
||||
# Method 1: Unlink globally
|
||||
npm unlink -g @qwen-code/qwen-code
|
||||
|
||||
# Method 2: Remove it in the packages/cli directory
|
||||
cd packages/cli
|
||||
npm unlink
|
||||
|
||||
# Verification has been lifted
|
||||
which qwen
|
||||
# It should display "qwen not found"
|
||||
|
||||
# Reinstall the global version if necessary
|
||||
npm install -g @qwen-code/qwen-code
|
||||
|
||||
# Verification Recovery
|
||||
which qwen
|
||||
qwen --version
|
||||
```
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
},
|
||||
'integration-vscode': 'Visual Studio Code',
|
||||
'integration-zed': 'Zed IDE',
|
||||
'integration-jetbrains': 'JetBrains IDEs',
|
||||
'integration-github-action': 'Github Actions',
|
||||
'Code with Qwen Code': {
|
||||
type: 'separator',
|
||||
|
||||
@@ -74,9 +74,6 @@ Settings are organized into categories. All settings should be placed within the
|
||||
| `ui.customThemes` | object | Custom theme definitions. | `{}` |
|
||||
| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` |
|
||||
| `ui.hideTips` | boolean | Hide helpful tips in the UI. | `false` |
|
||||
| `ui.hideBanner` | boolean | Hide the application banner. | `false` |
|
||||
| `ui.hideFooter` | boolean | Hide the footer from the UI. | `false` |
|
||||
| `ui.showMemoryUsage` | boolean | Display memory usage information in the UI. | `false` |
|
||||
| `ui.showLineNumbers` | boolean | Show line numbers in code blocks in the CLI output. | `true` |
|
||||
| `ui.showCitations` | boolean | Show citations for generated text in the chat. | `true` |
|
||||
| `enableWelcomeBack` | boolean | Show welcome back dialog when returning to a project with conversation history. When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. This feature integrates with the `/summary` command and quit confirmation dialog. | `true` |
|
||||
@@ -104,7 +101,7 @@ Settings are organized into categories. All settings should be placed within the
|
||||
| `model.name` | string | The Qwen model to use for conversations. | `undefined` |
|
||||
| `model.maxSessionTurns` | number | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` |
|
||||
| `model.summarizeToolOutput` | object | Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}` | `undefined` |
|
||||
| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, and `disableCacheControl`, along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` |
|
||||
| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, `disableCacheControl`, and `customHeaders` (custom HTTP headers for API requests), along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. | `undefined` |
|
||||
| `model.chatCompression.contextPercentageThreshold` | number | Sets the threshold for chat history compression as a percentage of the model's total token limit. This is a value between 0 and 1 that applies to both automatic compression and the manual `/compress` command. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. Use `0` to disable compression entirely. | `0.7` |
|
||||
| `model.skipNextSpeakerCheck` | boolean | Skip the next speaker check. | `false` |
|
||||
| `model.skipLoopDetection` | boolean | Disables loop detection checks. Loop detection prevents infinite loops in AI responses but can generate false positives that interrupt legitimate workflows. Enable this option if you experience frequent false positive loop detection interruptions. | `false` |
|
||||
@@ -114,12 +111,16 @@ Settings are organized into categories. All settings should be placed within the
|
||||
|
||||
**Example model.generationConfig:**
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"model": {
|
||||
"generationConfig": {
|
||||
"timeout": 60000,
|
||||
"disableCacheControl": false,
|
||||
"customHeaders": {
|
||||
"X-Request-ID": "req-123",
|
||||
"X-User-ID": "user-456"
|
||||
},
|
||||
"samplingParams": {
|
||||
"temperature": 0.2,
|
||||
"top_p": 0.8,
|
||||
@@ -130,6 +131,8 @@ Settings are organized into categories. All settings should be placed within the
|
||||
}
|
||||
```
|
||||
|
||||
The `customHeaders` field allows you to add custom HTTP headers to all API requests. This is useful for request tracing, monitoring, API gateway routing, or when different models require different headers. If `customHeaders` is defined in `modelProviders[].generationConfig.customHeaders`, it will be used directly; otherwise, headers from `model.generationConfig.customHeaders` will be used. No merging occurs between the two levels.
|
||||
|
||||
**model.openAILoggingDir examples:**
|
||||
|
||||
- `"~/qwen-logs"` - Logs to `~/qwen-logs` directory
|
||||
@@ -154,6 +157,10 @@ Use `modelProviders` to declare curated model lists per auth type that the `/mod
|
||||
"generationConfig": {
|
||||
"timeout": 60000,
|
||||
"maxRetries": 3,
|
||||
"customHeaders": {
|
||||
"X-Model-Version": "v1.0",
|
||||
"X-Request-Priority": "high"
|
||||
},
|
||||
"samplingParams": { "temperature": 0.2 }
|
||||
}
|
||||
}
|
||||
@@ -215,7 +222,7 @@ Per-field precedence for `generationConfig`:
|
||||
3. `settings.model.generationConfig`
|
||||
4. Content-generator defaults (`getDefaultGenerationConfig` for OpenAI, `getParameterValue` for Gemini, etc.)
|
||||
|
||||
`samplingParams` is treated atomically; provider values replace the entire object. Defaults from the content generator apply last so each provider retains its tuned baseline.
|
||||
`samplingParams` and `customHeaders` are both treated atomically; provider values replace the entire object. If `modelProviders[].generationConfig` defines these fields, they are used directly; otherwise, values from `model.generationConfig` are used. No merging occurs between provider and global configuration levels. Defaults from the content generator apply last so each provider retains its tuned baseline.
|
||||
|
||||
##### Selection persistence and recommendations
|
||||
|
||||
@@ -264,7 +271,6 @@ If you are experiencing performance issues with file searching (e.g., with `@` c
|
||||
| `tools.enableToolOutputTruncation` | boolean | Enable truncation of large tool outputs. | `true` | Requires restart: Yes |
|
||||
| `tools.truncateToolOutputThreshold` | number | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `25000` | Requires restart: Yes |
|
||||
| `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes |
|
||||
| `tools.autoAccept` | boolean | Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. | `false` | |
|
||||
|
||||
#### mcp
|
||||
|
||||
@@ -346,7 +352,6 @@ Here is an example of a `settings.json` file with the nested structure, new as o
|
||||
},
|
||||
"ui": {
|
||||
"theme": "GitHub",
|
||||
"hideBanner": true,
|
||||
"hideTips": false,
|
||||
"customWittyPhrases": [
|
||||
"You forget a thousand things every day. Make sure this is one of 'em",
|
||||
@@ -470,7 +475,7 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
| `--telemetry-otlp-protocol` | | Sets the OTLP protocol for telemetry (`grpc` or `http`). | | Defaults to `grpc`. See [telemetry](../../developers/development/telemetry) for more information. |
|
||||
| `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
|
||||
| `--checkpointing` | | Enables [checkpointing](../features/checkpointing). | | |
|
||||
| `--acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
|
||||
| `--acp` | | Enables ACP mode (Agent Client Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
|
||||
| `--experimental-skills` | | Enables experimental [Agent Skills](../features/skills) (registers the `skill` tool and loads Skills from `.qwen/skills/` and `~/.qwen/skills/`). | | Experimental. |
|
||||
| `--extensions` | `-e` | Specifies a list of extensions to use for the session. | Extension names | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension` |
|
||||
| `--list-extensions` | `-l` | Lists all available extensions and exits. | | |
|
||||
|
||||
@@ -59,6 +59,7 @@ Commands for managing AI tools and models.
|
||||
| ---------------- | --------------------------------------------- | --------------------------------------------- |
|
||||
| `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` |
|
||||
| `/tools` | Display currently available tool list | `/tools`, `/tools desc` |
|
||||
| `/skills` | List and run available skills (experimental) | `/skills`, `/skills <name>` |
|
||||
| `/approval-mode` | Change approval mode for tool usage | `/approval-mode <mode (auto-edit)> --project` |
|
||||
| →`plan` | Analysis only, no execution | Secure review |
|
||||
| →`default` | Require approval for edits | Daily use |
|
||||
|
||||
@@ -166,15 +166,6 @@ export SANDBOX_SET_UID_GID=true # Force host UID/GID
|
||||
export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping
|
||||
```
|
||||
|
||||
## Customizing the sandbox environment (Docker/Podman)
|
||||
|
||||
If you need extra tools inside the container (e.g., `git`, `python`, `rg`), create a custom Dockerfile:
|
||||
|
||||
- Path: `.qwen/sandbox.Dockerfile`
|
||||
- Then run with: `BUILD_SANDBOX=1 qwen -s ...`
|
||||
|
||||
This builds a project-specific image based on the default sandbox image.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common issues
|
||||
|
||||
@@ -27,6 +27,14 @@ Agent Skills package expertise into discoverable capabilities. Each Skill consis
|
||||
|
||||
Skills are **model-invoked** — the model autonomously decides when to use them based on your request and the Skill’s description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command`).
|
||||
|
||||
If you want to invoke a Skill explicitly, use the `/skills` slash command:
|
||||
|
||||
```bash
|
||||
/skills <skill-name>
|
||||
```
|
||||
|
||||
The `/skills` command is only available when you run with `--experimental-skills`. Use autocomplete to browse available Skills and descriptions.
|
||||
|
||||
### Benefits
|
||||
|
||||
- Extend Qwen Code for your workflows
|
||||
|
||||
57
docs/users/integration-jetbrains.md
Normal file
57
docs/users/integration-jetbrains.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# JetBrains IDEs
|
||||
|
||||
> JetBrains IDEs provide native support for AI coding assistants through the Agent Client Protocol (ACP). This integration allows you to use Qwen Code directly within your JetBrains IDE with real-time code suggestions.
|
||||
|
||||
### Features
|
||||
|
||||
- **Native agent experience**: Integrated AI assistant panel within your JetBrains IDE
|
||||
- **Agent Client Protocol**: Full support for ACP enabling advanced IDE interactions
|
||||
- **Symbol management**: #-mention files to add them to the conversation context
|
||||
- **Conversation history**: Access to past conversations within the IDE
|
||||
|
||||
### Requirements
|
||||
|
||||
- JetBrains IDE with ACP support (IntelliJ IDEA, WebStorm, PyCharm, etc.)
|
||||
- Qwen Code CLI installed
|
||||
|
||||
### Installation
|
||||
|
||||
1. Install Qwen Code CLI:
|
||||
|
||||
```bash
|
||||
npm install -g @qwen-code/qwen-code
|
||||
```
|
||||
|
||||
2. Open your JetBrains IDE and navigate to AI Chat tool window.
|
||||
|
||||
3. Click the 3-dot menu in the upper-right corner and select **Configure ACP Agent** and configure Qwen Code with the following settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"qwen": {
|
||||
"command": "/path/to/qwen",
|
||||
"args": ["--acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. The Qwen Code agent should now be available in the AI Assistant panel
|
||||
|
||||

|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Agent not appearing
|
||||
|
||||
- Run `qwen --version` in terminal to verify installation
|
||||
- Ensure your JetBrains IDE version supports ACP
|
||||
- Restart your JetBrains IDE
|
||||
|
||||
### Qwen Code not responding
|
||||
|
||||
- Check your internet connection
|
||||
- Verify CLI works by running `qwen` in terminal
|
||||
- [File an issue on GitHub](https://github.com/qwenlm/qwen-code/issues) if the problem persists
|
||||
@@ -18,23 +18,17 @@
|
||||
|
||||
### Requirements
|
||||
|
||||
- VS Code 1.98.0 or higher
|
||||
- VS Code 1.85.0 or higher
|
||||
|
||||
### Installation
|
||||
|
||||
1. Install Qwen Code CLI:
|
||||
|
||||
```bash
|
||||
npm install -g qwen-code
|
||||
```
|
||||
|
||||
2. Download and install the extension from the [Visual Studio Code Extension Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion).
|
||||
Download and install the extension from the [Visual Studio Code Extension Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Extension not installing
|
||||
|
||||
- Ensure you have VS Code 1.98.0 or higher
|
||||
- Ensure you have VS Code 1.85.0 or higher
|
||||
- Check that VS Code has permission to install extensions
|
||||
- Try installing directly from the Marketplace website
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Zed Editor
|
||||
|
||||
> Zed Editor provides native support for AI coding assistants through the Agent Control Protocol (ACP). This integration allows you to use Qwen Code directly within Zed's interface with real-time code suggestions.
|
||||
> Zed Editor provides native support for AI coding assistants through the Agent Client Protocol (ACP). This integration allows you to use Qwen Code directly within Zed's interface with real-time code suggestions.
|
||||
|
||||

|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
|
||||
1. Install Qwen Code CLI:
|
||||
|
||||
```bash
|
||||
npm install -g qwen-code
|
||||
```
|
||||
```bash
|
||||
npm install -g @qwen-code/qwen-code
|
||||
```
|
||||
|
||||
2. Download and install [Zed Editor](https://zed.dev/)
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ Qwen Code will:
|
||||
|
||||
### Test out other common workflows
|
||||
|
||||
There are a number of ways to work with Claude:
|
||||
There are a number of ways to work with Qwen Code:
|
||||
|
||||
**Refactor code**
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ This document lists the available keyboard shortcuts in Qwen Code.
|
||||
| Shortcut | Description |
|
||||
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `!` | Toggle shell mode when the input is empty. |
|
||||
| `?` | Toggle keyboard shortcuts display when the input is empty. |
|
||||
| `\` (at end of line) + `Enter` | Insert a newline. |
|
||||
| `Down Arrow` | Navigate down through the input history. |
|
||||
| `Enter` | Submit the current prompt. |
|
||||
@@ -38,6 +39,7 @@ This document lists the available keyboard shortcuts in Qwen Code.
|
||||
| `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B` | Move the cursor one word to the left. |
|
||||
| `Ctrl+N` | Navigate down through the input history. |
|
||||
| `Ctrl+P` | Navigate up through the input history. |
|
||||
| `Ctrl+R` | Reverse search through input/shell history. |
|
||||
| `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F` | Move the cursor one word to the right. |
|
||||
| `Ctrl+U` | Delete from the cursor to the beginning of the line. |
|
||||
| `Ctrl+V` | Paste clipboard content. If the clipboard contains an image, it will be saved and a reference to it will be inserted in the prompt. |
|
||||
|
||||
@@ -831,7 +831,7 @@ describe('Permission Control (E2E)', () => {
|
||||
TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
it.skip(
|
||||
'should execute dangerous commands without confirmation',
|
||||
async () => {
|
||||
const q = query({
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -6216,10 +6216,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
@@ -13882,10 +13879,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
@@ -17974,6 +17968,7 @@
|
||||
"ajv-formats": "^3.0.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chardet": "^2.1.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^17.1.0",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
@@ -18593,7 +18588,7 @@
|
||||
},
|
||||
"packages/sdk-typescript": {
|
||||
"name": "@qwen-code/sdk",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
|
||||
@@ -553,70 +553,6 @@ describe('loadCliConfig', () => {
|
||||
expect(config.getIncludePartialMessages()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => {
|
||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
[],
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
),
|
||||
argv,
|
||||
);
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to false when --memory flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
[],
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
),
|
||||
argv,
|
||||
);
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { ui: { showMemoryUsage: false } };
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
[],
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
),
|
||||
argv,
|
||||
);
|
||||
expect(config.getShowMemoryUsage()).toBe(false);
|
||||
});
|
||||
|
||||
it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => {
|
||||
process.argv = ['node', 'script.js', '--show-memory-usage'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = { ui: { showMemoryUsage: false } };
|
||||
const config = await loadCliConfig(
|
||||
settings,
|
||||
[],
|
||||
new ExtensionEnablementManager(
|
||||
ExtensionStorage.getUserExtensionsDir(),
|
||||
argv.extensions,
|
||||
),
|
||||
argv,
|
||||
);
|
||||
expect(config.getShowMemoryUsage()).toBe(true);
|
||||
});
|
||||
|
||||
describe('Proxy configuration', () => {
|
||||
const originalProxyEnv: { [key: string]: string | undefined } = {};
|
||||
const proxyEnvVars = [
|
||||
|
||||
@@ -105,7 +105,6 @@ export interface CliArgs {
|
||||
prompt: string | undefined;
|
||||
promptInteractive: string | undefined;
|
||||
allFiles: boolean | undefined;
|
||||
showMemoryUsage: boolean | undefined;
|
||||
yolo: boolean | undefined;
|
||||
approvalMode: string | undefined;
|
||||
telemetry: boolean | undefined;
|
||||
@@ -170,7 +169,17 @@ function normalizeOutputFormat(
|
||||
}
|
||||
|
||||
export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
const rawArgv = hideBin(process.argv);
|
||||
let rawArgv = hideBin(process.argv);
|
||||
|
||||
// hack: if the first argument is the CLI entry point, remove it
|
||||
if (
|
||||
rawArgv.length > 0 &&
|
||||
(rawArgv[0].endsWith('/dist/qwen-cli/cli.js') ||
|
||||
rawArgv[0].endsWith('/dist/cli.js'))
|
||||
) {
|
||||
rawArgv = rawArgv.slice(1);
|
||||
}
|
||||
|
||||
const yargsInstance = yargs(rawArgv)
|
||||
.locale('en')
|
||||
.scriptName('qwen')
|
||||
@@ -288,11 +297,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
description: 'Include ALL files in context?',
|
||||
default: false,
|
||||
})
|
||||
.option('show-memory-usage', {
|
||||
type: 'boolean',
|
||||
description: 'Show memory usage in status bar',
|
||||
default: false,
|
||||
})
|
||||
.option('yolo', {
|
||||
alias: 'y',
|
||||
type: 'boolean',
|
||||
@@ -488,10 +492,6 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
],
|
||||
description: 'Authentication type',
|
||||
})
|
||||
.deprecateOption(
|
||||
'show-memory-usage',
|
||||
'Use the "ui.showMemoryUsage" setting in settings.json instead. This flag will be removed in a future version.',
|
||||
)
|
||||
.deprecateOption(
|
||||
'sandbox-image',
|
||||
'Use the "tools.sandbox" setting in settings.json instead. This flag will be removed in a future version.',
|
||||
@@ -1004,8 +1004,6 @@ export async function loadCliConfig(
|
||||
userMemory: memoryContent,
|
||||
geminiMdFileCount: fileCount,
|
||||
approvalMode,
|
||||
showMemoryUsage:
|
||||
argv.showMemoryUsage || settings.ui?.showMemoryUsage || false,
|
||||
accessibility: {
|
||||
...settings.ui?.accessibility,
|
||||
screenReader,
|
||||
|
||||
@@ -2260,7 +2260,7 @@ describe('Settings Loading and Merging', () => {
|
||||
disableAutoUpdate: true,
|
||||
},
|
||||
ui: {
|
||||
hideBanner: true,
|
||||
hideTips: true,
|
||||
customThemes: {
|
||||
myTheme: {},
|
||||
},
|
||||
@@ -2283,7 +2283,7 @@ describe('Settings Loading and Merging', () => {
|
||||
const v1Settings = migrateSettingsToV1(v2Settings);
|
||||
expect(v1Settings).toEqual({
|
||||
disableAutoUpdate: true,
|
||||
hideBanner: true,
|
||||
hideTips: true,
|
||||
customThemes: {
|
||||
myTheme: {},
|
||||
},
|
||||
|
||||
@@ -90,13 +90,6 @@ const MIGRATION_MAP: Record<string, string> = {
|
||||
hideWindowTitle: 'ui.hideWindowTitle',
|
||||
showStatusInTitle: 'ui.showStatusInTitle',
|
||||
hideTips: 'ui.hideTips',
|
||||
hideBanner: 'ui.hideBanner',
|
||||
hideFooter: 'ui.hideFooter',
|
||||
hideCWD: 'ui.footer.hideCWD',
|
||||
hideSandboxStatus: 'ui.footer.hideSandboxStatus',
|
||||
hideModelInfo: 'ui.footer.hideModelInfo',
|
||||
hideContextSummary: 'ui.hideContextSummary',
|
||||
showMemoryUsage: 'ui.showMemoryUsage',
|
||||
showLineNumbers: 'ui.showLineNumbers',
|
||||
showCitations: 'ui.showCitations',
|
||||
ideMode: 'ide.enabled',
|
||||
|
||||
@@ -157,9 +157,6 @@ describe('SettingsSchema', () => {
|
||||
|
||||
it('should have showInDialog property configured', () => {
|
||||
// Check that user-facing settings are marked for dialog display
|
||||
expect(
|
||||
getSettingsSchema().ui.properties.showMemoryUsage.showInDialog,
|
||||
).toBe(true);
|
||||
expect(getSettingsSchema().general.properties.vimMode.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -171,17 +168,14 @@ describe('SettingsSchema', () => {
|
||||
).toBe(true);
|
||||
expect(
|
||||
getSettingsSchema().ui.properties.hideWindowTitle.showInDialog,
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
expect(getSettingsSchema().ui.properties.hideTips.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(getSettingsSchema().ui.properties.hideBanner.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
getSettingsSchema().privacy.properties.usageStatisticsEnabled
|
||||
.showInDialog,
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
|
||||
// Check that advanced settings are hidden from dialog
|
||||
expect(getSettingsSchema().security.properties.auth.showInDialog).toBe(
|
||||
@@ -194,7 +188,7 @@ describe('SettingsSchema', () => {
|
||||
expect(getSettingsSchema().telemetry.showInDialog).toBe(false);
|
||||
|
||||
// Check that some settings are appropriately hidden
|
||||
expect(getSettingsSchema().ui.properties.theme.showInDialog).toBe(false); // Changed to false
|
||||
expect(getSettingsSchema().ui.properties.theme.showInDialog).toBe(true);
|
||||
expect(getSettingsSchema().ui.properties.customThemes.showInDialog).toBe(
|
||||
false,
|
||||
); // Managed via theme editor
|
||||
@@ -203,13 +197,13 @@ describe('SettingsSchema', () => {
|
||||
).toBe(false); // Experimental feature
|
||||
expect(getSettingsSchema().ui.properties.accessibility.showInDialog).toBe(
|
||||
false,
|
||||
); // Changed to false
|
||||
);
|
||||
expect(
|
||||
getSettingsSchema().context.properties.fileFiltering.showInDialog,
|
||||
).toBe(false); // Changed to false
|
||||
).toBe(false);
|
||||
expect(
|
||||
getSettingsSchema().general.properties.preferredEditor.showInDialog,
|
||||
).toBe(false); // Changed to false
|
||||
).toBe(true);
|
||||
expect(
|
||||
getSettingsSchema().advanced.properties.autoConfigureMemory
|
||||
.showInDialog,
|
||||
@@ -287,7 +281,7 @@ describe('SettingsSchema', () => {
|
||||
expect(
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled
|
||||
.showInDialog,
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should have debugKeystrokeLogging setting in schema', () => {
|
||||
@@ -310,7 +304,7 @@ describe('SettingsSchema', () => {
|
||||
expect(
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging
|
||||
.showInDialog,
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
expect(
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging
|
||||
.description,
|
||||
|
||||
@@ -132,7 +132,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description: 'The preferred editor to open files in.',
|
||||
showInDialog: false,
|
||||
showInDialog: true,
|
||||
},
|
||||
vimMode: {
|
||||
type: 'boolean',
|
||||
@@ -163,13 +163,13 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
gitCoAuthor: {
|
||||
type: 'boolean',
|
||||
label: 'Git Co-Author',
|
||||
label: 'Add AI Co-Author to Commits',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description:
|
||||
'Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code.',
|
||||
showInDialog: false,
|
||||
showInDialog: true,
|
||||
},
|
||||
checkpointing: {
|
||||
type: 'object',
|
||||
@@ -198,13 +198,13 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable debug logging of keystrokes to the console.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
language: {
|
||||
type: 'enum',
|
||||
label: 'Language',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
requiresRestart: true,
|
||||
default: 'auto',
|
||||
description:
|
||||
'The language for the user interface. Use "auto" to detect from system settings. ' +
|
||||
@@ -221,7 +221,7 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
terminalBell: {
|
||||
type: 'boolean',
|
||||
label: 'Terminal Bell',
|
||||
label: 'Terminal Bell Notification',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
@@ -257,7 +257,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: 'text',
|
||||
description: 'The format of the CLI output.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{ value: 'text', label: 'Text' },
|
||||
{ value: 'json', label: 'JSON' },
|
||||
@@ -280,9 +280,9 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Theme',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
default: 'Qwen Dark' as string,
|
||||
description: 'The color theme for the UI.',
|
||||
showInDialog: false,
|
||||
showInDialog: true,
|
||||
},
|
||||
customThemes: {
|
||||
type: 'object',
|
||||
@@ -300,7 +300,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Hide the window title bar',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
showStatusInTitle: {
|
||||
type: 'boolean',
|
||||
@@ -310,7 +310,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: false,
|
||||
description:
|
||||
'Show Qwen Code status and thoughts in the terminal window title',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
hideTips: {
|
||||
type: 'boolean',
|
||||
@@ -321,89 +321,13 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Hide helpful tips in the UI',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideBanner: {
|
||||
type: 'boolean',
|
||||
label: 'Hide Banner',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Hide the application banner',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideContextSummary: {
|
||||
type: 'boolean',
|
||||
label: 'Hide Context Summary',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Hide the context summary (QWEN.md, MCP servers) above the input.',
|
||||
showInDialog: true,
|
||||
},
|
||||
footer: {
|
||||
type: 'object',
|
||||
label: 'Footer',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Settings for the footer.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
hideCWD: {
|
||||
type: 'boolean',
|
||||
label: 'Hide CWD',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Hide the current working directory path in the footer.',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideSandboxStatus: {
|
||||
type: 'boolean',
|
||||
label: 'Hide Sandbox Status',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Hide the sandbox status indicator in the footer.',
|
||||
showInDialog: true,
|
||||
},
|
||||
hideModelInfo: {
|
||||
type: 'boolean',
|
||||
label: 'Hide Model Info',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Hide the model name and context usage in the footer.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
hideFooter: {
|
||||
type: 'boolean',
|
||||
label: 'Hide Footer',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Hide the footer from the UI',
|
||||
showInDialog: true,
|
||||
},
|
||||
showMemoryUsage: {
|
||||
type: 'boolean',
|
||||
label: 'Show Memory Usage',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Display memory usage information in the UI',
|
||||
showInDialog: true,
|
||||
},
|
||||
showLineNumbers: {
|
||||
type: 'boolean',
|
||||
label: 'Show Line Numbers',
|
||||
label: 'Show Line Numbers in Code',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Show line numbers in the chat.',
|
||||
description: 'Show line numbers in the code output.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showCitations: {
|
||||
@@ -413,7 +337,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Show citations for generated text in the chat.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
customWittyPhrases: {
|
||||
type: 'array',
|
||||
@@ -426,7 +350,7 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
enableWelcomeBack: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Welcome Back',
|
||||
label: 'Show Welcome Back Dialog',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
@@ -450,7 +374,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Disable loading phrases for accessibility',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
screenReader: {
|
||||
type: 'boolean',
|
||||
@@ -460,7 +384,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: undefined as boolean | undefined,
|
||||
description:
|
||||
'Render output in plain-text to be more screen reader accessible',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -478,7 +402,7 @@ const SETTINGS_SCHEMA = {
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'IDE Mode',
|
||||
label: 'Auto-connect to IDE',
|
||||
category: 'IDE',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
@@ -513,7 +437,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable collection of usage statistics',
|
||||
showInDialog: false,
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -554,7 +478,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: -1,
|
||||
description:
|
||||
'Maximum number of user/model/tool turns to keep in a session. -1 means unlimited.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
summarizeToolOutput: {
|
||||
type: 'object',
|
||||
@@ -592,7 +516,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: true,
|
||||
description: 'Skip the next speaker check.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
skipLoopDetection: {
|
||||
type: 'boolean',
|
||||
@@ -601,7 +525,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Disable all loop detection checks (streaming and LLM).',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
skipStartupContext: {
|
||||
type: 'boolean',
|
||||
@@ -611,7 +535,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: false,
|
||||
description:
|
||||
'Avoid sending the workspace startup context at the beginning of each session.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
enableOpenAILogging: {
|
||||
type: 'boolean',
|
||||
@@ -620,7 +544,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Enable OpenAI logging.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
openAILoggingDir: {
|
||||
type: 'string',
|
||||
@@ -630,7 +554,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'Custom directory path for OpenAI API logs. If not specified, defaults to logs/openai in the current working directory.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
generationConfig: {
|
||||
type: 'object',
|
||||
@@ -650,7 +574,7 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Request timeout in milliseconds.',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'timeout',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
maxRetries: {
|
||||
type: 'number',
|
||||
@@ -661,7 +585,7 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Maximum number of retries for failed requests.',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'maxRetries',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
disableCacheControl: {
|
||||
type: 'boolean',
|
||||
@@ -672,7 +596,7 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Disable cache control for DashScope providers.',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'disableCacheControl',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
schemaCompliance: {
|
||||
type: 'enum',
|
||||
@@ -684,7 +608,7 @@ const SETTINGS_SCHEMA = {
|
||||
'The compliance mode for tool schemas sent to the model. Use "openapi_30" for strict OpenAPI 3.0 compatibility (e.g., for Gemini).',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'schemaCompliance',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{ value: 'auto', label: 'Auto (Default)' },
|
||||
{ value: 'openapi_30', label: 'OpenAPI 3.0 Strict' },
|
||||
@@ -729,7 +653,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: 200,
|
||||
description: 'Maximum number of directories to search for memory.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
includeDirectories: {
|
||||
type: 'array',
|
||||
@@ -749,7 +673,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Whether to load memory files from include directories.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
fileFiltering: {
|
||||
type: 'object',
|
||||
@@ -785,7 +709,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable recursive file search functionality',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
disableFuzzySearch: {
|
||||
type: 'boolean',
|
||||
@@ -794,7 +718,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Disable fuzzy search when searching for files.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -831,7 +755,7 @@ const SETTINGS_SCHEMA = {
|
||||
properties: {
|
||||
enableInteractiveShell: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Interactive Shell',
|
||||
label: 'Interactive Shell (PTY)',
|
||||
category: 'Tools',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
@@ -856,20 +780,10 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description: 'Show color in shell output.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
autoAccept: {
|
||||
type: 'boolean',
|
||||
label: 'Auto Accept',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Automatically accept and execute tool calls that are considered safe (e.g., read-only operations).',
|
||||
showInDialog: true,
|
||||
},
|
||||
core: {
|
||||
type: 'array',
|
||||
label: 'Core Tools',
|
||||
@@ -901,7 +815,7 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
approvalMode: {
|
||||
type: 'enum',
|
||||
label: 'Approval Mode',
|
||||
label: 'Tool Approval Mode',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: ApprovalMode.DEFAULT,
|
||||
@@ -915,6 +829,16 @@ const SETTINGS_SCHEMA = {
|
||||
{ value: ApprovalMode.YOLO, label: 'YOLO' },
|
||||
],
|
||||
},
|
||||
autoAccept: {
|
||||
type: 'boolean',
|
||||
label: 'Auto Accept',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Automatically accept and execute tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation.',
|
||||
showInDialog: false,
|
||||
},
|
||||
discoveryCommand: {
|
||||
type: 'string',
|
||||
label: 'Tool Discovery Command',
|
||||
@@ -941,7 +865,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: true,
|
||||
description:
|
||||
'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
useBuiltinRipgrep: {
|
||||
type: 'boolean',
|
||||
@@ -951,7 +875,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: true,
|
||||
description:
|
||||
'Use the bundled ripgrep binary. When set to false, the system-level "rg" command will be used instead. This setting is only effective when useRipgrep is true.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
enableToolOutputTruncation: {
|
||||
type: 'boolean',
|
||||
@@ -960,7 +884,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable truncation of large tool outputs.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
truncateToolOutputThreshold: {
|
||||
type: 'number',
|
||||
@@ -970,7 +894,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
description:
|
||||
'Truncate tool output if it is larger than this many characters. Set to -1 to disable.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
truncateToolOutputLines: {
|
||||
type: 'number',
|
||||
@@ -979,7 +903,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES,
|
||||
description: 'The number of lines to keep when truncating tool output.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1056,7 +980,7 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Setting to track whether Folder trust is enabled.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1224,7 +1148,7 @@ const SETTINGS_SCHEMA = {
|
||||
default: true,
|
||||
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.',
|
||||
showInDialog: true,
|
||||
showInDialog: false,
|
||||
},
|
||||
vlmSwitchMode: {
|
||||
type: 'string',
|
||||
@@ -1292,9 +1216,3 @@ type InferSettings<T extends SettingsSchema> = {
|
||||
};
|
||||
|
||||
export type Settings = InferSettings<SettingsSchemaType>;
|
||||
|
||||
export interface FooterSettings {
|
||||
hideCWD?: boolean;
|
||||
hideSandboxStatus?: boolean;
|
||||
hideModelInfo?: boolean;
|
||||
}
|
||||
|
||||
@@ -456,7 +456,6 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
promptInteractive: undefined,
|
||||
query: undefined,
|
||||
allFiles: undefined,
|
||||
showMemoryUsage: undefined,
|
||||
yolo: undefined,
|
||||
approvalMode: undefined,
|
||||
telemetry: undefined,
|
||||
|
||||
@@ -346,6 +346,7 @@ export async function main() {
|
||||
extensionEnablementManager,
|
||||
argv,
|
||||
);
|
||||
registerCleanup(() => config.shutdown());
|
||||
|
||||
if (config.getListExtensions()) {
|
||||
console.log('Installed extensions:');
|
||||
|
||||
@@ -97,8 +97,8 @@ export default {
|
||||
Preview: 'Vorschau',
|
||||
'(Use Enter to select, Tab to configure scope)':
|
||||
'(Enter zum Auswählen, Tab zum Konfigurieren des Bereichs)',
|
||||
'(Use Enter to apply scope, Tab to select theme)':
|
||||
'(Enter zum Anwenden des Bereichs, Tab zum Auswählen des Designs)',
|
||||
'(Use Enter to apply scope, Tab to go back)':
|
||||
'(Enter zum Anwenden des Bereichs, Tab zum Zurückgehen)',
|
||||
'Theme configuration unavailable due to NO_COLOR env variable.':
|
||||
'Design-Konfiguration aufgrund der NO_COLOR-Umgebungsvariable nicht verfügbar.',
|
||||
'Theme "{{themeName}}" not found.': 'Design "{{themeName}}" nicht gefunden.',
|
||||
@@ -260,8 +260,6 @@ export default {
|
||||
'View and edit Qwen Code settings':
|
||||
'Qwen Code Einstellungen anzeigen und bearbeiten',
|
||||
Settings: 'Einstellungen',
|
||||
'(Use Enter to select{{tabText}})': '(Enter zum Auswählen{{tabText}})',
|
||||
', Tab to change focus': ', Tab zum Fokuswechsel',
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.':
|
||||
'Um Änderungen zu sehen, muss Qwen Code neu gestartet werden. Drücken Sie r, um jetzt zu beenden und Änderungen anzuwenden.',
|
||||
'The command "/{{command}}" is not supported in non-interactive mode.':
|
||||
@@ -271,6 +269,12 @@ export default {
|
||||
// ============================================================================
|
||||
'Vim Mode': 'Vim-Modus',
|
||||
'Disable Auto Update': 'Automatische Updates deaktivieren',
|
||||
'Add AI Co-Author to Commits': 'KI als Co-Autor zu Commits hinzufügen',
|
||||
'Terminal Bell Notification': 'Terminal-Signalton',
|
||||
'Enable Usage Statistics': 'Nutzungsstatistiken aktivieren',
|
||||
Theme: 'Farbschema',
|
||||
'Preferred Editor': 'Bevorzugter Editor',
|
||||
'Auto-connect to IDE': 'Automatische Verbindung zur IDE',
|
||||
'Enable Prompt Completion': 'Eingabevervollständigung aktivieren',
|
||||
'Debug Keystroke Logging': 'Debug-Protokollierung von Tastatureingaben',
|
||||
Language: 'Sprache',
|
||||
@@ -278,17 +282,10 @@ export default {
|
||||
'Hide Window Title': 'Fenstertitel ausblenden',
|
||||
'Show Status in Title': 'Status im Titel anzeigen',
|
||||
'Hide Tips': 'Tipps ausblenden',
|
||||
'Hide Banner': 'Banner ausblenden',
|
||||
'Hide Context Summary': 'Kontextzusammenfassung ausblenden',
|
||||
'Hide CWD': 'Arbeitsverzeichnis ausblenden',
|
||||
'Hide Sandbox Status': 'Sandbox-Status ausblenden',
|
||||
'Hide Model Info': 'Modellinformationen ausblenden',
|
||||
'Hide Footer': 'Fußzeile ausblenden',
|
||||
'Show Memory Usage': 'Speichernutzung anzeigen',
|
||||
'Show Line Numbers': 'Zeilennummern anzeigen',
|
||||
'Show Line Numbers in Code': 'Zeilennummern im Code anzeigen',
|
||||
'Show Citations': 'Quellenangaben anzeigen',
|
||||
'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche',
|
||||
'Enable Welcome Back': 'Willkommen-zurück aktivieren',
|
||||
'Show Welcome Back Dialog': 'Willkommen-zurück-Dialog anzeigen',
|
||||
'Disable Loading Phrases': 'Ladesprüche deaktivieren',
|
||||
'Screen Reader Mode': 'Bildschirmleser-Modus',
|
||||
'IDE Mode': 'IDE-Modus',
|
||||
@@ -308,7 +305,7 @@ export default {
|
||||
'Respect .qwenignore': '.qwenignore beachten',
|
||||
'Enable Recursive File Search': 'Rekursive Dateisuche aktivieren',
|
||||
'Disable Fuzzy Search': 'Unscharfe Suche deaktivieren',
|
||||
'Enable Interactive Shell': 'Interaktive Shell aktivieren',
|
||||
'Interactive Shell (PTY)': 'Interaktive Shell (PTY)',
|
||||
'Show Color': 'Farbe anzeigen',
|
||||
'Auto Accept': 'Automatisch akzeptieren',
|
||||
'Use Ripgrep': 'Ripgrep verwenden',
|
||||
@@ -344,6 +341,11 @@ export default {
|
||||
'Show all directories in the workspace':
|
||||
'Alle Verzeichnisse im Arbeitsbereich anzeigen',
|
||||
'set external editor preference': 'Externen Editor festlegen',
|
||||
'Select Editor': 'Editor auswählen',
|
||||
'Editor Preference': 'Editor-Einstellung',
|
||||
'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.':
|
||||
'Diese Editoren werden derzeit unterstützt. Bitte beachten Sie, dass einige Editoren nicht im Sandbox-Modus verwendet werden können.',
|
||||
'Your preferred editor is:': 'Ihr bevorzugter Editor ist:',
|
||||
'Manage extensions': 'Erweiterungen verwalten',
|
||||
'List active extensions': 'Aktive Erweiterungen auflisten',
|
||||
'Update extensions. Usage: update <extension-names>|--all':
|
||||
@@ -434,7 +436,7 @@ export default {
|
||||
// ============================================================================
|
||||
// Commands - Approval Mode
|
||||
// ============================================================================
|
||||
'Approval Mode': 'Genehmigungsmodus',
|
||||
'Tool Approval Mode': 'Werkzeug-Genehmigungsmodus',
|
||||
'Current approval mode: {{mode}}': 'Aktueller Genehmigungsmodus: {{mode}}',
|
||||
'Available approval modes:': 'Verfügbare Genehmigungsmodi:',
|
||||
'Approval mode changed to: {{mode}}':
|
||||
@@ -476,8 +478,6 @@ export default {
|
||||
'Automatically approve all tools': 'Alle Werkzeuge automatisch genehmigen',
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.':
|
||||
'Arbeitsbereich-Genehmigungsmodus existiert und hat Vorrang. Benutzerebene-Änderung hat keine Wirkung.',
|
||||
'(Use Enter to select, Tab to change focus)':
|
||||
'(Enter zum Auswählen, Tab zum Fokuswechsel)',
|
||||
'Apply To': 'Anwenden auf',
|
||||
'User Settings': 'Benutzereinstellungen',
|
||||
'Workspace Settings': 'Arbeitsbereich-Einstellungen',
|
||||
|
||||
@@ -33,6 +33,25 @@ export default {
|
||||
'Model Context Protocol command (from external servers)':
|
||||
'Model Context Protocol command (from external servers)',
|
||||
'Keyboard Shortcuts:': 'Keyboard Shortcuts:',
|
||||
'Toggle this help display': 'Toggle this help display',
|
||||
'Toggle shell mode': 'Toggle shell mode',
|
||||
'Open command menu': 'Open command menu',
|
||||
'Add file context': 'Add file context',
|
||||
'Accept suggestion / Autocomplete': 'Accept suggestion / Autocomplete',
|
||||
'Reverse search history': 'Reverse search history',
|
||||
'Press ? again to close': 'Press ? again to close',
|
||||
// Keyboard shortcuts panel descriptions
|
||||
'for shell mode': 'for shell mode',
|
||||
'for commands': 'for commands',
|
||||
'for file paths': 'for file paths',
|
||||
'to clear input': 'to clear input',
|
||||
'to cycle approvals': 'to cycle approvals',
|
||||
'to quit': 'to quit',
|
||||
'for newline': 'for newline',
|
||||
'to clear screen': 'to clear screen',
|
||||
'to search history': 'to search history',
|
||||
'to paste images': 'to paste images',
|
||||
'for external editor': 'for external editor',
|
||||
'Jump through words in the input': 'Jump through words in the input',
|
||||
'Close dialogs, cancel requests, or quit application':
|
||||
'Close dialogs, cancel requests, or quit application',
|
||||
@@ -46,6 +65,7 @@ export default {
|
||||
'Connecting to MCP servers... ({{connected}}/{{total}})':
|
||||
'Connecting to MCP servers... ({{connected}}/{{total}})',
|
||||
'Type your message or @path/to/file': 'Type your message or @path/to/file',
|
||||
'? for shortcuts': '? for shortcuts',
|
||||
"Press 'i' for INSERT mode and 'Esc' for NORMAL mode.":
|
||||
"Press 'i' for INSERT mode and 'Esc' for NORMAL mode.",
|
||||
'Cancel operation / Clear input (double press)':
|
||||
@@ -98,8 +118,8 @@ export default {
|
||||
Preview: 'Preview',
|
||||
'(Use Enter to select, Tab to configure scope)':
|
||||
'(Use Enter to select, Tab to configure scope)',
|
||||
'(Use Enter to apply scope, Tab to select theme)':
|
||||
'(Use Enter to apply scope, Tab to select theme)',
|
||||
'(Use Enter to apply scope, Tab to go back)':
|
||||
'(Use Enter to apply scope, Tab to go back)',
|
||||
'Theme configuration unavailable due to NO_COLOR env variable.':
|
||||
'Theme configuration unavailable due to NO_COLOR env variable.',
|
||||
'Theme "{{themeName}}" not found.': 'Theme "{{themeName}}" not found.',
|
||||
@@ -257,8 +277,6 @@ export default {
|
||||
// ============================================================================
|
||||
'View and edit Qwen Code settings': 'View and edit Qwen Code settings',
|
||||
Settings: 'Settings',
|
||||
'(Use Enter to select{{tabText}})': '(Use Enter to select{{tabText}})',
|
||||
', Tab to change focus': ', Tab to change focus',
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.':
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.',
|
||||
'The command "/{{command}}" is not supported in non-interactive mode.':
|
||||
@@ -268,6 +286,12 @@ export default {
|
||||
// ============================================================================
|
||||
'Vim Mode': 'Vim Mode',
|
||||
'Disable Auto Update': 'Disable Auto Update',
|
||||
'Add AI Co-Author to Commits': 'Add AI Co-Author to Commits',
|
||||
'Terminal Bell Notification': 'Terminal Bell Notification',
|
||||
'Enable Usage Statistics': 'Enable Usage Statistics',
|
||||
Theme: 'Theme',
|
||||
'Preferred Editor': 'Preferred Editor',
|
||||
'Auto-connect to IDE': 'Auto-connect to IDE',
|
||||
'Enable Prompt Completion': 'Enable Prompt Completion',
|
||||
'Debug Keystroke Logging': 'Debug Keystroke Logging',
|
||||
Language: 'Language',
|
||||
@@ -275,17 +299,10 @@ export default {
|
||||
'Hide Window Title': 'Hide Window Title',
|
||||
'Show Status in Title': 'Show Status in Title',
|
||||
'Hide Tips': 'Hide Tips',
|
||||
'Hide Banner': 'Hide Banner',
|
||||
'Hide Context Summary': 'Hide Context Summary',
|
||||
'Hide CWD': 'Hide CWD',
|
||||
'Hide Sandbox Status': 'Hide Sandbox Status',
|
||||
'Hide Model Info': 'Hide Model Info',
|
||||
'Hide Footer': 'Hide Footer',
|
||||
'Show Memory Usage': 'Show Memory Usage',
|
||||
'Show Line Numbers': 'Show Line Numbers',
|
||||
'Show Line Numbers in Code': 'Show Line Numbers in Code',
|
||||
'Show Citations': 'Show Citations',
|
||||
'Custom Witty Phrases': 'Custom Witty Phrases',
|
||||
'Enable Welcome Back': 'Enable Welcome Back',
|
||||
'Show Welcome Back Dialog': 'Show Welcome Back Dialog',
|
||||
'Disable Loading Phrases': 'Disable Loading Phrases',
|
||||
'Screen Reader Mode': 'Screen Reader Mode',
|
||||
'IDE Mode': 'IDE Mode',
|
||||
@@ -305,7 +322,7 @@ export default {
|
||||
'Respect .qwenignore': 'Respect .qwenignore',
|
||||
'Enable Recursive File Search': 'Enable Recursive File Search',
|
||||
'Disable Fuzzy Search': 'Disable Fuzzy Search',
|
||||
'Enable Interactive Shell': 'Enable Interactive Shell',
|
||||
'Interactive Shell (PTY)': 'Interactive Shell (PTY)',
|
||||
'Show Color': 'Show Color',
|
||||
'Auto Accept': 'Auto Accept',
|
||||
'Use Ripgrep': 'Use Ripgrep',
|
||||
@@ -340,6 +357,11 @@ export default {
|
||||
'Show all directories in the workspace':
|
||||
'Show all directories in the workspace',
|
||||
'set external editor preference': 'set external editor preference',
|
||||
'Select Editor': 'Select Editor',
|
||||
'Editor Preference': 'Editor Preference',
|
||||
'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.':
|
||||
'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.',
|
||||
'Your preferred editor is:': 'Your preferred editor is:',
|
||||
'Manage extensions': 'Manage extensions',
|
||||
'List active extensions': 'List active extensions',
|
||||
'Update extensions. Usage: update <extension-names>|--all':
|
||||
@@ -427,7 +449,7 @@ export default {
|
||||
// ============================================================================
|
||||
// Commands - Approval Mode
|
||||
// ============================================================================
|
||||
'Approval Mode': 'Approval Mode',
|
||||
'Tool Approval Mode': 'Tool Approval Mode',
|
||||
'Current approval mode: {{mode}}': 'Current approval mode: {{mode}}',
|
||||
'Available approval modes:': 'Available approval modes:',
|
||||
'Approval mode changed to: {{mode}}': 'Approval mode changed to: {{mode}}',
|
||||
@@ -466,8 +488,6 @@ export default {
|
||||
'Automatically approve all tools': 'Automatically approve all tools',
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.':
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.',
|
||||
'(Use Enter to select, Tab to change focus)':
|
||||
'(Use Enter to select, Tab to change focus)',
|
||||
'Apply To': 'Apply To',
|
||||
'User Settings': 'User Settings',
|
||||
'Workspace Settings': 'Workspace Settings',
|
||||
@@ -891,14 +911,23 @@ export default {
|
||||
// ============================================================================
|
||||
// Startup Tips
|
||||
// ============================================================================
|
||||
'Tips for getting started:': 'Tips for getting started:',
|
||||
'1. Ask questions, edit files, or run commands.':
|
||||
'1. Ask questions, edit files, or run commands.',
|
||||
'2. Be specific for the best results.':
|
||||
'2. Be specific for the best results.',
|
||||
'files to customize your interactions with Qwen Code.':
|
||||
'files to customize your interactions with Qwen Code.',
|
||||
'for more information.': 'for more information.',
|
||||
'Tips:': 'Tips:',
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.':
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.':
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.':
|
||||
'Use /bug to submit issues to the maintainers when something goes off.',
|
||||
'Switch auth type quickly with /auth.':
|
||||
'Switch auth type quickly with /auth.',
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).':
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).',
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.':
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.',
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.':
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.',
|
||||
|
||||
// ============================================================================
|
||||
// Exit Screen / Stats
|
||||
|
||||
@@ -33,6 +33,13 @@ export default {
|
||||
'Model Context Protocol command (from external servers)':
|
||||
'Команда Model Context Protocol (из внешних серверов)',
|
||||
'Keyboard Shortcuts:': 'Горячие клавиши:',
|
||||
'Toggle this help display': 'Показать/скрыть эту справку',
|
||||
'Toggle shell mode': 'Переключить режим оболочки',
|
||||
'Open command menu': 'Открыть меню команд',
|
||||
'Add file context': 'Добавить файл в контекст',
|
||||
'Accept suggestion / Autocomplete': 'Принять подсказку / Автодополнение',
|
||||
'Reverse search history': 'Обратный поиск по истории',
|
||||
'Press ? again to close': 'Нажмите ? ещё раз, чтобы закрыть',
|
||||
'Jump through words in the input': 'Переход по словам во вводе',
|
||||
'Close dialogs, cancel requests, or quit application':
|
||||
'Закрыть диалоги, отменить запросы или выйти из приложения',
|
||||
@@ -46,6 +53,7 @@ export default {
|
||||
'Connecting to MCP servers... ({{connected}}/{{total}})':
|
||||
'Подключение к MCP-серверам... ({{connected}}/{{total}})',
|
||||
'Type your message or @path/to/file': 'Введите сообщение или @путь/к/файлу',
|
||||
'? for shortcuts': '? — горячие клавиши',
|
||||
"Press 'i' for INSERT mode and 'Esc' for NORMAL mode.":
|
||||
"Нажмите 'i' для режима ВСТАВКА и 'Esc' для ОБЫЧНОГО режима.",
|
||||
'Cancel operation / Clear input (double press)':
|
||||
@@ -60,6 +68,19 @@ export default {
|
||||
'submit a bug report': 'Отправка отчёта об ошибке',
|
||||
'About Qwen Code': 'Об Qwen Code',
|
||||
|
||||
// Keyboard shortcuts panel descriptions
|
||||
'for shell mode': 'режим оболочки',
|
||||
'for commands': 'меню команд',
|
||||
'for file paths': 'пути к файлам',
|
||||
'to clear input': 'очистить ввод',
|
||||
'to cycle approvals': 'переключить режим',
|
||||
'to quit': 'выход',
|
||||
'for newline': 'новая строка',
|
||||
'to clear screen': 'очистить экран',
|
||||
'to search history': 'поиск в истории',
|
||||
'to paste images': 'вставить изображения',
|
||||
'for external editor': 'внешний редактор',
|
||||
|
||||
// ============================================================================
|
||||
// Поля системной информации
|
||||
// ============================================================================
|
||||
@@ -100,8 +121,8 @@ export default {
|
||||
Preview: 'Предпросмотр',
|
||||
'(Use Enter to select, Tab to configure scope)':
|
||||
'(Enter для выбора, Tab для настройки области)',
|
||||
'(Use Enter to apply scope, Tab to select theme)':
|
||||
'(Enter для применения области, Tab для выбора темы)',
|
||||
'(Use Enter to apply scope, Tab to go back)':
|
||||
'(Enter для применения области, Tab для возврата)',
|
||||
'Theme configuration unavailable due to NO_COLOR env variable.':
|
||||
'Настройка темы недоступна из-за переменной окружения NO_COLOR.',
|
||||
'Theme "{{themeName}}" not found.': 'Тема "{{themeName}}" не найдена.',
|
||||
@@ -260,8 +281,6 @@ export default {
|
||||
// ============================================================================
|
||||
'View and edit Qwen Code settings': 'Просмотр и изменение настроек Qwen Code',
|
||||
Settings: 'Настройки',
|
||||
'(Use Enter to select{{tabText}})': '(Enter для выбора{{tabText}})',
|
||||
', Tab to change focus': ', Tab для смены фокуса',
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.':
|
||||
'Для применения изменений необходимо перезапустить Qwen Code. Нажмите r для выхода и применения изменений.',
|
||||
'The command "/{{command}}" is not supported in non-interactive mode.':
|
||||
@@ -271,6 +290,12 @@ export default {
|
||||
// ============================================================================
|
||||
'Vim Mode': 'Режим Vim',
|
||||
'Disable Auto Update': 'Отключить автообновление',
|
||||
'Add AI Co-Author to Commits': 'Добавлять ИИ как соавтора в коммиты',
|
||||
'Terminal Bell Notification': 'Звуковое уведомление терминала',
|
||||
'Enable Usage Statistics': 'Включить сбор статистики использования',
|
||||
Theme: 'Тема',
|
||||
'Preferred Editor': 'Предпочтительный редактор',
|
||||
'Auto-connect to IDE': 'Автоподключение к IDE',
|
||||
'Enable Prompt Completion': 'Включить автодополнение промптов',
|
||||
'Debug Keystroke Logging': 'Логирование нажатий клавиш для отладки',
|
||||
Language: 'Язык',
|
||||
@@ -278,17 +303,10 @@ export default {
|
||||
'Hide Window Title': 'Скрыть заголовок окна',
|
||||
'Show Status in Title': 'Показывать статус в заголовке',
|
||||
'Hide Tips': 'Скрыть подсказки',
|
||||
'Hide Banner': 'Скрыть баннер',
|
||||
'Hide Context Summary': 'Скрыть сводку контекста',
|
||||
'Hide CWD': 'Скрыть текущую директорию',
|
||||
'Hide Sandbox Status': 'Скрыть статус песочницы',
|
||||
'Hide Model Info': 'Скрыть информацию о модели',
|
||||
'Hide Footer': 'Скрыть нижний колонтитул',
|
||||
'Show Memory Usage': 'Показывать использование памяти',
|
||||
'Show Line Numbers': 'Показывать номера строк',
|
||||
'Show Line Numbers in Code': 'Показывать номера строк в коде',
|
||||
'Show Citations': 'Показывать цитаты',
|
||||
'Custom Witty Phrases': 'Пользовательские остроумные фразы',
|
||||
'Enable Welcome Back': 'Включить приветствие при возврате',
|
||||
'Show Welcome Back Dialog': 'Показывать диалог приветствия',
|
||||
'Disable Loading Phrases': 'Отключить фразы при загрузке',
|
||||
'Screen Reader Mode': 'Режим программы чтения с экрана',
|
||||
'IDE Mode': 'Режим IDE',
|
||||
@@ -308,7 +326,7 @@ export default {
|
||||
'Respect .qwenignore': 'Учитывать .qwenignore',
|
||||
'Enable Recursive File Search': 'Включить рекурсивный поиск файлов',
|
||||
'Disable Fuzzy Search': 'Отключить нечеткий поиск',
|
||||
'Enable Interactive Shell': 'Включить интерактивный терминал',
|
||||
'Interactive Shell (PTY)': 'Интерактивный терминал (PTY)',
|
||||
'Show Color': 'Показывать цвета',
|
||||
'Auto Accept': 'Автоподтверждение',
|
||||
'Use Ripgrep': 'Использовать Ripgrep',
|
||||
@@ -345,6 +363,11 @@ export default {
|
||||
'Показать все директории в рабочем пространстве',
|
||||
'set external editor preference':
|
||||
'Установка предпочитаемого внешнего редактора',
|
||||
'Select Editor': 'Выбрать редактор',
|
||||
'Editor Preference': 'Настройка редактора',
|
||||
'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.':
|
||||
'В настоящее время поддерживаются следующие редакторы. Обратите внимание, что некоторые редакторы нельзя использовать в режиме песочницы.',
|
||||
'Your preferred editor is:': 'Ваш предпочитаемый редактор:',
|
||||
'Manage extensions': 'Управление расширениями',
|
||||
'List active extensions': 'Показать активные расширения',
|
||||
'Update extensions. Usage: update <extension-names>|--all':
|
||||
@@ -434,7 +457,7 @@ export default {
|
||||
// ============================================================================
|
||||
// Команды - Режим подтверждения
|
||||
// ============================================================================
|
||||
'Approval Mode': 'Режим подтверждения',
|
||||
'Tool Approval Mode': 'Режим подтверждения инструментов',
|
||||
'Current approval mode: {{mode}}': 'Текущий режим подтверждения: {{mode}}',
|
||||
'Available approval modes:': 'Доступные режимы подтверждения:',
|
||||
'Approval mode changed to: {{mode}}':
|
||||
@@ -476,8 +499,6 @@ export default {
|
||||
'Автоматически подтверждать все инструменты',
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.':
|
||||
'Режим подтверждения рабочего пространства существует и имеет приоритет. Изменение на уровне пользователя не будет иметь эффекта.',
|
||||
'(Use Enter to select, Tab to change focus)':
|
||||
'(Enter для выбора, Tab для смены фокуса)',
|
||||
'Apply To': 'Применить к',
|
||||
'User Settings': 'Настройки пользователя',
|
||||
'Workspace Settings': 'Настройки рабочего пространства',
|
||||
|
||||
@@ -32,6 +32,25 @@ export default {
|
||||
'Model Context Protocol command (from external servers)':
|
||||
'模型上下文协议命令(来自外部服务器)',
|
||||
'Keyboard Shortcuts:': '键盘快捷键:',
|
||||
'Toggle this help display': '切换此帮助显示',
|
||||
'Toggle shell mode': '切换命令行模式',
|
||||
'Open command menu': '打开命令菜单',
|
||||
'Add file context': '添加文件上下文',
|
||||
'Accept suggestion / Autocomplete': '接受建议 / 自动补全',
|
||||
'Reverse search history': '反向搜索历史',
|
||||
'Press ? again to close': '再次按 ? 关闭',
|
||||
// Keyboard shortcuts panel descriptions
|
||||
'for shell mode': '命令行模式',
|
||||
'for commands': '命令菜单',
|
||||
'for file paths': '文件路径',
|
||||
'to clear input': '清空输入',
|
||||
'to cycle approvals': '切换审批模式',
|
||||
'to quit': '退出',
|
||||
'for newline': '换行',
|
||||
'to clear screen': '清屏',
|
||||
'to search history': '搜索历史',
|
||||
'to paste images': '粘贴图片',
|
||||
'for external editor': '外部编辑器',
|
||||
'Jump through words in the input': '在输入中按单词跳转',
|
||||
'Close dialogs, cancel requests, or quit application':
|
||||
'关闭对话框、取消请求或退出应用程序',
|
||||
@@ -45,6 +64,7 @@ export default {
|
||||
'Connecting to MCP servers... ({{connected}}/{{total}})':
|
||||
'正在连接到 MCP 服务器... ({{connected}}/{{total}})',
|
||||
'Type your message or @path/to/file': '输入您的消息或 @ 文件路径',
|
||||
'? for shortcuts': '按 ? 查看快捷键',
|
||||
"Press 'i' for INSERT mode and 'Esc' for NORMAL mode.":
|
||||
"按 'i' 进入插入模式,按 'Esc' 进入普通模式",
|
||||
'Cancel operation / Clear input (double press)':
|
||||
@@ -97,8 +117,8 @@ export default {
|
||||
Preview: '预览',
|
||||
'(Use Enter to select, Tab to configure scope)':
|
||||
'(使用 Enter 选择,Tab 配置作用域)',
|
||||
'(Use Enter to apply scope, Tab to select theme)':
|
||||
'(使用 Enter 应用作用域,Tab 选择主题)',
|
||||
'(Use Enter to apply scope, Tab to go back)':
|
||||
'(使用 Enter 应用作用域,Tab 返回)',
|
||||
'Theme configuration unavailable due to NO_COLOR env variable.':
|
||||
'由于 NO_COLOR 环境变量,主题配置不可用。',
|
||||
'Theme "{{themeName}}" not found.': '未找到主题 "{{themeName}}"。',
|
||||
@@ -248,8 +268,6 @@ export default {
|
||||
// ============================================================================
|
||||
'View and edit Qwen Code settings': '查看和编辑 Qwen Code 设置',
|
||||
Settings: '设置',
|
||||
'(Use Enter to select{{tabText}})': '(使用 Enter 选择{{tabText}})',
|
||||
', Tab to change focus': ',Tab 切换焦点',
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.':
|
||||
'要查看更改,必须重启 Qwen Code。按 r 退出并立即应用更改。',
|
||||
'The command "/{{command}}" is not supported in non-interactive mode.':
|
||||
@@ -259,6 +277,12 @@ export default {
|
||||
// ============================================================================
|
||||
'Vim Mode': 'Vim 模式',
|
||||
'Disable Auto Update': '禁用自动更新',
|
||||
'Add AI Co-Author to Commits': '在提交中添加 AI 协作者',
|
||||
'Terminal Bell Notification': '终端响铃通知',
|
||||
'Enable Usage Statistics': '启用使用统计',
|
||||
Theme: '主题',
|
||||
'Preferred Editor': '首选编辑器',
|
||||
'Auto-connect to IDE': '自动连接到 IDE',
|
||||
'Enable Prompt Completion': '启用提示补全',
|
||||
'Debug Keystroke Logging': '调试按键记录',
|
||||
Language: '语言',
|
||||
@@ -266,17 +290,10 @@ export default {
|
||||
'Hide Window Title': '隐藏窗口标题',
|
||||
'Show Status in Title': '在标题中显示状态',
|
||||
'Hide Tips': '隐藏提示',
|
||||
'Hide Banner': '隐藏横幅',
|
||||
'Hide Context Summary': '隐藏上下文摘要',
|
||||
'Hide CWD': '隐藏当前工作目录',
|
||||
'Hide Sandbox Status': '隐藏沙箱状态',
|
||||
'Hide Model Info': '隐藏模型信息',
|
||||
'Hide Footer': '隐藏页脚',
|
||||
'Show Memory Usage': '显示内存使用',
|
||||
'Show Line Numbers': '显示行号',
|
||||
'Show Line Numbers in Code': '在代码中显示行号',
|
||||
'Show Citations': '显示引用',
|
||||
'Custom Witty Phrases': '自定义诙谐短语',
|
||||
'Enable Welcome Back': '启用欢迎回来',
|
||||
'Show Welcome Back Dialog': '显示欢迎回来对话框',
|
||||
'Disable Loading Phrases': '禁用加载短语',
|
||||
'Screen Reader Mode': '屏幕阅读器模式',
|
||||
'IDE Mode': 'IDE 模式',
|
||||
@@ -295,7 +312,7 @@ export default {
|
||||
'Respect .qwenignore': '遵守 .qwenignore',
|
||||
'Enable Recursive File Search': '启用递归文件搜索',
|
||||
'Disable Fuzzy Search': '禁用模糊搜索',
|
||||
'Enable Interactive Shell': '启用交互式 Shell',
|
||||
'Interactive Shell (PTY)': '交互式 Shell (PTY)',
|
||||
'Show Color': '显示颜色',
|
||||
'Auto Accept': '自动接受',
|
||||
'Use Ripgrep': '使用 Ripgrep',
|
||||
@@ -327,6 +344,11 @@ export default {
|
||||
'将目录添加到工作区。使用逗号分隔多个路径',
|
||||
'Show all directories in the workspace': '显示工作区中的所有目录',
|
||||
'set external editor preference': '设置外部编辑器首选项',
|
||||
'Select Editor': '选择编辑器',
|
||||
'Editor Preference': '编辑器首选项',
|
||||
'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.':
|
||||
'当前支持以下编辑器。请注意,某些编辑器无法在沙箱模式下使用。',
|
||||
'Your preferred editor is:': '您的首选编辑器是:',
|
||||
'Manage extensions': '管理扩展',
|
||||
'List active extensions': '列出活动扩展',
|
||||
'Update extensions. Usage: update <extension-names>|--all':
|
||||
@@ -410,7 +432,7 @@ export default {
|
||||
// ============================================================================
|
||||
// Commands - Approval Mode
|
||||
// ============================================================================
|
||||
'Approval Mode': '审批模式',
|
||||
'Tool Approval Mode': '工具审批模式',
|
||||
'Current approval mode: {{mode}}': '当前审批模式:{{mode}}',
|
||||
'Available approval modes:': '可用的审批模式:',
|
||||
'Approval mode changed to: {{mode}}': '审批模式已更改为:{{mode}}',
|
||||
@@ -444,8 +466,6 @@ export default {
|
||||
'Automatically approve all tools': '自动批准所有工具',
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.':
|
||||
'工作区审批模式已存在并具有优先级。用户级别的更改将无效。',
|
||||
'(Use Enter to select, Tab to change focus)':
|
||||
'(使用 Enter 选择,Tab 切换焦点)',
|
||||
'Apply To': '应用于',
|
||||
'User Settings': '用户设置',
|
||||
'Workspace Settings': '工作区设置',
|
||||
@@ -845,13 +865,22 @@ export default {
|
||||
// ============================================================================
|
||||
// Startup Tips
|
||||
// ============================================================================
|
||||
'Tips for getting started:': '入门提示:',
|
||||
'1. Ask questions, edit files, or run commands.':
|
||||
'1. 提问、编辑文件或运行命令',
|
||||
'2. Be specific for the best results.': '2. 具体描述以获得最佳结果',
|
||||
'files to customize your interactions with Qwen Code.':
|
||||
'文件以自定义您与 Qwen Code 的交互',
|
||||
'for more information.': '获取更多信息',
|
||||
'Tips:': '提示:',
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.':
|
||||
'对话变长时用 /compress,总结历史并释放上下文。',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.':
|
||||
'用 /clear 或 /new 开启新思路;之前的会话会保留在历史记录中。',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.':
|
||||
'遇到问题时,用 /bug 将问题提交给维护者。',
|
||||
'Switch auth type quickly with /auth.': '用 /auth 快速切换认证方式。',
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).':
|
||||
'在 Qwen Code 中使用 ! 可运行任意 shell 命令(例如 !ls)。',
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.':
|
||||
'输入 / 打开命令弹窗;按 Tab 自动补全斜杠命令和保存的提示词。',
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.':
|
||||
'运行 qwen --continue 或 qwen --resume 可继续之前的会话。',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.':
|
||||
'按 Shift+Tab 或输入 /approval-mode 可快速切换权限模式。',
|
||||
|
||||
// ============================================================================
|
||||
// Exit Screen / Stats
|
||||
|
||||
@@ -31,6 +31,7 @@ import { quitCommand } from '../ui/commands/quitCommand.js';
|
||||
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
||||
import { resumeCommand } from '../ui/commands/resumeCommand.js';
|
||||
import { settingsCommand } from '../ui/commands/settingsCommand.js';
|
||||
import { skillsCommand } from '../ui/commands/skillsCommand.js';
|
||||
import { statsCommand } from '../ui/commands/statsCommand.js';
|
||||
import { summaryCommand } from '../ui/commands/summaryCommand.js';
|
||||
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
|
||||
@@ -78,6 +79,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
quitCommand,
|
||||
restoreCommand(this.config),
|
||||
resumeCommand,
|
||||
...(this.config?.getExperimentalSkills?.() ? [skillsCommand] : []),
|
||||
statsCommand,
|
||||
summaryCommand,
|
||||
themeCommand,
|
||||
|
||||
@@ -5,34 +5,15 @@
|
||||
*/
|
||||
|
||||
import { useIsScreenReaderEnabled } from 'ink';
|
||||
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
||||
import { lerp } from '../utils/math.js';
|
||||
import { useUIState } from './contexts/UIStateContext.js';
|
||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||
import { QuittingDisplay } from './components/QuittingDisplay.js';
|
||||
import { ScreenReaderAppLayout } from './layouts/ScreenReaderAppLayout.js';
|
||||
import { DefaultAppLayout } from './layouts/DefaultAppLayout.js';
|
||||
|
||||
const getContainerWidth = (terminalWidth: number): string => {
|
||||
if (terminalWidth <= 80) {
|
||||
return '98%';
|
||||
}
|
||||
if (terminalWidth >= 132) {
|
||||
return '90%';
|
||||
}
|
||||
|
||||
// Linearly interpolate between 80 columns (98%) and 132 columns (90%).
|
||||
const t = (terminalWidth - 80) / (132 - 80);
|
||||
const percentage = lerp(98, 90, t);
|
||||
|
||||
return `${Math.round(percentage)}%`;
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
const uiState = useUIState();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
const { columns } = useTerminalSize();
|
||||
const containerWidth = getContainerWidth(columns);
|
||||
|
||||
if (uiState.quittingMessages) {
|
||||
return <QuittingDisplay />;
|
||||
@@ -40,11 +21,7 @@ export const App = () => {
|
||||
|
||||
return (
|
||||
<StreamingContext.Provider value={uiState.streamingState}>
|
||||
{isScreenReaderEnabled ? (
|
||||
<ScreenReaderAppLayout />
|
||||
) : (
|
||||
<DefaultAppLayout width={containerWidth} />
|
||||
)}
|
||||
{isScreenReaderEnabled ? <ScreenReaderAppLayout /> : <DefaultAppLayout />}
|
||||
</StreamingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -294,10 +294,7 @@ describe('AppContainer State Management', () => {
|
||||
// Mock LoadedSettings
|
||||
mockSettings = {
|
||||
merged: {
|
||||
hideBanner: false,
|
||||
hideFooter: false,
|
||||
hideTips: false,
|
||||
showMemoryUsage: false,
|
||||
theme: 'default',
|
||||
ui: {
|
||||
showStatusInTitle: false,
|
||||
@@ -445,10 +442,7 @@ describe('AppContainer State Management', () => {
|
||||
it('handles settings with all display options disabled', () => {
|
||||
const settingsAllHidden = {
|
||||
merged: {
|
||||
hideBanner: true,
|
||||
hideFooter: true,
|
||||
hideTips: true,
|
||||
showMemoryUsage: false,
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
@@ -463,28 +457,6 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('handles settings with memory usage enabled', () => {
|
||||
const settingsWithMemory = {
|
||||
merged: {
|
||||
hideBanner: false,
|
||||
hideFooter: false,
|
||||
hideTips: false,
|
||||
showMemoryUsage: true,
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
expect(() => {
|
||||
render(
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
settings={settingsWithMemory}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>,
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Version Handling', () => {
|
||||
|
||||
@@ -271,7 +271,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
calculatePromptWidths(terminalWidth);
|
||||
return { inputWidth, suggestionsWidth };
|
||||
}, [terminalWidth]);
|
||||
const mainAreaWidth = Math.floor(terminalWidth * 0.9);
|
||||
// Uniform width for bordered box components: accounts for margins and caps at 100
|
||||
const mainAreaWidth = Math.min(terminalWidth - 4, 100);
|
||||
const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100);
|
||||
|
||||
const isValidPath = useCallback((filePath: string): boolean => {
|
||||
@@ -1387,6 +1388,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
const uiActions: UIActions = useMemo(
|
||||
() => ({
|
||||
openThemeDialog,
|
||||
openEditorDialog,
|
||||
handleThemeSelect,
|
||||
handleThemeHighlight,
|
||||
handleApprovalModeSelect,
|
||||
@@ -1424,6 +1427,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
handleResume,
|
||||
}),
|
||||
[
|
||||
openThemeDialog,
|
||||
openEditorDialog,
|
||||
handleThemeSelect,
|
||||
handleThemeHighlight,
|
||||
handleApprovalModeSelect,
|
||||
|
||||
@@ -83,12 +83,26 @@ export const useAuthCommand = (
|
||||
async (authType: AuthType, credentials?: OpenAICredentials) => {
|
||||
try {
|
||||
const authTypeScope = getPersistScopeForModelSelection(settings);
|
||||
|
||||
// Persist authType
|
||||
settings.setValue(
|
||||
authTypeScope,
|
||||
'security.auth.selectedType',
|
||||
authType,
|
||||
);
|
||||
|
||||
// Persist model from ContentGenerator config (handles fallback cases)
|
||||
// This ensures that when syncAfterAuthRefresh falls back to default model,
|
||||
// it gets persisted to settings.json
|
||||
const contentGeneratorConfig = config.getContentGeneratorConfig();
|
||||
if (contentGeneratorConfig?.model) {
|
||||
settings.setValue(
|
||||
authTypeScope,
|
||||
'model.name',
|
||||
contentGeneratorConfig.model,
|
||||
);
|
||||
}
|
||||
|
||||
// Only update credentials if not switching to QWEN_OAUTH,
|
||||
// so that OpenAI credentials are preserved when switching to QWEN_OAUTH.
|
||||
if (authType !== AuthType.QWEN_OAUTH && credentials) {
|
||||
@@ -106,9 +120,6 @@ export const useAuthCommand = (
|
||||
credentials.baseUrl,
|
||||
);
|
||||
}
|
||||
if (credentials?.model != null) {
|
||||
settings.setValue(authTypeScope, 'model.name', credentials.model);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleAuthFailure(error);
|
||||
|
||||
132
packages/cli/src/ui/commands/skillsCommand.ts
Normal file
132
packages/cli/src/ui/commands/skillsCommand.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
CommandKind,
|
||||
type CommandCompletionItem,
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
} from './types.js';
|
||||
import { MessageType, type HistoryItemSkillsList } from '../types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { AsyncFzf } from 'fzf';
|
||||
import type { SkillConfig } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export const skillsCommand: SlashCommand = {
|
||||
name: 'skills',
|
||||
get description() {
|
||||
return t('List available skills.');
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context: CommandContext, args?: string) => {
|
||||
const rawArgs = args?.trim() ?? '';
|
||||
const [skillName = ''] = rawArgs.split(/\s+/);
|
||||
|
||||
const skillManager = context.services.config?.getSkillManager();
|
||||
if (!skillManager) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: t('Could not retrieve skill manager.'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const skills = await skillManager.listSkills();
|
||||
if (skills.length === 0) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: t('No skills are currently available.'),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!skillName) {
|
||||
const sortedSkills = [...skills].sort((left, right) =>
|
||||
left.name.localeCompare(right.name),
|
||||
);
|
||||
const skillsListItem: HistoryItemSkillsList = {
|
||||
type: MessageType.SKILLS_LIST,
|
||||
skills: sortedSkills.map((skill) => ({ name: skill.name })),
|
||||
};
|
||||
context.ui.addItem(skillsListItem, Date.now());
|
||||
return;
|
||||
}
|
||||
const normalizedName = skillName.toLowerCase();
|
||||
const hasSkill = skills.some(
|
||||
(skill) => skill.name.toLowerCase() === normalizedName,
|
||||
);
|
||||
|
||||
if (!hasSkill) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: t('Unknown skill: {{name}}', { name: skillName }),
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawInput = context.invocation?.raw ?? `/skills ${rawArgs}`;
|
||||
return {
|
||||
type: 'submit_prompt',
|
||||
content: [{ text: rawInput }],
|
||||
};
|
||||
},
|
||||
completion: async (
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
): Promise<CommandCompletionItem[]> => {
|
||||
const skillManager = context.services.config?.getSkillManager();
|
||||
if (!skillManager) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const skills = await skillManager.listSkills();
|
||||
const normalizedPartial = partialArg.trim();
|
||||
const matches = await getSkillMatches(skills, normalizedPartial);
|
||||
|
||||
return matches.map((skill) => ({
|
||||
value: skill.name,
|
||||
description: skill.description,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
async function getSkillMatches(
|
||||
skills: SkillConfig[],
|
||||
query: string,
|
||||
): Promise<SkillConfig[]> {
|
||||
if (!query) {
|
||||
return skills;
|
||||
}
|
||||
|
||||
const names = skills.map((skill) => skill.name);
|
||||
const skillMap = new Map(skills.map((skill) => [skill.name, skill]));
|
||||
|
||||
try {
|
||||
const fzf = new AsyncFzf(names, {
|
||||
fuzzy: 'v2',
|
||||
casing: 'case-insensitive',
|
||||
});
|
||||
const results = (await fzf.find(query)) as Array<{ item: string }>;
|
||||
return results
|
||||
.map((result) => skillMap.get(result.item))
|
||||
.filter((skill): skill is SkillConfig => !!skill);
|
||||
} catch (error) {
|
||||
console.error('[skillsCommand] Fuzzy match failed:', error);
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return skills.filter((skill) =>
|
||||
skill.name.toLowerCase().startsWith(lowerQuery),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,12 @@ export enum CommandKind {
|
||||
MCP_PROMPT = 'mcp-prompt',
|
||||
}
|
||||
|
||||
export interface CommandCompletionItem {
|
||||
value: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// The standardized contract for any command in the system.
|
||||
export interface SlashCommand {
|
||||
name: string;
|
||||
@@ -234,7 +240,7 @@ export interface SlashCommand {
|
||||
completion?: (
|
||||
context: CommandContext,
|
||||
partialArg: string,
|
||||
) => Promise<string[]>;
|
||||
) => Promise<Array<string | CommandCompletionItem> | null>;
|
||||
|
||||
subCommands?: SlashCommand[];
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
} from '../../utils/systemInfoFields.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
type AboutBoxProps = ExtendedSystemInfo;
|
||||
type AboutBoxProps = ExtendedSystemInfo & {
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export const AboutBox: React.FC<AboutBoxProps> = (props) => {
|
||||
export const AboutBox: React.FC<AboutBoxProps> = ({ width, ...props }) => {
|
||||
const fields = getSystemInfoFields(props);
|
||||
|
||||
return (
|
||||
@@ -26,8 +28,7 @@ export const AboutBox: React.FC<AboutBoxProps> = (props) => {
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
marginY={1}
|
||||
width="100%"
|
||||
width={width}
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.accent}>
|
||||
|
||||
93
packages/cli/src/ui/components/AppHeader.test.tsx
Normal file
93
packages/cli/src/ui/components/AppHeader.test.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import { type UIState, UIStateContext } from '../contexts/UIStateContext.js';
|
||||
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize);
|
||||
|
||||
const createSettings = (options?: { hideTips?: boolean }): LoadedSettings =>
|
||||
({
|
||||
merged: {
|
||||
ui: {
|
||||
hideTips: options?.hideTips ?? true,
|
||||
},
|
||||
},
|
||||
}) as never;
|
||||
|
||||
const createMockConfig = (overrides = {}) => ({
|
||||
getContentGeneratorConfig: vi.fn(() => ({ authType: undefined })),
|
||||
getModel: vi.fn(() => 'gemini-pro'),
|
||||
getTargetDir: vi.fn(() => '/projects/qwen-code'),
|
||||
getMcpServers: vi.fn(() => ({})),
|
||||
getBlockedMcpServers: vi.fn(() => []),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getScreenReader: vi.fn(() => false),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
({
|
||||
branchName: 'main',
|
||||
nightly: false,
|
||||
debugMessage: '',
|
||||
sessionStats: {
|
||||
lastPromptTokenCount: 0,
|
||||
},
|
||||
...overrides,
|
||||
}) as UIState;
|
||||
|
||||
const renderWithProviders = (
|
||||
uiState: UIState,
|
||||
settings = createSettings(),
|
||||
config = createMockConfig(),
|
||||
) => {
|
||||
useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 });
|
||||
return render(
|
||||
<ConfigContext.Provider value={config as never}>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<VimModeProvider settings={settings}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<AppHeader version="1.2.3" />
|
||||
</UIStateContext.Provider>
|
||||
</VimModeProvider>
|
||||
</SettingsContext.Provider>
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('<AppHeader />', () => {
|
||||
it('shows the working directory', () => {
|
||||
const { lastFrame } = renderWithProviders(createMockUIState());
|
||||
expect(lastFrame()).toContain('/projects/qwen-code');
|
||||
});
|
||||
|
||||
it('hides the header when screen reader is enabled', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
createMockUIState(),
|
||||
createSettings(),
|
||||
createMockConfig({ getScreenReader: vi.fn(() => true) }),
|
||||
);
|
||||
// When screen reader is enabled, header is not rendered
|
||||
expect(lastFrame()).not.toContain('/projects/qwen-code');
|
||||
expect(lastFrame()).not.toContain('Qwen Code');
|
||||
});
|
||||
|
||||
it('shows the header with all info when banner is visible', () => {
|
||||
const { lastFrame } = renderWithProviders(createMockUIState());
|
||||
expect(lastFrame()).toContain('>_ Qwen Code');
|
||||
expect(lastFrame()).toContain('gemini-pro');
|
||||
expect(lastFrame()).toContain('/projects/qwen-code');
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,6 @@ import { Header } from './Header.js';
|
||||
import { Tips } from './Tips.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
interface AppHeaderProps {
|
||||
version: string;
|
||||
@@ -18,16 +17,25 @@ interface AppHeaderProps {
|
||||
export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
const { nightly } = useUIState();
|
||||
|
||||
const contentGeneratorConfig = config.getContentGeneratorConfig();
|
||||
const authType = contentGeneratorConfig?.authType;
|
||||
const model = config.getModel();
|
||||
const targetDir = config.getTargetDir();
|
||||
const showBanner = !config.getScreenReader();
|
||||
const showTips = !(settings.merged.ui?.hideTips || config.getScreenReader());
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{!(settings.merged.ui?.hideBanner || config.getScreenReader()) && (
|
||||
<Header version={version} nightly={nightly} />
|
||||
)}
|
||||
{!(settings.merged.ui?.hideTips || config.getScreenReader()) && (
|
||||
<Tips config={config} />
|
||||
{showBanner && (
|
||||
<Header
|
||||
version={version}
|
||||
authType={authType}
|
||||
model={model}
|
||||
workingDirectory={targetDir}
|
||||
/>
|
||||
)}
|
||||
{showTips && <Tips />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ export function ApprovalModeDialog({
|
||||
}: ApprovalModeDialogProps): React.JSX.Element {
|
||||
// Start with User scope by default
|
||||
const [selectedScope, setSelectedScope] = useState<SettingScope>(
|
||||
SettingScope.Workspace,
|
||||
SettingScope.User,
|
||||
);
|
||||
|
||||
// Track the currently highlighted approval mode
|
||||
@@ -90,19 +90,17 @@ export function ApprovalModeDialog({
|
||||
setSelectedScope(scope);
|
||||
}, []);
|
||||
|
||||
const handleScopeSelect = useCallback(
|
||||
(scope: SettingScope) => {
|
||||
onSelect(highlightedMode, scope);
|
||||
},
|
||||
[onSelect, highlightedMode],
|
||||
);
|
||||
const handleScopeSelect = useCallback((scope: SettingScope) => {
|
||||
setSelectedScope(scope);
|
||||
setMode('mode');
|
||||
}, []);
|
||||
|
||||
const [focusSection, setFocusSection] = useState<'mode' | 'scope'>('mode');
|
||||
const [mode, setMode] = useState<'mode' | 'scope'>('mode');
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'tab') {
|
||||
setFocusSection((prev) => (prev === 'mode' ? 'scope' : 'mode'));
|
||||
setMode((prev) => (prev === 'mode' ? 'scope' : 'mode'));
|
||||
}
|
||||
if (key.name === 'escape') {
|
||||
onSelect(undefined, selectedScope);
|
||||
@@ -127,59 +125,56 @@ export function ApprovalModeDialog({
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="row"
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{/* Approval Mode Selection */}
|
||||
<Text bold={focusSection === 'mode'} wrap="truncate">
|
||||
{focusSection === 'mode' ? '> ' : ' '}
|
||||
{t('Approval Mode')}{' '}
|
||||
<Text color={theme.text.secondary}>{otherScopeModifiedMessage}</Text>
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
<RadioButtonSelect
|
||||
items={modeItems}
|
||||
initialIndex={safeInitialModeIndex}
|
||||
onSelect={handleModeSelect}
|
||||
onHighlight={handleModeHighlight}
|
||||
isFocused={focusSection === 'mode'}
|
||||
maxItemsToShow={10}
|
||||
showScrollArrows={false}
|
||||
showNumbers={focusSection === 'mode'}
|
||||
/>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Scope Selection */}
|
||||
<Box marginTop={1}>
|
||||
<ScopeSelector
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={focusSection === 'scope'}
|
||||
initialScope={selectedScope}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Warning when workspace setting will override user setting */}
|
||||
{showWorkspacePriorityWarning && (
|
||||
<>
|
||||
<Text color={theme.status.warning} wrap="wrap">
|
||||
⚠{' '}
|
||||
{t(
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.',
|
||||
)}
|
||||
{mode === 'mode' ? (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
{/* Approval Mode Selection */}
|
||||
<Text bold={mode === 'mode'} wrap="truncate">
|
||||
{mode === 'mode' ? '> ' : ' '}
|
||||
{t('Approval Mode')}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{otherScopeModifiedMessage}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('(Use Enter to select, Tab to change focus)')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
<RadioButtonSelect
|
||||
items={modeItems}
|
||||
initialIndex={safeInitialModeIndex}
|
||||
onSelect={handleModeSelect}
|
||||
onHighlight={handleModeHighlight}
|
||||
isFocused={mode === 'mode'}
|
||||
maxItemsToShow={10}
|
||||
showScrollArrows={false}
|
||||
showNumbers={mode === 'mode'}
|
||||
/>
|
||||
{/* Warning when workspace setting will override user setting */}
|
||||
{showWorkspacePriorityWarning && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.status.warning} wrap="wrap">
|
||||
⚠{' '}
|
||||
{t(
|
||||
'Workspace approval mode exists and takes priority. User-level change will have no effect.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<ScopeSelector
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={mode === 'scope'}
|
||||
initialScope={selectedScope}
|
||||
/>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{mode === 'mode'
|
||||
? t('(Use Enter to select, Tab to configure scope)')
|
||||
: t('(Use Enter to apply scope, Tab to go back)')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -5,29 +5,10 @@
|
||||
*/
|
||||
|
||||
export const shortAsciiLogo = `
|
||||
██████╗ ██╗ ██╗███████╗███╗ ██╗
|
||||
▄▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄
|
||||
██╔═══██╗██║ ██║██╔════╝████╗ ██║
|
||||
██║ ██║██║ █╗ ██║█████╗ ██╔██╗ ██║
|
||||
██║▄▄ ██║██║███╗██║██╔══╝ ██║╚██╗██║
|
||||
╚██████╔╝╚███╔███╔╝███████╗██║ ╚████║
|
||||
╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝
|
||||
`;
|
||||
export const longAsciiLogo = `
|
||||
██╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗
|
||||
╚██╗ ██╔═══██╗██║ ██║██╔════╝████╗ ██║
|
||||
╚██╗ ██║ ██║██║ █╗ ██║█████╗ ██╔██╗ ██║
|
||||
██╔╝ ██║▄▄ ██║██║███╗██║██╔══╝ ██║╚██╗██║
|
||||
██╔╝ ╚██████╔╝╚███╔███╔╝███████╗██║ ╚████║
|
||||
╚═╝ ╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝
|
||||
`;
|
||||
|
||||
export const tinyAsciiLogo = `
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
╚══▀▀═╝ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═══╝
|
||||
`;
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type UIActions,
|
||||
} from '../contexts/UIActionsContext.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
// Mock VimModeContext hook
|
||||
vi.mock('../contexts/VimModeContext.js', () => ({
|
||||
useVimMode: vi.fn(() => ({
|
||||
@@ -146,92 +145,33 @@ const createMockConfig = (overrides = {}) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockSettings = (merged = {}) => ({
|
||||
merged: {
|
||||
hideFooter: false,
|
||||
showMemoryUsage: false,
|
||||
...merged,
|
||||
},
|
||||
});
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const renderComposer = (
|
||||
uiState: UIState,
|
||||
settings = createMockSettings(),
|
||||
config = createMockConfig(),
|
||||
uiActions = createMockUIActions(),
|
||||
) =>
|
||||
render(
|
||||
<ConfigContext.Provider value={config as any}>
|
||||
<SettingsContext.Provider value={settings as any}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
<Composer />
|
||||
</UIActionsContext.Provider>
|
||||
</UIStateContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
<Composer />
|
||||
</UIActionsContext.Provider>
|
||||
</UIStateContext.Provider>
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('Composer', () => {
|
||||
describe('Footer Display Settings', () => {
|
||||
it('renders Footer by default when hideFooter is false', () => {
|
||||
describe('Footer Display', () => {
|
||||
it('renders Footer by default', () => {
|
||||
const uiState = createMockUIState();
|
||||
const settings = createMockSettings({ hideFooter: false });
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
// Smoke check that the Footer renders when enabled.
|
||||
// Smoke check that the Footer renders
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
|
||||
it('does NOT render Footer when hideFooter is true', () => {
|
||||
const uiState = createMockUIState();
|
||||
const settings = createMockSettings({ hideFooter: true });
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
|
||||
// Check for content that only appears IN the Footer component itself
|
||||
expect(lastFrame()).not.toContain('[NORMAL]'); // Vim mode indicator
|
||||
expect(lastFrame()).not.toContain('(main'); // Branch name with parentheses
|
||||
});
|
||||
|
||||
it('passes correct props to Footer including vim mode when enabled', async () => {
|
||||
const uiState = createMockUIState({
|
||||
branchName: 'feature-branch',
|
||||
errorCount: 2,
|
||||
sessionStats: {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
metrics: {} as any,
|
||||
lastPromptTokenCount: 150,
|
||||
promptCount: 5,
|
||||
},
|
||||
});
|
||||
const config = createMockConfig({
|
||||
getModel: vi.fn(() => 'gemini-1.5-flash'),
|
||||
getTargetDir: vi.fn(() => '/project/path'),
|
||||
getDebugMode: vi.fn(() => true),
|
||||
});
|
||||
const settings = createMockSettings({
|
||||
hideFooter: false,
|
||||
showMemoryUsage: true,
|
||||
});
|
||||
// Mock vim mode for this test
|
||||
const { useVimMode } = await import('../contexts/VimModeContext.js');
|
||||
vi.mocked(useVimMode).mockReturnValueOnce({
|
||||
vimEnabled: true,
|
||||
vimMode: 'INSERT',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings, config);
|
||||
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
// Footer should be rendered with all the state passed through
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loading Indicator', () => {
|
||||
@@ -261,7 +201,7 @@ describe('Composer', () => {
|
||||
getAccessibility: vi.fn(() => ({ disableLoadingPhrases: true })),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, undefined, config);
|
||||
const { lastFrame } = renderComposer(uiState, config);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
@@ -318,7 +258,8 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Context and Status Display', () => {
|
||||
it('shows ContextSummaryDisplay in normal state', () => {
|
||||
// Note: ContextSummaryDisplay and status prompts are now rendered in Footer, not Composer
|
||||
it('shows empty space in normal state (ContextSummaryDisplay moved to Footer)', () => {
|
||||
const uiState = createMockUIState({
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: false,
|
||||
@@ -327,37 +268,43 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ContextSummaryDisplay');
|
||||
// ContextSummaryDisplay is now in Footer, so we just verify normal state renders
|
||||
expect(lastFrame()).toBeDefined();
|
||||
});
|
||||
|
||||
it('shows Ctrl+C exit prompt when ctrlCPressedOnce is true', () => {
|
||||
// Note: Ctrl+C, Ctrl+D, and Escape prompts are now rendered in Footer component
|
||||
// These are tested in Footer.test.tsx
|
||||
it('renders Footer which handles Ctrl+C exit prompt', () => {
|
||||
const uiState = createMockUIState({
|
||||
ctrlCPressedOnce: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('Press Ctrl+C again to exit');
|
||||
// Ctrl+C prompt is now inside Footer, verify Footer renders
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
|
||||
it('shows Ctrl+D exit prompt when ctrlDPressedOnce is true', () => {
|
||||
it('renders Footer which handles Ctrl+D exit prompt', () => {
|
||||
const uiState = createMockUIState({
|
||||
ctrlDPressedOnce: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('Press Ctrl+D again to exit');
|
||||
// Ctrl+D prompt is now inside Footer, verify Footer renders
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
|
||||
it('shows escape prompt when showEscapePrompt is true', () => {
|
||||
it('renders Footer which handles escape prompt', () => {
|
||||
const uiState = createMockUIState({
|
||||
showEscapePrompt: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('Press Esc again to clear');
|
||||
// Escape prompt is now inside Footer, verify Footer renders
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -382,7 +329,9 @@ describe('Composer', () => {
|
||||
expect(lastFrame()).not.toContain('InputPrompt');
|
||||
});
|
||||
|
||||
it('shows AutoAcceptIndicator when approval mode is not default and shell mode is inactive', () => {
|
||||
// Note: AutoAcceptIndicator and ShellModeIndicator are now rendered inside Footer component
|
||||
// These are tested in Footer.test.tsx
|
||||
it('renders Footer which contains AutoAcceptIndicator when approval mode is not default', () => {
|
||||
const uiState = createMockUIState({
|
||||
showAutoAcceptIndicator: ApprovalMode.YOLO,
|
||||
shellModeActive: false,
|
||||
@@ -390,17 +339,19 @@ describe('Composer', () => {
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('AutoAcceptIndicator');
|
||||
// AutoAcceptIndicator is now inside Footer, verify Footer renders
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
|
||||
it('shows ShellModeIndicator when shell mode is active', () => {
|
||||
it('renders Footer which contains ShellModeIndicator when shell mode is active', () => {
|
||||
const uiState = createMockUIState({
|
||||
shellModeActive: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShellModeIndicator');
|
||||
// ShellModeIndicator is now inside Footer, verify Footer renders
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,42 +4,46 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, useIsScreenReaderEnabled } from 'ink';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { ContextSummaryDisplay } from './ContextSummaryDisplay.js';
|
||||
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||
import { InputPrompt, calculatePromptWidths } from './InputPrompt.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||
import { KeyboardShortcuts } from './KeyboardShortcuts.js';
|
||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const Composer = () => {
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
const uiState = useUIState();
|
||||
const uiActions = useUIActions();
|
||||
const { vimEnabled } = useVimMode();
|
||||
const terminalWidth = process.stdout.columns;
|
||||
const isNarrow = isNarrowWidth(terminalWidth);
|
||||
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
|
||||
|
||||
const { contextFileNames, showAutoAcceptIndicator } = uiState;
|
||||
const { showAutoAcceptIndicator } = uiState;
|
||||
|
||||
// State for keyboard shortcuts display toggle
|
||||
const [showShortcuts, setShowShortcuts] = useState(false);
|
||||
const handleToggleShortcuts = useCallback(() => {
|
||||
setShowShortcuts((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// State for suggestions visibility
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const handleSuggestionsVisibilityChange = useCallback((visible: boolean) => {
|
||||
setShowSuggestions(visible);
|
||||
}, []);
|
||||
|
||||
// Use the container width of InputPrompt for width of DetailedMessagesDisplay
|
||||
const { containerWidth } = useMemo(
|
||||
@@ -48,7 +52,7 @@ export const Composer = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{!uiState.embeddedShellFocused && (
|
||||
<LoadingIndicator
|
||||
thought={
|
||||
@@ -70,55 +74,6 @@ export const Composer = () => {
|
||||
|
||||
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
||||
|
||||
<Box
|
||||
marginTop={1}
|
||||
justifyContent={
|
||||
settings.merged.ui?.hideContextSummary
|
||||
? 'flex-start'
|
||||
: 'space-between'
|
||||
}
|
||||
width="100%"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
>
|
||||
<Box marginRight={1}>
|
||||
{process.env['GEMINI_SYSTEM_MD'] && (
|
||||
<Text color={theme.status.error}>|⌐■_■| </Text>
|
||||
)}
|
||||
{uiState.ctrlCPressedOnce ? (
|
||||
<Text color={theme.status.warning}>
|
||||
{t('Press Ctrl+C again to exit.')}
|
||||
</Text>
|
||||
) : uiState.ctrlDPressedOnce ? (
|
||||
<Text color={theme.status.warning}>
|
||||
{t('Press Ctrl+D again to exit.')}
|
||||
</Text>
|
||||
) : uiState.showEscapePrompt ? (
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Press Esc again to clear.')}
|
||||
</Text>
|
||||
) : (
|
||||
!settings.merged.ui?.hideContextSummary && (
|
||||
<ContextSummaryDisplay
|
||||
ideContext={uiState.ideContextState}
|
||||
geminiMdFileCount={uiState.geminiMdFileCount}
|
||||
contextFileNames={contextFileNames}
|
||||
mcpServers={config.getMcpServers()}
|
||||
blockedMcpServers={config.getBlockedMcpServers()}
|
||||
showToolDescriptions={uiState.showToolDescriptions}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
<Box paddingTop={isNarrow ? 1 : 0}>
|
||||
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT &&
|
||||
!uiState.shellModeActive && (
|
||||
<AutoAcceptIndicator approvalMode={showAutoAcceptIndicator} />
|
||||
)}
|
||||
{uiState.shellModeActive && <ShellModeIndicator />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{uiState.showErrorDetails && (
|
||||
<OverflowProvider>
|
||||
<Box flexDirection="column">
|
||||
@@ -149,6 +104,9 @@ export const Composer = () => {
|
||||
setShellModeActive={uiActions.setShellModeActive}
|
||||
approvalMode={showAutoAcceptIndicator}
|
||||
onEscapePromptChange={uiActions.onEscapePromptChange}
|
||||
onToggleShortcuts={handleToggleShortcuts}
|
||||
showShortcuts={showShortcuts}
|
||||
onSuggestionsVisibilityChange={handleSuggestionsVisibilityChange}
|
||||
focus={true}
|
||||
vimHandleInput={uiActions.vimHandleInput}
|
||||
isEmbeddedShellFocused={uiState.embeddedShellFocused}
|
||||
@@ -160,7 +118,13 @@ export const Composer = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!settings.merged.ui?.hideFooter && !isScreenReaderEnabled && <Footer />}
|
||||
{/* Exclusive area: only one component visible at a time */}
|
||||
{!showSuggestions &&
|
||||
(showShortcuts ? (
|
||||
<KeyboardShortcuts />
|
||||
) : (
|
||||
!isScreenReaderEnabled && <Footer />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('ConsentPrompt', () => {
|
||||
{
|
||||
isPending: true,
|
||||
text: prompt,
|
||||
terminalWidth,
|
||||
contentWidth: terminalWidth,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ConsentPrompt = (props: ConsentPromptProps) => {
|
||||
<MarkdownDisplay
|
||||
isPending={true}
|
||||
text={prompt}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={terminalWidth}
|
||||
/>
|
||||
) : (
|
||||
prompt
|
||||
|
||||
@@ -17,15 +17,19 @@ export const ContextUsageDisplay = ({
|
||||
model: string;
|
||||
terminalWidth: number;
|
||||
}) => {
|
||||
const percentage = promptTokenCount / tokenLimit(model);
|
||||
const percentageLeft = ((1 - percentage) * 100).toFixed(0);
|
||||
if (promptTokenCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = terminalWidth < 100 ? '%' : '% context left';
|
||||
const percentage = promptTokenCount / tokenLimit(model);
|
||||
const percentageUsed = (percentage * 100).toFixed(1);
|
||||
|
||||
const label = terminalWidth < 100 ? '% used' : '% context used';
|
||||
|
||||
return (
|
||||
<Text color={theme.text.secondary}>
|
||||
({percentageLeft}
|
||||
{label})
|
||||
{percentageUsed}
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -152,12 +152,38 @@ export const DialogManager = ({
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (uiState.isEditorDialogOpen) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{uiState.editorError && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.status.error}>{uiState.editorError}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<EditorSettingsDialog
|
||||
onSelect={uiActions.handleEditorSelect}
|
||||
settings={settings}
|
||||
onExit={uiActions.exitEditorDialog}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (uiState.isSettingsDialogOpen) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<SettingsDialog
|
||||
settings={settings}
|
||||
onSelect={() => uiActions.closeSettingsDialog()}
|
||||
onSelect={(settingName) => {
|
||||
if (settingName === 'ui.theme') {
|
||||
uiActions.openThemeDialog();
|
||||
return;
|
||||
}
|
||||
if (settingName === 'general.preferredEditor') {
|
||||
uiActions.openEditorDialog();
|
||||
return;
|
||||
}
|
||||
uiActions.closeSettingsDialog();
|
||||
}}
|
||||
onRestartRequest={() => process.exit(0)}
|
||||
availableTerminalHeight={terminalHeight - staticExtraHeight}
|
||||
config={config}
|
||||
@@ -237,22 +263,6 @@ export const DialogManager = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
if (uiState.isEditorDialogOpen) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{uiState.editorError && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.status.error}>{uiState.editorError}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<EditorSettingsDialog
|
||||
onSelect={uiActions.handleEditorSelect}
|
||||
settings={settings}
|
||||
onExit={uiActions.exitEditorDialog}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (uiState.isPermissionsDialogOpen) {
|
||||
return (
|
||||
<PermissionsModifyTrustDialog
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type EditorDisplay,
|
||||
} from '../editors/editorSettingsManager.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { ScopeSelector } from './shared/ScopeSelector.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { EditorType } from '@qwen-code/qwen-code-core';
|
||||
@@ -35,13 +36,12 @@ export function EditorSettingsDialog({
|
||||
const [selectedScope, setSelectedScope] = useState<SettingScope>(
|
||||
SettingScope.User,
|
||||
);
|
||||
const [focusedSection, setFocusedSection] = useState<'editor' | 'scope'>(
|
||||
'editor',
|
||||
);
|
||||
const [mode, setMode] = useState<'editor' | 'scope'>('editor');
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'tab') {
|
||||
setFocusedSection((prev) => (prev === 'editor' ? 'scope' : 'editor'));
|
||||
setMode((prev) => (prev === 'editor' ? 'scope' : 'editor'));
|
||||
}
|
||||
if (key.name === 'escape') {
|
||||
onExit();
|
||||
@@ -65,23 +65,6 @@ export function EditorSettingsDialog({
|
||||
editorIndex = 0;
|
||||
}
|
||||
|
||||
const scopeItems = [
|
||||
{
|
||||
get label() {
|
||||
return t('User Settings');
|
||||
},
|
||||
value: SettingScope.User,
|
||||
key: SettingScope.User,
|
||||
},
|
||||
{
|
||||
get label() {
|
||||
return t('Workspace Settings');
|
||||
},
|
||||
value: SettingScope.Workspace,
|
||||
key: SettingScope.Workspace,
|
||||
},
|
||||
];
|
||||
|
||||
const handleEditorSelect = (editorType: EditorType | 'not_set') => {
|
||||
if (editorType === 'not_set') {
|
||||
onSelect(undefined, selectedScope);
|
||||
@@ -92,7 +75,11 @@ export function EditorSettingsDialog({
|
||||
|
||||
const handleScopeSelect = (scope: SettingScope) => {
|
||||
setSelectedScope(scope);
|
||||
setFocusedSection('editor');
|
||||
setMode('editor');
|
||||
};
|
||||
|
||||
const handleScopeHighlight = (scope: SettingScope) => {
|
||||
setSelectedScope(scope);
|
||||
};
|
||||
|
||||
let otherScopeModifiedMessage = '';
|
||||
@@ -131,54 +118,59 @@ export function EditorSettingsDialog({
|
||||
width="100%"
|
||||
>
|
||||
<Box flexDirection="column" width="45%" paddingRight={2}>
|
||||
<Text bold={focusedSection === 'editor'}>
|
||||
{focusedSection === 'editor' ? '> ' : ' '}Select Editor{' '}
|
||||
<Text color={theme.text.secondary}>{otherScopeModifiedMessage}</Text>
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={editorItems.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
disabled: item.disabled,
|
||||
key: item.type,
|
||||
}))}
|
||||
initialIndex={editorIndex}
|
||||
onSelect={handleEditorSelect}
|
||||
isFocused={focusedSection === 'editor'}
|
||||
key={selectedScope}
|
||||
/>
|
||||
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold={focusedSection === 'scope'}>
|
||||
{focusedSection === 'scope' ? '> ' : ' '}
|
||||
{t('Apply To')}
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
initialIndex={0}
|
||||
{mode === 'editor' ? (
|
||||
<Box flexDirection="column">
|
||||
<Text bold={mode === 'editor'} wrap="truncate">
|
||||
{mode === 'editor' ? '> ' : ' '}
|
||||
{t('Select Editor')}{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
{otherScopeModifiedMessage}
|
||||
</Text>
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
<RadioButtonSelect
|
||||
items={editorItems.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
disabled: item.disabled,
|
||||
key: item.type,
|
||||
}))}
|
||||
initialIndex={editorIndex}
|
||||
onSelect={handleEditorSelect}
|
||||
isFocused={mode === 'editor'}
|
||||
key={selectedScope}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<ScopeSelector
|
||||
onSelect={handleScopeSelect}
|
||||
isFocused={focusedSection === 'scope'}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={mode === 'scope'}
|
||||
initialScope={selectedScope}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
(Use Enter to select, Tab to change focus)
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{mode === 'editor'
|
||||
? t('(Use Enter to select, Tab to configure scope)')
|
||||
: t('(Use Enter to apply scope, Tab to go back)')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" width="55%" paddingLeft={2}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Editor Preference
|
||||
{t('Editor Preference')}
|
||||
</Text>
|
||||
<Box flexDirection="column" gap={1} marginTop={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
These editors are currently supported. Please note that some editors
|
||||
cannot be used in sandbox mode.
|
||||
{t(
|
||||
'These editors are currently supported. Please note that some editors cannot be used in sandbox mode.',
|
||||
)}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Your preferred editor is:{' '}
|
||||
{t('Your preferred editor is:')}{' '}
|
||||
<Text
|
||||
color={
|
||||
mergedEditorName === 'None'
|
||||
|
||||
@@ -8,41 +8,23 @@ import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Footer } from './Footer.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
import { tildeifyPath } from '@qwen-code/qwen-code-core';
|
||||
import { type UIState, UIStateContext } from '../contexts/UIStateContext.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize);
|
||||
|
||||
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
|
||||
return {
|
||||
...original,
|
||||
shortenPath: (p: string, len: number) => {
|
||||
if (p.length > len) {
|
||||
return '...' + p.slice(p.length - len + 3);
|
||||
}
|
||||
return p;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
model: 'gemini-pro',
|
||||
targetDir:
|
||||
'/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long',
|
||||
branchName: 'main',
|
||||
};
|
||||
|
||||
const createMockConfig = (overrides = {}) => ({
|
||||
getModel: vi.fn(() => defaultProps.model),
|
||||
getTargetDir: vi.fn(() => defaultProps.targetDir),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getMcpServers: vi.fn(() => ({})),
|
||||
getBlockedMcpServers: vi.fn(() => []),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -51,46 +33,31 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
sessionStats: {
|
||||
lastPromptTokenCount: 100,
|
||||
},
|
||||
branchName: defaultProps.branchName,
|
||||
geminiMdFileCount: 0,
|
||||
contextFileNames: [],
|
||||
showToolDescriptions: false,
|
||||
ideContextState: undefined,
|
||||
...overrides,
|
||||
}) as UIState;
|
||||
|
||||
const createDefaultSettings = (
|
||||
options: {
|
||||
showMemoryUsage?: boolean;
|
||||
hideCWD?: boolean;
|
||||
hideSandboxStatus?: boolean;
|
||||
hideModelInfo?: boolean;
|
||||
} = {},
|
||||
): LoadedSettings =>
|
||||
const createMockSettings = (): LoadedSettings =>
|
||||
({
|
||||
merged: {
|
||||
ui: {
|
||||
showMemoryUsage: options.showMemoryUsage,
|
||||
footer: {
|
||||
hideCWD: options.hideCWD,
|
||||
hideSandboxStatus: options.hideSandboxStatus,
|
||||
hideModelInfo: options.hideModelInfo,
|
||||
},
|
||||
general: {
|
||||
vimMode: false,
|
||||
},
|
||||
},
|
||||
}) as never;
|
||||
}) as LoadedSettings;
|
||||
|
||||
const renderWithWidth = (
|
||||
width: number,
|
||||
uiState: UIState,
|
||||
settings: LoadedSettings = createDefaultSettings(),
|
||||
) => {
|
||||
const renderWithWidth = (width: number, uiState: UIState) => {
|
||||
useTerminalSizeMock.mockReturnValue({ columns: width, rows: 24 });
|
||||
return render(
|
||||
<ConfigContext.Provider value={createMockConfig() as never}>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<VimModeProvider settings={settings}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<Footer />
|
||||
</UIStateContext.Provider>
|
||||
</VimModeProvider>
|
||||
</SettingsContext.Provider>
|
||||
<VimModeProvider settings={createMockSettings()}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<Footer />
|
||||
</UIStateContext.Provider>
|
||||
</VimModeProvider>
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
};
|
||||
@@ -101,161 +68,28 @@ describe('<Footer />', () => {
|
||||
expect(lastFrame()).toBeDefined();
|
||||
});
|
||||
|
||||
describe('path display', () => {
|
||||
it('should display a shortened path on a narrow terminal', () => {
|
||||
const { lastFrame } = renderWithWidth(79, createMockUIState());
|
||||
const tildePath = tildeifyPath(defaultProps.targetDir);
|
||||
const pathLength = Math.max(20, Math.floor(79 * 0.25));
|
||||
const expectedPath =
|
||||
'...' + tildePath.slice(tildePath.length - pathLength + 3);
|
||||
expect(lastFrame()).toContain(expectedPath);
|
||||
});
|
||||
|
||||
it('should use wide layout at 80 columns', () => {
|
||||
const { lastFrame } = renderWithWidth(80, createMockUIState());
|
||||
const tildePath = tildeifyPath(defaultProps.targetDir);
|
||||
const expectedPath =
|
||||
'...' + tildePath.slice(tildePath.length - 80 * 0.25 + 3);
|
||||
expect(lastFrame()).toContain(expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the branch name when provided', () => {
|
||||
it('does not display the working directory or branch name', () => {
|
||||
const { lastFrame } = renderWithWidth(120, createMockUIState());
|
||||
expect(lastFrame()).toContain(`(${defaultProps.branchName}*)`);
|
||||
expect(lastFrame()).not.toMatch(/\(.*\*\)/);
|
||||
});
|
||||
|
||||
it('does not display the branch name when not provided', () => {
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState({
|
||||
branchName: undefined,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).not.toContain(`(${defaultProps.branchName}*)`);
|
||||
});
|
||||
|
||||
it('displays the model name and context percentage', () => {
|
||||
it('displays the context percentage', () => {
|
||||
const { lastFrame } = renderWithWidth(120, createMockUIState());
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\(\d+% context left\)/);
|
||||
expect(lastFrame()).toMatch(/\d+(\.\d+)?% context used/);
|
||||
});
|
||||
|
||||
it('displays the model name and abbreviated context percentage', () => {
|
||||
it('displays the abbreviated context percentage on narrow terminal', () => {
|
||||
const { lastFrame } = renderWithWidth(99, createMockUIState());
|
||||
expect(lastFrame()).toContain(defaultProps.model);
|
||||
expect(lastFrame()).toMatch(/\(\d+%\)/);
|
||||
expect(lastFrame()).toMatch(/\d+%/);
|
||||
});
|
||||
|
||||
describe('sandbox and trust info', () => {
|
||||
it('should display untrusted when isTrustedFolder is false', () => {
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState({
|
||||
isTrustedFolder: false,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toContain('untrusted');
|
||||
});
|
||||
|
||||
it('should display custom sandbox info when SANDBOX env is set', () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState({
|
||||
isTrustedFolder: undefined,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toContain('test');
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', () => {
|
||||
vi.stubEnv('SANDBOX', 'sandbox-exec');
|
||||
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState({
|
||||
isTrustedFolder: true,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should display "no sandbox" when SANDBOX is not set and folder is trusted', () => {
|
||||
// Clear any SANDBOX env var that might be set.
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState({
|
||||
isTrustedFolder: true,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toContain('no sandbox');
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should prioritize untrusted message over sandbox info', () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState({
|
||||
isTrustedFolder: false,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toContain('untrusted');
|
||||
expect(lastFrame()).not.toMatch(/test-sandbox/s);
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
});
|
||||
|
||||
describe('footer configuration filtering (golden snapshots)', () => {
|
||||
it('renders complete footer with all sections visible (baseline)', () => {
|
||||
describe('footer rendering (golden snapshots)', () => {
|
||||
it('renders complete footer on wide terminal', () => {
|
||||
const { lastFrame } = renderWithWidth(120, createMockUIState());
|
||||
expect(lastFrame()).toMatchSnapshot('complete-footer-wide');
|
||||
});
|
||||
|
||||
it('renders footer with all optional sections hidden (minimal footer)', () => {
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState(),
|
||||
createDefaultSettings({
|
||||
hideCWD: true,
|
||||
hideSandboxStatus: true,
|
||||
hideModelInfo: true,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('footer-minimal');
|
||||
});
|
||||
|
||||
it('renders footer with only model info hidden (partial filtering)', () => {
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState(),
|
||||
createDefaultSettings({
|
||||
hideCWD: false,
|
||||
hideSandboxStatus: false,
|
||||
hideModelInfo: true,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('footer-no-model');
|
||||
});
|
||||
|
||||
it('renders footer with CWD and model info hidden to test alignment (only sandbox visible)', () => {
|
||||
const { lastFrame } = renderWithWidth(
|
||||
120,
|
||||
createMockUIState(),
|
||||
createDefaultSettings({
|
||||
hideCWD: true,
|
||||
hideSandboxStatus: false,
|
||||
hideModelInfo: true,
|
||||
}),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('footer-only-sandbox');
|
||||
});
|
||||
|
||||
it('renders complete footer in narrow terminal (baseline narrow)', () => {
|
||||
it('renders complete footer on narrow terminal', () => {
|
||||
const { lastFrame } = renderWithWidth(79, createMockUIState());
|
||||
expect(lastFrame()).toMatchSnapshot('complete-footer-narrow');
|
||||
});
|
||||
|
||||
@@ -7,159 +7,134 @@
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { DebugProfiler } from './DebugProfiler.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
const { vimEnabled, vimMode } = useVimMode();
|
||||
|
||||
const {
|
||||
model,
|
||||
targetDir,
|
||||
debugMode,
|
||||
branchName,
|
||||
debugMessage,
|
||||
errorCount,
|
||||
showErrorDetails,
|
||||
promptTokenCount,
|
||||
nightly,
|
||||
isTrustedFolder,
|
||||
showAutoAcceptIndicator,
|
||||
} = {
|
||||
model: config.getModel(),
|
||||
targetDir: config.getTargetDir(),
|
||||
debugMode: config.getDebugMode(),
|
||||
branchName: uiState.branchName,
|
||||
debugMessage: uiState.debugMessage,
|
||||
errorCount: uiState.errorCount,
|
||||
showErrorDetails: uiState.showErrorDetails,
|
||||
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
||||
nightly: uiState.nightly,
|
||||
isTrustedFolder: uiState.isTrustedFolder,
|
||||
showAutoAcceptIndicator: uiState.showAutoAcceptIndicator,
|
||||
};
|
||||
|
||||
const showMemoryUsage =
|
||||
config.getDebugMode() || settings.merged.ui?.showMemoryUsage || false;
|
||||
const hideCWD = settings.merged.ui?.footer?.hideCWD || false;
|
||||
const hideSandboxStatus =
|
||||
settings.merged.ui?.footer?.hideSandboxStatus || false;
|
||||
const hideModelInfo = settings.merged.ui?.footer?.hideModelInfo || false;
|
||||
const showErrorIndicator = !showErrorDetails && errorCount > 0;
|
||||
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
const isNarrow = isNarrowWidth(terminalWidth);
|
||||
|
||||
const pathLength = Math.max(20, Math.floor(terminalWidth * 0.25));
|
||||
const displayPath = shortenPath(tildeifyPath(targetDir), pathLength);
|
||||
// Determine sandbox info from environment
|
||||
const sandboxEnv = process.env['SANDBOX'];
|
||||
const sandboxInfo = sandboxEnv
|
||||
? sandboxEnv === 'sandbox-exec'
|
||||
? 'seatbelt'
|
||||
: sandboxEnv.startsWith('qwen-code')
|
||||
? 'docker'
|
||||
: sandboxEnv
|
||||
: null;
|
||||
|
||||
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
|
||||
const displayVimMode = vimEnabled ? vimMode : undefined;
|
||||
// Check if debug mode is enabled
|
||||
const debugMode = config.getDebugMode();
|
||||
|
||||
// Left section should show exactly ONE thing at any time, in priority order.
|
||||
const leftContent = uiState.ctrlCPressedOnce ? (
|
||||
<Text color={theme.status.warning}>{t('Press Ctrl+C again to exit.')}</Text>
|
||||
) : uiState.ctrlDPressedOnce ? (
|
||||
<Text color={theme.status.warning}>{t('Press Ctrl+D again to exit.')}</Text>
|
||||
) : uiState.showEscapePrompt ? (
|
||||
<Text color={theme.text.secondary}>{t('Press Esc again to clear.')}</Text>
|
||||
) : vimEnabled && vimMode === 'INSERT' ? (
|
||||
<Text color={theme.text.secondary}>-- INSERT --</Text>
|
||||
) : uiState.shellModeActive ? (
|
||||
<ShellModeIndicator />
|
||||
) : showAutoAcceptIndicator !== undefined &&
|
||||
showAutoAcceptIndicator !== ApprovalMode.DEFAULT ? (
|
||||
<AutoAcceptIndicator approvalMode={showAutoAcceptIndicator} />
|
||||
) : (
|
||||
<Text color={theme.text.secondary}>{t('? for shortcuts')}</Text>
|
||||
);
|
||||
|
||||
const rightItems: Array<{ key: string; node: React.ReactNode }> = [];
|
||||
if (sandboxInfo) {
|
||||
rightItems.push({
|
||||
key: 'sandbox',
|
||||
node: <Text color={theme.status.success}>🔒 {sandboxInfo}</Text>,
|
||||
});
|
||||
}
|
||||
if (debugMode) {
|
||||
rightItems.push({
|
||||
key: 'debug',
|
||||
node: <Text color={theme.status.warning}>Debug Mode</Text>,
|
||||
});
|
||||
}
|
||||
if (promptTokenCount > 0) {
|
||||
rightItems.push({
|
||||
key: 'context',
|
||||
node: (
|
||||
<Text color={theme.text.accent}>
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={promptTokenCount}
|
||||
model={model}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}
|
||||
if (showErrorIndicator) {
|
||||
rightItems.push({
|
||||
key: 'errors',
|
||||
node: <ConsoleSummaryDisplay errorCount={errorCount} />,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
justifyContent={justifyContent}
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
>
|
||||
{(debugMode || displayVimMode || !hideCWD) && (
|
||||
<Box>
|
||||
{debugMode && <DebugProfiler />}
|
||||
{displayVimMode && (
|
||||
<Text color={theme.text.secondary}>[{displayVimMode}] </Text>
|
||||
)}
|
||||
{!hideCWD &&
|
||||
(nightly ? (
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
<Text>
|
||||
{displayPath}
|
||||
{branchName && <Text> ({branchName}*)</Text>}
|
||||
</Text>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Text color={theme.text.link}>
|
||||
{displayPath}
|
||||
{branchName && (
|
||||
<Text color={theme.text.secondary}> ({branchName}*)</Text>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
{debugMode && (
|
||||
<Text color={theme.status.error}>
|
||||
{' ' + (debugMessage || '--debug')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{/* Left Section: Exactly one status line (exit prompts / mode indicator / default hint) */}
|
||||
<Box
|
||||
marginLeft={2}
|
||||
justifyContent="flex-start"
|
||||
flexDirection={isNarrow ? 'column' : 'row'}
|
||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||
>
|
||||
{leftContent}
|
||||
</Box>
|
||||
|
||||
{/* Middle Section: Centered Trust/Sandbox Info */}
|
||||
{!hideSandboxStatus && (
|
||||
<Box
|
||||
flexGrow={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
display="flex"
|
||||
>
|
||||
{isTrustedFolder === false ? (
|
||||
<Text color={theme.status.warning}>untrusted</Text>
|
||||
) : process.env['SANDBOX'] &&
|
||||
process.env['SANDBOX'] !== 'sandbox-exec' ? (
|
||||
<Text color="green">
|
||||
{process.env['SANDBOX'].replace(/^gemini-(?:cli-)?/, '')}
|
||||
</Text>
|
||||
) : process.env['SANDBOX'] === 'sandbox-exec' ? (
|
||||
<Text color={theme.status.warning}>
|
||||
macOS Seatbelt{' '}
|
||||
<Text color={theme.text.secondary}>
|
||||
({process.env['SEATBELT_PROFILE']})
|
||||
</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={theme.status.error}>
|
||||
no sandbox
|
||||
{terminalWidth >= 100 && (
|
||||
<Text color={theme.text.secondary}> (see /docs)</Text>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Right Section: Gemini Label and Console Summary */}
|
||||
{!hideModelInfo && (
|
||||
<Box alignItems="center" justifyContent="flex-end">
|
||||
<Box alignItems="center">
|
||||
<Text color={theme.text.accent}>
|
||||
{model}{' '}
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={promptTokenCount}
|
||||
model={model}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
</Text>
|
||||
{showMemoryUsage && <MemoryUsageDisplay />}
|
||||
{/* Right Section: Sandbox Info, Debug Mode, Context Usage, and Console Summary */}
|
||||
<Box alignItems="center" justifyContent="flex-end" marginRight={2}>
|
||||
{rightItems.map(({ key, node }, index) => (
|
||||
<Box key={key} alignItems="center">
|
||||
{index > 0 && <Text color={theme.text.secondary}> | </Text>}
|
||||
{node}
|
||||
</Box>
|
||||
<Box alignItems="center" paddingLeft={2}>
|
||||
{!showErrorDetails && errorCount > 0 && (
|
||||
<Box>
|
||||
<Text color={theme.ui.symbol}>| </Text>
|
||||
<ConsoleSummaryDisplay errorCount={errorCount} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,39 +6,96 @@
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import { Header } from './Header.js';
|
||||
import * as useTerminalSize from '../hooks/useTerminalSize.js';
|
||||
import { longAsciiLogo } from './AsciiArt.js';
|
||||
|
||||
vi.mock('../hooks/useTerminalSize.js');
|
||||
const useTerminalSizeMock = vi.mocked(useTerminalSize.useTerminalSize);
|
||||
|
||||
const defaultProps = {
|
||||
version: '1.0.0',
|
||||
authType: AuthType.QWEN_OAUTH,
|
||||
model: 'qwen-coder-plus',
|
||||
workingDirectory: '/home/user/projects/test',
|
||||
};
|
||||
|
||||
describe('<Header />', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
it('renders the long logo on a wide terminal', () => {
|
||||
vi.spyOn(useTerminalSize, 'useTerminalSize').mockReturnValue({
|
||||
columns: 120,
|
||||
rows: 20,
|
||||
});
|
||||
const { lastFrame } = render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(lastFrame()).toContain(longAsciiLogo);
|
||||
beforeEach(() => {
|
||||
// Default to wide terminal (shows both logo and info panel)
|
||||
useTerminalSizeMock.mockReturnValue({ columns: 120, rows: 24 });
|
||||
});
|
||||
|
||||
it('renders custom ASCII art when provided', () => {
|
||||
it('renders the ASCII logo on wide terminal', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Check that parts of the shortAsciiLogo are rendered
|
||||
expect(lastFrame()).toContain('██╔═══██╗');
|
||||
});
|
||||
|
||||
it('hides the ASCII logo on narrow terminal', () => {
|
||||
useTerminalSizeMock.mockReturnValue({ columns: 60, rows: 24 });
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Should not contain the logo but still show the info panel
|
||||
expect(lastFrame()).not.toContain('██╔═══██╗');
|
||||
expect(lastFrame()).toContain('>_ Qwen Code');
|
||||
});
|
||||
|
||||
it('renders custom ASCII art when provided on wide terminal', () => {
|
||||
const customArt = 'CUSTOM ART';
|
||||
const { lastFrame } = render(
|
||||
<Header version="1.0.0" nightly={false} customAsciiArt={customArt} />,
|
||||
<Header {...defaultProps} customAsciiArt={customArt} />,
|
||||
);
|
||||
expect(lastFrame()).toContain(customArt);
|
||||
});
|
||||
|
||||
it('displays the version number when nightly is true', () => {
|
||||
const { lastFrame } = render(<Header version="1.0.0" nightly={true} />);
|
||||
it('displays the version number', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('v1.0.0');
|
||||
});
|
||||
|
||||
it('does not display the version number when nightly is false', () => {
|
||||
const { lastFrame } = render(<Header version="1.0.0" nightly={false} />);
|
||||
expect(lastFrame()).not.toContain('v1.0.0');
|
||||
it('displays Qwen Code title with >_ prefix', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('>_ Qwen Code');
|
||||
});
|
||||
|
||||
it('displays auth type and model', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('Qwen OAuth');
|
||||
expect(lastFrame()).toContain('qwen-coder-plus');
|
||||
});
|
||||
|
||||
it('displays working directory', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
expect(lastFrame()).toContain('/home/user/projects/test');
|
||||
});
|
||||
|
||||
it('renders a custom working directory display', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} workingDirectory="custom display" />,
|
||||
);
|
||||
expect(lastFrame()).toContain('custom display');
|
||||
});
|
||||
|
||||
it('displays working directory without branch name', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Branch name is no longer shown in header
|
||||
expect(lastFrame()).toContain('/home/user/projects/test');
|
||||
expect(lastFrame()).not.toContain('(main*)');
|
||||
});
|
||||
|
||||
it('formats home directory with tilde', () => {
|
||||
const { lastFrame } = render(
|
||||
<Header {...defaultProps} workingDirectory="/Users/testuser/projects" />,
|
||||
);
|
||||
// The actual home dir replacement depends on os.homedir()
|
||||
// Just verify the path is shown
|
||||
expect(lastFrame()).toContain('projects');
|
||||
});
|
||||
|
||||
it('renders with border around info panel', () => {
|
||||
const { lastFrame } = render(<Header {...defaultProps} />);
|
||||
// Check for border characters (round border style uses these)
|
||||
expect(lastFrame()).toContain('╭');
|
||||
expect(lastFrame()).toContain('╯');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,64 +7,175 @@
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { AuthType, shortenPath, tildeifyPath } from '@qwen-code/qwen-code-core';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { shortAsciiLogo, longAsciiLogo, tinyAsciiLogo } from './AsciiArt.js';
|
||||
import { getAsciiArtWidth } from '../utils/textUtils.js';
|
||||
import { shortAsciiLogo } from './AsciiArt.js';
|
||||
import { getAsciiArtWidth, getCachedStringWidth } from '../utils/textUtils.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
|
||||
interface HeaderProps {
|
||||
customAsciiArt?: string; // For user-defined ASCII art
|
||||
version: string;
|
||||
nightly: boolean;
|
||||
authType?: AuthType;
|
||||
model: string;
|
||||
workingDirectory: string;
|
||||
}
|
||||
|
||||
function titleizeAuthType(value: string): string {
|
||||
return value
|
||||
.split(/[-_]/g)
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
if (part.toLowerCase() === 'ai') {
|
||||
return 'AI';
|
||||
}
|
||||
return part.charAt(0).toUpperCase() + part.slice(1);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Format auth type for display
|
||||
function formatAuthType(authType?: AuthType): string {
|
||||
if (!authType) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
switch (authType) {
|
||||
case AuthType.QWEN_OAUTH:
|
||||
return 'Qwen OAuth';
|
||||
case AuthType.USE_OPENAI:
|
||||
return 'OpenAI';
|
||||
case AuthType.USE_GEMINI:
|
||||
return 'Gemini';
|
||||
case AuthType.USE_VERTEX_AI:
|
||||
return 'Vertex AI';
|
||||
case AuthType.USE_ANTHROPIC:
|
||||
return 'Anthropic';
|
||||
default:
|
||||
return titleizeAuthType(String(authType));
|
||||
}
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({
|
||||
customAsciiArt,
|
||||
version,
|
||||
nightly,
|
||||
authType,
|
||||
model,
|
||||
workingDirectory,
|
||||
}) => {
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
let displayTitle;
|
||||
const widthOfLongLogo = getAsciiArtWidth(longAsciiLogo);
|
||||
const widthOfShortLogo = getAsciiArtWidth(shortAsciiLogo);
|
||||
|
||||
if (customAsciiArt) {
|
||||
displayTitle = customAsciiArt;
|
||||
} else if (terminalWidth >= widthOfLongLogo) {
|
||||
displayTitle = longAsciiLogo;
|
||||
} else if (terminalWidth >= widthOfShortLogo) {
|
||||
displayTitle = shortAsciiLogo;
|
||||
} else {
|
||||
displayTitle = tinyAsciiLogo;
|
||||
}
|
||||
const displayLogo = customAsciiArt ?? shortAsciiLogo;
|
||||
const logoWidth = getAsciiArtWidth(displayLogo);
|
||||
const formattedAuthType = formatAuthType(authType);
|
||||
|
||||
const artWidth = getAsciiArtWidth(displayTitle);
|
||||
// Calculate available space properly:
|
||||
// First determine if logo can be shown, then use remaining space for path
|
||||
const containerMarginX = 2; // marginLeft + marginRight on the outer container
|
||||
const logoGap = 2; // Gap between logo and info panel
|
||||
const infoPanelPaddingX = 1;
|
||||
const infoPanelBorderWidth = 2; // left + right border
|
||||
const infoPanelChromeWidth = infoPanelBorderWidth + infoPanelPaddingX * 2;
|
||||
const minPathLength = 40; // Minimum readable path length
|
||||
const minInfoPanelWidth = minPathLength + infoPanelChromeWidth;
|
||||
|
||||
const availableTerminalWidth = Math.max(
|
||||
0,
|
||||
terminalWidth - containerMarginX * 2,
|
||||
);
|
||||
|
||||
// Check if we have enough space for logo + gap + minimum info panel
|
||||
const showLogo =
|
||||
availableTerminalWidth >= logoWidth + logoGap + minInfoPanelWidth;
|
||||
|
||||
// Calculate available width for info panel (use all remaining space)
|
||||
// Cap at 60 when in two-column layout (with logo)
|
||||
const maxInfoPanelWidth = 60;
|
||||
const availableInfoPanelWidth = showLogo
|
||||
? Math.min(availableTerminalWidth - logoWidth - logoGap, maxInfoPanelWidth)
|
||||
: availableTerminalWidth;
|
||||
|
||||
// Calculate max path length (subtract padding/borders from available space)
|
||||
const maxPathLength = Math.max(
|
||||
0,
|
||||
availableInfoPanelWidth - infoPanelChromeWidth,
|
||||
);
|
||||
|
||||
const infoPanelContentWidth = Math.max(
|
||||
0,
|
||||
availableInfoPanelWidth - infoPanelChromeWidth,
|
||||
);
|
||||
const authModelText = `${formattedAuthType} | ${model}`;
|
||||
const authHintText = ' (/auth to change)';
|
||||
const showAuthHint =
|
||||
infoPanelContentWidth > 0 &&
|
||||
getCachedStringWidth(authModelText + authHintText) <= infoPanelContentWidth;
|
||||
|
||||
// Now shorten the path to fit the available space
|
||||
const tildeifiedPath = tildeifyPath(workingDirectory);
|
||||
const shortenedPath = shortenPath(tildeifiedPath, Math.max(3, maxPathLength));
|
||||
const displayPath =
|
||||
maxPathLength <= 0
|
||||
? ''
|
||||
: shortenedPath.length > maxPathLength
|
||||
? shortenedPath.slice(0, maxPathLength)
|
||||
: shortenedPath;
|
||||
|
||||
// Use theme gradient colors if available, otherwise use text colors (excluding primary)
|
||||
const gradientColors = theme.ui.gradient || [
|
||||
theme.text.secondary,
|
||||
theme.text.link,
|
||||
theme.text.accent,
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignItems="flex-start"
|
||||
width={artWidth}
|
||||
flexShrink={0}
|
||||
flexDirection="column"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
marginX={containerMarginX}
|
||||
width={availableTerminalWidth}
|
||||
>
|
||||
{theme.ui.gradient ? (
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
<Text>{displayTitle}</Text>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Text>{displayTitle}</Text>
|
||||
)}
|
||||
{nightly && (
|
||||
<Box width="100%" flexDirection="row" justifyContent="flex-end">
|
||||
{theme.ui.gradient ? (
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
<Text>v{version}</Text>
|
||||
{/* Left side: ASCII logo (only if enough space) */}
|
||||
{showLogo && (
|
||||
<>
|
||||
<Box flexShrink={0}>
|
||||
<Gradient colors={gradientColors}>
|
||||
<Text>{displayLogo}</Text>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Text>v{version}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{/* Fixed gap between logo and info panel */}
|
||||
<Box width={logoGap} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Right side: Info panel (flexible width, max 60 in two-column layout) */}
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
paddingX={infoPanelPaddingX}
|
||||
flexGrow={showLogo ? 0 : 1}
|
||||
width={showLogo ? availableInfoPanelWidth : undefined}
|
||||
>
|
||||
{/* Title line: >_ Qwen Code (v{version}) */}
|
||||
<Text>
|
||||
<Text bold color={theme.text.accent}>
|
||||
>_ Qwen Code
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> (v{version})</Text>
|
||||
</Text>
|
||||
{/* Empty line for spacing */}
|
||||
<Text> </Text>
|
||||
{/* Auth and Model line */}
|
||||
<Text>
|
||||
<Text color={theme.text.secondary}>{authModelText}</Text>
|
||||
{showAuthHint && (
|
||||
<Text color={theme.text.secondary}>{authHintText}</Text>
|
||||
)}
|
||||
</Text>
|
||||
{/* Directory line */}
|
||||
<Text color={theme.text.secondary}>{displayPath}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,15 +12,16 @@ import { t } from '../../i18n/index.js';
|
||||
|
||||
interface Help {
|
||||
commands: readonly SlashCommand[];
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const Help: React.FC<Help> = ({ commands }) => (
|
||||
export const Help: React.FC<Help> = ({ commands, width }) => (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginBottom={1}
|
||||
borderColor={theme.border.default}
|
||||
borderStyle="round"
|
||||
padding={1}
|
||||
width={width}
|
||||
>
|
||||
{/* Basics */}
|
||||
<Text bold color={theme.text.primary}>
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Help } from './Help.js';
|
||||
import type { SlashCommand } from '../commands/types.js';
|
||||
import { ExtensionsList } from './views/ExtensionsList.js';
|
||||
import { getMCPServerStatus } from '@qwen-code/qwen-code-core';
|
||||
import { SkillsList } from './views/SkillsList.js';
|
||||
import { ToolsList } from './views/ToolsList.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
|
||||
@@ -37,6 +38,7 @@ interface HistoryItemDisplayProps {
|
||||
item: HistoryItem;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
mainAreaWidth?: number;
|
||||
isPending: boolean;
|
||||
isFocused?: boolean;
|
||||
commands?: readonly SlashCommand[];
|
||||
@@ -49,6 +51,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
item,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
mainAreaWidth,
|
||||
isPending,
|
||||
commands,
|
||||
isFocused = true,
|
||||
@@ -57,9 +60,16 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
availableTerminalHeightGemini,
|
||||
}) => {
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
const contentWidth = terminalWidth - 4;
|
||||
const boxWidth = mainAreaWidth || contentWidth;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={itemForDisplay.id}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
key={itemForDisplay.id}
|
||||
marginLeft={2}
|
||||
marginRight={2}
|
||||
>
|
||||
{/* Render standard message types */}
|
||||
{itemForDisplay.type === 'user' && (
|
||||
<UserMessage text={itemForDisplay.text} />
|
||||
@@ -74,7 +84,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightGemini ?? availableTerminalHeight
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_content' && (
|
||||
@@ -84,7 +94,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightGemini ?? availableTerminalHeight
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought' && (
|
||||
@@ -94,7 +104,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightGemini ?? availableTerminalHeight
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought_content' && (
|
||||
@@ -104,7 +114,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightGemini ?? availableTerminalHeight
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'info' && (
|
||||
@@ -117,25 +127,32 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
<ErrorMessage text={itemForDisplay.text} />
|
||||
)}
|
||||
{itemForDisplay.type === 'about' && (
|
||||
<AboutBox {...itemForDisplay.systemInfo} />
|
||||
<AboutBox {...itemForDisplay.systemInfo} width={boxWidth} />
|
||||
)}
|
||||
{itemForDisplay.type === 'help' && commands && (
|
||||
<Help commands={commands} />
|
||||
<Help commands={commands} width={boxWidth} />
|
||||
)}
|
||||
{itemForDisplay.type === 'stats' && (
|
||||
<StatsDisplay duration={itemForDisplay.duration} />
|
||||
<StatsDisplay duration={itemForDisplay.duration} width={boxWidth} />
|
||||
)}
|
||||
{itemForDisplay.type === 'model_stats' && (
|
||||
<ModelStatsDisplay width={boxWidth} />
|
||||
)}
|
||||
{itemForDisplay.type === 'tool_stats' && (
|
||||
<ToolStatsDisplay width={boxWidth} />
|
||||
)}
|
||||
{itemForDisplay.type === 'model_stats' && <ModelStatsDisplay />}
|
||||
{itemForDisplay.type === 'tool_stats' && <ToolStatsDisplay />}
|
||||
{itemForDisplay.type === 'quit' && (
|
||||
<SessionSummaryDisplay duration={itemForDisplay.duration} />
|
||||
<SessionSummaryDisplay
|
||||
duration={itemForDisplay.duration}
|
||||
width={boxWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'tool_group' && (
|
||||
<ToolGroupMessage
|
||||
toolCalls={itemForDisplay.tools}
|
||||
groupId={itemForDisplay.id}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
isFocused={isFocused}
|
||||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
@@ -148,11 +165,14 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'extensions_list' && <ExtensionsList />}
|
||||
{itemForDisplay.type === 'tools_list' && (
|
||||
<ToolsList
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
tools={itemForDisplay.tools}
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'skills_list' && (
|
||||
<SkillsList skills={itemForDisplay.skills} />
|
||||
)}
|
||||
{itemForDisplay.type === 'mcp_status' && (
|
||||
<McpStatus {...itemForDisplay} serverStatus={getMCPServerStatus} />
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,9 @@ export interface InputPromptProps {
|
||||
setShellModeActive: (value: boolean) => void;
|
||||
approvalMode: ApprovalMode;
|
||||
onEscapePromptChange?: (showPrompt: boolean) => void;
|
||||
onToggleShortcuts?: () => void;
|
||||
showShortcuts?: boolean;
|
||||
onSuggestionsVisibilityChange?: (visible: boolean) => void;
|
||||
vimHandleInput?: (key: Key) => boolean;
|
||||
isEmbeddedShellFocused?: boolean;
|
||||
}
|
||||
@@ -96,6 +99,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
setShellModeActive,
|
||||
approvalMode,
|
||||
onEscapePromptChange,
|
||||
onToggleShortcuts,
|
||||
showShortcuts,
|
||||
onSuggestionsVisibilityChange,
|
||||
vimHandleInput,
|
||||
isEmbeddedShellFocused,
|
||||
}) => {
|
||||
@@ -338,11 +344,31 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
buffer.text === '' &&
|
||||
!completion.showSuggestions
|
||||
) {
|
||||
// Hide shortcuts when toggling shell mode
|
||||
if (showShortcuts && onToggleShortcuts) {
|
||||
onToggleShortcuts();
|
||||
}
|
||||
setShellModeActive(!shellModeActive);
|
||||
buffer.setText(''); // Clear the '!' from input
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle keyboard shortcuts display with "?" when buffer is empty
|
||||
if (
|
||||
key.sequence === '?' &&
|
||||
buffer.text === '' &&
|
||||
!completion.showSuggestions &&
|
||||
onToggleShortcuts
|
||||
) {
|
||||
onToggleShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide shortcuts on any other key press
|
||||
if (showShortcuts && onToggleShortcuts) {
|
||||
onToggleShortcuts();
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.ESCAPE](key)) {
|
||||
const cancelSearch = (
|
||||
setActive: (active: boolean) => void,
|
||||
@@ -670,6 +696,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
recentPasteTime,
|
||||
commandSearchActive,
|
||||
commandSearchCompletion,
|
||||
onToggleShortcuts,
|
||||
showShortcuts,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -689,6 +717,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
const activeCompletion = getActiveCompletion();
|
||||
const shouldShowSuggestions = activeCompletion.showSuggestions;
|
||||
|
||||
// Notify parent about suggestions visibility changes
|
||||
useEffect(() => {
|
||||
if (onSuggestionsVisibilityChange) {
|
||||
onSuggestionsVisibilityChange(shouldShowSuggestions);
|
||||
}
|
||||
}, [shouldShowSuggestions, onSuggestionsVisibilityChange]);
|
||||
|
||||
const showAutoAcceptStyling =
|
||||
!shellModeActive && approvalMode === ApprovalMode.AUTO_EDIT;
|
||||
const showYoloStyling =
|
||||
@@ -721,7 +756,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={borderColor}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text
|
||||
color={statusColor ?? theme.text.accent}
|
||||
@@ -852,7 +886,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
</Box>
|
||||
</Box>
|
||||
{shouldShowSuggestions && (
|
||||
<Box paddingRight={2}>
|
||||
<Box marginLeft={2} marginRight={2}>
|
||||
<SuggestionsDisplay
|
||||
suggestions={activeCompletion.suggestions}
|
||||
activeIndex={activeCompletion.activeSuggestionIndex}
|
||||
|
||||
118
packages/cli/src/ui/components/KeyboardShortcuts.tsx
Normal file
118
packages/cli/src/ui/components/KeyboardShortcuts.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @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';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface Shortcut {
|
||||
key: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Platform-specific key mappings
|
||||
const getNewlineKey = () =>
|
||||
process.platform === 'win32' ? 'ctrl+enter' : 'ctrl+j';
|
||||
const getPasteKey = () => (process.platform === 'darwin' ? 'cmd+v' : 'ctrl+v');
|
||||
const getExternalEditorKey = () =>
|
||||
process.platform === 'darwin' ? 'ctrl+x' : 'ctrl+x';
|
||||
|
||||
// Generate shortcuts with translations (called at render time)
|
||||
const getShortcuts = (): Shortcut[] => [
|
||||
{ key: '!', description: t('for shell mode') },
|
||||
{ key: '/', description: t('for commands') },
|
||||
{ key: '@', description: t('for file paths') },
|
||||
{ key: 'esc esc', description: t('to clear input') },
|
||||
{ key: 'shift+tab', description: t('to cycle approvals') },
|
||||
{ key: 'ctrl+c', description: t('to quit') },
|
||||
{ key: getNewlineKey(), description: t('for newline') + ' ⏎' },
|
||||
{ key: 'ctrl+l', description: t('to clear screen') },
|
||||
{ key: 'ctrl+r', description: t('to search history') },
|
||||
{ key: getPasteKey(), description: t('to paste images') },
|
||||
{ key: getExternalEditorKey(), description: t('for external editor') },
|
||||
];
|
||||
|
||||
const ShortcutItem: React.FC<{ shortcut: Shortcut }> = ({ shortcut }) => (
|
||||
<Text color={theme.text.secondary}>
|
||||
<Text color={theme.text.primary}>{shortcut.key}</Text>{' '}
|
||||
{shortcut.description}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// Layout constants
|
||||
const COLUMN_GAP = 4;
|
||||
const MARGIN_LEFT = 2;
|
||||
const MARGIN_RIGHT = 2;
|
||||
|
||||
// Column distribution for different layouts (3+4+4 for 3 cols, 6+5 for 2 cols)
|
||||
const COLUMN_SPLITS: Record<number, number[]> = {
|
||||
3: [3, 4, 4],
|
||||
2: [6, 5],
|
||||
1: [11],
|
||||
};
|
||||
|
||||
export const KeyboardShortcuts: React.FC = () => {
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
const shortcuts = getShortcuts();
|
||||
|
||||
// Helper to calculate width needed for a column layout
|
||||
const getShortcutWidth = (shortcut: Shortcut) =>
|
||||
shortcut.key.length + 1 + shortcut.description.length;
|
||||
|
||||
const calculateLayoutWidth = (splits: number[]): number => {
|
||||
let startIndex = 0;
|
||||
let totalWidth = 0;
|
||||
splits.forEach((count, colIndex) => {
|
||||
const columnItems = shortcuts.slice(startIndex, startIndex + count);
|
||||
const columnWidth = Math.max(...columnItems.map(getShortcutWidth));
|
||||
totalWidth += columnWidth;
|
||||
if (colIndex < splits.length - 1) {
|
||||
totalWidth += COLUMN_GAP;
|
||||
}
|
||||
startIndex += count;
|
||||
});
|
||||
return totalWidth;
|
||||
};
|
||||
|
||||
// Calculate number of columns based on terminal width and actual content
|
||||
const availableWidth = terminalWidth - MARGIN_LEFT - MARGIN_RIGHT;
|
||||
const width3Col = calculateLayoutWidth(COLUMN_SPLITS[3]);
|
||||
const width2Col = calculateLayoutWidth(COLUMN_SPLITS[2]);
|
||||
|
||||
const numColumns =
|
||||
availableWidth >= width3Col ? 3 : availableWidth >= width2Col ? 2 : 1;
|
||||
|
||||
// Split shortcuts into columns using predefined distribution
|
||||
const splits = COLUMN_SPLITS[numColumns];
|
||||
const columns: Shortcut[][] = [];
|
||||
let startIndex = 0;
|
||||
for (const count of splits) {
|
||||
columns.push(shortcuts.slice(startIndex, startIndex + count));
|
||||
startIndex += count;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
marginLeft={MARGIN_LEFT}
|
||||
marginRight={MARGIN_RIGHT}
|
||||
>
|
||||
{columns.map((column, colIndex) => (
|
||||
<Box
|
||||
key={colIndex}
|
||||
flexDirection="column"
|
||||
marginRight={colIndex < numColumns - 1 ? COLUMN_GAP : 0}
|
||||
>
|
||||
{column.map((shortcut) => (
|
||||
<ShortcutItem key={shortcut.key} shortcut={shortcut} />
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,7 @@ export const MainContent = () => {
|
||||
const uiState = useUIState();
|
||||
const {
|
||||
pendingHistoryItems,
|
||||
terminalWidth,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
availableTerminalHeight,
|
||||
@@ -36,7 +37,8 @@ export const MainContent = () => {
|
||||
<AppHeader key="app-header" version={version} />,
|
||||
...uiState.history.map((h) => (
|
||||
<HistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
terminalWidth={terminalWidth}
|
||||
mainAreaWidth={mainAreaWidth}
|
||||
availableTerminalHeight={staticAreaMaxItemHeight}
|
||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||
key={h.id}
|
||||
@@ -57,7 +59,8 @@ export const MainContent = () => {
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight ? availableTerminalHeight : undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
terminalWidth={terminalWidth}
|
||||
mainAreaWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
isFocused={!uiState.isEditorDialogOpen}
|
||||
|
||||
@@ -50,7 +50,13 @@ const StatRow: React.FC<StatRowProps> = ({
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const ModelStatsDisplay: React.FC = () => {
|
||||
interface ModelStatsDisplayProps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
|
||||
width,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { models } = stats.metrics;
|
||||
const activeModels = Object.entries(models).filter(
|
||||
@@ -64,6 +70,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
borderColor={theme.border.default}
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
width={width}
|
||||
>
|
||||
<Text color={theme.text.primary}>
|
||||
{t('No API calls have been made in this session.')}
|
||||
@@ -93,6 +100,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
||||
flexDirection="column"
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
width={width}
|
||||
>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{t('Model Stats For Nerds')}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const PlanSummaryDisplay: React.FC<PlanSummaryDisplayProps> = ({
|
||||
text={plan}
|
||||
isPending={false}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={childWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ export const QuittingDisplay = () => {
|
||||
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
|
||||
|
||||
const availableTerminalHeight = terminalHeight;
|
||||
const { mainAreaWidth } = uiState;
|
||||
|
||||
if (!uiState.quittingMessages) {
|
||||
return null;
|
||||
@@ -28,6 +29,7 @@ export const QuittingDisplay = () => {
|
||||
uiState.constrainHeight ? availableTerminalHeight : undefined
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
mainAreaWidth={mainAreaWidth}
|
||||
item={item}
|
||||
isPending={false}
|
||||
/>
|
||||
|
||||
@@ -127,8 +127,8 @@ export function SessionPicker(props: SessionPickerProps) {
|
||||
|
||||
const { columns: width, rows: height } = useTerminalSize();
|
||||
|
||||
// Calculate box width (width + 6 for border padding)
|
||||
const boxWidth = width + 6;
|
||||
// Calculate box width (marginX={2})
|
||||
const boxWidth = width - 4;
|
||||
// Calculate visible items (same heuristic as before)
|
||||
// Reserved space: header (1), footer (1), separators (2), borders (2)
|
||||
const reservedLines = 6;
|
||||
@@ -179,7 +179,7 @@ export function SessionPicker(props: SessionPickerProps) {
|
||||
|
||||
{/* Separator */}
|
||||
<Box>
|
||||
<Text color={theme.border.default}>{'─'.repeat(width - 2)}</Text>
|
||||
<Text color={theme.border.default}>{'─'.repeat(boxWidth - 2)}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Session list */}
|
||||
@@ -212,7 +212,7 @@ export function SessionPicker(props: SessionPickerProps) {
|
||||
isLast={visibleIndex === picker.visibleSessions.length - 1}
|
||||
showScrollUp={picker.showScrollUp}
|
||||
showScrollDown={picker.showScrollDown}
|
||||
maxPromptWidth={width}
|
||||
maxPromptWidth={boxWidth - 6}
|
||||
prefixChars={PREFIX_CHARS}
|
||||
boldSelectedPrefix={false}
|
||||
/>
|
||||
@@ -223,7 +223,7 @@ export function SessionPicker(props: SessionPickerProps) {
|
||||
|
||||
{/* Separator */}
|
||||
<Box>
|
||||
<Text color={theme.border.default}>{'─'.repeat(width - 2)}</Text>
|
||||
<Text color={theme.border.default}>{'─'.repeat(boxWidth - 2)}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
@@ -14,10 +14,12 @@ import { t } from '../../i18n/index.js';
|
||||
|
||||
interface SessionSummaryDisplayProps {
|
||||
duration: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
|
||||
duration,
|
||||
width,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const { stats } = useSessionStats();
|
||||
@@ -32,6 +34,7 @@ export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
|
||||
<StatsDisplay
|
||||
title={t('Agent powering down. Goodbye!')}
|
||||
duration={duration}
|
||||
width={width}
|
||||
/>
|
||||
{hasMessages && canResume && (
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -28,12 +28,12 @@ import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
import { saveModifiedSettings, TEST_ONLY } from '../../utils/settingsUtils.js';
|
||||
import {
|
||||
getSettingsSchema,
|
||||
type SettingDefinition,
|
||||
type SettingsSchemaType,
|
||||
} from '../../config/settingsSchema.js';
|
||||
getDialogSettingKeys,
|
||||
getSettingDefinition,
|
||||
saveModifiedSettings,
|
||||
TEST_ONLY,
|
||||
} from '../../utils/settingsUtils.js';
|
||||
|
||||
// Mock the VimModeContext
|
||||
const mockToggleVimEnabled = vi.fn();
|
||||
@@ -210,8 +210,9 @@ describe('SettingsDialog', () => {
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Settings');
|
||||
expect(output).toContain('Apply To');
|
||||
expect(output).toContain('Use Enter to select, Tab to change focus');
|
||||
// Scope selector is now in a separate view (Tab to switch)
|
||||
expect(output).not.toContain('Apply To');
|
||||
expect(output).toContain('(Use Enter to select, Tab to configure scope)');
|
||||
});
|
||||
|
||||
it('should accept availableTerminalHeight prop without errors', () => {
|
||||
@@ -231,7 +232,7 @@ describe('SettingsDialog', () => {
|
||||
const output = lastFrame();
|
||||
// Should still render properly with the height prop
|
||||
expect(output).toContain('Settings');
|
||||
expect(output).toContain('Use Enter to select');
|
||||
expect(output).toContain('Enter to select');
|
||||
});
|
||||
|
||||
it('should show settings list with default values', () => {
|
||||
@@ -281,7 +282,7 @@ describe('SettingsDialog', () => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('● Disable Auto Update');
|
||||
expect(lastFrame()).toContain('● Language');
|
||||
|
||||
// The active index should have changed (tested indirectly through behavior)
|
||||
unmount();
|
||||
@@ -342,7 +343,14 @@ describe('SettingsDialog', () => {
|
||||
|
||||
await wait();
|
||||
|
||||
expect(lastFrame()).toContain('● Vision Model Preview');
|
||||
const lastKey = getDialogSettingKeys().at(-1);
|
||||
expect(lastKey).toBeDefined();
|
||||
|
||||
const lastLabel = lastKey
|
||||
? (getSettingDefinition(lastKey)?.label ?? lastKey)
|
||||
: '';
|
||||
|
||||
expect(lastFrame()).toContain(`● ${lastLabel}`);
|
||||
|
||||
unmount();
|
||||
});
|
||||
@@ -362,17 +370,21 @@ describe('SettingsDialog', () => {
|
||||
|
||||
const { stdin, unmount, lastFrame } = render(component);
|
||||
|
||||
// Wait for initial render and verify we're on Vim Mode (first setting)
|
||||
// Wait for initial render and verify we're on Tool Approval Mode (first setting)
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('● Vim Mode');
|
||||
expect(lastFrame()).toContain('● Tool Approval Mode');
|
||||
});
|
||||
|
||||
// Navigate to Disable Auto Update setting and verify we're there
|
||||
// Navigate to Vim Mode setting (third setting - a boolean) and verify we're there
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // -> Language
|
||||
});
|
||||
await wait();
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // -> Vim Mode
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('● Disable Auto Update');
|
||||
expect(lastFrame()).toContain('● Vim Mode');
|
||||
});
|
||||
|
||||
// Toggle the setting
|
||||
@@ -392,10 +404,10 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['general.disableAutoUpdate']),
|
||||
new Set<string>(['general.vimMode']),
|
||||
{
|
||||
general: {
|
||||
disableAutoUpdate: true,
|
||||
vimMode: true,
|
||||
},
|
||||
},
|
||||
expect.any(LoadedSettings),
|
||||
@@ -406,51 +418,10 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
describe('enum values', () => {
|
||||
enum StringEnum {
|
||||
FOO = 'foo',
|
||||
BAR = 'bar',
|
||||
BAZ = 'baz',
|
||||
}
|
||||
|
||||
const SETTING: SettingDefinition = {
|
||||
type: 'enum',
|
||||
label: 'Theme',
|
||||
options: [
|
||||
{
|
||||
label: 'Foo',
|
||||
value: StringEnum.FOO,
|
||||
},
|
||||
{
|
||||
label: 'Bar',
|
||||
value: StringEnum.BAR,
|
||||
},
|
||||
{
|
||||
label: 'Baz',
|
||||
value: StringEnum.BAZ,
|
||||
},
|
||||
],
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: StringEnum.BAR,
|
||||
description: 'The color theme for the UI.',
|
||||
showInDialog: true,
|
||||
};
|
||||
|
||||
const FAKE_SCHEMA: SettingsSchemaType = {
|
||||
ui: {
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
theme: {
|
||||
...SETTING,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SettingsSchemaType;
|
||||
|
||||
it('toggles enum values with the enter key', async () => {
|
||||
vi.mocked(saveModifiedSettings).mockClear();
|
||||
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(FAKE_SCHEMA);
|
||||
// Use real schema - first setting "Tool Approval Mode" is an enum
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
const component = (
|
||||
@@ -459,24 +430,30 @@ describe('SettingsDialog', () => {
|
||||
</KeypressProvider>
|
||||
);
|
||||
|
||||
const { stdin, unmount } = render(component);
|
||||
const { stdin, unmount, lastFrame } = render(component);
|
||||
|
||||
// Press Enter to toggle current setting
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
await wait();
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
// Verify we're on Tool Approval Mode (first setting, an enum)
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('● Tool Approval Mode');
|
||||
});
|
||||
|
||||
// Press Enter to cycle the enum value
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
});
|
||||
await wait();
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Tool Approval Mode cycles through enum values
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['ui.theme']),
|
||||
{
|
||||
ui: {
|
||||
theme: StringEnum.BAZ,
|
||||
},
|
||||
},
|
||||
new Set<string>(['tools.approvalMode']),
|
||||
expect.objectContaining({
|
||||
tools: expect.objectContaining({
|
||||
approvalMode: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
expect.any(LoadedSettings),
|
||||
SettingScope.User,
|
||||
);
|
||||
@@ -486,10 +463,10 @@ describe('SettingsDialog', () => {
|
||||
|
||||
it('loops back when reaching the end of an enum', async () => {
|
||||
vi.mocked(saveModifiedSettings).mockClear();
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(FAKE_SCHEMA);
|
||||
// Use Tool Approval Mode set to YOLO (last value) to test looping back to first
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
theme: StringEnum.BAZ,
|
||||
tools: {
|
||||
approvalMode: 'yolo', // Last enum value
|
||||
},
|
||||
});
|
||||
const onSelect = vi.fn();
|
||||
@@ -499,24 +476,30 @@ describe('SettingsDialog', () => {
|
||||
</KeypressProvider>
|
||||
);
|
||||
|
||||
const { stdin, unmount } = render(component);
|
||||
const { stdin, unmount, lastFrame } = render(component);
|
||||
|
||||
// Press Enter to toggle current setting
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
await wait();
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
// Verify we're on Tool Approval Mode (first setting)
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('● Tool Approval Mode');
|
||||
});
|
||||
|
||||
// Press Enter to cycle - should loop back to first value (Plan)
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
});
|
||||
await wait();
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Should loop back to first enum value (Plan)
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['ui.theme']),
|
||||
{
|
||||
ui: {
|
||||
theme: StringEnum.FOO,
|
||||
},
|
||||
},
|
||||
new Set<string>(['tools.approvalMode']),
|
||||
expect.objectContaining({
|
||||
tools: expect.objectContaining({
|
||||
approvalMode: 'plan', // First enum value after YOLO
|
||||
}),
|
||||
}),
|
||||
expect.any(LoadedSettings),
|
||||
SettingScope.User,
|
||||
);
|
||||
@@ -599,12 +582,12 @@ describe('SettingsDialog', () => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// The UI should show the settings section is active and scope section is inactive
|
||||
expect(lastFrame()).toContain('● Vim Mode'); // Settings section active
|
||||
expect(lastFrame()).toContain(' Apply To'); // Scope section inactive
|
||||
// The UI should show settings mode is active (scope is in separate view)
|
||||
expect(lastFrame()).toContain('● Tool Approval Mode'); // Settings section active
|
||||
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view
|
||||
|
||||
// This test validates the initial state - scope selection behavior
|
||||
// is complex due to keypress handling, so we focus on state validation
|
||||
// This test validates the initial state - scope selection is now
|
||||
// accessed via Tab key, not shown alongside settings
|
||||
|
||||
unmount();
|
||||
});
|
||||
@@ -668,12 +651,12 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Wait for initial render
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Hide Window Title');
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Verify the dialog is rendered properly
|
||||
// Verify the dialog is rendered properly (scope is in separate view)
|
||||
expect(lastFrame()).toContain('Settings');
|
||||
expect(lastFrame()).toContain('Apply To');
|
||||
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view
|
||||
|
||||
// This test validates rendering - escape key behavior depends on complex
|
||||
// keypress handling that's difficult to test reliably in this environment
|
||||
@@ -1021,12 +1004,12 @@ describe('SettingsDialog', () => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Verify initial state: settings section active, scope section inactive
|
||||
expect(lastFrame()).toContain('● Vim Mode'); // Settings section active
|
||||
expect(lastFrame()).toContain(' Apply To'); // Scope section inactive
|
||||
// Verify initial state: settings mode active (scope is in separate view)
|
||||
expect(lastFrame()).toContain('● Tool Approval Mode'); // Settings mode active
|
||||
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view
|
||||
|
||||
// This test validates the rendered UI structure for tab navigation
|
||||
// Actual tab behavior testing is complex due to keypress handling
|
||||
// Tab now switches between settings view and scope view
|
||||
|
||||
unmount();
|
||||
});
|
||||
@@ -1083,17 +1066,16 @@ describe('SettingsDialog', () => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Verify the complete UI is rendered with all necessary sections
|
||||
// Verify the complete UI is rendered (scope is in separate view)
|
||||
expect(lastFrame()).toContain('Settings'); // Title
|
||||
expect(lastFrame()).toContain('● Vim Mode'); // Active setting
|
||||
expect(lastFrame()).toContain('Apply To'); // Scope section
|
||||
expect(lastFrame()).toContain('User Settings'); // Scope options (no numbers when settings focused)
|
||||
expect(lastFrame()).toContain('● Tool Approval Mode'); // Active setting
|
||||
expect(lastFrame()).not.toContain('Apply To'); // Scope is in a separate view (Tab to access)
|
||||
expect(lastFrame()).toContain(
|
||||
'(Use Enter to select, Tab to change focus)',
|
||||
'(Use Enter to select, Tab to configure scope)',
|
||||
); // Help text
|
||||
|
||||
// This test validates the complete UI structure is available for user workflow
|
||||
// Individual interactions are tested in focused unit tests
|
||||
// Scope selection is now accessed via Tab key (view switching like ThemeDialog)
|
||||
|
||||
unmount();
|
||||
});
|
||||
@@ -1275,7 +1257,6 @@ describe('SettingsDialog', () => {
|
||||
ui: {
|
||||
hideWindowTitle: true,
|
||||
hideTips: true,
|
||||
showMemoryUsage: true,
|
||||
showLineNumbers: true,
|
||||
showCitations: true,
|
||||
accessibility: {
|
||||
@@ -1324,7 +1305,6 @@ describe('SettingsDialog', () => {
|
||||
disableAutoUpdate: true,
|
||||
},
|
||||
ui: {
|
||||
showMemoryUsage: true,
|
||||
hideWindowTitle: false,
|
||||
},
|
||||
tools: {
|
||||
@@ -1375,9 +1355,7 @@ describe('SettingsDialog', () => {
|
||||
vimMode: true,
|
||||
disableAutoUpdate: false,
|
||||
},
|
||||
ui: {
|
||||
showMemoryUsage: true,
|
||||
},
|
||||
ui: {},
|
||||
},
|
||||
);
|
||||
const onSelect = vi.fn();
|
||||
@@ -1438,7 +1416,6 @@ describe('SettingsDialog', () => {
|
||||
disableLoadingPhrases: true,
|
||||
screenReader: true,
|
||||
},
|
||||
showMemoryUsage: true,
|
||||
showLineNumbers: true,
|
||||
},
|
||||
general: {
|
||||
@@ -1520,7 +1497,6 @@ describe('SettingsDialog', () => {
|
||||
ui: {
|
||||
hideWindowTitle: false,
|
||||
hideTips: false,
|
||||
showMemoryUsage: false,
|
||||
showLineNumbers: false,
|
||||
showCitations: false,
|
||||
accessibility: {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import type { LoadedSettings, Settings } from '../../config/settings.js';
|
||||
@@ -57,10 +58,8 @@ export function SettingsDialog({
|
||||
// Get vim mode context to sync vim mode changes
|
||||
const { vimEnabled, toggleVimEnabled } = useVimMode();
|
||||
|
||||
// Focus state: 'settings' or 'scope'
|
||||
const [focusSection, setFocusSection] = useState<'settings' | 'scope'>(
|
||||
'settings',
|
||||
);
|
||||
// Mode state: 'settings' or 'scope' (view switching like ThemeDialog)
|
||||
const [mode, setMode] = useState<'settings' | 'scope'>('settings');
|
||||
// Scope selector state (User by default)
|
||||
const [selectedScope, setSelectedScope] = useState<SettingScope>(
|
||||
SettingScope.User,
|
||||
@@ -105,7 +104,9 @@ export function SettingsDialog({
|
||||
updated = setPendingSettingValue(key, value, updated);
|
||||
} else if (
|
||||
(def?.type === 'number' && typeof value === 'number') ||
|
||||
(def?.type === 'string' && typeof value === 'string')
|
||||
(def?.type === 'string' && typeof value === 'string') ||
|
||||
(def?.type === 'enum' &&
|
||||
(typeof value === 'string' || typeof value === 'number'))
|
||||
) {
|
||||
updated = setPendingSettingValueAny(key, value, updated);
|
||||
}
|
||||
@@ -156,10 +157,6 @@ export function SettingsDialog({
|
||||
);
|
||||
}
|
||||
|
||||
setPendingSettings((prev) =>
|
||||
setPendingSettingValue(key, newValue as boolean, prev),
|
||||
);
|
||||
|
||||
if (!requiresRestart(key)) {
|
||||
const immediateSettings = new Set([key]);
|
||||
const immediateSettingsObject = setPendingSettingValueAny(
|
||||
@@ -381,15 +378,13 @@ export function SettingsDialog({
|
||||
|
||||
const handleScopeSelect = (scope: SettingScope) => {
|
||||
handleScopeHighlight(scope);
|
||||
setFocusSection('settings');
|
||||
setMode('settings');
|
||||
};
|
||||
|
||||
// Height constraint calculations similar to ThemeDialog
|
||||
const DIALOG_PADDING = 2;
|
||||
const SETTINGS_TITLE_HEIGHT = 2; // "Settings" title + spacing
|
||||
const SCROLL_ARROWS_HEIGHT = 2; // Up and down arrows
|
||||
const SPACING_HEIGHT = 1; // Space between settings list and scope
|
||||
const SCOPE_SELECTION_HEIGHT = 4; // Apply To section height
|
||||
const BOTTOM_HELP_TEXT_HEIGHT = 1; // Help text
|
||||
const RESTART_PROMPT_HEIGHT = showRestartPrompt ? 1 : 0;
|
||||
|
||||
@@ -397,71 +392,28 @@ export function SettingsDialog({
|
||||
availableTerminalHeight ?? Number.MAX_SAFE_INTEGER;
|
||||
currentAvailableTerminalHeight -= 2; // Top and bottom borders
|
||||
|
||||
// Start with basic fixed height (without scope selection)
|
||||
let totalFixedHeight =
|
||||
// Calculate fixed height (scope selection is now in a separate view, not included here)
|
||||
const totalFixedHeight =
|
||||
DIALOG_PADDING +
|
||||
SETTINGS_TITLE_HEIGHT +
|
||||
SCROLL_ARROWS_HEIGHT +
|
||||
SPACING_HEIGHT +
|
||||
BOTTOM_HELP_TEXT_HEIGHT +
|
||||
RESTART_PROMPT_HEIGHT;
|
||||
|
||||
// Calculate how much space we have for settings
|
||||
let availableHeightForSettings = Math.max(
|
||||
const availableHeightForSettings = Math.max(
|
||||
1,
|
||||
currentAvailableTerminalHeight - totalFixedHeight,
|
||||
);
|
||||
|
||||
// Each setting item takes 2 lines (the setting row + spacing)
|
||||
let maxVisibleItems = Math.max(1, Math.floor(availableHeightForSettings / 2));
|
||||
|
||||
// Decide whether to show scope selection based on remaining space
|
||||
let showScopeSelection = true;
|
||||
|
||||
// If we have limited height, prioritize showing more settings over scope selection
|
||||
if (availableTerminalHeight && availableTerminalHeight < 25) {
|
||||
// For very limited height, hide scope selection to show more settings
|
||||
const totalWithScope = totalFixedHeight + SCOPE_SELECTION_HEIGHT;
|
||||
const availableWithScope = Math.max(
|
||||
1,
|
||||
currentAvailableTerminalHeight - totalWithScope,
|
||||
);
|
||||
const maxItemsWithScope = Math.max(1, Math.floor(availableWithScope / 2));
|
||||
|
||||
// If hiding scope selection allows us to show significantly more settings, do it
|
||||
if (maxVisibleItems > maxItemsWithScope + 1) {
|
||||
showScopeSelection = false;
|
||||
} else {
|
||||
// Otherwise include scope selection and recalculate
|
||||
totalFixedHeight += SCOPE_SELECTION_HEIGHT;
|
||||
availableHeightForSettings = Math.max(
|
||||
1,
|
||||
currentAvailableTerminalHeight - totalFixedHeight,
|
||||
);
|
||||
maxVisibleItems = Math.max(1, Math.floor(availableHeightForSettings / 2));
|
||||
}
|
||||
} else {
|
||||
// For normal height, include scope selection
|
||||
totalFixedHeight += SCOPE_SELECTION_HEIGHT;
|
||||
availableHeightForSettings = Math.max(
|
||||
1,
|
||||
currentAvailableTerminalHeight - totalFixedHeight,
|
||||
);
|
||||
maxVisibleItems = Math.max(1, Math.floor(availableHeightForSettings / 2));
|
||||
}
|
||||
// Each setting item takes 1 line
|
||||
const maxVisibleItems = Math.max(1, availableHeightForSettings);
|
||||
|
||||
// Use the calculated maxVisibleItems or fall back to the original maxItemsToShow
|
||||
const effectiveMaxItemsToShow = availableTerminalHeight
|
||||
? Math.min(maxVisibleItems, items.length)
|
||||
: maxItemsToShow;
|
||||
|
||||
// Ensure focus stays on settings when scope selection is hidden
|
||||
React.useEffect(() => {
|
||||
if (!showScopeSelection && focusSection === 'scope') {
|
||||
setFocusSection('settings');
|
||||
}
|
||||
}, [showScopeSelection, focusSection]);
|
||||
|
||||
// Scroll logic for settings
|
||||
const visibleItems = items.slice(
|
||||
scrollOffset,
|
||||
@@ -474,10 +426,10 @@ export function SettingsDialog({
|
||||
useKeypress(
|
||||
(key) => {
|
||||
const { name, ctrl } = key;
|
||||
if (name === 'tab' && showScopeSelection) {
|
||||
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
|
||||
if (name === 'tab') {
|
||||
setMode((prev) => (prev === 'settings' ? 'scope' : 'settings'));
|
||||
}
|
||||
if (focusSection === 'settings') {
|
||||
if (mode === 'settings') {
|
||||
// If editing, capture input and control keys
|
||||
if (editingKey) {
|
||||
const definition = getSettingDefinition(editingKey);
|
||||
@@ -599,6 +551,18 @@ export function SettingsDialog({
|
||||
}
|
||||
} else if (name === 'return' || name === 'space') {
|
||||
const currentItem = items[activeSettingIndex];
|
||||
if (currentItem?.value === 'ui.theme') {
|
||||
if (name === 'return') {
|
||||
onSelect('ui.theme', selectedScope);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentItem?.value === 'general.preferredEditor') {
|
||||
if (name === 'return') {
|
||||
onSelect('general.preferredEditor', selectedScope);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
currentItem?.type === 'number' ||
|
||||
currentItem?.type === 'string'
|
||||
@@ -775,97 +739,95 @@ export function SettingsDialog({
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="row"
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Text bold={focusSection === 'settings'} wrap="truncate">
|
||||
{focusSection === 'settings' ? '> ' : ' '}
|
||||
{t('Settings')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{showScrollUp && <Text color={theme.text.secondary}>▲</Text>}
|
||||
{visibleItems.map((item, idx) => {
|
||||
const isActive =
|
||||
focusSection === 'settings' &&
|
||||
activeSettingIndex === idx + scrollOffset;
|
||||
{mode === 'settings' ? (
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<Text bold={mode === 'settings'} wrap="truncate">
|
||||
{mode === 'settings' ? '> ' : ' '}
|
||||
{t('Settings')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{showScrollUp && <Text color={theme.text.secondary}>▲</Text>}
|
||||
{visibleItems.map((item, idx) => {
|
||||
const isActive =
|
||||
mode === 'settings' && activeSettingIndex === idx + scrollOffset;
|
||||
|
||||
const scopeSettings = settings.forScope(selectedScope).settings;
|
||||
const mergedSettings = settings.merged;
|
||||
const scopeSettings = settings.forScope(selectedScope).settings;
|
||||
const mergedSettings = settings.merged;
|
||||
|
||||
let displayValue: string;
|
||||
if (editingKey === item.value) {
|
||||
// Show edit buffer with advanced cursor highlighting
|
||||
if (cursorVisible && editCursorPos < cpLen(editBuffer)) {
|
||||
// Cursor is in the middle or at start of text
|
||||
const beforeCursor = cpSlice(editBuffer, 0, editCursorPos);
|
||||
const atCursor = cpSlice(
|
||||
editBuffer,
|
||||
editCursorPos,
|
||||
editCursorPos + 1,
|
||||
let displayValue: string;
|
||||
if (editingKey === item.value) {
|
||||
// Show edit buffer with advanced cursor highlighting
|
||||
if (cursorVisible && editCursorPos < cpLen(editBuffer)) {
|
||||
// Cursor is in the middle or at start of text
|
||||
const beforeCursor = cpSlice(editBuffer, 0, editCursorPos);
|
||||
const atCursor = cpSlice(
|
||||
editBuffer,
|
||||
editCursorPos,
|
||||
editCursorPos + 1,
|
||||
);
|
||||
const afterCursor = cpSlice(editBuffer, editCursorPos + 1);
|
||||
displayValue =
|
||||
beforeCursor + chalk.inverse(atCursor) + afterCursor;
|
||||
} else if (cursorVisible && editCursorPos >= cpLen(editBuffer)) {
|
||||
// Cursor is at the end - show inverted space
|
||||
displayValue = editBuffer + chalk.inverse(' ');
|
||||
} else {
|
||||
// Cursor not visible
|
||||
displayValue = editBuffer;
|
||||
}
|
||||
} else if (item.type === 'number' || item.type === 'string') {
|
||||
// For numbers/strings, get the actual current value from pending settings
|
||||
const path = item.value.split('.');
|
||||
const currentValue = getNestedValue(pendingSettings, path);
|
||||
|
||||
const defaultValue = getDefaultValue(item.value);
|
||||
|
||||
if (currentValue !== undefined && currentValue !== null) {
|
||||
displayValue = String(currentValue);
|
||||
} else {
|
||||
displayValue =
|
||||
defaultValue !== undefined && defaultValue !== null
|
||||
? String(defaultValue)
|
||||
: '';
|
||||
}
|
||||
|
||||
// Add * if value differs from default OR if currently being modified
|
||||
const isModified = modifiedSettings.has(item.value);
|
||||
const effectiveCurrentValue =
|
||||
currentValue !== undefined && currentValue !== null
|
||||
? currentValue
|
||||
: defaultValue;
|
||||
const isDifferentFromDefault =
|
||||
effectiveCurrentValue !== defaultValue;
|
||||
|
||||
if (isDifferentFromDefault || isModified) {
|
||||
displayValue += '*';
|
||||
}
|
||||
} else {
|
||||
// For booleans and other types, use existing logic
|
||||
displayValue = getDisplayValue(
|
||||
item.value,
|
||||
scopeSettings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
pendingSettings,
|
||||
);
|
||||
const afterCursor = cpSlice(editBuffer, editCursorPos + 1);
|
||||
displayValue =
|
||||
beforeCursor + chalk.inverse(atCursor) + afterCursor;
|
||||
} else if (cursorVisible && editCursorPos >= cpLen(editBuffer)) {
|
||||
// Cursor is at the end - show inverted space
|
||||
displayValue = editBuffer + chalk.inverse(' ');
|
||||
} else {
|
||||
// Cursor not visible
|
||||
displayValue = editBuffer;
|
||||
}
|
||||
} else if (item.type === 'number' || item.type === 'string') {
|
||||
// For numbers/strings, get the actual current value from pending settings
|
||||
const path = item.value.split('.');
|
||||
const currentValue = getNestedValue(pendingSettings, path);
|
||||
const shouldBeGreyedOut = isDefaultValue(item.value, scopeSettings);
|
||||
|
||||
const defaultValue = getDefaultValue(item.value);
|
||||
|
||||
if (currentValue !== undefined && currentValue !== null) {
|
||||
displayValue = String(currentValue);
|
||||
} else {
|
||||
displayValue =
|
||||
defaultValue !== undefined && defaultValue !== null
|
||||
? String(defaultValue)
|
||||
: '';
|
||||
}
|
||||
|
||||
// Add * if value differs from default OR if currently being modified
|
||||
const isModified = modifiedSettings.has(item.value);
|
||||
const effectiveCurrentValue =
|
||||
currentValue !== undefined && currentValue !== null
|
||||
? currentValue
|
||||
: defaultValue;
|
||||
const isDifferentFromDefault =
|
||||
effectiveCurrentValue !== defaultValue;
|
||||
|
||||
if (isDifferentFromDefault || isModified) {
|
||||
displayValue += '*';
|
||||
}
|
||||
} else {
|
||||
// For booleans and other types, use existing logic
|
||||
displayValue = getDisplayValue(
|
||||
// Generate scope message for this setting
|
||||
const scopeMessage = getScopeMessageForSetting(
|
||||
item.value,
|
||||
scopeSettings,
|
||||
mergedSettings,
|
||||
modifiedSettings,
|
||||
pendingSettings,
|
||||
selectedScope,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
const shouldBeGreyedOut = isDefaultValue(item.value, scopeSettings);
|
||||
|
||||
// Generate scope message for this setting
|
||||
const scopeMessage = getScopeMessageForSetting(
|
||||
item.value,
|
||||
selectedScope,
|
||||
settings,
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={item.value}>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
return (
|
||||
<Box key={item.value} flexDirection="row" alignItems="center">
|
||||
<Box minWidth={2} flexShrink={0}>
|
||||
<Text
|
||||
color={
|
||||
@@ -898,40 +860,32 @@ export function SettingsDialog({
|
||||
{displayValue}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box height={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{showScrollDown && <Text color={theme.text.secondary}>▼</Text>}
|
||||
|
||||
<Box height={1} />
|
||||
|
||||
{/* Scope Selection - conditionally visible based on height constraints */}
|
||||
{showScopeSelection && (
|
||||
<Box marginTop={1}>
|
||||
<ScopeSelector
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={focusSection === 'scope'}
|
||||
initialScope={selectedScope}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box height={1} />
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('(Use Enter to select{{tabText}})', {
|
||||
tabText: showScopeSelection ? t(', Tab to change focus') : '',
|
||||
);
|
||||
})}
|
||||
{showScrollDown && <Text color={theme.text.secondary}>▼</Text>}
|
||||
</Box>
|
||||
) : (
|
||||
<ScopeSelector
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={mode === 'scope'}
|
||||
initialScope={selectedScope}
|
||||
/>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{mode === 'settings'
|
||||
? t('(Use Enter to select, Tab to configure scope)')
|
||||
: t('(Use Enter to apply scope, Tab to go back)')}
|
||||
</Text>
|
||||
{showRestartPrompt && (
|
||||
<Text color={theme.status.warning}>
|
||||
{t(
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{showRestartPrompt && (
|
||||
<Text color={theme.status.warning}>
|
||||
{t(
|
||||
'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,11 +160,13 @@ const ModelUsageTable: React.FC<{
|
||||
interface StatsDisplayProps {
|
||||
duration: string;
|
||||
title?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
duration,
|
||||
title,
|
||||
width,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { metrics } = stats;
|
||||
@@ -213,6 +215,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
flexDirection="column"
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
width={width}
|
||||
>
|
||||
{renderTitle()}
|
||||
<Box height={1} />
|
||||
|
||||
@@ -42,7 +42,7 @@ export function SuggestionsDisplay({
|
||||
}: SuggestionsDisplayProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box paddingX={1} width={width}>
|
||||
<Box width={width}>
|
||||
<Text color="gray">Loading suggestions...</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -70,7 +70,7 @@ export function SuggestionsDisplay({
|
||||
mode === 'slash' ? Math.min(maxLabelLength, Math.floor(width * 0.5)) : 0;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingX={1} width={width}>
|
||||
<Box flexDirection="column" width={width}>
|
||||
{scrollOffset > 0 && <Text color={theme.text.primary}>▲</Text>}
|
||||
|
||||
{visibleSuggestions.map((suggestion, index) => {
|
||||
@@ -106,7 +106,7 @@ export function SuggestionsDisplay({
|
||||
</Box>
|
||||
|
||||
{suggestion.description && (
|
||||
<Box flexGrow={1} paddingLeft={3}>
|
||||
<Box flexGrow={1} paddingLeft={2}>
|
||||
<Text color={textColor} wrap="truncate">
|
||||
{suggestion.description}
|
||||
</Text>
|
||||
|
||||
@@ -258,7 +258,7 @@ def fibonacci(n):
|
||||
+ print(f"Hello, {name}!")
|
||||
`}
|
||||
availableTerminalHeight={diffHeight}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
contentWidth={colorizeCodeWidth}
|
||||
theme={previewTheme}
|
||||
/>
|
||||
</Box>
|
||||
@@ -278,7 +278,7 @@ def fibonacci(n):
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
{mode === 'theme'
|
||||
? t('(Use Enter to select, Tab to configure scope)')
|
||||
: t('(Use Enter to apply scope, Tab to select theme)')}
|
||||
: t('(Use Enter to apply scope, Tab to go back)')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -4,42 +4,33 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { type Config } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
interface TipsProps {
|
||||
config: Config;
|
||||
}
|
||||
const startupTips = [
|
||||
'Use /compress when the conversation gets long to summarize history and free up context.',
|
||||
'Start a fresh idea with /clear or /new; the previous session stays available in history.',
|
||||
'Use /bug to submit issues to the maintainers when something goes off.',
|
||||
'Switch auth type quickly with /auth.',
|
||||
'You can run any shell commands from Qwen Code using ! (e.g. !ls).',
|
||||
'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.',
|
||||
'You can resume a previous conversation by running qwen --continue or qwen --resume.',
|
||||
'You can switch permission mode quickly with Shift+Tab or /approval-mode.',
|
||||
] as const;
|
||||
|
||||
export const Tips: React.FC = () => {
|
||||
const selectedTip = useMemo(() => {
|
||||
const randomIndex = Math.floor(Math.random() * startupTips.length);
|
||||
return startupTips[randomIndex];
|
||||
}, []);
|
||||
|
||||
export const Tips: React.FC<TipsProps> = ({ config }) => {
|
||||
const geminiMdFileCount = config.getGeminiMdFileCount();
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.primary}>{t('Tips for getting started:')}</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{t('1. Ask questions, edit files, or run commands.')}
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
{t('2. Be specific for the best results.')}
|
||||
</Text>
|
||||
{geminiMdFileCount === 0 && (
|
||||
<Text color={theme.text.primary}>
|
||||
3. Create{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
QWEN.md
|
||||
</Text>{' '}
|
||||
{t('files to customize your interactions with Qwen Code.')}
|
||||
</Text>
|
||||
)}
|
||||
<Text color={theme.text.primary}>
|
||||
{geminiMdFileCount === 0 ? '4.' : '3.'}{' '}
|
||||
<Text bold color={theme.text.accent}>
|
||||
/help
|
||||
</Text>{' '}
|
||||
{t('for more information.')}
|
||||
<Box marginLeft={2} marginRight={2}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Tips: ')}
|
||||
{t(selectedTip)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,13 @@ const StatRow: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const ToolStatsDisplay: React.FC = () => {
|
||||
interface ToolStatsDisplayProps {
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const ToolStatsDisplay: React.FC<ToolStatsDisplayProps> = ({
|
||||
width,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { tools } = stats.metrics;
|
||||
const activeTools = Object.entries(tools.byName).filter(
|
||||
@@ -67,6 +73,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
borderColor={theme.border.default}
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
width={width}
|
||||
>
|
||||
<Text color={theme.text.primary}>
|
||||
{t('No tool calls have been made in this session.')}
|
||||
@@ -101,7 +108,7 @@ export const ToolStatsDisplay: React.FC = () => {
|
||||
flexDirection="column"
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
width={70}
|
||||
width={width}
|
||||
>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{t('Tool Stats For Nerds')}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `"...s/to/make/it/long (main*) no sandbox gemini-pro (100%)"`;
|
||||
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on narrow terminal > complete-footer-narrow 1`] = `" ? for shortcuts 0.1% used"`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `"...directories/to/make/it/long (main*) no sandbox (see /docs) gemini-pro (100% context left)"`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `" no sandbox (see /docs)"`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `"...directories/to/make/it/long (main*) no sandbox (see /docs)"`;
|
||||
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on wide terminal > complete-footer-wide 1`] = `" ? for shortcuts 0.1% context used"`;
|
||||
|
||||
@@ -1,137 +1,137 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<HistoryItemDisplay /> > should render a full gemini item when using availableTerminalHeightGemini 1`] = `
|
||||
"✦ Example code block:
|
||||
1 Line 1
|
||||
2 Line 2
|
||||
3 Line 3
|
||||
4 Line 4
|
||||
5 Line 5
|
||||
6 Line 6
|
||||
7 Line 7
|
||||
8 Line 8
|
||||
9 Line 9
|
||||
10 Line 10
|
||||
11 Line 11
|
||||
12 Line 12
|
||||
13 Line 13
|
||||
14 Line 14
|
||||
15 Line 15
|
||||
16 Line 16
|
||||
17 Line 17
|
||||
18 Line 18
|
||||
19 Line 19
|
||||
20 Line 20
|
||||
21 Line 21
|
||||
22 Line 22
|
||||
23 Line 23
|
||||
24 Line 24
|
||||
25 Line 25
|
||||
26 Line 26
|
||||
27 Line 27
|
||||
28 Line 28
|
||||
29 Line 29
|
||||
30 Line 30
|
||||
31 Line 31
|
||||
32 Line 32
|
||||
33 Line 33
|
||||
34 Line 34
|
||||
35 Line 35
|
||||
36 Line 36
|
||||
37 Line 37
|
||||
38 Line 38
|
||||
39 Line 39
|
||||
40 Line 40
|
||||
41 Line 41
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
" ✦ Example code block:
|
||||
1 Line 1
|
||||
2 Line 2
|
||||
3 Line 3
|
||||
4 Line 4
|
||||
5 Line 5
|
||||
6 Line 6
|
||||
7 Line 7
|
||||
8 Line 8
|
||||
9 Line 9
|
||||
10 Line 10
|
||||
11 Line 11
|
||||
12 Line 12
|
||||
13 Line 13
|
||||
14 Line 14
|
||||
15 Line 15
|
||||
16 Line 16
|
||||
17 Line 17
|
||||
18 Line 18
|
||||
19 Line 19
|
||||
20 Line 20
|
||||
21 Line 21
|
||||
22 Line 22
|
||||
23 Line 23
|
||||
24 Line 24
|
||||
25 Line 25
|
||||
26 Line 26
|
||||
27 Line 27
|
||||
28 Line 28
|
||||
29 Line 29
|
||||
30 Line 30
|
||||
31 Line 31
|
||||
32 Line 32
|
||||
33 Line 33
|
||||
34 Line 34
|
||||
35 Line 35
|
||||
36 Line 36
|
||||
37 Line 37
|
||||
38 Line 38
|
||||
39 Line 39
|
||||
40 Line 40
|
||||
41 Line 41
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
`;
|
||||
|
||||
exports[`<HistoryItemDisplay /> > should render a full gemini_content item when using availableTerminalHeightGemini 1`] = `
|
||||
" Example code block:
|
||||
1 Line 1
|
||||
2 Line 2
|
||||
3 Line 3
|
||||
4 Line 4
|
||||
5 Line 5
|
||||
6 Line 6
|
||||
7 Line 7
|
||||
8 Line 8
|
||||
9 Line 9
|
||||
10 Line 10
|
||||
11 Line 11
|
||||
12 Line 12
|
||||
13 Line 13
|
||||
14 Line 14
|
||||
15 Line 15
|
||||
16 Line 16
|
||||
17 Line 17
|
||||
18 Line 18
|
||||
19 Line 19
|
||||
20 Line 20
|
||||
21 Line 21
|
||||
22 Line 22
|
||||
23 Line 23
|
||||
24 Line 24
|
||||
25 Line 25
|
||||
26 Line 26
|
||||
27 Line 27
|
||||
28 Line 28
|
||||
29 Line 29
|
||||
30 Line 30
|
||||
31 Line 31
|
||||
32 Line 32
|
||||
33 Line 33
|
||||
34 Line 34
|
||||
35 Line 35
|
||||
36 Line 36
|
||||
37 Line 37
|
||||
38 Line 38
|
||||
39 Line 39
|
||||
40 Line 40
|
||||
41 Line 41
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
" Example code block:
|
||||
1 Line 1
|
||||
2 Line 2
|
||||
3 Line 3
|
||||
4 Line 4
|
||||
5 Line 5
|
||||
6 Line 6
|
||||
7 Line 7
|
||||
8 Line 8
|
||||
9 Line 9
|
||||
10 Line 10
|
||||
11 Line 11
|
||||
12 Line 12
|
||||
13 Line 13
|
||||
14 Line 14
|
||||
15 Line 15
|
||||
16 Line 16
|
||||
17 Line 17
|
||||
18 Line 18
|
||||
19 Line 19
|
||||
20 Line 20
|
||||
21 Line 21
|
||||
22 Line 22
|
||||
23 Line 23
|
||||
24 Line 24
|
||||
25 Line 25
|
||||
26 Line 26
|
||||
27 Line 27
|
||||
28 Line 28
|
||||
29 Line 29
|
||||
30 Line 30
|
||||
31 Line 31
|
||||
32 Line 32
|
||||
33 Line 33
|
||||
34 Line 34
|
||||
35 Line 35
|
||||
36 Line 36
|
||||
37 Line 37
|
||||
38 Line 38
|
||||
39 Line 39
|
||||
40 Line 40
|
||||
41 Line 41
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
`;
|
||||
|
||||
exports[`<HistoryItemDisplay /> > should render a truncated gemini item 1`] = `
|
||||
"✦ Example code block:
|
||||
... first 41 lines hidden ...
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
" ✦ Example code block:
|
||||
... first 41 lines hidden ...
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
`;
|
||||
|
||||
exports[`<HistoryItemDisplay /> > should render a truncated gemini_content item 1`] = `
|
||||
" Example code block:
|
||||
... first 41 lines hidden ...
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
" Example code block:
|
||||
... first 41 lines hidden ...
|
||||
42 Line 42
|
||||
43 Line 43
|
||||
44 Line 44
|
||||
45 Line 45
|
||||
46 Line 46
|
||||
47 Line 47
|
||||
48 Line 48
|
||||
49 Line 49
|
||||
50 Line 50"
|
||||
`;
|
||||
|
||||
@@ -20,38 +20,38 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-collapsed-match 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
(r:) commit
|
||||
(r:) commit
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
git commit -m "feat: add search" in src/app"
|
||||
git commit -m "feat: add search" in src/app"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-expanded-match 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
(r:) commit
|
||||
(r:) commit
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
git commit -m "feat: add search" in src/app"
|
||||
git commit -m "feat: add search" in src/app"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
> Type your message or @path/to/file
|
||||
> Type your message or @path/to/file
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
! Type your message or @path/to/file
|
||||
! Type your message or @path/to/file
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
* Type your message or @path/to/file
|
||||
* Type your message or @path/to/file
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
> Type your message or @path/to/file
|
||||
> Type your message or @path/to/file
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
@@ -6,30 +6,17 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -40,30 +27,17 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -74,30 +48,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode true* │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code true* │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -108,30 +69,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false* │
|
||||
│ │
|
||||
│ Disable Auto Update false* │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false* │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false* │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode false* │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false* │
|
||||
│ Show Line Numbers in Code false* │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -142,30 +90,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode (Modified in System) false │
|
||||
│ │
|
||||
│ Disable Auto Update (Modified in System) false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode (Modified in System) false │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -176,30 +111,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode (Modified in Workspace) false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging (Modified in Workspace) false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode (Modified in Workspace) false │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -210,30 +132,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -244,30 +153,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false* │
|
||||
│ │
|
||||
│ Disable Auto Update true* │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false* │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode false* │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -278,30 +174,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE false │
|
||||
│ Show Line Numbers in Code false │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -312,30 +195,17 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode true* │
|
||||
│ │
|
||||
│ Disable Auto Update true* │
|
||||
│ │
|
||||
│ Debug Keystroke Logging true* │
|
||||
│ │
|
||||
│ ● Tool Approval Mode Default │
|
||||
│ Language Auto (detect from system) │
|
||||
│ │
|
||||
│ Terminal Bell true │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ │
|
||||
│ Hide Window Title true* │
|
||||
│ │
|
||||
│ Show Status in Title false │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ Interactive Shell (PTY) false │
|
||||
│ Theme Qwen Dark │
|
||||
│ Preferred Editor │
|
||||
│ Auto-connect to IDE true* │
|
||||
│ Show Line Numbers in Code true* │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus) │
|
||||
│ (Use Enter to select, Tab to configure scope) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -4,10 +4,11 @@ exports[`ThemeDialog Snapshots > should render correctly in scope selector mode
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Apply To │
|
||||
│ │
|
||||
│ ● 1. User Settings │
|
||||
│ 2. Workspace Settings │
|
||||
│ │
|
||||
│ (Use Enter to apply scope, Tab to select theme) │
|
||||
│ (Use Enter to apply scope, Tab to go back) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ToolStatsDisplay /> > should display stats for a single tool correctly 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ test-tool 1 100.0% 100ms │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 1 │
|
||||
│ » Accepted: 1 │
|
||||
│ » Rejected: 0 │
|
||||
│ » Modified: 0 │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: 100.0% │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ test-tool 1 100.0% 100ms │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 1 │
|
||||
│ » Accepted: 1 │
|
||||
│ » Rejected: 0 │
|
||||
│ » Modified: 0 │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: 100.0% │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolStatsDisplay /> > should display stats for multiple tools correctly 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ tool-a 2 50.0% 100ms │
|
||||
│ tool-b 1 100.0% 100ms │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 3 │
|
||||
│ » Accepted: 1 │
|
||||
│ » Rejected: 1 │
|
||||
│ » Modified: 1 │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: 33.3% │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ tool-a 2 50.0% 100ms │
|
||||
│ tool-b 1 100.0% 100ms │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 3 │
|
||||
│ » Accepted: 1 │
|
||||
│ » Rejected: 1 │
|
||||
│ » Modified: 1 │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: 33.3% │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolStatsDisplay /> > should handle large values without wrapping or overlapping 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ long-named-tool-for-testi99999999 88.9% 1ms │
|
||||
│ ng-wrapping-and-such 9 │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 222234566 │
|
||||
│ » Accepted: 123456789 │
|
||||
│ » Rejected: 98765432 │
|
||||
│ » Modified: 12345 │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: 55.6% │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ long-named-tool-for-testi99999999 88.9% 1ms │
|
||||
│ ng-wrapping-and-such 9 │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 222234566 │
|
||||
│ » Accepted: 123456789 │
|
||||
│ » Rejected: 98765432 │
|
||||
│ » Modified: 12345 │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: 55.6% │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolStatsDisplay /> > should handle zero decisions gracefully 1`] = `
|
||||
"╭────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ test-tool 1 100.0% 100ms │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 0 │
|
||||
│ » Accepted: 0 │
|
||||
│ » Rejected: 0 │
|
||||
│ » Modified: 0 │
|
||||
│ ──────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: -- │
|
||||
│ │
|
||||
╰────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Tool Stats For Nerds │
|
||||
│ │
|
||||
│ Tool Name Calls Success Rate Avg Duration │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ test-tool 1 100.0% 100ms │
|
||||
│ │
|
||||
│ User Decision Summary │
|
||||
│ Total Reviewed Suggestions: 0 │
|
||||
│ » Accepted: 0 │
|
||||
│ » Rejected: 0 │
|
||||
│ » Modified: 0 │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
|
||||
│ Overall Agreement Rate: -- │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolStatsDisplay /> > should render "no tool calls" message when there are no active tools 1`] = `
|
||||
|
||||
@@ -35,7 +35,7 @@ index 0000000..e69de29
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiffContent}
|
||||
filename="test.py"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -63,7 +63,7 @@ index 0000000..e69de29
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiffContent}
|
||||
filename="test.unknown"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -88,7 +88,7 @@ index 0000000..e69de29
|
||||
`;
|
||||
render(
|
||||
<OverflowProvider>
|
||||
<DiffRenderer diffContent={newFileDiffContent} terminalWidth={80} />
|
||||
<DiffRenderer diffContent={newFileDiffContent} contentWidth={80} />
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith(
|
||||
@@ -115,7 +115,7 @@ index 0000001..0000002 100644
|
||||
<DiffRenderer
|
||||
diffContent={existingFileDiffContent}
|
||||
filename="test.txt"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -145,7 +145,7 @@ index 1234567..1234567 100644
|
||||
<DiffRenderer
|
||||
diffContent={noChangeDiff}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -156,7 +156,7 @@ index 1234567..1234567 100644
|
||||
it('should handle empty diff content', () => {
|
||||
const { lastFrame } = render(
|
||||
<OverflowProvider>
|
||||
<DiffRenderer diffContent="" terminalWidth={80} />
|
||||
<DiffRenderer diffContent="" contentWidth={80} />
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).toContain('No diff content');
|
||||
@@ -182,7 +182,7 @@ index 123..456 100644
|
||||
<DiffRenderer
|
||||
diffContent={diffWithGap}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -219,7 +219,7 @@ index abc..def 100644
|
||||
<DiffRenderer
|
||||
diffContent={diffWithSmallGap}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -291,7 +291,7 @@ index 123..789 100644
|
||||
<DiffRenderer
|
||||
diffContent={diffWithMultipleHunks}
|
||||
filename="multi.js"
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={terminalWidth}
|
||||
availableTerminalHeight={height}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
@@ -323,7 +323,7 @@ fileDiff Index: file.txt
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiff}
|
||||
filename="TEST"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
@@ -353,7 +353,7 @@ fileDiff Index: Dockerfile
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiff}
|
||||
filename="Dockerfile"
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ interface DiffRendererProps {
|
||||
filename?: string;
|
||||
tabWidth?: number;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
theme?: Theme;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
filename,
|
||||
tabWidth = DEFAULT_TAB_WIDTH,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
theme,
|
||||
}) => {
|
||||
const screenReaderEnabled = useIsScreenReaderEnabled();
|
||||
@@ -155,7 +155,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
addedContent,
|
||||
language,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
theme,
|
||||
);
|
||||
} else {
|
||||
@@ -164,7 +164,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
filename,
|
||||
tabWidth,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ const renderDiffContent = (
|
||||
filename: string | undefined,
|
||||
tabWidth = DEFAULT_TAB_WIDTH,
|
||||
availableTerminalHeight: number | undefined,
|
||||
terminalWidth: number,
|
||||
contentWidth: number,
|
||||
) => {
|
||||
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
|
||||
const normalizedLines = parsedLines.map((line) => ({
|
||||
@@ -238,7 +238,7 @@ const renderDiffContent = (
|
||||
return (
|
||||
<MaxSizedBox
|
||||
maxHeight={availableTerminalHeight}
|
||||
maxWidth={terminalWidth}
|
||||
maxWidth={contentWidth}
|
||||
key={key}
|
||||
>
|
||||
{displayableLines.reduce<React.ReactNode[]>((acc, line, index) => {
|
||||
@@ -260,7 +260,7 @@ const renderDiffContent = (
|
||||
acc.push(
|
||||
<Box key={`gap-${index}`}>
|
||||
<Text wrap="truncate" color={semanticTheme.text.secondary}>
|
||||
{'═'.repeat(terminalWidth)}
|
||||
{'═'.repeat(contentWidth)}
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ErrorMessage: React.FC<ErrorMessageProps> = ({ text }) => {
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.status.error}>{prefix}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -14,14 +14,14 @@ interface GeminiMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
export const GeminiMessage: React.FC<GeminiMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
}) => {
|
||||
const prefix = '✦ ';
|
||||
const prefixWidth = prefix.length;
|
||||
@@ -38,7 +38,7 @@ export const GeminiMessage: React.FC<GeminiMessageProps> = ({
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -12,7 +12,7 @@ interface GeminiMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -25,7 +25,7 @@ export const GeminiMessageContent: React.FC<GeminiMessageContentProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
}) => {
|
||||
const originalPrefix = '✦ ';
|
||||
const prefixWidth = originalPrefix.length;
|
||||
@@ -36,7 +36,7 @@ export const GeminiMessageContent: React.FC<GeminiMessageContentProps> = ({
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ interface GeminiThoughtMessageProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,13 +24,13 @@ export const GeminiThoughtMessage: React.FC<GeminiThoughtMessageProps> = ({
|
||||
text,
|
||||
isPending,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
}) => {
|
||||
const prefix = '✦ ';
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.text.secondary}>{prefix}</Text>
|
||||
</Box>
|
||||
@@ -39,7 +39,7 @@ export const GeminiThoughtMessage: React.FC<GeminiThoughtMessageProps> = ({
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -13,7 +13,7 @@ interface GeminiThoughtMessageContentProps {
|
||||
text: string;
|
||||
isPending: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,17 +22,17 @@ interface GeminiThoughtMessageContentProps {
|
||||
*/
|
||||
export const GeminiThoughtMessageContent: React.FC<
|
||||
GeminiThoughtMessageContentProps
|
||||
> = ({ text, isPending, availableTerminalHeight, terminalWidth }) => {
|
||||
> = ({ text, isPending, availableTerminalHeight, contentWidth }) => {
|
||||
const originalPrefix = '✦ ';
|
||||
const prefixWidth = originalPrefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" paddingLeft={prefixWidth} marginBottom={1}>
|
||||
<Box flexDirection="column" paddingLeft={prefixWidth}>
|
||||
<MarkdownDisplay
|
||||
text={text}
|
||||
isPending={isPending}
|
||||
availableTerminalHeight={availableTerminalHeight}
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth - prefixWidth}
|
||||
textColor={theme.text.secondary}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const InfoMessage: React.FC<InfoMessageProps> = ({ text }) => {
|
||||
const prefixWidth = prefix.length;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={theme.status.warning}>{prefix}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={confirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={confirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={confirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={details}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={details}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -212,7 +212,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
{
|
||||
settings: {
|
||||
@@ -235,7 +235,7 @@ describe('ToolConfirmationMessage', () => {
|
||||
confirmationDetails={editConfirmationDetails}
|
||||
config={mockConfig}
|
||||
availableTerminalHeight={30}
|
||||
terminalWidth={80}
|
||||
contentWidth={80}
|
||||
/>,
|
||||
{
|
||||
settings: {
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface ToolConfirmationMessageProps {
|
||||
config: Config;
|
||||
isFocused?: boolean;
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
compactMode?: boolean;
|
||||
}
|
||||
|
||||
@@ -42,11 +42,10 @@ export const ToolConfirmationMessage: React.FC<
|
||||
config,
|
||||
isFocused = true,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
compactMode = false,
|
||||
}) => {
|
||||
const { onConfirm } = confirmationDetails;
|
||||
const childWidth = terminalWidth - 2; // 2 for padding
|
||||
|
||||
const settings = useSettings();
|
||||
const preferredEditor = settings.merged.general?.preferredEditor as
|
||||
@@ -226,7 +225,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
diffContent={confirmationDetails.fileDiff}
|
||||
filename={confirmationDetails.fileName}
|
||||
availableTerminalHeight={availableBodyContentHeight()}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
@@ -263,7 +262,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
<Box paddingX={1} marginLeft={1}>
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(childWidth - 4, 1)}
|
||||
maxWidth={Math.max(contentWidth, 1)}
|
||||
>
|
||||
<Box>
|
||||
<Text color={theme.text.link}>{executionProps.command}</Text>
|
||||
@@ -298,7 +297,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
text={planProps.plan}
|
||||
isPending={false}
|
||||
availableTerminalHeight={availableBodyContentHeight()}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
@@ -397,7 +396,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1} width={childWidth}>
|
||||
<Box flexDirection="column" padding={1} width={contentWidth}>
|
||||
{/* Body Content (Diff Renderer or Command Info) */}
|
||||
{/* No separate context display here anymore for edits */}
|
||||
<Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
|
||||
const baseProps = {
|
||||
groupId: 1,
|
||||
terminalWidth: 80,
|
||||
contentWidth: 80,
|
||||
isFocused: true,
|
||||
};
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={40}
|
||||
contentWidth={40}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
|
||||
@@ -19,7 +19,7 @@ interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
toolCalls: IndividualToolCallDisplay[];
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
isFocused?: boolean;
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
@@ -30,7 +30,7 @@ interface ToolGroupMessageProps {
|
||||
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
toolCalls,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
isFocused = true,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
@@ -58,9 +58,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
: theme.border.default;
|
||||
|
||||
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
|
||||
// This is a bit of a magic number, but it accounts for the border and
|
||||
// marginLeft.
|
||||
const innerWidth = terminalWidth - 4;
|
||||
// account for border (2 chars) and padding (2 chars)
|
||||
const innerWidth = contentWidth - 4;
|
||||
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
// note, after the CTA, this automatically moves over to the next 'confirming' tool
|
||||
@@ -96,8 +95,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
Ink to render the border of the box incorrectly and span multiple lines and even
|
||||
cause tearing.
|
||||
*/
|
||||
width="100%"
|
||||
marginLeft={1}
|
||||
width={contentWidth}
|
||||
borderDimColor={
|
||||
hasPending && (!isShellCommand || !isEmbeddedShellFocused)
|
||||
}
|
||||
@@ -112,7 +110,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
<ToolMessage
|
||||
{...tool}
|
||||
availableTerminalHeight={availableTerminalHeightPerToolMessage}
|
||||
terminalWidth={innerWidth}
|
||||
contentWidth={innerWidth}
|
||||
emphasis={
|
||||
isConfirming
|
||||
? 'high'
|
||||
@@ -135,7 +133,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightPerToolMessage
|
||||
}
|
||||
terminalWidth={innerWidth}
|
||||
contentWidth={innerWidth}
|
||||
/>
|
||||
)}
|
||||
{tool.outputFile && (
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('<ToolMessage />', () => {
|
||||
description: 'A tool for testing',
|
||||
resultDisplay: 'Test result',
|
||||
status: ToolCallStatus.Success,
|
||||
terminalWidth: 80,
|
||||
contentWidth: 80,
|
||||
confirmationDetails: undefined,
|
||||
emphasis: 'medium',
|
||||
config: mockConfig,
|
||||
@@ -241,7 +241,7 @@ describe('<ToolMessage />', () => {
|
||||
description: 'Delegate task to subagent',
|
||||
resultDisplay: subagentResultDisplay,
|
||||
status: ToolCallStatus.Executing,
|
||||
terminalWidth: 80,
|
||||
contentWidth: 80,
|
||||
callId: 'test-call-id-2',
|
||||
confirmationDetails: undefined,
|
||||
config: mockConfig,
|
||||
|
||||
@@ -186,7 +186,7 @@ const StringResultRenderer: React.FC<{
|
||||
text={displayData}
|
||||
isPending={false}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={childWidth}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
@@ -215,13 +215,13 @@ const DiffResultRenderer: React.FC<{
|
||||
diffContent={data.fileDiff}
|
||||
filename={data.fileName}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={childWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
export interface ToolMessageProps extends IndividualToolCallDisplay {
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
emphasis?: TextEmphasis;
|
||||
renderOutputAsMarkdown?: boolean;
|
||||
activeShellPtyId?: number | null;
|
||||
@@ -235,7 +235,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
resultDisplay,
|
||||
status,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
emphasis = 'medium',
|
||||
renderOutputAsMarkdown = true,
|
||||
activeShellPtyId,
|
||||
@@ -291,6 +291,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
MIN_LINES_SHOWN + 1, // enforce minimum lines shown
|
||||
)
|
||||
: undefined;
|
||||
const innerWidth = contentWidth - STATUS_INDICATOR_WIDTH;
|
||||
|
||||
// Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly,
|
||||
// we're forcing it to not render as markdown when the response is too long, it will fallback
|
||||
@@ -299,8 +300,6 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
renderOutputAsMarkdown = false;
|
||||
}
|
||||
|
||||
const childWidth = terminalWidth - 3; // account for padding.
|
||||
|
||||
// Use the custom hook to determine the display type
|
||||
const displayRenderer = useResultDisplayRenderer(resultDisplay);
|
||||
|
||||
@@ -333,14 +332,14 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
<PlanResultRenderer
|
||||
data={displayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={childWidth}
|
||||
childWidth={innerWidth}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'task' && config && (
|
||||
<SubagentExecutionRenderer
|
||||
data={displayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={childWidth}
|
||||
childWidth={innerWidth}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
@@ -348,7 +347,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
<DiffResultRenderer
|
||||
data={displayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={childWidth}
|
||||
childWidth={innerWidth}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'ansi' && (
|
||||
@@ -362,7 +361,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
data={displayRenderer.data}
|
||||
renderAsMarkdown={renderOutputAsMarkdown}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={childWidth}
|
||||
childWidth={innerWidth}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const WarningMessage: React.FC<WarningMessageProps> = ({ text }) => {
|
||||
const prefixWidth = 3;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Box width={prefixWidth}>
|
||||
<Text color={Colors.AccentYellow}>{prefix}</Text>
|
||||
</Box>
|
||||
|
||||
@@ -1,105 +1,108 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shell commands even when successful 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: o test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: o test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ? first-confirm - A tool for testing (high) │
|
||||
│MockConfirmation: Confirm first tool │
|
||||
│ │
|
||||
│MockTool[tool-2]: ? second-confirm - A tool for testing (low) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ? first-confirm - A tool for testing (high) │
|
||||
│MockConfirmation: Confirm first tool │
|
||||
│ │
|
||||
│MockTool[tool-2]: ? second-confirm - A tool for testing (low) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ read_file - Read a file (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) │
|
||||
│ │
|
||||
│MockTool[tool-3]: o write_file - Write to file (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ read_file - Read a file (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) │
|
||||
│ │
|
||||
│MockTool[tool-3]: o write_file - Write to file (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: o pending-tool - This tool is pending (medium) │
|
||||
│ │
|
||||
│MockTool[tool-3]: x error-tool - This tool failed (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: o pending-tool - This tool is pending (medium) │
|
||||
│ │
|
||||
│MockTool[tool-3]: x error-tool - This tool failed (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation (high) │
|
||||
│MockConfirmation: Are you sure you want to proceed? │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation │
|
||||
│(high) │
|
||||
│MockConfirmation: Are you sure you want to proceed? │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ✓ another-tool - Another tool (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ✓ another-tool - Another tool (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ very-long-tool-name-that-might-wrap - This is a very long description that │
|
||||
│might cause wrapping issues (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ │
|
||||
│very-long-tool-name-that-might-wrap - │
|
||||
│This is a very long description that │
|
||||
│might cause wrapping issues (medium) │
|
||||
╰──────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Height Calculation > calculates available height correctly with multiple tools with results 1`] = `
|
||||
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) │
|
||||
│ │
|
||||
│MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) │
|
||||
│ │
|
||||
│MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) │
|
||||
│ │
|
||||
│MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -45,6 +45,7 @@ export function ScopeSelector({
|
||||
{isFocused ? '> ' : ' '}
|
||||
{t('Apply To')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
initialIndex={safeInitialIndex}
|
||||
|
||||
@@ -172,7 +172,7 @@ export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
|
||||
confirmationDetails={data.pendingConfirmation}
|
||||
isFocused={true}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={childWidth - 4}
|
||||
compactMode={true}
|
||||
config={config}
|
||||
/>
|
||||
@@ -242,7 +242,7 @@ export const AgentExecutionDisplay: React.FC<AgentExecutionDisplayProps> = ({
|
||||
config={config}
|
||||
isFocused={true}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
contentWidth={childWidth - 4}
|
||||
compactMode={true}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
36
packages/cli/src/ui/components/views/SkillsList.tsx
Normal file
36
packages/cli/src/ui/components/views/SkillsList.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type SkillDefinition } from '../../types.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
|
||||
interface SkillsListProps {
|
||||
skills: readonly SkillDefinition[];
|
||||
}
|
||||
|
||||
export const SkillsList: React.FC<SkillsListProps> = ({ skills }) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{t('Available skills:')}
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{skills.length > 0 ? (
|
||||
skills.map((skill) => (
|
||||
<Box key={skill.name} flexDirection="row">
|
||||
<Text color={theme.text.primary}>{' '}- </Text>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{skill.name}
|
||||
</Text>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Text color={theme.text.primary}> {t('No skills available')}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -33,11 +33,7 @@ const mockTools: ToolDefinition[] = [
|
||||
describe('<ToolsList />', () => {
|
||||
it('renders correctly with descriptions', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolsList
|
||||
tools={mockTools}
|
||||
showDescriptions={true}
|
||||
terminalWidth={40}
|
||||
/>,
|
||||
<ToolsList tools={mockTools} showDescriptions={true} contentWidth={40} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
@@ -47,7 +43,7 @@ describe('<ToolsList />', () => {
|
||||
<ToolsList
|
||||
tools={mockTools}
|
||||
showDescriptions={false}
|
||||
terminalWidth={40}
|
||||
contentWidth={40}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
@@ -55,7 +51,7 @@ describe('<ToolsList />', () => {
|
||||
|
||||
it('renders correctly with no tools', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolsList tools={[]} showDescriptions={true} terminalWidth={40} />,
|
||||
<ToolsList tools={[]} showDescriptions={true} contentWidth={40} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -14,15 +14,15 @@ import { t } from '../../../i18n/index.js';
|
||||
interface ToolsListProps {
|
||||
tools: readonly ToolDefinition[];
|
||||
showDescriptions: boolean;
|
||||
terminalWidth: number;
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
export const ToolsList: React.FC<ToolsListProps> = ({
|
||||
tools,
|
||||
showDescriptions,
|
||||
terminalWidth,
|
||||
contentWidth,
|
||||
}) => (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text.primary}>
|
||||
{t('Available Qwen Code CLI tools:')}
|
||||
</Text>
|
||||
@@ -38,7 +38,7 @@ export const ToolsList: React.FC<ToolsListProps> = ({
|
||||
</Text>
|
||||
{showDescriptions && tool.description && (
|
||||
<MarkdownDisplay
|
||||
terminalWidth={terminalWidth}
|
||||
contentWidth={contentWidth}
|
||||
text={tool.description}
|
||||
isPending={false}
|
||||
/>
|
||||
|
||||
@@ -11,15 +11,13 @@ exports[`<ToolsList /> > renders correctly with descriptions 1`] = `
|
||||
2. note use this tool wisely and be sure to consider how this tool interacts with word wrap.
|
||||
3. important this tool is awesome.
|
||||
- Test Tool Three (test-tool-three)
|
||||
This is the third test tool.
|
||||
"
|
||||
This is the third test tool."
|
||||
`;
|
||||
|
||||
exports[`<ToolsList /> > renders correctly with no tools 1`] = `
|
||||
"Available Qwen Code CLI tools:
|
||||
|
||||
No tools available
|
||||
"
|
||||
No tools available"
|
||||
`;
|
||||
|
||||
exports[`<ToolsList /> > renders correctly without descriptions 1`] = `
|
||||
@@ -27,6 +25,5 @@ exports[`<ToolsList /> > renders correctly without descriptions 1`] = `
|
||||
|
||||
- Test Tool One
|
||||
- Test Tool Two
|
||||
- Test Tool Three
|
||||
"
|
||||
- Test Tool Three"
|
||||
`;
|
||||
|
||||
@@ -19,6 +19,8 @@ import { type VisionSwitchOutcome } from '../components/ModelSwitchDialog.js';
|
||||
import { type OpenAICredentials } from '../components/OpenAIKeyPrompt.js';
|
||||
|
||||
export interface UIActions {
|
||||
openThemeDialog: () => void;
|
||||
openEditorDialog: () => void;
|
||||
handleThemeSelect: (
|
||||
themeName: string | undefined,
|
||||
scope: SettingScope,
|
||||
|
||||
@@ -212,7 +212,6 @@ describe('useGeminiStream', () => {
|
||||
geminiMdFileCount: 0,
|
||||
alwaysSkipModificationConfirmation: false,
|
||||
vertexai: false,
|
||||
showMemoryUsage: false,
|
||||
contextFileName: undefined,
|
||||
getToolRegistry: vi.fn(
|
||||
() => ({ getToolSchemaList: vi.fn(() => []) }) as any,
|
||||
|
||||
@@ -573,6 +573,45 @@ describe('useSlashCompletion', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should map completion items with descriptions for argument suggestions', async () => {
|
||||
const mockCompletionFn = vi.fn().mockResolvedValue([
|
||||
{ value: 'pdf', description: 'Create PDF documents' },
|
||||
{ value: 'xlsx', description: 'Work with spreadsheets' },
|
||||
]);
|
||||
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'skills',
|
||||
description: 'List available skills',
|
||||
completion: mockCompletionFn,
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/skills ',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toEqual([
|
||||
{
|
||||
label: 'pdf',
|
||||
value: 'pdf',
|
||||
description: 'Create PDF documents',
|
||||
},
|
||||
{
|
||||
label: 'xlsx',
|
||||
value: 'xlsx',
|
||||
description: 'Work with spreadsheets',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call command.completion with an empty string when args start with a space', async () => {
|
||||
const mockCompletionFn = vi
|
||||
.fn()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AsyncFzf } from 'fzf';
|
||||
import type { Suggestion } from '../components/SuggestionsDisplay.js';
|
||||
import {
|
||||
CommandKind,
|
||||
type CommandCompletionItem,
|
||||
type CommandContext,
|
||||
type SlashCommand,
|
||||
} from '../commands/types.js';
|
||||
@@ -215,10 +216,9 @@ function useCommandSuggestions(
|
||||
)) || [];
|
||||
|
||||
if (!signal.aborted) {
|
||||
const finalSuggestions = results.map((s) => ({
|
||||
label: s,
|
||||
value: s,
|
||||
}));
|
||||
const finalSuggestions = results
|
||||
.map((item) => toSuggestion(item))
|
||||
.filter((suggestion): suggestion is Suggestion => !!suggestion);
|
||||
setSuggestions(finalSuggestions);
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -310,6 +310,20 @@ function useCommandSuggestions(
|
||||
return { suggestions, isLoading };
|
||||
}
|
||||
|
||||
function toSuggestion(item: string | CommandCompletionItem): Suggestion | null {
|
||||
if (typeof item === 'string') {
|
||||
return { label: item, value: item };
|
||||
}
|
||||
if (!item.value) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: item.label ?? item.value,
|
||||
value: item.value,
|
||||
description: item.description,
|
||||
};
|
||||
}
|
||||
|
||||
function useCompletionPositions(
|
||||
query: string | null,
|
||||
parserResult: CommandParserResult,
|
||||
|
||||
@@ -6,19 +6,21 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const TERMINAL_PADDING_X = 8;
|
||||
|
||||
/**
|
||||
* Returns the actual terminal size without any padding adjustments.
|
||||
* Components should handle their own margins/padding as needed.
|
||||
*/
|
||||
export function useTerminalSize(): { columns: number; rows: number } {
|
||||
const [size, setSize] = useState({
|
||||
columns: (process.stdout.columns || 60) - TERMINAL_PADDING_X,
|
||||
rows: process.stdout.rows || 20,
|
||||
columns: process.stdout.columns || 80,
|
||||
rows: process.stdout.rows || 24,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function updateSize() {
|
||||
setSize({
|
||||
columns: (process.stdout.columns || 60) - TERMINAL_PADDING_X,
|
||||
rows: process.stdout.rows || 20,
|
||||
columns: process.stdout.columns || 80,
|
||||
rows: process.stdout.rows || 24,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user