Skip to content

Commit efbccbf

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/tui-foundation-batch
2 parents 53d657e + fca2acd commit efbccbf

52 files changed

Lines changed: 6746 additions & 309 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/scripts/web-shell-visuals-publish.test.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@ function runHostingBlock(
202202
runAttempt = '1',
203203
} = {},
204204
) {
205-
const dir = mkdtempSync(join(tmpdir(), 'visuals-hosting-'));
205+
const scopeRoot = mkdtempSync(join(tmpdir(), 'visuals-hosting-scope-'));
206+
const dir = join(scopeRoot, 'fixture');
207+
writeFileSync(join(scopeRoot, 'package.json'), '{"type":"commonjs"}\n');
208+
mkdirSync(dir);
206209
try {
207210
return runHostingBlockIn(dir, hasImages, {
208211
publicBaseUrl,
@@ -213,7 +216,7 @@ function runHostingBlock(
213216
} finally {
214217
// The fixture used to leak a mkdtemp dir per call; capture everything
215218
// the assertions need inside, then tear it down.
216-
rmSync(dir, { recursive: true, force: true });
219+
rmSync(scopeRoot, { recursive: true, force: true });
217220
}
218221
}
219222

.github/workflows/ci.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,14 @@ jobs:
209209
# unit tests.
210210
if: "${{ !cancelled() && github.event_name != 'schedule' }}"
211211
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
212-
timeout-minutes: 60
212+
# Shared ECS hosts can stretch the normally 25-minute lane beyond an hour
213+
# while tests are still making progress. Keep a finite ceiling without
214+
# turning host contention into a false cancellation. The extension is
215+
# scoped to the pool that shows the contention: fork PRs from untrusted
216+
# authors and MAINTAINER_ECS_RUNNER_DISABLED=true fall back to
217+
# GitHub-hosted runners (see classify_pr), which keep the pre-contention
218+
# ceiling so a genuine hang there does not burn the extra 30 minutes.
219+
timeout-minutes: '${{ fromJSON(contains(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'', ''ecs-qwen'') && ''90'' || ''60'') }}'
213220
outputs:
214221
ci_profile: '${{ steps.ci_profile.outputs.ci_profile }}'
215222
permissions:
@@ -1305,7 +1312,11 @@ jobs:
13051312
# serialize this job behind the hour-long unit run.
13061313
if: "${{ !cancelled() && needs.classify_pr.outputs.skip_ci != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}"
13071314
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
1308-
timeout-minutes: 30
1315+
# Dependency installation alone can exceed 30 minutes on a contended shared
1316+
# ECS host, before the separately bounded 20-minute integration step starts.
1317+
# Same ECS-only scoping as the `test` job: hosted fallback routing keeps
1318+
# the pre-contention ceiling.
1319+
timeout-minutes: '${{ fromJSON(contains(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'', ''ecs-qwen'') && ''60'' || ''30'') }}'
13091320
permissions:
13101321
contents: 'read'
13111322
steps:

.github/workflows/e2e.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,9 @@ jobs:
216216
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
217217
KEEP_OUTPUT: 'true'
218218
VERBOSE: 'true'
219-
# Mapped for integration-tests/vitest.config.ts, which exempts
220-
# self-hosted runners from pressure-flake unhandled errors.
219+
# Mapped for integration-tests/vitest.config.ts, which caps each
220+
# shared-pool shard at one fork and exempts pressure-flake unhandled
221+
# errors.
221222
RUNNER_ENVIRONMENT: '${{ runner.environment }}'
222223
run: |-
223224
export TMPDIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"

.github/workflows/sdk-java.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,20 @@ jobs:
126126
tar -xzf "${archive}" -C "${RUNNER_TEMP}"
127127
echo "${RUNNER_TEMP}/apache-maven-${MAVEN_VERSION}/bin" >> "${GITHUB_PATH}"
128128
129-
- name: 'Run Java SDK tests'
129+
- name: 'Run Java SDK tests (self-hosted)'
130+
if: "${{ runner.environment == 'self-hosted' }}"
131+
working-directory: 'packages/sdk-java/qwencode'
132+
run: |-
133+
mkdir -p "${HOME}/.cache/qwen-code-ci"
134+
exec 9>"${HOME}/.cache/qwen-code-ci/sdk-java-tests.lock"
135+
if ! flock --wait 1200 9; then
136+
echo "::error::sdk-java host lock not acquired within 20 minutes"
137+
exit 1
138+
fi
139+
mvn --batch-mode --no-transfer-progress clean test
140+
141+
- name: 'Run Java SDK tests (hosted)'
142+
if: "${{ runner.environment == 'github-hosted' }}"
130143
working-directory: 'packages/sdk-java/qwencode'
131144
run: 'mvn --batch-mode --no-transfer-progress clean test'
132145

.github/workflows/serve-ab.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ jobs:
7272
# other fork PRs stay on ephemeral hosted runners. Keep in sync with
7373
# ci.yml's classify_pr routing. Kill-switch: MAINTAINER_ECS_RUNNER_DISABLED.
7474
runs-on: '${{ (github.repository == ''QwenLM/qwen-code'' && vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event.pull_request.head.repo.full_name == github.repository || contains(fromJSON(''["OWNER","MEMBER","COLLABORATOR"]''), github.event.pull_request.author_association))) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'
75-
# Two full checkouts, each npm-ci + build + drive: a healthy run lands
76-
# near twenty minutes, and a slow runner pushed a run past the old
77-
# 30-minute bound, cancelling it.
78-
timeout-minutes: 45
75+
# Two full checkouts, each npm-ci + build + drive. A contended shared ECS
76+
# runner completed the PR-head cycle but hit the 45-minute ceiling near
77+
# the end of the merge-base cycle, so keep a finite 60-minute budget.
78+
timeout-minutes: 60
7979
steps:
8080
- name: 'Restore workspace ownership'
8181
if: "${{ runner.environment == 'self-hosted' }}"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Web Shell workspace overview (sidebar)
2+
3+
Issue: https://github.com/QwenLM/qwen-code/issues/10399
4+
Design: `docs/design/web-shell/web-shell-workspace-overview.md`
5+
6+
## Baseline
7+
8+
- `main` @ d853f09f: a workspace row shows the folder name, untrusted /
9+
read-only badges and a git chip. The `` menu has one entry, Remove
10+
workspace, and only on removable secondary workspaces.
11+
- The daemon already answers `GET /workspaces/:w/{mcp,skills,extensions,channels,memory,hooks}`
12+
and `PATCH /workspaces/:w` (rename) and `POST /workspaces/:w/reload`.
13+
14+
## Local setup
15+
16+
1. `npm run build` from the repository root so `dist/cli.js` bundles the
17+
Web Shell from this branch.
18+
2. Isolated `QWEN_HOME` with a settings.json that registers one stdio MCP
19+
server that cannot start (for example `command: /nonexistent`) plus one
20+
that can, and at least one project skill under `.qwen/skills`.
21+
3. `node dist/cli.js serve --workspace <primary> --workspace <secondary>`
22+
on loopback; open the served Web Shell.
23+
24+
## Scenarios
25+
26+
### Header counts and chips
27+
28+
Expand the primary workspace with one session running a prompt.
29+
30+
Expected:
31+
32+
- The folder header shows `●1` (success tone) and the total session count; a
33+
session waiting for permission adds a warning-tone count.
34+
- Under the header: the full path, then chips `MCP a/b · Skills n ·
35+
Extensions n · Channels n · Context n`. The MCP chip is warning-toned and
36+
its tooltip names the failed server count.
37+
- Before the ACP child has initialized (fresh daemon, no session yet) the
38+
MCP / Skills chips show `` with tooltip "not initialized yet", never `0`.
39+
The Context chip is answered by the daemon from disk and settles at its
40+
file count (`0` for a workspace without a QWEN.md) as soon as the first
41+
round lands.
42+
43+
### Request gating
44+
45+
With the Network panel filtered to `/workspaces/`:
46+
47+
- A collapsed workspace issues no facet requests.
48+
- An expanded one issues one request per facet on expand, on window focus and
49+
every 30 s while the tab is visible; nothing while the tab is hidden.
50+
51+
### Workspace menu
52+
53+
Hover a trusted secondary workspace and open ``.
54+
55+
Expected:
56+
57+
- Rename… (only when the daemon advertises `dynamic_workspace_registration`),
58+
Copy path, New task, New worktree task, Reload runtime, Remove workspace.
59+
- Rename opens a dialog prefilled with the current display name; saving
60+
updates the row label without a reload; clearing the name restores the
61+
folder name.
62+
- Copy path puts the absolute path on the clipboard.
63+
- Reload runtime issues `POST /workspaces/<cwd>/reload` and refreshes the
64+
chips.
65+
- New worktree task opens a new draft with the composer's git mode set to
66+
worktree.
67+
68+
On the primary workspace the menu additionally shows a Manage group (MCP
69+
servers, Skills, Extensions, Channels, Settings) with the chip counts; each
70+
opens the corresponding page. Secondary workspaces do not get the group.
71+
72+
### Embedding switch
73+
74+
Mount `<WebShell sidebar={{ workspaceOverview: false }} />`.
75+
76+
Expected: folder headers, path line, chips and Projects count are gone; the
77+
`` menu still offers the non-overview actions.
78+
79+
## Automated coverage
80+
81+
- `packages/web-shell/client/components/sidebar/{workspaceOverviewModel,useWorkspaceOverview,WorkspaceOverview,WorkspaceMenu,WorkspaceRenameDialog}.test.*`
82+
- `packages/web-shell/client/components/sidebar/WebShellSidebar.workspace-removal.test.tsx`
83+
(menu gating, rename, reload, management targets)
84+
- `packages/web-shell/client/e2e/web-shell.workspace-overview.spec.ts`
85+
(chips, counts, request gating, menu contents against the mock daemon)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Web Shell workspace overview
2+
3+
Issue: https://github.com/QwenLM/qwen-code/issues/10399
4+
5+
## Goal
6+
7+
Make a workspace a first-class object in the Web Shell sidebar: show what it
8+
contains and whether it is healthy without opening a session, and put every
9+
workspace-level action behind its own menu. This is layer A of the plan in the
10+
issue — a frontend-only change on top of daemon routes that already exist.
11+
12+
## Behavior
13+
14+
### Folder header
15+
16+
- The header keeps its name, badges and git chip. It gains session counts at
17+
its right edge: sessions waiting on the user (warning tone), sessions with a
18+
prompt in flight (success tone), and the total. A total from a truncated
19+
catalog page shows as `N+`. Collapsing a row disables its catalog query, so
20+
the row keeps the last counts it computed: they stay visible while collapsed
21+
and refresh on the next expand. While the query is active and a page is
22+
missing (a session-source switch), no counts are shown rather than stale
23+
ones above an empty list.
24+
- The name carries the full path as a tooltip. While the section is expanded
25+
the path is printed under the header.
26+
- The Projects label shows the number of registered workspaces once there is
27+
more than one.
28+
29+
### Facet chips
30+
31+
While a trusted workspace is expanded, a chip row summarizes MCP servers
32+
(`connected/enabled`), skills (enabled), extensions (active, or
33+
`active/total` when they differ), channels (`connected/configured`) and
34+
context files (count). Hooks are available but off by default.
35+
36+
- MCP, skills and hooks are discovered by the workspace's ACP child. Until it
37+
reports `initialized`, the chip shows `` and the tooltip says the runtime
38+
is not initialized yet. A placeholder is never rendered as `0`.
39+
- Context files are read from disk by the daemon itself, so its answer is
40+
always definitive: a workspace without a QWEN.md shows `0`.
41+
- The MCP chip takes the warning tone when a server errored or discovery
42+
finished with an enabled server still not connected; the channels chip when
43+
an instance is in the error state.
44+
- Below the sidebar's tight width the chips drop their text labels.
45+
- Extensions, channels and context files are daemon-side facets. When they
46+
are unknown the daemon lacks the route or the fetch failed, so their tooltip
47+
says "unavailable on this daemon" rather than "not initialized yet". The
48+
chip row itself appears only once the first fetch round has landed, so a
49+
round still in flight never reads as a missing route.
50+
- Chips are read-only. Opening a management page is a menu action, so the
51+
chips never take the button role and their accessible names cannot collide
52+
with the navigation buttons that share the same words.
53+
54+
### Workspace menu
55+
56+
The hover `` on a workspace row replaces the single-item removal menu:
57+
58+
- Rename… (dynamic registration daemons, on registration-backed rows only —
59+
the daemon's bound workspace has no registration to persist a name in;
60+
opens a dialog; an empty name falls back to the folder name and control
61+
characters are refused before the request is sent), Copy path, New task,
62+
New worktree task (only when
63+
the git poll reports a branch — a worktree needs a repository, and the
64+
composer would otherwise have no chip to show or undo the armed intent).
65+
- Manage: MCP servers, Skills, Extensions, Channels, Settings, with the chip
66+
counts next to the first four (part of the item's accessible name). The
67+
menu keeps the row's last snapshot while the row is collapsed, so the
68+
counts do not vanish on collapse; the action area stays visible while its
69+
menu is open so Escape returns focus to the trigger.
70+
- Reload runtime (`POST /workspaces/:w/reload`), then Remove workspace.
71+
72+
Each entry appears only when the workspace's state allows it: untrusted rows
73+
that cannot be removed still show nothing, locked-workspace renderers still
74+
suppress the whole action area.
75+
76+
The Manage group is offered on the daemon's primary workspace only. The
77+
management pages read the connection's bound workspace, so a secondary row
78+
cannot open its own view yet; that is layer B1 of the issue.
79+
80+
## Data flow
81+
82+
`useWorkspaceOverview(client, cwd, { enabled, items })` fans out over
83+
`client.workspaceByCwd(cwd)` to `/mcp`, `/skills`, `/extensions`,
84+
`/channels`, `/memory` and `/hooks`, one request per requested facet. Each
85+
call fails independently — an older daemon without a route, a transient
86+
error, or a malformed body — leaves that facet `undefined` and keeps the
87+
others. A facet keeps its last known value across up to three consecutive
88+
unanswered rounds, then reads as unavailable, so a route that stays gone
89+
after a rollback cannot freeze a stale count on the chip. Rounds that time
90+
out after the next tick has already replaced them still count: the SDK's
91+
request deadline equals the poll cadence, so during a daemon hang every
92+
round is superseded before it lands, and only their observed misses can
93+
expire the facet. The budget is scoped to one bookkeeping session: a reset
94+
boundary (a cwd change, the section collapsing) advances an epoch, and a
95+
round launched before the boundary can neither book misses into the fresh
96+
session nor refill it with a stale success.
97+
98+
Fetching is gated on the section being expanded, the workspace trusted and the
99+
default header rendered (a locked sidebar's custom header has no chip or menu
100+
to feed), and polls every 30 s only while the document is visible, plus a
101+
refetch on window focus and on the sidebar's reload token. Collapsed rows cost
102+
nothing, and a synthetic fallback workspace without a real cwd is never asked.
103+
104+
Measured against the mock daemon (`npm run dev`, React StrictMode, 5 trusted
105+
workspaces all expanded, tab visible): after the initial round the sidebar
106+
issued 25 facet requests per 30 s tick — 50 over 60 s — next to the 33
107+
session-catalog and git-status requests the same rows already made in that
108+
window. Without StrictMode the initial round is 25 requests, not 50. That is
109+
the cost the layer-C overview endpoint collapses to 5 per tick.
110+
111+
Session counts come from the catalog page the row already lists; the primary
112+
workspace, whose sessions the sidebar lists itself, gets its counts passed in.
113+
114+
## Embedding
115+
116+
- `sidebar.workspaceOverview: false` keeps the plain folder headers;
117+
`{ items: [...] }` selects the chips.
118+
- `onOpenWorkspaceManagement(target, workspaceCwd)` and
119+
`onNewWorktreeSession(workspaceCwd)` are new sidebar callbacks; the app
120+
wires them to `openPanel` and to `createNewSession` with a worktree git
121+
intent. The intent is set in the same synchronous step that clears the
122+
previous one, so it belongs to that draft from the start: a prompt
123+
submitted while the clear is still in flight gets the worktree, and any
124+
later session start or draft workspace switch resets it like any other
125+
intent. An armed intent survives a transient git-status gap — only a
126+
session, an untrusted workspace, a no-branch answer for the draft's own
127+
workspace (the status is keyed by `workspaceCwd`, so the answer of a
128+
workspace being left never clears an intent armed for the new one) or a
129+
draft workspace switch clears it; re-selecting the draft's own workspace
130+
from the composer picker is a no-op, and an intent set while a session
131+
already exists is cleared immediately.
132+
133+
## Follow-ups (layers B and C in the issue)
134+
135+
- A Trust… menu entry. `POST /workspaces/:w/trust/request` only records a
136+
request that needs operator action and a daemon restart
137+
(`accepted: false, requiresOperatorAction: true`), so an entry today would
138+
promise a change it cannot make; it needs an "operator action required"
139+
feedback surface first.
140+
- Bind the management pages to a chosen workspace so every row can open its
141+
own MCP / Skills / Extensions view.
142+
- A Workspaces overview page with a table across workspaces.
143+
- `GET /workspaces/:w/overview` on the daemon to collapse the fan-out into one
144+
request, advertised as `workspace_overview`, once the workspace-runtime
145+
stack has landed.

integration-tests/vitest.config.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
1111
const __dirname = dirname(fileURLToPath(import.meta.url));
1212
const timeoutMinutes = Number(process.env['TB_TIMEOUT_MINUTES'] || '5');
1313
const testTimeoutMs = timeoutMinutes * 60 * 1000;
14+
const isSelfHostedRunner = process.env['RUNNER_ENVIRONMENT'] === 'self-hosted';
1415

1516
export default defineConfig({
1617
test: {
@@ -30,8 +31,11 @@ export default defineConfig({
3031
pool: 'forks',
3132
poolOptions: {
3233
forks: {
33-
minForks: 2,
34-
maxForks: 4,
34+
// Each ECS host runs several Actions runners. Keep every E2E shard to
35+
// one child process there so concurrent jobs cannot multiply the host
36+
// load and starve latency-sensitive integration paths.
37+
minForks: isSelfHostedRunner ? 1 : 2,
38+
maxForks: isSelfHostedRunner ? 1 : 4,
3539
},
3640
},
3741
// The worker->main `onTaskUpdate` RPC runs on a 60s budget; under
@@ -44,8 +48,7 @@ export default defineConfig({
4448
// the run; only unhandled errors stop being fatal — github-hosted Linux
4549
// (the nightly isolated legs) and local Linux runs keep the signal.
4650
dangerouslyIgnoreUnhandledErrors:
47-
process.platform !== 'linux' ||
48-
process.env['RUNNER_ENVIRONMENT'] === 'self-hosted',
51+
process.platform !== 'linux' || isSelfHostedRunner,
4952
},
5053
resolve: {
5154
alias: {

packages/acp-bridge/vitest.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export default defineConfig({
2929
},
3030
},
3131
test: {
32+
// Shared ECS hosts can pause an otherwise healthy test past Vitest's 5s
33+
// default when several CI runners on the same machine are busy.
34+
testTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
35+
? 60_000
36+
: undefined,
37+
hookTimeout: process.env['RUNNER_NAME']?.startsWith('ecs-qwen-')
38+
? 60_000
39+
: undefined,
3240
reporters: ['default'],
3341
silent: true,
3442
coverage: {

packages/cli/src/acp-integration/acpAgent.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2372,7 +2372,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
23722372
['non-positive', () => 0],
23732373
['non-integer', () => Date.now() + 0.5],
23742374
['non-safe', () => Number.MAX_SAFE_INTEGER + 1],
2375-
['beyond the timer range', () => Date.now() + 2_147_483_648],
2375+
['beyond the timer range', () => Number.MAX_SAFE_INTEGER],
23762376
])(
23772377
'rejects a %s trusted session initialization deadline before creating state',
23782378
async (_label, deadline) => {

0 commit comments

Comments
 (0)