Route live session ownership through a registry-backed owner index so multi-workspace sessions can resolve active sessions without scanning every bridge first. Expand trusted workspace load/resume and live read routing while keeping non-session surfaces primary-only.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(scripts): handle missing NPM dist-tags gracefully in release versioning (#6476)
getAndVerifyTags now returns null instead of throwing when no baseline
version exists on NPM. getPreviewVersion and getStableVersion fall back
to the package.json base version, matching the pattern already used by
getNightlyVersion. This prevents the release workflow from failing when
no nightly or preview dist-tag has been published yet.
* fix(scripts): harden release versioning against transient NPM errors and missing dist-tags (#6476)
- Distinguish 404 from transient errors in getVersionFromNPM so NPM
outages halt the release instead of silently falling back
- Consult getAllVersionsFromNPM when dist-tag is missing to derive
baseline from published versions rather than returning empty
- Add console.error logging when getAndVerifyTags returns null
- Validate package.json fallback version in getStableVersion and
getPreviewVersion
- Add tests for promote-nightly/patch throw paths, true greenfield
scenario, versions-list derivation, and transient error propagation
* fix(scripts): propagate transient NPM errors in getAllVersionsFromNPM (#6476)
getAllVersionsFromNPM silently swallowed all errors including transient
network failures (ETIMEDOUT, ECONNRESET), which became load-bearing now
that the missing-dist-tag fallback depends on it. Match the same 404-only
pattern already used by getVersionFromNPM. Also fix a DRY violation in
getPreviewVersion, correct misleading test comments, and add test
coverage for the versions-list error path and latest filter branch.
* fix(scripts): tighten 404 detection and tolerate transient versions-list failures (#6476)
- Remove redundant '404' substring check; E404 is the canonical npm error code
and bare '404' could false-match unrelated errors (port 4043, E4040)
- Catch transient versions-list errors in detectRollbackAndGetBaseline when
distTagVersion is already resolved, avoiding hard-blocking a release when
rollback detection is merely a safety net
- Update and add tests for the new fallback behavior and deprecated-versions path
* fix(scripts): guard release version fallback edge cases
* test(scripts): cover greenfield versions list E404
---------
Co-authored-by: Qwen Code Autofix <qwen-code-bot@alibabacloud.com>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code@autofix.bot>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
* Initial plan
* fix(core): detect non-SSE HTTP 200 responses in OpenAI streaming pipeline
When a gateway/proxy intercepts a streaming request and returns HTTP 200
with a non-SSE content-type (e.g. text/html), the OpenAI SDK silently
produces zero chunks, resulting in a confusing "Model stream ended without
a finish reason" error and an empty interaction log (response: null,
error: null).
Add NonSSEResponseError that is thrown early when the response content-type
is incompatible with SSE (not text/event-stream, application/json, etc.).
The error carries diagnostic metadata: HTTP status, content-type, bounded
body prefix, and request-id header — allowing users and maintainers to
distinguish "empty model stream" from "gateway returned non-SSE page".
Uses the OpenAI SDK's withResponse() API to access the raw HTTP response
headers before consuming the stream iterator. Falls back gracefully when
withResponse() is unavailable (e.g. in mocked environments).
ClosesQwenLM/qwen-code#6465
* fix(core): replace non-null assertion with nullish coalescing in content-type check
* fix(core): address non-sse review feedback
* fix(core): expose non-sse request id alias
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
* feat(channels): add dmPolicy config to disable private/DM messages
Add DmGate class mirroring GroupGate to gate DM/private messages in
channel adapters. Operators can now set dmPolicy: 'disabled' in their
channel config to silently drop all DM messages while keeping group
messages active.
Closes#6392
* fix(channels): address review feedback for dmPolicy
- Add dmPolicy: 'open' to all test config factories (8 files) to
maintain type correctness with required ChannelConfig field
- Add integration tests in ChannelBase.test.ts:
- preflightInbound: DM dropped + group passes when dmPolicy=disabled
- isStoredLoopTargetAuthorized: DM loop job disabled + group passes
- Add dmPolicy assertions in config-utils.test.ts (default + explicit)
- Keep dmPolicy as required field (not optional) for strict parity
with groupPolicy
* feat(cli): auto-retry next port when serve port is in use
When `qwen serve` or `npm run dev:daemon` encounters EADDRINUSE on the
default port (4170), automatically try the next available port (up to 10
attempts) instead of failing immediately. This allows running multiple
daemon instances side-by-side without manual port management.
- run-qwen-serve.ts: replace single listen() with recursive tryListen()
that retries on EADDRINUSE; --port 0 (ephemeral) skips retry
- daemon-dev.js: pre-scan for an available port via net probe before
spawning the daemon child, ensuring health-poll and web-shell target
the correct URL
- Tests: retry-then-succeed, non-EADDRINUSE immediate-fail, and existing
all-ports-exhausted test all pass (138 tests)
* test(cli): fix EADDRINUSE mock to create fresh server per listen attempt
The existing test reused a single fakeServer across all retry attempts,
accumulating 10+ once('listening') listeners and triggering a
MaxListenersExceededWarning. Create a new server per call to match
production behavior where each tryListen creates a fresh server.
* fix: address review feedback on port retry
- daemon-dev.js: strip IPv6 brackets before probe (ENOTFOUND fix),
add port range/NaN validation
- run-qwen-serve.ts: remove duplicate runtime error listener
(onListening already installs one via removeAllListeners + on)
- tests: add exhaustion (all 10 ports), port 0 EADDRINUSE no-retry,
and stderr retry message assertion (140 tests pass)
* fix: update stale comment referencing removed server.once('error', reject)
* fix: address R2 review — port cap, TLS reuse, exhaustion summary
- daemon-dev.js: cap probe at port 65535, skip probe when user
specifies --port, add --compacted-replay-max-bytes to whitelist
- run-qwen-serve.ts: move https.createServer before tryListen (avoid
recreating TLS context per retry), cap retry at 65535, log summary
"all ports X–Y are in use" on exhaustion
- tests: verify exhaustion summary stderr message
* fix: clear stale listening listeners on httpsServer before retry
* fix(core): omit deprecated temperature param for Claude 4.8+
Claude Opus 4.8 deprecated the `temperature` sampling parameter —
the server returns 400 with "temperature is deprecated for this model"
when it is sent.
Add `modelRejectsTemperature()` version gate (mirroring the existing
`modelRejectsManualThinking()` pattern) and conditionally omit
`temperature` from the request body for models with major > 4 or
4.minor >= 8. Older models and unknown/unversioned ids retain the
previous behavior (default temperature of 1).
Fixes#6519
* fix(core): address review — ternary form, debug warn, 5.x + unknown tests
- Switch from boolean-spread to ternary form for idiom consistency
with the thinking/output_config spreads at L697-698.
- Add a once-per-generator debugLogger.warn when a user-configured
temperature is silently dropped on 4.8+ (latched like effortClampWarned).
- Add test coverage for claude-sonnet-5 (5.x family omits temperature)
and unknown/unversioned model id (keeps temperature) to pin the
major > 4 branch and prevent future narrowing.
---------
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
POST /session/:id/approval-mode was gated with `mutate({ strict: true })`,
requiring a bearer token even though similar session-scoped routes like
POST /session/:id/model and POST /session/:id/language use `mutate()`
(non-strict). This prevented the Web Shell from setting YOLO approval
mode on daemons without a configured token, showing a confusing toast
error on every new YOLO session.
Relax the approval-mode route to `mutate()` to match the model route,
consistent with the session create/load/resume lifecycle paths that are
also non-strict.
Extend StopInput and SubagentStopInput with background_tasks and crons fields,
and wire existing BackgroundTaskRegistry and CronScheduler snapshots into
fireStopEvent/fireSubagentStopEvent. This enables hook scripts to observe
async work state at agent stop time for status reports, cleanup logic, and
monitoring.
Fields are always present (empty arrays when no active tasks/crons). Data
collection is non-blocking via try/catch to not delay stop hook response.
Closes#6529
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): keep status line on session model
Resolve status line model display drift after fast-model background subagents.
Refs #6512
* test(cli): cover preset status model fallback
* fix(web-shell): i18n for ~43 hardcoded English strings across 15 files
Multi-round audit replaced hardcoded UI strings with t() calls and
added en/zh-CN i18n keys for:
- Session timeline labels, aria-labels, and kind labels (MessageList)
- SubAgent tab labels, tools count, pending/running status
- Auth protocol option labels, placeholders, API key masking
- Voice dictation UI states (VoiceButton)
- Copy tooltip, Runtime badge, N/A, Server/Agent fallbacks
- Mermaid code block labels, image alt text, model switch summary
* fix(web-shell): i18n for missed models placeholder in AuthMessage
* fix(web-shell): invalidate timeline cache on locale switch
Store the t function reference in sessionTimelineCache alongside the
message signature. When the locale changes, t gets a new reference,
triggering cache invalidation and re-generating entries with the new
language labels.
Deferred IDE startup could report a timeout while the underlying connection
promise later completed, leaving internal IDE state inconsistent with the
visible startup failure state.
- Disconnect the IDE client when deferred startup connection times out
- Repeat cleanup if the original connection later succeeds after timeout
- Keep late rejection handling to avoid unhandled promise rejections
- Cover timeout cleanup, late success, and quick rejection cases
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* fix(cli): charge a streaming table's wrapped height so it doesn't jump
The rendered-height estimator charged a table `2 * dataRows + 5`, assuming
one line per data row. When cells wrap (a wide table — many columns, long
content — on a bounded content width), each row renders taller, so the live
frame briefly exceeds the viewport and ink repaints from the top (a jump to
the top; #6170's clip/commit recover it, so it does not lock, but the jump
is visible).
Charge each row (header + data) by its wrapped height instead: approximate
the column width as an equal share of the content area (TableRenderer
shrinks columns proportionally to fit; an equal share never gives a wide
cell more room than TableRenderer would, so it is a safe upper bound) and
sum the tallest wrapped cell per row plus the inter-row separators and
chrome. For a table that fits, every row is one line and the formula
reduces exactly to the previous `2 * dataRows + 5`.
Only the height estimate changes (shared by the render-side clip and the
incremental scrollback commit); no rendering behaviour changes.
28 estimator tests pass; MarkdownDisplay clip tests unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): charge a wide-terminal table's vertical fallback height
The pending-height estimator mirrored only TableRenderer's WIDTH-based
vertical-fallback trigger, not the maxRowLines one. On a wide terminal a
multi-column table whose cells wrap past MAX_ROW_LINES is laid out
vertically (label: value — much taller), but the estimator charged the
shorter horizontal height, so the live frame overflowed and Ink fell into
its from-top full-redraw path (the scroll-to-top lock).
Model both triggers: compute the tallest wrapped cell (maxRowLines) in the
same pass as the horizontal height, and when it exceeds MAX_ROW_LINES
charge the vertical height instead. The vertical estimate now also wraps
each label:value at contentWidth so a long value that wraps is not
under-counted. maxRowLines uses an equal column share (never wider than
TableRenderer's column), so it is an upper bound and a real vertical
table is never missed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): cap the live pending region at the viewport height (non-VP)
The estimator's source-line slice is the primary bound on the live
(non-<Static>) pending frame, but it is disabled whenever
availableTerminalHeight is undefined — which is exactly what happens when
constrainHeight is off (ctrl-s "show more lines"). A tall pending item, e.g.
a long vertical-fallback table, then renders past the viewport; Ink cannot
update incrementally, clears the terminal and redraws from the top on every
repaint — the scroll-to-top lock.
Wrap the non-VP pending region in an Ink maxHeight={availableTerminalHeight}
overflow="hidden" box as a hard backstop. availableTerminalHeight already
excludes the footer/controls, so the live frame can never exceed the
viewport and Ink never trips clearTerminal. While constrained the estimator
keeps content well under this, so the clamp is inert and only engages on
residual overflow. ShowMoreLines stays outside the clamp (it renders only
while constrained, so the clamp is inert then, and must not be clipped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): anchor the estimator's vertical trigger to the first row
Mirror TableRenderer's change to decide the horizontal-vs-vertical format
from the header + first data row only (not every row), so the estimator and
the renderer still agree on which format a streaming table uses. Row heights
above continue to sum every row; only the maxRowLines trigger is anchored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): commit a completed table that alone fills the pending budget
The rendered-height-aware incremental commit stalled when a single block
(a long-text table modelled tall / vertical) charged more than the commit
budget on its own. fitPendingSlice returns kept = the block's trailing
blank line, so the safe split boundary sits exactly at keptLines, but the
boundary search started at keptLines - 1 and missed it. The table has no
internal blank line and the blank before it was already committed, so the
search found nothing and broke the loop. Every later block then appended
past keptLines, so the search window never again contained a blank line —
nothing committed until the stream finalized and dumped all remaining
tables at once (the "stream a few tables, pause, then dump the rest" bug).
Start the boundary search at keptLines so a completed over-tall block's
trailing blank is found and the block commits to <Static>. Committing an
over-tall completed block is fine — only the live pending frame must stay
within the viewport.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): don't flash the header while its separator streams in
The forming-table hold-back released the header the moment its separator
line ended with `|` at a column count different from the header — meant to
let a genuinely mismatched separator render as plain text. But a separator
is typed one group at a time and momentarily ends with `|` at every
intermediate count (`| --- | --- |` on the way to seven columns), so the
header flashed as raw `| … |` text on every closed-group frame while the
separator streamed in — a visible strobe for wide (7-column) tables.
A streaming separator only ever gains columns, so treat a mismatch as final
only when it can no longer become valid: it overshot the header's column
count, or a further line has already committed it (it is not the trailing
line). Also hold the header while the separator is still a bare-pipe prefix
(`|`, `| `) before its first dash. The only remaining flash is the
unavoidable one-cell header window (`| Foo |`), indistinguishable from a
single-pipe line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: add a terminal-capture ratchet for the streaming-table scroll-to-top lock
Drives ten wide 7-column tables with ~200-char wrapping cells through the
real TUI via a chunked fake OpenAI server and counts the full-screen clears
(`\x1b[2J\x1b[3J\x1b[H`) the app emits while they stream. Each such clear
resets the terminal scroll position, so it is exactly the "jump to top" a
user hits when scrolling up mid-stream. With the pending-height estimator
fix the count is 0; without it the under-charged frame overflows and the
count is ~300. Ratchet fails if it exceeds 20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): tighten the streaming-table vertical height estimate (review)
Two accuracy fixes to fitPendingSlice from PR review, both reducing residual
under-charge of the vertical fallback:
- Charge each vertical data cell as its rendered `label: value` line (parsing
the header labels once), not the value alone — TableRenderer's
renderVerticalFormat prefixes the header label, so a long label pushed the
wrapped line count higher than the estimator accounted for.
- Only model the vertical layout once a data row exists (`dataRows > 0`). With
just a header + separator, TableRenderer keeps the horizontal header box, so
the estimator no longer charges the shorter 2-row vertical stub for that
transient state on a narrow terminal.
Adds a test where the `label: value` line itself wraps (covering the wrapped,
label-inclusive formula) and a zero-data-row narrow-terminal test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): correct the clamp margin and share MAX_ROW_LINES (review round 2)
- Charge tableClampRows + 2 when the clamp engages: TableRenderer wraps the
height-clamped <Text> in <Box marginY={1}>, so a clamped table renders two
margin rows beyond maxHeight that the estimator was dropping.
- Make TABLE_MAX_ROW_LINES the single source of truth (pending-rendered-height)
and import it into TableRenderer as MAX_ROW_LINES, so the renderer and the
estimator can never disagree on the wrap-to-vertical threshold. Direction is
util→renderer so the pure height module stays free of the React/ink graph.
- Correct the perColWidth comment: an equal column share is exact for uniform
columns but can under-count a heterogeneous table (the renderer shrinks a
narrow column below the share); the MainContent maxHeight backstop is the
hard cap for that residual case.
- Add a horizontal-layout test whose cells wrap within MAX_ROW_LINES, covering
the per-row wrapped contentRows sum (not just the old flat 2*dataRows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): estimator cleanups and stronger tests (review round 3)
- Charge the clamped-table margin: covered by a new test asserting a clamped
table costs tableClampRows + 2 (the marginY the earlier fix added).
- Strengthen the "no stall-then-dump" regression: require all three tables to
have committed (>= 3 items, each table marker present), so a partial stall
no longer passes.
- Reuse headerCells instead of re-parsing the header row inside the loop, drop
the redundant .trim() (splitMarkdownTableRow already trims), and start the
data-row loop at i + 2.
- Refresh the stale TABLE_CHROME_ROWS JSDoc (no longer 2*dataRows+5) and
document the estimator's known under-charge gaps (proportional column widths,
word-aware wrapping, the renderer's post-layout width fallback) that the
MainContent maxHeight backstop is the hard cap for.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(web-shell): restore the full composer in split-view panes
The in-window split view rendered each pane through a deliberately minimal composer: typing "/" opened nothing, and the approval-mode, model, and voice controls were all hidden. Wire each pane's composer to its own session so all four work per pane.
The slash menu lists the pane session's own daemon commands and is submitted through that session, where the daemon executes it server-side — so e.g. /clear clears that pane's session, not the outer chat. The approval-mode and model pickers drive the pane session's own setApprovalMode / setModel actions, and the SDK reflects the change back on the connection. The shared model-visibility filter moves to utils/composerModels so the main chat and split panes hide the same models.
* feat(web-shell): auto-approve pending tool call when a pane switches to yolo
Address review feedback on #6510: switching a split pane to yolo (or auto-edit for an edit tool) now auto-approves a tool call already awaiting approval in that pane — mirroring App's handleSetMode so the shortcut behaves the same as in the single-session chat. The pending approval is read through a ref so the async setApprovalMode resolution targets the approval current at that time, not a stale one.
Also cover the two untested branches of handleSelectMode: the isDaemonApprovalMode guard (invalid mode is rejected without hitting the daemon, reported via onError) and the setApprovalMode async-rejection path.
* fix(cua-driver): migrate Windows install/uninstall scripts to qwen-cua-driver
install.ps1 and uninstall.ps1 were essentially un-migrated from upstream
trycua/cua — binary name was `cua-driver.exe` instead of
`qwen-cua-driver.exe`, repo pointed to trycua/cua, install paths used
upstream vendor names.
Changes:
- Binary: cua-driver.exe → qwen-cua-driver.exe
- Repo: trycua/cua → QwenLM/qwen-code
- Install dir: Programs\Cua\cua-driver\ → Programs\Qwen\qwen-cua-driver\
- Home dir: .cua-driver → .qwen-cua-driver
- Autostart task: cua-driver-serve → qwen-cua-driver-serve
- All URLs and user-facing text updated
- BAKED_VERSION: 0.6.8 → 0.7.0
- Added PATH restart hint after install
- Docs URL in post-install-hints.txt and _install-rust.sh fallback
* fix(cua-driver): correct cua-driver-uia.exe process name in install.ps1
The UIA helper binary ships as cua-driver-uia.exe in release tarballs
(not qwen-prefixed), so taskkill and Get-Process must use the actual
binary name.
* docs(cua-driver): rewrite README with full install/verify/MCP guide
Covers macOS, Windows, Linux installation; verification steps; MCP
configuration for Qwen Code, Claude Code, and Codex; relative-coordinate
mode; uninstall instructions; and platform support matrix.
* fix(cli): show file path in compact tool summary for single collapsible tools
buildToolSummary() previously discarded the description field from
collapsible tools (ReadFile, Grep, Glob, ListFiles), showing only
generic counts like 'Read 1 file'. Now shows the actual file path or
search pattern for single tools, while preserving count format for
batches of multiple tools of the same type. Falls back to count format
when description is unavailable.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): sanitize description in buildToolSummary for error args and ANSI
When a tool call errors, useReactToolScheduler sets description to
JSON.stringify(args) which produces '{...}' blobs. Strip ANSI escape
sequences and reject JSON-looking descriptions so the summary falls
back to the count format instead of rendering raw JSON.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): broaden ANSI stripping and replace newlines with spaces in summary
Strip all common ANSI escape sequences (OSC, charset, CSI, single-byte
ESC) instead of just CSI. Replace all C0 control characters including
newlines with spaces so embedded \n in shell descriptions does not
break the single-line compact summary layout.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* test(cli): update ToolGroupMessage expectations for description-based summary
The buildToolSummary change from 'Read 1 file' to 'Read a.ts' broke
4 assertions in ToolGroupMessage.test.tsx. Update all 5 occurrences
to use the new description-based format.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
---------
Signed-off-by: Alex <alex.tech.lab@outlook.com>
Reject Windows-style absolute and traversal paths in record_artifact workspacePath validation by checking portable slash-normalized path semantics. This keeps artifact metadata aligned with the workspace-relative locator contract while preserving valid relative paths.
Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
Always display the current permission/approval mode in the footer,
including when in the default (Ask permissions) mode. Previously,
non-default modes showed indicators but the default mode showed nothing,
creating ambiguity for users switching between modes.
- Add grey ⏸ badge with 'Ask permissions' text for DEFAULT mode
- Update both main Footer and AgentFooter to render DEFAULT indicator
- Use theme.text.secondary for subtle, unobtrusive styling
- Badge is i18n-aware using existing t('Ask permissions') key
- Other mode indicators remain unchanged
Closes#6496
* fix(web-shell): count daemon sessions in Daemon Status usage dashboard
The Web Shell usage dashboard read usage_record.jsonl exclusively, but only
the TUI /clear path ever writes that file — so daemon / Web Shell sessions and
any un-cleared session (whose usage lives only in the per-session transcripts)
were never counted. Real-world "today" totals could undercount ~20x.
Add core loadUsageHistoryWithLive(): the durable persisted history unioned with
a bounded replay of recent transcripts, deduped by sessionId (persisted wins,
as the authoritative final snapshot). rebuildFromSessionJsonl gains an mtime
window and a skip-set (read from each transcript's first line) so the merge is
incremental and cheap. The daemon /usage/dashboard route and loadUsageDashboard
now use it.
The trailing window (35d) keeps the summary + daily charts exact while bounding
load latency (~1.7s vs ~13s for a full-year replay on a heavy history); older
heatmap cells fall back to persisted data. With no persisted base (fresh
machine / pure Web Shell user) it replays the full history, so the heatmap is
never silently truncated. Read-only: serving the dashboard never writes ~/.qwen.
* fix(web-shell): address review — skip transcripts by filename, add coverage
Skip already-persisted transcripts by their filename (`{sessionId}.jsonl`,
guaranteed by chatRecordingService) instead of opening each file to read the
sessionId from its first line — zero I/O per skipped session on a cache miss.
Add tests: the loadUsageDashboard wiring counts a transcript-only (daemon)
session, the rebuilt-empty (all-persisted) common case, and a corrupt
usage_record.jsonl falling back to a full transcript replay.
Prevent the 'Settings' label from wrapping to a new line when the
sidebar is narrow. Instead, the text is clipped via overflow:hidden
and only the gear icon remains visible.
* feat(cli): skill review dialog — inline preview, open-in-editor, turn-off option
The auto-skill review dialog now shows the staged SKILL.md inline
(sanitized, bounded reads, wrap-aware height cap), opens it in the
configured editor without advancing (with watcher-based preview refresh
so non-blocking GUI editors work), and offers a visible last option to
turn the feature off — effective immediately in-session, persisted at
workspace scope, non-destructive to the pending batch. Bulk options
render only while at least two skills remain. Re-enabling auto-skill
from /memory can resurface a batch put aside by turn-off.
* test(integration): render harness + capture scenarios for the skill review dialog
Browser-free harness that renders the production dialog from source via
an ESM loader hook; its before mode renders the globally installed qwen
(no local fixture — the baseline is what actually shipped, or a loud
failure). Terminal-capture scenarios produce the PR's before/after
screenshots.
* fix(cli): address review findings on the skill review dialog
- Sanitize the model-generated name and description in the dialog header,
same as the preview body — an escape sequence in the frontmatter must not
reach the terminal through the header fields.
- Clamp the preview width to the dialog container cap (min(columns-4, 100),
the same clamp DiffDialog uses) instead of the raw terminal width, which
broke the wrapped-row accounting on terminals wider than ~106 columns.
- Catch settings persistence failures in the turn-off option: surface the
error in the dialog and leave the feature untouched instead of letting the
throw escape the keypress handler.
- Extract the auto-open gate into shouldAutoOpenSkillReview and cover it
with a truth table (turn-off, /memory overlap, re-enable, Esc-dismiss).
- Cover the MemoryDialog auto-skill ON->OFF toggle direction.
- Release the capture harness temp dir with try/finally.
* fix(cli): guard the preview watcher against async errors and event bursts
An FSWatcher 'error' event after attach had no listener, so Node raised
it as an uncaught exception and the global handler exited the CLI.
Consume it and drop the watcher; the blocking-editor reload still works.
Also debounce the watch callback (300ms, same as SettingsWatcher): a
single editor save fires several raw events, and each one re-read the
file and re-attached the watcher.
* test(cli): drop white-box watcher tests, keep the end-to-end refresh test
The prototype-spy scaffolding tested implementation details (listener
registration, synthetic event bursts) and leaned on vite-node interop
quirks. The existing on-disk refresh test already exercises the watcher
path, debounce included.
* fix(cli): sanitize action errors, log preview read failures, cover key guards
- Render actionError through sanitizeMultilineForDisplay: error messages
can embed the staged path, whose basename derives from the
model-generated skill name.
- Log the underlying cause when the preview read fails; all failures
render the same 'Preview unavailable' otherwise.
- Cover Ctrl+O/Cmd+O inertness and Esc dismissal with tests.
- Document that getAutoSkillEnabled() also gates on bare/safe mode.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The WeCom intelligent robot channel (added in #6436) has its own guide and _meta.ts entry, but the channels overview was never updated. Add WeCom to the platform list, quick-start guide links, the `type` option and `token` exclusion note, new `botId`/`secret` option rows, the media-support note, and the slash-command channel enumeration.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(web-shell): rename scheduled tasks "查看历史" to "查看对话"
* feat(serve): bind cron_create durable tasks to dedicated sessions via keepalive
The cron_create tool (core layer) writes durable tasks to disk without a
sessionId because it has no access to the session bridge. The keepalive
loop runs in the daemon process where the bridge IS available, so it
retroactively binds unbound tasks to dedicated sessions — the same flow
POST /scheduled-tasks uses for UI-created tasks. Each unbound task gets:
spawnOrAttach(sessionScope:'thread'), named ⏰ prompt, sessionId written
back to disk. This makes chat-created tasks show "查看对话" with a clock
icon in the session list, matching the UI's "新建定时任务".
* feat(serve): watch tasks file for immediate binding of new cron_create tasks
The keepalive interval is 2-5 minutes, so a chat-created task could wait
that long before being bound to a dedicated session — showing no "查看对话"
link until the next tick. Adding a file watcher (same directory-watch +
debounce pattern the scheduler uses) triggers an immediate tick when
cron_create writes to disk, so the task is bound within ~500ms.
* feat(serve): bind cron_create tasks to current session + ⏰ rename via keepalive
Switch from creating a separate dedicated session to binding the task to
the current chat session (so the first message is already in the
transcript). The keepalive then renames that session to ⏰ prompt — the
core layer can't rename sessions (no bridge access), but the daemon
process can. A Set tracks renamed sessions to avoid repeated
updateMetadata calls. Unbound tasks (legacy/CLI) still get new sessions
via the existing bind path.
* fix(core): keep createDurable() tasks unbound by default
Reverts the auto-binding of durable tasks to the current session in
createDurable(). Binding to a specific session means only that session
can fire the task (#shouldFireDurable), but non-daemon paths (TUI, ACP,
headless) have no keepalive to rehydrate the session after exit — making
tool-created durable tasks go dormant.
The daemon keepalive (bindAndNameSessions) already handles binding
unbound tasks to dedicated sessions with ⏰ naming, so daemon-mode
tasks get the same UX without the regression.
* fix(serve): roll back orphan sessions in keepalive binding + add tests
When bindAndNameSessions spawns a dedicated session for an unbound task
but the subsequent updateCronTasks write fails (or the task was deleted
between read and write), the spawned session was left behind with no
owning task — the next tick would see the task still unbound (or spawn
more orphans). Add rollback: closeSession + removeSession on failure,
matching the POST /scheduled-tasks rollback pattern.
Also add positive test coverage for the new binding paths:
- unbound task → spawn + name + write sessionId to disk
- bound task without ⏰ prefix → named exactly once (renamed Set dedup)
- task vanishes before write → spawned session is rolled back
* fix(serve): add timeout to spawnOrAttach in keepalive binding + test hardening
BZ-D: spawnOrAttach in bindAndNameSessions had no timeout boundary — a
hung spawn would keep running=true and stall all subsequent ticks,
stopping heartbeats/revives for every scheduled-task session. Wrap with
withTimeout (configurable via spawnTimeoutMs, default 30s) and attach a
background handler to clean up late-resolved orphans.
Also generalized withTimeout error messages to include the operation
name, and made spawn timeout configurable for tests.
Test improvements (GPT-5 review suggestions):
- Assert spawnOrAttach payload (workspaceCwd + sessionScope: thread)
- Verify SessionService.removeSession called during rollback
- Regression test: createDurable stays unbound after enableDurable
- Hung-spawn test: tick completes despite non-abortable spawn hang
* fix(serve): keepalive hardening + i18n sync (review suggestions)
- i18n: sync English 'View history' → 'View conversation' to match
Chinese '查看对话'
- Prune renamed Set alongside reviveState when tasks are removed
- fs.watch: clarify null filename handling for Linux (treat as match)
- updateCronTasks: skip .map() when task not found (no-op optimization)
- Add tests: disabled unbound exclusion, naming failure resilience
* perf(cli): defer startup prefetch tasks
* fix(cli): await IDE for prompt-interactive startup
* perf(cli): defer interactive telemetry startup
* test(cli): add missing assertions and Zed/ACP path coverage for startup prefetch
Address three test coverage gaps identified during code review:
- Assert mockStartEarlyStartupPrefetches in both kitty protocol tests
(C1: API preconnect call was wired but never verified)
- Add Zed/ACP integration test verifying deferIdeConnection is false
when getExperimentalZedIntegration returns true (C2: Zed path was
entirely untested)
- Assert mockStartBackgroundHousekeeping in startup-prefetch test
(C3: unconditional housekeeping dispatch was never verified)
* docs: move startup prefetch design doc to performance subdirectory
* docs: translate startup prefetch design doc to English
* fix(cli): address startup prefetch review comments
Tighten the startup prefetch follow-up fixes from review while keeping
prompt-interactive telemetry on the fast interactive startup path.
- Preserve Error objects when deferred startup tasks fail
- Remove the unbalanced api_preconnect profiler lifecycle event
- Guard background housekeeping so it only runs for interactive configs
- Document and test prompt-interactive telemetry deferral semantics
* fix(cli): initialize telemetry for prompt-interactive prompts
Ensure sessions launched with an initial interactive prompt have
telemetry ready before the auto-submitted first request runs.
- Exclude prompt-interactive startup from telemetry deferral
- Pass a post-render telemetry option through interactive UI startup
- Skip duplicate post-render telemetry startup for initial prompts
- Update tests to cover the first-prompt telemetry guarantee
Note: Plain interactive TUI startup still defers telemetry post-render.
* fix(cli): preserve startup first-request guarantees
Keep deferred startup work from weakening first-request behavior in
interactive sessions that submit prompts automatically or remotely.
- Store telemetry deferral on Config and reuse that decision at render time
- Keep IDE startup awaited for prompt-interactive and input-file sessions
- Add a timeout for deferred IDE connection failures
- Cover ordinary interactive telemetry deferral and IDE startup edge cases
* fix(cli): make post-render IDE connection opt-in
Default startInteractiveUI to the already-connected IDE path so future
callers do not accidentally connect twice when initializeApp used its
eager default.
- Change the post-render IDE connection default to false
- Update startInteractiveUI tests to assert the safer default
* perf(cli): surface deferred IDE connection status
Make ordinary interactive IDE startup visible while preserving the
post-render prefetch path and first-paint performance tradeoff.
- Emit deferred IDE connection lifecycle events for connecting, success,
and failure states
- Surface IDE startup status in the TUI footer without blocking input
- Log late underlying IDE failures after timeout for better diagnostics
- Document telemetry deferral tradeoffs and add startup lifecycle tests
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* feat(channels): add natural channel memory intents
* fix(channels): add explicit guard and exhaustiveness check for clear_confirm intent
The clear_confirm path was handled as implicit fall-through at the bottom
of handleChannelMemoryIntent. If a new intent kind were added to the
ChannelMemoryIntent union, it would silently execute clearChannelMemory
without user confirmation — a data-loss risk.
Add explicit if (intent.kind === 'clear_confirm') guard and a
const _exhaustive: never assertion so TypeScript flags any unhandled
kinds at compile time.
* fix(channels): close session leak in classifier and fix regex separator
- BridgeChannelMemoryIntentClassifier now wraps prompt() in try/finally
to always call cancelSession(), preventing daemon session leaks on
every classifier invocation. Cleanup errors are caught so they cannot
mask a successful classification result.
- Add missing optional punctuation separator to the 以后记住 regex
pattern for consistency with other Chinese remember patterns.
* fix(channels): enforce pending clear state for channel memory confirmation
The clear_confirm intent executed clearChannelMemory directly without
verifying a prior clear_request was issued for the same chat. Any
authorized user could clear any chat's memory by sending the confirmation
phrase standalone, bypassing the two-step flow.
Add a per-target pending clear map (chatId + threadId, 60s TTL) that
is set during clear_request and verified+consumed during clear_confirm.
Standalone confirmation phrases now get rejected with a prompt to
issue the clear request first.
* fix(channels): include senderId in pendingClears key to prevent cross-user confirmation
User A could initiate clear_request in a group chat and User B could
confirm it, since the pending key only included chatId+threadId.
Add senderId to the key so only the user who initiated the clear
can confirm it.
* fix(channels): harden memory intent review fixes
* fix(channels): cover memory clear sender guard
* fix(channels): block group memory mutations
* fix(channels): avoid ambiguous memory saves
* test(channels): cover memory classifier cleanup
* test(channels): cover memory clear expiry
* fix(channels): restore channel memory slash aliases
* test(channels): cover memory intent edge cases
* fix(web-shell): remove meaningless "current" badge from split-view panes
In the split view every pane is an equal, independently interactive session (its own DaemonSessionProvider, SSE, transcript, and approvals), so tagging one pane as the workspace's "current" session carried no operational meaning. It only leaked a single-view concept into a peer-of-equals layout and reliably prompted "what is this?" questions from users. Panes are already identified by their titles.
Drop the isCurrent badge and its border highlight from ChatPane, and stop passing isCurrent from SplitView. The sidebar and session overview keep their "current" indicators, which are legitimate "you are here" navigation. currentSessionId is retained only to seed the initial pane.
* fix(web-shell): clear the split-view composer on send, not at turn end
A split-view pane kept the just-sent text sitting in its composer until the whole turn finished. handleSubmit committed the draft on the sendPrompt promise resolving, but that promise resolves via waitForAcceptedPromptCompletion (turn end), not at admission. Switch to the onAdmitted hook so the composer clears the moment the daemon accepts the prompt, matching the main view. A prompt rejected before admission still preserves the draft and surfaces the error.
* fix(web-shell): pass commitAccepted directly as onAdmitted; cover admit-then-fail
Review follow-up:
- commitAccepted is already `() => void`, so pass it directly as the onAdmitted option instead of wrapping it in a redundant `() => commitAccepted?.()` closure.
- Add a test for the turn failing after admission: the draft stays cleared (no second commit) and the error is still surfaced to onError.