Two fixes that together unblock visible thinking on third-party Anthropic
proxies (mimo, lgfzer, kiro-account-manager loopback, etc.):
1. src/utils/thinking.ts: when the API provider is firstParty but the base
URL is not Anthropic's own, treat the gateway as thinking-capable rather
than relying on a per-host whitelist. Drops the dead
isMiniMaxAnthropicEndpoint helper. SUPPORTED_CAPABILITIES=none still
opts out for any endpoint that genuinely cannot return reasoning.
2. desktop ThinkingBlock now defaults to expanded so reasoning is visible
the moment thinking deltas start arriving, matching the streaming
experience users expect from reasoning models. Click still toggles.
Verified end-to-end against mimo-v2.5-pro through the desktop UI: thinking
content streams in 40-110 char increments over a few seconds with the
"思考中" cursor visible, then collapses to "已思考" on completion.
Tested:
- src/utils/__tests__/thinking.test.ts (113 utils tests pass)
- desktop ThinkingBlock.test.tsx + chatBlocks.test.tsx (16 tests pass)
- Live mimo SSE probe confirms many small thinking_delta events upstream
- Browser MCP smoke: complex prompts show real-time thinking growth
Confidence: high
Scope-risk: narrow
* feat(agent-tool): rewrite contradictory review verdicts at the harness layer
The code-reviewer and security-reviewer subagents end with a parseable
verdict (`REVIEW: APPROVE` / `SECURITY: PASS`) and their prompts forbid
emitting the positive form alongside `[CRITICAL]` or `[HIGH]` findings.
That rule is enforced only in prose, so when the model slips, the
parent agent reads "APPROVE" and ships.
Add a pure post-processing pass over the subagent's final text in
`finalizeAgentTool`: detect the verdict line, look for severity tags
above it, rewrite to `CHANGES_NEEDED` when they conflict, and prepend
a one-line audit notice so the parent (and a human reading the
transcript) can see the harness intervened. Logs a
`tengu_agent_sentinel_mismatch` analytics event for fleet visibility.
Debugger's `ROOT CAUSE: FOUND` is intentionally out of scope — its
"evidence from a real command" rule isn't mechanically verifiable.
Tested: bun test src/tools/AgentTool/sentinelCheck.test.ts (11 cases)
Confidence: high
Scope-risk: narrow
* feat(agent-tool): show subagent_type in tool-use transcript line
Each `Agent` tool call previously rendered as just its description in
the transcript, which made it invisible whether the main agent picked
a specialist or fell back to the default. Diagnosing "why didn't it
use code-reviewer?" required reading analytics or guessing.
Append `→ <subagent_type>` to the rendered description when the type
is explicit. Omitted (general-purpose default or fork) keeps the old
format — absence of the marker is itself the signal.
Tested: visual smoke only — pure UI render change with no behaviour
Confidence: high
Scope-risk: narrow
* feat(agent-tool): nudge verification subagent after edits accumulate
Verification is the only built-in agent that's *required* by the
session-specific guidance for non-trivial implementations, but the
trigger condition ("3+ file edits / backend / infra change") is
evaluated by the main agent from its own work — a classic LLM
self-audit failure mode. The model can convince itself that 4 edits
"are mostly one core change with small adjustments" and skip
verification entirely.
Add a `verification_gate_reminder` attachment that fires once when
the main thread accumulates `editCount >= threshold` (default 3)
file-mutating tool uses (Edit / Write / NotebookEdit) without
invoking the verification subagent. The reminder renders as a
`<system-reminder>` block on the next turn telling the model to
spawn `subagent_type="verification"` before reporting completion,
along with a permission to skip if the change is genuinely trivial.
A verification subagent invocation resets the counter; a previously
fired reminder suppresses re-firing until the next reset, so the
nudge isn't spammed every turn.
Subagents don't see the reminder (it's main-thread only). The gate
also no-ops when the verification agent isn't loaded in the current
session (e.g. SDK build with built-ins disabled). Disabled by
`CLAUDE_CODE_VERIFICATION_GATE_OFF=1`; threshold tuned via
`CLAUDE_CODE_VERIFICATION_GATE_THRESHOLD=N`.
Tested: bun test src/utils/verificationGate.test.ts (10 cases)
Confidence: high
Scope-risk: moderate
* feat(agent-tool): tighten subagent routing and ship real coordinator mode
The flat 15-agent routing has three known weak spots that this commit
addresses together because they share AgentTool.tsx call-path edits:
1. **General-purpose default is a vacuum cleaner.** Omitting
`subagent_type` falls back to general-purpose, which has full tool
access and cheerfully takes on review/security/perf/migration work
that a specialist would have done better and cheaper. Add a regex
heuristic over the prompt: when it strongly signals a specialist
(e.g. "code review", "security audit", "root cause", "migrate from
X to Y") and that specialist is available, refuse the default and
throw an error pointing the model at the right type. Disabled with
`CLAUDE_CODE_GP_DEFAULT_STRICT=0`. Skipped in fork and coordinator
modes (they have their own routing).
2. **No bound on verifier-style loops.** The verification prompt says
"On FAIL: fix, resume, repeat until PASS" — if the verifier
incorrectly keeps flagging the same change, the loop only stops
when the token budget runs out. Add a per-session counter for each
built-in agent type. Cap defaults are 5 for verification (loop
target) and 8 for everything else. When exceeded, throw with a
message asking the model to consult the user. Caps tunable per
type via `CLAUDE_CODE_AGENT_LIMIT_<TYPE>=N`; off via
`CLAUDE_CODE_AGENT_LIMITER_OFF=1`. Counts main-thread invocations
only.
3. **Coordinator mode was a stub.** The `coordinator/workerAgent.ts`
that `getBuiltInAgents()` requires when `COORDINATOR_MODE` is on
was an auto-generated Proxy returning nothing useful. Replace with
a real registry: a new `WORKER_AGENT` (full-tool-access generic
delegate matching the existing coordinator prompt's
`subagent_type: "worker"` references) plus the 11 specialists
minus general-purpose, claude-code-guide, statusline-setup. Also
harden the coordinator's own prompt with a "you orchestrate, you
do not implement" rule (no direct Edit/Write/NotebookEdit) and a
specialist roster table so the coordinator picks the right
delegate. Improve the AgentTool error message when coordinator
mode receives a request without `subagent_type`.
Constraint: cannot restrict the coordinator's own tool pool at the
tool layer without invasive changes to main-thread tool assembly —
enforcement is prompt-level, consistent with the rest of the
architecture.
Rejected: a tighter heuristic that requires a domain-specific phrase
(e.g. "review THIS PR") was considered for #1 but rejected — too
narrow to catch the routing problems we actually see in fleet logs.
Tested: bun test src/tools/AgentTool/specialistRouter.test.ts
bun test src/tools/AgentTool/invocationLimiter.test.ts
bun test src/coordinator/workerAgent.test.ts
(29 cases total, all pass)
Confidence: medium
Scope-risk: moderate
Directive: if specialistRouter false-positives in production, prefer
tightening the regexes over removing the gate — the failure mode
it catches (general-purpose taking specialist work) is worse than
the failure mode of an extra retry.
* docs(desktop): add local Chrome DevTools MCP testing guide
Document the workflow for driving the desktop UI end-to-end via
Chrome DevTools MCP without going through Electron. The pure-browser
dev path normally fails — CSP blocks non-loopback origins, the
H5-disabled CORS gate blocks cross-origin browsers, and the desktop's
loopback exemption clears the H5 token so even the loopback path
401s. The stable bypass is a Vite proxy that same-origins API
traffic to the renderer.
This commit covers the docs side only:
- `docs/desktop/10-local-mcp-testing.md`: full how-to with Quick
Start, MCP control snippets, can/can't test matrix, the exact
reproduction recipes for each `feat/agent-routing-hardening`
change (sentinel, gp-strict, limiter, verification gate, routing
observability), a Windows-specific `electron:dev` workaround, and
a fault dictionary mapping the symptoms each blocker produces back
to its cause.
- `docs/desktop/index.md`: link the new section in the docs index.
- `.kiro/steering/desktop-mcp-testing.md`: concise rule file with
`inclusion: manual` so it loads via #-mention only when relevant,
not as default-included context.
The Vite proxy itself is intentionally NOT committed here. The docs
explain it as a dev-only change the developer adds locally and
either reverts or commits to a separate `feat/desktop-browser-dev-proxy`
branch — the production renderer relies on Electron IPC for the
server URL and must not see the dev proxy in main.
Tested: docs render via VitePress local preview; steering file
loads via Kiro `#desktop-mcp-testing` mention; instructions
verified against the live setup that produced
artifacts/desktop-bootstrapped.png and artifacts/desktop-agents-page.png.
Confidence: high
Scope-risk: narrow
* feat(desktop): unblock pure-browser dev MCP smoke with proxy + forceH5
The standalone-browser dev workflow (vite + Chrome DevTools MCP)
fails three ways without these escape hatches:
1. CSP `connect-src` only allows loopback origins, so the renderer
can't talk to the API server cross-origin even on a LAN.
2. With H5 access disabled the server returns 403 to all
cross-origin browser requests.
3. With H5 access enabled the desktop's loopback exemption
(`requiresH5AuthForServerUrl` returns false for `localhost:1420`)
calls `setAuthToken(null)`, so `buildSessionWebSocketUrl` omits
`?token=`, and the session WS handshake 401s into a 1006 close
reconnect loop. The UI shows "处理中..." but the message never
reaches the server — `wsManager.connections` stays `{}`, server
stdout is silent, and no `POST /api/sessions/.../message` ever
fires.
Two dev-only changes break that deadlock without affecting
production:
- `desktop/vite.config.ts`: add `server.proxy` for `/health`,
`/api`, `/ws`, `/local-file`, `/preview-fs` to `127.0.0.1:3456`.
Same-origins all backend traffic so CSP and CORS pass. Vite dev
only — Electron production path is unaffected because Electron
resolves the server URL via IPC at runtime.
- `desktop/src/lib/desktopRuntime.ts`: add a `?forceH5=1` query
param check in `initializeBrowserServerUrl`. When present, treat
the URL as needing H5 auth even if it's loopback, so the H5 token
survives bootstrap and gets attached to the session WS URL. In
production the param is never set, so behavior is identical.
Workflow with both in place:
http://localhost:1420/?serverUrl=http%3A%2F%2Flocalhost%3A1420
&forceH5=1&h5Token=<TOKEN>
Verified end-to-end via WS sniffer + server log:
- ws://localhost:1420/ws/<sid>?token=... opens
- recv {"type":"connected","sessionId":"..."}
- server logs "[WS] Client connected" + "Starting CLI for ..."
- Sending a "use Task with subagent_type=Explore" prompt produced
a `tool_use_complete` for the Agent tool with
`input.subagent_type="Explore"` exactly as routed.
Also rewrites `docs/desktop/10-local-mcp-testing.md` to record:
- both patches as required steps (was missing the forceH5 piece)
- exact PowerShell commands to mint a one-shot H5 token
- the WebSocket sniffer script and three-signal diagnosis recipe
for "is the backend actually processing"
- the Item 5 (routing observability) gap: the change in
`src/tools/AgentTool/UI.tsx` only affects CLI/Ink rendering;
the desktop's separate `ToolCallGroup.tsx` doesn't surface
`subagent_type` and would need its own follow-up patch.
Tested: ran `bun run src/server/index.ts` + `bun run dev` in the
desktop/, opened the documented URL via Chrome DevTools MCP,
sent a Task-with-explicit-subagent_type chat message, observed
the full WS frame sequence and server log lines listed above.
Confidence: high
Scope-risk: narrow
Directive: do not generalize the forceH5 escape hatch to a
permanent feature — when it's needed, the dev-mode
loopback-clears-token branch is doing the right thing for
Electron and should not be removed.
* feat(desktop): show subagent_type badge next to Agent tool header
The CLI/Ink change in src/tools/AgentTool/UI.tsx surfaces routing
in the terminal transcript as `<description> → <subagent_type>`,
but the desktop renderer is independent (`ToolCallGroup.tsx` /
`AgentCallCard`) and was dropping that signal entirely. Diagnosing
"why didn't it use code-reviewer?" required reading analytics or
guessing.
Add a small `→ <subagent_type>` chip next to the Agent header in
AgentCallCard. The badge mirrors the CLI behavior:
- Renders only when `input.subagent_type` is a non-empty string;
general-purpose / fork / omitted defaults stay quiet.
- Carries `title="subagent_type: <name>"` for hover detail.
- Uses the same border-pill treatment as other secondary
transcript chips so it doesn't compete with the description.
Verified end-to-end via the local Chrome DevTools MCP path: a
real chat with `subagent_type=Explore` renders `Agent → Explore
查找specialistRouter相关文件` in the live transcript, screenshot
saved to artifacts/desktop-routing-arrow-highlighted.png.
Tested: cd desktop && bun run test -- --run -t "subagent_type"
2 tests pass:
- renders subagent_type as → Explore badge next to Agent header
- omits subagent_type badge when input has no subagent_type
Confidence: high
Scope-risk: narrow
* feat(desktop): MCP tool visibility, per-tool toggle, marketplace install
Surface MCP capabilities so the agent never under-uses a configured server,
and add a one-click install path for new servers.
Tool visibility & control:
- GET /api/mcp/:name/tools returns the live tool catalog (handles disabled /
needs-auth / host-preflight-failed / connected states)
- POST /api/mcp/:name/tools/:tool/toggle hides a single tool from the agent,
persisted to ~/.claude.json disabledMcpTools (user scope, all projects);
fetchToolsForClient filters on it and the cache is invalidated on toggle
- Desktop MCP detail page gains an Overview/Tools tab strip (both edit and
read-only views) listing name, description, annotations, JSON schema, plus
an inline enable/disable switch with a "hidden from agent" marker
Marketplace:
- Curated builtin catalog (14 servers across 8 categories) plus opt-in remote
catalog sources cached under ~/.claude/cc-haha/mcp-marketplace.json
- Endpoints: list / refresh / add-source / remove-source
- Desktop marketplace page with category grouping, install dialog (with
required-env prompts), and source management
Plugins:
- Plugin list rows get inline enable/disable + uninstall, matching the MCP
settings UX, instead of requiring users to open the detail panel
- Extract shared ToggleSwitch component
Tested: bun test src/server/__tests__/mcp.test.ts (tools list + per-tool
toggle + marketplace), cd desktop && bun run lint, mcpSettings vitest, and a
browser smoke pass via the local Chrome DevTools MCP flow (tool toggle
persistence, marketplace render, plugin enable/disable + uninstall dialog).
Not-tested: full bun run verify gate
Scope-risk: moderate
* fix(agent-tool): rephrase gp-strict error to stop poisoning sessions
Live testing surfaced an unintended side effect of the gp-strict
redirect message. The original wording embedded attribute-style
fragments like `subagent_type="code-reviewer"`. When that error
came back to the model as a tool result, the model copied the
shape into a TEXTUAL `<tool_use name="Agent">{...}` block instead
of issuing a real tool call — and every subsequent Agent call in
that session degraded to text. Confirmed by inspecting the
session JSONL via /api/sessions: messages 4..N in the affected
session were `assistant: text(...)` carrying simulated tool_use
XML, not `tool_use` content blocks. Reproduced with two unrelated
models (claude-haiku-4.5 and mimo-v2.5-pro) on the same poisoned
session, and crucially DID NOT reproduce in a fresh session with
either model — so the failure is the message text inducing
text-form mimicry, not the model itself.
Fix:
- Extract the redirect text into `formatSpecialistRedirectMessage`
in specialistRouter.ts so it has its own test surface.
- Rephrase as prose that names the parameter and value without
any `key="value"` assignment-and-quotes form. New shape:
"This task looks like a job for the code-reviewer specialist
rather than the general-purpose default. Re-call Agent with
the subagent_type parameter set to code-reviewer. ..."
- Wire AgentTool.tsx through the helper so the throw site is one
line.
Add a regression assertion to specialistRouter.test.ts that the
formatted message contains NO `key="value"` shapes, no XML-ish
open tags, and no `"subagent_type":` JSON fragments — across all
ten specialist names. This is the property the live failure
violated; lock it.
The invocation-limiter message uses shell-style `KEY=N` syntax
(not `key="value"`), did not exhibit the same behavior in
testing, and is left untouched.
Tested: bun test src/tools/AgentTool/specialistRouter.test.ts
17 pass (13 existing + 4 new), 61 expect() calls
Confidence: high
Scope-risk: narrow
Directive: any future change to redirect/error text returned
to the model should preserve the no-key="value" property.
* feat(orchestration): require copying project tool rules into sub-agent prompts
Live test on the desktop `+` orchestration mode showed mimo-v2.5-pro fan
out three sub-agents in parallel for a multi-task PR review prompt — but
none of the dispatched prompts mentioned the project's mandated codegraph
MCP tool. The sub-agents fell back to plain Bash + git diff + grep, so the
project's preferred code-exploration tooling went unused.
Root cause: the orchestrator's `Propagate project conventions` rule was a
soft "for example, restate codegraph rules" bullet. The model read it as
optional. Built-in specialist system prompts (code-reviewer, security-
reviewer, debugger, etc.) explicitly enumerate Bash/grep/git as inspection
tools and never mention codegraph; CLAUDE.md is loaded but the system
prompt outweighs it. The orchestrator was the only place the gap could be
closed, and it skipped it.
This commit upgrades the rule from optional to REQUIRED in the
orchestration system prompt, names the concrete check ("scan CLAUDE.md,
AGENTS.md, .claude/rules"), and tells the orchestrator to copy matching
rules verbatim. Read-only research agents (Explore, Plan) are called out
explicitly because they run with omitClaudeMd:true and absolutely depend
on the orchestrator to forward project tool conventions.
A new ORCHESTRATION_PROPAGATE_RULES_MARKER export plus a regression test
in conversations.test.ts lock the imperative phrasing so it can't be
quietly downgraded back to a soft suggestion.
Wiring is unchanged: built-in code-reviewer/security-reviewer/commit-pr
already inherit CLAUDE.md and have MCP tools available. This is purely a
prompt-strength change, only active when the user enables the desktop
`+` orchestration toggle.
Tested: bun test src/server/__tests__/conversations.test.ts (new test
plus the two existing orchestration-prompt tests pass).
Confidence: high
Scope-risk: narrow
* fix(verification-gate): preserve reminder flag across verification reset
Caught by an orchestrator-mode code-reviewer dispatch on this branch.
When a verification subagent invocation reset the edit counter, the early
return in getVerificationGateState() hardcoded reminderAlreadyFired:false,
discarding any reminderFired flag we'd accumulated walking back from the
tail. Concretely: if a verification_gate_reminder fired AFTER the verify
(i.e. newer than verify in the transcript), the reverse walk would set
reminderFired=true on its way to the verify boundary, then throw it away
on the early return. The gate would then inject a fresh reminder every
turn while the post-verify edit count stayed above the threshold,
ballooning the prompt.
Fix: carry the accumulated reminderFired through the early return. The
reminder we saw is necessarily newer than this verify (we're walking
backwards from the tail and only got here after seeing it), so it's the
valid signal that the model was already nudged for the current edit batch.
Tests: extend verificationGate.test.ts with a "preserves a reminder fired
AFTER a verification reset (no spam loop)" case that fails on the old
hardcoded false and passes with the fix. The pre-existing "ignores a
reminder that fired BEFORE a verification reset" test still passes because
in that ordering the reminder is older than the verify, so reminderFired
stays false on the way back.
Tested: bun test src/utils/verificationGate.test.ts (11/11 pass).
Confidence: high
Scope-risk: narrow
* chat(i18n): rename `协调模式` to `编排模式` (zh / zh-TW)
Brings the simplified- and traditional-Chinese label for the desktop `+`
menu's orchestration toggle in line with the English (Orchestration mode),
Japanese (オーケストレーションモード), and Korean (오케스트레이션 모드)
labels — all five locales now use the same `orchestration` word family.
`协调` was accurate but vague; `编排` matches the term Chinese
developers already see in LangChain, CrewAI, AutoGen, and Anthropic docs,
and tells a user reading just the label what the toggle actually does
(plan → fan out → synthesize) rather than a generic `coordinate`.
UI-text only. No behavior, store, WS message, or i18n-key changes.
* feat(desktop): welcome-screen task cards with one-click orchestration
Adds four quick-start task cards to the new-session welcome screens — both
EmptySession (the no-session-yet entry) and ActiveSession's empty state
(when a fresh session tab has no messages yet). Each card pre-fills the
composer with a starter prompt for a common workflow:
- Pre-merge code review (auto-enables Orchestration mode)
- Investigate a failing test (auto-enables Orchestration mode)
- Write unit tests (no orchestration)
- Explain unfamiliar code (no orchestration)
Cards flagged `orchestrate: true` automatically turn on Orchestration
mode for the session that gets created (or for the live session in
ActiveSession's empty state), so a new user sees fan-out behavior on
first contact instead of having to discover the `+` menu first. This is
the intended discovery surface for the orchestration directive shipped on
this branch.
Plumbing:
- WelcomeTaskCards.tsx is a shared component used by both welcome
surfaces, so card list, copy, and styles stay in one place.
- EmptySession owns its own composer textarea, so card click flips
component state directly (setInput + draftOrchestrate flag) and the
flag is applied via setSessionCoordinatorMode after createSession()
resolves and before connectToSession() — chatStore replays the
persisted toggle on connect, so the CLI launches with the right
--append-system-prompt.
- ActiveSession's session is already live, so a card click dispatches a
`cc-haha:composer-prefill` window event that ChatInput listens for
and applies to its composer state when the sessionId matches the
active tab. Orchestration cards call setSessionCoordinatorMode
directly, which sends `set_coordinator_mode` over WS and triggers a
CLI restart with the orchestration directive — verified live: the
server logs `[WS] Restarted CLI for <id> with runtime override`
immediately after the click.
- Cards never disable an existing Orchestration toggle. Non-orchestration
cards simply leave the toggle alone, so users who pre-enabled
Orchestration via the `+` menu don't have it silently turned off.
- Hidden on phone-sized H5 viewports (composer is already dense there).
i18n: ten new keys under `empty.tasks.*` with localizations for en,
zh, zh-TW, jp, kr (the same five locales the orchestration label
already covers). The orchestration hint reads `将通过编排模式分派给
专家子代理处理` in zh — aligning with the
`协调模式`→`编排模式` rename shipped in the previous commit.
Tests: five new tests in EmptySession.test.tsx covering: cards render on
desktop, hidden on mobile, click pre-fills the composer, orchestration
cards persist coordinator mode after submit, non-orchestration cards
leave coordinator mode untouched. `bun run vitest` 22/22 pass.
`bun run lint` (tsc --noEmit) clean.
Tested live in browser: clicked the Pre-merge review card → composer
filled with the 78-char zh prompt → localStorage carries
`coordinator-modes[<sessionId>]: true` → server log shows the CLI
restarted with `--append-system-prompt`. Screenshot:
artifacts/desktop-welcome-task-cards.png.
Confidence: high
Scope-risk: narrow
* chore(scripts): one-click MCP smoke setup for Windows
Wraps the recurring 4-step setup from docs/desktop/10-local-mcp-testing.md
into a single idempotent script. Same workflow we'd otherwise type by hand
every time we want to drive the desktop UI from Chrome DevTools MCP.
scripts/dev-mcp-test.ps1 does:
1. Health-check the API server on :3456 — fail fast with the start
command if it's down.
2. Health-check Vite on :1420 — fail fast with the start command if
it's down.
3. Confirm Vite's /health proxy is wired through to the server (catches
stale vite.config.ts).
4. Ensure http://localhost:1420 is in H5 allowedOrigins (PUT only when
missing).
5. Regenerate a fresh H5 token via /api/h5-access/regenerate.
6. Build the full URL with serverUrl + forceH5=1 + h5Token, copy it to
the clipboard, and print it.
7. With -Open, also Start-Process the URL in the default browser.
8. With -Quiet, only print the URL on stdout (for piping into other
tools, e.g. un run mcp:test:open chaining).
Surfaced via two npm scripts:
bun run mcp:test # generate URL + clipboard
bun run mcp:test:open # also open in default browser
Deliberate non-goals:
- Does NOT start server / Vite. Those are persistent dev processes the
developer wants to control lifecycle for; auto-starting them from a
one-shot script makes "is it still running?" questions ambiguous and
leaves zombies on script exit. The script tells the user the exact
command to run if either is down.
- Does NOT touch Electron. This workflow is the documented browser path
for Chrome DevTools MCP smoke; the Electron path doesn't need any of
this and runs through bun run electron:dev.
- Does NOT modify vite.config.ts proxy or desktopRuntime.ts forceH5
bypass. Both are already on this branch, and validation of those two
being correctly wired is what step 3 covers.
Doc: docs/desktop/10-local-mcp-testing.md grew a new "step 0 一键脚本"
section pointing at the script and naming the two npm targets, ahead of
the original 4 manual steps so future maintainers find the script first.
Verified by running it: server + vite both up, allowedOrigins pre-existed
so no PUT needed, fresh token issued, URL copied to clipboard, browser
launched into the welcome screen with the four task cards rendering
correctly. Screenshot in artifacts/desktop-welcome-task-cards-final.png.
Confidence: high
Scope-risk: narrow
* feat(desktop): zero-token "Recent activity" panel with hand-off button
Closes the painful gap where opening a new chat in a project means
re-explaining "what was I just doing" to the model. The user explicitly
called out that they don't want the obvious fix (auto-feeding the
previous transcript or an LLM-generated summary) because that burns
tokens on every new session.
This ships a different shape: derive the activity summary on the server
from on-disk state (session JSONL + git working tree), render it as a
read-only panel on the welcome screen so the user sees it, and only
move bytes into the model when the user explicitly clicks "Continue
from here" — and even then, only a 4-5 line hand-off paragraph (~60
tokens), not the previous transcript.
Server (zero LLM, all derivation):
- GET /api/projects/recent-activity?workDir=<abs>&excludeSessionId=<id>
- projectActivityService.getRecentActivity reads:
- sessionService.listSessions to find the most recent meaningful
session (skips empty messageCount=0 sessions and any
excludeSessionId, e.g. the just-created Untitled tab in
ActiveSession's empty welcome state, so the panel surfaces the
ACTUAL previous work session).
- Streams that session's JSONL and derives:
* lastUserMessageExcerpt (160 chars max, mid-word ellipsis)
* filesEdited list — Edit/Write/NotebookEdit tool_use file_paths,
de-duplicated in first-appearance order, capped at 8.
Hard-capped at 50k JSONL lines so giant transcripts don't hang
the welcome render.
- getRepositoryContext (existing, cached) for branch + default
branch + dirty state, plus two cheap git rev-list/status calls
for ahead/behind counts and exact dirty file count.
- Strict timeouts on each git command so the panel never blocks UI.
- Wired into router.ts under case 'projects'.
Frontend:
- RecentActivityCard reads via projectsApi.recentActivity.
- Two paths into it:
1. EmptySession (sidebar "New session" → user picks workDir):
shows once a workDir is chosen. "Open this session" switches
to the previous tab; "Continue from here" prefills the
hand-off into the empty composer's textarea.
2. ActiveSession's empty welcome state (sidebar "在 X 中新建会话"
creates a session immediately): excludeSessionId={activeTabId}
skips the brand-new empty session, hideContinueSessionButton
hides "Open this session" since the user is already on an
empty tab. "Continue from here" dispatches the existing
cc-haha:composer-prefill window event so ChatInput picks it
up — same plumbing the welcome task cards use.
- Auto-refreshes every 60s — git state changes between renders
(commits in another terminal, etc.) get picked up without UI work.
- Hidden on phone-sized H5 (composer is dense enough already).
Hand-off paragraph contents (i18n'd, ~60-100 tokens):
Last session on <branch>: "<title>".
Files touched: <up-to-5-files> (+N more).
Local is ahead of upstream by N commit(s).
N file(s) have uncommitted changes.
Please continue from there. Pick up by ...(describe the next step).
Token cost summary:
- Render: 0 tokens. Pure on-disk derivation, never sent to a model.
- "Open this session": 0 tokens. Just a tab switch.
- "Continue from here": ~60-100 tokens, ONLY if the user types more
and presses send. The user sees the prefill in the textarea before
sending and can edit or delete it.
i18n: 18 new keys (empty.recentActivity.*) localized for the same five
locales as the orchestration toggle (en/zh/zh-TW/jp/kr).
Live-tested via Chrome DevTools MCP on this branch:
- Opened cc-haha welcome path.
- Server returns correct shape: branch=feat/agent-routing-hardening,
aheadCount=6, dirtyCount=13, lastSession with 10 messages and
correct title.
- excludeSessionId correctly skips the empty just-created session and
surfaces the previous "请审查我当前分支..." session.
- "Continue from here" click prefills the textarea with the 152-char
Chinese hand-off paragraph; localStorage and WS state untouched
(zero token cost confirmed).
- Screenshots: artifacts/desktop-recent-activity-card.png and
artifacts/desktop-recent-activity-handoff.png.
bun run lint passes (tsc --noEmit clean).
Confidence: high
Scope-risk: moderate (new endpoint, but read-only and isolated; new UI
panel, but additive only — empty workdir or no prior sessions just
hides the card)
Tested: live MCP smoke per above.
Not-tested: server-side unit tests for projectActivityService (planned
follow-up; live integration validated the happy path end-to-end).
* fix(desktop): compact recent-activity card so composer stays in view
Live MCP smoke caught a layout regression. Welcome screen vertical
budget on a 923px viewport is roughly:
79 tab bar
+ ~250 hero block (logo + title + subtitle)
+ ~165 task cards
+ ~222 composer (ChatInput in hero variant)
+ ~64 flex padding
≈ 780px before activity panel
The first cut of RecentActivityCard rendered a 235px column block
(title row, chips row, italic excerpt row, file-chips row, action row).
Together with the task cards that pushed total content to ~410px
beyond what fits, and ActiveSession's empty-state wrapper used
flex flex-1 + justify-center without overflow handling — so the
sibling ChatInput got pushed below viewport bottom (composer.bottom
1112 on a 923 viewport, visible:false confirmed via DOM measurement).
Two changes:
1. Compact the card to ~127px (down 108px, roughly half) by collapsing
to a single horizontal row: icon | title + chips column | action
buttons. The italic last-user-message excerpt and the file-name
chip strip both go away — they're nice-to-haves, not load-bearing.
The data they carried is preserved: filesEditedCount stays as a
chip with a tooltip showing the first 5 file basenames; the lead
excerpt is still in the hand-off paragraph the "Continue from here"
button injects, so the user gets it where it counts (in the
composer) instead of as ambient ornament on the welcome screen.
Action buttons collapse their labels under sm: breakpoints to keep
the card single-row even in narrow side panels.
2. Add min-h-0 + overflow-y-auto to ActiveSession's isEmpty wrapper.
Belt-and-suspenders: future taller content (extra cards, longer
localized strings) now scrolls inside the welcome region instead of
shoving the composer offscreen. Composer remains a flex sibling and
stays anchored at the bottom regardless of content height above it.
Verified live in MCP browser:
recentActivity height: 235 → 127px
composer.bottom: 1112 (offscreen) → 907 (visible, on a 923 viewport)
textareaVisible: false → true
Tested across two projects with very different activity profiles
(cc-haha: short title + 0 files edited; layout-editor: long title +
20 files edited + 1876 messages). Both render in single horizontal
row.
Screenshot: artifacts/desktop-recent-activity-compact.png
bun run lint passes (tsc --noEmit clean).
Confidence: high
Scope-risk: narrow (CSS / layout only, no behavioral change).
---------
Co-authored-by: 你的姓名 <you@example.com>
Bias the orchestrator toward delegating medium tasks (>2 steps or >1 file), reserving direct action for trivial work. Require restating relevant project rules (e.g. codegraph-first) inside sub-agent prompts, since read-only agents (Explore/Plan) run without project memory.
Tested: bun test conversations.test.ts (orchestration cases pass)
Confidence: high
Scope-risk: narrow
方案3 (desktop, manual-guided): when compactions keep firing only a turn or two
apart — the same thrash the CLI circuit breaker trips on — chatStore now appends
a one-time visible system notice suggesting the user start a fresh session
(the prior summary stays in the current one). Detection lives in module-level
state (compactionThrashBySession) so PerSessionState is untouched; counts user
turns between compactions, suggests after 3 rapid ones, deduped per session and
reset on /clear. New i18n key chat.contextExhausted (en/zh/zh-TW/jp/kr).
Also includes in-progress orchestration/coordinator work on the same files
(orchestrationPrompt.ts, conversationService coordinatorMode --append-system-prompt,
ws handler/events, chatStore, ChatInput, sessionRuntimeStore, types/chat) —
committed together per request.
Tested:
- bunx vitest run desktop/src/stores/chatStore.test.ts (102 pass, incl. new
方案3 rapid-compaction suggestion + spread-out negative case)
- cd desktop && bun run lint (clean)
- get_diagnostics clean across changed server + desktop files
Not-tested: full bun run check:server / check:desktop gates; pre-existing WS
runtime-restart integration tests are flaky in this env (CLI code 143).
Confidence: medium-high
Scope-risk: moderate
Third-party Anthropic-compatible relays reject oversized requests with a 400 context_too_large / 'exceeds the context window' body instead of Anthropic's 'prompt is too long' wording. That fell through to a raw API error with no recovery hint. Normalize it via a new isContextWindowExceededMessage() so getAssistantMessageFromError and classifyAPIError route it onto the existing prompt-too-long handling, surfacing the actionable 'Context limit reached / compact or clear' guidance (TUI) and businessError.prompt_too_long copy (desktop).
Tested: bun test src/services/api/errors.test.ts (4 pass)
Not-tested: full bun run check:server gate
Confidence: high
Scope-risk: narrow
Stop-generation set a 3s force-kill timer that called stopSession(sessionId). If the user switched provider/model in that window, the restart spawned a NEW CLI process under the same sessionId; the stale timer then SIGTERM-killed the new process during its startup grace window -> 'CLI exited during startup with code 143'.
Fix: tag each spawned process with an instanceId (sessionId#N). handleStopGeneration captures the live instanceId up front; the 3s fallback calls stopSessionInstance(sessionId, instanceId), which only kills when the current live process still matches that instance. A restart replaces the instance, so the stale timer becomes a no-op.
conversationService.ts: instanceId field + counter, getActiveInstanceId(), stopSessionInstance().
ws/handler.ts: handleStopGeneration uses the instance-guarded fallback.
conversations.test.ts: instance-guard regression tests.
Tested: bun test conversations.test.ts -t instance/stopSessionInstance (pass). Staged via hunk selection to exclude unrelated in-progress orchestration work that shares these files.
Confidence: high
Scope-risk: narrow
When the auto-compaction circuit breaker has tripped and the context is still over threshold, autoCompactIfNeeded now returns contextExhausted:true. This is the signal the desktop will use to suggest starting a fresh session (carrying the latest summary) instead of degrading silently. Pure predicate isContextExhausted() added + tested (15 pass). Desktop wiring (ws translation + chat suggestion card + new-session handoff) is intentionally not included here — those files currently carry unrelated in-progress changes.
Two context-management fixes (CLI/sidecar side):
1. Re-compaction loop (方案1+2). Auto-compaction previously only circuit-broke
on hard failures; a compaction that succeeded but left context still over
threshold reset the failure counter and re-compacted every turn, burning a
summary API call each time.
- autoCompact.ts: isIneffectiveCompaction() — when truePostCompactTokenCount
is still >= threshold, count it toward the existing circuit breaker instead
of resetting to 0; query.ts now honours that count on the success path.
- shouldThrottleAutoCompact() — suppress proactive autocompaction for a few
turns after a compaction, unless at the hard blocking limit (so we never
risk prompt-too-long by throttling).
2. Post-compact context meter stuck at ~100% (sessionService.ts). The transcript
context estimate counted the entire pre-compact history plus the summarization
call's huge input_tokens, pinning the indicator near full until the next turn.
Now the estimate is scoped to messages after the latest compact_boundary, with
a model fallback for the just-compacted/no-new-turn case. Non-compacted
sessions are unchanged.
Tested:
- bun test src/services/compact/autoCompact.test.ts (12 pass; throttle + ineffective predicates)
- bun test src/server/__tests__/conversations.test.ts -t "context" (6 pass incl. new compact-boundary scoping case)
Not-tested: 4 pre-existing WebSocket runtime-restart integration tests fail in this env (CLI subprocess code 143); unrelated to these changes.
Confidence: high
Scope-risk: moderate
- Update source: desktop/package.json publish.owner NanmiCoder -> 706412584 (electron-updater
now checks this fork's releases via app-update.yml). Also bump homepage and the legacy
Tauri updater endpoint to the fork.
- About page: GITHUB_REPO / issues / releases / changelog and the displayed repo name now
point to 706412584/cc-haha. Sidebar repo link and ActivitySettings profile subtitle too.
- Attribution: original author (NanmiCoder + Bilibili/Douyin/Xiaohongshu social links) is
preserved unchanged. Added a new "Fork maintainer" credit (706412584) with i18n keys
settings.about.forkMaintainer / forkMaintainerHint across en/zh/zh-TW/jp/kr.
Why: this is a self-maintained fork shipping its own builds; pointing the updater at upstream
risked overwriting custom builds with upstream releases and hid the fork's own releases.
Tested:
- bunx vitest run src/pages/ActivitySettings.test.tsx src/__tests__/generalSettings.test.tsx (59 pass)
- cd desktop && bun run lint (clean)
Not-tested: full verify (long-running)
Confidence: high
Scope-risk: moderate
Built-in agents:
- Add debugger, security-reviewer, refactor, migration, docs-writer, performance, commit-pr built-ins and register them in getBuiltInAgents().
Desktop:
- AskUserQuestion prompts no longer vanish silently: when an unanswered question card is cleared by turn end (message_complete / error) — e.g. a malformed question call — a visible system notice (chat.questionDropped) is appended instead. Added across en/zh/zh-TW/jp/kr.
Versioning:
- Bump desktop/package.json to 0.5.5 and add release-notes/v0.5.5.md.
Tested:
- bunx vitest run desktop/src/stores/chatStore.test.ts (99 pass, incl. dropped-question notice for message_complete + error, and no notice for non-AskUserQuestion)
- bun test src/tools/AgentTool/builtInAgents.test.ts (pass)
- cd desktop && bun run lint (clean)
- build:windows-x64 + package-smoke (PASS, Claude-Code-Haha-0.5.5-win-x64.exe)
Not-tested: full bun run verify / check:server (long-running)
Confidence: high
Scope-risk: moderate
These were accidentally committed in the previous catch-all commit. They're local tool artifacts (.codegraph index, .codegraph/daemon.pid runtime PID, personal IDE settings) that don't belong in the repo. Adding them to .gitignore and removing from tracking via git rm --cached so working copies stay intact.
ChatInput / SkillPickerMenu:
- + menu now has 4 entries: file, slash command, Skills, Plugins.
- Skills/Plugins open an inline picker above the composer (mirrors the file-search popover: ↑↓ navigate, Enter pick, Esc dismiss). Picking inserts a "@skill:<name>" / "@plugin:<name>" token at the cursor — visible to the user, also legible to the agent as "use this skill/plugin".
Queue "send now":
- sendQueuedMessageNow no longer aborts the running turn or promotes-and-waits — it just sends the message immediately. Removes the prior "Request was aborted" / code 143 retry loop caused by mid-turn stop_generation.
Theme follow-system persistence:
- settingsStore.loadSettings now honours a locally stored "system" theme over whatever concrete value the server last saw, since the server intentionally rejects "system". Fixes follow-system reverting to a concrete theme on every restart.
- uiStore.test.ts: updated the toggleTheme cycle test to include the system step (was a stale failure independent of these changes).
FileReadTool:
- Treat pages: "" as undefined ("read whole file") so the agent's empty-string slips don't trap the tool in a validation-error retry loop.
i18n:
- Added chat.openSkills / openPlugins / skillPicker.{title,empty} / pluginPicker.{title,empty} / sendNow across en / zh / zh-TW / jp / kr.
- Did NOT touch unrelated settings.skills.recommended.* / settings.plugins.* additions present in the working tree from another in-progress task.
Tested:
- bunx vitest run src/stores/chatStore.test.ts -t "message queue" (10 pass)
- bunx vitest run src/stores/settingsStore.test.ts src/stores/uiStore.test.ts (32 pass)
- bun run lint (desktop tsc --noEmit, clean)
- Windows package-smoke after build:windows-x64 (PASS)
Not-tested: full bun run check:desktop / check:server (long-running)
Confidence: high
Scope-risk: narrow
Built-in additions (always shipped):
- test-author: writes/runs tests for changed code, detects framework, reports changed-line coverage.
- code-reviewer: read-only static review, finds bugs/smells/security issues, ends with REVIEW: APPROVE | CHANGES_NEEDED.
Project-level addition under .claude/agents/:
- game-developer: covers Unity/Unreal/Godot and web JS engines. The system prompt forces engine-version detection (ProjectVersion.txt, *.uproject, project.godot, package.json), prefers querying real symbols already in the project (codegraph) over recalling APIs, and falls back to verifying uncertain APIs against the engine's official docs — to defend against API hallucinations across versions.
.gitignore: switched ".claude/" to ".claude/*" with explicit "!.claude/agents/**" so project agents can ship in the repo while user-private files like .claude/settings.json stay ignored.
Tests:
- builtInAgents.test.ts: asserts test-author + code-reviewer are registered, game-developer is NOT a built-in.
- gameDeveloperAgent.test.ts: parses .claude/agents/game-developer.md through the runtime parser and asserts the strengthened guidance (codegraph, official docs, ProjectVersion.txt) is present.
Tested: bun test src/tools/AgentTool/builtInAgents.test.ts src/tools/AgentTool/gameDeveloperAgent.test.ts (5 pass)
Not-tested: full bun run check:server (long-running)
Confidence: high
Scope-risk: narrow
v0.5.3's one-shot skip only blocked a single auto-drain, but a Stop emits
multiple idle events (message_complete + status idle); the second one still
drained one queued message and flipped the button back to running. Replace
the one-shot skip with a sticky queueDrainPaused flag: Stop pauses all
auto-drains until the user sends their next message (which clears it and
fires immediately, ahead of the queue). The queue then resumes FIFO on the
next idle. Bumps desktop app to 0.5.4 with release notes.
Tested: tsc --noEmit; Vitest chatStore 94 passed (incl. multi-idle stop /
priority message / queue resume); browser end-to-end (Stop keeps button at
Run with queue intact, priority send immediate, queue resumes); Windows x64
NSIS package + package-smoke PASS.
Scope-risk: narrow
Confidence: high
Stop no longer flushes the queue: a user-initiated Stop now skips exactly the
auto-drain triggered by its resulting idle (one-shot guard). The queue stays
intact, the next message the user sends fires immediately (priority, not
queued — even with items waiting), and the queue resumes FIFO on the next idle
after that turn. Queue enqueue/drain/edit mechanics are otherwise unchanged.
Also removes the Buy-Me-a-Coffee / donation section from README (zh/en).
Bumps desktop app to 0.5.3 with release notes.
Tested: desktop tsc --noEmit; Vitest chatStore 94 + pages/ActiveSession 43
passed (incl. stop-no-flush / priority-message / queue-resume case); Windows
x64 NSIS package + package-smoke PASS.
Scope-risk: narrow
Confidence: high
Desktop chat message queue (v0.5.2):
- Queue follow-up messages (Enter) while the agent is busy; FIFO auto-drain
on true idle. Collapsible, height-capped to-do panel above the composer so
it never inflates the composer. Per-item move-to-top/edit/delete/clear.
Never drains mid-stream, during tool execution, or while a permission
prompt is pending; Stop cancels the current turn but keeps the queue.
Per-session isolation. In-memory only (no persistence schema change).
Also included in this commit:
- Settings provider form: model-id comboboxes + fetch-models + per-slot
context-window auto-fill.
- Bundled skills: defineGoal, pdf, screenshot.
- Bump desktop app to 0.5.2 with release notes.
Tested: desktop tsc --noEmit; Vitest chatStore/pages/ActiveSession 136 passed
(incl. 7 queue tests); browser end-to-end queue smoke; Windows x64 NSIS
package + package-smoke PASS.
Not-tested: full check:desktop suite has pre-existing unrelated failures
(electron packaging, theme cycling, ThinkingBlock label); check:server.
Scope-risk: moderate
Confidence: medium
In-progress generations were interrupted when a set_runtime_config
arrived mid-stream: the handler immediately stopped and restarted the
CLI subprocess. Track per-session busy state from outbound status
events and queue the restart, applying it (with the latest override,
collapsing multiple toggles) once the session returns to idle.
Bumps desktop app to 0.5.1 with matching release notes.
Tested: desktop tsc --noEmit; Windows x64 NSIS package + package-smoke.
Not-tested: check:server, desktop Vitest.
Scope-risk: narrow
Confidence: medium
Main added two TranslationKeys after this branch's base — 'thinking.labelDone'
(the "Thought" label shown after thinking completes) and
'slashCmd.agent.description' — which the jp/kr/zh-TW locale files did not yet
cover, so the Record<TranslationKey, string> contract failed tsc after merging
main. Add both keys to each new locale:
- jp: '思考完了' / '選択した Agent でプロンプトを実行'
- kr: '사고 완료' / '선택한 Agent로 프롬프트 실행'
- zh-TW: '已思考' / '使用指定 Agent 執行提示'
en/zh are unchanged. `tsc --noEmit` is now clean and the i18n + timestamp
suites pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guard OpenAI-compatible proxy streaming bodies with the configured AI request timeout so a provider that emits partial SSE and then idles cannot leave proxy consumers waiting forever.
This is a proxy-level fix found while investigating #548; it does not claim to close the broader desktop interruption issue.
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:server
Confidence: medium
Scope-risk: narrow
The desktop notification poller fired on mount, racing the bootstrap that
resolves the dynamic server URL and confirms /health. Its first requests hit
the uninitialized default base URL and failed with "Failed to fetch", logging
spurious client_api_request_failed warnings to the diagnostics panel.
Add a whenDesktopServerReady() signal resolved once initializeDesktopServerUrl
sets the base URL and the healthcheck passes, and gate the poller on it so it
only starts once the server is reachable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The thinking block title always rendered the static "思考中"/"Thinking"
label; isActive only toggled the animated dots. Once thinking finished
the dots disappeared but the in-progress text stayed. Switch the label
to "已思考"/"Thought" when the block is no longer active.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The models API derives its model list from ANTHROPIC_MODEL and the
ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL env vars. A developer who exports
these for a custom provider (e.g. MiniMax) leaked them into the no-provider
fixture: the four collapsed to one model, the default model became the
custom one, and switched-model names fell back to the raw id — failing five
"available models" / "default model" assertions on that machine while passing
on clean CI.
Clear those four vars in the e2e setup and restore them in teardown, matching
the existing CLAUDE_CONFIG_DIR / CLAUDE_CLI_PATH isolation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The diagnostics panel filled with red ERROR/WARN entries during normal use
because the capture layer treated "wrote to console" as "something failed",
with no severity gatekeeping. Much of the noise was also test runs leaking
into the user's real ~/.claude/cc-haha/diagnostics.
- diagnosticsService: default unclassified events to info (not error);
drop writes under NODE_ENV=test when no CLAUDE_CONFIG_DIR is set; document
the console-capture contract (expected states use console.debug/info).
- index: don't install console/process capture under bun test.
- api/diagnostics: an ingested event with missing severity defaults to info.
- oauthRefreshLog (new): token refresh failure is gracefully handled and is
never an error — expected expiry (401/403/revoked) logs at debug, anything
else at warn. Wired into both Haha OAuth services.
- conversationService: classify cli_runtime_exit severity by exit code, so
clean/SIGTERM/SIGKILL exits are info and only abnormal codes stay error
(real "chat died" crashes remain perceivable).
- ws/handler: streaming partial tool-input JSON is normal — debug, not warn.
Tests: oauth-refresh-log + cli-exit-severity unit tests; diagnostics-service
covers the info default and the test-isolation guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Indent session rows under their project group and switch project folder icons between open and closed states when collapsed.
Tested: cd desktop && bun run test --run src/components/layout/Sidebar.test.tsx -t "groups sessions by project|collapses a project group"
Tested: cd desktop && bun run test --run src/components/layout/Sidebar.test.tsx
Tested: bun run check:desktop
Tested: Browser plugin smoke on http://127.0.0.1:3456/ confirmed first project open -> closed state, icon state, and pl-6 session indentation.
Confidence: high
Scope-risk: narrow
Normalize image blocks loaded from transcript history back into renderable data URLs and keep generated image metadata out of the visible user bubble. Image source metadata is applied in order so multi-image messages retain the correct paths and names.
Tested: bun run test -- --run src/stores/chatStore.test.ts
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
(cherry picked from commit 82381e8d647fa19e20853a96b9681fb548c17f0a)
Close the native preview WebContentsView when the Electron renderer starts a top-level navigation so refreshed session pages cannot leave the in-app browser surface behind.
Fixes#680
Tested:
- bun run verify
Confidence: high
Scope-risk: narrow
When the signing secrets are absent, `CSC_LINK`/`APPLE_*`/`WIN_CSC_*` render
as empty strings. electron-builder treats a present (even empty) CSC_LINK as
"a certificate is configured", tries to load it, resolves the empty path to
the working dir and dies with "<workdir> not a file" — which broke the v0.4.0
release build.
Drop those env vars from the release build step so unsigned builds behave
exactly like the (already-green) dev build, which only sets
CSC_IDENTITY_AUTO_DISCOVERY=false. Add them back once an Apple Developer ID /
Windows cert is available.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Release is gated on the tag only. `bun run verify` runs on PRs and locally,
not at release time, so a failing quality gate no longer blocks publishing.
- release-desktop.yml: remove the quality-preflight job and its `needs`.
- release-workflow.test.ts: assert the release workflow does NOT run verify.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Intro now explains why we migrated: users reported macOS performance
issues and Windows behaviour diverging from the macOS dev environment;
Electron's bundled Chromium unifies the rendering layer at the cost of a
larger installer.
- Fixes section rebuilt from the real v0.3.2..HEAD commits, keeping the
upstream issue numbers (#665#653#351#672#678#671#681#658#651#620)
so they can be closed against this release.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real-world testing showed the unsigned app just needs the original 0.3.2
one-liner after the "damaged" prompt (System Settings "Open Anyway" also
works). Drop the Sequoia / "don't trash it" / clear-DMG-first / script
walkthrough noise and match the historical release-note style.
- release-notes/v0.4.0.md: macOS = drag in + `xattr -cr ...`, Windows one
line, Linux kept short.
- docs/desktop/04-installation.md: same trim, remove duplicated FAQ entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unsigned builds ship manual downloads only, so the macOS auto-update zip
(~187MB) and the blockmaps just clutter the release page and dev artifacts.
- release-desktop.yml: stop attaching *.zip / *.blockmap to the GitHub
Release; users now see DMG/exe/AppImage/deb + install-macos-unsigned.sh.
- build-desktop-dev.yml: stop collecting *.zip / *.blockmap into the dev
artifact for the same reason.
- electron-builder still produces dmg+zip and the whole updater-metadata
pipeline (latest*.yml merge/validate) is untouched, so auto-update can be
re-enabled later by restoring the two asset globs.
- sync release-workflow.test.ts dev-artifact assertion accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use frameless Electron chrome only on Windows and remove the native Windows application menu so the packaged app matches the previous Tauri-style desktop surface.
Add a Windows-only manual drag fallback for desktop drag regions, while excluding tab reorder targets and preserving macOS/Linux native chrome and menu behavior.
Tested: cd desktop && bun run test --run electron/services/menu.test.ts electron/services/windows.test.ts src/hooks/useElectronWindowDragRegions.test.tsx src/components/layout/TabBar.test.tsx src/components/layout/Sidebar.test.tsx src/lib/desktopHost/electronHost.test.ts electron/ipc/capabilities.test.ts
Tested: cd desktop && SKIP_INSTALL=1 bun run build:windows-x64
Tested: Computer Use Windows packaged app smoke verified no native menu, custom controls, drag regions, tab reorder, close-to-background, and deepseek-v4-pro provider response FINAL_WINDOWS_ELECTRON_REAL_PROVIDER_OK.
Not-tested: full bun run verify.
Constraint: keep custom frameless behavior scoped to win32 so macOS and Linux native chrome paths remain intact.
Confidence: high
Scope-risk: moderate
- Rewrite release-notes/v0.4.0.md to match the historical style (no emoji,
Highlights/Fixes/Notes), keep it user-facing only (drop release-process
notes), and correct Linux (supported since the Tauri builds, not new).
- README.md / README.en.md: replace Tauri references with Electron, list
macOS / Windows / Linux, and point first-launch approval to the guide.
- Rewrite docs/desktop/04-installation.md for Electron: Electron asset
names, Linux section, and the unsigned-macOS flow (clear the DMG quarantine
before double-clicking; drop the WebView2/right-click-open leftovers).
- install-macos-unsigned.sh: keep the same-folder DMG flow, no online download.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove maintainer-facing release workflow notes from the public release markdown and keep the macOS unsigned install guidance concise.
Tested: git diff --check
Tested: bun run check:docs
Confidence: high
Scope-risk: narrow
Bump the Electron desktop package version to 0.4.0, publish a macOS unsigned install helper, and let the release workflow continue when Developer ID signing is not configured.
Tested: bash -n desktop/scripts/install-macos-unsigned.sh
Tested: bun test scripts/pr/release-workflow.test.ts
Tested: bun run scripts/release.ts 0.4.0 --dry
Tested: git diff --check
Tested: bun run check:docs
Tested: bun run check:policy
Confidence: high
Scope-risk: moderate
Summarize the v0.3.2 to v0.4.0 Electron migration window and document the installer paths users should follow.
Tested: bun run scripts/release.ts 0.4.0 --dry
Tested: git diff --check
Tested: bun run check:docs
Confidence: high
Scope-risk: narrow
Keep development desktop artifacts limited to distributable archives and update
metadata so macOS workflow downloads do not duplicate the packaged app bundle.
The package smoke step still validates the unpacked app before upload.
Tested: bun test scripts/pr/release-workflow.test.ts
Tested: bun run check:policy
Confidence: high
Scope-risk: narrow
Match Electron Builder Linux output by accepting linux-*-unpacked directories,
treating AppImage blockmaps as optional, and validating release asset names
against the generated x86_64/amd64 and arm64 artifacts.
Tested: bun test scripts/quality-gate/package-smoke/index.test.ts
Tested: bun test scripts/pr/release-workflow.test.ts scripts/quality-gate/package-smoke/index.test.ts
Tested: bun run check:policy
Tested: bun run check:native
Tested: bun run verify
Confidence: high
Scope-risk: moderate
Ensure Electron Builder has the project URL and maintainer metadata required by Linux deb targets, and lock the fields with release workflow coverage.
Tested: bun test scripts/pr/release-workflow.test.ts
Tested: bun run check:policy
Tested: bun run check:native
Tested: bun run verify
Confidence: high
Scope-risk: narrow
Replace the local pre-push quality gate with a reminder-only hook, keep manual quality commands documented, and update the contract test to prevent reintroducing a blocking push gate.
Tested: bash .git/hooks/pre-push </dev/null
Tested: bun test scripts/git-hooks/install.test.ts scripts/pr/quality-contract.test.ts
Tested: bun run check:policy
Tested: bun run check:docs
Tested: git diff --check
Confidence: high
Scope-risk: narrow
Use OS-assigned ports for server integration tests and publish the actual Bun server port after startup, preventing local port collisions from failing server checks.
Tested: bun test src/server/__tests__/e2e/business-flow.test.ts
Tested: bun test src/server/__tests__/e2e/full-flow.test.ts src/server/__tests__/conversations.test.ts src/server/__tests__/tasks.test.ts src/server/__tests__/haha-openai-oauth-api.test.ts
Tested: bun run check:server
Confidence: high
Scope-risk: narrow
Use container queries for the activity summary grid so the Token usage panel responds to its own width instead of the full desktop viewport. This prevents loose medium-width layouts and over-compressed five-column cards when the settings page is shown with sidebars.
Tested:
- cd desktop && bun run test -- src/pages/ActivitySettings.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- cd desktop && bun run test -- src/theme/globals.test.ts --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- bun run check:desktop
- Electron smoke at 1180px and 900px against the local dev server
Not-tested:
- Coverage report; this is a presentation-only desktop UI change.
Confidence: high
Scope-risk: narrow