Compare commits

..

295 Commits

Author SHA1 Message Date
yiliang114
c65436cf61 feat(chrome-extension): enhance native host communication and network logging
- Add troubleshooting documentation for native host setup issues
- Improve native host logging to home directory with fallback to tmp
- Enhance network logging in service worker with response body capture
- Update scripts to properly reference host.js from correct path
- Increase timeout for MCP session creation and long prompts from 3 to 5 minutes
- Add getConsoleLogs functionality to sidepanel for content script capture
- Improve browser-mcp-server network logs aggregation by request ID
- Update icon assets and improve manifest configuration

refactor(chrome-extension): consolidate host.js entry point and improve path resolution

- Create unified host.js entry point that delegates to src/host.js
- Improve path resolution for host scripts in installer and runner scripts
- Add proper path existence checks for browser-mcp-server.js
- Support running from different directory structures

style(chrome-extension): improve TypeScript type safety and error handling

- Add proper type definitions for message handling in side panel
- Add null checks and error handling for message parsing
- Improve React component callback implementations
2026-01-08 01:25:50 +08:00
yiliang114
814518889c refactor(chrome-extension): reorganize directory structure for better maintainability
This commit reorganizes the entire Chrome extension package structure for improved maintainability and clarity:

- Move all source files to `src/` directory (background, content, sidepanel)
- Move build configurations to `config/` directory
- Move documentation to `docs/` directory with proper categorization
- Move all script files to `scripts/` directory
- Move native-host specific files to appropriate subdirectories (`src`, `scripts`, `config`)
- Update package.json scripts to reflect new file locations
- Add comprehensive documentation files (debugging, development, architecture, API reference)
- Maintain all functionality while improving project organization

The reorganization separates source code from build output, centralizes documentation, and creates a clear separation of concerns making the project more maintainable and easier for developers to navigate.
2026-01-07 21:13:29 +08:00
yiliang114
39413b9df9 feat(chrome-extension): enhance network monitoring with webRequest API
Replace the existing network monitoring implementation with a more comprehensive solution that combines both webRequest and debugger APIs for broader coverage. The new implementation:

- Uses chrome.webRequest.onBeforeRequest to capture all outgoing requests
- Uses chrome.webRequest.onCompleted to capture completed responses
- Uses chrome.webRequest.onErrorOccurred to capture failed requests
- Retains debugger API integration for detailed network information
- Implements memory management with maximum 1000 logs per tab
- Adds proper initialization and cleanup for each tab
- Ensures graceful handling of debugger attachment failures
- Provides more reliable network activity capture across all tabs

This enhancement significantly improves the reliability and coverage of network monitoring functionality in the Chrome extension.
2026-01-07 20:08:19 +08:00
yiliang114
5249937b49 refactor(chrome-extension): rename chrome-qwen-bridge package to chrome-extension 2026-01-07 20:03:04 +08:00
yiliang114
c2965c1926 Merge branch 'main' of https://github.com/QwenLM/qwen-code into test/chrome-extension 2026-01-07 20:00:13 +08:00
tanzhenxin
570ec432af Merge pull request #1282 from BlockHand/fix-sandbox-ideInstall
feat: Optimize the issue where an error message indicating unfriendli…
2026-01-07 17:40:45 +08:00
tanzhenxin
bfc3bbfa9c update user messages 2026-01-07 17:25:27 +08:00
tanzhenxin
91af9bf6c8 Merge branch 'main' into fix-sandbox-ideInstall 2026-01-07 17:12:22 +08:00
tanzhenxin
f6771c0858 Merge pull request #1393 from Weaxs/main
[OpenaiContentGenerate] convertOpenAIResponseToGemini record thoughtsTokenCount
2026-01-07 17:05:28 +08:00
tanzhenxin
2c8be05029 Merge pull request #1415 from QwenLM/fix/openai-reasoning-config
fix(core): don’t force reasoning/topP defaults for OpenAI-compatible APIs
2026-01-07 16:59:26 +08:00
tanzhenxin
4744af1ea8 Merge pull request #1406 from QwenLM/fix/non-interactive-tool-permission
fix(cli,core): honor `tools.core` / `tools.allowed` in non-interactive runs
2026-01-07 16:59:19 +08:00
Mingholy
2c285394c7 Merge pull request #1423 from QwenLM/chore/release-v0.6.1
chore: bump version to 0.6.1
2026-01-07 16:55:45 +08:00
mingholy.lmh
f2d941e469 chore: bump version to 0.6.1 2026-01-07 16:25:14 +08:00
tanzhenxin
9b2dfe1e06 Merge pull request #1374 from QwenLM/fix/resume-command-broken-after-new-chat
Fix resume command broken after new chat
2026-01-07 16:21:47 +08:00
tanzhenxin
3e695cd82b Merge pull request #1146 from QwenLM/fix/windows-background-terminal-execute-x
fix: improve windows background process handling and cleanup
2026-01-07 16:21:14 +08:00
xwj02155382
177a91f1d5 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2026-01-07 15:50:03 +08:00
Weaxs
870d207f18 revert enable_thinking & thinking_budget 2026-01-07 10:25:34 +08:00
tanzhenxin
3f512528cb Merge pull request #1391 from tt-a1i/feat/approval-mode-direct-arg
feat(cli): add direct argument support for /approval-mode command
2026-01-07 09:39:33 +08:00
Tu Shaokun
0878ee4cbd fix: handle setApprovalMode error in untrusted folders
Add try/catch to gracefully handle errors when setting privileged
modes (yolo/auto-edit) in untrusted folders, returning an error
message instead of throwing.
2026-01-06 22:04:43 +08:00
Tu Shaokun
bfe7298858 refactor: apply session-only approval mode per review feedback
- Remove persistence to user settings (no setValue call)
- Only use config.setApprovalMode() for session scope
- Remove autocomplete feature for simplicity
- Align with Shift+Tab behavior
2026-01-06 21:57:21 +08:00
Tu Shaokun
2f2937aafe test: add explicit assertions for setApprovalMode argument
Verify the exact mode value passed to config.setApprovalMode to catch
potential regressions in settings merge/update mechanism.
2026-01-06 21:54:33 +08:00
Tu Shaokun
8fcdd86b91 feat(cli): add direct argument support for /approval-mode command
Allow users to set approval mode directly via argument instead of
opening the dialog. For example:
- /approval-mode plan
- /approval-mode yolo
- /approval-mode auto-edit
- /approval-mode default

If no argument is provided, the dialog opens as before.
If an invalid argument is provided, an error message shows valid options.

Also adds tab completion for mode arguments.

Fixes #1353
2026-01-06 21:54:33 +08:00
tanzhenxin
d7d7bf0c39 fix default values of reasoning config for openai compatible api 2026-01-06 19:39:28 +08:00
gwinthis
b95d9a8d2d Merge pull request #1414 from QwenLM/doc/qwencode-java
Doc/qwencode java
2026-01-06 17:54:47 +08:00
顾盼
6f39ae120c Merge pull request #1355 from QwenLM/feat/stable-acp-flag
feat: graduate `--experimental-acp` to stable `--acp` flag
2026-01-06 17:51:43 +08:00
顾盼
627857621a Merge pull request #1365 from QwenLM/fix/missing-whitespaces
fix: preserve whitespace in thinking content for stream-json output format
2026-01-06 17:51:26 +08:00
顾盼
65c7cf5d8f Merge pull request #1376 from QwenLM/fix/missing-error-throw-nonInteractive
fix: exit with non-zero code on API errors in text mode
2026-01-06 17:51:13 +08:00
顾盼
7a823060ac Merge pull request #1383 from QwenLM/fix/tool-result-text-mode
fix: improve tool execution feedback in non-interactive mode
2026-01-06 17:50:58 +08:00
xwj02155382
2c88ea6dc1 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2026-01-06 17:26:01 +08:00
skyfire
ad3086f7dd add qwencode-sdk java doc 2026-01-06 17:18:41 +08:00
skyfire
8f3bbef575 add qwencode-sdk java doc 2026-01-06 17:11:47 +08:00
xwj02155382
e2d6ab9b7e refactor: simplify background shell command handling
- Remove ineffective error detection for background processes
  (stdio is detached/ignored, so cumulativeOutput is always empty)
- Add kill command hints for both Windows and macOS/Linux
- Simplify code from 40 lines to 12 lines with clearer logic
- Add explanatory comment about why startup errors cannot be reliably detected
2026-01-06 16:46:56 +08:00
Weaxs
35bf5ef4d0 remove duplicate reasoning_content handle 2026-01-06 14:38:01 +08:00
Weaxs
1d16513e27 remove duplicate reasoning_content handle && remove extra_body.enable_thinking 2026-01-06 14:38:01 +08:00
Weaxs
731fd99800 remove duplicate reasoning_content handle && remove extra_body.enable_thinking 2026-01-06 14:21:42 +08:00
skyfire
c6ae0a8be7 for alpha stage 2026-01-06 11:16:47 +08:00
gwinthis
49892a8e17 Merge pull request #1412 from QwenLM/feat/javasdk
Feat/javasdk
2026-01-06 09:49:36 +08:00
skyfire
d1a3e828b7 add license 2026-01-06 09:21:58 +08:00
pomelo
b19bb6cb20 Merge pull request #1378 from afarber/add-german-language-support
feat(i18n): add German language support
2026-01-05 22:14:00 +08:00
skyfire
e8625658ba publish 0.0.1-alpha 2026-01-05 20:27:37 +08:00
tanzhenxin
19f8f631b4 Respect 'tools.core' and 'tools.allowed' settings in non-interactive mode, for both agent execution and custom command 2026-01-05 19:28:52 +08:00
skyfire
a4eb3adea8 for pom 2026-01-05 19:22:50 +08:00
skyfire
7dc7c6380d for pom 2026-01-05 18:14:40 +08:00
skyfire
d2d2b845c5 for README.md 2026-01-05 18:12:48 +08:00
skyfire
96080f84a6 for README.md 2026-01-05 18:00:38 +08:00
skyfire
2b6218e564 for README.md 2026-01-05 17:49:43 +08:00
skyfire
24edf32da8 for README.md 2026-01-05 17:46:18 +08:00
skyfire
51b08f700c for examples 2026-01-05 17:44:07 +08:00
tanzhenxin
58eac7f595 Merge pull request #1397 from liqiongyu/fix/1304-disable-update-nag
fix(cli): skip update check when disableUpdateNag is true
2026-01-05 14:19:13 +08:00
skyfire
32e8b01cf0 for javadoc 2026-01-04 19:39:00 +08:00
skyfire
db9d5cb45d add javadoc 2026-01-04 18:07:56 +08:00
liqoingyu
473cb7b951 fix(cli): skip update check when disableUpdateNag is true 2026-01-04 14:32:38 +08:00
Weaxs
e5cced8813 buildRequest add thinking config && convert Handle reasoning content 2026-01-02 18:59:23 +08:00
skyfire
73848d3867 fix arg 2026-01-01 01:30:58 +08:00
skyfire
6a62167f79 for README.md 2025-12-31 23:36:17 +08:00
skyfire
6ff437671e for README.md 2025-12-31 23:26:20 +08:00
skyfire
30f9e9c782 for README.md 2025-12-31 22:57:20 +08:00
skyfire
e4caa7a856 for partial message processing and event timeout processing 2025-12-31 20:15:51 +08:00
LaZzyMan
aaa66b3172 fix: add tool result and deny warning in text mode 2025-12-31 17:38:33 +08:00
Alexander Farber
0ae59b900c Add German umlauts 2025-12-30 16:50:23 +01:00
Alexander Farber
5a5dae1987 Add German language support and remove a misleading witty phrase 2025-12-30 16:35:34 +01:00
skyfire
ac7ba95d65 add permission 2025-12-30 20:08:05 +08:00
LaZzyMan
15912892f2 fix: missing error throw in non-Interactive mode 2025-12-30 19:40:24 +08:00
cris
e3c20b03bd reslove blank 2025-12-30 16:03:11 +08:00
cris
4db50d4158 fix resume unwork on windows 2025-12-30 16:00:55 +08:00
skyfire
4154493640 message and session use 2025-12-29 21:44:02 +08:00
Mingholy
105ad743fa Merge pull request #1284 from tt-a1i/fix/boolean-string-coercion
fix(core): coerce string boolean values in schema validation
2025-12-29 18:27:36 +08:00
mingholy.lmh
ac3f7cb8c8 fix: ts erros in test file 2025-12-29 17:20:25 +08:00
LaZzyMan
61aad5a162 fix: missing whitespaces for stream-json/json output format via GLM 4.7 model 2025-12-29 16:59:09 +08:00
xuewenjie
98c043bf50 test: update tests for detached process changes 2025-12-29 11:37:54 +08:00
顾盼
e27e9a5f18 Merge pull request #1288 from Weaxs/main
support merge ChatCompletionContentPart && add filterEmptyMessages
2025-12-29 10:50:30 +08:00
pomelo
2578d8c151 Merge pull request #1360 from IceyLiu/icey-feat
docs: add AionUi to ecosystem section
2025-12-29 10:12:53 +08:00
cris
f610133660 improve ad hoc method for windows background terminal task 2025-12-28 22:14:16 +08:00
VeryLiu-lab
a877fedc52 docs: add AionUi to ecosystem section
Add AionUi as a graphical interface option for Qwen Code users.
AionUi provides a modern GUI for command-line AI tools including
Qwen Code, offering an alternative to the terminal interface.
2025-12-28 21:56:59 +08:00
pomelo
2bc8079519 Merge pull request #1332 from QwenLM/fix-language
Fix multi-language and documentation related issues.
2025-12-26 23:02:37 +08:00
pomelo-nwu
25dbe98e6e fix(cli): prevent HTML comment escape by sanitizing --!> and --> 2025-12-26 22:45:35 +08:00
pomelo-nwu
e5dbd69899 feat: fix ci 2025-12-26 22:38:44 +08:00
Mingholy
17eb20c134 Merge pull request #1322 from QwenLM/mingholy/feat/headless-slash-commands
feat: support /compress and /summary commands for non-interactive & ACP
2025-12-26 18:10:55 +08:00
mingholy.lmh
5d59ceb6f3 fix: explicit output if command is not supported 2025-12-26 17:55:03 +08:00
mingholy.lmh
7f645b9726 fix: wrong slash_command in systemMessage 2025-12-26 17:55:03 +08:00
mingholy.lmh
8c109be48c refactor: unified allow list of supported commands in ACP or non-interactive mode 2025-12-26 17:55:03 +08:00
mingholy.lmh
e9a1d9a927 fix: failed unit test cases 2025-12-26 17:55:02 +08:00
mingholy.lmh
8aceddffa2 feat: support /compress and /summary commands for non-interactive & ACP
integration
2025-12-26 17:55:02 +08:00
tanzhenxin
cebe0448d0 Merge pull request #1327 from QwenLM/feat/vscode-ide-companion-context-left
context left on vscode ide companion
2025-12-26 17:26:35 +08:00
LaZzyMan
fe7ff5b148 feat: stable-acp-flag 2025-12-26 17:09:16 +08:00
tanzhenxin
919560e3a4 Merge pull request #1345 from QwenLM/feat/vscode-ida-companion-bash-toolcall-click-2
feat(vscode-ide-companion): in/output part in the bash toolcall can be clicked to open a temporary file
2025-12-26 16:55:35 +08:00
Mingholy
26bd4f882d Merge pull request #1334 from QwenLM/mingholy/chore/skip-bumping-unstable-sdk-version
chore: improve release-sdk workflow
2025-12-26 16:21:53 +08:00
yiliang114
4681f15941 wip 2025-12-26 15:31:07 +08:00
yiliang114
0a04a4bef2 Merge branch 'main' of https://github.com/QwenLM/qwen-code into test/chrome-extension 2025-12-26 11:35:29 +08:00
tanzhenxin
3787e95572 Merge pull request #1349 from QwenLM/fix/integration-test-3
fix one flaky integration test
2025-12-26 09:43:02 +08:00
tanzhenxin
7233d37bd1 fix one flaky integration test 2025-12-26 09:20:24 +08:00
yiliang114
93dcca5147 fix(vscode-ide-companion): fix test 2025-12-26 00:28:45 +08:00
cwtuan
f7d04323f3 Enhance VS Code extension description with download link (#1341)
Updated the VS Code extension note with a download link for the Qwen Code Companion.
2025-12-25 23:58:52 +08:00
yiliang114
9a27857f10 feat(vscode-ide-companion): support context left 2025-12-25 23:53:55 +08:00
yiliang114
452f4f3c0e Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/vscode-ide-companion-context-left 2025-12-25 23:51:57 +08:00
yiliang114
5cc01e5e09 feat(vscode-ide-companion): support context left 2025-12-25 23:51:50 +08:00
yiliang114
ac0be9fb84 feat(vscode-ide-companion): in/output part in the bash toolcall can be clicked to open a temporary file. 2025-12-25 16:59:32 +08:00
xuewenjie
5417de4219 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2025-12-25 16:44:30 +08:00
tanzhenxin
257c6705e1 Merge pull request #1343 from QwenLM/fix/integration-test-2
fix one flaky integration test
2025-12-25 16:08:54 +08:00
tanzhenxin
27e7438b75 fix one flaky integration test 2025-12-25 16:08:06 +08:00
tanzhenxin
8a3ff8db12 Merge pull request #1340 from QwenLM/feat/anthropic-provider-1
Follow up on pr #1331
2025-12-25 15:44:52 +08:00
tanzhenxin
26f8b67d4f add missing file 2025-12-25 15:24:56 +08:00
tanzhenxin
b64d636280 anthropic provider support follow-up 2025-12-25 15:24:42 +08:00
tanzhenxin
781c57b438 Merge pull request #1331 from QwenLM/feat/support-anthropic-provider
feat: add Anthropic provider, normalize auth/env config, and centralize logging
2025-12-25 11:44:38 +08:00
mingholy.lmh
c81c24d45d chore: improve release-sdk workflow 2025-12-25 10:46:57 +08:00
tanzhenxin
c53bdde747 support reasoning.budget_tokens config option 2025-12-25 10:18:38 +08:00
tanzhenxin
99db18069d add interleaved-thinking-2025-05-14 beta header for anthropic content generator 2025-12-25 09:42:06 +08:00
skyfire
422998d7f0 add ProcessTransport unitTest and fix bug 2025-12-24 21:20:47 +08:00
tanzhenxin
a0a5b831d4 add a few more tests 2025-12-24 20:54:40 +08:00
skyfire
68628bf952 add ProcessTransport 2025-12-24 20:45:17 +08:00
tanzhenxin
8f74dd224c add tests for loggingContentGenerator 2025-12-24 19:41:46 +08:00
tanzhenxin
b931d28f35 feat(core,cli): add Anthropic provider, normalize auth/env config, and centralize logging 2025-12-24 19:00:56 +08:00
mingholy.lmh
4407597794 chore: skip bumping sdk version when release nightly/preview or dry run 2025-12-24 18:12:23 +08:00
pomelo-nwu
101bd5f9b3 i18n: fix missing translations for /clear command 2025-12-24 17:28:49 +08:00
pomelo-nwu
61c626b618 fix: check-i18n script 2025-12-24 17:22:21 +08:00
Mingholy
9f65bd3b39 Merge pull request #1325 from QwenLM/mingholy/chore/revert-sdk-version
chore: revert sdk-typescript version to 0.1.0 and update release workflow
2025-12-24 17:22:02 +08:00
pomelo-nwu
a28278e950 feat: update code 2025-12-24 17:12:27 +08:00
pomelo
2b3830cf83 Merge pull request #1314 from QwenLM/feat/skills
Add experimental Skills feature
2025-12-24 17:01:28 +08:00
yiliang114
90bf101040 chore(vscode-ide-companion): simplify the implementation of context remaining 2025-12-24 14:29:25 +08:00
mingholy.lmh
2b9140940d chore: update release-sdk.yml to sync lockfile 2025-12-24 11:43:46 +08:00
pomelo
a8f7bab544 Merge pull request #1293 from fazilus/feat/russian
feat(i18n): update Russian translation with new strings
2025-12-24 11:38:29 +08:00
mingholy.lmh
4efdea0981 chore: revert sdk-typescript version to 0.1.0 and update release workflow 2025-12-24 11:04:33 +08:00
pomelo
05791d4200 Merge pull request #1302 from afarber/1095-figma-mcp-client-name
fix(mcp): update OAuth client name for Figma MCP server compatibility
2025-12-24 10:41:51 +08:00
Mingholy
add35d2904 Merge pull request #1321 from QwenLM/mingholy/fix/sdk-cli-path
fix: cli path parsing issue in Windows
2025-12-24 10:35:52 +08:00
pomelo-nwu
4ca62ba836 feat: adjust code 2025-12-24 10:26:30 +08:00
yiliang114
660901e1fd Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/vscode-ide-companion-context-left 2025-12-24 10:10:32 +08:00
skyfire
e5efad89e0 Merge branch 'feat/javasdk' of github.com:QwenLM/qwen-code into feat/javasdk 2025-12-24 10:01:28 +08:00
yiliang114
8e64c5acaf feat(vscode-ide-companion): support context left 2025-12-24 01:09:21 +08:00
tanzhenxin
bc2a7efcb3 Merge pull request #1297 from QwenLM/feat/gemini-3-integration
Add Gemini provider, remove legacy Google OAuth, and tune generation …
2025-12-23 22:16:39 +08:00
tanzhenxin
1dfd880e17 reset default topP to 0.95 as claude modes does not allow topP smaller than 0.95 2025-12-23 21:58:28 +08:00
skyfire
e09bb5f5c0 modify junit version to 5 and add org developers 2025-12-23 20:14:11 +08:00
乾离
24d11179d8 modify junit version to 5 and add org developers 2025-12-23 20:04:58 +08:00
mingholy.lmh
4f970c9987 fix: cli path parsing issue in Windows 2025-12-23 19:00:43 +08:00
乾离
2ef8b6f350 ProcessTransport stru init 2025-12-23 17:44:28 +08:00
乾离
5779f7ab1d project initialize 2025-12-23 17:20:12 +08:00
pomelo
398a1044ce Merge pull request #1247 from afarber/1244-language-output-default
feat(i18n): auto-detect LLM output language from system locale
2025-12-23 15:49:06 +08:00
tanzhenxin
251031cfc5 fix a link in skills.md 2025-12-23 15:09:23 +08:00
tanzhenxin
10a0c843c1 fix flaky tests 2025-12-23 14:52:03 +08:00
tanzhenxin
77c257d9d0 fix flaky tests 2025-12-23 14:50:47 +08:00
tanzhenxin
955547d523 minor updates to address review comments 2025-12-23 14:35:41 +08:00
tanzhenxin
3bc862df89 unset temperature, and set topP=0.8 for default provider 2025-12-23 13:56:06 +08:00
tanzhenxin
4311af96eb add docs 2025-12-23 10:53:09 +08:00
tanzhenxin
b49c11e9a2 add experimental-skills flag to enable skills feature 2025-12-23 10:24:57 +08:00
pomelo
642dda0315 Merge pull request #1312 from QwenLM/docs-1222
docs(readme): clarify value props, usage modes
2025-12-22 23:01:02 +08:00
pomelo-nwu
bbbdeb280d feat: add Português link 2025-12-22 23:00:09 +08:00
pomelo-nwu
0d43ddee2a feat: update readme.md 2025-12-22 22:49:51 +08:00
pomelo-nwu
50e03f2dd6 feat: update docs 2025-12-22 21:11:33 +08:00
pomelo-nwu
f440ff2f7f Merge branch 'docs-fix' into docs-1222 2025-12-22 20:58:33 +08:00
pomelo
9a6b0abc37 Merge pull request #286 from bl-ue/patch-1
feat: add a link to Gemini CLI Desktop for Qwen Code users who prefer desktop UIs
2025-12-22 20:56:29 +08:00
yiliang114
2b68cc8b74 chore(chrome-qwen-bridge): wip use chat ui 2025-12-22 19:12:36 +08:00
tanzhenxin
9cdd85c62a Merge branch 'main' into feat/skills 2025-12-22 16:00:57 +08:00
tanzhenxin
00547ba439 Merge pull request #1311 from QwenLM/fix/e2e
fix e2e workflow
2025-12-22 14:54:07 +08:00
tanzhenxin
fc1dac9dc7 update 2025-12-22 14:32:51 +08:00
tanzhenxin
338eb9038d fix e2e workflow 2025-12-22 14:28:36 +08:00
tanzhenxin
87d8d82be7 special handling for summarized thinking 2025-12-22 14:07:23 +08:00
tanzhenxin
e0b9044833 Merge pull request #1310 from QwenLM/fix/process-info-robust-20251222
Improve robustness of getProcessInfo with try-catch and empty output fallback
2025-12-22 14:02:51 +08:00
xuewenjie
f33f43e2f7 feat: improve getProcessInfo robustness with try-catch and empty output fallback 2025-12-22 11:38:38 +08:00
刘伟光
43e0815def feat: 修改链接ide之前的判断逻辑,检测是否安装过ide扩展 2025-12-22 11:22:51 +08:00
刘伟光
0c14f4ce08 Merge branch 'main' into fix-sandbox-ideInstall 2025-12-22 11:00:01 +08:00
tanzhenxin
fefc138485 Merge branch 'main' into feat/gemini-3-integration 2025-12-22 10:08:15 +08:00
tanzhenxin
4e7929850c Merge pull request #1309 from QwenLM/chore/v0.6.0
pump version to 0.6.0
2025-12-22 09:58:20 +08:00
tanzhenxin
9cc5c3ed8f pump version to 0.6.0 2025-12-22 09:35:30 +08:00
yiliang114
43c703a79d chore(chrome-qwen-bridge): wip use chat ui 2025-12-22 08:22:19 +08:00
yiliang114
84e190fd9d chore(chrome-qwen-bridge): connect & them 2025-12-20 18:51:56 +08:00
yiliang114
cc3cfb5d65 chore(chrome-qwen-bridge): connect & them 2025-12-20 18:51:49 +08:00
Alexander Farber
f07259a7c9 Add German UI language support and normalize locale codes for LLM output 2025-12-20 10:21:16 +01:00
Alexander Farber
4d9f25e9fe Auto-detect LLM output language from system locale on first startup 2025-12-20 10:21:16 +01:00
Alexander Farber
18e9b2340b Change header to: Copyright 2025 Qwen Team 2025-12-20 09:37:57 +01:00
Alexander Farber
ad427da340 Move constants to a new file for SSOT 2025-12-20 09:35:12 +01:00
Alexander Farber
484e0fd943 Change the client name to: Gemini CLI MCP Client 2025-12-20 09:21:15 +01:00
yiliang114
a1f893f0c6 chore(chrome-qwen-bridge): connect 2025-12-20 15:54:20 +08:00
yiliang114
a60c5c6697 feat(chrome-qwen-bridge): 🔥 init chrome qwen code bridge 2025-12-20 00:58:41 +08:00
tanzhenxin
a92be72e88 Merge pull request #1257 from QwenLM/feat/ide-companion-discovery
IDE companion discovery: switch to ~/.qwen/ide lock files
2025-12-19 18:29:11 +08:00
tanzhenxin
52cd1da4a7 update documentation 2025-12-19 18:16:59 +08:00
tanzhenxin
c5c556a326 remove pid from lockfile name of ide connection file 2025-12-19 18:12:04 +08:00
tanzhenxin
a8a863581b Merge branch 'main' into feat/ide-companion-discovery 2025-12-19 16:49:24 +08:00
tanzhenxin
e4468cfcbc Merge pull request #1231 from QwenLM/fix/windows-startup-and-exit-hangs
fix: optimize windows process tree retrieval to prevent hang
2025-12-19 16:48:40 +08:00
tanzhenxin
3bf30ead67 Merge pull request #1262 from QwenLM/chore/vscode-ide-companion-update-vscode-engine
chore(vscode-ide-companion): update vscode engine version from ^1.99.0 to ^1.85.0
2025-12-19 16:47:33 +08:00
Mingholy
a786f61e49 Merge pull request #1265 from QwenLM/mingholy/chore/bundle-cli-into-sdk
Bundle CLI into SDK package and separate CLI & SDK E2E tests
2025-12-19 16:45:35 +08:00
tanzhenxin
b8a16d362a Merge branch 'main' into feat/gemini-3-integration 2025-12-19 16:39:42 +08:00
tanzhenxin
17129024f4 Add Gemini provider, remove legacy Google OAuth, and tune generation defaults 2025-12-19 16:26:54 +08:00
mingholy.lmh
fa7d857945 fix: remove unused cli finding code 2025-12-19 16:18:22 +08:00
mingholy.lmh
90489933fd fix: lint issues 2025-12-19 15:52:11 +08:00
mingholy.lmh
3354b56a05 docs(sdk): update sdk docs 2025-12-19 15:47:11 +08:00
mingholy.lmh
d40447cee4 fix: failed test cases 2025-12-19 15:44:04 +08:00
mingholy.lmh
ba87cf63f6 chore: build and bundle CLI for SDK release 2025-12-19 15:44:04 +08:00
mingholy.lmh
00a8c6a924 chore: separate CLI and SDK integration test 2025-12-19 15:44:04 +08:00
mingholy.lmh
156134d3d4 chore(sdk): bundle CLI into SDK package and inherit the dependencies 2025-12-19 15:44:04 +08:00
tanzhenxin
b720209888 Merge pull request #1261 from QwenLM/fix/vscode-ide-companion-opt-task-stop
fix(vscode-ide-companion): Optimize stream termination handling and fix style layering issues
2025-12-19 15:15:04 +08:00
tanzhenxin
dfe667c364 Merge pull request #1269 from QwenLM/mingholy/fix/sampling-params
fix: default values of sampling params
2025-12-19 15:14:26 +08:00
xuewenjie
1386fba278 Revert other files to main, keep only process-utils changes 2025-12-19 15:01:26 +08:00
xuewenjie
d942250905 test: sync test files with code changes for IDE detection
- Update detect-ide.test.ts to remove ideProcessInfo parameter (now optional)
- Update process-utils.test.ts to match simplified Windows process detection logic
- Remove tests for removed IDE detection strategies (Strategy 1-4)
- All tests now passing (13 tests in detect-ide.test.ts, 6 tests in process-utils.test.ts)
2025-12-19 13:24:19 +08:00
xuewenjie
ec32a24508 fix: update ide-client tests to match new config file naming scheme
- Update config file naming from qwen-code-ide-server-{pid}-{timestamp}.json to qwen-code-ide-server-{port}.json
- Add readdir mock to return config file list
- Add validateWorkspacePath mock for workspace validation
- Add workspacePath field to all config objects in tests
- Remove getIdeProcessInfo dependency from tests
- All 23 tests now passing
2025-12-19 11:36:05 +08:00
刘伟光
34d8dbf9b2 feat: 兼容宿主机在不同ide上的instal提示 2025-12-19 11:07:33 +08:00
刘伟光
b3b2bc6ad5 feat: 兼容宿主机在不同ide上的instal提示 2025-12-19 10:39:05 +08:00
joeytoday
80bb2890df docs: Enhanced Video Playback Guidance 2025-12-19 10:33:20 +08:00
joeytoday
abd9ee2a7b docs: updated quick start video 2025-12-19 10:28:07 +08:00
joeytoday
b8df689e31 docs: rewrite README#Use-examples, add 4 methods to start qwen code 2025-12-19 10:22:17 +08:00
Fazil
15efeb0107 feat(i18n): update Russian translation with new strings 2025-12-18 15:14:08 +03:00
xuewenjie
c2b59038ae fix: escape backslashes in PowerShell command strings (CodeQL security fix)
Fixes CodeQL security alert: Incomplete string escaping or encoding

- Add escapeForPowerShellDoubleQuotes() helper function
- Properly escape both backslashes and double quotes in correct order
- Prevents command injection vulnerabilities in Windows process detection
- All existing tests pass
2025-12-18 17:32:11 +08:00
xuewenjie
27bf42b4f5 test: sync process-utils.test.ts with implementation logic
- Update Windows test cases to match multi-strategy IDE detection
- Add test for Strategy 1: known IDE process detection (code.exe)
- Add test for Strategy 3: shell parent detection (cmd.exe)
- Add test for Strategy 2: Git Bash with missing parent handling
- Fix test expectations to align with actual implementation behavior
- All 7 test cases now pass successfully
2025-12-18 17:28:56 +08:00
joeytoday
e610578ecc docs: updated README, deleted session management and check inline links, shorter why 2025-12-18 16:51:45 +08:00
tanzhenxin
d07ae35c90 Merge pull request #1286 from afarber/1220-resume-alias
fix(cli): add -r and -C aliases for --resume and --continue options
2025-12-18 16:32:36 +08:00
xuewenjie
cb59b5a9dc refactor(core): optimize Windows process detection and remove debug logging
- Replace execFileAsync with execAsync for complex PowerShell commands in getProcessInfo
- Remove unnecessary getProcessInfo retry logic when parent not in processMap
- Remove all debug logging code (writeDebugLog function and fs import)
- Improve performance by ~1.6-2.6 seconds per detection
- Keep execFileAsync for simple commands in getProcessTableWindows
2025-12-18 16:24:40 +08:00
joeytoday
235159216e docs: updated REA 2025-12-18 15:11:31 +08:00
joeytoday
93b30cca29 docs: restructured the README and added new content, including screenshots of the startup page and a quick-start video. 2025-12-18 15:06:47 +08:00
xuewenjie
01e62a2120 refactor: remove unused fs import from process-utils.ts 2025-12-18 15:06:01 +08:00
Alexander Farber
d464f61b72 Change -C to -c 2025-12-18 07:36:04 +01:00
Alexander Farber
f866f7f071 Add -r and -C aliases for --resume and --continue options 2025-12-18 07:36:04 +01:00
xuewenjie
7eabf543b4 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-startup-and-exit-hangs 2025-12-18 13:24:21 +08:00
Weaxs
d2bc46cbb4 remove filterEmptyMessages 2025-12-18 00:55:47 +08:00
Weaxs
84eb5c562f support merge ChatCompletionContentPart && add filterEmptyMessages 2025-12-18 00:46:48 +08:00
Alexander Farber
8106a6b0f4 Handle PAT tokens and credentials in git remote URL parsing (#1225) 2025-12-18 00:44:46 +08:00
pomelo
c0839dceac Merge pull request #1266 from QwenLM/docs-fix
docs:Fix the errors in the document
2025-12-17 22:04:27 +08:00
yiliang114
12f84fb730 fix(vscode-ide-companion): optimize stream termination handling and remove timeout for session_prompt 2025-12-17 21:00:26 +08:00
joeytoday
f9a1ee2442 docs: updated vscode showcase video 2025-12-17 16:47:37 +08:00
Tu Shaokun
7b01b26ff5 perf(core): avoid recompiling schema on retry 2025-12-17 16:27:42 +08:00
Tu Shaokun
0f3e97ea1c fix(core): coerce string boolean values in schema validation
Self-hosted LLMs sometimes return "true"/"false" strings instead of
actual boolean values for tool parameters like `is_background`. This
causes schema validation to fail with type errors.

Fixes #1267
2025-12-17 16:14:02 +08:00
joeytoday
f824004f99 docs: updated links in index.md 2025-12-17 15:03:23 +08:00
刘伟光
6ca54beba2 feat: Optimize the issue where an error message indicating unfriendliness occurs after executing the ideinstall command in the sandbox environment 2025-12-17 13:38:38 +08:00
Mingholy
e274b4469a Merge pull request #1214 from kfxmvp/fix/issue-1186-schema-converter
fix: add configurable OpenAPI 3.0 schema compliance for Gemini compatibility (#1186)
2025-12-17 11:12:57 +08:00
joeytoday
a4e3d764d3 docs: updated all links, click and open in vscode, new showcase video in overview 2025-12-17 11:10:31 +08:00
tanzhenxin
0a39c91264 Merge pull request #1275 from QwenLM/fix/integration-test
remove one flaky integration test
2025-12-17 10:06:28 +08:00
tanzhenxin
8fd7490d8f remove one flaky integration test 2025-12-17 09:27:25 +08:00
tanzhenxin
4f1766e19a Merge pull request #1239 from afarber/1179-add-resume-cmd
feat(ui): add /resume slash command to switch between sessions
2025-12-16 20:52:35 +08:00
tanzhenxin
bf52c6db0f fix review comments 2025-12-16 20:36:24 +08:00
tanzhenxin
9267677d38 fix failed test 2025-12-16 20:08:43 +08:00
tanzhenxin
fb8412a96a code refactor 2025-12-16 20:03:49 +08:00
tanzhenxin
2837aa6b7c rework /resume slash command 2025-12-16 19:54:55 +08:00
yiliang114
49b3e0dc92 chore(vscode-ide-companion): update vscode engine version from ^1.99.0 to ^1.85.0 2025-12-16 19:54:19 +08:00
mingholy.lmh
25d9c4f1a7 fix: default values of sampling params 2025-12-16 17:09:42 +08:00
joeytoday
d1a6b3207e docs: updated inline links 2025-12-16 17:01:47 +08:00
pomelo-nwu
1c62499977 feat: fix link 2025-12-16 15:40:01 +08:00
pomelo-nwu
4b8b4e2fe8 feat: update docs 2025-12-16 15:32:21 +08:00
tanzhenxin
9942b2b877 Merge branch 'main' into 1179-add-resume-cmd 2025-12-16 15:29:58 +08:00
tanzhenxin
850c52dc79 Merge pull request #1228 from afarber/add-git-co-author
feat: expose gitCoAuthor setting in settings.json and document it
2025-12-16 15:17:02 +08:00
tanzhenxin
61e378644e feat: update configuration and shell tool implementations
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-12-16 15:02:59 +08:00
tanzhenxin
bd3bdd82ea Merge pull request #1205 from afarber/rename-leftover-gemini-references
fix(ide): rename Gemini references to Qwen and fix IDE connection path
2025-12-16 14:46:14 +08:00
tanzhenxin
fc58291c5c Merge branch 'main' into add-git-co-author 2025-12-16 14:43:27 +08:00
tanzhenxin
633148b257 Revert IDE client discovery path changes 2025-12-16 14:30:25 +08:00
tanzhenxin
f9da1b819e Merge branch 'main' into feat/ide-companion-discovery 2025-12-16 14:11:25 +08:00
pomelo-nwu
36fb6b8291 feat: update docs 2025-12-16 13:48:10 +08:00
xuewenjie
f47c762620 Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-startup-and-exit-hangs 2025-12-16 11:35:13 +08:00
kefuxin
573c33f68a Merge remote-tracking branch 'upstream/main' into fix/issue-1186-schema-converter 2025-12-16 11:08:51 +08:00
xuewenjie
8673426d5c fix(core): use current chunk for shell output update instead of cumulative 2025-12-16 10:26:20 +08:00
pomelo
8130020277 Merge pull request #1260 from QwenLM/docs-byYijing
Docs: restructure docs to follow the Claude Code organization
2025-12-16 10:16:57 +08:00
yiliang114
32c085cf7d chore(vscode-ide-companion): update vscode engine version from ^1.99.0 to ^1.85.0 2025-12-15 23:54:26 +08:00
yiliang114
725843f9b3 fix(vscode-ide-companion): optimize stream termination handling and fix style layering issues 2025-12-15 23:41:36 +08:00
Alexander Farber
07fb6faf5f Add comments explaining regexes 2025-12-15 16:26:52 +01:00
Alexander Farber
1956507d90 Avoid ReDoS by using better regexes 2025-12-15 16:23:17 +01:00
yiliang114
54fd63f04b fix(vscode-ide-companion): optimize stream termination handling and fix style layering issues
Unify stream termination using the `sendStreamEnd` method to avoid duplicate code.
Add stream termination reason identification and handling for timeout and session expiration scenarios.
Centralize cleanup logic for various stream termination states in the WebView message hooks.
Adjust Tailwind CSS styles to resolve component layering display issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 22:41:38 +08:00
tanzhenxin
52faa0da5c Merge pull request #1259 from QwenLM/chore/v0.5.1
pump version to 0.5.1
2025-12-15 21:30:38 +08:00
tanzhenxin
87f1dd9061 pump version to 0.5.1 2025-12-15 20:48:02 +08:00
tanzhenxin
59c3d3d0f9 IDE companion discovery: switch to ~/.qwen/ide lock files 2025-12-15 20:15:26 +08:00
Alexander Farber
5d94763581 Add logs (TODO remove later) 2025-12-15 11:06:09 +01:00
Alexander Farber
5bd1822b7d Fix gitCoAuthor not added for combined flags like -am 2025-12-15 11:00:21 +01:00
Alexander Farber
65392a057d Detect git commit anywhere in command, not just at start 2025-12-15 10:19:08 +01:00
Alexander Farber
3b9d38a325 Expose gitCoAuthor setting in settings.json and document it 2025-12-15 10:19:08 +01:00
tanzhenxin
177fc42f04 Merge branch 'main' into feat/skills 2025-12-15 14:25:56 +08:00
Alexander Farber
4930a24d07 Polish the PR, minor improvements 2025-12-13 14:35:40 +01:00
Alexander Farber
7a97fcd5f1 Add tests for /resume command and update SettingsDialog snapshots 2025-12-13 14:03:35 +01:00
Alexander Farber
4504c7a0ac Rename ResumeSessionPicker to StandaloneSessionPicker and add documentation 2025-12-13 13:33:44 +01:00
Alexander Farber
56a62bcb2a Fix input focus issue by using useKeypress instead of useInput for ResumeSessionDialog 2025-12-13 13:08:07 +01:00
Alexander Farber
1098c23b26 Close dialog before async operations to prevent input capture 2025-12-13 13:08:07 +01:00
Alexander Farber
e76f47512c Add guards 2025-12-13 13:08:07 +01:00
Alexander Farber
f5c868702b Put shared code in new files 2025-12-13 13:08:07 +01:00
Alexander Farber
0d40cf2213 Refactor /resume command to use dialog instead of stand
alone Ink app
2025-12-13 13:08:07 +01:00
Alexander Farber
12877ac849 Refactor /resume command to use dialog instead of standalone Ink app 2025-12-13 13:08:07 +01:00
Alexander Farber
2de50ae436 Add tests 2025-12-13 13:08:07 +01:00
Alexander Farber
a761be80a5 Filter out empty sessions 2025-12-13 13:08:07 +01:00
Alexander Farber
6c77303172 Add /resume slash command to switch between previous sessions 2025-12-13 13:08:06 +01:00
xuewenjie
b272ac0119 Fix: Make cleanup strategy dynamic to support testing mocks 2025-12-12 17:47:03 +08:00
xuewenjie
574d89da14 Refactor ShellExecutionService cleanup to use strategy pattern 2025-12-12 17:03:04 +08:00
xuewenjie
4f2b2d0a3e fix: optimize windows process tree retrieval to prevent hang 2025-12-12 15:10:16 +08:00
kefuxin
44794121a8 docs: update MCP server schema compliance documentation
Update documentation to reflect the new `schemaCompliance` setting and detailed OpenAPI 3.0 transformation rules.

Suggested-by: afarber
2025-12-12 10:38:00 +08:00
Alexander Farber
bf905dcc17 Rename GEMINI_CLI_NO_RELAUNCH to QWEN_CODE_NO_RELAUNCH 2025-12-11 11:14:12 +01:00
Alexander Farber
95d3e5b744 Rename more references 2025-12-11 11:14:11 +01:00
Alexander Farber
6d3cf4cd98 Rrename Gemini references to Qwen and fix IDE connection path 2025-12-11 11:14:10 +01:00
Alexander Farber
68295d0bbf Rename leftover Gemini references to Qwen in UI strings 2025-12-11 11:14:09 +01:00
tanzhenxin
2560c2d1a2 Merge branch 'main' into feat/skills 2025-12-11 14:50:07 +08:00
kefuxin
84cccfe99a feat: add i18n for schemaCompliance setting 2025-12-11 14:30:38 +08:00
kefuxin
b6a3ab11e0 fix: improve Gemini compatibility by adding configurable schema converter
This commit addresses issue #1186 by introducing a configurable schema compliance
mechanism for tool definitions sent to LLMs.

Key changes:
1.  **New Configuration**: Added `model.generationConfig.schemaCompliance` setting (defaults to 'auto', optional 'openapi_30').
2.  **Schema Converter**: Implemented `toOpenAPI30` converter in `packages/core` to strictly downgrade modern JSON Schema to OpenAPI 3.0.3 (required for Gemini API), handling:
    -   Nullable types (`["string", "null"]` -> `nullable: true`)
    -   Numeric exclusive limits
    -   Const to Enum conversion
    -   Removal of tuples and invalid keywords (``, `dependencies`, etc.)
3.  **Tests**: Added comprehensive unit tests for the schema converter and updated pipeline tests.

Fixes #1186
2025-12-11 14:23:27 +08:00
tanzhenxin
bd6e16d41b draft version of skill tool feature 2025-12-10 17:18:44 +08:00
xuewenjie
16939c0bc8 Refactor ShellTool: remove ping hack and timeout, optimize cleanup 2025-12-10 13:49:51 +08:00
xuewenjie
6fc09a82fb fix: use && for windows background keep-alive ping and add test 2025-12-09 13:33:42 +08:00
xuewenjie
d622f8d1bf Merge branch 'main' of github.com:QwenLM/qwen-code into fix/windows-background-terminal-execute-x 2025-12-09 11:32:17 +08:00
xuewenjie
28d178b5c1 fix: handle windows background execution errors and add tests 2025-12-09 11:24:30 +08:00
xuewenjie
4c69d536ac test: fix shell tool tests by updating pid expectation and AbortSignal matching 2025-12-05 10:47:06 +08:00
xuewenjie
403fd06117 chore: update .gitignore 2025-12-04 15:55:17 +08:00
xuewenjie
d9928eab66 fix: improve windows background process handling and cleanup 2025-12-04 15:55:11 +08:00
bl-ue
2f0fa267c8 Fix name 2025-11-05 17:06:13 -07:00
bl-ue
fa6ae0a324 Typo 2025-08-19 07:53:09 -06:00
bl-ue
387be44866 Fix link 2025-08-15 11:22:28 -06:00
bl-ue
51b82771da Revert unintentional readme change 2025-08-11 18:25:32 -06:00
bl-ue
629cd14fad Add a link to Gemini Desktop for Qwen Code users who prefer graphical UIs 2025-08-11 11:17:37 -06:00
507 changed files with 57696 additions and 17578 deletions

View File

@@ -18,8 +18,6 @@ jobs:
- 'sandbox:docker'
node-version:
- '20.x'
- '22.x'
- '24.x'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -67,10 +65,13 @@ jobs:
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
KEEP_OUTPUT: 'true'
SANDBOX: '${{ matrix.sandbox }}'
VERBOSE: 'true'
run: |-
npm run "test:integration:${SANDBOX}"
if [[ "${{ matrix.sandbox }}" == "sandbox:docker" ]]; then
npm run test:integration:sandbox:docker
else
npm run test:integration:sandbox:none
fi
e2e-test-macos:
name: 'E2E Test - macOS'

View File

@@ -33,6 +33,10 @@ on:
type: 'boolean'
default: false
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: false
jobs:
release-sdk:
runs-on: 'ubuntu-latest'
@@ -46,6 +50,7 @@ jobs:
packages: 'write'
id-token: 'write'
issues: 'write'
pull-requests: 'write'
outputs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
@@ -86,6 +91,8 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
scope: '@qwen-code'
- name: 'Install Dependencies'
run: |-
@@ -121,6 +128,19 @@ jobs:
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
MANUAL_VERSION: '${{ inputs.version }}'
- name: 'Set SDK package version (local only)'
env:
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
run: |-
# Ensure the package version matches the computed release version.
# This is required for nightly/preview because npm does not allow re-publishing the same version.
npm version -w @qwen-code/sdk "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version
- name: 'Build CLI Bundle'
run: |
npm run build
npm run bundle
- name: 'Run Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
@@ -132,13 +152,6 @@ jobs:
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
- name: 'Build CLI for Integration Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
run: |
npm run build
npm run bundle
- name: 'Run SDK Integration Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
@@ -155,7 +168,21 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: 'Build SDK'
working-directory: 'packages/sdk-typescript'
run: |-
npm run build
- name: 'Publish @qwen-code/sdk'
working-directory: 'packages/sdk-typescript'
run: |-
npm publish --access public --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
env:
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
- name: 'Create and switch to a release branch'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' }}
id: 'release_branch'
env:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
@@ -164,50 +191,22 @@ jobs:
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
- name: 'Update package version'
working-directory: 'packages/sdk-typescript'
env:
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
run: |-
npm version "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version
- name: 'Commit and Conditionally Push package version'
- name: 'Commit and Push package version (stable only)'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' }}
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
git add packages/sdk-typescript/package.json
# Only persist version bumps after a successful publish.
git add packages/sdk-typescript/package.json package-lock.json
if git diff --staged --quiet; then
echo "No version changes to commit"
else
git commit -m "chore(release): sdk-typescript ${RELEASE_TAG}"
fi
if [[ "${IS_DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi
- name: 'Build SDK'
working-directory: 'packages/sdk-typescript'
run: |-
npm run build
- name: 'Configure npm for publishing'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
scope: '@qwen-code'
- name: 'Publish @qwen-code/sdk'
working-directory: 'packages/sdk-typescript'
run: |-
npm publish --access public --tag=${{ steps.version.outputs.NPM_TAG }} ${{ steps.vars.outputs.is_dry_run == 'true' && '--dry-run' || '' }}
env:
NODE_AUTH_TOKEN: '${{ secrets.NPM_TOKEN }}'
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
- name: 'Create GitHub Release and Tag'
if: |-
@@ -217,12 +216,68 @@ jobs:
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
PREVIOUS_RELEASE_TAG: '${{ steps.version.outputs.PREVIOUS_RELEASE_TAG }}'
IS_NIGHTLY: '${{ steps.vars.outputs.is_nightly }}'
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
REF: '${{ github.event.inputs.ref || github.sha }}'
run: |-
# For stable releases, use the release branch; for nightly/preview, use the current ref
if [[ "${IS_NIGHTLY}" == "true" || "${IS_PREVIEW}" == "true" ]]; then
TARGET="${REF}"
PRERELEASE_FLAG="--prerelease"
else
TARGET="${RELEASE_BRANCH}"
PRERELEASE_FLAG=""
fi
gh release create "sdk-typescript-${RELEASE_TAG}" \
--target "$RELEASE_BRANCH" \
--target "${TARGET}" \
--title "SDK TypeScript Release ${RELEASE_TAG}" \
--notes-start-tag "sdk-typescript-${PREVIOUS_RELEASE_TAG}" \
--generate-notes
--generate-notes \
${PRERELEASE_FLAG}
- name: 'Create PR to merge release branch into main'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' }}
id: 'pr'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
set -euo pipefail
pr_url="$(gh pr list --head "${RELEASE_BRANCH}" --base main --json url --jq '.[0].url')"
if [[ -z "${pr_url}" ]]; then
pr_url="$(gh pr create \
--base main \
--head "${RELEASE_BRANCH}" \
--title "chore(release): sdk-typescript ${RELEASE_TAG}" \
--body "Automated release PR for sdk-typescript ${RELEASE_TAG}.")"
fi
echo "PR_URL=${pr_url}" >> "${GITHUB_OUTPUT}"
- name: 'Wait for CI checks to complete'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
PR_URL: '${{ steps.pr.outputs.PR_URL }}'
run: |-
set -euo pipefail
echo "Waiting for CI checks to complete..."
gh pr checks "${PR_URL}" --watch --interval 30
- name: 'Enable auto-merge for release PR'
if: |-
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
PR_URL: '${{ steps.pr.outputs.PR_URL }}'
run: |-
set -euo pipefail
gh pr merge "${PR_URL}" --merge --auto
- name: 'Create Issue on Failure'
if: |-

View File

@@ -133,8 +133,8 @@ jobs:
${{ github.event.inputs.force_skip_tests != 'true' }}
run: |
npm run preflight
npm run test:integration:sandbox:none
npm run test:integration:sandbox:docker
npm run test:integration:cli:sandbox:none
npm run test:integration:cli:sandbox:docker
env:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ package-lock.json
.idea
*.iml
.cursor
.qoder
# OS metadata
.DS_Store

View File

@@ -2,27 +2,6 @@
We would love to accept your patches and contributions to this project.
## Before you begin
### Sign our Contributor License Agreement
Contributions to this project must be accompanied by a
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
You (or your employer) retain the copyright to your contribution; this simply
gives us permission to use and redistribute your contributions as part of the
project.
If you or your current employer have already signed the Google CLA (even if it
was for a different project), you probably don't need to do it again.
Visit <https://cla.developers.google.com/> to see your current agreements or to
sign a new one.
### Review our Community Guidelines
This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).
## Contribution Process
### Code Reviews
@@ -74,12 +53,6 @@ Your PR should have a clear, descriptive title and a detailed description of the
In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`).
## Forking
If you are forking the repository you will be able to run the Build, Test and Integration test workflows. However in order to make the integration tests run you'll need to add a [GitHub Repository Secret](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) with a value of `GEMINI_API_KEY` and set that to a valid API key that you have available. Your key and secret are private to your repo; no one without access can see your key and you cannot see any secrets related to this repo.
Additionally you will need to click on the `Actions` tab and enable workflows for your repository, you'll find it's the large blue button in the center of the screen.
## Development Setup and Workflow
This section guides contributors on how to build, modify, and understand the development setup of this project.
@@ -98,8 +71,8 @@ This section guides contributors on how to build, modify, and understand the dev
To clone the repository:
```bash
git clone https://github.com/google-gemini/gemini-cli.git # Or your fork's URL
cd gemini-cli
git clone https://github.com/QwenLM/qwen-code.git # Or your fork's URL
cd qwen-code
```
To install dependencies defined in `package.json` as well as root dependencies:
@@ -118,9 +91,9 @@ This command typically compiles TypeScript to JavaScript, bundles assets, and pr
### Enabling Sandboxing
[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum, setting `GEMINI_SANDBOX=true` in your `~/.env` and ensuring a sandboxing provider (e.g. `macOS Seatbelt`, `docker`, or `podman`) is available. See [Sandboxing](#sandboxing) for details.
[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum, setting `QWEN_SANDBOX=true` in your `~/.env` and ensuring a sandboxing provider (e.g. `macOS Seatbelt`, `docker`, or `podman`) is available. See [Sandboxing](#sandboxing) for details.
To build both the `gemini` CLI utility and the sandbox container, run `build:all` from the root directory:
To build both the `qwen-code` CLI utility and the sandbox container, run `build:all` from the root directory:
```bash
npm run build:all
@@ -130,13 +103,13 @@ To skip building the sandbox container, you can use `npm run build` instead.
### Running
To start the Gemini CLI from the source code (after building), run the following command from the root directory:
To start the Qwen Code application from the source code (after building), run the following command from the root directory:
```bash
npm start
```
If you'd like to run the source build outside of the gemini-cli folder, you can utilize `npm link path/to/gemini-cli/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) or `alias gemini="node path/to/gemini-cli/packages/cli"` to run with `gemini`
If you'd like to run the source build outside of the qwen-code folder, you can utilize `npm link path/to/qwen-code/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) to run with `qwen-code`
### Running Tests
@@ -154,7 +127,7 @@ This will run tests located in the `packages/core` and `packages/cli` directorie
#### Integration Tests
The integration tests are designed to validate the end-to-end functionality of the Gemini CLI. They are not run as part of the default `npm run test` command.
The integration tests are designed to validate the end-to-end functionality of Qwen Code. They are not run as part of the default `npm run test` command.
To run the integration tests, use the following command:
@@ -209,19 +182,61 @@ npm run lint
### Coding Conventions
- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase.
- Consult [QWEN.md](https://github.com/QwenLM/qwen-code/blob/main/QWEN.md) (typically found in the project root) for specific instructions related to AI-assisted development, including conventions for React, comments, and Git usage.
- **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages.
### Project Structure
- `packages/`: Contains the individual sub-packages of the project.
- `cli/`: The command-line interface.
- `core/`: The core backend logic for the Gemini CLI.
- `core/`: The core backend logic for Qwen Code.
- `docs/`: Contains all project documentation.
- `scripts/`: Utility scripts for building, testing, and development tasks.
For more detailed architecture, see `docs/architecture.md`.
## Documentation Development
This section describes how to develop and preview the documentation locally.
### Prerequisites
1. Ensure you have Node.js (version 18+) installed
2. Have npm or yarn available
### Setup Documentation Site Locally
To work on the documentation and preview changes locally:
1. Navigate to the `docs-site` directory:
```bash
cd docs-site
```
2. Install dependencies:
```bash
npm install
```
3. Link the documentation content from the main `docs` directory:
```bash
npm run link
```
This creates a symbolic link from `../docs` to `content` in the docs-site project, allowing the documentation content to be served by the Next.js site.
4. Start the development server:
```bash
npm run dev
```
5. Open [http://localhost:3000](http://localhost:3000) in your browser to see the documentation site with live updates as you make changes.
Any changes made to the documentation files in the main `docs` directory will be reflected immediately in the documentation site.
## Debugging
### VS Code:
@@ -231,7 +246,7 @@ For more detailed architecture, see `docs/architecture.md`.
```bash
npm run debug
```
This command runs `node --inspect-brk dist/gemini.js` within the `packages/cli` directory, pausing execution until a debugger attaches. You can then open `chrome://inspect` in your Chrome browser to connect to the debugger.
This command runs `node --inspect-brk dist/index.js` within the `packages/cli` directory, pausing execution until a debugger attaches. You can then open `chrome://inspect` in your Chrome browser to connect to the debugger.
2. In VS Code, use the "Attach" launch configuration (found in `.vscode/launch.json`).
Alternatively, you can use the "Launch Program" configuration in VS Code if you prefer to launch the currently open file directly, but 'F5' is generally recommended.
@@ -239,16 +254,16 @@ Alternatively, you can use the "Launch Program" configuration in VS Code if you
To hit a breakpoint inside the sandbox container run:
```bash
DEBUG=1 gemini
DEBUG=1 qwen-code
```
**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect gemini-cli due to automatic exclusion. Use `.gemini/.env` files for gemini-cli specific debug settings.
**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect qwen-code due to automatic exclusion. Use `.qwen-code/.env` files for qwen-code specific debug settings.
### React DevTools
To debug the CLI's React-based UI, you can use React DevTools. Ink, the library used for the CLI's interface, is compatible with React DevTools version 4.x.
1. **Start the Gemini CLI in development mode:**
1. **Start the Qwen Code application in development mode:**
```bash
DEV=true npm start
@@ -270,23 +285,10 @@ To debug the CLI's React-based UI, you can use React DevTools. Ink, the library
```
Your running CLI application should then connect to React DevTools.
![](/docs/assets/connected_devtools.png)
## Sandboxing
### macOS Seatbelt
On macOS, `qwen` uses Seatbelt (`sandbox-exec`) under a `permissive-open` profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that restricts writes to the project folder but otherwise allows all other operations and outbound network traffic ("open") by default. You can switch to a `restrictive-closed` profile (see `packages/cli/src/utils/sandbox-macos-restrictive-closed.sb`) that declines all operations and outbound network traffic ("closed") by default by setting `SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file. Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}` (see below for proxied networking). You can also switch to a custom profile `SEATBELT_PROFILE=<profile>` if you also create a file `.qwen/sandbox-macos-<profile>.sb` under your project settings directory `.qwen`.
### Container-based Sandboxing (All Platforms)
For stronger container-based sandboxing on macOS or other platforms, you can set `GEMINI_SANDBOX=true|docker|podman|<command>` in your environment or `.env` file. The specified command (or if `true` then either `docker` or `podman`) must be installed on the host machine. Once enabled, `npm run build:all` will build a minimal container ("sandbox") image and `npm start` will launch inside a fresh instance of that container. The first build can take 20-30s (mostly due to downloading of the base image) but after that both build and start overhead should be minimal. Default builds (`npm run build`) will not rebuild the sandbox.
Container-based sandboxing mounts the project directory (and system temp directory) with read-write access and is started/stopped/removed automatically as you start/stop Gemini CLI. Files created within the sandbox should be automatically mapped to your user/group on host machine. You can easily specify additional mounts, ports, or environment variables by setting `SANDBOX_{MOUNTS,PORTS,ENV}` as needed. You can also fully customize the sandbox for your projects by creating the files `.qwen/sandbox.Dockerfile` and/or `.qwen/sandbox.bashrc` under your project settings directory (`.qwen`) and running `qwen` with `BUILD_SANDBOX=1` to trigger building of your custom sandbox.
#### Proxied Networking
All sandboxing methods, including macOS Seatbelt using `*-proxied` profiles, support restricting outbound network traffic through a custom proxy server that can be specified as `GEMINI_SANDBOX_PROXY_COMMAND=<command>`, where `<command>` must start a proxy server that listens on `:::8877` for relevant requests. See `docs/examples/proxy-script.md` for a minimal proxy that only allows `HTTPS` connections to `example.com:443` (e.g. `curl https://example.com`) and declines all other requests. The proxy is started and stopped automatically alongside the sandbox.
> TBD
## Manual Publish

View File

@@ -1,9 +1,9 @@
# Makefile for gemini-cli
# Makefile for qwen-code
.PHONY: help install build build-sandbox build-all test lint format preflight clean start debug release run-npx create-alias
help:
@echo "Makefile for gemini-cli"
@echo "Makefile for qwen-code"
@echo ""
@echo "Usage:"
@echo " make install - Install npm dependencies"
@@ -14,11 +14,11 @@ help:
@echo " make format - Format the code"
@echo " make preflight - Run formatting, linting, and tests"
@echo " make clean - Remove generated files"
@echo " make start - Start the Gemini CLI"
@echo " make debug - Start the Gemini CLI in debug mode"
@echo " make start - Start the Qwen Code CLI"
@echo " make debug - Start the Qwen Code CLI in debug mode"
@echo ""
@echo " make run-npx - Run the CLI using npx (for testing the published package)"
@echo " make create-alias - Create a 'gemini' alias for your shell"
@echo " make create-alias - Create a 'qwen' alias for your shell"
install:
npm install

411
README.md
View File

@@ -1,382 +1,152 @@
# Qwen Code
<div align="center">
![Qwen Code Screenshot](./docs/assets/qwen-screenshot.png)
[![npm version](https://img.shields.io/npm/v/@qwen-code/qwen-code.svg)](https://www.npmjs.com/package/@qwen-code/qwen-code)
[![License](https://img.shields.io/github/license/QwenLM/qwen-code.svg)](./LICENSE)
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
[![Downloads](https://img.shields.io/npm/dm/@qwen-code/qwen-code.svg)](https://www.npmjs.com/package/@qwen-code/qwen-code)
**AI-powered command-line workflow tool for developers**
**An open-source AI agent that lives in your terminal.**
[Installation](#installation) • [Quick Start](#quick-start) • [Features](#key-features) • [Documentation](./docs/) • [Contributing](./CONTRIBUTING.md)
<a href="https://qwenlm.github.io/qwen-code-docs/zh/users/overview">中文</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/de/users/overview">Deutsch</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/fr/users/overview">français</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ja/users/overview">日本語</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ru/users/overview">Русский</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/pt-BR/users/overview">Português (Brasil)</a>
</div>
<div align="center">
<a href="https://qwenlm.github.io/qwen-code-docs/de/">Deutsch</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/fr">français</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ja/">日本語</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/ru">Русский</a> |
<a href="https://qwenlm.github.io/qwen-code-docs/zh/">中文</a>
</div>
Qwen Code is an open-source AI agent for the terminal, optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder). It helps you understand large codebases, automate tedious work, and ship faster.
Qwen Code is a powerful command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli), specifically optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) models. It enhances your development workflow with advanced code understanding, automated tasks, and intelligent assistance.
![](https://gw.alicdn.com/imgextra/i1/O1CN01D2DviS1wwtEtMwIzJ_!!6000000006373-2-tps-1600-900.png)
## 💡 Free Options Available
## Why Qwen Code?
Get started with Qwen Code at no cost using any of these free options:
### 🔥 Qwen OAuth (Recommended)
- **2,000 requests per day** with no token limits
- **60 requests per minute** rate limit
- Simply run `qwen` and authenticate with your qwen.ai account
- Automatic credential management and refresh
- Use `/auth` command to switch to Qwen OAuth if you have initialized with OpenAI compatible mode
### 🌏 Regional Free Tiers
- **Mainland China**: ModelScope offers **2,000 free API calls per day**
- **International**: OpenRouter provides **up to 1,000 free API calls per day** worldwide
For detailed setup instructions, see [Authorization](#authorization).
> [!WARNING]
> **Token Usage Notice**: Qwen Code may issue multiple API calls per cycle, resulting in higher token usage (similar to Claude Code). We're actively optimizing API efficiency.
## Key Features
- **Code Understanding & Editing** - Query and edit large codebases beyond traditional context window limits
- **Workflow Automation** - Automate operational tasks like handling pull requests and complex rebases
- **Enhanced Parser** - Adapted parser specifically optimized for Qwen-Coder models
- **Vision Model Support** - Automatically detect images in your input and seamlessly switch to vision-capable models for multimodal analysis
- **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.
## Installation
### Prerequisites
Ensure you have [Node.js version 20](https://nodejs.org/en/download) or higher installed.
#### Prerequisites
```bash
# Node.js 20+
curl -qL https://www.npmjs.com/install.sh | sh
```
### Install from npm
#### NPM (recommended)
```bash
npm install -g @qwen-code/qwen-code@latest
qwen --version
```
### Install from source
```bash
git clone https://github.com/QwenLM/qwen-code.git
cd qwen-code
npm install
npm install -g .
```
### Install globally with Homebrew (macOS/Linux)
#### Homebrew (macOS, Linux)
```bash
brew install qwen-code
```
## VS Code Extension
In addition to the CLI tool, Qwen Code also provides a **VS Code extension** that brings AI-powered coding assistance directly into your editor with features like file system operations, native diffing, interactive chat, and more.
> 📦 The extension is currently in development. For installation, features, and development guide, see the [VS Code Extension README](./packages/vscode-ide-companion/README.md).
## Quick Start
```bash
# Start Qwen Code
# Start Qwen Code (interactive)
qwen
# Example commands
> Explain this codebase structure
> Help me refactor this function
> Generate unit tests for this module
# Then, in the session:
/help
/auth
```
### Session Management
On first use, you'll be prompted to sign in. You can run `/auth` anytime to switch authentication methods.
Control your token usage with configurable session limits to optimize costs and performance.
Example prompts:
#### Configure Session Token Limit
Create or edit `.qwen/settings.json` in your home directory:
```json
{
"sessionTokenLimit": 32000
}
```text
What does this project do?
Explain the codebase structure.
Help me refactor this function.
Generate unit tests for this module.
```
#### Session Commands
- **`/compress`** - Compress conversation history to continue within token limits
- **`/clear`** - Clear all conversation history and start fresh
- **`/stats`** - Check current token usage and limits
> 📝 **Note**: Session token limit applies to a single conversation, not cumulative API calls.
### Vision Model Configuration
Qwen Code includes intelligent vision model auto-switching that detects images in your input and can automatically switch to vision-capable models for multimodal analysis. **This feature is enabled by default** - when you include images in your queries, you'll see a dialog asking how you'd like to handle the vision model switch.
#### Skip the Switch Dialog (Optional)
If you don't want to see the interactive dialog each time, configure the default behavior in your `.qwen/settings.json`:
```json
{
"experimental": {
"vlmSwitchMode": "once"
}
}
```
**Available modes:**
- **`"once"`** - Switch to vision model for this query only, then revert
- **`"session"`** - Switch to vision model for the entire session
- **`"persist"`** - Continue with current model (no switching)
- **Not set** - Show interactive dialog each time (default)
#### Command Line Override
You can also set the behavior via command line:
```bash
# Switch once per query
qwen --vlm-switch-mode once
# Switch for entire session
qwen --vlm-switch-mode session
# Never switch automatically
qwen --vlm-switch-mode persist
```
#### Disable Vision Models (Optional)
To completely disable vision model support, add to your `.qwen/settings.json`:
```json
{
"experimental": {
"visionModelPreview": false
}
}
```
> 💡 **Tip**: In YOLO mode (`--yolo`), vision switching happens automatically without prompts when images are detected.
### Authorization
Choose your preferred authentication method based on your needs:
#### 1. Qwen OAuth (🚀 Recommended - Start in 30 seconds)
The easiest way to get started - completely free with generous quotas:
```bash
# Just run this command and follow the browser authentication
qwen
```
**What happens:**
1. **Instant Setup**: CLI opens your browser automatically
2. **One-Click Login**: Authenticate with your qwen.ai account
3. **Automatic Management**: Credentials cached locally for future use
4. **No Configuration**: Zero setup required - just start coding!
**Free Tier Benefits:**
-**2,000 requests/day** (no token counting needed)
-**60 requests/minute** rate limit
-**Automatic credential refresh**
-**Zero cost** for individual users
- **Note**: Model fallback may occur to maintain service quality
#### 2. OpenAI-Compatible API
Use API keys for OpenAI or other compatible providers:
**Configuration Methods:**
1. **Environment Variables**
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="your_api_endpoint"
export OPENAI_MODEL="your_model_choice"
```
2. **Project `.env` File**
Create a `.env` file in your project root:
```env
OPENAI_API_KEY=your_api_key_here
OPENAI_BASE_URL=your_api_endpoint
OPENAI_MODEL=your_model_choice
```
**API Provider Options**
> ⚠️ **Regional Notice:**
>
> - **Mainland China**: Use Alibaba Cloud Bailian or ModelScope
> - **International**: Use Alibaba Cloud ModelStudio or OpenRouter
<details>
<summary><b>🇨🇳 For Users in Mainland China</b></summary>
<summary>Click to watch a demo video</summary>
**Option 1: Alibaba Cloud Bailian** ([Apply for API Key](https://bailian.console.aliyun.com/))
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3-coder-plus"
```
**Option 2: ModelScope (Free Tier)** ([Apply for API Key](https://modelscope.cn/docs/model-service/API-Inference/intro))
- ✅ **2,000 free API calls per day**
- ⚠️ Connect your Aliyun account to avoid authentication errors
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://api-inference.modelscope.cn/v1"
export OPENAI_MODEL="Qwen/Qwen3-Coder-480B-A35B-Instruct"
```
<video src="https://cloud.video.taobao.com/vod/HLfyppnCHplRV9Qhz2xSqeazHeRzYtG-EYJnHAqtzkQ.mp4" controls>
Your browser does not support the video tag.
</video>
</details>
<details>
<summary><b>🌍 For International Users</b></summary>
## Authentication
**Option 1: Alibaba Cloud ModelStudio** ([Apply for API Key](https://modelstudio.console.alibabacloud.com/))
Qwen Code supports two authentication methods:
- **Qwen OAuth (recommended & free)**: sign in with your `qwen.ai` account in a browser.
- **OpenAI-compatible API**: use `OPENAI_API_KEY` (and optionally a custom base URL / model).
#### Qwen OAuth (recommended)
Start `qwen`, then run:
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3-coder-plus"
/auth
```
**Option 2: OpenRouter (Free Tier Available)** ([Apply for API Key](https://openrouter.ai/))
Choose **Qwen OAuth** and complete the browser flow. Your credentials are cached locally so you usually won't need to log in again.
#### OpenAI-compatible API (API key)
Environment variables (recommended for CI / headless environments):
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
export OPENAI_MODEL="qwen/qwen3-coder:free"
export OPENAI_API_KEY="your-api-key-here"
export OPENAI_BASE_URL="https://api.openai.com/v1" # optional
export OPENAI_MODEL="gpt-4o" # optional
```
</details>
For details (including `.qwen/.env` loading and security notes), see the [authentication guide](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/auth/).
## Usage Examples
## Usage
### 🔍 Explore Codebases
As an open-source terminal agent, you can use Qwen Code in four primary ways:
1. Interactive mode (terminal UI)
2. Headless mode (scripts, CI)
3. IDE integration (VS Code, Zed)
4. TypeScript SDK
#### Interactive mode
```bash
cd your-project/
qwen
# Architecture analysis
> Describe the main pieces of this system's architecture
> What are the key dependencies and how do they interact?
> Find all API endpoints and their authentication methods
```
### 💻 Code Development
Run `qwen` in your project folder to launch the interactive terminal UI. Use `@` to reference local files (for example `@src/main.ts`).
#### Headless mode
```bash
# Refactoring
> Refactor this function to improve readability and performance
> Convert this class to use dependency injection
> Split this large module into smaller, focused components
# Code generation
> Create a REST API endpoint for user management
> Generate unit tests for the authentication module
> Add error handling to all database operations
cd your-project/
qwen -p "your question"
```
### 🔄 Automate Workflows
Use `-p` to run Qwen Code without the interactive UI—ideal for scripts, automation, and CI/CD. Learn more: [Headless mode](https://qwenlm.github.io/qwen-code-docs/en/users/features/headless).
```bash
# Git automation
> Analyze git commits from the last 7 days, grouped by feature
> Create a changelog from recent commits
> Find all TODO comments and create GitHub issues
#### IDE integration
# File operations
> Convert all images in this directory to PNG format
> Rename all test files to follow the *.test.ts pattern
> Find and remove all console.log statements
```
Use Qwen Code inside your editor (VS Code and Zed):
### 🐛 Debugging & Analysis
- [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/)
```bash
# Performance analysis
> Identify performance bottlenecks in this React component
> Find all N+1 query problems in the codebase
#### TypeScript SDK
# Security audit
> Check for potential SQL injection vulnerabilities
> Find all hardcoded credentials or API keys
```
Build on top of Qwen Code with the TypeScript SDK:
## Popular Tasks
### 📚 Understand New Codebases
```text
> What are the core business logic components?
> What security mechanisms are in place?
> How does the data flow through the system?
> What are the main design patterns used?
> Generate a dependency graph for this module
```
### 🔨 Code Refactoring & Optimization
```text
> What parts of this module can be optimized?
> Help me refactor this class to follow SOLID principles
> Add proper error handling and logging
> Convert callbacks to async/await pattern
> Implement caching for expensive operations
```
### 📝 Documentation & Testing
```text
> Generate comprehensive JSDoc comments for all public APIs
> Write unit tests with edge cases for this component
> Create API documentation in OpenAPI format
> Add inline comments explaining complex algorithms
> Generate a README for this module
```
### 🚀 Development Acceleration
```text
> Set up a new Express server with authentication
> Create a React component with TypeScript and tests
> Implement a rate limiter middleware
> Add database migrations for new schema
> Configure CI/CD pipeline for this project
```
- [Use the Qwen Code SDK](./packages/sdk-typescript/README.md)
## Commands & Shortcuts
@@ -386,6 +156,7 @@ qwen
- `/clear` - Clear conversation history
- `/compress` - Compress history to save tokens
- `/stats` - Show current session information
- `/bug` - Submit a bug report
- `/exit` or `/quit` - Exit Qwen Code
### Keyboard Shortcuts
@@ -394,6 +165,19 @@ qwen
- `Ctrl+D` - Exit (on empty line)
- `Up/Down` - Navigate command history
> Learn more about [Commands](https://qwenlm.github.io/qwen-code-docs/en/users/features/commands/)
>
> **Tip**: In YOLO mode (`--yolo`), vision switching happens automatically without prompts when images are detected. Learn more about [Approval Mode](https://qwenlm.github.io/qwen-code-docs/en/users/features/approval-mode/)
## Configuration
Qwen Code can be configured via `settings.json`, environment variables, and CLI flags.
- **User settings**: `~/.qwen/settings.json`
- **Project settings**: `.qwen/settings.json`
See [settings](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/) for available options and precedence.
## Benchmark Results
### Terminal-Bench Performance
@@ -403,24 +187,19 @@ qwen
| Qwen Code | Qwen3-Coder-480A35 | 37.5% |
| Qwen Code | Qwen3-Coder-30BA3B | 31.3% |
## Development & Contributing
## Ecosystem
See [CONTRIBUTING.md](./CONTRIBUTING.md) to learn how to contribute to the project.
Looking for a graphical interface?
For detailed authentication setup, see the [authentication guide](./docs/cli/authentication.md).
- [**AionUi**](https://github.com/iOfficeAI/AionUi) A modern GUI for command-line AI tools including Qwen Code
- [**Gemini CLI Desktop**](https://github.com/Piebald-AI/gemini-cli-desktop) A cross-platform desktop/web/mobile UI for Qwen Code
## Troubleshooting
If you encounter issues, check the [troubleshooting guide](docs/troubleshooting.md).
If you encounter issues, check the [troubleshooting guide](https://qwenlm.github.io/qwen-code-docs/en/users/support/troubleshooting/).
To report a bug from within the CLI, run `/bug` and include a short title and repro steps.
## 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.
## License
[LICENSE](./LICENSE)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=QwenLM/qwen-code&type=Date)](https://www.star-history.com/#QwenLM/qwen-code&Date)

View File

@@ -11,6 +11,7 @@ export default {
type: 'separator',
},
'sdk-typescript': 'Typescript SDK',
'sdk-java': 'Java SDK(alpha)',
'Dive Into Qwen Code': {
title: 'Dive Into Qwen Code',
type: 'separator',

312
docs/developers/sdk-java.md Normal file
View File

@@ -0,0 +1,312 @@
# Qwen Code Java SDK
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
## Requirements
- Java >= 1.8
- Maven >= 3.6.0 (for building from source)
- qwen-code >= 0.5.0
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Installation
Add the following dependency to your Maven `pom.xml`:
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<version>{$version}</version>
</dependency>
```
Or if using Gradle, add to your `build.gradle`:
```gradle
implementation 'com.alibaba:qwencode-sdk:{$version}'
```
## Building and Running
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
```
## Quick Start
The simplest way to use the SDK is through the `QwenCodeCli.simpleQuery()` method:
```java
public static void runSimpleExample() {
List<String> result = QwenCodeCli.simpleQuery("hello world");
result.forEach(logger::info);
}
```
For more advanced usage with custom transport options:
```java
public static void runTransportOptionsExample() {
TransportOptions options = new TransportOptions()
.setModel("qwen3-coder-flash")
.setPermissionMode(PermissionMode.AUTO_EDIT)
.setCwd("./")
.setEnv(new HashMap<String, String>() {{put("CUSTOM_VAR", "value");}})
.setIncludePartialMessages(true)
.setTurnTimeout(new Timeout(120L, TimeUnit.SECONDS))
.setMessageTimeout(new Timeout(90L, TimeUnit.SECONDS))
.setAllowedTools(Arrays.asList("read_file", "write_file", "list_directory"));
List<String> result = QwenCodeCli.simpleQuery("who are you, what are your capabilities?", options);
result.forEach(logger::info);
}
```
For streaming content handling with custom content consumers:
```java
public static void runStreamingExample() {
QwenCodeCli.simpleQuery("who are you, what are your capabilities?",
new TransportOptions().setMessageTimeout(new Timeout(10L, TimeUnit.SECONDS)), new AssistantContentSimpleConsumers() {
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
logger.info("Text content received: {}", textAssistantContent.getText());
}
@Override
public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) {
logger.info("Thinking content received: {}", thingkingAssistantContent.getThinking());
}
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseContent) {
logger.info("Tool use content received: {} with arguments: {}",
toolUseContent, toolUseContent.getInput());
}
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultContent) {
logger.info("Tool result content received: {}", toolResultContent.getContent());
}
@Override
public void onOtherContent(Session session, AssistantContent<?> other) {
logger.info("Other content received: {}", other);
}
@Override
public void onUsage(Session session, AssistantUsage assistantUsage) {
logger.info("Usage information received: Input tokens: {}, Output tokens: {}",
assistantUsage.getUsage().getInputTokens(), assistantUsage.getUsage().getOutputTokens());
}
}.setDefaultPermissionOperation(Operation.allow));
logger.info("Streaming example completed.");
}
```
other examples see src/test/java/com/alibaba/qwen/code/cli/example
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Features
### Permission Modes
The SDK supports different permission modes for controlling tool execution:
- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (edit, write_file) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.
### Session Event Consumers and Assistant Content Consumers
The SDK provides two key interfaces for handling events and content from the CLI:
#### SessionEventConsumers Interface
The `SessionEventConsumers` interface provides callbacks for different types of messages during a session:
- `onSystemMessage`: Handles system messages from the CLI (receives Session and SDKSystemMessage)
- `onResultMessage`: Handles result messages from the CLI (receives Session and SDKResultMessage)
- `onAssistantMessage`: Handles assistant messages (AI responses) (receives Session and SDKAssistantMessage)
- `onPartialAssistantMessage`: Handles partial assistant messages during streaming (receives Session and SDKPartialAssistantMessage)
- `onUserMessage`: Handles user messages (receives Session and SDKUserMessage)
- `onOtherMessage`: Handles other types of messages (receives Session and String message)
- `onControlResponse`: Handles control responses (receives Session and CLIControlResponse)
- `onControlRequest`: Handles control requests (receives Session and CLIControlRequest, returns CLIControlResponse)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlRequest<CLIControlPermissionRequest>, returns Behavior)
#### AssistantContentConsumers Interface
The `AssistantContentConsumers` interface handles different types of content within assistant messages:
- `onText`: Handles text content (receives Session and TextAssistantContent)
- `onThinking`: Handles thinking content (receives Session and ThingkingAssistantContent)
- `onToolUse`: Handles tool use content (receives Session and ToolUseAssistantContent)
- `onToolResult`: Handles tool result content (receives Session and ToolResultAssistantContent)
- `onOtherContent`: Handles other content types (receives Session and AssistantContent)
- `onUsage`: Handles usage information (receives Session and AssistantUsage)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlPermissionRequest, returns Behavior)
- `onOtherControlRequest`: Handles other control requests (receives Session and ControlRequestPayload, returns ControlResponsePayload)
#### Relationship Between the Interfaces
**Important Note on Event Hierarchy:**
- `SessionEventConsumers` is the **high-level** event processor that handles different message types (system, assistant, user, etc.)
- `AssistantContentConsumers` is the **low-level** content processor that handles different types of content within assistant messages (text, tools, thinking, etc.)
**Processor Relationship:**
- `SessionEventConsumers``AssistantContentConsumers` (SessionEventConsumers uses AssistantContentConsumers to process content within assistant messages)
**Event Derivation Relationships:**
- `onAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``onPermissionRequest`, `onOtherControlRequest`
**Event Timeout Relationships:**
Each event handler method has a corresponding timeout method that allows customizing the timeout behavior for that specific event:
- `onSystemMessage``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default Timeout Values:**
- `SessionEventSimpleConsumers` default timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)
- `AssistantContentSimpleConsumers` default timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)
**Timeout Hierarchy Requirements:**
For proper operation, the following timeout relationships should be maintained:
- `onAssistantMessageTimeout` return value should be greater than `onTextTimeout`, `onThinkingTimeout`, `onToolUseTimeout`, `onToolResultTimeout`, and `onOtherContentTimeout` return values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return values
### Transport Options
The `TransportOptions` class allows configuration of how the SDK communicates with the Qwen Code CLI:
- `pathToQwenExecutable`: Path to the Qwen Code CLI executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: Additional command-line options to pass to the CLI
### Session Control Features
- **Session creation**: Use `QwenCodeCli.newSession()` to create a new session with custom options
- **Session management**: The `Session` class provides methods to send prompts, handle responses, and manage session state
- **Session cleanup**: Always close sessions using `session.close()` to properly terminate the CLI process
- **Session resumption**: Use `setResumeSessionId()` in `TransportOptions` to resume a previous session
- **Session interruption**: Use `session.interrupt()` to interrupt a currently running prompt
- **Dynamic model switching**: Use `session.setModel()` to change the model during a session
- **Dynamic permission mode switching**: Use `session.setPermissionMode()` to change the permission mode during a session
### Thread Pool Configuration
The SDK uses a thread pool for managing concurrent operations with the following default configuration:
- **Core Pool Size**: 30 threads
- **Maximum Pool Size**: 100 threads
- **Keep-Alive Time**: 60 seconds
- **Queue Capacity**: 300 tasks (using LinkedBlockingQueue)
- **Thread Naming**: "qwen_code_cli-pool-{number}"
- **Daemon Threads**: false
- **Rejected Execution Handler**: CallerRunsPolicy
## Error Handling
The SDK provides specific exception types for different error scenarios:
- `SessionControlException`: Thrown when there's an issue with session control (creation, initialization, etc.)
- `SessionSendPromptException`: Thrown when there's an issue sending a prompt or receiving a response
- `SessionClosedException`: Thrown when attempting to use a closed session
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: yes, requires Qwen CLI 0.5.5 or higher.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### Q: How do I handle long-running requests?
A: The SDK includes timeout utilities. You can configure timeouts using the `Timeout` class in `TransportOptions`.
### Q: Why are some tools not executing?
A: This is likely due to permission modes. Check your permission mode settings and consider using `allowedTools` to pre-approve certain tools.
### Q: How do I resume a previous session?
A: Use the `setResumeSessionId()` method in `TransportOptions` to resume a previous session.
### Q: Can I customize the environment for the CLI process?
A: Yes, use the `setEnv()` method in `TransportOptions` to pass environment variables to the CLI process.
## License
Apache-2.0 - see [LICENSE](./LICENSE) for details.

View File

@@ -627,7 +627,12 @@ The MCP integration tracks several states:
### Schema Compatibility
- **Property stripping:** The system automatically removes certain schema properties (`$schema`, `additionalProperties`) for Qwen API compatibility
- **Schema compliance mode:** By default (`schemaCompliance: "auto"`), tool schemas are passed through as-is. Set `"model": { "generationConfig": { "schemaCompliance": "openapi_30" } }` in your `settings.json` to convert models to Strict OpenAPI 3.0 format.
- **OpenAPI 3.0 transformations:** When `openapi_30` mode is enabled, the system handles:
- Nullable types: `["string", "null"]` -> `type: "string", nullable: true`
- Const values: `const: "foo"` -> `enum: ["foo"]`
- Exclusive limits: numeric `exclusiveMinimum` -> boolean form with `minimum`
- Keyword removal: `$schema`, `$id`, `dependencies`, `patternProperties`
- **Name sanitization:** Tool names are automatically sanitized to meet API requirements
- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing

View File

@@ -14,7 +14,7 @@ Learn how to use Qwen Code as an end user. This section covers:
- Configuration options
- Troubleshooting
### [Developer Guide](./developers/contributing)
### [Developer Guide](./developers/architecture)
Learn how to contribute to and develop Qwen Code. This section covers:

View File

@@ -0,0 +1,80 @@
# Chrome 扩展 Native Host 排查步骤
适用于遇到“Specified native messaging host not found.”、“Native host has exited.”、“Handshake timeout”等情况。
## 1. 核对 manifest
路径:`~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json`
内容应为:
```json
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "/Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/host.js",
"type": "stdio",
"allowed_origins": [
"chrome-extension://kbpfhhpfobobomiighfkhojhmefogdgh/"
]
}
```
一键覆盖命令:
```bash
cat > "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json" <<'EOF'
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "/Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/host.js",
"type": "stdio",
"allowed_origins": [
"chrome-extension://kbpfhhpfobobomiighfkhojhmefogdgh/"
]
}
EOF
```
> 修改 manifest 后务必**彻底退出并重启 Chrome**,再在扩展页点击“重新加载”插件。
## 2. 确保可执行与 Node 路径
Host 入口已设置 shebang `/usr/local/bin/node`。确保脚本可执行:
```bash
chmod +x /Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/host.js
chmod +x /Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/src/host.js
```
## 3. 日志位置
- 主日志:`~/.qwen/chrome-bridge/qwen-bridge-host.log`
- 如果主目录不可写,回退:`/tmp/qwen-bridge-host.log``/var/folders/.../T/qwen-bridge-host.log`
若文件为空,说明 host 可能没被 Chrome 拉起或启动后被立即杀掉(查看 manifest 是否正确、Chrome 是否重启)。
## 4. 手动运行自检
```bash
node /Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/host.js
```
进程会挂起等待 stdin无输出属正常日志文件应记录启动信息。`Ctrl+C` 退出。
## 5. 常见错误与对应操作
- `Specified native messaging host not found.`
Manifest 中 `path``allowed_origins` 不对,或 Chrome 未重启。按第 1 步覆盖,重启 Chrome。
- `Native host has exited.` / `Handshake timeout`
多为 manifest 不被 Chrome 接受或 host 无法启动。确认第 1、2 步,重启 Chrome再看日志是否收到 “Received … bytes”/信号。
## 6. 快速排查命令合集
```bash
# 查看当前 manifest
cat "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json"
# 覆盖 manifest见第 1 步)
cat > "$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json" <<'EOF'
{ ...如上... }
EOF
# 确保可执行
chmod +x /Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/host.js
chmod +x /Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/packages/chrome-extension/native-host/src/host.js
# 查看日志
cat ~/.qwen/chrome-bridge/qwen-bridge-host.log 2>/dev/null || echo "no log"
```

View File

@@ -189,8 +189,8 @@ Then select "create" and follow the prompts to define:
> - Create project-specific subagents in `.qwen/agents/` for team sharing
> - Use descriptive `description` fields to enable automatic delegation
> - Limit tool access to what each subagent actually needs
> - Know more about [Sub Agents](/users/features/sub-agents)
> - Know more about [Approval Mode](/users/features/approval-mode)
> - Know more about [Sub Agents](./features/sub-agents)
> - Know more about [Approval Mode](./features/approval-mode)
## Work with tests
@@ -318,7 +318,7 @@ This provides a directory listing with file information.
Show me the data from @github: repos/owner/repo/issues
```
This fetches data from connected MCP servers using the format @server: resource. See [MCP](/users/features/mcp) for details.
This fetches data from connected MCP servers using the format @server: resource. See [MCP](./features/mcp) for details.
> [!tip]
>

View File

@@ -6,7 +6,7 @@ Qwen Code includes the ability to automatically ignore files, similar to `.gitig
## How it works
When you add a path to your `.qwenignore` file, tools that respect this file will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](/developers/tools/multi-file) command, any paths in your `.qwenignore` file will be automatically excluded.
When you add a path to your `.qwenignore` file, tools that respect this file will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](../../developers/tools/multi-file) command, any paths in your `.qwenignore` file will be automatically excluded.
For the most part, `.qwenignore` follows the conventions of `.gitignore` files:
@@ -20,9 +20,9 @@ You can update your `.qwenignore` file at any time. To apply the changes, you mu
## How to use `.qwenignore`
| Step | Description |
| ---------------------- | ------------------------------------------------------------ |
| **Enable .qwenignore** | Create a file named `.qwenignore` in your project root directory |
| Step | Description |
| ---------------------- | -------------------------------------------------------------------------------------- |
| **Enable .qwenignore** | Create a file named `.qwenignore` in your project root directory |
| **Add ignore rules** | Open `.qwenignore` file and add paths to ignore, example: `/archive/` or `apikeys.txt` |
### `.qwenignore` examples

View File

@@ -2,7 +2,7 @@
> [!tip]
>
> **Authentication / API keys:** Authentication (Qwen OAuth vs OpenAI-compatible API) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](/users/configuration/auth)**.
> **Authentication / API keys:** Authentication (Qwen OAuth vs OpenAI-compatible API) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](../configuration/auth)**.
> [!note]
>
@@ -42,7 +42,8 @@ Qwen Code uses JSON settings files for persistent configuration. There are four
In addition to a project settings file, a project's `.qwen` directory can contain other project-specific files related to Qwen Code's operation, such as:
- [Custom sandbox profiles](/users/features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`).
- [Custom sandbox profiles](../features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`).
- [Agent Skills](../features/skills) (experimental) under `.qwen/skills/` (each Skill is a directory containing a `SKILL.md`).
### Available settings in `settings.json`
@@ -50,13 +51,14 @@ Settings are organized into categories. All settings should be placed within the
#### general
| Setting | Type | Description | Default |
| ------------------------------- | ------- | ------------------------------------------ | ----------- |
| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
| `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
| `general.disableAutoUpdate` | boolean | Disable automatic updates. | `false` |
| `general.disableUpdateNag` | boolean | Disable update notification prompts. | `false` |
| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` |
| Setting | Type | Description | Default |
| ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | ----------- |
| `general.preferredEditor` | string | The preferred editor to open files in. | `undefined` |
| `general.vimMode` | boolean | Enable Vim keybindings. | `false` |
| `general.disableAutoUpdate` | boolean | Disable automatic updates. | `false` |
| `general.disableUpdateNag` | boolean | Disable update notification prompts. | `false` |
| `general.gitCoAuthor` | boolean | Automatically add a Co-authored-by trailer to git commit messages when commits are made through Qwen Code. | `true` |
| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` |
#### output
@@ -68,7 +70,7 @@ Settings are organized into categories. All settings should be placed within the
| Setting | Type | Description | Default |
| ---------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| `ui.theme` | string | The color theme for the UI. See [Themes](/users/configuration/themes) for available options. | `undefined` |
| `ui.theme` | string | The color theme for the UI. See [Themes](../configuration/themes) for available options. | `undefined` |
| `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` |
@@ -325,7 +327,7 @@ The CLI keeps a history of shell commands you run. To avoid conflicts between di
Environment variables are a common way to configure applications, especially for sensitive information (like tokens) or for settings that might change between environments.
Qwen Code can automatically load environment variables from `.env` files.
For authentication-related variables (like `OPENAI_*`) and the recommended `.qwen/.env` approach, see **[Authentication](/users/configuration/auth)**.
For authentication-related variables (like `OPENAI_*`) and the recommended `.qwen/.env` approach, see **[Authentication](../configuration/auth)**.
> [!tip]
>
@@ -356,38 +358,40 @@ Arguments passed directly when running the CLI can override other configurations
### Command-Line Arguments Table
| Argument | Alias | Description | Possible Values | Notes |
| ---------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model` | `-m` | Specifies the Qwen model to use for this session. | Model name | Example: `npm start -- --model qwen3-coder-plus` |
| `--prompt` | `-p` | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. | Your prompt text | For scripting examples, use the `--output-format json` flag to get structured output. |
| `--prompt-interactive` | `-i` | Starts an interactive session with the provided prompt as the initial input. | Your prompt text | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"` |
| `--output-format` | `-o` | Specifies the format of the CLI output for non-interactive mode. | `text`, `json`, `stream-json` | `text`: (Default) The standard human-readable output. `json`: A machine-readable JSON output emitted at the end of execution. `stream-json`: Streaming JSON messages emitted as they occur during execution. For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](/users/features/headless) for detailed information. |
| `--input-format` | | Specifies the format consumed from standard input. | `text`, `stream-json` | `text`: (Default) Standard text input from stdin or command-line arguments. `stream-json`: JSON message protocol via stdin for bidirectional communication. Requirement: `--input-format stream-json` requires `--output-format stream-json` to be set. When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](/users/features/headless) for detailed information. |
| `--include-partial-messages` | | Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming. | | Default: `false`. Requirement: Requires `--output-format stream-json` to be set. See [Headless Mode](/users/features/headless) for detailed information about stream events. |
| `--sandbox` | `-s` | Enables sandbox mode for this session. | | |
| `--sandbox-image` | | Sets the sandbox image URI. | | |
| `--debug` | `-d` | Enables debug mode for this session, providing more verbose output. | | |
| `--all-files` | `-a` | If set, recursively includes all files within the current directory as context for the prompt. | | |
| `--help` | `-h` | Displays help information about command-line arguments. | | |
| `--show-memory-usage` | | Displays the current memory usage. | | |
| `--yolo` | | Enables YOLO mode, which automatically approves all tool calls. | | |
| `--approval-mode` | | Sets the approval mode for tool calls. | `plan`, `default`, `auto-edit`, `yolo` | Supported modes: `plan`: Analyze only—do not modify files or execute commands. `default`: Require approval for file edits or shell commands (default behavior). `auto-edit`: Automatically approve edit tools (edit, write_file) while prompting for others. `yolo`: Automatically approve all tool calls (equivalent to `--yolo`). Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. Example: `qwen --approval-mode auto-edit`<br>See more about [Approval Mode](/users/features/approval-mode). |
| `--allowed-tools` | | A comma-separated list of tool names that will bypass the confirmation dialog. | Tool names | Example: `qwen --allowed-tools "Shell(git status)"` |
| `--telemetry` | | Enables [telemetry](/developers/development/telemetry). | | |
| `--telemetry-target` | | Sets the telemetry target. | | See [telemetry](/developers/development/telemetry) for more information. |
| `--telemetry-otlp-endpoint` | | Sets the OTLP endpoint for telemetry. | | See [telemetry](/developers/development/telemetry) for more information. |
| `--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](/users/features/checkpointing). | | |
| `--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. | | |
| `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. |
| `--include-directories` | | Includes additional directories in the workspace for multi-directory support. | Directory paths | Can be specified multiple times or as comma-separated values. 5 directories can be added at maximum. Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` |
| `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | |
| `--version` | | Displays the version of the CLI. | | |
| `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. |
| `--openai-logging-dir` | | Sets a custom directory path for OpenAI API logs. | Directory path | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging` |
| `--tavily-api-key` | | Sets the Tavily API key for web search functionality for this session. | API key | Example: `qwen --tavily-api-key tvly-your-api-key-here` |
| Argument | Alias | Description | Possible Values | Notes |
| ---------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--model` | `-m` | Specifies the Qwen model to use for this session. | Model name | Example: `npm start -- --model qwen3-coder-plus` |
| `--prompt` | `-p` | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. | Your prompt text | For scripting examples, use the `--output-format json` flag to get structured output. |
| `--prompt-interactive` | `-i` | Starts an interactive session with the provided prompt as the initial input. | Your prompt text | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"` |
| `--output-format` | `-o` | Specifies the format of the CLI output for non-interactive mode. | `text`, `json`, `stream-json` | `text`: (Default) The standard human-readable output. `json`: A machine-readable JSON output emitted at the end of execution. `stream-json`: Streaming JSON messages emitted as they occur during execution. For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](../features/headless) for detailed information. |
| `--input-format` | | Specifies the format consumed from standard input. | `text`, `stream-json` | `text`: (Default) Standard text input from stdin or command-line arguments. `stream-json`: JSON message protocol via stdin for bidirectional communication. Requirement: `--input-format stream-json` requires `--output-format stream-json` to be set. When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](../features/headless) for detailed information. |
| `--include-partial-messages` | | Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming. | | Default: `false`. Requirement: Requires `--output-format stream-json` to be set. See [Headless Mode](../features/headless) for detailed information about stream events. |
| `--sandbox` | `-s` | Enables sandbox mode for this session. | | |
| `--sandbox-image` | | Sets the sandbox image URI. | | |
| `--debug` | `-d` | Enables debug mode for this session, providing more verbose output. | | |
| `--all-files` | `-a` | If set, recursively includes all files within the current directory as context for the prompt. | | |
| `--help` | `-h` | Displays help information about command-line arguments. | | |
| `--show-memory-usage` | | Displays the current memory usage. | | |
| `--yolo` | | Enables YOLO mode, which automatically approves all tool calls. | | |
| `--approval-mode` | | Sets the approval mode for tool calls. | `plan`, `default`, `auto-edit`, `yolo` | Supported modes: `plan`: Analyze only—do not modify files or execute commands. `default`: Require approval for file edits or shell commands (default behavior). `auto-edit`: Automatically approve edit tools (edit, write_file) while prompting for others. `yolo`: Automatically approve all tool calls (equivalent to `--yolo`). Cannot be used together with `--yolo`. Use `--approval-mode=yolo` instead of `--yolo` for the new unified approach. Example: `qwen --approval-mode auto-edit`<br>See more about [Approval Mode](../features/approval-mode). |
| `--allowed-tools` | | A comma-separated list of tool names that will bypass the confirmation dialog. | Tool names | Example: `qwen --allowed-tools "Shell(git status)"` |
| `--telemetry` | | Enables [telemetry](/developers/development/telemetry). | | |
| `--telemetry-target` | | Sets the telemetry target. | | See [telemetry](/developers/development/telemetry) for more information. |
| `--telemetry-otlp-endpoint` | | Sets the OTLP endpoint for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
| `--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. |
| `--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. | | |
| `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. |
| `--include-directories` | | Includes additional directories in the workspace for multi-directory support. | Directory paths | Can be specified multiple times or as comma-separated values. 5 directories can be added at maximum. Example: `--include-directories /path/to/project1,/path/to/project2` or `--include-directories /path/to/project1 --include-directories /path/to/project2` |
| `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | |
| `--version` | | Displays the version of the CLI. | | |
| `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. |
| `--openai-logging-dir` | | Sets a custom directory path for OpenAI API logs. | Directory path | This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion. Example: `qwen --openai-logging-dir "~/qwen-logs" --openai-logging` |
| `--tavily-api-key` | | Sets the Tavily API key for web search functionality for this session. | API key | Example: `qwen --tavily-api-key tvly-your-api-key-here` |
## Context Files (Hierarchical Instructional Context)
@@ -437,11 +441,11 @@ This example demonstrates how you can provide general project context, specific
- Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file.
- Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project.
- **Concatenation & UI Indication:** The contents of all found context files are concatenated (with separators indicating their origin and path) and provided as part of the system prompt. The CLI footer displays the count of loaded context files, giving you a quick visual cue about the active instructional context.
- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](/users/configuration/memory).
- **Importing Content:** You can modularize your context files by importing other Markdown files using the `@path/to/file.md` syntax. For more details, see the [Memory Import Processor documentation](../configuration/memory).
- **Commands for Memory Management:**
- Use `/memory refresh` to force a re-scan and reload of all context files from all configured locations. This updates the AI's instructional context.
- Use `/memory show` to display the combined instructional context currently loaded, allowing you to verify the hierarchy and content being used by the AI.
- See the [Commands documentation](/users/reference/cli-reference) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
- See the [Commands documentation](../features/commands) for full details on the `/memory` command and its sub-commands (`show` and `refresh`).
By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor Qwen Code's responses to your specific needs and projects.
@@ -449,7 +453,7 @@ By understanding and utilizing these configuration layers and the hierarchical n
Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system.
[Sandbox](/users/features/sandbox) is disabled by default, but you can enable it in a few ways:
[Sandbox](../features/sandbox) is disabled by default, but you can enable it in a few ways:
- Using `--sandbox` or `-s` flag.
- Setting `GEMINI_SANDBOX` environment variable.

View File

@@ -32,7 +32,7 @@ Qwen Code comes with a selection of pre-defined themes, which you can list using
### Theme Persistence
Selected themes are saved in Qwen Code's [configuration](./configuration.md) so your preference is remembered across sessions.
Selected themes are saved in Qwen Code's [configuration](../configuration/settings) so your preference is remembered across sessions.
---
@@ -140,25 +140,21 @@ The theme file must be a valid JSON file that follows the same structure as a cu
### Example Custom Theme
<img src="https://gw.alicdn.com/imgextra/i1/O1CN01Em30Hc1jYXAdIgls3_!!6000000004560-2-tps-1009-629.png" alt=" " style="zoom:100%;text-align:center;margin: 0 auto;" />
### Using Your Custom Theme
- Select your custom theme using the `/theme` command in Qwen Code. Your custom theme will appear in the theme selection dialog.
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`.
- Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](./configuration.md) as other settings.
- Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](../configuration/settings) as other settings.
## Themes Preview
| Dark Theme | Preview | Light Theme | Preview |
| :-: | :-: | :-: | :-: |
| ANSI | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01ZInJiq1GdSZc9gHsI_!!6000000000645-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | ANSI Light | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01IiJQFC1h9E3MXQj6W_!!6000000004234-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Atom OneDark | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01Zlx1SO1Sw21SkTKV3_!!6000000002310-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Ayu Light | <img src="https://gw.alicdn.com/imgextra/i3/O1CN01zEUc1V1jeUJsnCgQb_!!6000000004573-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Ayu | <img src="https://gw.alicdn.com/imgextra/i3/O1CN019upo6v1SmPhmRjzfN_!!6000000002289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Default Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01RHjrEs1u7TXq3M6l3_!!6000000005990-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Default | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016pIeXz1pFC8owmR4Q_!!6000000005330-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | GitHub Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01US2b0g1VETCPAVWLA_!!6000000002621-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Dracula | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016htnWH20c3gd2LpUR_!!6000000006869-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Google Code | <img src="https://gw.alicdn.com/imgextra/i1/O1CN01Ng29ab23iQ2BuYKz8_!!6000000007289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| GitHub | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01fFCRda1IQIQ9qDNqv_!!6000000000887-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Xcode | <img src="https://gw.alicdn.com/imgextra/i1/O1CN010E3QAi1Huh5o1E9LN_!!6000000000818-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Dark Theme | Preview | Light Theme | Preview |
| :----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| ANSI | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01ZInJiq1GdSZc9gHsI_!!6000000000645-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | ANSI Light | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01IiJQFC1h9E3MXQj6W_!!6000000004234-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Atom OneDark | <img src="https://gw.alicdn.com/imgextra/i2/O1CN01Zlx1SO1Sw21SkTKV3_!!6000000002310-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Ayu Light | <img src="https://gw.alicdn.com/imgextra/i3/O1CN01zEUc1V1jeUJsnCgQb_!!6000000004573-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Ayu | <img src="https://gw.alicdn.com/imgextra/i3/O1CN019upo6v1SmPhmRjzfN_!!6000000002289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Default Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01RHjrEs1u7TXq3M6l3_!!6000000005990-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Default | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016pIeXz1pFC8owmR4Q_!!6000000005330-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | GitHub Light | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01US2b0g1VETCPAVWLA_!!6000000002621-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| Dracula | <img src="https://gw.alicdn.com/imgextra/i4/O1CN016htnWH20c3gd2LpUR_!!6000000006869-2-tps-1140-934.png" style="zoom:30%;text-align:center;margin: 0 auto;" /> | Google Code | <img src="https://gw.alicdn.com/imgextra/i1/O1CN01Ng29ab23iQ2BuYKz8_!!6000000007289-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |
| GitHub | <img src="https://gw.alicdn.com/imgextra/i4/O1CN01fFCRda1IQIQ9qDNqv_!!6000000000887-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> | Xcode | <img src="https://gw.alicdn.com/imgextra/i1/O1CN010E3QAi1Huh5o1E9LN_!!6000000000818-2-tps-1140-934.png" alt=" " style="zoom:30%;text-align:center;margin: 0 auto;" /> |

View File

@@ -56,6 +56,6 @@ If you need to change a decision or see all your settings, you have a couple of
For advanced users, it's helpful to know the exact order of operations for how trust is determined:
1. **IDE Trust Signal**: If you are using the [IDE Integration](/users/ide-integration/ide-integration), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority.
1. **IDE Trust Signal**: If you are using the [IDE Integration](../ide-integration/ide-integration), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority.
2. **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.qwen/trustedFolders.json` file.

View File

@@ -1,6 +1,7 @@
export default {
commands: 'Commands',
'sub-agents': 'SubAgents',
skills: 'Skills (Experimental)',
headless: 'Headless Mode',
checkpointing: {
display: 'hidden',
@@ -9,4 +10,5 @@ export default {
mcp: 'MCP',
'token-caching': 'Token Caching',
sandbox: 'Sandboxing',
language: 'i18n',
};

View File

@@ -1,3 +1,5 @@
# Approval Mode
Qwen Code offers three distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level.
## Permission Modes Comparison

View File

@@ -20,10 +20,11 @@ These commands help you save, restore, and summarize work progress.
| Command | Description | Usage Examples |
| ----------- | --------------------------------------------------------- | ------------------------------------ |
| `/init` | Analyze current directory and create initial context file | `/init` |
| `/summary` | Generate project summary based on conversation history | `/summary` |
| `/compress` | Replace chat history with summary to save Tokens | `/compress` |
| `/resume` | Resume a previous conversation session | `/resume` |
| `/restore` | Restore files to state before tool execution | `/restore` (list) or `/restore <ID>` |
| `/init` | Analyze current directory and create initial context file | `/init` |
### 1.2 Interface and Workspace Control
@@ -47,7 +48,7 @@ Commands specifically for controlling interface and output language.
| → `ui [language]` | Set UI interface language | `/language ui zh-CN` |
| → `output [language]` | Set LLM output language | `/language output Chinese` |
- Available UI languages: `zh-CN` (Simplified Chinese), `en-US` (English)
- Available built-in UI languages: `zh-CN` (Simplified Chinese), `en-US` (English), `ru-RU` (Russian), `de-DE` (German)
- Output language examples: `Chinese`, `English`, `Japanese`, etc.
### 1.4 Tool and Model Management
@@ -71,17 +72,16 @@ Commands for managing AI tools and models.
Commands for obtaining information and performing system settings.
| Command | Description | Usage Examples |
| --------------- | ----------------------------------------------- | ------------------------------------------------ |
| `/help` | Display help information for available commands | `/help` or `/?` |
| `/about` | Display version information | `/about` |
| `/stats` | Display detailed statistics for current session | `/stats` |
| `/settings` | Open settings editor | `/settings` |
| `/auth` | Change authentication method | `/auth` |
| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` |
| `/copy` | Copy last output content to clipboard | `/copy` |
| `/quit-confirm` | Show confirmation dialog before quitting | `/quit-confirm` (shortcut: press `Ctrl+C` twice) |
| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
| Command | Description | Usage Examples |
| ----------- | ----------------------------------------------- | -------------------------------- |
| `/help` | Display help information for available commands | `/help` or `/?` |
| `/about` | Display version information | `/about` |
| `/stats` | Display detailed statistics for current session | `/stats` |
| `/settings` | Open settings editor | `/settings` |
| `/auth` | Change authentication method | `/auth` |
| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` |
| `/copy` | Copy last output content to clipboard | `/copy` |
| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
### 1.6 Common Shortcuts

View File

@@ -189,21 +189,22 @@ qwen -p "Write code" --output-format stream-json --include-partial-messages | jq
Key command-line options for headless usage:
| Option | Description | Example |
| ---------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
| `--prompt`, `-p` | Run in headless mode | `qwen -p "query"` |
| `--output-format`, `-o` | Specify output format (text, json, stream-json) | `qwen -p "query" --output-format json` |
| `--input-format` | Specify input format (text, stream-json) | `qwen --input-format text --output-format stream-json` |
| `--include-partial-messages` | Include partial messages in stream-json output | `qwen -p "query" --output-format stream-json --include-partial-messages` |
| `--debug`, `-d` | Enable debug mode | `qwen -p "query" --debug` |
| `--all-files`, `-a` | Include all files in context | `qwen -p "query" --all-files` |
| `--include-directories` | Include additional directories | `qwen -p "query" --include-directories src,docs` |
| `--yolo`, `-y` | Auto-approve all actions | `qwen -p "query" --yolo` |
| `--approval-mode` | Set approval mode | `qwen -p "query" --approval-mode auto_edit` |
| `--continue` | Resume the most recent session for this project | `qwen --continue -p "Pick up where we left off"` |
| `--resume [sessionId]` | Resume a specific session (or choose interactively) | `qwen --resume 123e... -p "Finish the refactor"` |
| Option | Description | Example |
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------ |
| `--prompt`, `-p` | Run in headless mode | `qwen -p "query"` |
| `--output-format`, `-o` | Specify output format (text, json, stream-json) | `qwen -p "query" --output-format json` |
| `--input-format` | Specify input format (text, stream-json) | `qwen --input-format text --output-format stream-json` |
| `--include-partial-messages` | Include partial messages in stream-json output | `qwen -p "query" --output-format stream-json --include-partial-messages` |
| `--debug`, `-d` | Enable debug mode | `qwen -p "query" --debug` |
| `--all-files`, `-a` | Include all files in context | `qwen -p "query" --all-files` |
| `--include-directories` | Include additional directories | `qwen -p "query" --include-directories src,docs` |
| `--yolo`, `-y` | Auto-approve all actions | `qwen -p "query" --yolo` |
| `--approval-mode` | Set approval mode | `qwen -p "query" --approval-mode auto_edit` |
| `--continue` | Resume the most recent session for this project | `qwen --continue -p "Pick up where we left off"` |
| `--resume [sessionId]` | Resume a specific session (or choose interactively) | `qwen --resume 123e... -p "Finish the refactor"` |
| `--experimental-skills` | Enable experimental Skills (registers the `skill` tool) | `qwen --experimental-skills -p "What Skills are available?"` |
For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](/users/configuration/settings).
For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](../configuration/settings).
## Examples
@@ -276,7 +277,7 @@ tail -5 usage.log
## Resources
- [CLI Configuration](/users/configuration/settings#command-line-arguments) - Complete configuration guide
- [Authentication](/users/configuration/settings#environment-variables-for-api-access) - Setup authentication
- [Commands](/users/reference/cli-reference) - Interactive commands reference
- [Tutorials](/users/quickstart) - Step-by-step automation guides
- [CLI Configuration](../configuration/settings#command-line-arguments) - Complete configuration guide
- [Authentication](../configuration/settings#environment-variables-for-api-access) - Setup authentication
- [Commands](../features/commands) - Interactive commands reference
- [Tutorials](../quickstart) - Step-by-step automation guides

View File

@@ -0,0 +1,136 @@
# Internationalization (i18n) & Language
Qwen Code is built for multilingual workflows: it supports UI localization (i18n/l10n) in the CLI, lets you choose the assistant output language, and allows custom UI language packs.
## Overview
From a user point of view, Qwen Codes “internationalization” spans multiple layers:
| Capability / Setting | What it controls | Where stored |
| ------------------------ | ---------------------------------------------------------------------- | ---------------------------- |
| `/language ui` | Terminal UI text (menus, system messages, prompts) | `~/.qwen/settings.json` |
| `/language output` | Language the AI responds in (an output preference, not UI translation) | `~/.qwen/output-language.md` |
| Custom UI language packs | Overrides/extends built-in UI translations | `~/.qwen/locales/*.js` |
## UI Language
This is the CLIs UI localization layer (i18n/l10n): it controls the language of menus, prompts, and system messages.
### Setting the UI Language
Use the `/language ui` command:
```bash
/language ui zh-CN # Chinese
/language ui en-US # English
/language ui ru-RU # Russian
/language ui de-DE # German
```
Aliases are also supported:
```bash
/language ui zh # Chinese
/language ui en # English
/language ui ru # Russian
/language ui de # German
```
### Auto-detection
On first startup, Qwen Code detects your system locale and sets the UI language automatically.
Detection priority:
1. `QWEN_CODE_LANG` environment variable
2. `LANG` environment variable
3. System locale via JavaScript Intl API
4. Default: English
## LLM Output Language
The LLM output language controls what language the AI assistant responds in, regardless of what language you type your questions in.
### How It Works
The LLM output language is controlled by a rule file at `~/.qwen/output-language.md`. This file is automatically included in the LLM's context during startup, instructing it to respond in the specified language.
### Auto-detection
On first startup, if no `output-language.md` file exists, Qwen Code automatically creates one based on your system locale. For example:
- System locale `zh` creates a rule for Chinese responses
- System locale `en` creates a rule for English responses
- System locale `ru` creates a rule for Russian responses
- System locale `de` creates a rule for German responses
### Manual Setting
Use `/language output <language>` to change:
```bash
/language output Chinese
/language output English
/language output Japanese
/language output German
```
Any language name works. The LLM will be instructed to respond in that language.
> [!note]
>
> After changing the output language, restart Qwen Code for the change to take effect.
### File Location
```
~/.qwen/output-language.md
```
## Configuration
### Via Settings Dialog
1. Run `/settings`
2. Find "Language" under General
3. Select your preferred UI language
### Via Environment Variable
```bash
export QWEN_CODE_LANG=zh
```
This influences auto-detection on first startup (if you havent set a UI language and no `output-language.md` file exists yet).
## Custom Language Packs
For UI translations, you can create custom language packs in `~/.qwen/locales/`:
- Example: `~/.qwen/locales/es.js` for Spanish
- Example: `~/.qwen/locales/fr.js` for French
User directory takes precedence over built-in translations.
> [!tip]
>
> Contributions are welcome! If youd like to improve built-in translations or add new languages.
> For a concrete example, see [PR #1238: feat(i18n): add Russian language support](https://github.com/QwenLM/qwen-code/pull/1238).
### Language Pack Format
```javascript
// ~/.qwen/locales/es.js
export default {
Hello: 'Hola',
Settings: 'Configuracion',
// ... more translations
};
```
## Related Commands
- `/language` - Show current language settings
- `/language ui [lang]` - Set UI language
- `/language output <language>` - Set LLM output language
- `/settings` - Open settings dialog

View File

@@ -12,6 +12,7 @@ With MCP servers connected, you can ask Qwen Code to:
- Automate workflows (repeatable tasks exposed as tools/prompts)
> [!tip]
>
> If youre looking for the “one command to get started”, jump to [Quick start](#quick-start).
## Quick start
@@ -51,7 +52,8 @@ qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
```
> [!tip]
> For advanced configuration layers (system defaults/system settings and precedence rules), see [Settings](/users/configuration/settings).
>
> For advanced configuration layers (system defaults/system settings and precedence rules), see [Settings](../configuration/settings).
## Configure servers
@@ -64,6 +66,7 @@ qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
| `stdio` | Local process (scripts, CLIs, Docker) on your machine | `command`, `args` (+ optional `cwd`, `env`) |
> [!note]
>
> If a server supports both, prefer **HTTP** over **SSE**.
### Configure via `settings.json` vs `qwen mcp add`

View File

@@ -220,6 +220,6 @@ qwen -s -p "run shell command: mount | grep workspace"
## Related documentation
- [Configuration](/users/configuration/settings): Full configuration options.
- [Commands](/users/reference/cli-reference): Available commands.
- [Troubleshooting](/users/support/troubleshooting): General troubleshooting.
- [Configuration](../configuration/settings): Full configuration options.
- [Commands](../features/commands): Available commands.
- [Troubleshooting](../support/troubleshooting): General troubleshooting.

View File

@@ -0,0 +1,282 @@
# Agent Skills (Experimental)
> Create, manage, and share Skills to extend Qwen Codes capabilities.
This guide shows you how to create, use, and manage Agent Skills in **Qwen Code**. Skills are modular capabilities that extend the models effectiveness through organized folders containing instructions (and optionally scripts/resources).
> [!note]
>
> Skills are currently **experimental** and must be enabled with `--experimental-skills`.
## Prerequisites
- Qwen Code (recent version)
- Run with the experimental flag enabled:
```bash
qwen --experimental-skills
```
- Basic familiarity with Qwen Code ([Quickstart](../quickstart.md))
## What are Agent Skills?
Agent Skills package expertise into discoverable capabilities. Each Skill consists of a `SKILL.md` file with instructions that the model can load when relevant, plus optional supporting files like scripts and templates.
### How Skills are invoked
Skills are **model-invoked** — the model autonomously decides when to use them based on your request and the Skills description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command`).
### Benefits
- Extend Qwen Code for your workflows
- Share expertise across your team via git
- Reduce repetitive prompting
- Compose multiple Skills for complex tasks
## Create a Skill
Skills are stored as directories containing a `SKILL.md` file.
### Personal Skills
Personal Skills are available across all your projects. Store them in `~/.qwen/skills/`:
```bash
mkdir -p ~/.qwen/skills/my-skill-name
```
Use personal Skills for:
- Your individual workflows and preferences
- Experimental Skills youre developing
- Personal productivity helpers
### Project Skills
Project Skills are shared with your team. Store them in `.qwen/skills/` within your project:
```bash
mkdir -p .qwen/skills/my-skill-name
```
Use project Skills for:
- Team workflows and conventions
- Project-specific expertise
- Shared utilities and scripts
Project Skills can be checked into git and automatically become available to teammates.
## Write `SKILL.md`
Create a `SKILL.md` file with YAML frontmatter and Markdown content:
```yaml
---
name: your-skill-name
description: Brief description of what this Skill does and when to use it
---
# Your Skill Name
## Instructions
Provide clear, step-by-step guidance for Qwen Code.
## Examples
Show concrete examples of using this Skill.
```
### Field requirements
Qwen Code currently validates that:
- `name` is a non-empty string
- `description` is a non-empty string
Recommended conventions (not strictly enforced yet):
- Use lowercase letters, numbers, and hyphens in `name`
- Make `description` specific: include both **what** the Skill does and **when** to use it (key words users will naturally mention)
## Add supporting files
Create additional files alongside `SKILL.md`:
```text
my-skill/
├── SKILL.md (required)
├── reference.md (optional documentation)
├── examples.md (optional examples)
├── scripts/
│ └── helper.py (optional utility)
└── templates/
└── template.txt (optional template)
```
Reference these files from `SKILL.md`:
````markdown
For advanced usage, see [reference.md](reference.md).
Run the helper script:
```bash
python scripts/helper.py input.txt
```
````
## View available Skills
When `--experimental-skills` is enabled, Qwen Code discovers Skills from:
- Personal Skills: `~/.qwen/skills/`
- Project Skills: `.qwen/skills/`
To view available Skills, ask Qwen Code directly:
```text
What Skills are available?
```
Or inspect the filesystem:
```bash
# List personal Skills
ls ~/.qwen/skills/
# List project Skills (if in a project directory)
ls .qwen/skills/
# View a specific Skills content
cat ~/.qwen/skills/my-skill/SKILL.md
```
## Test a Skill
After creating a Skill, test it by asking questions that match your description.
Example: if your description mentions “PDF files”:
```text
Can you help me extract text from this PDF?
```
The model autonomously decides to use your Skill if it matches the request — you dont need to explicitly invoke it.
## Debug a Skill
If Qwen Code doesnt use your Skill, check these common issues:
### Make the description specific
Too vague:
```yaml
description: Helps with documents
```
Specific:
```yaml
description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDFs, forms, or document extraction.
```
### Verify file path
- Personal Skills: `~/.qwen/skills/<skill-name>/SKILL.md`
- Project Skills: `.qwen/skills/<skill-name>/SKILL.md`
```bash
# Personal
ls ~/.qwen/skills/my-skill/SKILL.md
# Project
ls .qwen/skills/my-skill/SKILL.md
```
### Check YAML syntax
Invalid YAML prevents the Skill metadata from loading correctly.
```bash
cat SKILL.md | head -n 15
```
Ensure:
- Opening `---` on line 1
- Closing `---` before Markdown content
- Valid YAML syntax (no tabs, correct indentation)
### View errors
Run Qwen Code with debug mode to see Skill loading errors:
```bash
qwen --experimental-skills --debug
```
## Share Skills with your team
You can share Skills through project repositories:
1. Add the Skill under `.qwen/skills/`
2. Commit and push
3. Teammates pull the changes and run with `--experimental-skills`
```bash
git add .qwen/skills/
git commit -m "Add team Skill for PDF processing"
git push
```
## Update a Skill
Edit `SKILL.md` directly:
```bash
# Personal Skill
code ~/.qwen/skills/my-skill/SKILL.md
# Project Skill
code .qwen/skills/my-skill/SKILL.md
```
Changes take effect the next time you start Qwen Code. If Qwen Code is already running, restart it to load the updates.
## Remove a Skill
Delete the Skill directory:
```bash
# Personal
rm -rf ~/.qwen/skills/my-skill
# Project
rm -rf .qwen/skills/my-skill
git commit -m "Remove unused Skill"
```
## Best practices
### Keep Skills focused
One Skill should address one capability:
- Focused: “PDF form filling”, “Excel analysis”, “Git commit messages”
- Too broad: “Document processing” (split into smaller Skills)
### Write clear descriptions
Help the model discover when to use Skills by including specific triggers:
```yaml
description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or .xlsx data.
```
### Test with your team
- Does the Skill activate when expected?
- Are the instructions clear?
- Are there missing examples or edge cases?

View File

@@ -16,16 +16,15 @@ The plugin **MUST** run a local HTTP server that implements the **Model Context
- **Endpoint:** The server should expose a single endpoint (e.g., `/mcp`) for all MCP communication.
- **Port:** The server **MUST** listen on a dynamically assigned port (i.e., listen on port `0`).
### 2. Discovery Mechanism: The Port File
### 2. Discovery Mechanism: The Lock File
For Qwen Code to connect, it needs to discover which IDE instance it's running in and what port your server is using. The plugin **MUST** facilitate this by creating a "discovery file."
For Qwen Code to connect, it needs to discover what port your server is using. The plugin **MUST** facilitate this by creating a "lock file" and setting the port environment variable.
- **How the CLI Finds the File:** The CLI determines the Process ID (PID) of the IDE it's running in by traversing the process tree. It then looks for a discovery file that contains this PID in its name.
- **File Location:** The file must be created in a specific directory: `os.tmpdir()/qwen/ide/`. Your plugin must create this directory if it doesn't exist.
- **How the CLI Finds the File:** The CLI reads the port from `QWEN_CODE_IDE_SERVER_PORT`, then reads `~/.qwen/ide/<PORT>.lock`. (Legacy fallbacks exist for older extensions; see note below.)
- **File Location:** The file must be created in a specific directory: `~/.qwen/ide/`. Your plugin must create this directory if it doesn't exist.
- **File Naming Convention:** The filename is critical and **MUST** follow the pattern:
`qwen-code-ide-server-${PID}-${PORT}.json`
- `${PID}`: The process ID of the parent IDE process. Your plugin must determine this PID and include it in the filename.
- `${PORT}`: The port your MCP server is listening on.
`<PORT>.lock`
- `<PORT>`: The port your MCP server is listening on.
- **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure:
```json
@@ -33,21 +32,20 @@ For Qwen Code to connect, it needs to discover which IDE instance it's running i
"port": 12345,
"workspacePath": "/path/to/project1:/path/to/project2",
"authToken": "a-very-secret-token",
"ideInfo": {
"name": "vscode",
"displayName": "VS Code"
}
"ppid": 1234,
"ideName": "VS Code"
}
```
- `port` (number, required): The port of the MCP server.
- `workspacePath` (string, required): A list of all open workspace root paths, delimited by the OS-specific path separator (`:` for Linux/macOS, `;` for Windows). The CLI uses this path to ensure it's running in the same project folder that's open in the IDE. If the CLI's current working directory is not a sub-directory of `workspacePath`, the connection will be rejected. Your plugin **MUST** provide the correct, absolute path(s) to the root of the open workspace(s).
- `authToken` (string, required): A secret token for securing the connection. The CLI will include this token in an `Authorization: Bearer <token>` header on all requests.
- `ideInfo` (object, required): Information about the IDE.
- `name` (string, required): A short, lowercase identifier for the IDE (e.g., `vscode`, `jetbrains`).
- `displayName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).
- `ppid` (number, required): The parent process ID of the IDE process.
- `ideName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).
- **Authentication:** To secure the connection, the plugin **MUST** generate a unique, secret token and include it in the discovery file. The CLI will then include this token in the `Authorization` header for all requests to the MCP server (e.g., `Authorization: Bearer a-very-secret-token`). Your server **MUST** validate this token on every request and reject any that are unauthorized.
- **Tie-Breaking with Environment Variables (Recommended):** For the most reliable experience, your plugin **SHOULD** both create the discovery file and set the `QWEN_CODE_IDE_SERVER_PORT` environment variable in the integrated terminal. The file serves as the primary discovery mechanism, but the environment variable is crucial for tie-breaking. If a user has multiple IDE windows open for the same workspace, the CLI uses the `QWEN_CODE_IDE_SERVER_PORT` variable to identify and connect to the correct window's server.
- **Environment Variables (Required):** Your plugin **MUST** set `QWEN_CODE_IDE_SERVER_PORT` in the integrated terminal so the CLI can locate the correct `<PORT>.lock` file.
**Legacy note:** For extensions older than v0.5.1, Qwen Code may fall back to reading JSON files in the system temp directory named `qwen-code-ide-server-<PID>.json` or `qwen-code-ide-server-<PORT>.json`. New integrations should not rely on these legacy files.
## II. The Context Interface

View File

@@ -2,7 +2,7 @@
Qwen Code can integrate with your IDE to provide a more seamless and context-aware experience. This integration allows the CLI to understand your workspace better and enables powerful features like native in-editor diffing.
Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](/users/ide-integration/ide-companion-spec).
Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](../ide-integration/ide-companion-spec).
## Features

View File

@@ -6,41 +6,14 @@
Use it to perform GitHub pull request reviews, triage issues, perform code analysis and modification, and more using [Qwen Code] conversationally (e.g., `@qwencoder fix this issue`) directly inside your GitHub repositories.
- [qwen-code-action](#qwen-code-action)
- [Overview](#overview)
- [Features](#features)
- [Quick Start](#quick-start)
- [1. Get a Qwen API Key](#1-get-a-qwen-api-key)
- [2. Add it as a GitHub Secret](#2-add-it-as-a-github-secret)
- [3. Update your .gitignore](#3-update-your-gitignore)
- [4. Choose a Workflow](#4-choose-a-workflow)
- [5. Try it out](#5-try-it-out)
- [Workflows](#workflows)
- [Qwen Code Dispatch](#qwen-code-dispatch)
- [Issue Triage](#issue-triage)
- [Pull Request Review](#pull-request-review)
- [Qwen Code CLI Assistant](#qwen-code-cli-assistant)
- [Configuration](#configuration)
- [Inputs](#inputs)
- [Outputs](#outputs)
- [Repository Variables](#repository-variables)
- [Secrets](#secrets)
- [Authentication](#authentication)
- [GitHub Authentication](#github-authentication)
- [Extensions](#extensions)
- [Best Practices](#best-practices)
- [Customization](#customization)
- [Contributing](#contributing)
## Features
- **Automation**: Trigger workflows based on events (e.g. issue opening) or schedules (e.g. nightly).
- **On-demand Collaboration**: Trigger workflows in issue and pull request
comments by mentioning the [Qwen Code CLI] (e.g., `@qwencoder /review`).
- **Extensible with Tools**: Leverage [Qwen Code] models' tool-calling capabilities to
interact with other CLIs like the [GitHub CLI] (`gh`).
comments by mentioning the [Qwen Code CLI](./features/commands) (e.g., `@qwencoder /review`).
- **Extensible with Tools**: Leverage [Qwen Code](../developers/tools/introduction.md) models' tool-calling capabilities to interact with other CLIs like the [GitHub CLI] (`gh`).
- **Customizable**: Use a `QWEN.md` file in your repository to provide
project-specific instructions and context to [Qwen Code CLI].
project-specific instructions and context to [Qwen Code CLI](./features/commands).
## Quick Start
@@ -48,7 +21,7 @@ Get started with Qwen Code CLI in your repository in just a few minutes:
### 1. Get a Qwen API Key
Obtain your API key from [DashScope] (Alibaba Cloud's AI platform)
Obtain your API key from [DashScope](https://help.aliyun.com/zh/model-studio/qwen-code) (Alibaba Cloud's AI platform)
### 2. Add it as a GitHub Secret
@@ -90,7 +63,7 @@ You have two options to set up a workflow:
**Option B: Manually copy workflows**
1. Copy the pre-built workflows from the [`examples/workflows`](./examples/workflows) directory to your repository's `.github/workflows` directory. Note: the `qwen-dispatch.yml` workflow must also be copied, which triggers the workflows to run.
1. Copy the pre-built workflows from the [`examples/workflows`](./common-workflow) directory to your repository's `.github/workflows` directory. Note: the `qwen-dispatch.yml` workflow must also be copied, which triggers the workflows to run.
### 5. Try it out
@@ -119,30 +92,19 @@ This action provides several pre-built workflows for different use cases. Each w
### Qwen Code Dispatch
This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to
the appropriate workflow based on the triggering event and the command provided
in the comment. For a detailed guide on how to set up the dispatch workflow, go
to the
[Qwen Code Dispatch workflow documentation](./examples/workflows/qwen-dispatch).
This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to the appropriate workflow based on the triggering event and the command provided in the comment. For a detailed guide on how to set up the dispatch workflow, go to the [Qwen Code Dispatch workflow documentation](./common-workflow).
### Issue Triage
This action can be used to triage GitHub Issues automatically or on a schedule.
For a detailed guide on how to set up the issue triage system, go to the
[GitHub Issue Triage workflow documentation](./examples/workflows/issue-triage).
This action can be used to triage GitHub Issues automatically or on a schedule. For a detailed guide on how to set up the issue triage system, go to the [GitHub Issue Triage workflow documentation](./examples/workflows/issue-triage).
### Pull Request Review
This action can be used to automatically review pull requests when they are
opened. For a detailed guide on how to set up the pull request review system,
go to the [GitHub PR Review workflow documentation](./examples/workflows/pr-review).
This action can be used to automatically review pull requests when they are opened. For a detailed guide on how to set up the pull request review system, go to the [GitHub PR Review workflow documentation](./common-workflow).
### Qwen Code CLI Assistant
This type of action can be used to invoke a general-purpose, conversational Qwen Code
AI assistant within the pull requests and issues to perform a wide range of
tasks. For a detailed guide on how to set up the general-purpose Qwen Code CLI workflow,
go to the [Qwen Code Assistant workflow documentation](./examples/workflows/qwen-assistant).
This type of action can be used to invoke a general-purpose, conversational Qwen Code AI assistant within the pull requests and issues to perform a wide range of tasks. For a detailed guide on how to set up the general-purpose Qwen Code CLI workflow, go to the [Qwen Code Assistant workflow documentation](./common-workflow).
## Configuration
@@ -222,8 +184,7 @@ To add a secret:
2. Enter the secret name and value.
3. Save.
For more information, refer to the
[official GitHub documentation on creating and using encrypted secrets][secrets].
For more information, refer to the [official GitHub documentation on creating and using encrypted secrets][secrets].
## Authentication
@@ -239,7 +200,7 @@ You can authenticate with GitHub in two ways:
authentication, we recommend creating a custom GitHub App.
For detailed setup instructions for both Qwen and GitHub authentication, go to the
[**Authentication documentation**](./docs/authentication.md).
[**Authentication documentation**](./configuration/auth).
## Extensions
@@ -247,7 +208,7 @@ The Qwen Code CLI can be extended with additional functionality through extensio
These extensions are installed from source from their GitHub repositories.
For detailed instructions on how to set up and configure extensions, go to the
[Extensions documentation](./docs/extensions.md).
[Extensions documentation](../developers/extensions/extension).
## Best Practices
@@ -258,20 +219,18 @@ Key recommendations include:
- **Securing Your Repository:** Implementing branch and tag protection, and restricting pull request approvers.
- **Monitoring and Auditing:** Regularly reviewing action logs and enabling OpenTelemetry for deeper insights into performance and behavior.
For a comprehensive guide on securing your repository and workflows, please refer to our [**Best Practices documentation**](./docs/best-practices.md).
For a comprehensive guide on securing your repository and workflows, please refer to our [**Best Practices documentation**](./common-workflow).
## Customization
Create a [QWEN.md] file in the root of your repository to provide
project-specific context and instructions to [Qwen Code CLI]. This is useful for defining
Create a QWEN.md file in the root of your repository to provide
project-specific context and instructions to [Qwen Code CLI](./common-workflow). This is useful for defining
coding conventions, architectural patterns, or other guidelines the model should
follow for a given repository.
## Contributing
Contributions are welcome! Check out the Qwen Code CLI
[**Contributing Guide**](./CONTRIBUTING.md) for more details on how to get
started.
Contributions are welcome! Check out the Qwen Code CLI **Contributing Guide** for more details on how to get started.
[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
[Qwen Code]: https://github.com/QwenLM/qwen-code

View File

@@ -4,7 +4,7 @@
<br/>
<video src="https://cloud.video.taobao.com/vod/JnvYMhUia2EKFAaiuErqNpzWE9mz3odG76vArAHNg94.mp4" controls width="800">
<video src="https://cloud.video.taobao.com/vod/IKKwfM-kqNI3OJjM_U8uMCSMAoeEcJhs6VNCQmZxUfk.mp4" controls width="800">
Your browser does not support the video tag.
</video>

View File

@@ -7,7 +7,7 @@
### Features
- **Native agent experience**: Integrated AI assistant panel within Zed's interface
- **Agent Control Protocol**: Full support for ACP enabling advanced IDE interactions
- **Agent Client Protocol**: Full support for ACP enabling advanced IDE interactions
- **File management**: @-mention files to add them to the conversation context
- **Conversation history**: Access to past conversations within Zed
@@ -32,7 +32,7 @@
"Qwen Code": {
"type": "custom",
"command": "qwen",
"args": ["--experimental-acp"],
"args": ["--acp"],
"env": {}
}
```

View File

@@ -1,4 +1,6 @@
# Qwen Code overview
[![@qwen-code/qwen-code downloads](https://img.shields.io/npm/dw/@qwen-code/qwen-code.svg)](https://npm-compare.com/@qwen-code/qwen-code)
[![@qwen-code/qwen-code version](https://img.shields.io/npm/v/@qwen-code/qwen-code.svg)](https://www.npmjs.com/package/@qwen-code/qwen-code)
> Learn about Qwen Code, Qwen's agentic coding tool that lives in your terminal and helps you turn ideas into code faster than ever before.
@@ -36,27 +38,27 @@ Select **Qwen OAuth (Free)** authentication and follow the prompts to log in. Th
what does this project do?
```
![](https://gw.alicdn.com/imgextra/i2/O1CN01XoPbZm1CrsZzvMQ6m_!!6000000000135-1-tps-772-646.gif)
![](https://cloud.video.taobao.com/vod/j7-QtQScn8UEAaEdiv619fSkk5p-t17orpDbSqKVL5A.mp4)
You'll be prompted to log in on first use. That's it! [Continue with Quickstart (5 mins) →](/users/quickstart)
You'll be prompted to log in on first use. That's it! [Continue with Quickstart (5 mins) →](./quickstart)
> [!tip]
>
> See [troubleshooting](/users/support/troubleshooting) if you hit issues.
> See [troubleshooting](./support/troubleshooting) if you hit issues.
> [!note]
>
> **New VS Code Extension (Beta)**: Prefer a graphical interface? Our new **VS Code extension** provides an easy-to-use native IDE experience without requiring terminal familiarity. Simply install from the marketplace and start coding with Qwen Code directly in your sidebar. You can search for **Qwen Code** in the VS Code Marketplace and download it.
> **New VS Code Extension (Beta)**: Prefer a graphical interface? Our new **VS Code extension** provides an easy-to-use native IDE experience without requiring terminal familiarity. Simply install from the marketplace and start coding with Qwen Code directly in your sidebar. Download and install the [Qwen Code Companion](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) now.
## What Qwen Code does for you
- **Build features from descriptions**: Tell Qwen Code what you want to build in plain language. It will make a plan, write the code, and ensure it works.
- **Debug and fix issues**: Describe a bug or paste an error message. Qwen Code will analyze your codebase, identify the problem, and implement a fix.
- **Navigate any codebase**: Ask anything about your team's codebase, and get a thoughtful answer back. Qwen Code maintains awareness of your entire project structure, can find up-to-date information from the web, and with [MCP](/users/features/mcp) can pull from external datasources like Google Drive, Figma, and Slack.
- **Navigate any codebase**: Ask anything about your team's codebase, and get a thoughtful answer back. Qwen Code maintains awareness of your entire project structure, can find up-to-date information from the web, and with [MCP](./features/mcp) can pull from external datasources like Google Drive, Figma, and Slack.
- **Automate tedious tasks**: Fix fiddly lint issues, resolve merge conflicts, and write release notes. Do all this in a single command from your developer machines, or automatically in CI.
## Why developers love Qwen Code
- **Works in your terminal**: Not another chat window. Not another IDE. Qwen Code meets you where you already work, with the tools you already love.
- **Takes action**: Qwen Code can directly edit files, run commands, and create commits. Need more? [MCP](/users/features/mcp) lets Qwen Code read your design docs in Google Drive, update your tickets in Jira, or use _your_ custom developer tooling.
- **Takes action**: Qwen Code can directly edit files, run commands, and create commits. Need more? [MCP](./features/mcp) lets Qwen Code read your design docs in Google Drive, update your tickets in Jira, or use _your_ custom developer tooling.
- **Unix philosophy**: Qwen Code is composable and scriptable. `tail -f app.log | qwen -p "Slack me if you see any anomalies appear in this log stream"` _works_. Your CI can run `qwen -p "If there are new text strings, translate them into French and raise a PR for @lang-fr-team to review"`.

View File

@@ -206,7 +206,7 @@ Here are the most important commands for daily use:
| → `output [language]` | Set LLM output language | `/language output Chinese` |
| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
See the [CLI reference](/users/reference/cli-reference) for a complete list of commands.
See the [CLI reference](./features/commands) for a complete list of commands.
## Pro tips for beginners
@@ -225,9 +225,9 @@ See the [CLI reference](/users/reference/cli-reference) for a complete list of c
3. build a webpage that allows users to see and edit their information
```
**Let Claude explore first**
**Let Qwen Code explore first**
- Before making changes, let Claude understand your code:
- Before making changes, let Qwen Code understand your code:
```
analyze the database schema

View File

@@ -23,7 +23,7 @@ When you authenticate using your qwen.ai account, these Terms of Service and Pri
- **Terms of Service:** Your use is governed by the [Qwen Terms of Service](https://qwen.ai/termsservice).
- **Privacy Notice:** The collection and use of your data is described in the [Qwen Privacy Policy](https://qwen.ai/privacypolicy).
For details about authentication setup, quotas, and supported features, see [Authentication Setup](/users/configuration/settings).
For details about authentication setup, quotas, and supported features, see [Authentication Setup](../configuration/settings).
## 2. If you are using OpenAI-Compatible API Authentication
@@ -37,7 +37,7 @@ Qwen Code supports various OpenAI-compatible providers. Please refer to your spe
## Usage Statistics and Telemetry
Qwen Code may collect anonymous usage statistics and [telemetry](/developers/development/telemetry) data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings.
Qwen Code may collect anonymous usage statistics and [telemetry](../../developers/development/telemetry) data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings.
### What Data is Collected
@@ -91,4 +91,4 @@ You can switch between Qwen OAuth and OpenAI-compatible API authentication at an
2. **Within the CLI**: Use the `/auth` command to reconfigure your authentication method
3. **Environment variables**: Set up `.env` files for automatic OpenAI-compatible API authentication
For detailed instructions, see the [Authentication Setup](/users/configuration/settings#environment-variables-for-api-access) documentation.
For detailed instructions, see the [Authentication Setup](../configuration/settings#environment-variables-for-api-access) documentation.

View File

@@ -31,7 +31,7 @@ This guide provides solutions to common issues and debugging tips, including top
1. In your home directory: `~/.qwen/settings.json`.
2. In your project's root directory: `./.qwen/settings.json`.
Refer to [Qwen Code Configuration](/users/configuration/settings) for more details.
Refer to [Qwen Code Configuration](../configuration/settings) for more details.
- **Q: Why don't I see cached token counts in my stats output?**
- A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (Qwen API key or Google Cloud Vertex AI) but not for OAuth users (such as Google Personal/Enterprise accounts like Google Gmail or Google Workspace, respectively). This is because the Qwen Code Assist API does not support cached content creation. You can still view your total token usage using the `/stats` command.
@@ -59,7 +59,7 @@ This guide provides solutions to common issues and debugging tips, including top
- **Error: "Operation not permitted", "Permission denied", or similar.**
- **Cause:** When sandboxing is enabled, Qwen Code may attempt operations that are restricted by your sandbox configuration, such as writing outside the project directory or system temp directory.
- **Solution:** Refer to the [Configuration: Sandboxing](/users/features/sandbox) documentation for more information, including how to customize your sandbox configuration.
- **Solution:** Refer to the [Configuration: Sandboxing](../features/sandbox) documentation for more information, including how to customize your sandbox configuration.
- **Qwen Code is not running in interactive mode in "CI" environments**
- **Issue:** Qwen Code does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g. `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment.
@@ -84,12 +84,12 @@ This guide provides solutions to common issues and debugging tips, including top
The Qwen Code uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation.
| Exit Code | Error Type | Description |
| --------- | -------------------------- | ------------------------------------------------------------ |
| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. |
| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) |
| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt). |
| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. |
| Exit Code | Error Type | Description |
| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. |
| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) |
| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt). |
| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. |
| 53 | `FatalTurnLimitedError` | The maximum number of conversational turns for the session was reached. (non-interactive mode only) |
## Debugging Tips

View File

@@ -24,6 +24,8 @@ export default tseslint.config(
'.integration-tests/**',
'packages/**/.integration-test/**',
'dist/**',
'docs-site/.next/**',
'docs-site/out/**',
],
},
eslint.configs.recommended,

View File

@@ -80,10 +80,11 @@ type PermissionHandler = (
/**
* Sets up an ACP test environment with all necessary utilities.
* @param useNewFlag - If true, uses --acp; if false, uses --experimental-acp (for backward compatibility testing)
*/
function setupAcpTest(
rig: TestRig,
options?: { permissionHandler?: PermissionHandler },
options?: { permissionHandler?: PermissionHandler; useNewFlag?: boolean },
) {
const pending = new Map<number, PendingRequest>();
let nextRequestId = 1;
@@ -95,9 +96,13 @@ function setupAcpTest(
const permissionHandler =
options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' }));
// Use --acp by default, but allow testing with --experimental-acp for backward compatibility
const acpFlag =
options?.useNewFlag !== false ? '--acp' : '--experimental-acp';
const agent = spawn(
'node',
[rig.bundlePath, '--experimental-acp', '--no-chat-recording'],
[rig.bundlePath, acpFlag, '--no-chat-recording'],
{
cwd: rig.testDir!,
stdio: ['pipe', 'pipe', 'pipe'],
@@ -621,3 +626,99 @@ function setupAcpTest(
}
});
});
(IS_SANDBOX ? describe.skip : describe)(
'acp flag backward compatibility',
() => {
it('should work with deprecated --experimental-acp flag and show warning', async () => {
const rig = new TestRig();
rig.setup('acp backward compatibility');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig, {
useNewFlag: false,
});
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// Verify deprecation warning is shown
const stderrOutput = stderr.join('');
expect(stderrOutput).toContain('--experimental-acp is deprecated');
expect(stderrOutput).toContain('Please use --acp instead');
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Verify functionality still works
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: 'Say hello.' }],
});
expect(promptResult).toBeDefined();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
it('should work with new --acp flag without warnings', async () => {
const rig = new TestRig();
rig.setup('acp new flag');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig, {
useNewFlag: true,
});
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// Verify no deprecation warning is shown
const stderrOutput = stderr.join('');
expect(stderrOutput).not.toContain('--experimental-acp is deprecated');
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Verify functionality works
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: 'Say hello.' }],
});
expect(promptResult).toBeDefined();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
},
);

View File

@@ -5,8 +5,6 @@
*/
import { describe, it, expect } from 'vitest';
import { existsSync } from 'node:fs';
import * as path from 'node:path';
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
describe('file-system', () => {
@@ -202,8 +200,8 @@ describe('file-system', () => {
const readAttempt = toolLogs.find(
(log) => log.toolRequest.name === 'read_file',
);
const writeAttempt = toolLogs.find(
(log) => log.toolRequest.name === 'write_file',
const editAttempt = toolLogs.find(
(log) => log.toolRequest.name === 'edit_file',
);
const successfulReplace = toolLogs.find(
(log) => log.toolRequest.name === 'replace' && log.toolRequest.success,
@@ -226,15 +224,15 @@ describe('file-system', () => {
// CRITICAL: Verify that no matter what the model did, it never successfully
// wrote or replaced anything.
if (writeAttempt) {
if (editAttempt) {
console.error(
'A write_file attempt was made when no file should be written.',
'A edit_file attempt was made when no file should be written.',
);
printDebugInfo(rig, result);
}
expect(
writeAttempt,
'write_file should not have been called',
editAttempt,
'edit_file should not have been called',
).toBeUndefined();
if (successfulReplace) {
@@ -245,12 +243,5 @@ describe('file-system', () => {
successfulReplace,
'A successful replace should not have occurred',
).toBeUndefined();
// Final verification: ensure the file was not created.
const filePath = path.join(rig.testDir!, fileName);
const fileExists = existsSync(filePath);
expect(fileExists, 'The non-existent file should not be created').toBe(
false,
);
});
});

View File

@@ -952,7 +952,8 @@ describe('Permission Control (E2E)', () => {
TEST_TIMEOUT,
);
it(
// FIXME: This test is flaky and sometimes fails with no tool calls.
it.skip(
'should allow read-only tools without restrictions',
async () => {
// Create test files for the model to read

View File

@@ -314,4 +314,88 @@ describe('System Control (E2E)', () => {
);
});
});
describe('supportedCommands API', () => {
it('should return list of supported slash commands', async () => {
const sessionId = crypto.randomUUID();
const generator = (async function* () {
yield {
type: 'user',
session_id: sessionId,
message: { role: 'user', content: 'Hello' },
parent_tool_use_id: null,
} as SDKUserMessage;
})();
const q = query({
prompt: generator,
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
model: 'qwen3-max',
debug: false,
},
});
try {
const result = await q.supportedCommands();
// Start consuming messages to trigger initialization
const messageConsumer = (async () => {
try {
for await (const _message of q) {
// Just consume messages
}
} catch (error) {
// Ignore errors from query being closed
if (error instanceof Error && error.message !== 'Query is closed') {
throw error;
}
}
})();
// Verify result structure
expect(result).toBeDefined();
expect(result).toHaveProperty('commands');
expect(Array.isArray(result?.['commands'])).toBe(true);
const commands = result?.['commands'] as string[];
// Verify default allowed built-in commands are present
expect(commands).toContain('init');
expect(commands).toContain('summary');
expect(commands).toContain('compress');
// Verify commands are sorted
const sortedCommands = [...commands].sort();
expect(commands).toEqual(sortedCommands);
// Verify all commands are strings
commands.forEach((cmd) => {
expect(typeof cmd).toBe('string');
expect(cmd.length).toBeGreaterThan(0);
});
await q.close();
await messageConsumer;
} catch (error) {
await q.close();
throw error;
}
});
it('should throw error when supportedCommands is called on closed query', async () => {
const q = query({
prompt: 'Hello',
options: {
...SHARED_TEST_OPTIONS,
cwd: testDir,
model: 'qwen3-max',
},
});
await q.close();
await expect(q.supportedCommands()).rejects.toThrow('Query is closed');
});
});
});

View File

@@ -5,8 +5,8 @@
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { writeFileSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestRig } from './test-helper.js';
// Windows skip (Option A: avoid infra scope)
@@ -121,21 +121,4 @@ d('BOM end-to-end integration', () => {
'BOM_OK UTF-32BE',
);
});
it('Can describe a PNG file', async () => {
const imagePath = resolve(
process.cwd(),
'docs/assets/gemini-screenshot.png',
);
const imageContent = readFileSync(imagePath);
const filename = 'gemini-screenshot.png';
writeFileSync(join(dir, filename), imageContent);
const prompt = `What is shown in the image ${filename}?`;
const output = await rig.run(prompt);
await rig.waitForToolCall('read_file');
const lower = output.toLowerCase();
// The response is non-deterministic, so we just check for some
// keywords that are very likely to be in the response.
expect(lower.includes('gemini')).toBeTruthy();
});
});

9855
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.5.0",
"version": "0.6.1",
"engines": {
"node": ">=20.0.0"
},
@@ -13,14 +13,11 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.1"
},
"scripts": {
"start": "cross-env node scripts/start.js",
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
"auth:npm": "npx google-artifactregistry-auth",
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
"auth": "npm run auth:npm && npm run auth:docker",
"generate": "node scripts/generate-git-commit-info.js",
"build": "node scripts/build.js",
"build-and-start": "npm run build && npm run start",
@@ -76,6 +73,7 @@
"LICENSE"
],
"devDependencies": {
"@types/chrome": "^0.1.32",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
"@types/minimatch": "^5.1.2",
@@ -95,7 +93,6 @@
"eslint-plugin-react-hooks": "^5.2.0",
"glob": "^10.5.0",
"globals": "^16.0.0",
"google-artifactregistry-auth": "^3.4.0",
"husky": "^9.1.7",
"json": "^11.0.0",
"lint-staged": "^16.1.6",

View File

@@ -0,0 +1 @@
kbpfhhpfobobomiighfkhojhmefogdgh

25
packages/chrome-extension/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Dependencies
node_modules/
# Build outputs
dist/
*.zip
# Logs
*.log
# OS files
.DS_Store
Thumbs.db
# IDE files
.idea/
.vscode/
*.swp
*.swo
# Temporary files
*.tmp
.temp/
packages/chrome-extension/extension/sidepanel/dist/

View File

@@ -0,0 +1,84 @@
# Installation Guide for Qwen CLI Chrome Extension
This document describes how to install the Qwen CLI Chrome Extension.
## Prerequisites
1. **Node.js**: Install from [nodejs.org](https://nodejs.org/) (version 18 or higher)
2. **Qwen CLI**: Install the Qwen CLI tool (optional but recommended for full functionality)
3. **Chrome Browser**: Version 88 or higher
## Installation Steps
### Method 1: Full Installation (Recommended)
```bash
cd packages/chrome-extension
npm run install:all
```
This command will:
1. Guide you through Chrome extension installation
2. Automatically configure the Native Host
3. Save the Extension ID for future use
4. Start the debugging environment
### Method 2: Component Installation
You can install components separately:
```bash
# Install Chrome extension only
npm run install:extension
# Configure Native Host only
npm run install:host
```
### Method 3: Manual Installation
#### Chrome Extension Installation
1. Open Chrome and navigate to `chrome://extensions/`
2. Enable "Developer mode" (toggle in top right)
3. Click "Load unpacked"
4. Select the `packages/chrome-extension/extension` folder
5. Note the Extension ID that appears (you'll need this for the next step)
#### Native Host Installation
The Native Messaging Host allows the Chrome extension to communicate with Qwen CLI.
For macOS/Linux:
```bash
cd packages/chrome-extension/native-host
./scripts/smart-install.sh
```
When prompted, enter your Chrome Extension ID.
For Windows:
1. Run Command Prompt as Administrator
2. Navigate to the `packages/chrome-extension/native-host` directory
3. Run the installation script: `install.bat`
4. Enter your Chrome Extension ID when prompted
## Verification
To verify the installation:
1. Run the development environment:
```bash
npm run dev
```
2. You should see Chrome launch with the extension installed and DevTools open.
3. Check that the extension appears in the Chrome toolbar.
## Updates
To update the host configuration (if you get a new extension ID):
```bash
npm run update:host
```

View File

@@ -0,0 +1,62 @@
# Quick Start Guide for Qwen CLI Chrome Extension
Get started quickly with the Qwen CLI Chrome Extension.
## Installation
1. **Prerequisites**: Make sure you have Node.js installed:
```bash
node --version
```
2. **Install the extension and native host**:
```bash
cd packages/chrome-extension
npm run install:all
```
## Running the Extension
1. **Start development mode**:
```bash
npm run dev
```
This will launch Chrome with the extension loaded and open DevTools.
2. **In Chrome**:
- Look for the Qwen CLI Chrome Extension icon in the toolbar
- Click the icon to open the popup interface
3. **Connect to Qwen CLI** (if installed):
- Click "Connect to Qwen CLI" in the extension popup
- Click "Start Qwen CLI" to launch the AI interface
## Basic Usage
- **Extract Page Content**: Click "Extract Page Data" to send the current page to Qwen
- **Take Screenshot**: Click "Capture Screenshot" to take and analyze a screenshot
- **Monitor Network**: Ask Qwen to "show me the network requests" to view recent network activity
- **View Console Logs**: Ask Qwen to "show me the console logs" to view browser console output
## Development
1. **Build the extension**:
```bash
npm run build
```
2. **Watch for changes during development**:
```bash
npm run build:ui:watch
```
3. **View native host logs**:
```bash
npm run logs
```
## Next Steps
- Check out the [Development Guide](docs/development.md) for more details on the architecture
- Read the [Debugging Guide](docs/debugging.md) if you encounter issues
- Learn about the [Architecture](docs/architecture.md) for deeper understanding

View File

@@ -0,0 +1,204 @@
# Qwen CLI Chrome Extension - Chrome Extension
A Chrome extension that bridges your browser with Qwen CLI, enabling AI-powered analysis and interaction with web content.
> This package is part of the [Qwen Code](https://github.com/QwenLM/qwen-code) mono repository.
## Features
- **Page Data Extraction**: Extract structured data from any webpage including text, links, images, and metadata
- **Screenshot Capture**: Capture and analyze screenshots with AI
- **Console & Network Monitoring**: Monitor console logs and network requests
- **Selected Text Processing**: Send selected text to Qwen CLI for processing
- **AI Analysis**: Leverage Qwen's AI capabilities to analyze web content
- **MCP Server Integration**: Support for multiple MCP (Model Context Protocol) servers
## Architecture
```
┌─────────────────────┐
│ Chrome Extension │
│ - Content Script │
│ - Background Worker│
│ - Popup UI │
└──────────┬──────────┘
Native Messaging
┌──────▼──────────┐
│ Native Host │
│ (Node.js) │
└──────┬──────────┘
┌──────▼──────────┐
│ Qwen CLI │
│ + MCP Servers │
└─────────────────┘
```
## Installation
### Prerequisites
1. **Node.js**: Install from [nodejs.org](https://nodejs.org/)
2. **Qwen CLI**: Install the Qwen CLI tool (required for full functionality)
3. **Chrome Browser**: Version 88 or higher
### Step 1: Install the Chrome Extension
1. Open Chrome and navigate to `chrome://extensions/`
2. Enable "Developer mode" (toggle in top right)
3. Click "Load unpacked"
4. Select the `chrome-extension/extension` folder
5. Note the Extension ID that appears (you'll need this for the next step)
### Step 2: Install the Native Messaging Host
The Native Messaging Host allows the Chrome extension to communicate with Qwen CLI.
#### macOS/Linux
```bash
cd chrome-extension/native-host
./install.sh
```
When prompted, enter your Chrome Extension ID.
#### Windows
1. Run Command Prompt as Administrator
2. Navigate to the `native-host` directory:
```cmd
cd chrome-extension\native-host
```
3. Run the installation script:
```cmd
install.bat
```
4. Enter your Chrome Extension ID when prompted
### Step 3: Configure Qwen CLI (Optional)
If you want to use MCP servers with the extension:
```bash
# Add chrome-devtools MCP server
qwen mcp add chrome-devtools
# Add other MCP servers as needed
qwen mcp add playwright-mcp
```
## Usage
### Basic Usage
1. Click the Qwen CLI Chrome Extension extension icon in Chrome
2. Click "Connect to Qwen CLI" to establish connection
3. Click "Start Qwen CLI" to launch the CLI process
4. Use the action buttons to:
- Extract and analyze page data
- Capture screenshots
- Send selected text to Qwen
- Monitor console and network logs
### Advanced Settings
In the popup's "Advanced Settings" section, you can configure:
- **MCP Servers**: Comma-separated list of MCP servers to load
- **HTTP Port**: Port for Qwen CLI HTTP server (default: 8080)
- **Auto-connect**: Automatically connect when opening the popup
### API Actions
The extension supports the following actions that can be sent to Qwen CLI:
- `analyze_page`: Analyze extracted page data
- `analyze_screenshot`: Analyze captured screenshot
- `ai_analyze`: Perform AI analysis on content
- `process_text`: Process selected text
- Custom actions based on your MCP server configurations
## Development
### Project Structure
```
chrome-extension/
├── extension/ # Chrome extension source
│ ├── manifest.json # Extension manifest
│ ├── background/ # Service worker
│ ├── content/ # Content scripts
│ ├── popup/ # Popup UI
│ └── icons/ # Extension icons
├── native-host/ # Native messaging host
│ ├── host.js # Node.js host script
│ ├── manifest.json # Native host manifest
│ └── install scripts # Platform-specific installers
└── docs/ # Documentation
```
### Building from Source
1. Clone the repository
2. No build step required - the extension uses vanilla JavaScript
3. Load the extension as unpacked in Chrome for development
### Testing
1. Enable Chrome Developer Tools
2. Check the extension's background page console for logs
3. Native host logs are written to:
- macOS/Linux: `/tmp/qwen-bridge-host.log`
- Windows: `%TEMP%\qwen-bridge-host.log`
## Troubleshooting
### Extension not connecting to Native Host
1. Verify Node.js is installed: `node --version`
2. Check that the Native Host is properly installed
3. Ensure the Extension ID in the manifest matches your actual extension
4. Check logs for errors
### Qwen CLI not starting
1. Verify Qwen CLI is installed: `qwen --version`
2. Check that Qwen CLI can run normally from terminal
3. Review Native Host logs for error messages
### No response from Qwen CLI
1. Ensure Qwen CLI server is running
2. Check the configured HTTP port is not in use
3. Verify MCP servers are properly configured
## Security Considerations
- The extension requires broad permissions to function properly
- Native Messaging Host runs with user privileges
- All communication between components uses structured JSON messages
- No sensitive data is stored; all processing is ephemeral
## Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
MIT License - See LICENSE file for details
## Support
For issues, questions, or feature requests:
- Open an issue on GitHub
- Check the logs for debugging information
- Ensure all prerequisites are properly installed

View File

@@ -0,0 +1,102 @@
/**
* esbuild configuration for Chrome Extension Side Panel React App
* Bundles React components with Tailwind CSS
*/
import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as path from 'path';
import postcss from 'postcss';
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
const isWatch = process.argv.includes('--watch');
const isProduction = process.argv.includes('--production');
/**
* Custom CSS plugin that processes CSS through PostCSS/Tailwind
* and injects it as inline JavaScript
*/
const cssInjectPlugin = {
name: 'css-inject',
setup(build) {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const cssPath = args.path;
let cssContent = await fs.promises.readFile(cssPath, 'utf8');
// Handle @import statements
const importRegex = /@import\s+['"]([^'"]+)['"]\s*;/g;
let match;
while ((match = importRegex.exec(cssContent)) !== null) {
const importPath = path.resolve(path.dirname(cssPath), match[1]);
if (fs.existsSync(importPath)) {
const importedContent = await fs.promises.readFile(importPath, 'utf8');
cssContent = cssContent.replace(match[0], importedContent);
}
}
// Process with PostCSS and Tailwind
const result = await postcss([
tailwindcss({
config: path.resolve(process.cwd(), 'config/tailwind.config.js'),
}),
autoprefixer,
]).process(cssContent, {
from: cssPath,
});
// Convert to JavaScript that injects CSS
const minifiedCss = isProduction
? result.css.replace(/\s+/g, ' ').trim()
: result.css;
const jsContent = `
(function() {
const style = document.createElement('style');
style.textContent = ${JSON.stringify(minifiedCss)};
document.head.appendChild(style);
})();
`;
return {
contents: jsContent,
loader: 'js',
};
});
},
};
async function build() {
const ctx = await esbuild.context({
entryPoints: ['src/sidepanel/index.tsx'],
bundle: true,
format: 'iife',
minify: isProduction,
sourcemap: !isProduction,
platform: 'browser',
outfile: 'extension/sidepanel/dist/sidepanel-app.js',
jsx: 'automatic',
define: {
'process.env.NODE_ENV': isProduction ? '"production"' : '"development"',
},
plugins: [cssInjectPlugin],
loader: {
'.tsx': 'tsx',
'.ts': 'ts',
},
});
if (isWatch) {
console.log('Watching for changes...');
await ctx.watch();
} else {
await ctx.rebuild();
await ctx.dispose();
console.log('Build complete!');
}
}
build().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,53 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/sidepanel/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
qwen: {
orange: '#615fff',
},
'clay-orange': '#4f46e5',
ivory: '#f5f5ff',
slate: '#141420',
green: '#6bcf7f',
success: '#74c991',
error: '#c74e39',
warning: '#e1c08d',
loading: 'var(--app-secondary-foreground)',
},
spacing: {
small: '4px',
medium: '8px',
large: '12px',
xlarge: '16px',
},
borderRadius: {
small: '4px',
medium: '6px',
large: '8px',
},
animation: {
'completion-menu-enter': 'completion-menu-enter 0.15s ease-out',
'pulse-slow': 'pulse 1.5s infinite',
'slide-up': 'slideUp 0.3s ease-out',
fadeIn: 'fadeIn 0.2s ease-in',
},
keyframes: {
'completion-menu-enter': {
'0%': { opacity: '0', transform: 'translateY(4px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideUp: {
'0%': { transform: 'translateY(100%)' },
'100%': { transform: 'translateY(0)' },
},
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
},
},
plugins: [],
};

View File

@@ -0,0 +1,253 @@
# API Reference for Qwen CLI Chrome Extension
This document provides reference for the APIs and message formats used in the Qwen CLI Chrome Extension.
## Extension to Native Host Messages
The extension communicates with the native host using the following message formats:
### Handshake
```
Request:
{
"type": "handshake",
"version": "1.0.0"
}
Response:
{
"type": "handshake_response",
"version": "1.0.0",
"qwenInstalled": boolean,
"qwenVersion": string,
"qwenStatus": "disconnected"|"connected"|"running"
}
```
### Start Qwen CLI
```
Request:
{
"type": "start_qwen",
"cwd": string,
"config": object (optional)
}
Response:
{
"success": boolean,
"data": object,
"error": string (if success is false)
}
```
### Send Prompt to Qwen CLI
```
Request:
{
"type": "qwen_prompt",
"text": string
}
Response:
{
"success": boolean,
"data": object,
"error": string (if success is false)
}
```
### Extract Page Data
```
Request:
{
"type": "EXTRACT_DATA"
}
Response:
{
"success": boolean,
"data": {
"url": string,
"title": string,
"content": {
"text": string,
"html": string,
"markdown": string
},
"links": array,
"images": array,
"forms": array
},
"error": string (if success is false)
}
```
## Browser MCP Tools
The extension provides the following MCP tools to Qwen CLI:
### browser_read_page
```
Description: Read the content of the current browser page
Input: {}
Output: {
"url": string,
"title": string,
"content": string,
"linksCount": number,
"imagesCount": number
}
```
### browser_capture_screenshot
```
Description: Capture a screenshot of the current browser tab
Input: {}
Output: {
"data": string (base64 encoded PNG),
"mimeType": "image/png"
}
```
### browser_get_network_logs
```
Description: Get network request logs from the current browser tab
Input: {}
Output: {
"text": string (JSON formatted network logs)
}
```
### browser_get_console_logs
```
Description: Get console logs from the current browser tab
Input: {}
Output: {
"text": string (formatted console logs)
}
```
## Internal Extension Messages
The extension components communicate internally using these message formats:
### Get Status
```
Request:
{
"type": "GET_STATUS"
}
Response:
{
"connected": boolean,
"status": string,
"availableCommands": array,
"mcpTools": array,
"internalTools": array
}
```
### Send Message
```
Request:
{
"type": "sendMessage",
"data": {
"text": string,
"cwd": string (optional)
}
}
Response:
{
"success": boolean,
"error": string (if success is false)
}
```
### Get Network Logs
```
Request:
{
"type": "GET_NETWORK_LOGS"
}
Response:
{
"success": boolean,
"data": array,
"error": string (if success is false)
}
```
## Event Types
The extension broadcasts various events:
### Status Update
```
{
"type": "STATUS_UPDATE",
"status": string
}
```
### Stream Start/End
```
{
"type": "streamStart"
}
```
or
```
{
"type": "streamEnd"
}
```
### Tool Progress
```
{
"type": "toolProgress",
"data": {
"name": string,
"stage": "start"|"end",
"ok": boolean,
"error": string (if applicable)
}
}
```
## Network Log Format
Network logs returned by the browser_get_network_logs tool have the following structure:
```
{
"method": string (e.g., "Network.requestWillBeSent"),
"params": {
"requestId": string,
"request": {
"url": string,
"method": string,
"headers": object
},
"response": {
"status": number,
"statusText": string,
"headers": object
},
"timestamp": number
}
}
```
## Error Handling
All API responses include error handling:
- Success responses include a `success: true` field and result data
- Error responses include a `success: false` field and an `error` string
- The native host logs detailed error information for debugging

View File

@@ -0,0 +1,146 @@
# Architecture Overview for Qwen CLI Chrome Extension
This document describes the architecture of the Qwen CLI Chrome Extension.
## Overview
The Qwen CLI Chrome Extension connects your browser with the Qwen CLI, enabling AI-powered analysis and interaction with web content. It uses the Chrome Native Messaging API to securely communicate with the native host process.
## System Architecture
```
┌─────────────────────┐
│ Chrome Browser │
│ ┌─────────────────┐│
│ │ Extension UI ││ ← Popup/Side panel interface
│ └─────────────────┘│
│ ┌─────────────────┐│
│ │ Content Script ││ ← Page content extraction
│ └─────────────────┘│
│ ┌─────────────────┐│
│ │ Background ││ ← Service worker handling
│ │ (Service Worker)││ messaging and logic
│ └─────────────────┘│
└──────────┬──────────┘
Native Messaging
┌──────▼──────────┐
│ Native Host │
│ (Node.js) │ ← Bridge between extension
└──────┬──────────┘ and Qwen CLI
┌──────▼──────────┐
│ Qwen CLI │
│ + MCP Servers │ ← AI processing and tools
└─────────────────┘
```
## Components
### 1. Extension UI (Popup/Side Panel)
The user interface of the extension provides:
- Connection management to Qwen CLI
- Action buttons for various features
- Status information
- Settings and configuration
### 2. Content Script
The content script runs on web pages and provides:
- Page content extraction
- Console log capture
- Element selection and highlighting
- Text selection utilities
- Direct DOM interaction
### 3. Background Script (Service Worker)
The background service worker handles:
- Communication with the native host
- Message routing between components
- Browser API interactions
- Network monitoring (via debugger API)
- State management
### 4. Native Host (Node.js)
The native host acts as a bridge between the extension and Qwen CLI:
- Implements the Native Messaging protocol
- Communicates with Qwen CLI using ACP (Agent Communication Protocol)
- Handles file system operations
- Manages MCP (Model Context Protocol) servers
- Provides browser-specific tools via HTTP bridge
### 5. Qwen CLI
The main AI processing component:
- Runs AI models and processes requests
- Manages MCP servers
- Provides tool access (shell commands, file operations, etc.)
## Security Architecture
The extension follows Chrome's security model:
1. **Native Messaging Security**: Communication between extension and native host is restricted by manifest permissions
2. **Content Security Policy**: Prevents XSS attacks and injection
3. **Sandboxed Execution**: Native host runs with user privileges, not elevated permissions
4. **Origin Restrictions**: Communication is limited to allowed origins
## Data Flow
### Page Analysis Request
1. User initiates "Analyze Page" from extension UI
2. Background script sends message to content script
3. Content script extracts page data (text, links, images, etc.)
4. Data is sent back to background script
5. Background script sends data to native host
6. Native host forwards to Qwen CLI
7. Qwen CLI processes and responds with AI analysis
8. Response flows back to extension UI
### Network Monitoring
1. Background script uses Chrome Debugger API to monitor network requests
2. Network events are captured and stored per tab
3. When requested, network logs are provided to Qwen CLI via native host
4. This allows AI to analyze API calls and network activity
## Communication Protocols
### Native Messaging Protocol
JSON-based messages exchanged between extension and native host:
```json
{
"type": "message_type",
"id": "request_id",
"data": { ... }
}
```
### ACP (Agent Communication Protocol)
Used between native host and Qwen CLI:
- JSON-RPC over stdio
- Content-Length framed messages
- Request/response with error handling
## Extension Permissions
The extension requires specific permissions for full functionality:
- `activeTab`: Access to current tab for content extraction
- `tabs`: Tab management and information
- `storage`: Local storage for settings and state
- `nativeMessaging`: Communication with native host
- `debugger`: Network request monitoring
- `webNavigation`: Navigation event monitoring
- `scripting`: Content script injection
- `cookies`: Cookie access for web automation
- `webRequest`: Network request monitoring
- `sidePanel`: Side panel UI support
- `host_permissions`: Access to all URLs

View File

@@ -0,0 +1,96 @@
# Debugging Guide for Qwen CLI Chrome Extension
This document outlines the debugging process for the Qwen CLI Chrome Extension.
## Debugging Setup
The extension provides several debugging options to help troubleshoot issues.
### Development Mode
To start the extension in development mode with debugging enabled:
```bash
npm run dev
```
This will:
- Launch Chrome with the extension loaded
- Open DevTools automatically
- Start a test server at http://localhost:3000
- Provide a test page for functionality verification
### Native Host Logging
The native host logs are stored at:
- **macOS/Linux**: `/tmp/qwen-bridge-host.log`
- **Windows**: `%TEMP%\qwen-bridge-host.log`
To monitor the logs in real-time:
```bash
npm run logs
```
Or directly:
```bash
tail -f /tmp/qwen-bridge-host.log
```
### Chrome Extension Debugging
1. Open Chrome Extensions page (`chrome://extensions/`)
2. Enable "Developer mode"
3. Find the Qwen CLI Chrome Extension extension
4. Click "Inspect views" on the service worker to open DevTools for background scripts
5. Use the popup/panel's DevTools for UI debugging
## Common Debugging Scenarios
### Connection Issues
If the extension can't connect to the native host:
1. Verify Node.js is installed: `node --version`
2. Check the native host installation: `./native-host/scripts/smart-install.sh`
3. Check logs: `/tmp/qwen-bridge-host.log`
4. Verify extension ID matches the one in native host manifest
### Qwen CLI Communication Issues
If the extension can't communicate with Qwen CLI:
1. Verify Qwen CLI is installed: `qwen --version`
2. Check that Qwen CLI is running when the extension tries to connect
3. Check the extension's console logs for error messages
4. Verify the MCP server configuration
### Content Script Issues
If content scripts aren't working properly:
1. Check the content script logs in the page's DevTools console
2. Verify the content script is properly injected
3. Check for CSP restrictions on the target page
## Debugging Scripts
The following scripts are available for debugging:
- `npm run dev`: Full development environment with Chrome auto-launch
- `npm run logs`: Tail the native host log file
- `npm run clean`: Clean all build artifacts and logs
- `npm run dev:chrome`: Start Chrome with extension loaded and DevTools open
## Troubleshooting Tips
### Check Extension Status
Check the extension's status in the extension popup or through the API.
### Verify Permissions
Ensure all required permissions are granted in the extension settings.
### Network Requests
Monitor network requests to ensure proper communication between components.
### Console Messages
Watch console messages in both the extension's background script and content scripts.

View File

@@ -0,0 +1,69 @@
# Development Guide for Qwen CLI Chrome Extension
This document outlines the development process for the Qwen CLI Chrome Extension.
## Directory Structure
```
packages/chrome-extension/
├── src/ # Source code
│ ├── background/ # Background script source
│ ├── content/ # Content script source
│ ├── sidepanel/ # Side panel React components
│ ├── common/ # Shared utilities
│ └── types/ # TypeScript definitions
├── extension/ # Build output (production-ready extension)
│ ├── background/
│ ├── content/
│ ├── popup/
│ ├── sidepanel/
│ ├── icons/
│ └── manifest.json
├── native-host/ # Native messaging host
│ ├── src/ # Source files
│ ├── dist/ # Built files
│ ├── scripts/ # Installation scripts
│ └── config/ # Configuration templates
├── docs/ # Documentation
├── scripts/ # Build and development scripts
├── test/ # Test files
├── config/ # Configuration files
├── README.md
├── DEVELOPMENT.md # This file
├── DEBUGGING.md
├── INSTALL.md
├── QUICK_START.md
└── package.json
```
## Development Setup
1. Install dependencies:
```bash
npm install
```
2. Start development server:
```bash
npm run dev
```
## Building
To build the extension:
```bash
npm run build
```
This will compile the source files and output the production-ready extension to the `extension/` directory.
## Testing
Unit tests are located in the `test/unit/` directory.
Integration tests are located in the `test/integration/` directory.
End-to-end tests are located in the `test/e2e/` directory.
Run all tests:
```bash
npm run test
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,480 @@
/**
* Content Script for Qwen CLI Chrome Extension
* Extracts data from web pages and communicates with background script
*/
if (window.__QWEN_BRIDGE_CONTENT_SCRIPT_LOADED__) {
console.debug('Qwen Bridge content script already loaded, skipping.');
} else {
window.__QWEN_BRIDGE_CONTENT_SCRIPT_LOADED__ = true;
// Data extraction functions
function extractPageData() {
const data = {
// Basic page info
url: window.location.href,
title: document.title,
domain: window.location.hostname,
path: window.location.pathname,
timestamp: new Date().toISOString(),
// Meta information
meta: {},
// Page content
content: {
text: '',
html: '',
markdown: ''
},
// Structured data
links: [],
images: [],
forms: [],
// Console logs
consoleLogs: [],
// Performance metrics
performance: {}
};
// Extract meta tags
document.querySelectorAll('meta').forEach(meta => {
const name = meta.getAttribute('name') || meta.getAttribute('property');
const content = meta.getAttribute('content');
if (name && content) {
data.meta[name] = content;
}
});
// Extract main content (try to find article or main element first)
const mainContent = document.querySelector('article, main, [role="main"]') || document.body;
data.content.text = extractTextContent(mainContent);
data.content.html = mainContent.innerHTML;
data.content.markdown = htmlToMarkdown(mainContent);
// Extract all links
document.querySelectorAll('a[href]').forEach(link => {
data.links.push({
text: link.textContent.trim(),
href: link.href,
target: link.target,
isExternal: isExternalLink(link.href)
});
});
// Extract all images
document.querySelectorAll('img').forEach(img => {
data.images.push({
src: img.src,
alt: img.alt,
title: img.title,
width: img.naturalWidth,
height: img.naturalHeight
});
});
// Extract form data (structure only, no sensitive data)
document.querySelectorAll('form').forEach(form => {
const formData = {
action: form.action,
method: form.method,
fields: []
};
form.querySelectorAll('input, textarea, select').forEach(field => {
formData.fields.push({
type: field.type || field.tagName.toLowerCase(),
name: field.name,
id: field.id,
placeholder: field.placeholder,
required: field.required
});
});
data.forms.push(formData);
});
// Get performance metrics
if (window.performance && window.performance.timing) {
const perf = window.performance.timing;
data.performance = {
loadTime: perf.loadEventEnd - perf.navigationStart,
domReady: perf.domContentLoadedEventEnd - perf.navigationStart,
firstPaint: getFirstPaintTime()
};
}
return data;
}
// Extract clean text content
function extractTextContent(element) {
// Clone the element to avoid modifying the original
const clone = element.cloneNode(true);
// Remove script and style elements
clone.querySelectorAll('script, style, noscript').forEach(el => el.remove());
// Get text content and clean it up
let text = clone.textContent || '';
// Remove excessive whitespace
text = text.replace(/\s+/g, ' ').trim();
// Limit length to prevent excessive data
const maxLength = 50000; // 50KB limit
if (text.length > maxLength) {
text = text.substring(0, maxLength) + '...';
}
return text;
}
// Simple HTML to Markdown converter
function htmlToMarkdown(element) {
const clone = element.cloneNode(true);
// Remove script and style elements
clone.querySelectorAll('script, style, noscript').forEach(el => el.remove());
let markdown = '';
const walker = document.createTreeWalker(
clone,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
null,
false
);
let node;
let listStack = [];
while (node = walker.nextNode()) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
markdown += text + ' ';
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
switch (node.tagName.toLowerCase()) {
case 'h1':
markdown += '\n# ' + node.textContent.trim() + '\n';
break;
case 'h2':
markdown += '\n## ' + node.textContent.trim() + '\n';
break;
case 'h3':
markdown += '\n### ' + node.textContent.trim() + '\n';
break;
case 'h4':
markdown += '\n#### ' + node.textContent.trim() + '\n';
break;
case 'h5':
markdown += '\n##### ' + node.textContent.trim() + '\n';
break;
case 'h6':
markdown += '\n###### ' + node.textContent.trim() + '\n';
break;
case 'p':
markdown += '\n' + node.textContent.trim() + '\n';
break;
case 'br':
markdown += '\n';
break;
case 'a':
const href = node.getAttribute('href');
const text = node.textContent.trim();
if (href) {
markdown += `[${text}](${href}) `;
}
break;
case 'img':
const src = node.getAttribute('src');
const alt = node.getAttribute('alt') || '';
if (src) {
markdown += `![${alt}](${src}) `;
}
break;
case 'ul':
case 'ol':
markdown += '\n';
listStack.push(node.tagName.toLowerCase());
break;
case 'li':
const listType = listStack[listStack.length - 1];
const prefix = listType === 'ol' ? '1. ' : '- ';
markdown += prefix + node.textContent.trim() + '\n';
break;
case 'code':
markdown += '`' + node.textContent + '`';
break;
case 'pre':
markdown += '\n```\n' + node.textContent + '\n```\n';
break;
case 'blockquote':
markdown += '\n> ' + node.textContent.trim() + '\n';
break;
case 'strong':
case 'b':
markdown += '**' + node.textContent + '**';
break;
case 'em':
case 'i':
markdown += '*' + node.textContent + '*';
break;
}
}
}
// Limit markdown length
const maxLength = 30000;
if (markdown.length > maxLength) {
markdown = markdown.substring(0, maxLength) + '...';
}
return markdown.trim();
}
// Check if link is external
function isExternalLink(url) {
try {
const link = new URL(url);
return link.hostname !== window.location.hostname;
} catch {
return false;
}
}
// Get first paint time
function getFirstPaintTime() {
if (window.performance && window.performance.getEntriesByType) {
const paintEntries = window.performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(entry => entry.name === 'first-paint');
return firstPaint ? firstPaint.startTime : null;
}
return null;
}
// Console log interceptor
const consoleLogs = [];
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info
};
// Intercept console methods
['log', 'error', 'warn', 'info'].forEach(method => {
console[method] = function(...args) {
// Store the log
consoleLogs.push({
type: method,
message: args.map(arg => {
try {
if (typeof arg === 'object') {
return JSON.stringify(arg);
}
return String(arg);
} catch {
return String(arg);
}
}).join(' '),
timestamp: new Date().toISOString(),
stack: new Error().stack
});
// Keep only last 100 logs to prevent memory issues
if (consoleLogs.length > 100) {
consoleLogs.shift();
}
// Call original console method
originalConsole[method].apply(console, args);
};
});
// Get selected text
function getSelectedText() {
return window.getSelection().toString();
}
// Highlight element on page
function highlightElement(selector) {
try {
const element = document.querySelector(selector);
if (element) {
// Store original style
const originalStyle = element.style.cssText;
// Apply highlight
element.style.cssText += `
outline: 3px solid #FF6B6B !important;
background-color: rgba(255, 107, 107, 0.1) !important;
transition: all 0.3s ease !important;
`;
// Remove highlight after 3 seconds
setTimeout(() => {
element.style.cssText = originalStyle;
}, 3000);
return true;
}
return false;
} catch (error) {
console.error('Failed to highlight element:', error);
return false;
}
}
// Execute custom JavaScript in page context
function executeInPageContext(code) {
try {
const script = document.createElement('script');
script.textContent = `
(function() {
try {
const result = ${code};
window.postMessage({
type: 'QWEN_BRIDGE_RESULT',
success: true,
result: result
}, '*');
} catch (error) {
window.postMessage({
type: 'QWEN_BRIDGE_RESULT',
success: false,
error: error.message
}, '*');
}
})();
`;
document.documentElement.appendChild(script);
script.remove();
return new Promise((resolve, reject) => {
const listener = (event) => {
if (event.data && event.data.type === 'QWEN_BRIDGE_RESULT') {
window.removeEventListener('message', listener);
if (event.data.success) {
resolve(event.data.result);
} else {
reject(new Error(event.data.error));
}
}
};
window.addEventListener('message', listener);
// Timeout after 5 seconds
setTimeout(() => {
window.removeEventListener('message', listener);
reject(new Error('Execution timeout'));
}, 5000);
});
} catch (error) {
return Promise.reject(error);
}
}
// Message listener for communication with background script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('Content script received message:', request);
switch (request.type) {
case 'EXTRACT_DATA':
// Extract and send page data
const pageData = extractPageData();
pageData.consoleLogs = consoleLogs;
sendResponse({
success: true,
data: pageData
});
break;
case 'GET_CONSOLE_LOGS':
// Get captured console logs
sendResponse({
success: true,
data: consoleLogs.slice() // Return a copy
});
break;
case 'GET_SELECTED_TEXT':
// Get currently selected text
sendResponse({
success: true,
data: getSelectedText()
});
break;
case 'HIGHLIGHT_ELEMENT':
// Highlight an element on the page
const highlighted = highlightElement(request.selector);
sendResponse({
success: highlighted
});
break;
case 'EXECUTE_CODE':
// Execute JavaScript in page context
executeInPageContext(request.code)
.then(result => {
sendResponse({
success: true,
data: result
});
})
.catch(error => {
sendResponse({
success: false,
error: error.message
});
});
return true; // Will respond asynchronously
case 'SCROLL_TO':
// Scroll to specific position
window.scrollTo({
top: request.y || 0,
left: request.x || 0,
behavior: request.smooth ? 'smooth' : 'auto'
});
sendResponse({ success: true });
break;
case 'QWEN_EVENT':
// Handle events from Qwen CLI
console.log('Qwen event received:', request.event);
// Could trigger UI updates or other actions based on event
break;
default:
sendResponse({
success: false,
error: 'Unknown request type'
});
}
});
// Notify background script that content script is loaded
chrome.runtime.sendMessage({
type: 'CONTENT_SCRIPT_LOADED',
url: window.location.href
}).catch(() => {
// Ignore errors if background script is not ready
});
// Export for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
extractPageData,
extractTextContent,
htmlToMarkdown,
getSelectedText,
highlightElement
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,61 @@
{
"manifest_version": 3,
"name": "Qwen CLI Chrome Extension",
"version": "1.0.0",
"description": "Bridge between Chrome browser and Qwen CLI for enhanced AI interactions",
"permissions": [
"activeTab",
"tabs",
"storage",
"nativeMessaging",
"debugger",
"webNavigation",
"scripting",
"cookies",
"webRequest",
"sidePanel"
],
"host_permissions": ["<all_urls>"],
"externally_connectable": {
"ids": ["aohjeidlpcjalobgghfkkehjbdhacjlo"],
"matches": ["https://*/*"]
},
"background": {
"service_worker": "background/service-worker.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content-script.js"],
"run_at": "document_idle"
}
],
"action": {
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"side_panel": {
"default_path": "sidepanel/sidepanel.html"
},
"options_ui": {
"page": "options/options.html",
"open_in_tab": true
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}

View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Qwen CLI Chrome Extension - Options</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
padding: 40px;
}
h1 {
color: #333;
font-size: 2em;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.section {
margin: 30px 0;
padding: 25px;
background: #f8f9fa;
border-radius: 10px;
}
.section h2 {
color: #667eea;
margin-bottom: 15px;
font-size: 1.3em;
}
.option-group {
margin: 20px 0;
}
label {
display: block;
margin-bottom: 8px;
color: #444;
font-weight: 500;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
input[type="checkbox"] {
margin-right: 8px;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.save-status {
display: inline-block;
margin-left: 15px;
color: #4caf50;
opacity: 0;
transition: opacity 0.3s;
}
.save-status.show {
opacity: 1;
}
.help-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.info-box {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.info-box h3 {
color: #1976d2;
margin-bottom: 8px;
}
.info-box p {
color: #555;
}
</style>
</head>
<body>
<div class="container">
<h1>⚙️ Qwen CLI Chrome Extension Settings</h1>
<p class="subtitle">Configure your Chrome extension and Qwen CLI integration</p>
<div class="section">
<h2>🔌 Connection Settings</h2>
<div class="option-group">
<label for="httpPort">HTTP Server Port</label>
<input type="number" id="httpPort" min="1024" max="65535" value="8080">
<p class="help-text">Port for Qwen CLI HTTP server (default: 8080)</p>
</div>
<div class="option-group">
<label for="mcpServers">MCP Servers</label>
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright">
<p class="help-text">Comma-separated list of MCP servers to load</p>
</div>
<div class="option-group">
<label class="checkbox-label">
<input type="checkbox" id="autoConnect">
<span>Auto-connect on startup</span>
</label>
<p class="help-text">Automatically connect to Qwen CLI when opening the popup</p>
</div>
</div>
<div class="section">
<h2>🎨 Display Settings</h2>
<div class="option-group">
<label class="checkbox-label">
<input type="checkbox" id="showNotifications">
<span>Show notifications</span>
</label>
<p class="help-text">Display desktop notifications for important events</p>
</div>
<div class="option-group">
<label class="checkbox-label">
<input type="checkbox" id="debugMode">
<span>Debug mode</span>
</label>
<p class="help-text">Show detailed debug information in console</p>
</div>
</div>
<div class="info-box">
<h3> Native Host Status</h3>
<p id="nativeHostStatus">Checking...</p>
</div>
<div class="info-box">
<h3>📍 Extension ID</h3>
<p id="extensionId">Loading...</p>
</div>
<button id="saveBtn">Save Settings</button>
<span class="save-status" id="saveStatus">✓ Settings saved</span>
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
<p style="text-align: center; color: #999; font-size: 14px;">
Qwen CLI Chrome Extension v1.0.0 |
<a href="https://github.com/QwenLM/qwen-code" style="color: #667eea;">GitHub</a> |
<a href="#" id="helpLink" style="color: #667eea;">Help</a>
</p>
</div>
</div>
<script src="options.js"></script>
</body>
</html>

View File

@@ -0,0 +1,80 @@
/**
* Options page script for Qwen CLI Chrome Extension
*/
// Load saved settings
async function loadSettings() {
const settings = await chrome.storage.local.get([
'httpPort',
'mcpServers',
'autoConnect',
'showNotifications',
'debugMode'
]);
// Set values in form
document.getElementById('httpPort').value = settings.httpPort || 8080;
document.getElementById('mcpServers').value = settings.mcpServers || '';
document.getElementById('autoConnect').checked = settings.autoConnect || false;
document.getElementById('showNotifications').checked = settings.showNotifications || false;
document.getElementById('debugMode').checked = settings.debugMode || false;
}
// Save settings
document.getElementById('saveBtn').addEventListener('click', async () => {
const settings = {
httpPort: parseInt(document.getElementById('httpPort').value) || 8080,
mcpServers: document.getElementById('mcpServers').value,
autoConnect: document.getElementById('autoConnect').checked,
showNotifications: document.getElementById('showNotifications').checked,
debugMode: document.getElementById('debugMode').checked
};
await chrome.storage.local.set(settings);
// Show saved status
const saveStatus = document.getElementById('saveStatus');
saveStatus.classList.add('show');
setTimeout(() => {
saveStatus.classList.remove('show');
}, 2000);
});
// Check Native Host status
async function checkNativeHostStatus() {
try {
// Try to send a message to check if Native Host is installed
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (response) => {
if (chrome.runtime.lastError) {
document.getElementById('nativeHostStatus').textContent =
'❌ Not installed - Please run install script';
} else if (response && response.connected) {
document.getElementById('nativeHostStatus').textContent =
'✅ Connected and running';
} else {
document.getElementById('nativeHostStatus').textContent =
'⚠️ Installed but not connected';
}
});
} catch (error) {
document.getElementById('nativeHostStatus').textContent =
'❌ Error checking status';
}
}
// Show extension ID
document.getElementById('extensionId').textContent = chrome.runtime.id;
// Help link
document.getElementById('helpLink').addEventListener('click', (e) => {
e.preventDefault();
chrome.tabs.create({
url: 'https://github.com/QwenLM/qwen-code/tree/main/packages/chrome-extension',
});
});
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
checkNativeHostStatus();
});

View File

@@ -0,0 +1,385 @@
/* Popup Styles for Qwen CLI Chrome Extension */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 400px;
min-height: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
min-height: 500px;
display: flex;
flex-direction: column;
}
/* Header */
.header {
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
}
.logo .icon {
width: 24px;
height: 24px;
}
.logo h1 {
font-size: 18px;
font-weight: 600;
}
/* Status Indicator */
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff4444;
animation: pulse 2s infinite;
}
.status-indicator.connected .status-dot {
background: #44ff44;
}
.status-indicator.connecting .status-dot {
background: #ffaa44;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Sections */
.section {
padding: 16px;
border-bottom: 1px solid #e5e5e5;
}
.section:last-of-type {
border-bottom: none;
}
.section h2 {
font-size: 14px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* Buttons */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.btn-secondary:hover:not(:disabled) {
background: #e5e5e5;
}
.btn-small {
padding: 4px 12px;
font-size: 12px;
}
.btn-icon {
width: 32px;
height: 32px;
padding: 4px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
}
.btn-icon:hover {
background: #f5f5f5;
}
.btn-icon svg {
width: 100%;
height: 100%;
stroke: #666;
}
/* Connection Section */
.connection-controls {
display: flex;
gap: 8px;
}
.error-message {
margin-top: 8px;
padding: 8px;
background: #fee;
color: #c00;
border-radius: 4px;
font-size: 12px;
}
/* Action Buttons */
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.action-btn {
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 12px;
color: #666;
}
.action-btn:hover:not(:disabled) {
border-color: #667eea;
background: #f8f9ff;
color: #667eea;
}
.action-btn:hover:not(:disabled) .action-icon {
stroke: #667eea;
}
.action-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.action-icon {
width: 24px;
height: 24px;
stroke: #999;
}
/* Response Section */
.response-container {
background: #f9f9f9;
border-radius: 8px;
overflow: hidden;
}
.response-header {
padding: 8px 12px;
background: #f0f0f0;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.response-type {
font-size: 12px;
font-weight: 600;
color: #666;
}
.response-content {
padding: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #333;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
/* Settings Section */
.settings-section details {
cursor: pointer;
}
.settings-section summary {
font-size: 14px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 4px 0;
user-select: none;
}
.settings-section summary:hover {
color: #667eea;
}
.settings-content {
margin-top: 12px;
}
.setting-item {
margin-bottom: 12px;
}
.setting-item label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #666;
}
.setting-item input[type="text"],
.setting-item input[type="number"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.setting-item input[type="checkbox"] {
margin-right: 8px;
}
/* Footer */
.footer {
padding: 12px 16px;
background: #f9f9f9;
border-top: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
color: #999;
margin-top: auto;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.version {
color: #bbb;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Loading state */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease;
}

View File

@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Qwen CLI Chrome Extension</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="logo">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h1>Qwen CLI Chrome Extension</h1>
</div>
<div class="status-indicator" id="statusIndicator">
<span class="status-dot"></span>
<span class="status-text">Disconnected</span>
</div>
</header>
<!-- Connection Section -->
<section class="section connection-section">
<h2>Connection</h2>
<div class="connection-controls">
<button id="connectBtn" class="btn btn-primary">
Connect to Qwen CLI
</button>
<button id="startQwenBtn" class="btn btn-secondary" disabled>
Start Qwen CLI
</button>
</div>
<div id="connectionError" class="error-message" style="display: none;"></div>
</section>
<!-- Actions Section -->
<section class="section actions-section">
<h2>Quick Actions</h2>
<div class="action-buttons">
<button id="extractDataBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Extract Page Data
</button>
<button id="captureScreenBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Capture Screenshot
</button>
<button id="analyzePageBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
Analyze with AI
</button>
<button id="getSelectedBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Send Selected Text
</button>
<button id="networkLogsBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
Network Logs
</button>
<button id="consoleLogsBtn" class="action-btn" disabled>
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Console Logs
</button>
</div>
</section>
<!-- Response Section -->
<section class="section response-section" id="responseSection" style="display: none;">
<h2>Response</h2>
<div class="response-container">
<div class="response-header">
<span id="responseType" class="response-type"></span>
<button id="copyResponseBtn" class="btn-icon" title="Copy to clipboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
<pre id="responseContent" class="response-content"></pre>
</div>
</section>
<!-- Settings Section -->
<section class="section settings-section">
<details>
<summary>Advanced Settings</summary>
<div class="settings-content">
<div class="setting-item">
<label for="mcpServers">MCP Servers:</label>
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright" />
</div>
<div class="setting-item">
<label for="httpPort">HTTP Port:</label>
<input type="number" id="httpPort" placeholder="8080" value="8080" />
</div>
<div class="setting-item">
<label for="autoConnect">
<input type="checkbox" id="autoConnect" />
Auto-connect on startup
</label>
</div>
<button id="saveSettingsBtn" class="btn btn-small">Save Settings</button>
</div>
</details>
</section>
<!-- Footer -->
<footer class="footer">
<a href="#" id="openOptionsBtn">Options</a>
<span></span>
<a href="#" id="helpBtn">Help</a>
<span></span>
<span class="version">v1.0.0</span>
</footer>
</div>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,523 @@
/**
* Popup Script for Qwen CLI Chrome Extension
* Handles UI interactions and communication with background script
*/
// UI Elements
const statusIndicator = document.getElementById('statusIndicator');
const statusText = statusIndicator.querySelector('.status-text');
const connectBtn = document.getElementById('connectBtn');
const startQwenBtn = document.getElementById('startQwenBtn');
const connectionError = document.getElementById('connectionError');
const responseSection = document.getElementById('responseSection');
const responseType = document.getElementById('responseType');
const responseContent = document.getElementById('responseContent');
const copyResponseBtn = document.getElementById('copyResponseBtn');
// Action buttons
const extractDataBtn = document.getElementById('extractDataBtn');
const captureScreenBtn = document.getElementById('captureScreenBtn');
const analyzePageBtn = document.getElementById('analyzePageBtn');
const getSelectedBtn = document.getElementById('getSelectedBtn');
const networkLogsBtn = document.getElementById('networkLogsBtn');
const consoleLogsBtn = document.getElementById('consoleLogsBtn');
// Settings
const mcpServersInput = document.getElementById('mcpServers');
const httpPortInput = document.getElementById('httpPort');
const autoConnectCheckbox = document.getElementById('autoConnect');
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
// Footer links
const openOptionsBtn = document.getElementById('openOptionsBtn');
const helpBtn = document.getElementById('helpBtn');
// State
let isConnected = false;
let qwenStatus = 'disconnected';
// Initialize popup
document.addEventListener('DOMContentLoaded', async () => {
await loadSettings();
await checkConnectionStatus();
// Auto-connect if enabled
const settings = await chrome.storage.local.get(['autoConnect']);
if (settings.autoConnect && !isConnected) {
connectToQwen();
}
});
// Load saved settings
async function loadSettings() {
const settings = await chrome.storage.local.get([
'mcpServers',
'httpPort',
'autoConnect'
]);
if (settings.mcpServers) {
mcpServersInput.value = settings.mcpServers;
}
if (settings.httpPort) {
httpPortInput.value = settings.httpPort;
}
if (settings.autoConnect !== undefined) {
autoConnectCheckbox.checked = settings.autoConnect;
}
}
// Save settings
saveSettingsBtn.addEventListener('click', async () => {
await chrome.storage.local.set({
mcpServers: mcpServersInput.value,
httpPort: parseInt(httpPortInput.value) || 8080,
autoConnect: autoConnectCheckbox.checked
});
saveSettingsBtn.textContent = 'Saved!';
setTimeout(() => {
saveSettingsBtn.textContent = 'Save Settings';
}, 2000);
});
// Check connection status
async function checkConnectionStatus() {
try {
const response = await chrome.runtime.sendMessage({ type: 'GET_STATUS' });
updateConnectionStatus(response.connected, response.status);
} catch (error) {
updateConnectionStatus(false, 'disconnected');
}
}
// Update UI based on connection status
function updateConnectionStatus(connected, status) {
isConnected = connected;
qwenStatus = status;
// Update status indicator
statusIndicator.classList.toggle('connected', connected);
statusIndicator.classList.toggle('connecting', status === 'connecting');
statusText.textContent = getStatusText(status);
// Update button states
connectBtn.textContent = connected ? 'Disconnect' : 'Connect to Qwen CLI';
connectBtn.classList.toggle('btn-danger', connected);
startQwenBtn.disabled = !connected || status === 'running';
// Enable/disable action buttons
const actionButtons = [
extractDataBtn,
captureScreenBtn,
analyzePageBtn,
getSelectedBtn,
networkLogsBtn,
consoleLogsBtn
];
actionButtons.forEach(btn => {
btn.disabled = !connected || status !== 'running';
});
}
// Get human-readable status text
function getStatusText(status) {
switch (status) {
case 'connected':
return 'Connected';
case 'running':
return 'Qwen CLI Running';
case 'connecting':
return 'Connecting...';
case 'disconnected':
return 'Disconnected';
case 'stopped':
return 'Qwen CLI Stopped';
default:
return 'Unknown';
}
}
// Connect button handler
connectBtn.addEventListener('click', () => {
if (isConnected) {
disconnectFromQwen();
} else {
connectToQwen();
}
});
// Connect to Qwen CLI
async function connectToQwen() {
updateConnectionStatus(false, 'connecting');
connectionError.style.display = 'none';
try {
const response = await chrome.runtime.sendMessage({ type: 'CONNECT' });
if (response.success) {
updateConnectionStatus(true, response.status);
} else {
throw new Error(response.error || 'Connection failed');
}
} catch (error) {
console.error('Connection error:', error);
connectionError.textContent = `Error: ${error.message}`;
connectionError.style.display = 'block';
updateConnectionStatus(false, 'disconnected');
}
}
// Disconnect from Qwen CLI
function disconnectFromQwen() {
// Simply close the popup to disconnect
// The native port will be closed when the extension unloads
updateConnectionStatus(false, 'disconnected');
window.close();
}
// Start Qwen CLI button handler
startQwenBtn.addEventListener('click', async () => {
startQwenBtn.disabled = true;
startQwenBtn.textContent = 'Starting...';
try {
const settings = await chrome.storage.local.get(['mcpServers', 'httpPort']);
const response = await chrome.runtime.sendMessage({
type: 'START_QWEN_CLI',
config: {
mcpServers: settings.mcpServers ? settings.mcpServers.split(',').map(s => s.trim()) : [],
httpPort: settings.httpPort || 8080
}
});
if (response.success) {
updateConnectionStatus(true, 'running');
showResponse('Qwen CLI Started', response.data || 'Successfully started');
} else {
throw new Error(response.error || 'Failed to start Qwen CLI');
}
} catch (error) {
console.error('Start error:', error);
connectionError.textContent = `Error: ${error.message}`;
connectionError.style.display = 'block';
} finally {
startQwenBtn.textContent = 'Start Qwen CLI';
}
});
// Extract page data button handler
extractDataBtn.addEventListener('click', async () => {
try {
showLoading('Extracting page data...');
const response = await chrome.runtime.sendMessage({
type: 'EXTRACT_PAGE_DATA'
});
if (response.success) {
// Send to Qwen CLI
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'analyze_page',
data: response.data
});
if (qwenResponse.success) {
showResponse('Page Analysis', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to extract data: ${error.message}`);
}
});
// Capture screenshot button handler
captureScreenBtn.addEventListener('click', async () => {
try {
showLoading('Capturing screenshot...');
const response = await chrome.runtime.sendMessage({
type: 'CAPTURE_SCREENSHOT'
});
if (response.success) {
// Send to Qwen CLI
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'analyze_screenshot',
data: {
screenshot: response.data,
url: (await chrome.tabs.query({ active: true, currentWindow: true }))[0].url
}
});
if (qwenResponse.success) {
showResponse('Screenshot Analysis', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to capture screenshot: ${error.message}`);
}
});
// Analyze page with AI button handler
analyzePageBtn.addEventListener('click', async () => {
try {
showLoading('Analyzing page with AI...');
// First extract page data
const extractResponse = await chrome.runtime.sendMessage({
type: 'EXTRACT_PAGE_DATA'
});
if (!extractResponse.success) {
throw new Error(extractResponse.error);
}
// Send to Qwen for AI analysis
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'ai_analyze',
data: {
pageData: extractResponse.data,
prompt: 'Please analyze this webpage and provide insights about its content, purpose, and any notable features.'
}
});
if (qwenResponse.success) {
showResponse('AI Analysis', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} catch (error) {
showError(`Analysis failed: ${error.message}`);
}
});
// Get selected text button handler
getSelectedBtn.addEventListener('click', async () => {
try {
showLoading('Getting selected text...');
// Get active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab) {
throw new Error('No active tab found');
}
// Check if we can access this page
if (tab.url && (tab.url.startsWith('chrome://') ||
tab.url.startsWith('chrome-extension://') ||
tab.url.startsWith('edge://') ||
tab.url.startsWith('about:'))) {
throw new Error('Cannot access this page (browser internal page)');
}
// Try to inject content script first
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content/content-script.js']
});
} catch (injectError) {
console.log('Script injection skipped:', injectError.message);
}
// Get selected text from content script
let response;
try {
response = await chrome.tabs.sendMessage(tab.id, {
type: 'GET_SELECTED_TEXT'
});
} catch (msgError) {
throw new Error('Cannot connect to page. Please refresh the page and try again.');
}
if (response.success && response.data) {
// Send to Qwen CLI
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'process_text',
data: {
text: response.data,
context: 'selected_text'
}
});
if (qwenResponse.success) {
showResponse('Selected Text Processed', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} else {
showError('No text selected. Please select some text on the page first.');
}
} catch (error) {
showError(`Failed to process selected text: ${error.message}`);
}
});
// Network logs button handler
networkLogsBtn.addEventListener('click', async () => {
try {
showLoading('Getting network logs...');
const response = await chrome.runtime.sendMessage({
type: 'GET_NETWORK_LOGS'
});
if (response.success) {
showResponse('Network Logs', JSON.stringify(response.data, null, 2));
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to get network logs: ${error.message}`);
}
});
// Console logs button handler
consoleLogsBtn.addEventListener('click', async () => {
try {
showLoading('Getting console logs...');
// Get active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab) {
throw new Error('No active tab found');
}
// Check if we can access this page
if (tab.url && (tab.url.startsWith('chrome://') ||
tab.url.startsWith('chrome-extension://') ||
tab.url.startsWith('edge://') ||
tab.url.startsWith('about:'))) {
throw new Error('Cannot access this page (browser internal page)');
}
// Try to inject content script first
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content/content-script.js']
});
} catch (injectError) {
console.log('Script injection skipped:', injectError.message);
}
// Get console logs from content script
let response;
try {
response = await chrome.tabs.sendMessage(tab.id, {
type: 'EXTRACT_DATA'
});
} catch (msgError) {
throw new Error('Cannot connect to page. Please refresh the page and try again.');
}
if (response.success) {
const consoleLogs = response.data.consoleLogs || [];
if (consoleLogs.length > 0) {
showResponse('Console Logs', JSON.stringify(consoleLogs, null, 2));
} else {
showResponse('Console Logs', 'No console logs captured');
}
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to get console logs: ${error.message}`);
}
});
// Copy response button handler
copyResponseBtn.addEventListener('click', () => {
const text = responseContent.textContent;
navigator.clipboard.writeText(text).then(() => {
const originalTitle = copyResponseBtn.title;
copyResponseBtn.title = 'Copied!';
setTimeout(() => {
copyResponseBtn.title = originalTitle;
}, 2000);
});
});
// Footer link handlers
openOptionsBtn.addEventListener('click', (e) => {
e.preventDefault();
// Use try-catch to handle potential errors
try {
chrome.runtime.openOptionsPage(() => {
if (chrome.runtime.lastError) {
// If opening options page fails, open it in a new tab as fallback
console.error('Error opening options page:', chrome.runtime.lastError);
chrome.tabs.create({
url: chrome.runtime.getURL('options/options.html')
});
}
});
} catch (error) {
console.error('Failed to open options page:', error);
// Fallback: open in new tab
chrome.tabs.create({
url: chrome.runtime.getURL('options/options.html')
});
}
});
helpBtn.addEventListener('click', (e) => {
e.preventDefault();
chrome.tabs.create({
url: 'https://github.com/QwenLM/qwen-code/tree/main/packages/chrome-extension',
});
});
// Helper functions
function showLoading(message) {
responseSection.style.display = 'block';
responseType.textContent = 'Loading';
responseContent.textContent = message;
responseSection.classList.add('loading');
}
function showResponse(type, content) {
responseSection.style.display = 'block';
responseType.textContent = type;
responseContent.textContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
responseSection.classList.remove('loading');
responseSection.classList.add('fade-in');
}
function showError(message) {
responseSection.style.display = 'block';
responseType.textContent = 'Error';
responseType.style.color = '#c00';
responseContent.textContent = message;
responseSection.classList.remove('loading');
}
// Listen for status updates from background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'STATUS_UPDATE') {
updateConnectionStatus(message.status !== 'disconnected', message.status);
} else if (message.type === 'QWEN_EVENT') {
// Handle events from Qwen CLI
console.log('Qwen event received:', message.event);
// Could update UI based on event
}
});

View File

@@ -0,0 +1,402 @@
/* Side Panel Styles for Qwen CLI Chrome Extension */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
width: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
min-height: 100%;
display: flex;
flex-direction: column;
}
/* Header */
.header {
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.logo {
display: flex;
align-items: center;
gap: 8px;
}
.logo .icon {
width: 24px;
height: 24px;
}
.logo h1 {
font-size: 18px;
font-weight: 600;
}
/* Status Indicator */
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff4444;
animation: pulse 2s infinite;
}
.status-indicator.connected .status-dot {
background: #44ff44;
}
.status-indicator.connecting .status-dot {
background: #ffaa44;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Sections */
.section {
padding: 16px;
border-bottom: 1px solid #e5e5e5;
}
.section:last-of-type {
border-bottom: none;
}
.section h2 {
font-size: 14px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* Buttons */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.btn-secondary:hover:not(:disabled) {
background: #e5e5e5;
}
.btn-small {
padding: 4px 12px;
font-size: 12px;
}
.btn-icon {
width: 32px;
height: 32px;
padding: 4px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
}
.btn-icon:hover {
background: #f5f5f5;
}
.btn-icon svg {
width: 100%;
height: 100%;
stroke: #666;
}
/* Connection Section */
.connection-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.error-message {
margin-top: 8px;
padding: 8px;
background: #fee;
color: #c00;
border-radius: 4px;
font-size: 12px;
}
/* Action Buttons */
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.action-btn {
padding: 12px;
border: 1px solid #e5e5e5;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 12px;
color: #666;
}
.action-btn:hover:not(:disabled) {
border-color: #667eea;
background: #f8f9ff;
color: #667eea;
}
.action-btn:hover:not(:disabled) .action-icon {
stroke: #667eea;
}
.action-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.action-icon {
width: 24px;
height: 24px;
stroke: #999;
}
/* Response Section */
.response-section {
flex: 1;
display: flex;
flex-direction: column;
}
.response-container {
background: #f9f9f9;
border-radius: 8px;
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
}
.response-header {
padding: 8px 12px;
background: #f0f0f0;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.response-type {
font-size: 12px;
font-weight: 600;
color: #666;
}
.response-content {
padding: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #333;
flex: 1;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
}
/* Settings Section */
.settings-section details {
cursor: pointer;
}
.settings-section summary {
font-size: 14px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 4px 0;
user-select: none;
}
.settings-section summary:hover {
color: #667eea;
}
.settings-content {
margin-top: 12px;
}
.setting-item {
margin-bottom: 12px;
}
.setting-item label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #666;
}
.setting-item input[type="text"],
.setting-item input[type="number"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.setting-item input[type="checkbox"] {
margin-right: 8px;
}
/* Footer */
.footer {
padding: 12px 16px;
background: #f9f9f9;
border-top: 1px solid #e5e5e5;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
color: #999;
margin-top: auto;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.version {
color: #bbb;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Loading state */
.loading {
position: relative;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease;
}

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Qwen Code</title>
<style>
/* Base reset and full-height container */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #1e1e1e;
color: #e0e0e0;
}
/* Loading state */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #333;
border-top-color: #615fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: #888;
font-size: 14px;
}
</style>
</head>
<body>
<div id="root">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Loading Qwen Code...</div>
</div>
</div>
<script>
// Inject extension URI for resource loading
try {
const extensionUri = chrome.runtime.getURL('');
document.body.setAttribute('data-extension-uri', extensionUri);
window.__EXTENSION_URI__ = extensionUri;
} catch (e) {
console.warn('Failed to inject extension URI:', e);
}
</script>
<script src="dist/sidepanel-app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,480 @@
/**
* Side Panel Script for Qwen CLI Chrome Extension
* Handles UI interactions and communication with background script
*/
// UI Elements
const statusIndicator = document.getElementById('statusIndicator');
const statusText = statusIndicator.querySelector('.status-text');
const connectBtn = document.getElementById('connectBtn');
const startQwenBtn = document.getElementById('startQwenBtn');
const connectionError = document.getElementById('connectionError');
const responseSection = document.getElementById('responseSection');
const responseType = document.getElementById('responseType');
const responseContent = document.getElementById('responseContent');
const copyResponseBtn = document.getElementById('copyResponseBtn');
// Action buttons
const extractDataBtn = document.getElementById('extractDataBtn');
const captureScreenBtn = document.getElementById('captureScreenBtn');
const analyzePageBtn = document.getElementById('analyzePageBtn');
const getSelectedBtn = document.getElementById('getSelectedBtn');
const networkLogsBtn = document.getElementById('networkLogsBtn');
const consoleLogsBtn = document.getElementById('consoleLogsBtn');
// Settings
const mcpServersInput = document.getElementById('mcpServers');
const httpPortInput = document.getElementById('httpPort');
const autoConnectCheckbox = document.getElementById('autoConnect');
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
// Footer links
const openOptionsBtn = document.getElementById('openOptionsBtn');
const helpBtn = document.getElementById('helpBtn');
// State
let isConnected = false;
let qwenStatus = 'disconnected';
// Initialize side panel
document.addEventListener('DOMContentLoaded', async () => {
await loadSettings();
await checkConnectionStatus();
// Auto-connect if enabled
const settings = await chrome.storage.local.get(['autoConnect']);
if (settings.autoConnect && !isConnected) {
connectToQwen();
}
});
// Load saved settings
async function loadSettings() {
const settings = await chrome.storage.local.get([
'mcpServers',
'httpPort',
'autoConnect'
]);
if (settings.mcpServers) {
mcpServersInput.value = settings.mcpServers;
}
if (settings.httpPort) {
httpPortInput.value = settings.httpPort;
}
if (settings.autoConnect !== undefined) {
autoConnectCheckbox.checked = settings.autoConnect;
}
}
// Save settings
saveSettingsBtn.addEventListener('click', async () => {
await chrome.storage.local.set({
mcpServers: mcpServersInput.value,
httpPort: parseInt(httpPortInput.value) || 8080,
autoConnect: autoConnectCheckbox.checked
});
saveSettingsBtn.textContent = 'Saved!';
setTimeout(() => {
saveSettingsBtn.textContent = 'Save Settings';
}, 2000);
});
// Check connection status
async function checkConnectionStatus() {
try {
const response = await chrome.runtime.sendMessage({ type: 'GET_STATUS' });
updateConnectionStatus(response.connected, response.status);
} catch (error) {
updateConnectionStatus(false, 'disconnected');
}
}
// Update UI based on connection status
function updateConnectionStatus(connected, status) {
isConnected = connected;
qwenStatus = status;
// Update status indicator
statusIndicator.classList.toggle('connected', connected);
statusIndicator.classList.toggle('connecting', status === 'connecting');
statusText.textContent = getStatusText(status);
// Update button states
connectBtn.textContent = connected ? 'Disconnect' : 'Connect to Qwen CLI';
connectBtn.classList.toggle('btn-danger', connected);
startQwenBtn.disabled = !connected || status === 'running';
// Enable/disable action buttons
const actionButtons = [
extractDataBtn,
captureScreenBtn,
analyzePageBtn,
getSelectedBtn,
networkLogsBtn,
consoleLogsBtn
];
actionButtons.forEach(btn => {
btn.disabled = !connected || status !== 'running';
});
}
// Get human-readable status text
function getStatusText(status) {
switch (status) {
case 'connected':
return 'Connected';
case 'running':
return 'Qwen CLI Running';
case 'connecting':
return 'Connecting...';
case 'disconnected':
return 'Disconnected';
case 'stopped':
return 'Qwen CLI Stopped';
default:
return 'Unknown';
}
}
// Connect button handler
connectBtn.addEventListener('click', () => {
if (isConnected) {
disconnectFromQwen();
} else {
connectToQwen();
}
});
// Connect to Qwen CLI
async function connectToQwen() {
updateConnectionStatus(false, 'connecting');
connectionError.style.display = 'none';
try {
const response = await chrome.runtime.sendMessage({ type: 'CONNECT' });
if (response.success) {
updateConnectionStatus(true, response.status);
} else {
throw new Error(response.error || 'Connection failed');
}
} catch (error) {
console.error('Connection error:', error);
connectionError.textContent = `Error: ${error.message}`;
connectionError.style.display = 'block';
updateConnectionStatus(false, 'disconnected');
}
}
// Disconnect from Qwen CLI
async function disconnectFromQwen() {
try {
await chrome.runtime.sendMessage({ type: 'DISCONNECT' });
} catch (error) {
console.error('Disconnect error:', error);
}
updateConnectionStatus(false, 'disconnected');
}
// Start Qwen CLI button handler
startQwenBtn.addEventListener('click', async () => {
startQwenBtn.disabled = true;
startQwenBtn.textContent = 'Starting...';
try {
const settings = await chrome.storage.local.get(['mcpServers', 'httpPort']);
const response = await chrome.runtime.sendMessage({
type: 'START_QWEN_CLI',
config: {
mcpServers: settings.mcpServers ? settings.mcpServers.split(',').map(s => s.trim()) : [],
httpPort: settings.httpPort || 8080
}
});
if (response.success) {
updateConnectionStatus(true, 'running');
showResponse('Qwen CLI Started', response.data || 'Successfully started');
} else {
throw new Error(response.error || 'Failed to start Qwen CLI');
}
} catch (error) {
console.error('Start error:', error);
connectionError.textContent = `Error: ${error.message}`;
connectionError.style.display = 'block';
} finally {
startQwenBtn.textContent = 'Start Qwen CLI';
}
});
// Extract page data button handler
extractDataBtn.addEventListener('click', async () => {
try {
showLoading('Extracting page data...');
const response = await chrome.runtime.sendMessage({
type: 'EXTRACT_PAGE_DATA'
});
if (response.success) {
// Send to Qwen CLI
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'analyze_page',
data: response.data
});
if (qwenResponse.success) {
showResponse('Page Analysis', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to extract data: ${error.message}`);
}
});
// Capture screenshot button handler
captureScreenBtn.addEventListener('click', async () => {
try {
showLoading('Capturing screenshot...');
const response = await chrome.runtime.sendMessage({
type: 'CAPTURE_SCREENSHOT'
});
if (response.success) {
// Send to Qwen CLI
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'analyze_screenshot',
data: {
screenshot: response.data,
url: (await chrome.tabs.query({ active: true, currentWindow: true }))[0].url
}
});
if (qwenResponse.success) {
showResponse('Screenshot Analysis', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to capture screenshot: ${error.message}`);
}
});
// Analyze page with AI button handler
analyzePageBtn.addEventListener('click', async () => {
try {
showLoading('Analyzing page with AI...');
// First extract page data
const extractResponse = await chrome.runtime.sendMessage({
type: 'EXTRACT_PAGE_DATA'
});
if (!extractResponse.success) {
throw new Error(extractResponse.error);
}
// Send to Qwen for AI analysis
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'ai_analyze',
data: {
pageData: extractResponse.data,
prompt: 'Please analyze this webpage and provide insights about its content, purpose, and any notable features.'
}
});
if (qwenResponse.success) {
showResponse('AI Analysis', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} catch (error) {
showError(`Analysis failed: ${error.message}`);
}
});
// Get selected text button handler
getSelectedBtn.addEventListener('click', async () => {
try {
showLoading('Getting selected text...');
// Get active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab) {
throw new Error('No active tab found');
}
// Get selected text from content script
const response = await chrome.tabs.sendMessage(tab.id, {
type: 'GET_SELECTED_TEXT'
});
if (response.success && response.data) {
// Send to Qwen CLI
const qwenResponse = await chrome.runtime.sendMessage({
type: 'SEND_TO_QWEN',
action: 'process_text',
data: {
text: response.data,
context: 'selected_text'
}
});
if (qwenResponse.success) {
showResponse('Selected Text Processed', qwenResponse.data);
} else {
throw new Error(qwenResponse.error);
}
} else {
showError('No text selected. Please select some text on the page first.');
}
} catch (error) {
showError(`Failed to process selected text: ${error.message}`);
}
});
// Network logs button handler
networkLogsBtn.addEventListener('click', async () => {
try {
showLoading('Getting network logs...');
const response = await chrome.runtime.sendMessage({
type: 'GET_NETWORK_LOGS'
});
if (response.success) {
showResponse('Network Logs', JSON.stringify(response.data, null, 2));
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to get network logs: ${error.message}`);
}
});
// Console logs button handler
consoleLogsBtn.addEventListener('click', async () => {
try {
showLoading('Getting console logs...');
// Get active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
if (!tab) {
throw new Error('No active tab found');
}
// Get console logs from content script
const response = await chrome.tabs.sendMessage(tab.id, {
type: 'EXTRACT_DATA'
});
if (response.success) {
const consoleLogs = response.data.consoleLogs || [];
if (consoleLogs.length > 0) {
showResponse('Console Logs', JSON.stringify(consoleLogs, null, 2));
} else {
showResponse('Console Logs', 'No console logs captured');
}
} else {
throw new Error(response.error);
}
} catch (error) {
showError(`Failed to get console logs: ${error.message}`);
}
});
// Copy response button handler
copyResponseBtn.addEventListener('click', () => {
const text = responseContent.textContent;
navigator.clipboard.writeText(text).then(() => {
const originalTitle = copyResponseBtn.title;
copyResponseBtn.title = 'Copied!';
setTimeout(() => {
copyResponseBtn.title = originalTitle;
}, 2000);
});
});
// Footer link handlers
openOptionsBtn.addEventListener('click', (e) => {
e.preventDefault();
// Use try-catch to handle potential errors
try {
chrome.runtime.openOptionsPage(() => {
if (chrome.runtime.lastError) {
// If opening options page fails, open it in a new tab as fallback
console.error('Error opening options page:', chrome.runtime.lastError);
chrome.tabs.create({
url: chrome.runtime.getURL('options/options.html')
});
}
});
} catch (error) {
console.error('Failed to open options page:', error);
// Fallback: open in new tab
chrome.tabs.create({
url: chrome.runtime.getURL('options/options.html')
});
}
});
helpBtn.addEventListener('click', (e) => {
e.preventDefault();
chrome.tabs.create({
url: 'https://github.com/QwenLM/qwen-code/tree/main/packages/chrome-extension',
});
});
// Helper functions
function showLoading(message) {
responseSection.style.display = 'block';
responseType.textContent = 'Loading';
responseContent.textContent = message;
responseSection.classList.add('loading');
}
function showResponse(type, content) {
responseSection.style.display = 'block';
responseType.textContent = type;
responseType.style.color = '#666';
responseContent.textContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
responseSection.classList.remove('loading');
responseSection.classList.add('fade-in');
}
function showError(message) {
responseSection.style.display = 'block';
responseType.textContent = 'Error';
responseType.style.color = '#c00';
responseContent.textContent = message;
responseSection.classList.remove('loading');
}
// Listen for status updates from background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'STATUS_UPDATE') {
updateConnectionStatus(message.status !== 'disconnected', message.status);
} else if (message.type === 'QWEN_EVENT') {
// Handle events from Qwen CLI
console.log('Qwen event received:', message.event);
// Could update UI based on event
}
});

View File

@@ -0,0 +1 @@
kbpfhhpfobobomiighfkhojhmefogdgh

View File

@@ -0,0 +1,9 @@
{
"name": "com.qwen.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "__PATH__",
"type": "stdio",
"allowed_origins": [
"__EXTENSION_ID__"
]
}

View File

@@ -0,0 +1,7 @@
#!/usr/local/bin/node
/**
* Native host entry point
* Delegates to the single source of truth in src/host.js
*/
require('./src/host.js');

View File

@@ -0,0 +1,7 @@
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "HOST_PATH",
"type": "stdio",
"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID/"]
}

View File

@@ -0,0 +1,23 @@
{
"name": "qwen-cli-bridge-host",
"version": "1.0.0",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"main": "host.js",
"scripts": {
"test": "node host.js --test"
},
"keywords": [
"chrome-extension",
"native-messaging",
"qwen",
"cli",
"bridge"
],
"author": "",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
},
"dependencies": {},
"devDependencies": {}
}

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# 添加必要的 PATH
export PATH="/usr/local/bin:/Users/yiliang/.npm-global/bin:$PATH"
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
HOST_SCRIPT="$SCRIPT_DIR/../host.js"
LOG="/var/folders/sy/9mwf8c3n2b57__q35fyxwdhh0000gp/T/qwen-wrapper.log"
echo "$(date): Wrapper started" >> "$LOG"
echo "$(date): PATH=$PATH" >> "$LOG"
# 使用完整路径运行 node
exec /usr/local/bin/node "$HOST_SCRIPT" 2>> "$LOG"

View File

@@ -0,0 +1,2 @@
@echo off
node "%~dp0..\\host.js" %*

View File

@@ -0,0 +1,99 @@
@echo off
setlocal enabledelayedexpansion
REM Qwen CLI Chrome Extension - Native Host Installation Script for Windows
REM This script installs the Native Messaging host for the Chrome extension
echo ========================================
echo Qwen CLI Chrome Extension - Native Host Installer
echo ========================================
echo.
REM Set variables
set HOST_NAME=com.qwen.cli.bridge
set SCRIPT_DIR=%~dp0
set HOST_SCRIPT=%SCRIPT_DIR%host.bat
set HOST_JS=%SCRIPT_DIR%..\host.js
REM Check if Node.js is installed
where node >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo Error: Node.js is not installed
echo Please install Node.js from https://nodejs.org/
pause
exit /b 1
)
REM Check if qwen CLI is installed
where qwen >nul 2>nul
if %ERRORLEVEL% NEQ 0 (
echo Warning: qwen CLI is not installed
echo Please install qwen CLI to use all features
echo Installation will continue...
echo.
)
REM Check if host files exist
if not exist "%HOST_SCRIPT%" (
echo Error: host.bat not found in %SCRIPT_DIR%
pause
exit /b 1
)
if not exist "%HOST_JS%" (
echo Error: host.js not found at %HOST_JS%
pause
exit /b 1
)
REM Get extension ID
set /p EXTENSION_ID="Enter your Chrome extension ID (found in chrome://extensions): "
if "%EXTENSION_ID%"=="" (
echo Error: Extension ID is required
pause
exit /b 1
)
REM Create manifest
set MANIFEST_FILE=%SCRIPT_DIR%manifest-windows.json
echo Creating manifest: %MANIFEST_FILE%
(
echo {
echo "name": "%HOST_NAME%",
echo "description": "Native messaging host for Qwen CLI Chrome Extension",
echo "path": "%HOST_SCRIPT:\=\\%",
echo "type": "stdio",
echo "allowed_origins": [
echo "chrome-extension://%EXTENSION_ID%/"
echo ]
echo }
) > "%MANIFEST_FILE%"
REM Add registry entry for Chrome
echo.
echo Adding registry entry for Chrome...
reg add "HKCU\Software\Google\Chrome\NativeMessagingHosts\%HOST_NAME%" /ve /t REG_SZ /d "%MANIFEST_FILE%" /f
if %ERRORLEVEL% EQU 0 (
echo.
echo ✅ Installation complete!
echo.
echo Next steps:
echo 1. Load the Chrome extension in chrome://extensions
echo 2. Enable 'Developer mode'
echo 3. Click 'Load unpacked' and select: %SCRIPT_DIR%..\extension
echo 4. Copy the extension ID and re-run this script if needed
echo 5. Click the extension icon and connect to Qwen CLI
echo.
echo Host manifest: %MANIFEST_FILE%
echo Log file location: %%TEMP%%\qwen-bridge-host.log
) else (
echo.
echo ❌ Failed to add registry entry
echo Please run this script as Administrator
)
echo.
pause

View File

@@ -0,0 +1,94 @@
#!/bin/bash
# Qwen CLI Chrome Extension - Native Host Installation Script for macOS/Linux
# This script installs the Native Messaging host for the Chrome extension
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
HOST_NAME="com.qwen.cli.bridge"
HOST_SCRIPT="$SCRIPT_DIR/../host.js"
echo "========================================"
echo "Qwen CLI Chrome Extension - Native Host Installer"
echo "========================================"
echo ""
# Detect OS
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
TARGET_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
BROWSER="Chrome"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux
TARGET_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
BROWSER="Chrome"
else
echo "Error: Unsupported operating system"
exit 1
fi
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed"
echo "Please install Node.js from https://nodejs.org/"
exit 1
fi
# Check if qwen CLI is installed
if ! command -v qwen &> /dev/null; then
echo "Warning: qwen CLI is not installed"
echo "Please install qwen CLI to use all features"
echo "Installation will continue..."
echo ""
fi
# Create target directory if it doesn't exist
echo "Creating directory: $TARGET_DIR"
mkdir -p "$TARGET_DIR"
# Ensure host script exists and is executable
if [ ! -f "$HOST_SCRIPT" ]; then
echo "Error: host.js not found at $HOST_SCRIPT"
exit 1
fi
chmod +x "$HOST_SCRIPT"
# Create the manifest file with the correct path
MANIFEST_FILE="$TARGET_DIR/$HOST_NAME.json"
echo "Creating manifest: $MANIFEST_FILE"
# Get the extension ID (you need to update this after installing the extension)
read -p "Enter your Chrome extension ID (found in chrome://extensions): " EXTENSION_ID
if [ -z "$EXTENSION_ID" ]; then
echo "Error: Extension ID is required"
exit 1
fi
# Create the manifest
cat > "$MANIFEST_FILE" << EOF
{
"name": "$HOST_NAME",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$HOST_SCRIPT",
"type": "stdio",
"allowed_origins": [
"chrome-extension://$EXTENSION_ID/"
]
}
EOF
echo ""
echo "✅ Installation complete!"
echo ""
echo "Next steps:"
echo "1. Load the Chrome extension in chrome://extensions"
echo "2. Enable 'Developer mode'"
echo "3. Click 'Load unpacked' and select: $SCRIPT_DIR/../extension"
echo "4. Copy the extension ID and re-run this script if needed"
echo "5. Click the extension icon and connect to Qwen CLI"
echo ""
echo "Host installed at: $MANIFEST_FILE"
echo "Log file location: /tmp/qwen-bridge-host.log"
echo ""

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# Native Host 包装脚本 - 确保 Node.js 环境正确设置
# 获取脚本所在目录
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 设置 Node.js 路径 (使用系统中的 node)
NODE_PATH="/usr/local/bin/node"
# 如果 /usr/local/bin/node 不存在,尝试其他位置
if [ ! -f "$NODE_PATH" ]; then
NODE_PATH=$(which node)
fi
# 执行 Native Host
# Prefer local CLI build if available and QWEN_CLI_PATH is not set
if [ -z "$QWEN_CLI_PATH" ]; then
LOCAL_CLI="$DIR/../../cli/dist/index.js"
if [ -f "$LOCAL_CLI" ]; then
export QWEN_CLI_PATH="$LOCAL_CLI"
fi
fi
exec "$NODE_PATH" "$DIR/../host.js"

View File

@@ -0,0 +1,307 @@
#!/bin/bash
# Qwen CLI Chrome Extension - 智能 Native Host 安装器
# 自动检测 Chrome 插件并配置 Native Host
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
HOST_NAME="com.qwen.cli.bridge"
HOST_SCRIPT="$SCRIPT_DIR/../host.js"
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ ║${NC}"
echo -e "${CYAN}║ 🔧 Qwen CLI Chrome Extension - Native Host 安装器 ║${NC}"
echo -e "${CYAN}║ ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
# 检测操作系统
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macOS"
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
EXTENSIONS_DIR="$HOME/Library/Application Support/Google/Chrome/Default/Extensions"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="Linux"
MANIFEST_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
EXTENSIONS_DIR="$HOME/.config/google-chrome/Default/Extensions"
else
echo -e "${RED}✗ 不支持的操作系统${NC}"
exit 1
fi
echo -e "${BLUE}检测到系统:${NC} $OS"
echo ""
# 检查 Node.js
echo -e "${BLUE}检查依赖...${NC}"
if ! command -v node &> /dev/null; then
echo -e "${RED}✗ Node.js 未安装${NC}"
echo -e " 请访问 https://nodejs.org 安装 Node.js"
exit 1
fi
echo -e "${GREEN}${NC} Node.js $(node --version)"
# 尝试自动检测扩展 ID
echo -e "\n${BLUE}查找已安装的 Qwen CLI Chrome Extension 扩展...${NC}"
EXTENSION_ID=""
AUTO_DETECTED=false
# 方法1: 从 Chrome 扩展目录查找
if [[ -d "$EXTENSIONS_DIR" ]]; then
for ext_id in "$EXTENSIONS_DIR"/*; do
if [[ -d "$ext_id" ]]; then
ext_id_name=$(basename "$ext_id")
# 检查最新版本目录
for version_dir in "$ext_id"/*; do
if [[ -f "$version_dir/manifest.json" ]]; then
# 检查是否是我们的扩展
if grep -q "Qwen CLI Chrome Extension" "$version_dir/manifest.json" 2>/dev/null; then
EXTENSION_ID="$ext_id_name"
AUTO_DETECTED=true
echo -e "${GREEN}${NC} 自动检测到扩展 ID: ${CYAN}$EXTENSION_ID${NC}"
break 2
fi
fi
done
fi
done
fi
# 方法2: 检查之前保存的 ID
if [[ -z "$EXTENSION_ID" && -f "$SCRIPT_DIR/../.extension-id" ]]; then
EXTENSION_ID=$(cat "$SCRIPT_DIR/../.extension-id")
echo -e "${GREEN}${NC} 使用保存的扩展 ID: ${CYAN}$EXTENSION_ID${NC}"
AUTO_DETECTED=true
fi
# 如果自动检测失败,提供选项
if [[ -z "$EXTENSION_ID" ]]; then
echo -e "${YELLOW}⚠️ 未能自动检测到扩展${NC}"
echo ""
echo -e "请选择:"
echo -e " ${CYAN}1)${NC} 我已经安装了扩展(输入扩展 ID"
echo -e " ${CYAN}2)${NC} 我还没有安装扩展(通用配置)"
echo -e " ${CYAN}3)${NC} 打开 Chrome 扩展页面查看"
echo ""
read -p "选择 (1/2/3): " CHOICE
case $CHOICE in
1)
echo ""
echo -e "${YELLOW}请输入扩展 ID:${NC}"
echo -e "${CYAN}提示: 在 chrome://extensions 页面找到 Qwen CLI Chrome ExtensionID 在扩展卡片上${NC}"
read -p "> " EXTENSION_ID
if [[ -n "$EXTENSION_ID" ]]; then
# 保存 ID 供以后使用
echo "$EXTENSION_ID" > "$SCRIPT_DIR/../.extension-id"
echo -e "${GREEN}${NC} 扩展 ID 已保存"
fi
;;
2)
echo -e "\n${CYAN}将使用通用配置(允许所有开发扩展)${NC}"
EXTENSION_ID="*"
;;
3)
echo -e "\n${CYAN}正在打开 Chrome 扩展页面...${NC}"
open "chrome://extensions" 2>/dev/null || xdg-open "chrome://extensions" 2>/dev/null || echo "请手动打开 chrome://extensions"
echo ""
echo -e "${YELLOW}找到 Qwen CLI Chrome Extension 扩展后,输入其 ID:${NC}"
read -p "> " EXTENSION_ID
if [[ -n "$EXTENSION_ID" && "$EXTENSION_ID" != "*" ]]; then
echo "$EXTENSION_ID" > "$SCRIPT_DIR/../.extension-id"
fi
;;
*)
echo -e "${RED}无效的选择${NC}"
exit 1
;;
esac
fi
# 创建 Native Host 目录
echo -e "\n${BLUE}配置 Native Host...${NC}"
mkdir -p "$MANIFEST_DIR"
# 创建 manifest 文件
MANIFEST_FILE="$MANIFEST_DIR/$HOST_NAME.json"
if [[ "$EXTENSION_ID" == "*" ]]; then
# 通用配置
cat > "$MANIFEST_FILE" << EOF
{
"name": "$HOST_NAME",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$HOST_SCRIPT",
"type": "stdio",
"allowed_origins": [
"chrome-extension://*/"
]
}
EOF
echo -e "${GREEN}${NC} Native Host 已配置(通用模式)"
else
# 特定扩展 ID 配置
cat > "$MANIFEST_FILE" << EOF
{
"name": "$HOST_NAME",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$HOST_SCRIPT",
"type": "stdio",
"allowed_origins": [
"chrome-extension://$EXTENSION_ID/",
"chrome-extension://*/"
]
}
EOF
echo -e "${GREEN}${NC} Native Host 已配置(扩展 ID: $EXTENSION_ID"
fi
# 验证配置
echo -e "\n${BLUE}验证配置...${NC}"
# 检查 host.js 是否存在
if [[ ! -f "$HOST_SCRIPT" ]]; then
echo -e "${RED}✗ host.js 文件不存在${NC}"
exit 1
fi
# 确保 host.js 可执行
chmod +x "$HOST_SCRIPT"
echo -e "${GREEN}${NC} host.js 已设置为可执行"
# 检查 manifest 文件
if [[ -f "$MANIFEST_FILE" ]]; then
echo -e "${GREEN}${NC} Manifest 文件已创建: $MANIFEST_FILE"
else
echo -e "${RED}✗ Manifest 文件创建失败${NC}"
exit 1
fi
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ ✅ Native Host 安装成功! ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
# 显示下一步
if [[ "$AUTO_DETECTED" == true ]]; then
echo -e "${CYAN}检测到扩展已安装,你可以直接使用了!${NC}"
echo ""
echo -e "使用方法:"
echo -e " 1. 点击 Chrome 工具栏的扩展图标"
echo -e " 2. 点击 'Connect to Qwen CLI'"
echo -e " 3. 开始使用各项功能"
else
echo -e "${YELLOW}下一步:${NC}"
echo -e " 1. 在 Chrome 中打开 ${CYAN}chrome://extensions/${NC}"
echo -e " 2. 开启${CYAN}「开发者模式」${NC}(右上角)"
echo -e " 3. 点击${CYAN}「加载已解压的扩展程序」${NC}"
echo -e " 4. 选择目录: ${CYAN}$SCRIPT_DIR/../extension${NC}"
echo -e " 5. 安装完成后,重新运行此脚本以更新配置"
fi
echo ""
echo -e "${CYAN}提示:${NC}"
echo -e " • 如需重新配置,随时可以重新运行此脚本"
echo -e " • 日志文件位置: /tmp/qwen-bridge-host.log"
echo -e " • 如遇问题,请查看: $SCRIPT_DIR/../docs/debugging.md"
echo ""
# 询问是否测试连接
if [[ "$AUTO_DETECTED" == true ]]; then
echo -e "${CYAN}是否测试 Native Host 连接?(y/n)${NC}"
read -p "> " TEST_CONNECTION
if [[ "$TEST_CONNECTION" == "y" ]] || [[ "$TEST_CONNECTION" == "Y" ]]; then
echo -e "\n${BLUE}测试连接...${NC}"
# 创建测试脚本
cat > /tmp/test-native-host.js << 'EOF'
const chrome = {
runtime: {
connectNative: () => {
console.log("Chrome API not available in Node.js environment");
console.log("请在 Chrome 扩展中测试连接");
}
}
};
// 直接测试 host.js
const { spawn } = require('child_process');
const path = require('path');
const hostPath = process.argv[2];
if (!hostPath) {
console.error("Missing host path");
process.exit(1);
}
console.log("Testing host at:", hostPath);
const host = spawn('node', [hostPath], {
stdio: ['pipe', 'pipe', 'pipe']
});
// 发送测试消息
const testMessage = JSON.stringify({ type: 'handshake', version: '1.0.0' });
const length = Buffer.allocUnsafe(4);
length.writeUInt32LE(Buffer.byteLength(testMessage), 0);
host.stdin.write(length);
host.stdin.write(testMessage);
// 读取响应
let responseBuffer = Buffer.alloc(0);
let messageLength = null;
host.stdout.on('data', (data) => {
responseBuffer = Buffer.concat([responseBuffer, data]);
if (messageLength === null && responseBuffer.length >= 4) {
messageLength = responseBuffer.readUInt32LE(0);
responseBuffer = responseBuffer.slice(4);
}
if (messageLength !== null && responseBuffer.length >= messageLength) {
const message = JSON.parse(responseBuffer.slice(0, messageLength).toString());
console.log("Response received:", message);
if (message.type === 'handshake_response') {
console.log("✅ Native Host 响应正常");
}
host.kill();
process.exit(0);
}
});
host.on('error', (error) => {
console.error("❌ Host error:", error.message);
process.exit(1);
});
setTimeout(() => {
console.error("❌ 测试超时");
host.kill();
process.exit(1);
}, 5000);
EOF
node /tmp/test-native-host.js "$HOST_SCRIPT"
rm /tmp/test-native-host.js
fi
fi
echo -e "${GREEN}安装完成!${NC}"

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Native Host 启动脚本
# Chrome 在 macOS 上需要这个包装脚本来正确启动 Node.js
# 获取脚本所在目录
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 日志文件
LOG_FILE="/tmp/qwen-bridge-host.log"
# 记录启动信息
echo "[$(date)] Native Host 启动..." >> "$LOG_FILE"
echo "[$(date)] 工作目录: $DIR" >> "$LOG_FILE"
echo "[$(date)] Node 路径: $(which node)" >> "$LOG_FILE"
# 启动 Node.js Native Host
exec /usr/bin/env node "$DIR/../host.js" 2>> "$LOG_FILE"

View File

@@ -0,0 +1,149 @@
#!/bin/bash
# Qwen CLI Chrome Extension - Native Host Configuration Updater
# 用于在更换电脑或浏览器后更新Native Host配置
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
HOST_NAME="com.qwen.cli.bridge"
HOST_SCRIPT="$SCRIPT_DIR/../host.js"
echo "==============================================="
echo "Qwen CLI Chrome Extension - Native Host Configuration Updater"
echo "==============================================="
echo ""
# Detect OS
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macOS"
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="Linux"
MANIFEST_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
else
echo "Error: Unsupported operating system"
exit 1
fi
echo "Detected OS: $OS"
echo ""
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed"
echo "Please install Node.js from https://nodejs.org/"
exit 1
fi
echo "✓ Node.js $(node --version) is installed"
echo ""
# Create Native Host directory
echo "Creating Native Host directory..."
mkdir -p "$MANIFEST_DIR"
echo "✓ Directory created: $MANIFEST_DIR"
echo ""
# Check if host.js exists
if [[ ! -f "$HOST_SCRIPT" ]]; then
echo "Error: host.js not found at $HOST_SCRIPT"
exit 1
fi
# Make host.js executable
chmod +x "$HOST_SCRIPT"
echo "✓ Made host.js executable"
echo ""
# Get extension ID
echo "How would you like to configure the extension?"
echo "1) Use specific extension ID (recommended for production)"
echo "2) Use generic configuration (allows any development extension)"
echo ""
read -p "Choose option (1/2): " CONFIG_OPTION
MANIFEST_FILE="$MANIFEST_DIR/$HOST_NAME.json"
if [[ "$CONFIG_OPTION" == "1" ]]; then
echo ""
echo "Please enter your Chrome extension ID:"
echo "Tip: Find it in chrome://extensions page for Qwen CLI Chrome Extension"
read -p "Extension ID: " EXTENSION_ID
if [[ -z "$EXTENSION_ID" ]]; then
echo "Error: Extension ID is required"
exit 1
fi
# Save extension ID for future use
echo "$EXTENSION_ID" > "$SCRIPT_DIR/../.extension-id"
# Create manifest with specific extension ID
cat > "$MANIFEST_FILE" << EOF
{
"name": "$HOST_NAME",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$HOST_SCRIPT",
"type": "stdio",
"allowed_origins": [
"chrome-extension://$EXTENSION_ID/",
"chrome-extension://*/"
]
}
EOF
echo ""
echo "✓ Native Host configured for extension ID: $EXTENSION_ID"
elif [[ "$CONFIG_OPTION" == "2" ]]; then
# Create manifest with generic configuration
cat > "$MANIFEST_FILE" << EOF
{
"name": "$HOST_NAME",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$HOST_SCRIPT",
"type": "stdio",
"allowed_origins": [
"chrome-extension://*/"
]
}
EOF
echo ""
echo "✓ Native Host configured with generic settings (allows any development extension)"
else
echo "Invalid option"
exit 1
fi
echo ""
echo "✓ Manifest file created: $MANIFEST_FILE"
echo ""
# Verify configuration
echo "Verifying configuration..."
if [[ -f "$MANIFEST_FILE" ]]; then
echo "✓ Configuration verified successfully"
echo ""
echo "Configuration details:"
cat "$MANIFEST_FILE"
echo ""
else
echo "✗ Configuration verification failed"
exit 1
fi
echo "==============================================="
echo "✅ Native Host configuration updated successfully!"
echo "==============================================="
echo ""
echo "Next steps:"
echo "1. Restart Chrome if it's running"
echo "2. Navigate to chrome://extensions"
echo "3. Reload the Qwen CLI Chrome Extension extension"
echo "4. Click the extension icon and connect to Qwen CLI"
echo ""
echo "Note: Run this script whenever you:"
echo " • Switch to a new computer"
echo " • Change browsers"
echo " • Reinstall Chrome"
echo " • Get a new extension ID"
echo ""

View File

@@ -0,0 +1,9 @@
#!/bin/bash
exec 2>> /tmp/qwen-wrapper-error.log
echo "$(date): Wrapper started" >> /tmp/qwen-wrapper-error.log
echo "$(date): PWD=$PWD" >> /tmp/qwen-wrapper-error.log
echo "$(date): Node=$(which node)" >> /tmp/qwen-wrapper-error.log
# 运行实际的 host.js
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
exec /usr/local/bin/node "$SCRIPT_DIR/../host.js"

View File

@@ -0,0 +1,370 @@
#!/usr/bin/env node
/**
* Browser MCP Server
* Provides browser tools (read_page, capture_screenshot, etc.) to Qwen CLI
* Communicates with Native Host via HTTP to get browser data
*/
const http = require('http');
const BRIDGE_URL = 'http://127.0.0.1:18765';
// MCP Protocol version
const PROTOCOL_VERSION = '2024-11-05';
// Available tools
const TOOLS = [
{
name: 'browser_read_page',
description:
'Read the content of the current browser page. Returns URL, title, text content, links, and images.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'browser_capture_screenshot',
description:
'Capture a screenshot of the current browser tab. Returns a base64-encoded PNG image.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'browser_get_network_logs',
description:
'Get network request logs from the current browser tab. Useful for debugging API calls.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'browser_get_console_logs',
description:
'Get console logs (log, error, warn, info) from the current browser tab.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
];
// Send request to Native Host HTTP bridge
async function callBridge(method, params = {}) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ method, params });
const req = http.request(
BRIDGE_URL,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
},
},
(res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
try {
const result = JSON.parse(body);
if (result.success) {
resolve(result.data);
} else {
reject(new Error(result.error || 'Unknown error'));
}
} catch (err) {
reject(new Error(`Failed to parse response: ${err.message}`));
}
});
},
);
req.on('error', (err) => {
reject(
new Error(
`Bridge connection failed: ${err.message}. Make sure Chrome extension is running.`,
),
);
});
req.write(data);
req.end();
});
}
// Handle MCP tool calls
async function handleToolCall(name, args) {
switch (name) {
case 'browser_read_page': {
const data = await callBridge('read_page');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
url: data.url,
title: data.title,
content: data.content?.text || data.content?.markdown || '',
linksCount: data.links?.length || 0,
imagesCount: data.images?.length || 0,
},
null,
2,
),
},
],
};
}
case 'browser_capture_screenshot': {
const data = await callBridge('capture_screenshot');
return {
content: [
{
type: 'image',
data: data.dataUrl?.replace(/^data:image\/png;base64,/, '') || '',
mimeType: 'image/png',
},
],
};
}
case 'browser_get_network_logs': {
const data = await callBridge('get_network_logs');
const logs = data.logs || [];
if (!logs.length) {
return {
content: [
{
type: 'text',
text:
'No network entries captured yet. Try reloading the page or triggering a request, then run again.',
},
],
};
}
// Aggregate by requestId to include method/url/status/headers/bodies
const byRequest = new Map();
for (const log of logs) {
const reqId = log.params?.requestId;
if (!reqId) continue;
const entry = byRequest.get(reqId) || { requestId: reqId };
switch (log.method) {
case 'Network.requestWillBeSent': {
entry.method = log.params?.request?.method;
entry.url =
log.params?.request?.url || log.params?.documentURL || entry.url;
entry.requestHeaders = log.params?.request?.headers;
entry.requestBody = log.params?.request?.postData;
entry.timestamp = log.timestamp;
break;
}
case 'Network.responseReceived': {
entry.status = log.params?.response?.status;
entry.statusText = log.params?.response?.statusText;
entry.responseHeaders = log.params?.response?.headers;
entry.timestamp = log.timestamp;
break;
}
case 'Network.responseBody': {
entry.responseBody = log.params?.body;
entry.responseBodyBase64 = log.params?.base64Encoded;
if (log.params?.error) entry.responseBodyError = log.params.error;
entry.timestamp = log.timestamp;
break;
}
case 'Network.loadingFailed': {
entry.error = log.params?.errorText || log.params?.error;
entry.timestamp = log.timestamp;
break;
}
default:
break;
}
byRequest.set(reqId, entry);
}
// Take the most recent 20 requests
const items = Array.from(byRequest.values()).slice(-20);
const text = `Network requests (last ${items.length}):\n${JSON.stringify(
items,
null,
2,
)}`;
return {
content: [
{
type: 'text',
text,
},
],
};
}
case 'browser_get_console_logs': {
const data = await callBridge('get_console_logs');
const logs = data.logs || [];
const formatted = logs
.slice(-50)
.map((log) => `[${log.type}] ${log.message}`)
.join('\n');
return {
content: [
{
type: 'text',
text: `Console logs (last ${Math.min(logs.length, 50)} entries):\n${formatted || '(no logs captured)'}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// JSON-RPC framing over stdio (Content-Length)
let inputBuffer = Buffer.alloc(0);
function writeMessage(obj) {
const json = Buffer.from(JSON.stringify(obj), 'utf8');
const header = Buffer.from(`Content-Length: ${json.length}\r\n\r\n`, 'utf8');
process.stdout.write(header);
process.stdout.write(json);
}
function sendResponse(id, result) {
writeMessage({ jsonrpc: '2.0', id, result });
}
function sendError(id, code, message) {
writeMessage({ jsonrpc: '2.0', id, error: { code, message } });
}
// Handle incoming JSON-RPC messages
async function handleMessage(message) {
const { id, method, params } = message;
try {
switch (method) {
case 'initialize':
sendResponse(id, {
protocolVersion: PROTOCOL_VERSION,
capabilities: {
tools: {},
},
serverInfo: {
name: 'chrome-browser',
version: '1.0.0',
},
});
break;
case 'tool': {
// Return functionDeclarations compatible with Qwen's mcpToTool expectation
const functionDeclarations = TOOLS.map(t => ({
name: t.name,
description: t.description,
parametersJsonSchema: t.inputSchema || { type: 'object', properties: {} },
}));
sendResponse(id, { functionDeclarations });
break;
}
case 'notifications/initialized':
// No response needed for notifications
break;
case 'tools/list':
sendResponse(id, { tools: TOOLS });
break;
case 'tools/call':
try {
const result = await handleToolCall(
params.name,
params.arguments || {},
);
sendResponse(id, result);
} catch (err) {
sendResponse(id, {
content: [
{
type: 'text',
text: `Error: ${err.message}`,
},
],
isError: true,
});
}
break;
case 'ping':
sendResponse(id, {});
break;
default:
if (id !== undefined) {
sendError(id, -32601, `Method not found: ${method}`);
}
}
} catch (err) {
if (id !== undefined) {
sendError(id, -32603, err.message);
}
}
}
// Main: Read JSON-RPC messages from stdin (Content-Length framed)
process.stdin.on('data', (chunk) => {
inputBuffer = Buffer.concat([inputBuffer, chunk]);
while (true) {
let headerEnd = inputBuffer.indexOf('\r\n\r\n');
let sepLen = 4;
if (headerEnd === -1) {
headerEnd = inputBuffer.indexOf('\n\n');
sepLen = 2;
}
if (headerEnd === -1) return; // wait for full header
const headerStr = inputBuffer.slice(0, headerEnd).toString('utf8');
const match = headerStr.match(/Content-Length:\s*(\d+)/i);
if (!match) {
// drop until next header
inputBuffer = inputBuffer.slice(headerEnd + sepLen);
continue;
}
const length = parseInt(match[1], 10);
const totalLen = headerEnd + sepLen + length;
if (inputBuffer.length < totalLen) return; // wait for full body
const body = inputBuffer.slice(headerEnd + sepLen, totalLen);
inputBuffer = inputBuffer.slice(totalLen);
try {
const message = JSON.parse(body.toString('utf8'));
// Debug to stderr (not stdout): show basic method flow
try { console.error('[MCP <-]', message.method || 'response', message.id ?? ''); } catch (_) {}
handleMessage(message);
} catch (e) {
try { console.error('[MCP] JSON parse error:', e.message); } catch (_) {}
// ignore parse errors
}
}
});
// Handle errors
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
{
"name": "@qwen-code/chrome-bridge",
"version": "1.0.0",
"description": "Chrome extension bridge for Qwen CLI - enables AI-powered browser interactions",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/QwenLM/qwen-code.git",
"directory": "packages/chrome-extension"
},
"keywords": [
"chrome-extension",
"qwen",
"cli",
"bridge",
"native-messaging",
"mcp",
"ai"
],
"author": "Qwen Team",
"license": "Apache-2.0",
"type": "module",
"files": [
"extension/",
"native-host/",
"README.md"
],
"scripts": {
"dev": "./scripts/debug.sh",
"build:ui": "node config/esbuild.config.js",
"build:ui:watch": "node config/esbuild.config.js --watch",
"build": "npm run build:ui",
"install:extension": "./scripts/first-install.sh",
"install:host": "cd native-host && ./scripts/smart-install.sh",
"install:all": "./scripts/first-install.sh",
"update:host": "cd native-host && ./scripts/update-host-config.sh",
"dev:chrome": "open -a 'Google Chrome' --args --load-extension=$PWD/extension --auto-open-devtools-for-tabs",
"package": "zip -r chrome-extension.zip extension/",
"clean": "rm -rf dist *.zip /tmp/qwen-bridge-host.log /tmp/qwen-server.log .extension-id extension/sidepanel/dist/sidepanel-app.js extension/sidepanel/dist/sidepanel-app.js.map",
"logs": "tail -f /tmp/qwen-bridge-host.log"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"markdown-it": "^14.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/chrome": "^0.1.32",
"@types/markdown-it": "^14.1.2",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"autoprefixer": "^10.4.22",
"esbuild": "^0.25.3",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1 @@
kbpfhhpfobobomiighfkhojhmefogdgh

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# Build script for Chrome extension package
echo "Building Chrome Qwen Bridge..."
# Ensure we're in the project root directory (where both scripts/ and extension/ are)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR/.."
# Create dist directory
mkdir -p dist
# Copy extension files to dist
echo "Copying extension files..."
cp -r extension dist/
# Create a zip file for Chrome Web Store
echo "Creating extension package..."
cd dist
zip -r ../chrome-extension.zip extension/
cd ..
echo "✅ Build complete!"
echo " Extension package: chrome-extension.zip"
echo " Extension files: dist/extension/"

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Clean up build artifacts and temporary files for Chrome Extension
echo "Cleaning up Chrome Extension build artifacts..."
# Remove build output
rm -rf extension/sidepanel/dist/sidepanel-app.js
rm -rf extension/sidepanel/dist/sidepanel-app.js.map
# Remove any zip files
rm -f chrome-extension.zip
# Remove log files
rm -f /tmp/qwen-bridge-host.log
rm -f /tmp/qwen-server.log
# Remove saved extension ID
rm -f .extension-id
# Remove any dist directories
rm -rf dist/
echo "Cleanup complete!"

View File

@@ -0,0 +1,357 @@
#!/bin/bash
# Qwen CLI Chrome Extension - macOS 一键调试脚本
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# 获取脚本目录
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 检查是否首次安装
if [[ ! -f "$SCRIPT_DIR/.extension-id" ]]; then
echo -e "${YELLOW}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ ║${NC}"
echo -e "${YELLOW}║ ⚠️ 检测到首次运行,需要先安装插件 ║${NC}"
echo -e "${YELLOW}║ ║${NC}"
echo -e "${YELLOW}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}即将启动首次安装向导...${NC}"
sleep 2
exec "$SCRIPT_DIR/first-install.sh"
exit 0
fi
# 清屏显示标题
clear
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ ║${NC}"
echo -e "${CYAN}║ 🚀 Qwen CLI Chrome Extension - macOS 调试环境 ║${NC}"
echo -e "${CYAN}║ ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
# 第一步:检查环境
echo -e "${BLUE}[1/5]${NC} 检查开发环境..."
# 检查 Node.js
if ! command -v node &> /dev/null; then
echo -e "${RED}${NC} Node.js 未安装,请先安装 Node.js"
echo " 访问 https://nodejs.org 下载安装"
exit 1
fi
echo -e "${GREEN}${NC} Node.js $(node --version)"
# 检查 Chrome
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if [[ ! -f "$CHROME_PATH" ]]; then
echo -e "${RED}${NC} Chrome 未找到"
exit 1
fi
echo -e "${GREEN}${NC} Chrome 已安装"
# 第二步:配置 Native Host
echo -e "\n${BLUE}[2/5]${NC} 配置 Native Host..."
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
mkdir -p "$MANIFEST_DIR"
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$SCRIPT_DIR/../native-host/src/host.js",
"type": "stdio",
"allowed_origins": ["chrome-extension://*/"]
}
EOF
echo -e "${GREEN}${NC} Native Host 已配置"
# 第三步:检查 Qwen CLI
echo -e "\n${BLUE}[3/5]${NC} 检查 Qwen CLI..."
QWEN_AVAILABLE=false
if command -v qwen &> /dev/null; then
QWEN_AVAILABLE=true
QWEN_VERSION=$(qwen --version 2>/dev/null || echo "已安装")
echo -e "${GREEN}${NC} Qwen CLI ${QWEN_VERSION}"
echo -e "${CYAN}${NC} 使用 ACP 模式与 Chrome 插件通信"
else
echo -e "${YELLOW}!${NC} Qwen CLI 未安装(插件基础功能仍可使用)"
echo -e " 安装方法: npm install -g @anthropic-ai/qwen-code"
fi
# 第四步:启动测试页面
echo -e "\n${BLUE}[4/5]${NC} 启动测试服务器..."
# 创建测试页面
cat > /tmp/qwen-test.html << 'HTML'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Qwen CLI Chrome Extension 测试页面</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
}
.content {
padding: 40px;
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.status {
display: inline-block;
padding: 5px 15px;
background: rgba(255,255,255,0.2);
border-radius: 20px;
margin-top: 10px;
}
.test-section {
margin: 30px 0;
padding: 25px;
background: #f8f9fa;
border-radius: 10px;
}
.test-section h2 {
color: #667eea;
margin-bottom: 15px;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
margin: 5px;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
#console {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 8px;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 14px;
min-height: 150px;
max-height: 300px;
overflow-y: auto;
margin-top: 15px;
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-left: 3px solid transparent;
}
.log-entry.info { border-left-color: #3b82f6; }
.log-entry.warn { border-left-color: #f59e0b; color: #fbbf24; }
.log-entry.error { border-left-color: #ef4444; color: #f87171; }
.instructions {
background: #e0e7ff;
padding: 20px;
border-radius: 10px;
margin-top: 20px;
}
.instructions h3 {
color: #4c1d95;
margin-bottom: 10px;
}
.instructions ol {
margin-left: 20px;
color: #4c1d95;
}
.instructions li {
margin: 8px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 Qwen CLI Chrome Extension</h1>
<div class="status">调试环境已就绪</div>
</div>
<div class="content">
<div class="test-section">
<h2>📝 测试功能</h2>
<button onclick="testLog()">测试 Console Log</button>
<button onclick="testError()">测试 Console Error</button>
<button onclick="testNetwork()">测试网络请求</button>
<button onclick="testSelection()">测试文本选择</button>
<div id="console"></div>
</div>
<div class="test-section">
<h2>📄 示例内容</h2>
<p>这是一段可以被插件提取的示例文本。你可以选择这段文字,然后使用插件的"Send Selected Text"功能。</p>
<ul style="margin: 15px 0;">
<li>列表项 1Lorem ipsum dolor sit amet</li>
<li>列表项 2Consectetur adipiscing elit</li>
<li>列表项 3Sed do eiusmod tempor incididunt</li>
</ul>
<blockquote style="border-left: 4px solid #667eea; padding-left: 15px; margin: 15px 0; color: #666;">
"这是一个引用块,可以测试 Markdown 转换功能。"
</blockquote>
</div>
<div class="instructions">
<h3>🎯 使用说明</h3>
<ol>
<li>点击 Chrome 工具栏中的插件图标</li>
<li>点击 "Connect to Qwen CLI" 建立连接</li>
<li>如果安装了 Qwen CLI点击 "Start Qwen CLI"</li>
<li>使用各种功能按钮测试插件功能</li>
<li>按 F12 打开 DevTools 查看详细日志</li>
</ol>
</div>
</div>
</div>
<script>
const consoleDiv = document.getElementById('console');
function addLog(message, type = 'info') {
const entry = document.createElement('div');
entry.className = 'log-entry ' + type;
const time = new Date().toLocaleTimeString();
entry.textContent = `[${time}] ${message}`;
consoleDiv.appendChild(entry);
consoleDiv.scrollTop = consoleDiv.scrollHeight;
// 同时输出到真实 console
console[type](message);
}
function testLog() {
addLog('这是一条测试日志消息', 'info');
}
function testError() {
addLog('这是一条测试错误消息', 'error');
}
function testNetwork() {
addLog('发起网络请求...', 'info');
fetch('https://api.github.com/zen')
.then(res => res.text())
.then(data => addLog('请求成功: ' + data, 'info'))
.catch(err => addLog('请求失败: ' + err.message, 'error'));
}
function testSelection() {
const selection = window.getSelection().toString();
if (selection) {
addLog('选中的文本: ' + selection, 'info');
} else {
addLog('请先选择一些文本', 'warn');
}
}
// 初始化
addLog('测试页面已加载', 'info');
addLog('插件调试环境已就绪', 'info');
</script>
</body>
</html>
HTML
# 启动 Python HTTP 服务器
cd /tmp
python3 -m http.server 3000 > /tmp/test-server.log 2>&1 &
TEST_PID=$!
sleep 1
echo -e "${GREEN}${NC} 测试服务器已启动 (http://localhost:3000)"
# 第五步:启动 Chrome
echo -e "\n${BLUE}[5/5]${NC} 启动 Chrome 并加载插件..."
"$CHROME_PATH" \
--load-extension="$SCRIPT_DIR/../extension" \
--auto-open-devtools-for-tabs \
--no-first-run \
--no-default-browser-check \
"http://localhost:3000/qwen-test.html" &
CHROME_PID=$!
echo -e "${GREEN}${NC} Chrome 已启动"
# 显示最终状态
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ ✅ 调试环境启动成功! ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}📍 服务状态:${NC}"
echo -e " • Chrome: 运行中"
echo -e " • 测试页面: ${BLUE}http://localhost:3000/qwen-test.html${NC}"
echo -e " • 插件: 已加载到工具栏"
if [ "$QWEN_AVAILABLE" = true ]; then
echo -e " • Qwen CLI: 可用 (ACP 模式)"
fi
echo ""
echo -e "${CYAN}🔍 调试位置:${NC}"
echo -e " • 插件日志: Chrome DevTools Console"
echo -e " • 后台脚本: chrome://extensions → Service Worker"
echo -e " • Native Host: /tmp/qwen-bridge-host.log"
echo ""
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
echo ""
# 清理函数
cleanup() {
echo -e "\n${YELLOW}正在停止服务...${NC}"
# 停止进程
[ ! -z "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
echo -e "${GREEN}${NC} 已停止所有服务"
exit 0
}
# 捕获中断信号
trap cleanup INT TERM
# 保持运行
while true; do
sleep 1
done

View File

@@ -0,0 +1,529 @@
#!/usr/bin/env node
/**
* 开发环境一键启动脚本
* 自动完成所有配置和启动步骤
*/
const { spawn, exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const readline = require('readline');
// 颜色输出
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m',
cyan: '\x1b[36m',
};
function log(message, color = '') {
console.log(`${color}${message}${colors.reset}`);
}
function logStep(step, message) {
log(`\n[${step}] ${message}`, colors.bright + colors.blue);
}
function logSuccess(message) {
log(`${message}`, colors.green);
}
function logWarning(message) {
log(`⚠️ ${message}`, colors.yellow);
}
function logError(message) {
log(`${message}`, colors.red);
}
function logInfo(message) {
log(` ${message}`, colors.cyan);
}
// 检查命令是否存在
function commandExists(command) {
return new Promise((resolve) => {
exec(`command -v ${command}`, (error) => {
resolve(!error);
});
});
}
// 获取 Chrome 路径
function getChromePath() {
const platform = process.platform;
const chromePaths = {
darwin: [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
],
win32: [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe',
],
linux: [
'/usr/bin/google-chrome',
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
],
};
const paths = chromePaths[platform] || [];
for (const chromePath of paths) {
if (fs.existsSync(chromePath)) {
return chromePath;
}
}
return null;
}
// 获取扩展 ID
function getExtensionId(extensionPath) {
// 这是一个简化的方法,实际的 Extension ID 是通过 Chrome 生成的
// 开发时可以固定使用一个 ID
return 'development-extension-id';
}
// 安装 Native Host
async function installNativeHost(extensionPath) {
logStep(2, 'Installing Native Host...');
const hostPath = path.join(extensionPath, 'native-host');
const scriptPath = path.join(hostPath, 'host.js');
if (!fs.existsSync(scriptPath)) {
logError('Native host script not found!');
return false;
}
const platform = process.platform;
const hostName = 'com.qwen.cli.bridge';
let manifestDir;
if (platform === 'darwin') {
manifestDir = path.join(
os.homedir(),
'Library/Application Support/Google/Chrome/NativeMessagingHosts',
);
} else if (platform === 'linux') {
manifestDir = path.join(
os.homedir(),
'.config/google-chrome/NativeMessagingHosts',
);
} else if (platform === 'win32') {
// Windows 需要写注册表
logWarning(
'Windows requires registry modification. Please run install.bat manually.',
);
return true;
} else {
logError('Unsupported platform');
return false;
}
// 创建目录
if (!fs.existsSync(manifestDir)) {
fs.mkdirSync(manifestDir, { recursive: true });
}
// 创建 manifest 文件
const manifest = {
name: hostName,
description: 'Native messaging host for Qwen CLI Chrome Extension',
path: scriptPath,
type: 'stdio',
allowed_origins: [
'chrome-extension://jniepomhbdkeifkadbfolbcihcmfpfjo/', // 开发用 ID
'chrome-extension://*/', // 允许任何扩展(仅开发环境)
],
};
const manifestPath = path.join(manifestDir, `${hostName}.json`);
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
logSuccess(`Native Host installed at: ${manifestPath}`);
return true;
}
// 检查 Qwen CLI
async function checkQwenCli() {
logStep(3, 'Checking Qwen CLI...');
const qwenExists = await commandExists('qwen');
if (qwenExists) {
logSuccess('Qwen CLI is installed');
// 获取版本
return new Promise((resolve) => {
exec('qwen --version', (error, stdout) => {
if (!error && stdout) {
logInfo(`Version: ${stdout.trim()}`);
}
resolve(true);
});
});
} else {
logWarning('Qwen CLI is not installed');
logInfo(
'You can still use the extension, but some features will be limited',
);
return false;
}
}
// 启动 Qwen CLI 服务器
function startQwenServer(port = 8080) {
logStep(4, 'Starting Qwen CLI Server...');
return new Promise((resolve) => {
// 检查端口是否被占用
exec(`lsof -i:${port} || netstat -an | grep ${port}`, (error, stdout) => {
if (stdout && stdout.length > 0) {
logWarning(`Port ${port} is already in use`);
logInfo('Qwen server might already be running');
resolve(null);
return;
}
// 启动服务器
const qwenProcess = spawn('qwen', ['server', '--port', String(port)], {
detached: false,
stdio: ['ignore', 'pipe', 'pipe'],
});
qwenProcess.stdout.on('data', (data) => {
const output = data.toString();
if (output.includes('Server started') || output.includes('listening')) {
logSuccess(`Qwen server started on port ${port}`);
resolve(qwenProcess);
}
});
qwenProcess.stderr.on('data', (data) => {
logError(`Qwen server error: ${data}`);
});
qwenProcess.on('error', (error) => {
logError(`Failed to start Qwen server: ${error.message}`);
resolve(null);
});
// 超时处理
setTimeout(() => {
logWarning('Qwen server start timeout, continuing anyway...');
resolve(qwenProcess);
}, 5000);
});
});
}
// 启动 Chrome 开发模式
function startChrome(extensionPath, chromePath) {
logStep(5, 'Starting Chrome with extension...');
const args = [
`--load-extension=${extensionPath}`,
'--auto-open-devtools-for-tabs', // 自动打开 DevTools
'--disable-extensions-except=' + extensionPath,
'--no-first-run',
'--no-default-browser-check',
'--disable-default-apps',
'--disable-popup-blocking',
'--disable-translate',
'--disable-sync',
'--no-pings',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-device-discovery-notifications',
];
// 开发模式特定参数
if (process.env.DEBUG === 'true') {
args.push('--enable-logging=stderr');
args.push('--v=1');
}
// 添加测试页面
args.push('http://localhost:3000'); // 或其他测试页面
const chromeProcess = spawn(chromePath, args, {
detached: false,
stdio: 'inherit',
});
chromeProcess.on('error', (error) => {
logError(`Failed to start Chrome: ${error.message}`);
});
logSuccess('Chrome started with extension loaded');
logInfo('Extension should be visible in the toolbar');
return chromeProcess;
}
// 创建测试服务器
function createTestServer(port = 3000) {
logStep(6, 'Starting test server...');
const http = require('http');
const testHtml = `
<!DOCTYPE html>
<html>
<head>
<title>Qwen CLI Chrome Extension Test Page</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.test-content {
margin: 20px 0;
}
.test-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
.test-button:hover {
opacity: 0.9;
}
#console-output {
background: #f5f5f5;
padding: 10px;
border-radius: 5px;
font-family: monospace;
min-height: 100px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 Qwen CLI Chrome Extension Test Page</h1>
<div class="test-content">
<h2>Test Content</h2>
<p>This is a test page for the Qwen CLI Chrome Extension.</p>
<p>Click the extension icon in your toolbar to start testing!</p>
<h3>Sample Data</h3>
<ul>
<li>Item 1: Lorem ipsum dolor sit amet</li>
<li>Item 2: Consectetur adipiscing elit</li>
<li>Item 3: Sed do eiusmod tempor incididunt</li>
</ul>
<h3>Test Actions</h3>
<button class="test-button" onclick="testLog()">Test Console Log</button>
<button class="test-button" onclick="testError()">Test Console Error</button>
<button class="test-button" onclick="testNetwork()">Test Network Request</button>
<h3>Console Output</h3>
<div id="console-output"></div>
</div>
<div class="test-content">
<h2>Test Form</h2>
<form>
<input type="text" placeholder="Test input" style="padding: 5px; margin: 5px;">
<textarea placeholder="Test textarea" style="padding: 5px; margin: 5px;"></textarea>
<select style="padding: 5px; margin: 5px;">
<option>Option 1</option>
<option>Option 2</option>
</select>
</form>
</div>
<div class="test-content">
<h2>Images</h2>
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzY2N2VlYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPjIwMHgxMDA8L3RleHQ+PC9zdmc+" alt="Test Image">
</div>
</div>
<script>
function addOutput(message, type = 'log') {
const output = document.getElementById('console-output');
const time = new Date().toLocaleTimeString();
const color = type === 'error' ? 'red' : type === 'warn' ? 'orange' : 'black';
output.innerHTML += \`<div style="color: \${color}">[\${time}] \${message}</div>\`;
console[type](message);
}
function testLog() {
addOutput('This is a test log message', 'log');
}
function testError() {
addOutput('This is a test error message', 'error');
}
async function testNetwork() {
addOutput('Making network request...', 'log');
try {
const response = await fetch('https://api.github.com/users/github');
const data = await response.json();
addOutput('Network request successful: ' + JSON.stringify(data).substring(0, 100) + '...', 'log');
} catch (error) {
addOutput('Network request failed: ' + error.message, 'error');
}
}
// 自动记录一些日志
console.log('Test page loaded');
console.info('Extension test environment ready');
</script>
</body>
</html>
`;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(testHtml);
});
server.listen(port, () => {
logSuccess(`Test server running at http://localhost:${port}`);
});
return server;
}
// 主函数
async function main() {
console.clear();
log(
`
╔════════════════════════════════════════════════════════════════╗
║ ║
║ 🚀 Qwen CLI Chrome Extension - Development Environment Setup ║
║ ║
╚════════════════════════════════════════════════════════════════╝
`,
colors.bright + colors.cyan,
);
const extensionPath = path.join(__dirname, 'extension');
// Step 1: 检查 Chrome
logStep(1, 'Checking Chrome installation...');
const chromePath = getChromePath();
if (!chromePath) {
logError('Chrome not found! Please install Google Chrome.');
process.exit(1);
}
logSuccess(`Chrome found at: ${chromePath}`);
// Step 2: 安装 Native Host
const nativeHostInstalled = await installNativeHost(__dirname);
if (!nativeHostInstalled && process.platform === 'win32') {
logWarning(
'Please run install.bat as Administrator to complete Native Host setup',
);
}
// Step 3: 检查 Qwen CLI
const qwenInstalled = await checkQwenCli();
// Step 4: 启动 Qwen 服务器(如果已安装)
let qwenProcess = null;
if (qwenInstalled) {
qwenProcess = await startQwenServer(8080);
}
// Step 5: 启动测试服务器
const testServer = createTestServer(3000);
// Step 6: 启动 Chrome
await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待服务器启动
const chromeProcess = startChrome(extensionPath, chromePath);
// 设置清理处理
const cleanup = () => {
log('\n\nShutting down...', colors.yellow);
if (qwenProcess) {
qwenProcess.kill();
logInfo('Qwen server stopped');
}
if (testServer) {
testServer.close();
logInfo('Test server stopped');
}
if (chromeProcess) {
chromeProcess.kill();
logInfo('Chrome stopped');
}
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// 显示使用说明
log(
`
╔════════════════════════════════════════════════════════════════╗
║ ✅ Setup Complete! ║
╠════════════════════════════════════════════════════════════════╣
║ ║
║ 📍 Chrome is running with the extension loaded ║
║ 📍 Test page: http://localhost:3000 ║
${qwenInstalled ? '📍 Qwen server: http://localhost:8080 ' : '📍 Qwen CLI not installed (limited functionality) '}
║ ║
║ 📝 How to debug: ║
║ 1. Click the extension icon in Chrome toolbar ║
║ 2. Open Chrome DevTools (F12) to see console logs ║
║ 3. Check background page: chrome://extensions → Details ║
║ 4. Native Host logs: /tmp/qwen-bridge-host.log ║
║ ║
║ 🛑 Press Ctrl+C to stop all services ║
║ ║
╚════════════════════════════════════════════════════════════════╝
`,
colors.bright + colors.green,
);
// 保持进程运行
await new Promise(() => {});
}
// 运行
main().catch((error) => {
logError(`Fatal error: ${error.message}`);
process.exit(1);
});

View File

@@ -0,0 +1,120 @@
#!/bin/bash
# Qwen CLI Chrome Extension - 首次安装脚本
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
clear
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ ║${NC}"
echo -e "${CYAN}║ 🎯 Qwen CLI Chrome Extension - 首次安装向导 ║${NC}"
echo -e "${CYAN}║ ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${YELLOW}这是首次安装,需要手动加载插件到 Chrome。${NC}"
echo ""
# 步骤 1: 配置 Native Host
echo -e "${BLUE}步骤 1:${NC} 配置 Native Host..."
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
mkdir -p "$MANIFEST_DIR"
# 先创建一个临时的 manifest允许所有扩展
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$SCRIPT_DIR/../native-host/src/host.js",
"type": "stdio",
"allowed_origins": ["chrome-extension://*/"]
}
EOF
echo -e "${GREEN}${NC} Native Host 已配置"
# 步骤 2: 打开 Chrome 扩展页面
echo -e "\n${BLUE}步骤 2:${NC} 打开 Chrome 扩展管理页面..."
open -a "Google Chrome" "chrome://extensions"
sleep 2
echo -e "${GREEN}${NC} 已打开扩展管理页面"
# 步骤 3: 指导用户安装
echo ""
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
echo -e "${YELLOW}请按照以下步骤手动安装插件:${NC}"
echo ""
echo -e " 1⃣ 在 Chrome 扩展页面,${GREEN}开启「开发者模式」${NC}(右上角开关)"
echo ""
echo -e " 2⃣ 点击 ${GREEN}「加载已解压的扩展程序」${NC} 按钮"
echo ""
echo -e " 3⃣ 选择以下目录:"
echo -e " ${BLUE}$SCRIPT_DIR/../extension${NC}"
echo ""
echo -e " 4${YELLOW}重要:${NC} 记下显示的扩展 ID类似 ${CYAN}abcdefghijklmnopqrstuvwx${NC}"
echo ""
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
echo ""
# 等待用户输入扩展 ID
echo -e "${YELLOW}请输入扩展 ID安装后显示的 ID${NC}"
read -p "> " EXTENSION_ID
if [[ -z "$EXTENSION_ID" ]]; then
echo -e "${RED}✗ 未输入扩展 ID${NC}"
echo -e "${YELLOW}你可以稍后手动更新 Native Host 配置${NC}"
else
# 更新 manifest 文件,添加具体的扩展 ID
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$SCRIPT_DIR/../native-host/src/host.js",
"type": "stdio",
"allowed_origins": [
"chrome-extension://$EXTENSION_ID/",
"chrome-extension://*/"
]
}
EOF
# 保存扩展 ID 供后续使用
echo "$EXTENSION_ID" > "$SCRIPT_DIR/.extension-id"
echo -e "${GREEN}${NC} Native Host 已更新,支持扩展 ID: $EXTENSION_ID"
fi
echo ""
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✅ 首次安装完成! ${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e "现在你可以:"
echo ""
echo -e " 1. 运行 ${CYAN}npm run dev${NC} 启动调试环境"
echo -e " 2. 点击 Chrome 工具栏的插件图标开始使用"
echo ""
echo -e "${YELLOW}提示:${NC}"
echo -e " • 如果看不到插件图标,点击拼图图标并固定插件"
echo -e " • 首次连接可能需要刷新页面"
echo ""
# 询问是否立即启动
echo -e "${CYAN}是否立即启动调试环境?(y/n)${NC}"
read -p "> " START_NOW
if [[ "$START_NOW" == "y" ]] || [[ "$START_NOW" == "Y" ]]; then
echo -e "\n${GREEN}正在启动调试环境...${NC}\n"
exec "$SCRIPT_DIR/debug.sh"
fi

View File

@@ -0,0 +1,24 @@
#!/bin/bash
echo "🔧 配置 Native Host 使用特定扩展 ID..."
EXTENSION_ID="cimaabkejokbhjkdnajgfniiolfjgbhd"
CONFIG_FILE="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json"
RUN_SCRIPT="$PWD/native-host/run.sh"
# 创建配置(使用特定扩展 ID
cat > "$CONFIG_FILE" <<EOF
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$RUN_SCRIPT",
"type": "stdio",
"allowed_origins": [
"chrome-extension://$EXTENSION_ID/"
]
}
EOF
echo "✅ 配置已更新(仅允许扩展 ID: $EXTENSION_ID"
echo ""
cat "$CONFIG_FILE"

View File

@@ -0,0 +1,300 @@
#!/bin/bash
# 快速启动脚本 - 适用于 macOS/Linux
# 一键启动所有服务进行调试
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 打印带颜色的消息
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[✓]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[!]${NC} $1"
}
print_error() {
echo -e "${RED}[✗]${NC} $1"
}
# 获取脚本所在目录
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
EXTENSION_DIR="$SCRIPT_DIR/extension"
NATIVE_HOST_DIR="$SCRIPT_DIR/native-host"
# 清屏并显示标题
clear
echo "======================================"
echo " Qwen CLI Chrome Extension - Quick Start"
echo "======================================"
echo ""
# 1. 检查 Chrome 是否安装
print_info "Checking Chrome installation..."
if [[ "$OSTYPE" == "darwin"* ]]; then
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if [[ ! -f "$CHROME_PATH" ]]; then
CHROME_PATH="/Applications/Chromium.app/Contents/MacOS/Chromium"
fi
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
CHROME_PATH=$(which google-chrome || which chromium-browser || which chromium || echo "")
fi
if [[ -z "$CHROME_PATH" ]] || [[ ! -f "$CHROME_PATH" ]]; then
print_error "Chrome not found! Please install Google Chrome first."
exit 1
fi
print_success "Chrome found: $CHROME_PATH"
# 2. 快速安装 Native Host (如果需要)
print_info "Setting up Native Host..."
if [[ "$OSTYPE" == "darwin"* ]]; then
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
MANIFEST_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
fi
mkdir -p "$MANIFEST_DIR"
# 创建 manifest
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
{
"name": "com.qwen.cli.bridge",
"description": "Native messaging host for Qwen CLI Chrome Extension",
"path": "$NATIVE_HOST_DIR/host.js",
"type": "stdio",
"allowed_origins": [
"chrome-extension://*/",
"chrome-extension://jniepomhbdkeifkadbfolbcihcmfpfjo/"
]
}
EOF
print_success "Native Host configured"
# 3. 检查 Qwen CLI
print_info "Checking Qwen CLI..."
if command -v qwen &> /dev/null; then
print_success "Qwen CLI is installed"
QWEN_VERSION=$(qwen --version 2>/dev/null || echo "unknown")
print_info "Version: $QWEN_VERSION"
# 尝试启动 Qwen server
print_info "Starting Qwen server on port 8080..."
# 检查端口是否被占用
if lsof -i:8080 &> /dev/null; then
print_warning "Port 8080 is already in use, skipping Qwen server start"
else
# 在后台启动 Qwen server
nohup qwen server --port 8080 > /tmp/qwen-server.log 2>&1 &
QWEN_PID=$!
sleep 2
if kill -0 $QWEN_PID 2>/dev/null; then
print_success "Qwen server started (PID: $QWEN_PID)"
echo $QWEN_PID > /tmp/qwen-server.pid
else
print_warning "Failed to start Qwen server, continuing anyway..."
fi
fi
else
print_warning "Qwen CLI not installed - some features will be limited"
fi
# 4. 启动简单的测试服务器
print_info "Starting test server..."
# 创建简单的 Python HTTP 服务器
cat > /tmp/test-server.py << 'EOF'
#!/usr/bin/env python3
import http.server
import socketserver
PORT = 3000
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Qwen CLI Chrome Extension Test</title>
<style>
body {
font-family: Arial;
padding: 40px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.container {
background: white;
color: #333;
padding: 30px;
border-radius: 10px;
max-width: 800px;
margin: 0 auto;
}
h1 { color: #667eea; }
button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
margin: 5px;
cursor: pointer;
}
button:hover { opacity: 0.9; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 Qwen CLI Chrome Extension Test Page</h1>
<p>Extension debugging environment is ready!</p>
<h2>Quick Tests</h2>
<button onclick="console.log('Test log message')">Test Console Log</button>
<button onclick="console.error('Test error message')">Test Console Error</button>
<button onclick="fetch('/api/test').catch(e => console.error(e))">Test Network Request</button>
<h2>Instructions</h2>
<ol>
<li>Click the extension icon in Chrome toolbar</li>
<li>Click "Connect to Qwen CLI"</li>
<li>Try the various features</li>
<li>Open DevTools (F12) to see console output</li>
</ol>
<h2>Sample Content</h2>
<p>This is sample text content that can be extracted by the extension.</p>
<ul>
<li>Item 1: Lorem ipsum dolor sit amet</li>
<li>Item 2: Consectetur adipiscing elit</li>
<li>Item 3: Sed do eiusmod tempor</li>
</ul>
</div>
<script>
console.log('Test page loaded successfully');
console.info('Ready for debugging');
</script>
</body>
</html>
"""
class MyHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html_content.encode())
with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
print(f"Test server running at http://localhost:{PORT}")
httpd.serve_forever()
EOF
python3 /tmp/test-server.py > /tmp/test-server.log 2>&1 &
TEST_SERVER_PID=$!
echo $TEST_SERVER_PID > /tmp/test-server.pid
sleep 1
print_success "Test server started at http://localhost:3000"
# 5. 启动 Chrome
print_info "Starting Chrome with extension..."
# Chrome 参数
CHROME_ARGS=(
"--load-extension=$EXTENSION_DIR"
"--auto-open-devtools-for-tabs"
"--no-first-run"
"--no-default-browser-check"
"--disable-default-apps"
"http://localhost:3000"
)
# 启动 Chrome
"$CHROME_PATH" "${CHROME_ARGS[@]}" &
CHROME_PID=$!
print_success "Chrome started with extension loaded"
# 6. 显示状态和清理指令
echo ""
echo "======================================"
echo " ✅ All Services Running"
echo "======================================"
echo ""
echo "📌 Chrome: Running (PID: $CHROME_PID)"
echo "📌 Test Page: http://localhost:3000"
if [[ -n "${QWEN_PID:-}" ]]; then
echo "📌 Qwen Server: http://localhost:8080 (PID: $QWEN_PID)"
fi
echo "📌 Extension: Loaded in Chrome toolbar"
echo ""
echo "📝 Debug Locations:"
echo " • Extension Logs: Chrome DevTools Console"
echo " • Background Page: chrome://extensions → Service Worker"
echo " • Native Host Log: /tmp/qwen-bridge-host.log"
if [[ -n "${QWEN_PID:-}" ]]; then
echo " • Qwen Server Log: /tmp/qwen-server.log"
fi
echo ""
echo "🛑 To stop all services, run: $SCRIPT_DIR/stop.sh"
echo " Or press Ctrl+C to stop this script"
echo ""
# 创建停止脚本
cat > "$SCRIPT_DIR/stop.sh" << 'STOP_SCRIPT'
#!/bin/bash
echo "Stopping services..."
# 停止 Qwen server
if [[ -f /tmp/qwen-server.pid ]]; then
PID=$(cat /tmp/qwen-server.pid)
if kill -0 $PID 2>/dev/null; then
kill $PID
echo "✓ Qwen server stopped"
fi
rm /tmp/qwen-server.pid
fi
# 停止测试服务器
if [[ -f /tmp/test-server.pid ]]; then
PID=$(cat /tmp/test-server.pid)
if kill -0 $PID 2>/dev/null; then
kill $PID
echo "✓ Test server stopped"
fi
rm /tmp/test-server.pid
fi
echo "✓ All services stopped"
STOP_SCRIPT
chmod +x "$SCRIPT_DIR/stop.sh"
# 等待用户中断
trap 'echo "Stopping services..."; $SCRIPT_DIR/stop.sh; exit 0' INT TERM
# 保持脚本运行
while true; do
sleep 1
done

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,491 @@
/**
* Content Script for Qwen CLI Chrome Extension
* Extracts data from web pages and communicates with background script
*/
if (window.__QWEN_BRIDGE_CONTENT_SCRIPT_LOADED__) {
console.debug('Qwen Bridge content script already loaded, skipping.');
} else {
window.__QWEN_BRIDGE_CONTENT_SCRIPT_LOADED__ = true;
// Data extraction functions
function extractPageData() {
const data = {
// Basic page info
url: window.location.href,
title: document.title,
domain: window.location.hostname,
path: window.location.pathname,
timestamp: new Date().toISOString(),
// Meta information
meta: {},
// Page content
content: {
text: '',
html: '',
markdown: '',
},
// Structured data
links: [],
images: [],
forms: [],
// Console logs
consoleLogs: [],
// Performance metrics
performance: {},
};
// Extract meta tags
document.querySelectorAll('meta').forEach((meta) => {
const name = meta.getAttribute('name') || meta.getAttribute('property');
const content = meta.getAttribute('content');
if (name && content) {
data.meta[name] = content;
}
});
// Extract main content (try to find article or main element first)
const mainContent =
document.querySelector('article, main, [role="main"]') || document.body;
data.content.text = extractTextContent(mainContent);
data.content.html = mainContent.innerHTML;
data.content.markdown = htmlToMarkdown(mainContent);
// Extract all links
document.querySelectorAll('a[href]').forEach((link) => {
data.links.push({
text: link.textContent.trim(),
href: link.href,
target: link.target,
isExternal: isExternalLink(link.href),
});
});
// Extract all images
document.querySelectorAll('img').forEach((img) => {
data.images.push({
src: img.src,
alt: img.alt,
title: img.title,
width: img.naturalWidth,
height: img.naturalHeight,
});
});
// Extract form data (structure only, no sensitive data)
document.querySelectorAll('form').forEach((form) => {
const formData = {
action: form.action,
method: form.method,
fields: [],
};
form.querySelectorAll('input, textarea, select').forEach((field) => {
formData.fields.push({
type: field.type || field.tagName.toLowerCase(),
name: field.name,
id: field.id,
placeholder: field.placeholder,
required: field.required,
});
});
data.forms.push(formData);
});
// Get performance metrics
if (window.performance && window.performance.timing) {
const perf = window.performance.timing;
data.performance = {
loadTime: perf.loadEventEnd - perf.navigationStart,
domReady: perf.domContentLoadedEventEnd - perf.navigationStart,
firstPaint: getFirstPaintTime(),
};
}
return data;
}
// Extract clean text content
function extractTextContent(element) {
// Clone the element to avoid modifying the original
const clone = element.cloneNode(true);
// Remove script and style elements
clone
.querySelectorAll('script, style, noscript')
.forEach((el) => el.remove());
// Get text content and clean it up
let text = clone.textContent || '';
// Remove excessive whitespace
text = text.replace(/\s+/g, ' ').trim();
// Limit length to prevent excessive data
const maxLength = 50000; // 50KB limit
if (text.length > maxLength) {
text = text.substring(0, maxLength) + '...';
}
return text;
}
// Simple HTML to Markdown converter
function htmlToMarkdown(element) {
const clone = element.cloneNode(true);
// Remove script and style elements
clone
.querySelectorAll('script, style, noscript')
.forEach((el) => el.remove());
let markdown = '';
const walker = document.createTreeWalker(
clone,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
null,
false,
);
let node;
let listStack = [];
while ((node = walker.nextNode())) {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent.trim();
if (text) {
markdown += text + ' ';
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
switch (node.tagName.toLowerCase()) {
case 'h1':
markdown += '\n# ' + node.textContent.trim() + '\n';
break;
case 'h2':
markdown += '\n## ' + node.textContent.trim() + '\n';
break;
case 'h3':
markdown += '\n### ' + node.textContent.trim() + '\n';
break;
case 'h4':
markdown += '\n#### ' + node.textContent.trim() + '\n';
break;
case 'h5':
markdown += '\n##### ' + node.textContent.trim() + '\n';
break;
case 'h6':
markdown += '\n###### ' + node.textContent.trim() + '\n';
break;
case 'p':
markdown += '\n' + node.textContent.trim() + '\n';
break;
case 'br':
markdown += '\n';
break;
case 'a':
const href = node.getAttribute('href');
const text = node.textContent.trim();
if (href) {
markdown += `[${text}](${href}) `;
}
break;
case 'img':
const src = node.getAttribute('src');
const alt = node.getAttribute('alt') || '';
if (src) {
markdown += `![${alt}](${src}) `;
}
break;
case 'ul':
case 'ol':
markdown += '\n';
listStack.push(node.tagName.toLowerCase());
break;
case 'li':
const listType = listStack[listStack.length - 1];
const prefix = listType === 'ol' ? '1. ' : '- ';
markdown += prefix + node.textContent.trim() + '\n';
break;
case 'code':
markdown += '`' + node.textContent + '`';
break;
case 'pre':
markdown += '\n```\n' + node.textContent + '\n```\n';
break;
case 'blockquote':
markdown += '\n> ' + node.textContent.trim() + '\n';
break;
case 'strong':
case 'b':
markdown += '**' + node.textContent + '**';
break;
case 'em':
case 'i':
markdown += '*' + node.textContent + '*';
break;
}
}
}
// Limit markdown length
const maxLength = 30000;
if (markdown.length > maxLength) {
markdown = markdown.substring(0, maxLength) + '...';
}
return markdown.trim();
}
// Check if link is external
function isExternalLink(url) {
try {
const link = new URL(url);
return link.hostname !== window.location.hostname;
} catch {
return false;
}
}
// Get first paint time
function getFirstPaintTime() {
if (window.performance && window.performance.getEntriesByType) {
const paintEntries = window.performance.getEntriesByType('paint');
const firstPaint = paintEntries.find(
(entry) => entry.name === 'first-paint',
);
return firstPaint ? firstPaint.startTime : null;
}
return null;
}
// Console log interceptor
const consoleLogs = [];
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
};
// Intercept console methods
['log', 'error', 'warn', 'info'].forEach((method) => {
console[method] = function (...args) {
// Store the log
consoleLogs.push({
type: method,
message: args
.map((arg) => {
try {
if (typeof arg === 'object') {
return JSON.stringify(arg);
}
return String(arg);
} catch {
return String(arg);
}
})
.join(' '),
timestamp: new Date().toISOString(),
stack: new Error().stack,
});
// Keep only last 100 logs to prevent memory issues
if (consoleLogs.length > 100) {
consoleLogs.shift();
}
// Call original console method
originalConsole[method].apply(console, args);
};
});
// Get selected text
function getSelectedText() {
return window.getSelection().toString();
}
// Highlight element on page
function highlightElement(selector) {
try {
const element = document.querySelector(selector);
if (element) {
// Store original style
const originalStyle = element.style.cssText;
// Apply highlight
element.style.cssText += `
outline: 3px solid #FF6B6B !important;
background-color: rgba(255, 107, 107, 0.1) !important;
transition: all 0.3s ease !important;
`;
// Remove highlight after 3 seconds
setTimeout(() => {
element.style.cssText = originalStyle;
}, 3000);
return true;
}
return false;
} catch (error) {
console.error('Failed to highlight element:', error);
return false;
}
}
// Execute custom JavaScript in page context
function executeInPageContext(code) {
try {
const script = document.createElement('script');
script.textContent = `
(function() {
try {
const result = ${code};
window.postMessage({
type: 'QWEN_BRIDGE_RESULT',
success: true,
result: result
}, '*');
} catch (error) {
window.postMessage({
type: 'QWEN_BRIDGE_RESULT',
success: false,
error: error.message
}, '*');
}
})();
`;
document.documentElement.appendChild(script);
script.remove();
return new Promise((resolve, reject) => {
const listener = (event) => {
if (event.data && event.data.type === 'QWEN_BRIDGE_RESULT') {
window.removeEventListener('message', listener);
if (event.data.success) {
resolve(event.data.result);
} else {
reject(new Error(event.data.error));
}
}
};
window.addEventListener('message', listener);
// Timeout after 5 seconds
setTimeout(() => {
window.removeEventListener('message', listener);
reject(new Error('Execution timeout'));
}, 5000);
});
} catch (error) {
return Promise.reject(error);
}
}
// Message listener for communication with background script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('Content script received message:', request);
switch (request.type) {
case 'EXTRACT_DATA':
// Extract and send page data
const pageData = extractPageData();
pageData.consoleLogs = consoleLogs;
sendResponse({
success: true,
data: pageData,
});
break;
case 'GET_CONSOLE_LOGS':
// Get captured console logs
sendResponse({
success: true,
data: consoleLogs.slice(), // Return a copy
});
break;
case 'GET_SELECTED_TEXT':
// Get currently selected text
sendResponse({
success: true,
data: getSelectedText(),
});
break;
case 'HIGHLIGHT_ELEMENT':
// Highlight an element on the page
const highlighted = highlightElement(request.selector);
sendResponse({
success: highlighted,
});
break;
case 'EXECUTE_CODE':
// Execute JavaScript in page context
executeInPageContext(request.code)
.then((result) => {
sendResponse({
success: true,
data: result,
});
})
.catch((error) => {
sendResponse({
success: false,
error: error.message,
});
});
return true; // Will respond asynchronously
case 'SCROLL_TO':
// Scroll to specific position
window.scrollTo({
top: request.y || 0,
left: request.x || 0,
behavior: request.smooth ? 'smooth' : 'auto',
});
sendResponse({ success: true });
break;
case 'QWEN_EVENT':
// Handle events from Qwen CLI
console.log('Qwen event received:', request.event);
// Could trigger UI updates or other actions based on event
break;
default:
sendResponse({
success: false,
error: 'Unknown request type',
});
}
});
// Notify background script that content script is loaded
chrome.runtime
.sendMessage({
type: 'CONTENT_SCRIPT_LOADED',
url: window.location.href,
})
.catch(() => {
// Ignore errors if background script is not ready
});
// Export for testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
extractPageData,
extractTextContent,
htmlToMarkdown,
getSelectedText,
highlightElement,
};
}
}

View File

@@ -0,0 +1,773 @@
/**
* Chrome Extension Side Panel App
* Simplified version adapted from vscode-ide-companion
*/
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useVSCode } from './hooks/useVSCode.js';
import { InputForm } from './components/layout/InputForm.js';
import { EmptyState } from './components/layout/EmptyState.js';
import {
UserMessage,
AssistantMessage,
WaitingMessage,
} from './components/messages/index.js';
import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer.js';
import type {
PermissionOption,
ToolCall,
} from './components/PermissionDrawer/PermissionRequest.js';
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
export const App: React.FC = () => {
const vscode = useVSCode();
// State
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, setInputText] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
const [loadingMessage, setLoadingMessage] = useState<string | null>(null);
const [streamingContent, setStreamingContent] = useState('');
// Debug: cache slash-commands (available_commands_update) & MCP tools list
const [availableCommands, setAvailableCommands] = useState<any[]>([]);
const [mcpTools, setMcpTools] = useState<any[]>([]);
const [internalTools, setInternalTools] = useState<any[]>([]);
const [showToolsPanel, setShowToolsPanel] = useState(false);
const [authUri, setAuthUri] = useState<string | null>(null);
const [isComposing, setIsComposing] = useState(false);
const [permissionRequest, setPermissionRequest] = useState<{
requestId: number;
options: PermissionOption[];
toolCall: ToolCall;
} | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const inputFieldRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, streamingContent]);
// Listen for messages from background script
useEffect(() => {
const handleMessage = (message: { type: string; data?: unknown }) => {
console.log('[App] Received message:', message);
switch (message.type) {
case 'STATUS_UPDATE':
const statusData = message.data as { status: string } | undefined;
if (statusData && 'status' in statusData) {
setIsConnected(statusData.status !== 'disconnected');
} else {
setIsConnected(false); // default to disconnected if status data is missing
}
break;
case 'hostInfo': {
console.log('[HostInfo]', (message as any).data);
break;
}
case 'hostLog': {
const line = (message as { data?: { line?: string } }).data?.line;
if (line) console.log('[HostLog]', line);
break;
}
case 'authUpdate': {
const uri = (message as { data?: { authUri?: string } }).data
?.authUri;
if (uri) setAuthUri(uri);
break;
}
case 'availableCommands': {
const cmds =
(message as { data?: { availableCommands?: any[] } }).data
?.availableCommands || [];
setAvailableCommands(cmds);
console.log('[App] Available commands:', cmds);
break;
}
case 'mcpTools': {
const tools =
(message as { data?: { tools?: any[] } }).data?.tools || [];
setMcpTools(tools);
console.log('[App] MCP tools:', tools);
break;
}
case 'internalMcpTools': {
const tools =
(message as { data?: { tools?: any[] } }).data?.tools || [];
setInternalTools(tools);
console.log('[App] Internal MCP tools:', tools);
break;
}
case 'toolProgress': {
const payload =
(
message as {
data?: {
name?: string;
stage?: string;
ok?: boolean;
error?: string;
};
}
).data || ({} as any);
const name = payload.name || '';
const stage = payload.stage || '';
const ok = payload.ok;
const pretty = (n: string) => {
switch (n) {
case 'read_page':
return 'Read Page';
case 'capture_screenshot':
return 'Capture Screenshot';
case 'get_network_logs':
return 'Get Network Logs';
case 'get_console_logs':
return 'Get Console Logs';
default:
return n;
}
};
if (stage === 'start') {
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Running tool: ${pretty(name)}`,
timestamp: Date.now(),
},
]);
} else if (stage === 'end') {
const endText =
ok === false
? `Tool failed: ${pretty(name)}${payload.error ? `${payload.error}` : ''}`
: `Tool finished: ${pretty(name)}`;
setMessages((prev) => [
...prev,
{ role: 'assistant', content: endText, timestamp: Date.now() },
]);
}
break;
}
case 'streamStart':
setIsStreaming(true);
setIsWaitingForResponse(false);
setStreamingContent('');
break;
case 'streamChunk':
setStreamingContent(
(prev) =>
prev +
((message as { data: { chunk: string } }).data?.chunk || ''),
);
break;
case 'streamEnd':
if (streamingContent) {
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: streamingContent,
timestamp: Date.now(),
},
]);
}
setIsStreaming(false);
setStreamingContent('');
break;
case 'message':
const msgData = (message as { data: Message }).data;
if (msgData) {
setMessages((prev) => [
...prev,
{
role: msgData.role,
content: msgData.content,
timestamp: msgData.timestamp || Date.now(),
},
]);
}
break;
case 'error':
setIsStreaming(false);
setIsWaitingForResponse(false);
setLoadingMessage(null);
break;
case 'permissionRequest':
// Handle permission request from Qwen CLI
console.log('[App] Permission request:', message);
const permData = (
message as {
data: {
requestId: number;
options: PermissionOption[];
toolCall: ToolCall;
};
}
).data;
if (permData) {
setPermissionRequest({
requestId: permData.requestId,
options: permData.options,
toolCall: permData.toolCall,
});
}
break;
}
};
// Add Chrome message listener
if (typeof chrome !== 'undefined' && chrome.runtime) {
chrome.runtime.onMessage.addListener(handleMessage);
return () => {
chrome.runtime.onMessage.removeListener(handleMessage);
};
}
}, [streamingContent]);
// Check connection status on mount
useEffect(() => {
const checkStatus = async () => {
const response = (await vscode.postMessage({ type: 'GET_STATUS' })) as {
connected?: boolean;
status?: string;
availableCommands?: any[];
mcpTools?: any[];
internalTools?: any[];
} | null;
if (response) {
setIsConnected(response.connected || false);
if (Array.isArray(response.availableCommands)) {
setAvailableCommands(response.availableCommands);
}
if (Array.isArray(response.mcpTools)) {
setMcpTools(response.mcpTools);
}
if (Array.isArray(response.internalTools)) {
setInternalTools(response.internalTools);
}
}
};
checkStatus();
}, [vscode]);
// Handle submit
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
const text = inputText.trim();
if (!text || isStreaming || isWaitingForResponse) return;
// Add user message
setMessages((prev) => [
...prev,
{
role: 'user',
content: text,
timestamp: Date.now(),
},
]);
// Clear input
setInputText('');
if (inputFieldRef.current) {
inputFieldRef.current.textContent = '';
}
// Send to background
setIsWaitingForResponse(true);
setLoadingMessage('Thinking...');
await vscode.postMessage({
type: 'sendMessage',
data: { text },
});
},
[inputText, isStreaming, isWaitingForResponse, vscode],
);
// Handle cancel
const handleCancel = useCallback(async () => {
await vscode.postMessage({ type: 'cancelStreaming', data: {} });
setIsStreaming(false);
setIsWaitingForResponse(false);
setLoadingMessage(null);
}, [vscode]);
// Handle connect
const handleConnect = useCallback(async () => {
setLoadingMessage('Connecting...');
const response = (await vscode.postMessage({ type: 'CONNECT' })) as {
success?: boolean;
status?: string;
} | null;
if (response?.success) {
setIsConnected(true);
setLoadingMessage(null);
} else {
setLoadingMessage('Connection failed');
setTimeout(() => setLoadingMessage(null), 3000);
}
}, [vscode]);
// Read current page and ask Qwen to analyze (bypasses MCP; uses content-script extractor)
const handleReadPage = useCallback(async () => {
try {
setIsWaitingForResponse(true);
setLoadingMessage('Reading page...');
const extract = (await vscode.postMessage({
type: 'EXTRACT_PAGE_DATA',
})) as any;
if (!extract || !extract.success) {
setIsWaitingForResponse(false);
setLoadingMessage(null);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Read Page failed: ${extract?.error || 'unknown error'}`,
timestamp: Date.now(),
},
]);
return;
}
await vscode.postMessage({
type: 'SEND_TO_QWEN',
action: 'analyze_page',
data: extract.data,
});
// streamStart will arrive from service worker; keep waiting state until it starts streaming
} catch (err: any) {
setIsWaitingForResponse(false);
setLoadingMessage(null);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Read Page error: ${err?.message || String(err)}`,
timestamp: Date.now(),
},
]);
}
}, [vscode]);
// Get network logs and send to Qwen to analyze (bypasses MCP; uses debugger API)
const handleGetNetworkLogs = useCallback(async () => {
try {
setIsWaitingForResponse(true);
setLoadingMessage('Collecting network logs...');
const resp = (await vscode.postMessage({
type: 'GET_NETWORK_LOGS',
})) as any;
if (!resp || !resp.success) {
setIsWaitingForResponse(false);
setLoadingMessage(null);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Get Network Logs failed: ${resp?.error || 'unknown error'}`,
timestamp: Date.now(),
},
]);
return;
}
const logs = resp.data || resp.logs || [];
const summary = Array.isArray(logs) ? logs.slice(-50) : [];
const text =
`Network logs (last ${summary.length} entries):\n` +
JSON.stringify(
summary.map((l: any) => ({
method: l.method,
url: l.params?.request?.url || l.params?.documentURL,
status: l.params?.response?.status,
timestamp: l.timestamp,
})),
null,
2,
);
// Show a short message to user
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: 'Running tool: Get Network Logs…',
timestamp: Date.now(),
},
]);
// Ask Qwen to analyze
await vscode.postMessage({
type: 'SEND_TO_QWEN',
action: 'ai_analyze',
data: {
pageData: { content: { text } },
prompt:
'Please analyze these network logs, list failed or slow requests and possible causes.',
},
});
} catch (err: any) {
setIsWaitingForResponse(false);
setLoadingMessage(null);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Get Network Logs error: ${err?.message || String(err)}`,
timestamp: Date.now(),
},
]);
}
}, [vscode]);
// Handle permission response
const handlePermissionResponse = useCallback(
(optionId: string) => {
if (!permissionRequest) return;
console.log(
'[App] Sending permission response:',
optionId,
'for requestId:',
permissionRequest.requestId,
);
vscode.postMessage({
type: 'permissionResponse',
data: {
requestId: permissionRequest.requestId,
optionId,
},
});
setPermissionRequest(null);
},
[vscode, permissionRequest],
);
// Get console logs and send to Qwen to analyze (bypasses MCP; uses content-script capture)
const handleGetConsoleLogs = useCallback(async () => {
try {
setIsWaitingForResponse(true);
setLoadingMessage('Collecting console logs...');
const resp = (await vscode.postMessage({
type: 'GET_CONSOLE_LOGS',
})) as any;
if (!resp || !resp.success) {
setIsWaitingForResponse(false);
setLoadingMessage(null);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Get Console Logs failed: ${resp?.error || 'unknown error'}`,
timestamp: Date.now(),
},
]);
return;
}
const logs = resp.data || [];
const formatted = logs
.slice(-50)
.map((l: any) => `[${l.type}] ${l.message}`)
.join('\n');
const text = `Console logs (last ${Math.min(logs.length, 50)} entries):
${formatted || '(no logs captured)'}`;
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: 'Running tool: Get Console Logs…',
timestamp: Date.now(),
},
]);
await vscode.postMessage({
type: 'SEND_TO_QWEN',
action: 'ai_analyze',
data: {
pageData: { content: { text } },
prompt:
'Please analyze these console logs and summarize errors/warnings.',
},
});
} catch (err: any) {
setIsWaitingForResponse(false);
setLoadingMessage(null);
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: `Get Console Logs error: ${err?.message || String(err)}`,
timestamp: Date.now(),
},
]);
}
}, [vscode]);
const hasContent = messages.length > 0 || isStreaming || streamingContent;
return (
<div className="chat-container relative flex flex-col h-screen bg-[#1e1e1e] text-white">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b border-gray-700">
<h1 className="text-sm font-medium">Qwen Code</h1>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-gray-500'}`}
/>
<span className="text-xs text-gray-400">
{isConnected
? `Connected (${mcpTools.length + internalTools.length} tools)`
: 'Disconnected'}
</span>
{isConnected && (
<button
className="text-xs px-2 py-0.5 rounded bg-gray-700 hover:bg-gray-600"
onClick={handleReadPage}
title="Read current page"
>
Read Page
</button>
)}
{isConnected && (
<button
className="text-xs px-2 py-0.5 rounded bg-gray-700 hover:bg-gray-600"
onClick={handleGetNetworkLogs}
title="Get network logs"
>
Network Logs
</button>
)}
{isConnected && (
<button
className="text-xs px-2 py-0.5 rounded bg-gray-700 hover:bg-gray-600"
onClick={handleGetConsoleLogs}
title="Get console logs"
>
Console Logs
</button>
)}
{isConnected && mcpTools.length + internalTools.length > 0 && (
<button
className="text-xs px-2 py-0.5 rounded bg-gray-700 hover:bg-gray-600"
onClick={() => setShowToolsPanel((v) => !v)}
title="Show available tools"
>
Tools
</button>
)}
</div>
</div>
{/* Messages */}
<div
ref={messagesContainerRef}
className="flex-1 overflow-y-auto p-4 space-y-4"
>
{!hasContent ? (
<EmptyState
isAuthenticated={isConnected}
loadingMessage={!isConnected ? 'Click Connect to start' : undefined}
/>
) : (
<>
{messages.map((msg, index) =>
msg.role === 'user' ? (
<UserMessage
key={index}
content={msg.content}
timestamp={msg.timestamp}
onFileClick={() => {}}
/>
) : (
<AssistantMessage
key={index}
content={msg.content}
timestamp={msg.timestamp}
onFileClick={() => {}}
/>
),
)}
{/* Streaming message */}
{isStreaming && streamingContent && (
<AssistantMessage
content={streamingContent}
timestamp={Date.now()}
onFileClick={() => {}}
/>
)}
{/* Waiting indicator */}
{isWaitingForResponse && loadingMessage && (
<WaitingMessage loadingMessage={loadingMessage} />
)}
{/* If streaming started but no chunks yet, show thinking indicator */}
{isStreaming && !streamingContent && (
<WaitingMessage
loadingMessage={loadingMessage || 'Thinking...'}
/>
)}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* Input */}
{isConnected ? (
<InputForm
inputText={inputText}
inputFieldRef={inputFieldRef as React.RefObject<HTMLDivElement>}
isStreaming={isStreaming}
isWaitingForResponse={isWaitingForResponse}
isComposing={isComposing}
editMode="default"
thinkingEnabled={false}
activeFileName={null}
activeSelection={null}
skipAutoActiveContext={true}
onInputChange={setInputText}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onKeyDown={() => {}}
onSubmit={handleSubmit}
onCancel={handleCancel}
onToggleEditMode={() => {}}
onToggleThinking={() => {}}
onFocusActiveEditor={() => {}}
onToggleSkipAutoActiveContext={() => {}}
onShowCommandMenu={() => {}}
onAttachContext={() => {}}
completionIsOpen={false}
completionItems={[]}
onCompletionSelect={() => {}}
onCompletionClose={() => {}}
/>
) : (
<div className="p-4 border-t border-gray-700">
<button
onClick={handleConnect}
className="w-full py-2 px-4 bg-indigo-600 hover:bg-indigo-700 rounded text-white text-sm font-medium transition-colors"
>
Connect to Qwen CLI
</button>
</div>
)}
{/* Permission Request Drawer */}
{permissionRequest && (
<PermissionDrawer
isOpen={!!permissionRequest}
options={permissionRequest.options}
toolCall={permissionRequest.toolCall}
onResponse={handlePermissionResponse}
onClose={() => setPermissionRequest(null)}
/>
)}
{/* Auth Required banner */}
{authUri && (
<div className="absolute left-3 right-3 top-10 z-50 bg-[#2a2d2e] border border-yellow-600 text-yellow-200 rounded p-2 text-[12px] flex items-center justify-between gap-2">
<div>Authentication required. Click to sign in.</div>
<div className="flex items-center gap-2">
<button
className="px-2 py-0.5 rounded bg-yellow-700 hover:bg-yellow-600 text-white"
onClick={() => {
try {
chrome.tabs.create({ url: authUri });
} catch (_) {}
}}
>
Open Link
</button>
<button
className="px-2 py-0.5 rounded bg-gray-700 hover:bg-gray-600"
onClick={() => setAuthUri(null)}
>
Dismiss
</button>
</div>
</div>
)}
{/* Debug: Tools panel */}
{showToolsPanel && mcpTools.length + internalTools.length > 0 && (
<div className="absolute right-3 top-10 z-50 max-w-[80%] w-[360px] max-h-[50vh] overflow-auto bg-[#2a2d2e] text-[13px] text-gray-200 border border-gray-700 rounded shadow-lg p-2">
<div className="flex items-center justify-between mb-2">
<div className="font-semibold">
Available Tools ({mcpTools.length + internalTools.length})
</div>
<button
className="text-gray-400 hover:text-gray-200"
onClick={() => setShowToolsPanel(false)}
>
×
</button>
</div>
<div className="text-[11px] text-gray-400 mb-1">
Internal (chrome-browser)
</div>
<ul className="space-y-1 mb-2">
{internalTools.map((t: any, i: number) => {
const name = (t && (t.name || t.tool?.name)) || String(t);
const desc = (t && (t.description || t.tool?.description)) || '';
return (
<li
key={`internal-${i}`}
className="px-2 py-1 rounded hover:bg-[#3a3d3e]"
>
<div className="font-mono text-xs text-[#a6e22e] break-all">
{name}
</div>
{desc && (
<div className="text-[11px] text-gray-400 break-words">
{desc}
</div>
)}
</li>
);
})}
</ul>
<div className="text-[11px] text-gray-400 mb-1">Discovered (MCP)</div>
<ul className="space-y-1">
{mcpTools.map((t: any, i: number) => {
const name = (t && (t.name || t.tool?.name)) || String(t);
const desc = (t && (t.description || t.tool?.description)) || '';
return (
<li
key={`discovered-${i}`}
className="px-2 py-1 rounded hover:bg-[#3a3d3e]"
>
<div className="font-mono text-xs text-[#a6e22e] break-all">
{name}
</div>
{desc && (
<div className="text-[11px] text-gray-400 break-words">
{desc}
</div>
)}
</li>
);
})}
</ul>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,312 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useEffect, useState, useRef } from 'react';
import type { PermissionOption, ToolCall } from './PermissionRequest.js';
interface PermissionDrawerProps {
isOpen: boolean;
options: PermissionOption[];
toolCall: ToolCall;
onResponse: (optionId: string) => void;
onClose?: () => void;
}
export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
isOpen,
options,
toolCall,
onResponse,
onClose,
}) => {
const [focusedIndex, setFocusedIndex] = useState(0);
const [customMessage, setCustomMessage] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
// Correct the ref type for custom input to HTMLInputElement to avoid subsequent forced casting
const customInputRef = useRef<HTMLInputElement>(null);
console.log('PermissionDrawer rendered with isOpen:', isOpen, toolCall);
// Prefer file name from locations, fall back to content[].path if present
const getAffectedFileName = (): string => {
const fromLocations = toolCall.locations?.[0]?.path;
if (fromLocations) {
return fromLocations.split('/').pop() || fromLocations;
}
// Some tool calls (e.g. write/edit with diff content) only include path in content
const fromContent = Array.isArray(toolCall.content)
? (
toolCall.content.find(
(c: unknown) =>
typeof c === 'object' &&
c !== null &&
'path' in (c as Record<string, unknown>),
) as { path?: unknown } | undefined
)?.path
: undefined;
if (typeof fromContent === 'string' && fromContent.length > 0) {
return fromContent.split('/').pop() || fromContent;
}
return 'file';
};
// Get the title for the permission request
const getTitle = () => {
if (toolCall.kind === 'edit' || toolCall.kind === 'write') {
const fileName = getAffectedFileName();
return (
<>
Make this edit to{' '}
<span className="font-mono text-[var(--app-primary-foreground)]">
{fileName}
</span>
?
</>
);
}
if (toolCall.kind === 'execute' || toolCall.kind === 'bash') {
return 'Allow this bash command?';
}
if (toolCall.kind === 'read') {
const fileName = getAffectedFileName();
return (
<>
Allow read from{' '}
<span className="font-mono text-[var(--app-primary-foreground)]">
{fileName}
</span>
?
</>
);
}
return toolCall.title || 'Permission Required';
};
// Handle keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen) {
return;
}
// Number keys 1-9 for quick select
const numMatch = e.key.match(/^[1-9]$/);
if (
numMatch &&
!customInputRef.current?.contains(document.activeElement)
) {
const index = parseInt(e.key, 10) - 1;
if (index < options.length) {
e.preventDefault();
onResponse(options[index].optionId);
}
return;
}
// Arrow keys for navigation
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
const totalItems = options.length + 1; // +1 for custom input
if (e.key === 'ArrowDown') {
setFocusedIndex((prev) => (prev + 1) % totalItems);
} else {
setFocusedIndex((prev) => (prev - 1 + totalItems) % totalItems);
}
}
// Enter to select
if (
e.key === 'Enter' &&
!customInputRef.current?.contains(document.activeElement)
) {
e.preventDefault();
if (focusedIndex < options.length) {
onResponse(options[focusedIndex].optionId);
}
}
// Escape to cancel permission and close (align with CLI behavior)
if (e.key === 'Escape') {
e.preventDefault();
const rejectOptionId =
options.find((o) => o.kind.includes('reject'))?.optionId ||
options.find((o) => o.optionId === 'cancel')?.optionId ||
'cancel';
onResponse(rejectOptionId);
if (onClose) {
onClose();
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, options, onResponse, onClose, focusedIndex]);
// Focus container when opened
useEffect(() => {
if (isOpen && containerRef.current) {
containerRef.current.focus();
}
}, [isOpen]);
// Reset focus to the first option when the drawer opens or the options change
useEffect(() => {
if (isOpen) {
setFocusedIndex(0);
}
}, [isOpen, options.length]);
if (!isOpen) {
return null;
}
return (
<div className="fixed inset-x-0 bottom-0 z-[1000] p-2">
{/* Main container */}
<div
ref={containerRef}
className="relative flex flex-col rounded-large border p-2 outline-none animate-slide-up"
style={{
backgroundColor: 'var(--app-input-secondary-background)',
borderColor: 'var(--app-input-border)',
}}
tabIndex={0}
data-focused-index={focusedIndex}
>
{/* Background layer */}
<div
className="p-2 absolute inset-0 rounded-large"
style={{ backgroundColor: 'var(--app-input-background)' }}
/>
{/* Title + Description (from toolCall.title) */}
<div className="relative z-[1] text-[1.1em] text-[var(--app-primary-foreground)] flex flex-col min-h-0">
<div className="font-bold text-[var(--app-primary-foreground)] mb-0.5">
{getTitle()}
</div>
{(toolCall.kind === 'edit' ||
toolCall.kind === 'write' ||
toolCall.kind === 'read' ||
toolCall.kind === 'execute' ||
toolCall.kind === 'bash') &&
toolCall.title && (
<div
/* 13px, normal font weight; normal whitespace wrapping + long word breaking; maximum 3 lines with overflow ellipsis */
className="text-[13px] font-normal text-[var(--app-secondary-foreground)] opacity-90 font-mono whitespace-normal break-words q-line-clamp-3 mb-2"
style={{
fontSize: '.9em',
color: 'var(--app-secondary-foreground)',
marginBottom: '6px',
}}
title={toolCall.title}
>
{toolCall.title}
</div>
)}
</div>
{/* Options */}
<div className="relative z-[1] flex flex-col gap-1 pb-1">
{options.map((option, index) => {
const isFocused = focusedIndex === index;
return (
<button
key={option.optionId}
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] hover:bg-[var(--app-button-background)] ${
isFocused
? 'text-[var(--app-list-active-foreground)] bg-[var(--app-list-active-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
: 'hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
}`}
onClick={() => onResponse(option.optionId)}
onMouseEnter={() => setFocusedIndex(index)}
>
{/* Number badge */}
<span className="inline-flex items-center justify-center min-w-[10px] h-5 font-semibold opacity-60">
{index + 1}
</span>
{/* Option text */}
<span className="font-semibold">{option.name}</span>
</button>
);
})}
{/* Custom message input (extracted component) */}
{(() => {
const isFocused = focusedIndex === options.length;
const rejectOptionId = options.find((o) =>
o.kind.includes('reject'),
)?.optionId;
return (
<CustomMessageInputRow
isFocused={isFocused}
customMessage={customMessage}
setCustomMessage={setCustomMessage}
onFocusRow={() => setFocusedIndex(options.length)}
onSubmitReject={() => {
if (rejectOptionId) {
onResponse(rejectOptionId);
}
}}
inputRef={customInputRef}
/>
);
})()}
</div>
</div>
{/* Moved slide-up keyframes to Tailwind theme (tailwind.config.js) */}
</div>
);
};
/**
* CustomMessageInputRow: Reusable custom input row component (without hooks)
*/
interface CustomMessageInputRowProps {
isFocused: boolean;
customMessage: string;
setCustomMessage: (val: string) => void;
onFocusRow: () => void; // Set focus when mouse enters or input box is focused
onSubmitReject: () => void; // Triggered when Enter is pressed (selecting reject option)
inputRef: React.RefObject<HTMLInputElement | null>;
}
const CustomMessageInputRow: React.FC<CustomMessageInputRowProps> = ({
isFocused,
customMessage,
setCustomMessage,
onFocusRow,
onSubmitReject,
inputRef,
}) => (
<div
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] cursor-text text-[var(--app-primary-foreground)] ${
isFocused ? 'text-[var(--app-list-active-foreground)]' : ''
}`}
onMouseEnter={onFocusRow}
onClick={() => inputRef.current?.focus()}
>
<input
ref={inputRef as React.LegacyRef<HTMLInputElement> | undefined}
type="text"
placeholder="Tell Qwen what to do instead"
spellCheck={false}
className="flex-1 bg-transparent border-0 outline-none text-sm placeholder:opacity-70"
style={{ color: 'var(--app-input-foreground)' }}
value={customMessage}
onChange={(e) => setCustomMessage(e.target.value)}
onFocus={onFocusRow}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && customMessage.trim()) {
e.preventDefault();
onSubmitReject();
}
}}
/>
</div>
);

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export interface PermissionOption {
name: string;
kind: string;
optionId: string;
}
export interface ToolCall {
title?: string;
kind?: string;
toolCallId?: string;
rawInput?: {
command?: string;
description?: string;
[key: string]: unknown;
};
content?: Array<{
type: string;
[key: string]: unknown;
}>;
locations?: Array<{
path: string;
line?: number | null;
}>;
status?: string;
}
export interface PermissionRequestProps {
options: PermissionOption[];
toolCall: ToolCall;
onResponse: (optionId: string) => void;
}

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