mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-09 10:29:22 +00:00
Compare commits
259 Commits
fix/integr
...
test/chrom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da22de9f36 | ||
|
|
c65436cf61 | ||
|
|
814518889c | ||
|
|
39413b9df9 | ||
|
|
5249937b49 | ||
|
|
c2965c1926 | ||
|
|
570ec432af | ||
|
|
bfc3bbfa9c | ||
|
|
91af9bf6c8 | ||
|
|
f6771c0858 | ||
|
|
2c8be05029 | ||
|
|
4744af1ea8 | ||
|
|
2c285394c7 | ||
|
|
f2d941e469 | ||
|
|
9b2dfe1e06 | ||
|
|
3e695cd82b | ||
|
|
177a91f1d5 | ||
|
|
870d207f18 | ||
|
|
3f512528cb | ||
|
|
0878ee4cbd | ||
|
|
bfe7298858 | ||
|
|
2f2937aafe | ||
|
|
8fcdd86b91 | ||
|
|
d7d7bf0c39 | ||
|
|
b95d9a8d2d | ||
|
|
6f39ae120c | ||
|
|
627857621a | ||
|
|
65c7cf5d8f | ||
|
|
7a823060ac | ||
|
|
2c88ea6dc1 | ||
|
|
ad3086f7dd | ||
|
|
8f3bbef575 | ||
|
|
e2d6ab9b7e | ||
|
|
35bf5ef4d0 | ||
|
|
1d16513e27 | ||
|
|
731fd99800 | ||
|
|
c6ae0a8be7 | ||
|
|
49892a8e17 | ||
|
|
d1a3e828b7 | ||
|
|
b19bb6cb20 | ||
|
|
e8625658ba | ||
|
|
19f8f631b4 | ||
|
|
a4eb3adea8 | ||
|
|
7dc7c6380d | ||
|
|
d2d2b845c5 | ||
|
|
96080f84a6 | ||
|
|
2b6218e564 | ||
|
|
24edf32da8 | ||
|
|
51b08f700c | ||
|
|
58eac7f595 | ||
|
|
32e8b01cf0 | ||
|
|
db9d5cb45d | ||
|
|
473cb7b951 | ||
|
|
e5cced8813 | ||
|
|
73848d3867 | ||
|
|
6a62167f79 | ||
|
|
6ff437671e | ||
|
|
30f9e9c782 | ||
|
|
e4caa7a856 | ||
|
|
aaa66b3172 | ||
|
|
0ae59b900c | ||
|
|
5a5dae1987 | ||
|
|
ac7ba95d65 | ||
|
|
15912892f2 | ||
|
|
e3c20b03bd | ||
|
|
4db50d4158 | ||
|
|
4154493640 | ||
|
|
105ad743fa | ||
|
|
ac3f7cb8c8 | ||
|
|
61aad5a162 | ||
|
|
98c043bf50 | ||
|
|
e27e9a5f18 | ||
|
|
2578d8c151 | ||
|
|
f610133660 | ||
|
|
a877fedc52 | ||
|
|
2bc8079519 | ||
|
|
25dbe98e6e | ||
|
|
e5dbd69899 | ||
|
|
17eb20c134 | ||
|
|
5d59ceb6f3 | ||
|
|
7f645b9726 | ||
|
|
8c109be48c | ||
|
|
e9a1d9a927 | ||
|
|
8aceddffa2 | ||
|
|
cebe0448d0 | ||
|
|
fe7ff5b148 | ||
|
|
919560e3a4 | ||
|
|
26bd4f882d | ||
|
|
4681f15941 | ||
|
|
0a04a4bef2 | ||
|
|
3787e95572 | ||
|
|
7233d37bd1 | ||
|
|
93dcca5147 | ||
|
|
f7d04323f3 | ||
|
|
9a27857f10 | ||
|
|
452f4f3c0e | ||
|
|
5cc01e5e09 | ||
|
|
ac0be9fb84 | ||
|
|
5417de4219 | ||
|
|
257c6705e1 | ||
|
|
27e7438b75 | ||
|
|
8a3ff8db12 | ||
|
|
26f8b67d4f | ||
|
|
b64d636280 | ||
|
|
781c57b438 | ||
|
|
c81c24d45d | ||
|
|
c53bdde747 | ||
|
|
99db18069d | ||
|
|
422998d7f0 | ||
|
|
a0a5b831d4 | ||
|
|
68628bf952 | ||
|
|
8f74dd224c | ||
|
|
b931d28f35 | ||
|
|
4407597794 | ||
|
|
101bd5f9b3 | ||
|
|
61c626b618 | ||
|
|
9f65bd3b39 | ||
|
|
a28278e950 | ||
|
|
2b3830cf83 | ||
|
|
90bf101040 | ||
|
|
2b9140940d | ||
|
|
a8f7bab544 | ||
|
|
4efdea0981 | ||
|
|
05791d4200 | ||
|
|
add35d2904 | ||
|
|
4ca62ba836 | ||
|
|
660901e1fd | ||
|
|
e5efad89e0 | ||
|
|
8e64c5acaf | ||
|
|
bc2a7efcb3 | ||
|
|
1dfd880e17 | ||
|
|
e09bb5f5c0 | ||
|
|
24d11179d8 | ||
|
|
4f970c9987 | ||
|
|
2ef8b6f350 | ||
|
|
5779f7ab1d | ||
|
|
398a1044ce | ||
|
|
251031cfc5 | ||
|
|
10a0c843c1 | ||
|
|
77c257d9d0 | ||
|
|
955547d523 | ||
|
|
3bc862df89 | ||
|
|
4311af96eb | ||
|
|
b49c11e9a2 | ||
|
|
642dda0315 | ||
|
|
bbbdeb280d | ||
|
|
0d43ddee2a | ||
|
|
50e03f2dd6 | ||
|
|
f440ff2f7f | ||
|
|
9a6b0abc37 | ||
|
|
2b68cc8b74 | ||
|
|
9cdd85c62a | ||
|
|
00547ba439 | ||
|
|
fc1dac9dc7 | ||
|
|
338eb9038d | ||
|
|
87d8d82be7 | ||
|
|
e0b9044833 | ||
|
|
f33f43e2f7 | ||
|
|
43e0815def | ||
|
|
0c14f4ce08 | ||
|
|
fefc138485 | ||
|
|
4e7929850c | ||
|
|
9cc5c3ed8f | ||
|
|
43c703a79d | ||
|
|
84e190fd9d | ||
|
|
cc3cfb5d65 | ||
|
|
f07259a7c9 | ||
|
|
4d9f25e9fe | ||
|
|
18e9b2340b | ||
|
|
ad427da340 | ||
|
|
484e0fd943 | ||
|
|
a1f893f0c6 | ||
|
|
a60c5c6697 | ||
|
|
a92be72e88 | ||
|
|
52cd1da4a7 | ||
|
|
c5c556a326 | ||
|
|
a8a863581b | ||
|
|
e4468cfcbc | ||
|
|
3bf30ead67 | ||
|
|
a786f61e49 | ||
|
|
b8a16d362a | ||
|
|
17129024f4 | ||
|
|
fa7d857945 | ||
|
|
90489933fd | ||
|
|
3354b56a05 | ||
|
|
d40447cee4 | ||
|
|
ba87cf63f6 | ||
|
|
00a8c6a924 | ||
|
|
156134d3d4 | ||
|
|
b720209888 | ||
|
|
dfe667c364 | ||
|
|
1386fba278 | ||
|
|
d942250905 | ||
|
|
ec32a24508 | ||
|
|
34d8dbf9b2 | ||
|
|
b3b2bc6ad5 | ||
|
|
80bb2890df | ||
|
|
abd9ee2a7b | ||
|
|
b8df689e31 | ||
|
|
15efeb0107 | ||
|
|
c2b59038ae | ||
|
|
27bf42b4f5 | ||
|
|
e610578ecc | ||
|
|
d07ae35c90 | ||
|
|
cb59b5a9dc | ||
|
|
235159216e | ||
|
|
93b30cca29 | ||
|
|
01e62a2120 | ||
|
|
d464f61b72 | ||
|
|
f866f7f071 | ||
|
|
7eabf543b4 | ||
|
|
d2bc46cbb4 | ||
|
|
84eb5c562f | ||
|
|
8106a6b0f4 | ||
|
|
c0839dceac | ||
|
|
12f84fb730 | ||
|
|
f9a1ee2442 | ||
|
|
7b01b26ff5 | ||
|
|
0f3e97ea1c | ||
|
|
f824004f99 | ||
|
|
6ca54beba2 | ||
|
|
e274b4469a | ||
|
|
a4e3d764d3 | ||
|
|
0a39c91264 | ||
|
|
49b3e0dc92 | ||
|
|
25d9c4f1a7 | ||
|
|
d1a6b3207e | ||
|
|
1c62499977 | ||
|
|
4b8b4e2fe8 | ||
|
|
f9da1b819e | ||
|
|
36fb6b8291 | ||
|
|
f47c762620 | ||
|
|
573c33f68a | ||
|
|
8673426d5c | ||
|
|
32c085cf7d | ||
|
|
725843f9b3 | ||
|
|
54fd63f04b | ||
|
|
59c3d3d0f9 | ||
|
|
177fc42f04 | ||
|
|
b272ac0119 | ||
|
|
574d89da14 | ||
|
|
4f2b2d0a3e | ||
|
|
44794121a8 | ||
|
|
2560c2d1a2 | ||
|
|
84cccfe99a | ||
|
|
b6a3ab11e0 | ||
|
|
bd6e16d41b | ||
|
|
16939c0bc8 | ||
|
|
6fc09a82fb | ||
|
|
d622f8d1bf | ||
|
|
28d178b5c1 | ||
|
|
4c69d536ac | ||
|
|
403fd06117 | ||
|
|
d9928eab66 | ||
|
|
2f0fa267c8 | ||
|
|
fa6ae0a324 | ||
|
|
387be44866 | ||
|
|
51b82771da | ||
|
|
629cd14fad |
9
.github/workflows/e2e.yml
vendored
9
.github/workflows/e2e.yml
vendored
@@ -18,8 +18,6 @@ jobs:
|
|||||||
- 'sandbox:docker'
|
- 'sandbox:docker'
|
||||||
node-version:
|
node-version:
|
||||||
- '20.x'
|
- '20.x'
|
||||||
- '22.x'
|
|
||||||
- '24.x'
|
|
||||||
steps:
|
steps:
|
||||||
- name: 'Checkout'
|
- name: 'Checkout'
|
||||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||||
@@ -67,10 +65,13 @@ jobs:
|
|||||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||||
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
||||||
KEEP_OUTPUT: 'true'
|
KEEP_OUTPUT: 'true'
|
||||||
SANDBOX: '${{ matrix.sandbox }}'
|
|
||||||
VERBOSE: 'true'
|
VERBOSE: 'true'
|
||||||
run: |-
|
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:
|
e2e-test-macos:
|
||||||
name: 'E2E Test - macOS'
|
name: 'E2E Test - macOS'
|
||||||
|
|||||||
143
.github/workflows/release-sdk.yml
vendored
143
.github/workflows/release-sdk.yml
vendored
@@ -33,6 +33,10 @@ on:
|
|||||||
type: 'boolean'
|
type: 'boolean'
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: '${{ github.workflow }}'
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-sdk:
|
release-sdk:
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
@@ -46,6 +50,7 @@ jobs:
|
|||||||
packages: 'write'
|
packages: 'write'
|
||||||
id-token: 'write'
|
id-token: 'write'
|
||||||
issues: 'write'
|
issues: 'write'
|
||||||
|
pull-requests: 'write'
|
||||||
outputs:
|
outputs:
|
||||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||||
|
|
||||||
@@ -86,6 +91,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
scope: '@qwen-code'
|
||||||
|
|
||||||
- name: 'Install Dependencies'
|
- name: 'Install Dependencies'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -121,6 +128,19 @@ jobs:
|
|||||||
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
|
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
|
||||||
MANUAL_VERSION: '${{ inputs.version }}'
|
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'
|
- name: 'Run Tests'
|
||||||
if: |-
|
if: |-
|
||||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||||
@@ -132,13 +152,6 @@ jobs:
|
|||||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||||
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
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'
|
- name: 'Run SDK Integration Tests'
|
||||||
if: |-
|
if: |-
|
||||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||||
@@ -155,7 +168,21 @@ jobs:
|
|||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
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'
|
- 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'
|
id: 'release_branch'
|
||||||
env:
|
env:
|
||||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||||
@@ -164,50 +191,22 @@ jobs:
|
|||||||
git switch -c "${BRANCH_NAME}"
|
git switch -c "${BRANCH_NAME}"
|
||||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: 'Update package version'
|
- name: 'Commit and Push package version (stable only)'
|
||||||
working-directory: 'packages/sdk-typescript'
|
if: |-
|
||||||
env:
|
${{ steps.vars.outputs.is_dry_run == 'false' && steps.vars.outputs.is_nightly == 'false' && steps.vars.outputs.is_preview == 'false' }}
|
||||||
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'
|
|
||||||
env:
|
env:
|
||||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||||
IS_DRY_RUN: '${{ steps.vars.outputs.is_dry_run }}'
|
|
||||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||||
run: |-
|
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
|
if git diff --staged --quiet; then
|
||||||
echo "No version changes to commit"
|
echo "No version changes to commit"
|
||||||
else
|
else
|
||||||
git commit -m "chore(release): sdk-typescript ${RELEASE_TAG}"
|
git commit -m "chore(release): sdk-typescript ${RELEASE_TAG}"
|
||||||
fi
|
fi
|
||||||
if [[ "${IS_DRY_RUN}" == "false" ]]; then
|
echo "Pushing release branch to remote..."
|
||||||
echo "Pushing release branch to remote..."
|
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
|
||||||
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 }}'
|
|
||||||
|
|
||||||
- name: 'Create GitHub Release and Tag'
|
- name: 'Create GitHub Release and Tag'
|
||||||
if: |-
|
if: |-
|
||||||
@@ -217,12 +216,68 @@ jobs:
|
|||||||
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
RELEASE_BRANCH: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||||
PREVIOUS_RELEASE_TAG: '${{ steps.version.outputs.PREVIOUS_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: |-
|
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}" \
|
gh release create "sdk-typescript-${RELEASE_TAG}" \
|
||||||
--target "$RELEASE_BRANCH" \
|
--target "${TARGET}" \
|
||||||
--title "SDK TypeScript Release ${RELEASE_TAG}" \
|
--title "SDK TypeScript Release ${RELEASE_TAG}" \
|
||||||
--notes-start-tag "sdk-typescript-${PREVIOUS_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'
|
- name: 'Create Issue on Failure'
|
||||||
if: |-
|
if: |-
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -133,8 +133,8 @@ jobs:
|
|||||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||||
run: |
|
run: |
|
||||||
npm run preflight
|
npm run preflight
|
||||||
npm run test:integration:sandbox:none
|
npm run test:integration:cli:sandbox:none
|
||||||
npm run test:integration:sandbox:docker
|
npm run test:integration:cli:sandbox:docker
|
||||||
env:
|
env:
|
||||||
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
||||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ package-lock.json
|
|||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
.cursor
|
.cursor
|
||||||
|
.qoder
|
||||||
|
|
||||||
# OS metadata
|
# OS metadata
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -2,27 +2,6 @@
|
|||||||
|
|
||||||
We would love to accept your patches and contributions to this project.
|
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
|
## Contribution Process
|
||||||
|
|
||||||
### Code Reviews
|
### 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`).
|
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
|
## Development Setup and Workflow
|
||||||
|
|
||||||
This section guides contributors on how to build, modify, and understand the development setup of this project.
|
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:
|
To clone the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/google-gemini/gemini-cli.git # Or your fork's URL
|
git clone https://github.com/QwenLM/qwen-code.git # Or your fork's URL
|
||||||
cd gemini-cli
|
cd qwen-code
|
||||||
```
|
```
|
||||||
|
|
||||||
To install dependencies defined in `package.json` as well as root dependencies:
|
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
|
### 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
|
```bash
|
||||||
npm run build:all
|
npm run build:all
|
||||||
@@ -130,13 +103,13 @@ To skip building the sandbox container, you can use `npm run build` instead.
|
|||||||
|
|
||||||
### Running
|
### 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
|
```bash
|
||||||
npm start
|
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
|
### Running Tests
|
||||||
|
|
||||||
@@ -154,7 +127,7 @@ This will run tests located in the `packages/core` and `packages/cli` directorie
|
|||||||
|
|
||||||
#### Integration Tests
|
#### 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:
|
To run the integration tests, use the following command:
|
||||||
|
|
||||||
@@ -209,19 +182,61 @@ npm run lint
|
|||||||
### Coding Conventions
|
### Coding Conventions
|
||||||
|
|
||||||
- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase.
|
- 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.
|
- **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages.
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
- `packages/`: Contains the individual sub-packages of the project.
|
- `packages/`: Contains the individual sub-packages of the project.
|
||||||
- `cli/`: The command-line interface.
|
- `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.
|
- `docs/`: Contains all project documentation.
|
||||||
- `scripts/`: Utility scripts for building, testing, and development tasks.
|
- `scripts/`: Utility scripts for building, testing, and development tasks.
|
||||||
|
|
||||||
For more detailed architecture, see `docs/architecture.md`.
|
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
|
## Debugging
|
||||||
|
|
||||||
### VS Code:
|
### VS Code:
|
||||||
@@ -231,7 +246,7 @@ For more detailed architecture, see `docs/architecture.md`.
|
|||||||
```bash
|
```bash
|
||||||
npm run debug
|
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`).
|
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.
|
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:
|
To hit a breakpoint inside the sandbox container run:
|
||||||
|
|
||||||
```bash
|
```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
|
### 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.
|
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
|
```bash
|
||||||
DEV=true npm start
|
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.
|
Your running CLI application should then connect to React DevTools.
|
||||||

|
|
||||||
|
|
||||||
## Sandboxing
|
## Sandboxing
|
||||||
|
|
||||||
### macOS Seatbelt
|
> TBD
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Manual Publish
|
## Manual Publish
|
||||||
|
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -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
|
.PHONY: help install build build-sandbox build-all test lint format preflight clean start debug release run-npx create-alias
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Makefile for gemini-cli"
|
@echo "Makefile for qwen-code"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Usage:"
|
@echo "Usage:"
|
||||||
@echo " make install - Install npm dependencies"
|
@echo " make install - Install npm dependencies"
|
||||||
@@ -14,11 +14,11 @@ help:
|
|||||||
@echo " make format - Format the code"
|
@echo " make format - Format the code"
|
||||||
@echo " make preflight - Run formatting, linting, and tests"
|
@echo " make preflight - Run formatting, linting, and tests"
|
||||||
@echo " make clean - Remove generated files"
|
@echo " make clean - Remove generated files"
|
||||||
@echo " make start - Start the Gemini CLI"
|
@echo " make start - Start the Qwen Code CLI"
|
||||||
@echo " make debug - Start the Gemini CLI in debug mode"
|
@echo " make debug - Start the Qwen Code CLI in debug mode"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " make run-npx - Run the CLI using npx (for testing the published package)"
|
@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:
|
install:
|
||||||
npm install
|
npm install
|
||||||
|
|||||||
411
README.md
411
README.md
@@ -1,382 +1,152 @@
|
|||||||
# Qwen Code
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/@qwen-code/qwen-code)
|
[](https://www.npmjs.com/package/@qwen-code/qwen-code)
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
[](https://nodejs.org/)
|
[](https://nodejs.org/)
|
||||||
[](https://www.npmjs.com/package/@qwen-code/qwen-code)
|
[](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>
|
||||||
|
|
||||||
<div align="center">
|
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.
|
||||||
|
|
||||||
<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 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.
|

|
||||||
|
|
||||||
## 💡 Free Options Available
|
## Why Qwen Code?
|
||||||
|
|
||||||
Get started with Qwen Code at no cost using any of these free options:
|
- **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.
|
||||||
### 🔥 Qwen OAuth (Recommended)
|
- **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.
|
||||||
- **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
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
#### Prerequisites
|
||||||
|
|
||||||
Ensure you have [Node.js version 20](https://nodejs.org/en/download) or higher installed.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Node.js 20+
|
||||||
curl -qL https://www.npmjs.com/install.sh | sh
|
curl -qL https://www.npmjs.com/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install from npm
|
#### NPM (recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @qwen-code/qwen-code@latest
|
npm install -g @qwen-code/qwen-code@latest
|
||||||
qwen --version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install from source
|
#### Homebrew (macOS, Linux)
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/QwenLM/qwen-code.git
|
|
||||||
cd qwen-code
|
|
||||||
npm install
|
|
||||||
npm install -g .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install globally with Homebrew (macOS/Linux)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install qwen-code
|
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
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start Qwen Code
|
# Start Qwen Code (interactive)
|
||||||
qwen
|
qwen
|
||||||
|
|
||||||
# Example commands
|
# Then, in the session:
|
||||||
> Explain this codebase structure
|
/help
|
||||||
> Help me refactor this function
|
/auth
|
||||||
> Generate unit tests for this module
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```text
|
||||||
|
What does this project do?
|
||||||
Create or edit `.qwen/settings.json` in your home directory:
|
Explain the codebase structure.
|
||||||
|
Help me refactor this function.
|
||||||
```json
|
Generate unit tests for this module.
|
||||||
{
|
|
||||||
"sessionTokenLimit": 32000
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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>
|
<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/))
|
<video src="https://cloud.video.taobao.com/vod/HLfyppnCHplRV9Qhz2xSqeazHeRzYtG-EYJnHAqtzkQ.mp4" controls>
|
||||||
|
Your browser does not support the video tag.
|
||||||
```bash
|
</video>
|
||||||
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"
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
## Authentication
|
||||||
<summary><b>🌍 For International Users</b></summary>
|
|
||||||
|
|
||||||
**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
|
```bash
|
||||||
export OPENAI_API_KEY="your_api_key_here"
|
/auth
|
||||||
export OPENAI_BASE_URL="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
||||||
export OPENAI_MODEL="qwen3-coder-plus"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**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
|
```bash
|
||||||
export OPENAI_API_KEY="your_api_key_here"
|
export OPENAI_API_KEY="your-api-key-here"
|
||||||
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
|
export OPENAI_BASE_URL="https://api.openai.com/v1" # optional
|
||||||
export OPENAI_MODEL="qwen/qwen3-coder:free"
|
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
|
```bash
|
||||||
cd your-project/
|
cd your-project/
|
||||||
qwen
|
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
|
```bash
|
||||||
# Refactoring
|
cd your-project/
|
||||||
> Refactor this function to improve readability and performance
|
qwen -p "your question"
|
||||||
> 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🔄 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
|
#### IDE integration
|
||||||
# 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
|
|
||||||
|
|
||||||
# File operations
|
Use Qwen Code inside your editor (VS Code and Zed):
|
||||||
> 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🐛 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
|
#### TypeScript SDK
|
||||||
# Performance analysis
|
|
||||||
> Identify performance bottlenecks in this React component
|
|
||||||
> Find all N+1 query problems in the codebase
|
|
||||||
|
|
||||||
# Security audit
|
Build on top of Qwen Code with the TypeScript SDK:
|
||||||
> Check for potential SQL injection vulnerabilities
|
|
||||||
> Find all hardcoded credentials or API keys
|
|
||||||
```
|
|
||||||
|
|
||||||
## Popular Tasks
|
- [Use the Qwen Code SDK](./packages/sdk-typescript/README.md)
|
||||||
|
|
||||||
### 📚 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Commands & Shortcuts
|
## Commands & Shortcuts
|
||||||
|
|
||||||
@@ -386,6 +156,7 @@ qwen
|
|||||||
- `/clear` - Clear conversation history
|
- `/clear` - Clear conversation history
|
||||||
- `/compress` - Compress history to save tokens
|
- `/compress` - Compress history to save tokens
|
||||||
- `/stats` - Show current session information
|
- `/stats` - Show current session information
|
||||||
|
- `/bug` - Submit a bug report
|
||||||
- `/exit` or `/quit` - Exit Qwen Code
|
- `/exit` or `/quit` - Exit Qwen Code
|
||||||
|
|
||||||
### Keyboard Shortcuts
|
### Keyboard Shortcuts
|
||||||
@@ -394,6 +165,19 @@ qwen
|
|||||||
- `Ctrl+D` - Exit (on empty line)
|
- `Ctrl+D` - Exit (on empty line)
|
||||||
- `Up/Down` - Navigate command history
|
- `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
|
## Benchmark Results
|
||||||
|
|
||||||
### Terminal-Bench Performance
|
### Terminal-Bench Performance
|
||||||
@@ -403,24 +187,19 @@ qwen
|
|||||||
| Qwen Code | Qwen3-Coder-480A35 | 37.5% |
|
| Qwen Code | Qwen3-Coder-480A35 | 37.5% |
|
||||||
| Qwen Code | Qwen3-Coder-30BA3B | 31.3% |
|
| 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
|
## 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
|
## 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.
|
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
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#QwenLM/qwen-code&Date)
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
type: 'separator',
|
type: 'separator',
|
||||||
},
|
},
|
||||||
'sdk-typescript': 'Typescript SDK',
|
'sdk-typescript': 'Typescript SDK',
|
||||||
|
'sdk-java': 'Java SDK(alpha)',
|
||||||
'Dive Into Qwen Code': {
|
'Dive Into Qwen Code': {
|
||||||
title: 'Dive Into Qwen Code',
|
title: 'Dive Into Qwen Code',
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
|
|||||||
312
docs/developers/sdk-java.md
Normal file
312
docs/developers/sdk-java.md
Normal 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.
|
||||||
@@ -627,7 +627,12 @@ The MCP integration tracks several states:
|
|||||||
|
|
||||||
### Schema Compatibility
|
### 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
|
- **Name sanitization:** Tool names are automatically sanitized to meet API requirements
|
||||||
- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing
|
- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Learn how to use Qwen Code as an end user. This section covers:
|
|||||||
- Configuration options
|
- Configuration options
|
||||||
- Troubleshooting
|
- Troubleshooting
|
||||||
|
|
||||||
### [Developer Guide](./developers/contributing)
|
### [Developer Guide](./developers/architecture)
|
||||||
|
|
||||||
Learn how to contribute to and develop Qwen Code. This section covers:
|
Learn how to contribute to and develop Qwen Code. This section covers:
|
||||||
|
|
||||||
|
|||||||
80
docs/native-host-troubleshooting.md
Normal file
80
docs/native-host-troubleshooting.md
Normal 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"
|
||||||
|
```
|
||||||
@@ -189,8 +189,8 @@ Then select "create" and follow the prompts to define:
|
|||||||
> - Create project-specific subagents in `.qwen/agents/` for team sharing
|
> - Create project-specific subagents in `.qwen/agents/` for team sharing
|
||||||
> - Use descriptive `description` fields to enable automatic delegation
|
> - Use descriptive `description` fields to enable automatic delegation
|
||||||
> - Limit tool access to what each subagent actually needs
|
> - Limit tool access to what each subagent actually needs
|
||||||
> - Know more about [Sub Agents](/users/features/sub-agents)
|
> - Know more about [Sub Agents](./features/sub-agents)
|
||||||
> - Know more about [Approval Mode](/users/features/approval-mode)
|
> - Know more about [Approval Mode](./features/approval-mode)
|
||||||
|
|
||||||
## Work with tests
|
## 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
|
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]
|
> [!tip]
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Qwen Code includes the ability to automatically ignore files, similar to `.gitig
|
|||||||
|
|
||||||
## How it works
|
## 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:
|
For the most part, `.qwenignore` follows the conventions of `.gitignore` files:
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> [!tip]
|
> [!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]
|
> [!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:
|
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`
|
### Available settings in `settings.json`
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ Settings are organized into categories. All settings should be placed within the
|
|||||||
|
|
||||||
| Setting | Type | Description | Default |
|
| 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.customThemes` | object | Custom theme definitions. | `{}` |
|
||||||
| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` |
|
| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` |
|
||||||
| `ui.hideTips` | boolean | Hide helpful tips in the UI. | `false` |
|
| `ui.hideTips` | boolean | Hide helpful tips in the UI. | `false` |
|
||||||
@@ -326,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.
|
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.
|
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]
|
> [!tip]
|
||||||
>
|
>
|
||||||
@@ -357,38 +358,40 @@ Arguments passed directly when running the CLI can override other configurations
|
|||||||
|
|
||||||
### Command-Line Arguments Table
|
### Command-Line Arguments Table
|
||||||
|
|
||||||
| Argument | Alias | Description | Possible Values | Notes |
|
| 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` |
|
| `--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` | `-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"` |
|
| `--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. |
|
| `--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](/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](../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. |
|
| `--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` | `-s` | Enables sandbox mode for this session. | | |
|
||||||
| `--sandbox-image` | | Sets the sandbox image URI. | | |
|
| `--sandbox-image` | | Sets the sandbox image URI. | | |
|
||||||
| `--debug` | `-d` | Enables debug mode for this session, providing more verbose output. | | |
|
| `--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. | | |
|
| `--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. | | |
|
| `--help` | `-h` | Displays help information about command-line arguments. | | |
|
||||||
| `--show-memory-usage` | | Displays the current memory usage. | | |
|
| `--show-memory-usage` | | Displays the current memory usage. | | |
|
||||||
| `--yolo` | | Enables YOLO mode, which automatically approves all tool calls. | | |
|
| `--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). |
|
| `--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)"` |
|
| `--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` | | Enables [telemetry](/developers/development/telemetry). | | |
|
||||||
| `--telemetry-target` | | Sets the telemetry target. | | See [telemetry](/developers/development/telemetry) for more information. |
|
| `--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-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-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. |
|
| `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
|
||||||
| `--checkpointing` | | Enables [checkpointing](/users/features/checkpointing). | | |
|
| `--checkpointing` | | Enables [checkpointing](../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` |
|
| `--acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
|
||||||
| `--list-extensions` | `-l` | Lists all available extensions and exits. | | |
|
| `--experimental-skills` | | Enables experimental [Agent Skills](../features/skills) (registers the `skill` tool and loads Skills from `.qwen/skills/` and `~/.qwen/skills/`). | | Experimental. |
|
||||||
| `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. |
|
| `--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` |
|
||||||
| `--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` |
|
| `--list-extensions` | `-l` | Lists all available extensions and exits. | | |
|
||||||
| `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | |
|
| `--proxy` | | Sets the proxy for the CLI. | Proxy URL | Example: `--proxy http://localhost:7890`. |
|
||||||
| `--version` | | Displays the version of the CLI. | | |
|
| `--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` |
|
||||||
| `--openai-logging` | | Enables logging of OpenAI API calls for debugging and analysis. | | This flag overrides the `enableOpenAILogging` setting in `settings.json`. |
|
| `--screen-reader` | | Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. | | |
|
||||||
| `--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` |
|
| `--version` | | Displays the version of the CLI. | | |
|
||||||
| `--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` |
|
| `--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)
|
## Context Files (Hierarchical Instructional Context)
|
||||||
|
|
||||||
@@ -438,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.
|
- 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.
|
- 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.
|
- **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:**
|
- **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 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.
|
- 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.
|
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.
|
||||||
|
|
||||||
@@ -450,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.
|
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.
|
- Using `--sandbox` or `-s` flag.
|
||||||
- Setting `GEMINI_SANDBOX` environment variable.
|
- Setting `GEMINI_SANDBOX` environment variable.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Qwen Code comes with a selection of pre-defined themes, which you can list using
|
|||||||
|
|
||||||
### Theme Persistence
|
### 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ The theme file must be a valid JSON file that follows the same structure as a cu
|
|||||||
|
|
||||||
- Select your custom theme using the `/theme` command in Qwen Code. Your custom theme will appear in the theme selection dialog.
|
- 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`.
|
- 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
|
## Themes Preview
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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.
|
2. **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.qwen/trustedFolders.json` file.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
commands: 'Commands',
|
commands: 'Commands',
|
||||||
'sub-agents': 'SubAgents',
|
'sub-agents': 'SubAgents',
|
||||||
|
skills: 'Skills (Experimental)',
|
||||||
headless: 'Headless Mode',
|
headless: 'Headless Mode',
|
||||||
checkpointing: {
|
checkpointing: {
|
||||||
display: 'hidden',
|
display: 'hidden',
|
||||||
@@ -9,4 +10,5 @@ export default {
|
|||||||
mcp: 'MCP',
|
mcp: 'MCP',
|
||||||
'token-caching': 'Token Caching',
|
'token-caching': 'Token Caching',
|
||||||
sandbox: 'Sandboxing',
|
sandbox: 'Sandboxing',
|
||||||
|
language: 'i18n',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
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
|
## Permission Modes Comparison
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Commands specifically for controlling interface and output language.
|
|||||||
| → `ui [language]` | Set UI interface language | `/language ui zh-CN` |
|
| → `ui [language]` | Set UI interface language | `/language ui zh-CN` |
|
||||||
| → `output [language]` | Set LLM output language | `/language output Chinese` |
|
| → `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.
|
- Output language examples: `Chinese`, `English`, `Japanese`, etc.
|
||||||
|
|
||||||
### 1.4 Tool and Model Management
|
### 1.4 Tool and Model Management
|
||||||
@@ -72,17 +72,16 @@ Commands for managing AI tools and models.
|
|||||||
|
|
||||||
Commands for obtaining information and performing system settings.
|
Commands for obtaining information and performing system settings.
|
||||||
|
|
||||||
| Command | Description | Usage Examples |
|
| Command | Description | Usage Examples |
|
||||||
| --------------- | ----------------------------------------------- | ------------------------------------------------ |
|
| ----------- | ----------------------------------------------- | -------------------------------- |
|
||||||
| `/help` | Display help information for available commands | `/help` or `/?` |
|
| `/help` | Display help information for available commands | `/help` or `/?` |
|
||||||
| `/about` | Display version information | `/about` |
|
| `/about` | Display version information | `/about` |
|
||||||
| `/stats` | Display detailed statistics for current session | `/stats` |
|
| `/stats` | Display detailed statistics for current session | `/stats` |
|
||||||
| `/settings` | Open settings editor | `/settings` |
|
| `/settings` | Open settings editor | `/settings` |
|
||||||
| `/auth` | Change authentication method | `/auth` |
|
| `/auth` | Change authentication method | `/auth` |
|
||||||
| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` |
|
| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` |
|
||||||
| `/copy` | Copy last output content to clipboard | `/copy` |
|
| `/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` |
|
||||||
| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
|
|
||||||
|
|
||||||
### 1.6 Common Shortcuts
|
### 1.6 Common Shortcuts
|
||||||
|
|
||||||
|
|||||||
@@ -189,21 +189,22 @@ qwen -p "Write code" --output-format stream-json --include-partial-messages | jq
|
|||||||
|
|
||||||
Key command-line options for headless usage:
|
Key command-line options for headless usage:
|
||||||
|
|
||||||
| Option | Description | Example |
|
| Option | Description | Example |
|
||||||
| ---------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
|
| ---------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
| `--prompt`, `-p` | Run in headless mode | `qwen -p "query"` |
|
| `--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` |
|
| `--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` |
|
| `--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` |
|
| `--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` |
|
| `--debug`, `-d` | Enable debug mode | `qwen -p "query" --debug` |
|
||||||
| `--all-files`, `-a` | Include all files in context | `qwen -p "query" --all-files` |
|
| `--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` |
|
| `--include-directories` | Include additional directories | `qwen -p "query" --include-directories src,docs` |
|
||||||
| `--yolo`, `-y` | Auto-approve all actions | `qwen -p "query" --yolo` |
|
| `--yolo`, `-y` | Auto-approve all actions | `qwen -p "query" --yolo` |
|
||||||
| `--approval-mode` | Set approval mode | `qwen -p "query" --approval-mode auto_edit` |
|
| `--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"` |
|
| `--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"` |
|
| `--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
|
## Examples
|
||||||
|
|
||||||
@@ -276,7 +277,7 @@ tail -5 usage.log
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [CLI Configuration](/users/configuration/settings#command-line-arguments) - Complete configuration guide
|
- [CLI Configuration](../configuration/settings#command-line-arguments) - Complete configuration guide
|
||||||
- [Authentication](/users/configuration/settings#environment-variables-for-api-access) - Setup authentication
|
- [Authentication](../configuration/settings#environment-variables-for-api-access) - Setup authentication
|
||||||
- [Commands](/users/reference/cli-reference) - Interactive commands reference
|
- [Commands](../features/commands) - Interactive commands reference
|
||||||
- [Tutorials](/users/quickstart) - Step-by-step automation guides
|
- [Tutorials](../quickstart) - Step-by-step automation guides
|
||||||
|
|||||||
136
docs/users/features/language.md
Normal file
136
docs/users/features/language.md
Normal 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 Code’s “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 CLI’s 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 haven’t 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 you’d 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
|
||||||
@@ -12,6 +12,7 @@ With MCP servers connected, you can ask Qwen Code to:
|
|||||||
- Automate workflows (repeatable tasks exposed as tools/prompts)
|
- Automate workflows (repeatable tasks exposed as tools/prompts)
|
||||||
|
|
||||||
> [!tip]
|
> [!tip]
|
||||||
|
>
|
||||||
> If you’re looking for the “one command to get started”, jump to [Quick start](#quick-start).
|
> If you’re looking for the “one command to get started”, jump to [Quick start](#quick-start).
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
@@ -51,7 +52,8 @@ qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp
|
|||||||
```
|
```
|
||||||
|
|
||||||
> [!tip]
|
> [!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
|
## 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`) |
|
| `stdio` | Local process (scripts, CLIs, Docker) on your machine | `command`, `args` (+ optional `cwd`, `env`) |
|
||||||
|
|
||||||
> [!note]
|
> [!note]
|
||||||
|
>
|
||||||
> If a server supports both, prefer **HTTP** over **SSE**.
|
> If a server supports both, prefer **HTTP** over **SSE**.
|
||||||
|
|
||||||
### Configure via `settings.json` vs `qwen mcp add`
|
### Configure via `settings.json` vs `qwen mcp add`
|
||||||
|
|||||||
@@ -220,6 +220,6 @@ qwen -s -p "run shell command: mount | grep workspace"
|
|||||||
|
|
||||||
## Related documentation
|
## Related documentation
|
||||||
|
|
||||||
- [Configuration](/users/configuration/settings): Full configuration options.
|
- [Configuration](../configuration/settings): Full configuration options.
|
||||||
- [Commands](/users/reference/cli-reference): Available commands.
|
- [Commands](../features/commands): Available commands.
|
||||||
- [Troubleshooting](/users/support/troubleshooting): General troubleshooting.
|
- [Troubleshooting](../support/troubleshooting): General troubleshooting.
|
||||||
|
|||||||
282
docs/users/features/skills.md
Normal file
282
docs/users/features/skills.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
# Agent Skills (Experimental)
|
||||||
|
|
||||||
|
> Create, manage, and share Skills to extend Qwen Code’s capabilities.
|
||||||
|
|
||||||
|
This guide shows you how to create, use, and manage Agent Skills in **Qwen Code**. Skills are modular capabilities that extend the model’s 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 Skill’s 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 you’re 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 Skill’s 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 don’t need to explicitly invoke it.
|
||||||
|
|
||||||
|
## Debug a Skill
|
||||||
|
|
||||||
|
If Qwen Code doesn’t 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?
|
||||||
@@ -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.
|
- **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`).
|
- **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.
|
- **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: `os.tmpdir()/qwen/ide/`. Your plugin must create this directory if it doesn't exist.
|
- **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:
|
- **File Naming Convention:** The filename is critical and **MUST** follow the pattern:
|
||||||
`qwen-code-ide-server-${PID}-${PORT}.json`
|
`<PORT>.lock`
|
||||||
- `${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}`: The port your MCP server is listening on.
|
|
||||||
- **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure:
|
- **File Content & Workspace Validation:** The file **MUST** contain a JSON object with the following structure:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -33,21 +32,20 @@ For Qwen Code to connect, it needs to discover which IDE instance it's running i
|
|||||||
"port": 12345,
|
"port": 12345,
|
||||||
"workspacePath": "/path/to/project1:/path/to/project2",
|
"workspacePath": "/path/to/project1:/path/to/project2",
|
||||||
"authToken": "a-very-secret-token",
|
"authToken": "a-very-secret-token",
|
||||||
"ideInfo": {
|
"ppid": 1234,
|
||||||
"name": "vscode",
|
"ideName": "VS Code"
|
||||||
"displayName": "VS Code"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- `port` (number, required): The port of the MCP server.
|
- `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).
|
- `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.
|
- `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.
|
- `ppid` (number, required): The parent process ID of the IDE process.
|
||||||
- `name` (string, required): A short, lowercase identifier for the IDE (e.g., `vscode`, `jetbrains`).
|
- `ideName` (string, required): A user-friendly name for the IDE (e.g., `VS Code`, `JetBrains IDE`).
|
||||||
- `displayName` (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.
|
- **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
|
## II. The Context Interface
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Features
|
||||||
|
|
||||||
- **Automation**: Trigger workflows based on events (e.g. issue opening) or schedules (e.g. nightly).
|
- **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
|
- **On-demand Collaboration**: Trigger workflows in issue and pull request
|
||||||
comments by mentioning the [Qwen Code CLI] (e.g., `@qwencoder /review`).
|
comments by mentioning the [Qwen Code CLI](./features/commands) (e.g., `@qwencoder /review`).
|
||||||
- **Extensible with Tools**: Leverage [Qwen Code] models' tool-calling capabilities to
|
- **Extensible with Tools**: Leverage [Qwen Code](../developers/tools/introduction.md) models' tool-calling capabilities to interact with other CLIs like the [GitHub CLI] (`gh`).
|
||||||
interact with other CLIs like the [GitHub CLI] (`gh`).
|
|
||||||
- **Customizable**: Use a `QWEN.md` file in your repository to provide
|
- **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
|
## 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
|
### 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
|
### 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**
|
**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
|
### 5. Try it out
|
||||||
|
|
||||||
@@ -119,30 +92,19 @@ This action provides several pre-built workflows for different use cases. Each w
|
|||||||
|
|
||||||
### Qwen Code Dispatch
|
### Qwen Code Dispatch
|
||||||
|
|
||||||
This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to
|
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).
|
||||||
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).
|
|
||||||
|
|
||||||
### Issue Triage
|
### Issue Triage
|
||||||
|
|
||||||
This action can be used to triage GitHub Issues automatically or on a schedule.
|
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).
|
||||||
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
|
### Pull Request Review
|
||||||
|
|
||||||
This action can be used to automatically review pull requests when they are
|
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).
|
||||||
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).
|
|
||||||
|
|
||||||
### Qwen Code CLI Assistant
|
### Qwen Code CLI Assistant
|
||||||
|
|
||||||
This type of action can be used to invoke a general-purpose, conversational Qwen Code
|
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).
|
||||||
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).
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -222,8 +184,7 @@ To add a secret:
|
|||||||
2. Enter the secret name and value.
|
2. Enter the secret name and value.
|
||||||
3. Save.
|
3. Save.
|
||||||
|
|
||||||
For more information, refer to the
|
For more information, refer to the [official GitHub documentation on creating and using encrypted secrets][secrets].
|
||||||
[official GitHub documentation on creating and using encrypted secrets][secrets].
|
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
@@ -239,7 +200,7 @@ You can authenticate with GitHub in two ways:
|
|||||||
authentication, we recommend creating a custom GitHub App.
|
authentication, we recommend creating a custom GitHub App.
|
||||||
|
|
||||||
For detailed setup instructions for both Qwen and GitHub authentication, go to the
|
For detailed setup instructions for both Qwen and GitHub authentication, go to the
|
||||||
[**Authentication documentation**](./docs/authentication.md).
|
[**Authentication documentation**](./configuration/auth).
|
||||||
|
|
||||||
## Extensions
|
## 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.
|
These extensions are installed from source from their GitHub repositories.
|
||||||
|
|
||||||
For detailed instructions on how to set up and configure extensions, go to the
|
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
|
## Best Practices
|
||||||
|
|
||||||
@@ -258,20 +219,18 @@ Key recommendations include:
|
|||||||
- **Securing Your Repository:** Implementing branch and tag protection, and restricting pull request approvers.
|
- **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.
|
- **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
|
## Customization
|
||||||
|
|
||||||
Create a [QWEN.md] file in the root of your repository to provide
|
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
|
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
|
coding conventions, architectural patterns, or other guidelines the model should
|
||||||
follow for a given repository.
|
follow for a given repository.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Check out the Qwen Code CLI
|
Contributions are welcome! Check out the Qwen Code CLI **Contributing Guide** for more details on how to get started.
|
||||||
[**Contributing Guide**](./CONTRIBUTING.md) for more details on how to get
|
|
||||||
started.
|
|
||||||
|
|
||||||
[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
|
[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
|
||||||
[Qwen Code]: https://github.com/QwenLM/qwen-code
|
[Qwen Code]: https://github.com/QwenLM/qwen-code
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<br/>
|
<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.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **Native agent experience**: Integrated AI assistant panel within Zed's interface
|
- **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
|
- **File management**: @-mention files to add them to the conversation context
|
||||||
- **Conversation history**: Access to past conversations within Zed
|
- **Conversation history**: Access to past conversations within Zed
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"Qwen Code": {
|
"Qwen Code": {
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
"command": "qwen",
|
"command": "qwen",
|
||||||
"args": ["--experimental-acp"],
|
"args": ["--acp"],
|
||||||
"env": {}
|
"env": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Qwen Code overview
|
# Qwen Code overview
|
||||||
|
[](https://npm-compare.com/@qwen-code/qwen-code)
|
||||||
|
[](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.
|
> 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?
|
what does this project do?
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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]
|
> [!tip]
|
||||||
>
|
>
|
||||||
> See [troubleshooting](/users/support/troubleshooting) if you hit issues.
|
> See [troubleshooting](./support/troubleshooting) if you hit issues.
|
||||||
|
|
||||||
> [!note]
|
> [!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
|
## 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.
|
- **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.
|
- **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.
|
- **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
|
## 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.
|
- **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"`.
|
- **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"`.
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ Here are the most important commands for daily use:
|
|||||||
| → `output [language]` | Set LLM output language | `/language output Chinese` |
|
| → `output [language]` | Set LLM output language | `/language output Chinese` |
|
||||||
| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` |
|
| `/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
|
## 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
|
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
|
analyze the database schema
|
||||||
|
|||||||
@@ -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).
|
- **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).
|
- **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
|
## 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
|
## 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
|
### 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
|
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
|
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.
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ This guide provides solutions to common issues and debugging tips, including top
|
|||||||
1. In your home directory: `~/.qwen/settings.json`.
|
1. In your home directory: `~/.qwen/settings.json`.
|
||||||
2. In your project's root 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?**
|
- **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.
|
- 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.**
|
- **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.
|
- **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**
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default tseslint.config(
|
|||||||
'.integration-tests/**',
|
'.integration-tests/**',
|
||||||
'packages/**/.integration-test/**',
|
'packages/**/.integration-test/**',
|
||||||
'dist/**',
|
'dist/**',
|
||||||
|
'docs-site/.next/**',
|
||||||
|
'docs-site/out/**',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ type PermissionHandler = (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up an ACP test environment with all necessary utilities.
|
* 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(
|
function setupAcpTest(
|
||||||
rig: TestRig,
|
rig: TestRig,
|
||||||
options?: { permissionHandler?: PermissionHandler },
|
options?: { permissionHandler?: PermissionHandler; useNewFlag?: boolean },
|
||||||
) {
|
) {
|
||||||
const pending = new Map<number, PendingRequest>();
|
const pending = new Map<number, PendingRequest>();
|
||||||
let nextRequestId = 1;
|
let nextRequestId = 1;
|
||||||
@@ -95,9 +96,13 @@ function setupAcpTest(
|
|||||||
const permissionHandler =
|
const permissionHandler =
|
||||||
options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' }));
|
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(
|
const agent = spawn(
|
||||||
'node',
|
'node',
|
||||||
[rig.bundlePath, '--experimental-acp', '--no-chat-recording'],
|
[rig.bundlePath, acpFlag, '--no-chat-recording'],
|
||||||
{
|
{
|
||||||
cwd: rig.testDir!,
|
cwd: rig.testDir!,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
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';
|
import { TestRig, printDebugInfo, validateModelOutput } from './test-helper.js';
|
||||||
|
|
||||||
describe('file-system', () => {
|
describe('file-system', () => {
|
||||||
@@ -202,8 +200,8 @@ describe('file-system', () => {
|
|||||||
const readAttempt = toolLogs.find(
|
const readAttempt = toolLogs.find(
|
||||||
(log) => log.toolRequest.name === 'read_file',
|
(log) => log.toolRequest.name === 'read_file',
|
||||||
);
|
);
|
||||||
const writeAttempt = toolLogs.find(
|
const editAttempt = toolLogs.find(
|
||||||
(log) => log.toolRequest.name === 'write_file',
|
(log) => log.toolRequest.name === 'edit_file',
|
||||||
);
|
);
|
||||||
const successfulReplace = toolLogs.find(
|
const successfulReplace = toolLogs.find(
|
||||||
(log) => log.toolRequest.name === 'replace' && log.toolRequest.success,
|
(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
|
// CRITICAL: Verify that no matter what the model did, it never successfully
|
||||||
// wrote or replaced anything.
|
// wrote or replaced anything.
|
||||||
if (writeAttempt) {
|
if (editAttempt) {
|
||||||
console.error(
|
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);
|
printDebugInfo(rig, result);
|
||||||
}
|
}
|
||||||
expect(
|
expect(
|
||||||
writeAttempt,
|
editAttempt,
|
||||||
'write_file should not have been called',
|
'edit_file should not have been called',
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
|
|
||||||
if (successfulReplace) {
|
if (successfulReplace) {
|
||||||
@@ -245,12 +243,5 @@ describe('file-system', () => {
|
|||||||
successfulReplace,
|
successfulReplace,
|
||||||
'A successful replace should not have occurred',
|
'A successful replace should not have occurred',
|
||||||
).toBeUndefined();
|
).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,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -952,7 +952,8 @@ describe('Permission Control (E2E)', () => {
|
|||||||
TEST_TIMEOUT,
|
TEST_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
it(
|
// FIXME: This test is flaky and sometimes fails with no tool calls.
|
||||||
|
it.skip(
|
||||||
'should allow read-only tools without restrictions',
|
'should allow read-only tools without restrictions',
|
||||||
async () => {
|
async () => {
|
||||||
// Create test files for the model to read
|
// Create test files for the model to read
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
9855
package-lock.json
generated
9855
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@qwen-code/qwen-code",
|
"name": "@qwen-code/qwen-code",
|
||||||
"version": "0.5.1",
|
"version": "0.6.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -13,14 +13,11 @@
|
|||||||
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
"url": "git+https://github.com/QwenLM/qwen-code.git"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1"
|
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env node scripts/start.js",
|
"start": "cross-env node scripts/start.js",
|
||||||
"debug": "cross-env DEBUG=1 node --inspect-brk 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",
|
"generate": "node scripts/generate-git-commit-info.js",
|
||||||
"build": "node scripts/build.js",
|
"build": "node scripts/build.js",
|
||||||
"build-and-start": "npm run build && npm run start",
|
"build-and-start": "npm run build && npm run start",
|
||||||
@@ -76,6 +73,7 @@
|
|||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/chrome": "^0.1.32",
|
||||||
"@types/marked": "^5.0.2",
|
"@types/marked": "^5.0.2",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/minimatch": "^5.1.2",
|
"@types/minimatch": "^5.1.2",
|
||||||
@@ -95,7 +93,6 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"glob": "^10.5.0",
|
"glob": "^10.5.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"google-artifactregistry-auth": "^3.4.0",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"json": "^11.0.0",
|
"json": "^11.0.0",
|
||||||
"lint-staged": "^16.1.6",
|
"lint-staged": "^16.1.6",
|
||||||
|
|||||||
23
packages/chrome-extension/.gitignore
vendored
Normal file
23
packages/chrome-extension/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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/
|
||||||
157
packages/chrome-extension/QUICK_START.md
Normal file
157
packages/chrome-extension/QUICK_START.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# 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/dist/extension` folder (先运行 `npm run build`)
|
||||||
|
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
|
||||||
|
```
|
||||||
206
packages/chrome-extension/README.md
Normal file
206
packages/chrome-extension/README.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# 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/dist/extension` folder (运行 `npm run build` 后生成)
|
||||||
|
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
|
||||||
|
./scripts/smart-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/
|
||||||
|
├── public/ # 静态资源(manifest、icons、sidepanel.html 模板)
|
||||||
|
├── src/ # 前端代码
|
||||||
|
│ ├── background/ # Service worker 源码
|
||||||
|
│ ├── content/ # Content script 源码
|
||||||
|
│ └── sidepanel/ # React Side Panel 源码
|
||||||
|
├── dist/extension/ # 构建输出,加载 unpacked 时使用
|
||||||
|
├── 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. Run `npm run build`(输出到 `dist/extension`,适合加载/打包)
|
||||||
|
- 开发模式可用 `npm run dev`(watch 同步 + esbuild,输出同样在 `dist/extension`)
|
||||||
|
3. 在 Chrome 中加载 unpacked:选择 `packages/chrome-extension/dist/extension`
|
||||||
|
|
||||||
|
### 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: `$HOME/.qwen/chrome-bridge/qwen-bridge-host.log`(若主目录不可写则回落 `/tmp/qwen-bridge-host.log`)
|
||||||
|
- Windows: `%USERPROFILE%\.qwen\chrome-bridge\qwen-bridge-host.log`(若不可写则回落 `%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
|
||||||
109
packages/chrome-extension/config/esbuild.config.js
Normal file
109
packages/chrome-extension/config/esbuild.config.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* @file esbuild configuration for Chrome Extension Side Panel React App
|
||||||
|
* Bundles React components with Tailwind CSS
|
||||||
|
* @type {import('esbuild').BuildOptions}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global process, console */
|
||||||
|
|
||||||
|
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');
|
||||||
|
const outDir = process.env.EXTENSION_OUT_DIR || 'extension';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: path.join(outDir, '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);
|
||||||
|
});
|
||||||
6
packages/chrome-extension/config/postcss.config.js
Normal file
6
packages/chrome-extension/config/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
53
packages/chrome-extension/config/tailwind.config.js
Normal file
53
packages/chrome-extension/config/tailwind.config.js
Normal 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: [],
|
||||||
|
};
|
||||||
253
packages/chrome-extension/docs/api-reference.md
Normal file
253
packages/chrome-extension/docs/api-reference.md
Normal 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
|
||||||
146
packages/chrome-extension/docs/architecture.md
Normal file
146
packages/chrome-extension/docs/architecture.md
Normal 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
|
||||||
103
packages/chrome-extension/docs/debugging.md
Normal file
103
packages/chrome-extension/docs/debugging.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# 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
|
||||||
|
- You can open any target page manually for debugging
|
||||||
|
|
||||||
|
### Native Host Logging
|
||||||
|
|
||||||
|
The native host logs are stored at:
|
||||||
|
|
||||||
|
- **macOS/Linux**: `$HOME/.qwen/chrome-bridge/qwen-bridge-host.log`(若主目录不可写则回落 `/tmp/qwen-bridge-host.log`)
|
||||||
|
- **Windows**: `%USERPROFILE%\.qwen\chrome-bridge\qwen-bridge-host.log`(若不可写则回落 `%TEMP%\qwen-bridge-host.log`)
|
||||||
|
|
||||||
|
To monitor the logs in real-time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run logs
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f "$HOME/.qwen/chrome-bridge/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: `$HOME/.qwen/chrome-bridge/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.
|
||||||
69
packages/chrome-extension/docs/development.md
Normal file
69
packages/chrome-extension/docs/development.md
Normal 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
|
||||||
|
```
|
||||||
7
packages/chrome-extension/native-host/host.js
Executable file
7
packages/chrome-extension/native-host/host.js
Executable 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');
|
||||||
23
packages/chrome-extension/native-host/package.json
Normal file
23
packages/chrome-extension/native-host/package.json
Normal 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": {}
|
||||||
|
}
|
||||||
2
packages/chrome-extension/native-host/scripts/host.bat
Executable file
2
packages/chrome-extension/native-host/scripts/host.bat
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
node "%~dp0..\\host.js" %*
|
||||||
99
packages/chrome-extension/native-host/scripts/install.bat
Executable file
99
packages/chrome-extension/native-host/scripts/install.bat
Executable 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%..\dist\extension (run "npm run build" first)
|
||||||
|
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: %%USERPROFILE%%\.qwen\chrome-bridge\qwen-bridge-host.log (fallback: %%TEMP%%\qwen-bridge-host.log)
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo ❌ Failed to add registry entry
|
||||||
|
echo Please run this script as Administrator
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
321
packages/chrome-extension/native-host/scripts/smart-install.sh
Executable file
321
packages/chrome-extension/native-host/scripts/smart-install.sh
Executable file
@@ -0,0 +1,321 @@
|
|||||||
|
#!/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 )"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
HOST_NAME="com.qwen.cli.bridge"
|
||||||
|
HOST_SCRIPT="$SCRIPT_DIR/../host.js"
|
||||||
|
EXTENSION_ID_FILE="$ROOT_DIR/.extension-id"
|
||||||
|
|
||||||
|
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(统一使用根目录的 .extension-id,兼容旧路径)
|
||||||
|
if [[ -z "$EXTENSION_ID" ]]; then
|
||||||
|
if [[ -f "$EXTENSION_ID_FILE" ]]; then
|
||||||
|
EXTENSION_ID=$(cat "$EXTENSION_ID_FILE")
|
||||||
|
echo -e "${GREEN}✓${NC} 使用保存的扩展 ID: ${CYAN}$EXTENSION_ID${NC}"
|
||||||
|
AUTO_DETECTED=true
|
||||||
|
else
|
||||||
|
for legacy in "$SCRIPT_DIR/../.extension-id" "$SCRIPT_DIR/../../scripts/.extension-id"; do
|
||||||
|
if [[ -z "$EXTENSION_ID" && -f "$legacy" ]]; then
|
||||||
|
EXTENSION_ID=$(cat "$legacy")
|
||||||
|
echo "$EXTENSION_ID" > "$EXTENSION_ID_FILE"
|
||||||
|
echo -e "${GREEN}✓${NC} 已从旧路径迁移扩展 ID: ${CYAN}$EXTENSION_ID${NC}"
|
||||||
|
AUTO_DETECTED=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
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 Extension,ID 在扩展卡片上${NC}"
|
||||||
|
read -p "> " EXTENSION_ID
|
||||||
|
if [[ -n "$EXTENSION_ID" ]]; then
|
||||||
|
# 保存 ID 供以后使用
|
||||||
|
echo "$EXTENSION_ID" > "$EXTENSION_ID_FILE"
|
||||||
|
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" > "$EXTENSION_ID_FILE"
|
||||||
|
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 " • 日志文件位置: \$HOME/.qwen/chrome-bridge/qwen-bridge-host.log(若主目录不可写则回落 /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}"
|
||||||
151
packages/chrome-extension/native-host/scripts/update-host-config.sh
Executable file
151
packages/chrome-extension/native-host/scripts/update-host-config.sh
Executable file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Qwen CLI Chrome Extension - Native Host Configuration Updater
|
||||||
|
# 用于在更换电脑或浏览器后更新Native Host配置
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
HOST_NAME="com.qwen.cli.bridge"
|
||||||
|
HOST_SCRIPT="$SCRIPT_DIR/../host.js"
|
||||||
|
EXTENSION_ID_FILE="$ROOT_DIR/.extension-id"
|
||||||
|
|
||||||
|
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" > "$EXTENSION_ID_FILE"
|
||||||
|
|
||||||
|
# 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 ""
|
||||||
370
packages/chrome-extension/native-host/src/browser-mcp-server.js
Executable file
370
packages/chrome-extension/native-host/src/browser-mcp-server.js
Executable 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);
|
||||||
|
});
|
||||||
1254
packages/chrome-extension/native-host/src/host.js
Executable file
1254
packages/chrome-extension/native-host/src/host.js
Executable file
File diff suppressed because it is too large
Load Diff
63
packages/chrome-extension/package.json
Normal file
63
packages/chrome-extension/package.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"public/",
|
||||||
|
"native-host/",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "EXTENSION_OUT_DIR=dist/extension node scripts/dev-watch.js",
|
||||||
|
"debug:mac": "./scripts/debug.sh",
|
||||||
|
"sync:extension": "node scripts/sync-extension.js",
|
||||||
|
"build:ui": "node config/esbuild.config.js",
|
||||||
|
"build:ui:watch": "node config/esbuild.config.js --watch",
|
||||||
|
"build": "EXTENSION_OUT_DIR=dist/extension npm run clean && EXTENSION_OUT_DIR=dist/extension node scripts/sync-extension.js && EXTENSION_OUT_DIR=dist/extension node config/esbuild.config.js --production",
|
||||||
|
"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/dist/extension --auto-open-devtools-for-tabs",
|
||||||
|
"package": "npm run build && cd dist && zip -r ../chrome-extension.zip extension/",
|
||||||
|
"clean": "./scripts/clean.sh",
|
||||||
|
"logs": "tail -f $HOME/.qwen/chrome-bridge/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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
packages/chrome-extension/public/icons/icon-128.png
Normal file
BIN
packages/chrome-extension/public/icons/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
packages/chrome-extension/public/icons/icon-16.png
Normal file
BIN
packages/chrome-extension/public/icons/icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 920 B |
BIN
packages/chrome-extension/public/icons/icon-48.png
Normal file
BIN
packages/chrome-extension/public/icons/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
4
packages/chrome-extension/public/icons/icon.svg
Normal file
4
packages/chrome-extension/public/icons/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 70 KiB |
56
packages/chrome-extension/public/manifest.json
Normal file
56
packages/chrome-extension/public/manifest.json
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon-16.png",
|
||||||
|
"48": "icons/icon-48.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
69
packages/chrome-extension/public/sidepanel/sidepanel.html
Normal file
69
packages/chrome-extension/public/sidepanel/sidepanel.html
Normal 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>
|
||||||
22
packages/chrome-extension/scripts/build.sh
Executable file
22
packages/chrome-extension/scripts/build.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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/.."
|
||||||
|
|
||||||
|
# Build latest assets into dist/extension
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Create a zip file for Chrome Web Store / unpacked install
|
||||||
|
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/"
|
||||||
24
packages/chrome-extension/scripts/clean.sh
Executable file
24
packages/chrome-extension/scripts/clean.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Clean up build artifacts and temporary files for Chrome Extension
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
echo "Cleaning up Chrome Extension build artifacts..."
|
||||||
|
|
||||||
|
# Remove any dist directories and zips
|
||||||
|
rm -rf dist/
|
||||||
|
rm -f chrome-extension.zip
|
||||||
|
|
||||||
|
# Remove log files
|
||||||
|
rm -f "$HOME/.qwen/chrome-bridge/qwen-bridge-host.log"
|
||||||
|
rm -f /tmp/qwen-bridge-host.log
|
||||||
|
rm -f /tmp/qwen-server.log
|
||||||
|
|
||||||
|
# Remove saved extension ID (new unified path + legacy paths)
|
||||||
|
rm -f "$ROOT_DIR/.extension-id"
|
||||||
|
rm -f "$SCRIPT_DIR/.extension-id"
|
||||||
|
rm -f "$SCRIPT_DIR/../native-host/.extension-id"
|
||||||
|
|
||||||
|
echo "Cleanup complete!"
|
||||||
170
packages/chrome-extension/scripts/debug.sh
Executable file
170
packages/chrome-extension/scripts/debug.sh
Executable file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/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 )"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
EXTENSION_ID_FILE="$ROOT_DIR/.extension-id"
|
||||||
|
|
||||||
|
# 兼容旧路径的 .extension-id(如存在则迁移到统一位置)
|
||||||
|
if [[ ! -f "$EXTENSION_ID_FILE" ]]; then
|
||||||
|
for legacy in "$SCRIPT_DIR/.extension-id" "$SCRIPT_DIR/../native-host/.extension-id"; do
|
||||||
|
if [[ -f "$legacy" ]]; then
|
||||||
|
cp "$legacy" "$EXTENSION_ID_FILE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查是否首次安装
|
||||||
|
if [[ ! -f "$EXTENSION_ID_FILE" ]]; 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/6]${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 已安装"
|
||||||
|
EXT_DIR="$SCRIPT_DIR/../dist/extension"
|
||||||
|
|
||||||
|
# 第二步:配置 Native Host
|
||||||
|
echo -e "\n${BLUE}[2/6]${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/6]${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/6]${NC} 构建扩展..."
|
||||||
|
(
|
||||||
|
cd "$SCRIPT_DIR/.."
|
||||||
|
EXTENSION_OUT_DIR=dist/extension npm run build >/tmp/qwen-bridge-build.log 2>&1
|
||||||
|
)
|
||||||
|
if [[ ! -d "$EXT_DIR" ]]; then
|
||||||
|
echo -e "${RED}✗${NC} 构建失败,查看 /tmp/qwen-bridge-build.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓${NC} 构建完成,输出目录: ${EXT_DIR}"
|
||||||
|
|
||||||
|
# 第五步:启动测试页面
|
||||||
|
# 第五步:启动 Chrome
|
||||||
|
echo -e "\n${BLUE}[5/5]${NC} 启动 Chrome 并加载插件..."
|
||||||
|
|
||||||
|
"$CHROME_PATH" \
|
||||||
|
--load-extension="$EXT_DIR" \
|
||||||
|
--auto-open-devtools-for-tabs \
|
||||||
|
--no-first-run \
|
||||||
|
--no-default-browser-check \
|
||||||
|
"about:blank" &
|
||||||
|
|
||||||
|
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 " • 插件: 已加载到工具栏"
|
||||||
|
|
||||||
|
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: $HOME/.qwen/chrome-bridge/qwen-bridge-host.log (fallback: /tmp/qwen-bridge-host.log)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 清理函数
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n${YELLOW}正在停止服务...${NC}"
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} 已停止服务"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 捕获中断信号
|
||||||
|
trap cleanup INT TERM
|
||||||
|
|
||||||
|
# 保持运行
|
||||||
|
while true; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
77
packages/chrome-extension/scripts/dev-watch.js
Normal file
77
packages/chrome-extension/scripts/dev-watch.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发模式:同步资源 + esbuild watch 到 dist/extension(可通过 EXTENSION_OUT_DIR 覆盖)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
|
const outDir = process.env.EXTENSION_OUT_DIR || 'dist/extension';
|
||||||
|
|
||||||
|
function startProcess(command, args, options = {}) {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, EXTENSION_OUT_DIR: outDir },
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
if (signal === 'SIGINT') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error(`${command} ${args.join(' ')} exited with code ${code}`);
|
||||||
|
process.exit(code || 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 先做一次完整同步,保证 dist/extension 准备好静态资源和脚本
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const syncOnce = startProcess('node', [
|
||||||
|
'scripts/sync-extension.js',
|
||||||
|
`--target=${outDir}`,
|
||||||
|
]);
|
||||||
|
syncOnce.on('exit', (code) => {
|
||||||
|
if (code === 0) resolve();
|
||||||
|
else reject(new Error('Initial sync failed'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 并行开启 watch:静态/脚本同步 + sidepanel esbuild
|
||||||
|
const watchers = [
|
||||||
|
startProcess('node', [
|
||||||
|
'scripts/sync-extension.js',
|
||||||
|
'--watch',
|
||||||
|
`--target=${outDir}`,
|
||||||
|
]),
|
||||||
|
startProcess('node', ['config/esbuild.config.js', '--watch']),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 优雅退出
|
||||||
|
const shutdown = () => {
|
||||||
|
watchers.forEach((proc) => {
|
||||||
|
if (!proc.killed) {
|
||||||
|
proc.kill('SIGINT');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
531
packages/chrome-extension/scripts/dev.js
Normal file
531
packages/chrome-extension/scripts/dev.js
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发环境一键启动脚本
|
||||||
|
* 自动完成所有配置和启动步骤
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, exec } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
// const readline = require('readline'); // Commenting out unused import
|
||||||
|
|
||||||
|
// 颜色输出
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建测试服务器
|
||||||
|
async function createTestServer(port = 3000) {
|
||||||
|
logStep(6, 'Starting test server...');
|
||||||
|
|
||||||
|
const { default: http } = await import('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,
|
||||||
|
process.env.EXTENSION_OUT_DIR || 'dist/extension',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(extensionPath)) {
|
||||||
|
logWarning(
|
||||||
|
`Extension output not found at ${extensionPath}. Run "npm run build" first or set EXTENSION_OUT_DIR.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: $HOME/.qwen/chrome-bridge/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);
|
||||||
|
});
|
||||||
122
packages/chrome-extension/scripts/first-install.sh
Executable file
122
packages/chrome-extension/scripts/first-install.sh
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/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 )"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
EXTENSION_ID_FILE="$ROOT_DIR/.extension-id"
|
||||||
|
|
||||||
|
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" > "$EXTENSION_ID_FILE"
|
||||||
|
|
||||||
|
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
|
||||||
24
packages/chrome-extension/scripts/set-extension-id.sh
Executable file
24
packages/chrome-extension/scripts/set-extension-id.sh
Executable 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"
|
||||||
307
packages/chrome-extension/scripts/start.sh
Executable file
307
packages/chrome-extension/scripts/start.sh
Executable file
@@ -0,0 +1,307 @@
|
|||||||
|
#!/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="${EXTENSION_OUT_DIR:-"$SCRIPT_DIR/dist/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..."
|
||||||
|
|
||||||
|
# Ensure extension is built
|
||||||
|
if [[ ! -d "$EXTENSION_DIR" ]]; then
|
||||||
|
echo "Extension output not found at $EXTENSION_DIR"
|
||||||
|
echo "Please run: EXTENSION_OUT_DIR=dist/extension npm run build"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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: \$HOME/.qwen/chrome-bridge/qwen-bridge-host.log (fallback: /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
|
||||||
124
packages/chrome-extension/scripts/sync-extension.js
Normal file
124
packages/chrome-extension/scripts/sync-extension.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将源代码同步到目标扩展目录(默认 dist/extension 或通过 EXTENSION_OUT_DIR/--target 指定)。
|
||||||
|
* - 复制 public 下的静态资源(排除 sidepanel/dist 旧构建)
|
||||||
|
* - 用 src/ 下的 background、content 覆盖对应目录
|
||||||
|
* 支持 --watch 监听变更(不清空输出,便于与 esbuild --watch 共存)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { watch } from 'fs';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const projectRoot = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const isWatch = args.includes('--watch');
|
||||||
|
const targetArg = args.find((arg) => arg.startsWith('--target='));
|
||||||
|
const targetDir = path.resolve(
|
||||||
|
projectRoot,
|
||||||
|
targetArg
|
||||||
|
? targetArg.split('=')[1]
|
||||||
|
: process.env.EXTENSION_OUT_DIR || 'extension',
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticSrcDir = path.join(projectRoot, 'public');
|
||||||
|
const copyPairs = [
|
||||||
|
['src/background', 'background'],
|
||||||
|
['src/content', 'content'],
|
||||||
|
];
|
||||||
|
|
||||||
|
async function copyStatic(clean = false) {
|
||||||
|
if (clean) {
|
||||||
|
await fs.rm(targetDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
await fs.mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
await fs.cp(staticSrcDir, targetDir, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (src) => {
|
||||||
|
// 跳过旧的 sidepanel/dist 构建产物,交由 esbuild 重新生成
|
||||||
|
return !src.includes(`${path.sep}sidepanel${path.sep}dist${path.sep}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
`Static assets synced -> ${path.relative(projectRoot, targetDir)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copySources() {
|
||||||
|
for (const [src, destRelative] of copyPairs) {
|
||||||
|
const srcPath = path.join(projectRoot, src);
|
||||||
|
const destPath = path.join(targetDir, destRelative);
|
||||||
|
await fs.mkdir(destPath, { recursive: true });
|
||||||
|
await fs.cp(srcPath, destPath, { recursive: true });
|
||||||
|
console.log(`Synced ${src} -> ${path.relative(projectRoot, destPath)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncAll({ clean } = { clean: false }) {
|
||||||
|
await copyStatic(clean);
|
||||||
|
await copySources();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWatchers() {
|
||||||
|
const watchTargets = [
|
||||||
|
path.join(projectRoot, 'public'),
|
||||||
|
path.join(projectRoot, 'src', 'background'),
|
||||||
|
path.join(projectRoot, 'src', 'content'),
|
||||||
|
];
|
||||||
|
|
||||||
|
let syncing = false;
|
||||||
|
let pending = false;
|
||||||
|
|
||||||
|
const triggerSync = (reason = 'change') => {
|
||||||
|
if (syncing) {
|
||||||
|
pending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncing = true;
|
||||||
|
syncAll({ clean: false })
|
||||||
|
.then(() => console.log(`[watch] synced after ${reason}`))
|
||||||
|
.catch((err) => console.error('Sync error:', err))
|
||||||
|
.finally(() => {
|
||||||
|
syncing = false;
|
||||||
|
if (pending) {
|
||||||
|
pending = false;
|
||||||
|
triggerSync('pending change');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watchTargets.forEach((dir) => {
|
||||||
|
watch(dir, { recursive: true }, (_, filename) => {
|
||||||
|
if (
|
||||||
|
filename &&
|
||||||
|
filename.includes(`${path.sep}sidepanel${path.sep}dist${path.sep}`)
|
||||||
|
) {
|
||||||
|
// 让 esbuild 管理 sidepanel/dist 输出
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
triggerSync(`${path.relative(projectRoot, dir)}/${filename || ''}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Watching extension sources -> ${path.relative(projectRoot, targetDir)} (sidepanel/dist excluded)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await syncAll({ clean: !isWatch });
|
||||||
|
if (isWatch) {
|
||||||
|
startWatchers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Failed to sync extension assets:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
1546
packages/chrome-extension/src/background/service-worker.js
Normal file
1546
packages/chrome-extension/src/background/service-worker.js
Normal file
File diff suppressed because it is too large
Load Diff
491
packages/chrome-extension/src/content/content-script.js
Normal file
491
packages/chrome-extension/src/content/content-script.js
Normal 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 += ` `;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
823
packages/chrome-extension/src/sidepanel/App.tsx
Normal file
823
packages/chrome-extension/src/sidepanel/App.tsx
Normal file
@@ -0,0 +1,823 @@
|
|||||||
|
/**
|
||||||
|
* Chrome Extension Side Panel App
|
||||||
|
* Simplified version adapted from vscode-ide-companion
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
// Add other properties as needed based on the actual structure
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InternalTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
// Add other properties as needed based on the actual structure
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [mcpTools, setMcpTools] = useState<McpTool[]>([]);
|
||||||
|
const [internalTools, setInternalTools] = useState<InternalTool[]>([]);
|
||||||
|
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': {
|
||||||
|
const messageAny = message as { data?: unknown };
|
||||||
|
console.log('[HostInfo]', messageAny.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'hostLog': {
|
||||||
|
const logMessage = message as { data?: { line?: string } };
|
||||||
|
const line = logMessage.data?.line;
|
||||||
|
if (line) console.log('[HostLog]', line);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'authUpdate': {
|
||||||
|
const authMessage = message as { data?: { authUri?: string } };
|
||||||
|
const uri = authMessage.data?.authUri;
|
||||||
|
if (uri) setAuthUri(uri);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'mcpTools': {
|
||||||
|
const toolMessage: { data?: { tools?: McpTool[] } } = message as {
|
||||||
|
data?: { tools?: McpTool[] };
|
||||||
|
};
|
||||||
|
const tools = toolMessage.data?.tools || [];
|
||||||
|
setMcpTools(tools);
|
||||||
|
console.log('[App] MCP tools:', tools);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'internalMcpTools': {
|
||||||
|
const internalToolMessage: { data?: { tools?: InternalTool[] } } =
|
||||||
|
message as { data?: { tools?: InternalTool[] } };
|
||||||
|
const tools = internalToolMessage.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 || {};
|
||||||
|
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': {
|
||||||
|
const chunkMessage = message as { data: { chunk: string } };
|
||||||
|
setStreamingContent(
|
||||||
|
(prev) => prev + (chunkMessage.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle unknown message types
|
||||||
|
console.log('[App] Unknown message type:', message.type);
|
||||||
|
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;
|
||||||
|
mcpTools?: McpTool[];
|
||||||
|
internalTools?: InternalTool[];
|
||||||
|
} | null;
|
||||||
|
if (response) {
|
||||||
|
setIsConnected(response.connected || false);
|
||||||
|
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` : '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={() => {
|
||||||
|
// No action required
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AssistantMessage
|
||||||
|
key={index}
|
||||||
|
content={msg.content}
|
||||||
|
timestamp={msg.timestamp}
|
||||||
|
onFileClick={() => {
|
||||||
|
// No action required
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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={() => {
|
||||||
|
// No special key handling required
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onToggleEditMode={() => {
|
||||||
|
// No edit mode toggle required
|
||||||
|
}}
|
||||||
|
onToggleThinking={() => {
|
||||||
|
// No thinking mode toggle required
|
||||||
|
}}
|
||||||
|
onFocusActiveEditor={() => {
|
||||||
|
// No editor focus required
|
||||||
|
}}
|
||||||
|
onToggleSkipAutoActiveContext={() => {
|
||||||
|
// No context toggle required
|
||||||
|
}}
|
||||||
|
onShowCommandMenu={() => {
|
||||||
|
// No command menu required
|
||||||
|
}}
|
||||||
|
onAttachContext={() => {
|
||||||
|
// No context attachment required
|
||||||
|
}}
|
||||||
|
completionIsOpen={false}
|
||||||
|
completionItems={[]}
|
||||||
|
onCompletionSelect={() => {
|
||||||
|
// No completion selection required
|
||||||
|
}}
|
||||||
|
onCompletionClose={() => {
|
||||||
|
// No completion closing required
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<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 {
|
||||||
|
// Ignore errors when opening tab
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
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: InternalTool & {
|
||||||
|
tool?: { name?: string; description?: string };
|
||||||
|
},
|
||||||
|
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: McpTool & { tool?: { name?: string; description?: string } },
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Edit mode related icons
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import type { IconProps } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit pencil icon (16x16)
|
||||||
|
* Used for "Ask before edits" mode
|
||||||
|
*/
|
||||||
|
export const EditPencilIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M11.013 2.513a1.75 1.75 0 0 1 2.475 2.474L6.226 12.25a2.751 2.751 0 0 1-.892.596l-2.047.848a.75.75 0 0 1-.98-.98l.848-2.047a2.75 2.75 0 0 1 .596-.892l7.262-7.261Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto/fast-forward icon (16x16)
|
||||||
|
* Used for "Edit automatically" mode
|
||||||
|
*/
|
||||||
|
export const AutoEditIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M2.53 3.956A1 1 0 0 0 1 4.804v6.392a1 1 0 0 0 1.53.848l5.113-3.196c.16-.1.279-.233.357-.383v2.73a1 1 0 0 0 1.53.849l5.113-3.196a1 1 0 0 0 0-1.696L9.53 3.956A1 1 0 0 0 8 4.804v2.731a.992.992 0 0 0-.357-.383L2.53 3.956Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan mode/bars icon (16x16)
|
||||||
|
* Used for "Plan mode"
|
||||||
|
*/
|
||||||
|
export const PlanModeIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M4.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1ZM10.5 2a.5.5 0 0 0-.5.5v11a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-1Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code brackets icon (20x20)
|
||||||
|
* Used for active file indicator
|
||||||
|
*/
|
||||||
|
export const CodeBracketsIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M6.28 5.22a.75.75 0 0 1 0 1.06L2.56 10l3.72 3.72a.75.75 0 0 1-1.06 1.06L.97 10.53a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Zm7.44 0a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L17.44 10l-3.72-3.72a.75.75 0 0 1 0-1.06ZM11.377 2.011a.75.75 0 0 1 .612.867l-2.5 14.5a.75.75 0 0 1-1.478-.255l2.5-14.5a.75.75 0 0 1 .866-.612Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide context (eye slash) icon (20x20)
|
||||||
|
* Used to indicate the active selection will NOT be auto-loaded into context
|
||||||
|
*/
|
||||||
|
export const HideContextIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l14.5 14.5a.75.75 0 1 0 1.06-1.06l-1.745-1.745a10.029 10.029 0 0 0 3.3-4.38 1.651 1.651 0 0 0 0-1.185A10.004 10.004 0 0 0 9.999 3a9.956 9.956 0 0 0-4.744 1.194L3.28 2.22ZM7.752 6.69l1.092 1.092a2.5 2.5 0 0 1 3.374 3.373l1.091 1.092a4 4 0 0 0-5.557-5.557Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
<path d="m10.748 13.93 2.523 2.523a9.987 9.987 0 0 1-3.27.547c-4.258 0-7.894-2.66-9.337-6.41a1.651 1.651 0 0 1 0-1.186A10.007 10.007 0 0 1 2.839 6.02L6.07 9.252a4 4 0 0 0 4.678 4.678Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slash command icon (20x20)
|
||||||
|
* Used for command menu button
|
||||||
|
*/
|
||||||
|
export const SlashCommandIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12.528 3.047a.75.75 0 0 1 .449.961L8.433 16.504a.75.75 0 1 1-1.41-.512l4.544-12.496a.75.75 0 0 1 .961-.449Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link/attachment icon (20x20)
|
||||||
|
* Used for attach context button
|
||||||
|
*/
|
||||||
|
export const LinkIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M15.621 4.379a3 3 0 0 0-4.242 0l-7 7a3 3 0 0 0 4.241 4.243h.001l.497-.5a.75.75 0 0 1 1.064 1.057l-.498.501-.002.002a4.5 4.5 0 0 1-6.364-6.364l7-7a4.5 4.5 0 0 1 6.368 6.36l-3.455 3.553A2.625 2.625 0 1 1 9.52 9.52l3.45-3.451a.75.75 0 1 1 1.061 1.06l-3.45 3.451a1.125 1.125 0 0 0 1.587 1.595l3.454-3.553a3 3 0 0 0 0-4.242Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open diff icon (16x16)
|
||||||
|
* Used for opening diff in VS Code
|
||||||
|
*/
|
||||||
|
export const OpenDiffIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M13.5 7l-4-4v3h-6v2h6v3l4-4z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* File and document related icons
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import type { IconProps } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File document icon (16x16)
|
||||||
|
* Used for file completion menu
|
||||||
|
*/
|
||||||
|
export const FileIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M9 2H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7l-5-5zm3 7V3.5L10.5 2H10v3a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V2H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1zM6 3h3v2H6V3z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const FileListIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M5 3.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save document icon (16x16)
|
||||||
|
* Used for save session button
|
||||||
|
*/
|
||||||
|
export const SaveDocumentIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M2.66663 2.66663H10.6666L13.3333 5.33329V13.3333H2.66663V2.66663Z" />
|
||||||
|
<path d="M8 10.6666V8M8 8V5.33329M8 8H10.6666M8 8H5.33329" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folder icon (16x16)
|
||||||
|
* Useful for directory entries in completion lists
|
||||||
|
*/
|
||||||
|
export const FolderIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M1.5 3A1.5 1.5 0 0 1 3 1.5h3.086a1.5 1.5 0 0 1 1.06.44L8.5 3H13A1.5 1.5 0 0 1 14.5 4.5v7A1.5 1.5 0 0 1 13 13H3A1.5 1.5 0 0 1 1.5 11.5v-8Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Navigation and action icons
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import type { IconProps } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chevron down icon (20x20)
|
||||||
|
* Used for dropdown arrows
|
||||||
|
*/
|
||||||
|
export const ChevronDownIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plus icon (20x20)
|
||||||
|
* Used for new session button
|
||||||
|
*/
|
||||||
|
export const PlusIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small plus icon (16x16)
|
||||||
|
* Used for default attachment type
|
||||||
|
*/
|
||||||
|
export const PlusSmallIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M8 2a.5.5 0 0 1 .5.5V5h2.5a.5.5 0 0 1 0 1H8.5v2.5a.5.5 0 0 1-1 0V6H5a.5.5 0 0 1 0-1h2.5V2.5A.5.5 0 0 1 8 2Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arrow up icon (20x20)
|
||||||
|
* Used for send message button
|
||||||
|
*/
|
||||||
|
export const ArrowUpIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close X icon (14x14)
|
||||||
|
* Used for close buttons in banners and dialogs
|
||||||
|
*/
|
||||||
|
export const CloseIcon: React.FC<IconProps> = ({
|
||||||
|
size = 14,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 1L13 13M1 13L13 1"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CloseSmallIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search/magnifying glass icon (20x20)
|
||||||
|
* Used for search input
|
||||||
|
*/
|
||||||
|
export const SearchIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh/reload icon (16x16)
|
||||||
|
* Used for refresh session list
|
||||||
|
*/
|
||||||
|
export const RefreshIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M13.3333 8C13.3333 10.9455 10.9455 13.3333 8 13.3333C5.05451 13.3333 2.66663 10.9455 2.66663 8C2.66663 5.05451 5.05451 2.66663 8 2.66663" />
|
||||||
|
<path d="M10.6666 8L13.3333 8M13.3333 8L13.3333 5.33333M13.3333 8L10.6666 10.6667" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Special UI icons
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import type { IconProps } from './types.js';
|
||||||
|
|
||||||
|
interface ThinkingIconProps extends IconProps {
|
||||||
|
/**
|
||||||
|
* Whether thinking is enabled (affects styling)
|
||||||
|
*/
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
enabled = false,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.00293 1.11523L8.35059 1.12402H8.35352C11.9915 1.30834 14.8848 4.31624 14.8848 8C14.8848 11.8025 11.8025 14.8848 8 14.8848C4.19752 14.8848 1.11523 11.8025 1.11523 8C1.11523 7.67691 1.37711 7.41504 1.7002 7.41504C2.02319 7.41514 2.28516 7.67698 2.28516 8C2.28516 11.1563 4.84369 13.7148 8 13.7148C11.1563 13.7148 13.7148 11.1563 13.7148 8C13.7148 4.94263 11.3141 2.4464 8.29492 2.29297V2.29199L7.99609 2.28516H7.9873V2.28418L7.89648 2.27539L7.88281 2.27441V2.27344C7.61596 2.21897 7.41513 1.98293 7.41504 1.7002C7.41504 1.37711 7.67691 1.11523 8 1.11523H8.00293ZM8 3.81543C8.32309 3.81543 8.58496 4.0773 8.58496 4.40039V7.6377L10.9619 8.82715C11.2505 8.97169 11.3678 9.32256 11.2236 9.61133C11.0972 9.86425 10.8117 9.98544 10.5488 9.91504L10.5352 9.91211V9.91016L10.4502 9.87891L10.4385 9.87402V9.87305L7.73828 8.52344C7.54007 8.42433 7.41504 8.22155 7.41504 8V4.40039C7.41504 4.0773 7.67691 3.81543 8 3.81543ZM2.44336 5.12695C2.77573 5.19517 3.02597 5.48929 3.02637 5.8418C3.02637 6.19456 2.7761 6.49022 2.44336 6.55859L2.2959 6.57324C1.89241 6.57324 1.56543 6.24529 1.56543 5.8418C1.56588 5.43853 1.89284 5.1123 2.2959 5.1123L2.44336 5.12695ZM3.46094 2.72949C3.86418 2.72984 4.19017 3.05712 4.19043 3.45996V3.46094C4.19009 3.86393 3.86392 4.19008 3.46094 4.19043H3.45996C3.05712 4.19017 2.72983 3.86419 2.72949 3.46094V3.45996C2.72976 3.05686 3.05686 2.72976 3.45996 2.72949H3.46094ZM5.98926 1.58008C6.32235 1.64818 6.57324 1.94276 6.57324 2.2959L6.55859 2.44336C6.49022 2.7761 6.19456 3.02637 5.8418 3.02637C5.43884 3.02591 5.11251 2.69895 5.1123 2.2959L5.12695 2.14844C5.19504 1.81591 5.48906 1.56583 5.8418 1.56543L5.98926 1.58008Z"
|
||||||
|
strokeWidth="0.27"
|
||||||
|
style={{
|
||||||
|
stroke: enabled
|
||||||
|
? 'var(--app-qwen-ivory)'
|
||||||
|
: 'var(--app-secondary-foreground)',
|
||||||
|
fill: enabled
|
||||||
|
? 'var(--app-qwen-ivory)'
|
||||||
|
: 'var(--app-secondary-foreground)',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TerminalIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.14648 7.14648C5.34175 6.95122 5.65825 6.95122 5.85352 7.14648L8.35352 9.64648C8.44728 9.74025 8.5 9.86739 8.5 10C8.5 10.0994 8.47037 10.1958 8.41602 10.2773L8.35352 10.3535L5.85352 12.8535C5.65825 13.0488 5.34175 13.0488 5.14648 12.8535C4.95122 12.6583 4.95122 12.3417 5.14648 12.1465L7.29297 10L5.14648 7.85352C4.95122 7.65825 4.95122 7.34175 5.14648 7.14648Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
<path d="M14.5 12C14.7761 12 15 12.2239 15 12.5C15 12.7761 14.7761 13 14.5 13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H14.5Z" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.5 4C17.3284 4 18 4.67157 18 5.5V14.5C18 15.3284 17.3284 16 16.5 16H3.5C2.67157 16 2 15.3284 2 14.5V5.5C2 4.67157 2.67157 4 3.5 4H16.5ZM3.5 5C3.22386 5 3 5.22386 3 5.5V14.5C3 14.7761 3.22386 15 3.5 15H16.5C16.7761 15 17 14.7761 17 14.5V5.5C17 5.22386 16.7761 5 16.5 5H3.5Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Status and state related icons
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import type { IconProps } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan completed icon (14x14)
|
||||||
|
* Used for completed plan items
|
||||||
|
*/
|
||||||
|
export const PlanCompletedIcon: React.FC<IconProps> = ({
|
||||||
|
size = 14,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle cx="7" cy="7" r="6" fill="currentColor" opacity="0.2" />
|
||||||
|
<path
|
||||||
|
d="M4 7.5L6 9.5L10 4.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan in progress icon (14x14)
|
||||||
|
* Used for in-progress plan items
|
||||||
|
*/
|
||||||
|
export const PlanInProgressIcon: React.FC<IconProps> = ({
|
||||||
|
size = 14,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="7"
|
||||||
|
cy="7"
|
||||||
|
r="5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan pending icon (14x14)
|
||||||
|
* Used for pending plan items
|
||||||
|
*/
|
||||||
|
export const PlanPendingIcon: React.FC<IconProps> = ({
|
||||||
|
size = 14,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="7"
|
||||||
|
cy="7"
|
||||||
|
r="5.5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warning triangle icon (20x20)
|
||||||
|
* Used for warning messages
|
||||||
|
*/
|
||||||
|
export const WarningTriangleIcon: React.FC<IconProps> = ({
|
||||||
|
size = 20,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User profile icon (16x16)
|
||||||
|
* Used for login command
|
||||||
|
*/
|
||||||
|
export const UserIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SymbolIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M8 1a.5.5 0 0 1 .5.5v5.793l2.146-2.147a.5.5 0 0 1 .708.708l-3 3a.5.5 0 0 1-.708 0l-3-3a.5.5 0 1 1 .708-.708L7.5 7.293V1.5A.5.5 0 0 1 8 1Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SelectionIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Stop icon for canceling operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import type { IconProps } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop/square icon (16x16)
|
||||||
|
* Used for stop/cancel operations
|
||||||
|
*/
|
||||||
|
export const StopIcon: React.FC<IconProps> = ({
|
||||||
|
size = 16,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={className}
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<rect x="4" y="4" width="8" height="8" rx="1" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { IconProps } from './types.js';
|
||||||
|
export { FileIcon, FileListIcon, FolderIcon } from './FileIcons.js';
|
||||||
|
|
||||||
|
// Navigation icons
|
||||||
|
export {
|
||||||
|
ChevronDownIcon,
|
||||||
|
PlusIcon,
|
||||||
|
PlusSmallIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
CloseIcon,
|
||||||
|
CloseSmallIcon,
|
||||||
|
SearchIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
} from './NavigationIcons.js';
|
||||||
|
|
||||||
|
// Edit mode icons
|
||||||
|
export {
|
||||||
|
EditPencilIcon,
|
||||||
|
AutoEditIcon,
|
||||||
|
PlanModeIcon,
|
||||||
|
CodeBracketsIcon,
|
||||||
|
HideContextIcon,
|
||||||
|
SlashCommandIcon,
|
||||||
|
LinkIcon,
|
||||||
|
OpenDiffIcon,
|
||||||
|
} from './EditIcons.js';
|
||||||
|
|
||||||
|
// Status icons
|
||||||
|
export {
|
||||||
|
PlanCompletedIcon,
|
||||||
|
PlanInProgressIcon,
|
||||||
|
PlanPendingIcon,
|
||||||
|
WarningTriangleIcon,
|
||||||
|
UserIcon,
|
||||||
|
SymbolIcon,
|
||||||
|
SelectionIcon,
|
||||||
|
} from './StatusIcons.js';
|
||||||
|
|
||||||
|
// Special icons
|
||||||
|
export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js';
|
||||||
|
|
||||||
|
// Stop icon
|
||||||
|
export { StopIcon } from './StopIcon.js';
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Common icon props interface
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
export interface IconProps extends React.SVGProps<SVGSVGElement> {
|
||||||
|
/**
|
||||||
|
* Icon size (width and height)
|
||||||
|
* @default 16
|
||||||
|
*/
|
||||||
|
size?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional CSS classes
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { ChevronDownIcon, PlusIcon } from '../icons/index.js';
|
||||||
|
|
||||||
|
interface ChatHeaderProps {
|
||||||
|
currentSessionTitle: string;
|
||||||
|
onLoadSessions: () => void;
|
||||||
|
onNewSession: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||||
|
currentSessionTitle,
|
||||||
|
onLoadSessions,
|
||||||
|
onNewSession,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="chat-header flex items-center select-none w-full border-b border-[var(--app-primary-border-color)] bg-[var(--app-header-background)] py-1.5 px-2.5"
|
||||||
|
style={{ borderBottom: '1px solid var(--app-primary-border-color)' }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1.5 py-0.5 px-2 bg-transparent border-none rounded cursor-pointer outline-none min-w-0 max-w-[300px] overflow-hidden text-[var(--vscode-chat-font-size,13px)] font-[var(--vscode-chat-font-family)] hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
|
||||||
|
onClick={onLoadSessions}
|
||||||
|
title="Past conversations"
|
||||||
|
>
|
||||||
|
<span className="whitespace-nowrap overflow-hidden text-ellipsis min-w-0 font-medium">
|
||||||
|
{currentSessionTitle}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon className="w-4 h-4 flex-shrink-0" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-1"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center p-1 bg-transparent border-none rounded cursor-pointer outline-none hover:bg-[var(--app-ghost-button-hover-background)]"
|
||||||
|
onClick={onNewSession}
|
||||||
|
title="New Session"
|
||||||
|
style={{ padding: '4px' }}
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import type { CompletionItem } from '../../../types/completionItemTypes.js';
|
||||||
|
|
||||||
|
interface CompletionMenuProps {
|
||||||
|
items: CompletionItem[];
|
||||||
|
onSelect: (item: CompletionItem) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
selectedIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompletionMenu: React.FC<CompletionMenuProps> = ({
|
||||||
|
items,
|
||||||
|
onSelect,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
selectedIndex = 0,
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [selected, setSelected] = useState(selectedIndex);
|
||||||
|
// Mount state to drive a simple Tailwind transition (replaces CSS keyframes)
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => setSelected(selectedIndex), [selectedIndex]);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
setSelected((prev) => Math.min(prev + 1, items.length - 1));
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
setSelected((prev) => Math.max(prev - 1, 0));
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault();
|
||||||
|
if (items[selected]) {
|
||||||
|
onSelect(items[selected]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault();
|
||||||
|
onClose();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [items, selected, onSelect, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedEl = containerRef.current?.querySelector(
|
||||||
|
`[data-index="${selected}"]`,
|
||||||
|
);
|
||||||
|
if (selectedEl) {
|
||||||
|
selectedEl.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
role="menu"
|
||||||
|
className={[
|
||||||
|
'completion-menu',
|
||||||
|
// Positioning and container styling
|
||||||
|
'absolute bottom-full left-0 right-0 mb-2 flex flex-col overflow-hidden',
|
||||||
|
'rounded-large border bg-[var(--app-menu-background)]',
|
||||||
|
'border-[var(--app-input-border)] max-h-[50vh] z-[1000]',
|
||||||
|
// Mount animation (fade + slight slide up) via keyframes
|
||||||
|
mounted ? 'animate-completion-menu-enter' : '',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{/* Optional top spacer for visual separation from the input */}
|
||||||
|
<div className="h-1" />
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
// Semantic
|
||||||
|
'completion-menu-list',
|
||||||
|
// Scroll area
|
||||||
|
'flex max-h-[300px] flex-col overflow-y-auto',
|
||||||
|
// Spacing driven by theme vars
|
||||||
|
'p-[var(--app-list-padding)] pb-2 gap-[var(--app-list-gap)]',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<div className="completion-menu-section-label px-3 py-1 text-[var(--app-primary-foreground)] opacity-50 text-[0.9em]">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const isActive = index === selected;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
data-index={index}
|
||||||
|
role="menuitem"
|
||||||
|
onClick={() => onSelect(item)}
|
||||||
|
onMouseEnter={() => setSelected(index)}
|
||||||
|
className={[
|
||||||
|
// Semantic
|
||||||
|
'completion-menu-item',
|
||||||
|
// Hit area
|
||||||
|
'mx-1 cursor-pointer rounded-[var(--app-list-border-radius)]',
|
||||||
|
'p-[var(--app-list-item-padding)]',
|
||||||
|
// Active background
|
||||||
|
isActive ? 'bg-[var(--app-list-active-background)]' : '',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<div className="completion-menu-item-row flex items-center justify-between gap-2">
|
||||||
|
{item.icon && (
|
||||||
|
<span className="completion-menu-item-icon inline-flex h-4 w-4 items-center justify-center text-[var(--vscode-symbolIcon-fileForeground,#cccccc)]">
|
||||||
|
{item.icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
'completion-menu-item-label flex-1 truncate',
|
||||||
|
isActive
|
||||||
|
? 'text-[var(--app-list-active-foreground)]'
|
||||||
|
: 'text-[var(--app-primary-foreground)]',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
{item.description && (
|
||||||
|
<span
|
||||||
|
className="completion-menu-item-desc max-w-[50%] truncate text-[0.9em] text-[var(--app-secondary-foreground)] opacity-70"
|
||||||
|
title={item.description}
|
||||||
|
>
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
isAuthenticated?: boolean;
|
||||||
|
loadingMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get extension asset URL
|
||||||
|
function getExtensionAssetUrl(assetPath: string): string {
|
||||||
|
if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getURL) {
|
||||||
|
return chrome.runtime.getURL(assetPath);
|
||||||
|
}
|
||||||
|
// Fallback during development or if chrome API is not available
|
||||||
|
return assetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||||
|
isAuthenticated = false,
|
||||||
|
loadingMessage,
|
||||||
|
}) => {
|
||||||
|
const iconSrc = getExtensionAssetUrl('icons/icon-source.png');
|
||||||
|
|
||||||
|
const description = loadingMessage
|
||||||
|
? 'Preparing Qwen Code Chrome Extension...'
|
||||||
|
: isAuthenticated
|
||||||
|
? 'What would you like to do? Ask about this codebase or we can start writing code.'
|
||||||
|
: 'Welcome! Please log in to start using Qwen Code.';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-5 md:p-10">
|
||||||
|
<div className="flex flex-col items-center gap-8 w-full">
|
||||||
|
{/* Qwen Logo */}
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
<img
|
||||||
|
src={iconSrc}
|
||||||
|
alt="Qwen Logo"
|
||||||
|
className="w-[60px] h-[60px] object-contain"
|
||||||
|
onError={(e) => {
|
||||||
|
// Fallback to a div with text if image fails to load
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
const parent = target.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
const fallback = document.createElement('div');
|
||||||
|
fallback.className =
|
||||||
|
'w-[60px] h-[60px] flex items-center justify-center text-2xl font-bold';
|
||||||
|
fallback.textContent = 'Q';
|
||||||
|
parent.appendChild(fallback);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-[15px] text-app-primary-foreground leading-normal font-normal max-w-[400px]">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* FileLink component - Clickable file path links
|
||||||
|
* Supports clicking to open files and jump to specified line and column numbers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||||
|
// Tailwind rewrite: styles from FileLink.css are now expressed as utility classes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for FileLink
|
||||||
|
*/
|
||||||
|
interface FileLinkProps {
|
||||||
|
/** File path */
|
||||||
|
path: string;
|
||||||
|
/** Optional line number (starting from 1) */
|
||||||
|
line?: number | null;
|
||||||
|
/** Optional column number (starting from 1) */
|
||||||
|
column?: number | null;
|
||||||
|
/** Whether to show full path, default false (show filename only) */
|
||||||
|
showFullPath?: boolean;
|
||||||
|
/** Optional custom class name */
|
||||||
|
className?: string;
|
||||||
|
/** Whether to disable click behavior (use when parent element handles clicks) */
|
||||||
|
disableClick?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract filename from full path
|
||||||
|
* @param path File path
|
||||||
|
* @returns Filename
|
||||||
|
*/
|
||||||
|
function getFileName(path: string): string {
|
||||||
|
const segments = path.split(/[/\\]/);
|
||||||
|
return segments[segments.length - 1] || path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileLink component - Clickable file link
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Click to open file
|
||||||
|
* - Support line and column number navigation
|
||||||
|
* - Hover to show full path
|
||||||
|
* - Optional display mode (full path vs filename only)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <FileLink path="/src/App.tsx" line={42} />
|
||||||
|
* <FileLink path="/src/components/Button.tsx" line={10} column={5} showFullPath={true} />
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const FileLink: React.FC<FileLinkProps> = ({
|
||||||
|
path,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
showFullPath = false,
|
||||||
|
className = '',
|
||||||
|
disableClick = false,
|
||||||
|
}) => {
|
||||||
|
const vscode = useVSCode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click event - Send message to VSCode to open file
|
||||||
|
*/
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
// Always prevent default behavior (prevent <a> tag # navigation)
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (disableClick) {
|
||||||
|
// If click is disabled, return directly without stopping propagation
|
||||||
|
// This allows parent elements to handle click events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If click is enabled, stop event propagation
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Build full path including line and column numbers
|
||||||
|
let fullPath = path;
|
||||||
|
if (line !== null && line !== undefined) {
|
||||||
|
fullPath += `:${line}`;
|
||||||
|
if (column !== null && column !== undefined) {
|
||||||
|
fullPath += `:${column}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[FileLink] Opening file:', fullPath);
|
||||||
|
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'openFile',
|
||||||
|
data: { path: fullPath },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build display text
|
||||||
|
const displayPath = showFullPath ? path : getFileName(path);
|
||||||
|
|
||||||
|
// Build hover tooltip (always show full path)
|
||||||
|
const fullDisplayText =
|
||||||
|
line !== null && line !== undefined
|
||||||
|
? column !== null && column !== undefined
|
||||||
|
? `${path}:${line}:${column}`
|
||||||
|
: `${path}:${line}`
|
||||||
|
: path;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className={[
|
||||||
|
'file-link',
|
||||||
|
// Layout + interaction
|
||||||
|
// Use items-center + leading-none to vertically center within surrounding rows
|
||||||
|
'inline-flex items-center leading-none',
|
||||||
|
disableClick
|
||||||
|
? 'pointer-events-none cursor-[inherit] hover:no-underline'
|
||||||
|
: 'cursor-pointer',
|
||||||
|
// Typography + color: match theme body text and fixed size
|
||||||
|
'text-[11px] no-underline hover:underline',
|
||||||
|
'text-[var(--app-primary-foreground)]',
|
||||||
|
// Transitions
|
||||||
|
'transition-colors duration-100 ease-in-out',
|
||||||
|
// Focus ring (keyboard nav)
|
||||||
|
'focus:outline focus:outline-1 focus:outline-[var(--vscode-focusBorder)] focus:outline-offset-2 focus:rounded-[2px]',
|
||||||
|
// Active state
|
||||||
|
'active:opacity-80',
|
||||||
|
className,
|
||||||
|
].join(' ')}
|
||||||
|
onClick={handleClick}
|
||||||
|
title={fullDisplayText}
|
||||||
|
role="button"
|
||||||
|
aria-label={`Open file: ${fullDisplayText}`}
|
||||||
|
// Inherit font family from context so it matches theme body text.
|
||||||
|
>
|
||||||
|
<span className="file-link-path">{displayPath}</span>
|
||||||
|
{line !== null && line !== undefined && (
|
||||||
|
<span className="file-link-location opacity-70 text-[0.9em] font-normal dark:opacity-60">
|
||||||
|
:{line}
|
||||||
|
{column !== null && column !== undefined && <>:{column}</>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import {
|
||||||
|
EditPencilIcon,
|
||||||
|
AutoEditIcon,
|
||||||
|
PlanModeIcon,
|
||||||
|
CodeBracketsIcon,
|
||||||
|
HideContextIcon,
|
||||||
|
// ThinkingIcon, // Temporarily disabled
|
||||||
|
SlashCommandIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
StopIcon,
|
||||||
|
} from '../icons/index.js';
|
||||||
|
import { CompletionMenu } from '../layout/CompletionMenu.js';
|
||||||
|
import type { CompletionItem } from '../../../types/completionItemTypes.js';
|
||||||
|
import { getApprovalModeInfoFromString } from '../../types/acpTypes.js';
|
||||||
|
import type { ApprovalModeValue } from '../../../types/approvalModeValueTypes.js';
|
||||||
|
|
||||||
|
interface InputFormProps {
|
||||||
|
inputText: string;
|
||||||
|
// Note: RefObject<T> carries nullability in its `current` property, so the
|
||||||
|
// generic should be `HTMLDivElement` (not `HTMLDivElement | null`).
|
||||||
|
inputFieldRef: React.RefObject<HTMLDivElement>;
|
||||||
|
isStreaming: boolean;
|
||||||
|
isWaitingForResponse: boolean;
|
||||||
|
isComposing: boolean;
|
||||||
|
editMode: ApprovalModeValue;
|
||||||
|
thinkingEnabled: boolean;
|
||||||
|
activeFileName: string | null;
|
||||||
|
activeSelection: { startLine: number; endLine: number } | null;
|
||||||
|
// Whether to auto-load the active editor selection/path into context
|
||||||
|
skipAutoActiveContext: boolean;
|
||||||
|
onInputChange: (text: string) => void;
|
||||||
|
onCompositionStart: () => void;
|
||||||
|
onCompositionEnd: () => void;
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onToggleEditMode: () => void;
|
||||||
|
onToggleThinking: () => void;
|
||||||
|
onFocusActiveEditor: () => void;
|
||||||
|
onToggleSkipAutoActiveContext: () => void;
|
||||||
|
onShowCommandMenu: () => void;
|
||||||
|
onAttachContext: () => void;
|
||||||
|
completionIsOpen: boolean;
|
||||||
|
completionItems?: CompletionItem[];
|
||||||
|
onCompletionSelect?: (item: CompletionItem) => void;
|
||||||
|
onCompletionClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get edit mode display info using helper function
|
||||||
|
const getEditModeInfo = (editMode: ApprovalModeValue) => {
|
||||||
|
const info = getApprovalModeInfoFromString(editMode);
|
||||||
|
|
||||||
|
// Map icon types to actual icons
|
||||||
|
let icon = null;
|
||||||
|
switch (info.iconType) {
|
||||||
|
case 'edit':
|
||||||
|
icon = <EditPencilIcon />;
|
||||||
|
break;
|
||||||
|
case 'auto':
|
||||||
|
icon = <AutoEditIcon />;
|
||||||
|
break;
|
||||||
|
case 'plan':
|
||||||
|
icon = <PlanModeIcon />;
|
||||||
|
break;
|
||||||
|
case 'yolo':
|
||||||
|
icon = <AutoEditIcon />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
icon = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: info.label,
|
||||||
|
title: info.title,
|
||||||
|
icon,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputForm: React.FC<InputFormProps> = ({
|
||||||
|
inputText,
|
||||||
|
inputFieldRef,
|
||||||
|
isStreaming,
|
||||||
|
isWaitingForResponse,
|
||||||
|
isComposing,
|
||||||
|
editMode,
|
||||||
|
// thinkingEnabled, // Temporarily disabled
|
||||||
|
activeFileName,
|
||||||
|
activeSelection,
|
||||||
|
skipAutoActiveContext,
|
||||||
|
onInputChange,
|
||||||
|
onCompositionStart,
|
||||||
|
onCompositionEnd,
|
||||||
|
onKeyDown,
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
onToggleEditMode,
|
||||||
|
// onToggleThinking, // Temporarily disabled
|
||||||
|
onToggleSkipAutoActiveContext,
|
||||||
|
onShowCommandMenu,
|
||||||
|
onAttachContext,
|
||||||
|
completionIsOpen,
|
||||||
|
completionItems,
|
||||||
|
onCompletionSelect,
|
||||||
|
onCompletionClose,
|
||||||
|
}) => {
|
||||||
|
const editModeInfo = getEditModeInfo(editMode);
|
||||||
|
const composerDisabled = isStreaming || isWaitingForResponse;
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
// ESC should cancel the current interaction (stop generation)
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If composing (Chinese IME input), don't process Enter key
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && !isComposing) {
|
||||||
|
// If CompletionMenu is open, let it handle Enter key
|
||||||
|
if (completionIsOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(e);
|
||||||
|
}
|
||||||
|
onKeyDown(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Selection label like "6 lines selected"; no line numbers
|
||||||
|
const selectedLinesCount = activeSelection
|
||||||
|
? Math.max(1, activeSelection.endLine - activeSelection.startLine + 1)
|
||||||
|
: 0;
|
||||||
|
const selectedLinesText =
|
||||||
|
selectedLinesCount > 0
|
||||||
|
? `${selectedLinesCount} ${selectedLinesCount === 1 ? 'line' : 'lines'} selected`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-1 px-4 pb-4 absolute bottom-0 left-0 right-0 bg-gradient-to-b from-transparent to-[var(--app-primary-background)]">
|
||||||
|
<div className="block">
|
||||||
|
<form className="composer-form" onSubmit={onSubmit}>
|
||||||
|
{/* Inner background layer */}
|
||||||
|
<div className="composer-overlay" />
|
||||||
|
|
||||||
|
{/* Banner area */}
|
||||||
|
<div className="input-banner" />
|
||||||
|
|
||||||
|
<div className="relative flex z-[1]">
|
||||||
|
{completionIsOpen &&
|
||||||
|
completionItems &&
|
||||||
|
completionItems.length > 0 &&
|
||||||
|
onCompletionSelect &&
|
||||||
|
onCompletionClose && (
|
||||||
|
<CompletionMenu
|
||||||
|
items={completionItems}
|
||||||
|
onSelect={onCompletionSelect}
|
||||||
|
onClose={onCompletionClose}
|
||||||
|
title={undefined}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={inputFieldRef}
|
||||||
|
contentEditable="plaintext-only"
|
||||||
|
className="composer-input"
|
||||||
|
role="textbox"
|
||||||
|
aria-label="Message input"
|
||||||
|
aria-multiline="true"
|
||||||
|
data-placeholder="Ask Qwen Code …"
|
||||||
|
// Use a data flag so CSS can show placeholder even if the browser
|
||||||
|
// inserts an invisible <br> into contentEditable (so :empty no longer matches)
|
||||||
|
data-empty={
|
||||||
|
inputText.replace(/\u200B/g, '').trim().length === 0
|
||||||
|
? 'true'
|
||||||
|
: 'false'
|
||||||
|
}
|
||||||
|
onInput={(e) => {
|
||||||
|
const target = e.target as HTMLDivElement;
|
||||||
|
// Filter out zero-width space that we use to maintain height
|
||||||
|
const text = target.textContent?.replace(/\u200B/g, '') || '';
|
||||||
|
onInputChange(text);
|
||||||
|
}}
|
||||||
|
onCompositionStart={onCompositionStart}
|
||||||
|
onCompositionEnd={onCompositionEnd}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
suppressContentEditableWarning
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="composer-actions">
|
||||||
|
{/* Edit mode button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-text-compact btn-text-compact--primary"
|
||||||
|
title={editModeInfo.title}
|
||||||
|
onClick={onToggleEditMode}
|
||||||
|
>
|
||||||
|
{editModeInfo.icon}
|
||||||
|
{/* Let the label truncate with ellipsis; hide on very small screens */}
|
||||||
|
<span className="hidden sm:inline">{editModeInfo.text}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Active file indicator */}
|
||||||
|
{activeFileName && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-text-compact btn-text-compact--primary"
|
||||||
|
title={(() => {
|
||||||
|
if (skipAutoActiveContext) {
|
||||||
|
return selectedLinesText
|
||||||
|
? `Active selection will NOT be auto-loaded into context: ${selectedLinesText}`
|
||||||
|
: `Active file will NOT be auto-loaded into context: ${activeFileName}`;
|
||||||
|
}
|
||||||
|
return selectedLinesText
|
||||||
|
? `Showing Qwen Code your current selection: ${selectedLinesText}`
|
||||||
|
: `Showing Qwen Code your current file: ${activeFileName}`;
|
||||||
|
})()}
|
||||||
|
onClick={onToggleSkipAutoActiveContext}
|
||||||
|
>
|
||||||
|
{skipAutoActiveContext ? (
|
||||||
|
<HideContextIcon />
|
||||||
|
) : (
|
||||||
|
<CodeBracketsIcon />
|
||||||
|
)}
|
||||||
|
{/* Truncate file path/selection; hide label on very small screens */}
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{selectedLinesText || activeFileName}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1 min-w-0" />
|
||||||
|
|
||||||
|
{/* @yiliang114. closed temporarily */}
|
||||||
|
{/* Thinking button */}
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
className={`btn-icon-compact ${thinkingEnabled ? 'btn-icon-compact--active' : ''}`}
|
||||||
|
title={thinkingEnabled ? 'Thinking on' : 'Thinking off'}
|
||||||
|
onClick={onToggleThinking}
|
||||||
|
>
|
||||||
|
<ThinkingIcon enabled={thinkingEnabled} />
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
{/* Command button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-icon-compact hover:text-[var(--app-primary-foreground)]"
|
||||||
|
title="Show command menu (/)"
|
||||||
|
onClick={onShowCommandMenu}
|
||||||
|
>
|
||||||
|
<SlashCommandIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Attach button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-icon-compact hover:text-[var(--app-primary-foreground)]"
|
||||||
|
title="Attach context (Cmd/Ctrl + /)"
|
||||||
|
onClick={onAttachContext}
|
||||||
|
>
|
||||||
|
<LinkIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Send/Stop button */}
|
||||||
|
{isStreaming || isWaitingForResponse ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||||
|
onClick={onCancel}
|
||||||
|
title="Stop generation"
|
||||||
|
>
|
||||||
|
<StopIcon />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||||
|
disabled={composerDisabled || !inputText.trim()}
|
||||||
|
>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { generateIconUrl } from '../../utils/resourceUrl.js';
|
||||||
|
|
||||||
|
interface OnboardingPageProps {
|
||||||
|
onLogin: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Onboarding: React.FC<OnboardingPageProps> = ({ onLogin }) => {
|
||||||
|
const iconUri = generateIconUrl('icon.png');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-5 md:p-10">
|
||||||
|
<div className="flex flex-col items-center gap-8 w-full max-w-md mx-auto">
|
||||||
|
<div className="flex flex-col items-center gap-6">
|
||||||
|
{/* Application icon container */}
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={iconUri}
|
||||||
|
alt="Qwen Code Logo"
|
||||||
|
className="w-[80px] h-[80px] object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-2xl font-bold text-app-primary-foreground mb-2">
|
||||||
|
Welcome to Qwen Code
|
||||||
|
</h1>
|
||||||
|
<p className="text-app-secondary-foreground max-w-sm">
|
||||||
|
Unlock the power of AI to understand, navigate, and transform your
|
||||||
|
codebase faster than ever before.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onLogin}
|
||||||
|
className="w-full px-4 py-3 bg-[#4f46e5] text-white font-medium rounded-lg shadow-sm hover:bg-[#4338ca] transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Get Started with Qwen Code
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
getTimeAgo,
|
||||||
|
groupSessionsByDate,
|
||||||
|
} from '../../utils/sessionGrouping.js';
|
||||||
|
import { SearchIcon } from '../icons/index.js';
|
||||||
|
|
||||||
|
interface SessionSelectorProps {
|
||||||
|
visible: boolean;
|
||||||
|
sessions: Array<Record<string, unknown>>;
|
||||||
|
currentSessionId: string | null;
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchChange: (query: string) => void;
|
||||||
|
onSelectSession: (sessionId: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
hasMore?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onLoadMore?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session selector component
|
||||||
|
* Display session list and support search and selection
|
||||||
|
*/
|
||||||
|
export const SessionSelector: React.FC<SessionSelectorProps> = ({
|
||||||
|
visible,
|
||||||
|
sessions,
|
||||||
|
currentSessionId,
|
||||||
|
searchQuery,
|
||||||
|
onSearchChange,
|
||||||
|
onSelectSession,
|
||||||
|
onClose,
|
||||||
|
hasMore = false,
|
||||||
|
isLoading = false,
|
||||||
|
onLoadMore,
|
||||||
|
}) => {
|
||||||
|
if (!visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasNoSessions = sessions.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="session-selector-backdrop fixed top-0 left-0 right-0 bottom-0 z-[999] bg-transparent"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="session-dropdown fixed bg-[var(--app-menu-background)] rounded-[var(--corner-radius-small)] w-[min(400px,calc(100vw-32px))] max-h-[min(500px,50vh)] flex flex-col shadow-[0_4px_16px_rgba(0,0,0,0.1)] z-[1000] outline-none text-[var(--vscode-chat-font-size,13px)] font-[var(--vscode-chat-font-family)]"
|
||||||
|
tabIndex={-1}
|
||||||
|
style={{
|
||||||
|
top: '30px',
|
||||||
|
left: '10px',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Search Box */}
|
||||||
|
<div className="session-search p-2 flex items-center gap-2">
|
||||||
|
<SearchIcon className="session-search-icon w-4 h-4 opacity-50 flex-shrink-0 text-[var(--app-primary-foreground)]" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="session-search-input flex-1 bg-transparent border-none outline-none text-[var(--app-menu-foreground)] text-[var(--vscode-chat-font-size,13px)] font-[var(--vscode-chat-font-family)] p-0 placeholder:text-[var(--app-input-placeholder-foreground)] placeholder:opacity-60"
|
||||||
|
placeholder="Search sessions…"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Session List with Grouping */}
|
||||||
|
<div
|
||||||
|
className="session-list-content overflow-y-auto flex-1 select-none p-2"
|
||||||
|
onScroll={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
const distanceToBottom =
|
||||||
|
el.scrollHeight - (el.scrollTop + el.clientHeight);
|
||||||
|
if (distanceToBottom < 48 && hasMore && !isLoading) {
|
||||||
|
onLoadMore?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasNoSessions ? (
|
||||||
|
<div
|
||||||
|
className="p-5 text-center text-[var(--app-secondary-foreground)]"
|
||||||
|
style={{
|
||||||
|
padding: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'var(--app-secondary-foreground)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchQuery ? 'No matching sessions' : 'No sessions available'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
groupSessionsByDate(sessions).map((group) => (
|
||||||
|
<React.Fragment key={group.label}>
|
||||||
|
<div className="session-group-label p-1 px-2 text-[var(--app-primary-foreground)] opacity-50 text-[0.9em] font-medium [&:not(:first-child)]:mt-2">
|
||||||
|
{group.label}
|
||||||
|
</div>
|
||||||
|
<div className="session-group flex flex-col gap-[2px]">
|
||||||
|
{group.sessions.map((session) => {
|
||||||
|
const sessionId =
|
||||||
|
(session.id as string) ||
|
||||||
|
(session.sessionId as string) ||
|
||||||
|
'';
|
||||||
|
const title =
|
||||||
|
(session.title as string) ||
|
||||||
|
(session.name as string) ||
|
||||||
|
'Untitled';
|
||||||
|
const lastUpdated =
|
||||||
|
(session.lastUpdated as string) ||
|
||||||
|
(session.startTime as string) ||
|
||||||
|
'';
|
||||||
|
const isActive = sessionId === currentSessionId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={sessionId}
|
||||||
|
className={`session-item flex items-center justify-between py-1.5 px-2 bg-transparent border-none rounded-md cursor-pointer text-left w-full text-[var(--vscode-chat-font-size,13px)] font-[var(--vscode-chat-font-family)] text-[var(--app-primary-foreground)] transition-colors duration-100 hover:bg-[var(--app-list-hover-background)] ${
|
||||||
|
isActive
|
||||||
|
? 'active bg-[var(--app-list-active-background)] text-[var(--app-list-active-foreground)] font-[600]'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
onSelectSession(sessionId);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="session-item-title flex-1 overflow-hidden text-ellipsis whitespace-nowrap min-w-0">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span className="session-item-time opacity-60 text-[0.9em] flex-shrink-0 ml-3">
|
||||||
|
{getTimeAgo(lastUpdated)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="p-2 text-center opacity-60 text-[0.9em]">
|
||||||
|
{isLoading ? 'Loading…' : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* AssistantMessage Component Styles
|
||||||
|
* Pseudo-elements (::before) for bullet points and (::after) for timeline connectors
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Bullet point indicator using ::before pseudo-element */
|
||||||
|
.assistant-message-container.assistant-message-default::before,
|
||||||
|
.assistant-message-container.assistant-message-success::before,
|
||||||
|
.assistant-message-container.assistant-message-error::before,
|
||||||
|
.assistant-message-container.assistant-message-warning::before,
|
||||||
|
.assistant-message-container.assistant-message-loading::before {
|
||||||
|
content: '\25cf';
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
padding-top: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default state - secondary foreground color */
|
||||||
|
.assistant-message-container.assistant-message-default::before {
|
||||||
|
color: var(--app-secondary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success state - green bullet (maps to .ge) */
|
||||||
|
.assistant-message-container.assistant-message-success::before {
|
||||||
|
color: #74c991;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error state - red bullet (maps to .be) */
|
||||||
|
.assistant-message-container.assistant-message-error::before {
|
||||||
|
color: #c74e39;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warning state - yellow/orange bullet (maps to .ue) */
|
||||||
|
.assistant-message-container.assistant-message-warning::before {
|
||||||
|
color: #e1c08d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state - static bullet (maps to .he) */
|
||||||
|
.assistant-message-container.assistant-message-loading::before {
|
||||||
|
color: var(--app-secondary-foreground);
|
||||||
|
background-color: var(--app-secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message-container.assistant-message-loading::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { MessageContent } from '../MessageContent.js';
|
||||||
|
import './AssistantMessage.css';
|
||||||
|
|
||||||
|
interface AssistantMessageProps {
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
onFileClick?: (path: string) => void;
|
||||||
|
status?: 'default' | 'success' | 'error' | 'warning' | 'loading';
|
||||||
|
// When true, render without the left status bullet (no ::before dot)
|
||||||
|
hideStatusIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AssistantMessage component - renders AI responses with Qwen Code styling
|
||||||
|
* Supports different states: default, success, error, warning, loading
|
||||||
|
*/
|
||||||
|
export const AssistantMessage: React.FC<AssistantMessageProps> = ({
|
||||||
|
content,
|
||||||
|
timestamp: _timestamp,
|
||||||
|
onFileClick,
|
||||||
|
status = 'default',
|
||||||
|
hideStatusIcon = false,
|
||||||
|
}) => {
|
||||||
|
// Empty content not rendered directly, avoid poor visual experience from only showing ::before dot
|
||||||
|
if (!content || content.trim().length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map status to CSS class (only for ::before pseudo-element)
|
||||||
|
const getStatusClass = () => {
|
||||||
|
if (hideStatusIcon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case 'success':
|
||||||
|
return 'assistant-message-success';
|
||||||
|
case 'error':
|
||||||
|
return 'assistant-message-error';
|
||||||
|
case 'warning':
|
||||||
|
return 'assistant-message-warning';
|
||||||
|
case 'loading':
|
||||||
|
return 'assistant-message-loading';
|
||||||
|
default:
|
||||||
|
return 'assistant-message-default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`qwen-message message-item assistant-message-container ${getStatusClass()}`}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
paddingLeft: '30px',
|
||||||
|
userSelect: 'text',
|
||||||
|
position: 'relative',
|
||||||
|
// paddingTop: '8px',
|
||||||
|
// paddingBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ width: '100%' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
width: '100%',
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MessageContent
|
||||||
|
content={content}
|
||||||
|
onFileClick={onFileClick}
|
||||||
|
enableFileLinks={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Styles for MarkdownRenderer component
|
||||||
|
*/
|
||||||
|
|
||||||
|
.markdown-content {
|
||||||
|
/* Base styles for markdown content */
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--app-primary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1,
|
||||||
|
.markdown-content h2,
|
||||||
|
.markdown-content h3,
|
||||||
|
.markdown-content h4,
|
||||||
|
.markdown-content h5,
|
||||||
|
.markdown-content h6 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
border-bottom: 1px solid var(--app-primary-border-color);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-bottom: 1px solid var(--app-primary-border-color);
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h4 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h5,
|
||||||
|
.markdown-content h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
margin-top: 0;
|
||||||
|
/* margin-bottom: 1em; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure list markers are visible even with global CSS resets */
|
||||||
|
.markdown-content ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
list-style-position: outside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
list-style-position: outside;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested list styles */
|
||||||
|
.markdown-content ul ul {
|
||||||
|
list-style-type: circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul ul ul {
|
||||||
|
list-style-type: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol ol {
|
||||||
|
list-style-type: lower-alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol ol ol {
|
||||||
|
list-style-type: lower-roman;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the marker explicitly so themes don't hide it */
|
||||||
|
.markdown-content li::marker {
|
||||||
|
color: var(--app-secondary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li > p {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
margin: 0 0 1em;
|
||||||
|
padding: 0 1em;
|
||||||
|
border-left: 0.25em solid var(--app-primary-border-color);
|
||||||
|
color: var(--app-secondary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a {
|
||||||
|
color: var(--app-link-foreground, #007acc);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content a:hover {
|
||||||
|
color: var(--app-link-active-foreground, #005a9e);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
font-family: var(
|
||||||
|
--app-monospace-font-family,
|
||||||
|
'SF Mono',
|
||||||
|
Monaco,
|
||||||
|
'Cascadia Code',
|
||||||
|
'Roboto Mono',
|
||||||
|
Consolas,
|
||||||
|
'Courier New',
|
||||||
|
monospace
|
||||||
|
);
|
||||||
|
font-size: 0.9em;
|
||||||
|
background-color: var(--app-code-background, rgba(0, 0, 0, 0.05));
|
||||||
|
border: 1px solid var(--app-primary-border-color);
|
||||||
|
border-radius: var(--corner-radius-small, 4px);
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
white-space: pre-wrap; /* Support automatic line wrapping */
|
||||||
|
word-break: break-word; /* Break words when necessary */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 1em;
|
||||||
|
overflow-x: auto;
|
||||||
|
background-color: var(--app-code-background, rgba(0, 0, 0, 0.05));
|
||||||
|
border: 1px solid var(--app-primary-border-color);
|
||||||
|
border-radius: var(--corner-radius-small, 4px);
|
||||||
|
font-family: var(
|
||||||
|
--app-monospace-font-family,
|
||||||
|
'SF Mono',
|
||||||
|
Monaco,
|
||||||
|
'Cascadia Code',
|
||||||
|
'Roboto Mono',
|
||||||
|
Consolas,
|
||||||
|
'Courier New',
|
||||||
|
monospace
|
||||||
|
);
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre code {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre-wrap; /* Support automatic line wrapping */
|
||||||
|
word-break: break-word; /* Break words when necessary */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content .file-path-link {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font-family: var(
|
||||||
|
--app-monospace-font-family,
|
||||||
|
'SF Mono',
|
||||||
|
Monaco,
|
||||||
|
'Cascadia Code',
|
||||||
|
'Roboto Mono',
|
||||||
|
Consolas,
|
||||||
|
'Courier New',
|
||||||
|
monospace
|
||||||
|
);
|
||||||
|
font-size: 0.95em;
|
||||||
|
color: var(--app-link-foreground, #007acc);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content .file-path-link:hover {
|
||||||
|
color: var(--app-link-active-foreground, #005a9e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--app-primary-border-color);
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th,
|
||||||
|
.markdown-content td {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border: 1px solid var(--app-primary-border-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th {
|
||||||
|
background-color: var(--app-secondary-background);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Qwen Team
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* MarkdownRenderer component - renders markdown content with syntax highlighting and clickable file paths
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import MarkdownIt from 'markdown-it';
|
||||||
|
import type { Options as MarkdownItOptions } from 'markdown-it';
|
||||||
|
import './MarkdownRenderer.css';
|
||||||
|
|
||||||
|
interface MarkdownRendererProps {
|
||||||
|
content: string;
|
||||||
|
onFileClick?: (filePath: string) => void;
|
||||||
|
/** When false, do not convert file paths into clickable links. Default: true */
|
||||||
|
enableFileLinks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular expressions for parsing content
|
||||||
|
*/
|
||||||
|
// Match absolute file paths like: /path/to/file.ts or C:\path\to\file.ts
|
||||||
|
const FILE_PATH_REGEX =
|
||||||
|
/(?:[a-zA-Z]:)?[/\\](?:[\w\-. ]+[/\\])+[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi;
|
||||||
|
// Match file paths with optional line numbers like: /path/to/file.ts#7-14 or C:\path\to\file.ts#7
|
||||||
|
const FILE_PATH_WITH_LINES_REGEX =
|
||||||
|
/(?:[a-zA-Z]:)?[/\\](?:[\w\-. ]+[/\\])+[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MarkdownRenderer component - renders markdown content with enhanced features
|
||||||
|
*/
|
||||||
|
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||||
|
content,
|
||||||
|
onFileClick,
|
||||||
|
enableFileLinks = true,
|
||||||
|
}) => {
|
||||||
|
/**
|
||||||
|
* Initialize markdown-it with plugins
|
||||||
|
*/
|
||||||
|
const getMarkdownInstance = (): MarkdownIt => {
|
||||||
|
// Create markdown-it instance with options
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: false, // Disable HTML for security
|
||||||
|
xhtmlOut: false,
|
||||||
|
breaks: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
|
} as MarkdownItOptions);
|
||||||
|
|
||||||
|
return md;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render markdown content to HTML
|
||||||
|
*/
|
||||||
|
const renderMarkdown = (): string => {
|
||||||
|
try {
|
||||||
|
const md = getMarkdownInstance();
|
||||||
|
|
||||||
|
// Process the markdown content
|
||||||
|
let html = md.render(content);
|
||||||
|
|
||||||
|
// Post-process to add file path click handlers unless disabled
|
||||||
|
if (enableFileLinks) {
|
||||||
|
html = processFilePaths(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering markdown:', error);
|
||||||
|
// Fallback to plain text if markdown rendering fails
|
||||||
|
return escapeHtml(content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML characters for security
|
||||||
|
*/
|
||||||
|
const escapeHtml = (unsafe: string): string =>
|
||||||
|
unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process file paths in HTML to make them clickable
|
||||||
|
*/
|
||||||
|
const processFilePaths = (html: string): string => {
|
||||||
|
// If DOM is not available, bail out to avoid breaking SSR
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build non-global variants to avoid .test() statefulness
|
||||||
|
const FILE_PATH_NO_G = new RegExp(
|
||||||
|
FILE_PATH_REGEX.source,
|
||||||
|
FILE_PATH_REGEX.flags.replace('g', ''),
|
||||||
|
);
|
||||||
|
const FILE_PATH_WITH_LINES_NO_G = new RegExp(
|
||||||
|
FILE_PATH_WITH_LINES_REGEX.source,
|
||||||
|
FILE_PATH_WITH_LINES_REGEX.flags.replace('g', ''),
|
||||||
|
);
|
||||||
|
// Match a bare file name like README.md (no leading slash)
|
||||||
|
const BARE_FILE_REGEX =
|
||||||
|
/[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|ya?ml|toml|xml|html|vue|svelte)/i;
|
||||||
|
|
||||||
|
// Parse HTML into a DOM tree so we don't replace inside attributes
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
const union = new RegExp(
|
||||||
|
`${FILE_PATH_WITH_LINES_REGEX.source}|${FILE_PATH_REGEX.source}|${BARE_FILE_REGEX.source}`,
|
||||||
|
'gi',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert a "path#fragment" into VS Code friendly "path:line" (we only keep the start line)
|
||||||
|
const normalizePathAndLine = (
|
||||||
|
raw: string,
|
||||||
|
): { displayText: string; dataPath: string } => {
|
||||||
|
const displayText = raw;
|
||||||
|
let base = raw;
|
||||||
|
// Extract hash fragment like #12, #L12 or #12-34 and keep only the first number
|
||||||
|
const hashIndex = raw.indexOf('#');
|
||||||
|
if (hashIndex >= 0) {
|
||||||
|
const frag = raw.slice(hashIndex + 1);
|
||||||
|
// Accept L12, 12 or 12-34
|
||||||
|
const m = frag.match(/^L?(\d+)(?:-\d+)?$/i);
|
||||||
|
if (m) {
|
||||||
|
const line = parseInt(m[1], 10);
|
||||||
|
base = raw.slice(0, hashIndex);
|
||||||
|
return { displayText, dataPath: `${base}:${line}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { displayText, dataPath: base };
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeLink = (text: string) => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
// Pass base path (with optional :line) to the handler; keep the full text as label
|
||||||
|
const { dataPath } = normalizePathAndLine(text);
|
||||||
|
link.className = 'file-path-link';
|
||||||
|
link.textContent = text;
|
||||||
|
link.setAttribute('href', '#');
|
||||||
|
link.setAttribute('title', `Open ${text}`);
|
||||||
|
// Carry file path via data attribute; click handled by event delegation
|
||||||
|
link.setAttribute('data-file-path', dataPath);
|
||||||
|
return link;
|
||||||
|
};
|
||||||
|
|
||||||
|
const upgradeAnchorIfFilePath = (a: HTMLAnchorElement) => {
|
||||||
|
const href = a.getAttribute('href') || '';
|
||||||
|
const text = (a.textContent || '').trim();
|
||||||
|
|
||||||
|
// Helper: identify dot-chained code refs (e.g. vscode.commands.register)
|
||||||
|
// but DO NOT treat filenames/paths as code refs.
|
||||||
|
const isCodeReference = (str: string): boolean => {
|
||||||
|
if (BARE_FILE_REGEX.test(str)) {
|
||||||
|
return false; // looks like a filename
|
||||||
|
}
|
||||||
|
if (/[/\\]/.test(str)) {
|
||||||
|
return false; // contains a path separator
|
||||||
|
}
|
||||||
|
const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/;
|
||||||
|
return codeRefPattern.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If linkify turned a bare filename (e.g. README.md) into http://<filename>, convert it back
|
||||||
|
const httpMatch = href.match(/^https?:\/\/(.+)$/i);
|
||||||
|
if (httpMatch) {
|
||||||
|
try {
|
||||||
|
const url = new URL(href);
|
||||||
|
const host = url.hostname || '';
|
||||||
|
const pathname = url.pathname || '';
|
||||||
|
const noPath = pathname === '' || pathname === '/';
|
||||||
|
|
||||||
|
// Case 1: anchor text itself is a bare filename and equals the host (e.g. README.md)
|
||||||
|
if (
|
||||||
|
noPath &&
|
||||||
|
BARE_FILE_REGEX.test(text) &&
|
||||||
|
host.toLowerCase() === text.toLowerCase()
|
||||||
|
) {
|
||||||
|
const { dataPath } = normalizePathAndLine(text);
|
||||||
|
a.classList.add('file-path-link');
|
||||||
|
a.setAttribute('href', '#');
|
||||||
|
a.setAttribute('title', `Open ${text}`);
|
||||||
|
a.setAttribute('data-file-path', dataPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2: host itself looks like a filename (rare but happens), use it
|
||||||
|
if (noPath && BARE_FILE_REGEX.test(host)) {
|
||||||
|
const { dataPath } = normalizePathAndLine(host);
|
||||||
|
a.classList.add('file-path-link');
|
||||||
|
a.setAttribute('href', '#');
|
||||||
|
a.setAttribute('title', `Open ${text || host}`);
|
||||||
|
a.setAttribute('data-file-path', dataPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through; unparseable URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore other external protocols
|
||||||
|
if (/^(https?|mailto|ftp|data):/i.test(href)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = href || text;
|
||||||
|
|
||||||
|
// Skip if it looks like a code reference
|
||||||
|
if (isCodeReference(candidate)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
FILE_PATH_WITH_LINES_NO_G.test(candidate) ||
|
||||||
|
FILE_PATH_NO_G.test(candidate)
|
||||||
|
) {
|
||||||
|
const { dataPath } = normalizePathAndLine(candidate);
|
||||||
|
a.classList.add('file-path-link');
|
||||||
|
a.setAttribute('href', '#');
|
||||||
|
a.setAttribute('title', `Open ${text || href}`);
|
||||||
|
a.setAttribute('data-file-path', dataPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bare file name or relative path (e.g. README.md or docs/README.md)
|
||||||
|
if (BARE_FILE_REGEX.test(candidate)) {
|
||||||
|
const { dataPath } = normalizePathAndLine(candidate);
|
||||||
|
a.classList.add('file-path-link');
|
||||||
|
a.setAttribute('href', '#');
|
||||||
|
a.setAttribute('title', `Open ${text || href}`);
|
||||||
|
a.setAttribute('data-file-path', dataPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: identify dot-chained code refs (e.g. vscode.commands.register)
|
||||||
|
// but DO NOT treat filenames/paths as code refs.
|
||||||
|
const isCodeReference = (str: string): boolean => {
|
||||||
|
if (BARE_FILE_REGEX.test(str)) {
|
||||||
|
return false; // looks like a filename
|
||||||
|
}
|
||||||
|
if (/[/\\]/.test(str)) {
|
||||||
|
return false; // contains a path separator
|
||||||
|
}
|
||||||
|
const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/;
|
||||||
|
return codeRefPattern.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
const walk = (node: Node) => {
|
||||||
|
// Do not transform inside existing anchors
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const el = node as HTMLElement;
|
||||||
|
if (el.tagName.toLowerCase() === 'a') {
|
||||||
|
upgradeAnchorIfFilePath(el as HTMLAnchorElement);
|
||||||
|
return; // Don't descend into <a>
|
||||||
|
}
|
||||||
|
// Avoid transforming inside code/pre blocks
|
||||||
|
const tag = el.tagName.toLowerCase();
|
||||||
|
if (tag === 'code' || tag === 'pre') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child = node.firstChild; child; ) {
|
||||||
|
const next = child.nextSibling; // child may be replaced
|
||||||
|
if (child.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = child.nodeValue || '';
|
||||||
|
union.lastIndex = 0;
|
||||||
|
const hasMatch = union.test(text);
|
||||||
|
union.lastIndex = 0;
|
||||||
|
if (hasMatch) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
let lastIndex = 0;
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = union.exec(text))) {
|
||||||
|
const matchText = m[0];
|
||||||
|
const idx = m.index;
|
||||||
|
|
||||||
|
// Skip if it looks like a code reference
|
||||||
|
if (isCodeReference(matchText)) {
|
||||||
|
// Just add the text as-is without creating a link
|
||||||
|
if (idx > lastIndex) {
|
||||||
|
frag.appendChild(
|
||||||
|
document.createTextNode(text.slice(lastIndex, idx)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frag.appendChild(document.createTextNode(matchText));
|
||||||
|
lastIndex = idx + matchText.length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx > lastIndex) {
|
||||||
|
frag.appendChild(
|
||||||
|
document.createTextNode(text.slice(lastIndex, idx)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
frag.appendChild(makeLink(matchText));
|
||||||
|
lastIndex = idx + matchText.length;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
|
||||||
|
}
|
||||||
|
node.replaceChild(frag, child);
|
||||||
|
}
|
||||||
|
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
walk(child);
|
||||||
|
}
|
||||||
|
child = next;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(container);
|
||||||
|
return container.innerHTML;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event delegation: intercept clicks on generated file-path links
|
||||||
|
const handleContainerClick = (
|
||||||
|
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||||
|
) => {
|
||||||
|
// If file links disabled, do nothing
|
||||||
|
if (!enableFileLinks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find nearest anchor with our marker class
|
||||||
|
const anchor = (target.closest &&
|
||||||
|
target.closest('a.file-path-link')) as HTMLAnchorElement | null;
|
||||||
|
if (anchor) {
|
||||||
|
const filePath = anchor.getAttribute('data-file-path');
|
||||||
|
if (!filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onFileClick?.(filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: intercept "http://README.md" style links that slipped through
|
||||||
|
const anyAnchor = (target.closest &&
|
||||||
|
target.closest('a')) as HTMLAnchorElement | null;
|
||||||
|
if (!anyAnchor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = anyAnchor.getAttribute('href') || '';
|
||||||
|
if (!/^https?:\/\//i.test(href)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(href);
|
||||||
|
const host = url.hostname || '';
|
||||||
|
const path = url.pathname || '';
|
||||||
|
const noPath = path === '' || path === '/';
|
||||||
|
|
||||||
|
// Basic bare filename heuristic on the host part (e.g. README.md)
|
||||||
|
if (noPath && /\.[a-z0-9]+$/i.test(host)) {
|
||||||
|
// Prefer the readable text content if it looks like a file
|
||||||
|
const text = (anyAnchor.textContent || '').trim();
|
||||||
|
const candidate = /\.[a-z0-9]+$/i.test(text) ? text : host;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onFileClick?.(candidate);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="markdown-content"
|
||||||
|
onClick={handleContainerClick}
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderMarkdown() }}
|
||||||
|
style={{
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user