1601 Commits

Author SHA1 Message Date
程序员阿江(Relakkes)
dda92e6deb feat(providers): import from cc-switch and fetch model lists
Two additions to the provider settings page, both modelled on cc-switch.

One-click import from cc-switch:
- Reads the local cc-switch installation and offers its Claude Code
  providers for bulk import. SQLite (cc-switch v3.8.0+) is the primary
  store, with the legacy v2 config.json as a fallback used only when no
  database exists — once cc-switch migrates it archives that file, so
  reading it alongside a database would surface pre-migration data.
- Supports cc-switch v3.1.0 and newer. Older installs wrote a config
  format cc-switch itself dropped in v3.6.0; those are refused explicitly
  rather than reported as an empty scan.
- Degrades honestly when cc-switch's storage moves: a structure we cannot
  read reports why, distinguishing "cc-switch too old" from "cc-haha does
  not recognise this layout" from "the file could not be read at all".
  Only id/app_type/settings_config are required; other columns are
  optional and unknown ones are ignored.
- Full credentials are resolved server-side during import and never
  appear in the scan payload.

Fetch model lists:
- Probes the provider's Base URL for an OpenAI-compatible /models
  endpoint, walking cc-switch's candidate ladder (version segments and
  nine vendor compat suffixes) and falling through on 404/405.
- Only http(s) endpoints are fetched; a 2xx that carries no model list is
  reported as a failure with the upstream's own message rather than as an
  empty catalog, so a key rejected behind a 200 is not read as "this
  provider has no models".
2026-07-30 00:19:06 +08:00
程序员阿江(Relakkes)
8ec8833bec fix(security): harden local runtime boundaries 2026-07-29 18:37:11 +08:00
程序员阿江(Relakkes)
d433572f6e docs: add LINUX DO friend link to acknowledgements 2026-07-29 18:15:55 +08:00
程序员阿江(Relakkes)
152f2b802a fix(tasks): keep background agent output readable without symlinks #1141
A background agent never writes its .output file — registerAsyncAgent
symlinks it at the agent transcript, and the transcript is the only source
of content. Windows reserves SeCreateSymbolicLinkPrivilege for
administrators unless Developer Mode is on, so symlink() throws EPERM
there, and the catch quietly fell back to creating an empty placeholder.
Nothing ever appends to it, so every background agent's .output stayed 0
bytes and the result was lost. A junction is no substitute — junctions
only work for directories, and the target here is a file.

When symlinks are unavailable, register a read redirect instead: reads and
the output_file path handed to the model resolve to the transcript itself,
which checkReadableInternalPath already allows. The probe is synchronous
because registerAsyncAgent is sync and AgentTool exposes the path in the
same tick — a redirect registered from the async body would land too late.

Writes and deletes now go through getOwnedTaskOutputPath() so they can
never follow a redirect into a file this module doesn't own; without that,
cleanupTaskOutput would unlink the agent transcript.

Unix short-circuits the probe, so behaviour there is unchanged, as is
Windows with Developer Mode enabled.

Adds the first tests for diskOutput.ts, covering both symlink paths and
pinning the two destructive cases.
2026-07-29 17:56:42 +08:00
程序员阿江(Relakkes)
0480d2f1ec fix(desktop): keep the pet task panel clear of the macOS menu bar #1140
Dragging clamps against the mascot alone, so the mascot can reach a
display edge through the window's transparent padding. At the top edge
that means asking for a negative window y on purpose -- and the activity
panel lives in exactly the padding that goes off-screen with it. Measured
against the shipped layout: of a 96px panel, 78px ends up above the work
area, leaving an 18px sliver under the menu bar.

This is not a regression in 8f3a2f092; it is that fix's other half. The
mascot reaching the menu bar and the panel following it off-screen are
the same negative y.

So the panel changes sides instead. The main process is the only side
that knows the window position and the work area, so it decides and the
renderer follows, the way the Codex overlay does it.

Three things that are load-bearing:

- The test is placement-independent -- panel height against the room
  above the mascot -- because the flip frees the very space a
  "does it still fit above?" test would measure next, and would then
  flip back once per frame. A 24px hysteresis covers the boundary.
- Flipping moves the mascot inside the window, so the window moves the
  opposite way to hold it still on screen. Mid-drag that has to rebase
  the drag's window origin too, or the next tick recomputes the pre-flip
  position. A restore needs the same treatment: a saved y belongs to the
  mascot offset it was saved with, and the renderer always starts the
  panel above, so restoring the bare window position would drop the
  mascot by the panel's height on the next launch.
- The renderer only sends drag start and end -- the cursor sampler in
  this process drives everything between -- so a flip decided mid-drag
  has no reply to ride back on and goes out as an event.

The panel box is the union of every reported region past the mascot,
which keeps the IPC payload shape unchanged.

Left and right are deliberately untouched. The panel is 352px wide in a
384px window, so it can only slide +/-16px before the window itself
clips it, while reaching a side edge needs about 120px. Those need the
window to grow or move, which is a different change.

Falsified each layer by reverting it: the placement test, the window
compensation, the drag rebase, and the restore anchor each turn their
own case red.
2026-07-29 17:51:08 +08:00
程序员阿江(Relakkes)
bd44d0a17d fix(desktop): refine slash menu and launch warnings 2026-07-29 14:30:07 +08:00
程序员阿江(Relakkes)
a8d3950231 Merge origin/main into local main 2026-07-29 14:25:28 +08:00
程序员阿江(Relakkes)
d92ac7f2c0 feat(desktop): detect and persist the display locale 2026-07-29 13:09:03 +08:00
程序员阿江(Relakkes)
de52656bb2 fix(server): drop CLI messages replayed after a reconnect
Opening a long-finished session showed it "start talking again": dozens
of `已思考` bubbles streaming in, nothing rendered between them. Nothing
was actually re-run — the transcript stops at the moment the turn ended
and the trace holds only the original 13 API calls. The whole wall is a
replay of output that had already been rendered hours earlier.

The source is not in this server at all. `WebSocketTransport` (inherited
upstream, used for the CLI's `--sdk-url` connection) buffers every
outbound message that carries a uuid, and on every successful reconnect
replays that buffer from the start — `onBunOpen` passes an empty
`lastId`, which skips the branch that would evict already-confirmed
messages, and `replayBufferedMessages` deliberately does not clear the
buffer afterwards. It is safe to do that only because the code assumes
"The server deduplicates by UUID". We never implemented that contract:
`X-Last-Request-Id` appears nowhere outside the transport itself, and
the one uuid check in `handler.ts` only guards task-notification
persistence and forwards regardless. So each reconnect pushed the whole
window through untouched.

The transport also detects system sleep explicitly and keeps resetting
its reconnect budget, so a closed laptop guarantees the reconnect rather
than preventing it. The diagnostics for the reported session (pid 58975)
recorded 31 sleep detections and 31 replays of 858 messages each, spread
over the ten hours between the turn ending and the session being opened
— 13 thinking blocks re-delivered 31 times. Across this machine the same
event has fired 10811 times; it has been happening all along.

Only thinking showed it. Replayed user messages are idempotent, replayed
tool calls are upserted by `toolUseId`, replayed tool results are folded
into the tool card and stay invisible, and replayed reply text is caught
by the wake/reconnect guard added in 2a937c6cc. Thinking had nothing:
its UIMessage carries no `transcriptMessageId`, and one cannot be added
— the wire event is `{type, text}` and the hydrated id comes from a
transcript uuid minted at write time, so the two sides share no id.
Every earlier fix keyed on that field, which is why five of them missed
this. `handleSdkPayload` now skips uuids it has already processed, which
covers replayed partial-message stream events too — that is what the
wall was actually made of — and does so without touching the WS protocol
or the transcript shape.

The renderer keeps a second line of defence for whole-block replays that
might arrive by some other route: a thinking chunk equal to an existing
block is dropped, blank chunks no longer open an empty bubble, and
`appendAssistantTextMessage` also rejects text identical to a hydrated
reply. Equality, not substring — a streamed delta is a fragment and is
almost always a substring of some earlier reply, so a substring test
would swallow normal output. `tool_use_complete` now flushes pending
text the way the streaming path's `content_start` already does, so the
two paths stop disagreeing about where a reply ends.

The CLI's empty `lastId` is left alone; fixing it needs the server to
answer with `x-last-request-id`. Note that only the Bun branch replays —
the Node branch calls `replayBufferedMessages` inside a check for an
upgrade header that the `ws` package removed in v3, so it never fires.
Any regression test written under Node would pass without exercising it.

Tested: bun run check:server (232 files, 2492 tests)
Tested: bun run check:desktop (275 files, 3383 tests)
Tested: bun run check:chat-contract (183 tests)
Not-tested: real sleep/wake cycle against a packaged desktop build.
2026-07-29 11:53:23 +08:00
程序员阿江-Relakkes
195299b0e2
Merge pull request #1134 from RaspberryLee/feat/slash-menu-sections-upstream
feat(desktop): group slash menu commands
2026-07-29 11:16:18 +08:00
程序员阿江(Relakkes)
d0267132eb fix(desktop): save IM settings without binding fields #1137 2026-07-29 10:55:35 +08:00
程序员阿江(Relakkes)
b21c63cff3 fix(cli): backport Claude Code stability fixes 2026-07-29 09:36:03 +08:00
程序员阿江(Relakkes)
07a06227ca feat(site): pick the docs language from the browser on first visit
The site already ships both languages, but / always served Chinese, so
English readers had to find the switcher themselves. A Chinese browser now
stays on /, everything else lands on /en.

The decision runs as an inline script in index.html, next to the existing
theme anti-flash block: deferring it to React would show English readers a
full screen of Chinese first. It only fires on the root path — /en, /start
and /en/start are deliberate destinations, and redirecting those would kick
readers off any cross-language link they follow. A manual switch is stored
in localStorage and wins over the browser language, otherwise switching
would be undone on the next visit to the home page.

resolveRootRedirect carries the same rules as a testable pure function.
check-docs asserts the inline copy and the module agree on the storage key,
the Chinese tag pattern and the root-only guard, so the two cannot drift.
2026-07-29 09:23:19 +08:00
程序员阿江(Relakkes)
047f4fbfea docs: make the English README default with a zh-CN companion
The English README becomes README.md so the GitHub landing page reads in
English; the Chinese one moves to README.zh-CN.md. Both cross-link at the
top. Drops the source-origin framing from the intro and removes the
Disclaimer section, and points the license badge at MIT.

change-policy and pr-triage hardcoded README.en.md, which would have
dropped README.zh-CN.md out of the docs lane.
2026-07-29 09:23:11 +08:00
程序员阿江(Relakkes)
9e9b5eeeae chore: relicense under MIT 2026-07-29 09:23:05 +08:00
程序员阿江(Relakkes)
600712e61a feat(desktop): add "chat about this" handoff to AskUserQuestion #1121
The desktop card only had Submit, so a user who thinks none of the options
fit had nowhere to go. The CLI has had this exit for a while — QuestionView
renders a "{N}. Chat about this" line — but the desktop surface never got it.

Adds the same handoff as a secondary button next to Submit. Deliberately not
gated on allAnswered: not recognising your question in any of the options is
exactly when nothing is filled in. Whatever was already picked rides along so
switching to a conversation doesn't discard it.

The handoff travels as a denial because that's the only channel carrying free
text back to the model, which is the catch: buildDenyMessage wrapped every
denial in REJECT_MESSAGE's "STOP what you are doing and wait for the user".
That contradicts the whole point and would leave the user staring at a silent
turn, so AskUserQuestion joins ExitPlanMode as a denial with its own
instruction — here, to open the conversation and ask what needs clarifying.

The wording moves into constants/messages.ts and the CLI now references it too,
so the two surfaces can't drift apart.

Also fixes the status badge, which read "Answered" after a handoff and
misreported what the user did.
2026-07-28 22:48:54 +08:00
程序员阿江(Relakkes)
148f8dcea5 fix(desktop): restore depth in the session tab strip #1123
The strip, the active tab and the content below it were all
`--color-surface`: on the白 theme that is three planes of #FFFFFF with a
3px terracotta rule as the only separator, which is what #1123 reported as
"粗犷". The flattening came from c712f5285, which lifted the strip's ground
from `--cc-s2` to `--cc-bg` and dropped the active tab's fill.

The strip is frame, not paper. It now sits on the sidebar's ground
(`--color-surface-sidebar`, the same `--cc-s0` the sidebar already uses) and
the active tab is filled with `--color-surface`, so it reads as a sheet
lifted off the desk and continuous with the view it opens onto. No new
tokens, and the top band no longer changes colour halfway across.

The issue also proposed pill/segmented shapes. Those are segmented controls
— fixed item counts, short labels, no close/drag/overflow — so they are not
adopted here; a document tab is what this strip is. Its icon request is,
and it turned up a real bug alongside it.

Also in this change:

- Hover no longer uses `--color-surface-hover`. That token is tuned for
  hovering *on* paper, so on both ink themes it lands brighter than paper
  itself (dark #2B271F vs #201D17, measured luminance 0.0206 vs 0.0125) and
  a hovered tab outshone the selected one. Hover now shares paper with the
  active tab and selection is carried by the rule plus the label weight,
  which is strictly stronger in all six themes.
- Tabs size to their titles (`min-w-[140px] max-w-[200px]`) instead of a
  dead 180px, matching the workspace preview tabs. Chevron scrolling moves
  by a fraction of the visible strip; pointer reordering already measured
  with `getBoundingClientRect` and is unaffected.
- One fixed icon slot per tab, filling in the four kinds that had no glyph
  (session, market, traces, subagent). The running dot used to be
  *inserted* ahead of the label, so a title jumped sideways the moment its
  session started and jumped back when it finished; the dot now swaps into
  the slot and every title starts at the same x.

The `rather than a filled pill` guard is rewritten rather than deleted: the
ban is on the pill *shape* (radius, drop shadow, gaps), not on the fill, and
the comment says so — asserting `bg-transparent` is what would flatten the
strip again. Four tests added for the layering contract, the fluid width,
the icon slot's no-shift property, and the scroll step.

Verified: check:desktop green (275 files / 3369 passed / 1 skipped), and a
six-theme walkthrough over the built dist confirming visible separation in
every theme and the ink-theme hover inversion gone.
2026-07-28 22:28:28 +08:00
程序员阿江(Relakkes)
7ed30d08f5 fix(cli): avoid quadratic long-session normalization #1100 2026-07-28 22:22:44 +08:00
程序员阿江(Relakkes)
eef49d3395 fix(desktop): keep image-only messages in turn order #1095 2026-07-28 22:09:48 +08:00
程序员阿江(Relakkes)
d141543794 fix(telegram): complete secure agent control loop #1130 2026-07-28 21:38:18 +08:00
RaspberryLee
62307ae13c
Merge branch 'main' into feat/slash-menu-sections-upstream 2026-07-28 20:31:31 +08:00
程序员阿江(Relakkes)
977de9f9eb docs: README 增加飞书用户群入口 2026-07-28 19:35:47 +08:00
RaspberryLee
2346427c53 feat(desktop): group slash menu commands 2026-07-28 19:13:14 +08:00
程序员阿江(Relakkes)
5e156ec03e chore(site): 文档站自定义域名切换到 cchaha.ai
把 9 个文件里的 11 处 claudecode-haha.relakkesyang.org 换成 cchaha.ai。
apex 走 Cloudflare CNAME flattening,www 由 GitHub 自动 301 到裸域名。

- docs/public/CNAME 与 prepare-static-output.mjs 的 expectedCustomDomain
  必须同步:后者是硬校验,不一致会让 postbuild 直接 throw、Pages 发不出去。
  它同时驱动 canonical / hreflang / og:url / sitemap.xml / robots.txt。
- meta.js 的 SITE_ORIGIN 管前端运行时那一份同名标签。
- AGENTS.md 里的契约描述一并更新,否则后续改动会照旧域名回退。
- 桌面端 IM 文档跳转链接及其测试断言。

验证:site build 通过(78 路由 + 74 重定向)、check:docs 通过(78 页 323 链接)、
sitemap 80 条 loc 全为新域名、AdapterSettings 6 tests passed。
2026-07-28 13:15:56 +08:00
程序员阿江(Relakkes)
5581249147 chore(sponsor): 下线接口AI 赞助与 Provider 预设
- provider presets 移除 jiekouai 预设(预设数 11 → 10)
- README.md / README.en.md 赞助商表格移除接口AI 整行及推广链接
- 删除 docs/images/sponsors/jiekou-logo.svg
- docs 中英文模型页的中转服务商清单去掉接口AI
- provider-presets 测试同步移除 jiekouai 断言;依赖该预设 defaultEnv /
  authStrategy 的 conversation-service、conversations 测试 fixture 改用
  shengsuanyun 预设
2026-07-28 11:30:57 +08:00
程序员阿江(Relakkes)
05085b83bc fix(mcp): keep project-scoped servers visible across restarts (#1126)
Project-scoped MCP servers written to a directory that never hosted a
session disappeared from the desktop settings list after an app restart:
the discovery set (cwd + recent projects + /api/mcp/project-paths) only
enumerated registry entries with local-scope servers, and .mcp.json files
leave no trace in the global config.

- register the target project when addMcpConfig writes a project-scoped
  server, treat on-disk .mcp.json as the source of truth in project-paths,
  and self-heal registry entries for pre-existing files on first browse
- stop removeMcpConfig from mutating the shared safeParseJSON cache entry:
  removals poisoned every later parse of byte-identical .mcp.json content,
  so a server moved between projects parsed as already deleted; add
  safeParseJSONWithoutCache for callers that edit the parsed value, switch
  the settings raw-update fallback to it, and make
  filterInvalidPermissionRules pure for the same reason
- fetch the full known-project set when PluginDetail refreshes the MCP
  store instead of overwriting the list with a one-project view
2026-07-28 10:08:38 +08:00
程序员阿江(Relakkes)
b33c8dbce2 fix(desktop): ship viewport-fit=cover in the H5 markup
On a phone the H5 client rendered under a gray band where the status bar
sits, instead of running its own background up to the top edge the way
other mobile sites do.

iOS WebKit reads the viewport meta once, while parsing the document.
touchH5.ts rewrote it at runtime to pin the scale and carried
viewport-fit=cover along, but that rewrite cannot turn safe areas on
retroactively — so every env(safe-area-inset-*) in globals.css resolved to
0px and the browser painted its own chrome color behind the status bar.
Declaring cover in index.html is what actually takes effect.

Add a theme-color meta on the same pass. With the page now running under
the status bar, the browser chrome needs to match the palette rather than
guess at it; the pre-hydration script sets it before first paint and
applyTheme() keeps it in step, including for a palette another window
picked.
2026-07-28 01:17:47 +08:00
程序员阿江(Relakkes)
617a0a334d fix(desktop): index session history without narrating it
The sidebar reported "Optimizing history N/M" above the session list while
the SQLite index built. Indexing is background housekeeping the user cannot
act on, and the counter sat there for the whole build.

Drop the visible progress row and narrow the live region to `degraded` —
the one state a user can perceive, where history really is served the slow
way. Building/ready/off now stay silent for screen readers too, so the
behavior is the same regardless of how the sidebar is read.

The four locale strings this leaves unused are deleted in all five
languages, with a resurrection guard alongside the existing one for the
removed installed-skills keys.
2026-07-28 01:17:38 +08:00
程序员阿江(Relakkes)
8bad67e590 fix(release): integrate RPM artifacts into desktop release 2026-07-28 00:51:10 +08:00
程序员阿江-Relakkes
e3f5e6eb1f
Merge pull request #1018 from Raphaelowo/fix(adapters)-shorten-Windows-paths-in-tool-summaries
优化Win工具显示路径
2026-07-28 00:49:41 +08:00
程序员阿江(Relakkes)
ec5094fb57 fix(desktop): stop a failed log write from crashing the main process
Launching from Finder or the Dock leaves the main process with stdio that has
no reader. Writing there fails asynchronously with EPIPE from inside the stream
machinery, and with no `error` listener Node escalates it to an uncaught
exception — which Electron shows as "A JavaScript error occurred in the main
process".

This is reachable in ordinary use: the sidecar exit handler logs a line every
time a sidecar dies, so any crashed or killed sidecar could raise that dialog.
Guarding that one call site would not help, since the main process has ~25
console call sites and all of them write to the same two streams. A try/catch
at the call site cannot help either, because the throw happens off-stack.

Install the guard on stdout/stderr before anything logs. Losing a diagnostic
line is acceptable; killing the user's session over one is not — real
diagnostics already persist to a file through appendHostDiagnostic.
2026-07-28 00:39:18 +08:00
程序员阿江(Relakkes)
eaeab84b16 perf(provider): serve the model catalog without blocking on a refresh
The model catalog only decides which rows appear in the model picker, but
`/api/models` and `/api/models/current` sit inside the `settingsStore.fetchAll`
gate that blocks the desktop first paint. With an unreachable upstream, every
cold start waited out the full 5s request timeout before rendering, then handed
back the bundled fallback that was available for free all along. Nothing was
cached on failure either, so each later call paid the timeout again.

Both official providers had grown their own copy of the same cache, so the fix
is one shared strategy instead of two patches:

- default path is stale-while-revalidate — answer from cache (even stale) or
  the bundled fallback, and refresh in the background
- a failed refresh is remembered for a backoff window, so an unreachable
  endpoint is not re-dialed on every call
- concurrent callers collapse into a single upstream request, since
  /api/models and /api/models/current are requested in the same tick
- forceRefresh keeps its blocking semantics for explicit refreshes

Measured against a real unreachable endpoint: 5000ms -> under 1ms.

The regression tests use an endpoint that never settles, so restoring the
blocking await makes them hang rather than quietly slow down.
2026-07-28 00:39:18 +08:00
程序员阿江(Relakkes)
cfed362e89 fix(desktop): give trace detail tabs a way back to the list
The trace list lives inside Settings, but opening a row jumps to a
sibling top-level tab with no return path: the detail header only
offered copy/refresh/open-window, so getting back to the list meant
finding a tab labelled "Settings" — which does not match the mental
model of someone reading a trace.

Add a "back to list" control to the detail header that returns to the
Settings trace section and closes the tab it came from, mirroring
returnFromWorkbench. It stays available in the loading and error states,
where being stranded hurts most.

Along the same path:
- Tag trace tabs with the account_tree glyph, so the title can carry the
  session name instead of a truncated "Model trace: " prefix.
- Scroll the selected Settings rail entry into view. Settings remounts on
  re-entry with the rail scrolled to the top, which left the selected
  section highlighted off-screen after returning.
- Drop the row action that duplicated the row click.
- Fall back to a browser tab for "open in separate window" outside the
  desktop shell, where it was a dead button.

Navigation is consolidated in lib/traceNavigation.ts so the list, deep
links, and the return path share one definition.
2026-07-28 00:39:18 +08:00
程序员阿江-Relakkes
ede359995f
Merge pull request #1040 from WenYin-Community/main
添加独立的 RPM 打包 workflow,支持为 Linux 系统生成 RPM 安装包(x86_64 + aarch64)
2026-07-28 00:38:17 +08:00
程序员阿江-Relakkes
5058efa44a
Merge pull request #1053 from leehom0123/fix/openai-responses-read-image
fix(proxy): 保留 Responses 工具结果中的图片
2026-07-28 00:22:36 +08:00
程序员阿江(Relakkes)
af68a003d0 docs: crop the preview screenshot to the shared frame
At 2000x1255 it was the odd one out in the README grid: that cell's caption
sat higher than the two beside it, which is the whole reason the shot was
swapped in. Cutting the sidebar column away at the panel divider (x=480) and
shaving the empty 31px off the top and bottom margins lands exactly on the
2000x1436 frame the other five use, so the grid lines up and the tour stage
fills edge to edge.

Nothing in the UI was cut — the tab bar, the project chip and the zoom control
all sit inside the kept region, and the crop leaves the conversation next to
the browser preview, which is what this shot is about. The `--wide` special
case added for the taller frame goes away with it.
2026-07-27 23:20:02 +08:00
程序员阿江(Relakkes)
a7210e1fee docs: swap in a sharper preview screenshot
The old workspace-preview shot was cramped at the size the home page and the
README render it. The new one is a full-window capture with the element picker
open — exactly what the copy next to it promises.

It is wider than the other seven shots (2000x1255 against 2000x1436), and the
tour stage pins `aspect-ratio` on the image itself, so dropping it in would
have stretched it. Each tab now carries its own shot shape — `wide` here,
`tall` for the phone screenshot that used to be matched by id — while the
stage keeps its 2000/1436 height, so the panel still doesn't jump between
tabs.
2026-07-27 23:09:46 +08:00
程序员阿江(Relakkes)
128eb77e08 fix(desktop): keep the run location in the composer toolbar for the whole session
Collapsing directory, branch and worktree into one pill was supposed to end
with the location holding still: editable while the session is a draft,
read-only afterwards, same row either way. It did not. The condition read
`isHeroComposer`, and ActiveSession renders the hero variant only while the
session is empty, so the variant and the draft state flip in the same render.
The location dropped back out to a chip below the panel at exactly the moment
it was meant to stay put. The condition is the composer's width now, not its
variant.

The test written to guard this rendered `variant="hero"` against a session
with messages — a combination ActiveSession never produces — so it passed
while the shipped composer still moved the chip. It renders the default
variant now and asserts the chip sits inside the panel.

Sizing that row exposed the rest of the mismatch: the draft and the live
session were two different geometries. The draft inset its divider inside the
panel's padding; the live one welded a `-mx-4 -mb-4` band to the panel edge.
The first message therefore shifted every control 4px left and 4px down and
stretched the divider by 34px. The live row adopts the draft spacing, because
EmptySession renders the same values — two shells against one.

That alone would have grown the panel by 8px, but the live textarea was also
paying for a descender gap: a textarea is inline-block, and the hero branch
escapes it only by sitting in a flex row. `block` recovers 6px, so the panel
ends up 2px taller with four alignment defects gone.

The narrow layouts keep the band. `p-3` has no padding to spend on inset, and
they never swap variants mid-session, so there is nothing there to hold still.
2026-07-27 18:52:02 +08:00
程序员阿江(Relakkes)
dbf01846e0 docs: lay the README preview out three across
Three rows of two pushed the grid past a screenful. Three across in
two rows keeps the whole preview visible at once.

Captions are tightened to fit a 337px column on one line — the
English theme caption in particular was breaking "Ink blue" across
two lines mid-phrase.
2026-07-27 18:47:45 +08:00
程序员阿江(Relakkes)
1f2d1a930a docs: widen the README preview to six screenshots
Four frames left the desktop app looking narrower than it is. Six
cover the arc a first-time reader needs: it codes, you review, you
verify, it looks good, it grows, and it keeps you company.

Adds the built-in browser preview — the frame that closes the loop by
showing the page the session just changed — plus the desktop pets.
Every caption now carries a second line saying what the frame proves,
so the grid reads without opening each image.
2026-07-27 18:34:19 +08:00
程序员阿江(Relakkes)
4a9a2fd038 Merge: rebuild the documentation site around two readers 2026-07-27 17:33:03 +08:00
程序员阿江(Relakkes)
c2cd615824 docs: rebuild the documentation site around two readers
The site had drifted from the product. Every screenshot predated the
v0.5.0 UI redesign, the reading experience shipped no search and no
syntax highlighting, and a third of the pages were internal process
artefacts — migration task lists addressed to agentic workers, a
release runbook, a proposal marked "historical".

Reorganise around the only two people who read this: someone getting
the desktop app running for the first time, and someone reading the
source. Five sections replace nine — start / desktop / im / cli /
internals — and the pages that served neither reader are gone.

Site rewrite:

- Palette lifted from the desktop app's 「纸·墨·印」 themes, so the
  site and the product read as one thing. Light mirrors 纯白, dark
  mirrors 墨夜, and dark mode exists at all now.
- Fonts are self-hosted. The old @import from Google Fonts is
  unreachable from mainland China, which left every heading in a
  fallback serif; it also only requested weight 600 while the CSS
  asked for 900, so Latin and CJK in the same heading disagreed.
- Docs were shipped as one 968KB manifest downloaded on every page
  view. Split into a 32KB index plus one lazily imported chunk per
  page; the entry bundle is now 101KB gzipped.
- Add search, syntax highlighting, per-route meta with canonical and
  hreflang, a sitemap, and an error boundary. Replace the 44vh
  mobile sidebar with a drawer.
- Image dimensions are read at build time and written into the tag,
  so lazy images reserve their space instead of collapsing.

Screenshots are recaptured from a real v0.5.0 build against a clean
demo project, with tokens, QR codes and paired accounts redacted.
The previous set is deleted rather than kept alongside.

Routes follow file paths, so the restructure would have broken every
inbound link; 37 old paths redirect, in both languages. The PR policy
gate and CODEOWNERS also hardcoded docs/guide/contributing.md.

Verified: check:docs 78 pages / 323 links / 0 problems, check:policy
127 pass. Walked every route at 1440 and 390 in both themes for
overflow, contrast, keyboard reachability and focus management.
2026-07-27 17:32:41 +08:00
程序员阿江(Relakkes)
730e21f717 fix(test): stop attributing stray processes to the no-CLR installer stages
`Verify Windows installer execution` failed twice in a row on the v0.5.0
tag, both times at the same assertion:

    Elevated default-mode reinstall without CLR expected process exit
    code 20, received 22.

20 and 22 are different answers to "why did setup stop". 20 is legacy
recovery refusing to continue; 22 is "a matching process is running". The
stage breaks the CLR on purpose so the installer cannot run PowerShell,
which is exactly when CcHahaFindInstallProcess degrades from resolving
paths to matching bare image names -- `Claude Code Haha.exe`, the three
`claude-sidecar*` names, `OpenConsole.exe`, `winpty-agent.exe`, `rg.exe`.
Any process on the runner carrying one of those names answers for the
stage, whoever started it.

The stage before it expects 22, so a stray match there is indistinguishable
from a pass; the stage after it expects 20, so the same stray match is a
failure. That asymmetry is why this reads as "one flaky assertion" rather
than "the whole no-CLR group is unguarded".

This does not fix the installer, because the installer is not wrong:
refusing to delete user data when it cannot confirm what is running is the
intended fail-closed behaviour, and neither installer.nsh nor this script
changed between v0.4.11 (green) and v0.5.0 (red). What changed is what was
running on the runner. So the script now controls that instead of assuming
it:

- Both stages that expect 20 first clear any process matching the same
  name list. `WaitForExit` only covers the PIDs this script started; the
  fallback matches names, so it also sees leftovers from earlier steps of
  this job -- the compiled-sidecar smoke starts 20 sidecars -- and any
  child a probe spawned.
- Clearing warns instead of throwing when something survives. A survivor
  is runner-owned and out of reach, and failing there would replace the
  stage's own failure with a less informative one.
- A mismatched exit code now prints every matching process with PID and
  path, and a baseline is printed before the first install. Between those
  two, a future failure says whether the runner was dirty or the installer
  regressed, which this run had no way to answer.

The name list is duplicated from installer.nsh by necessity -- NSIS
compile-time state is not readable from PowerShell -- and is commented on
both sides to be kept in sync.

Not verified locally: this needs Windows and an ephemeral runner (the
script refuses to run unless CI=true, since it mutates installer registry
state). Reviewed for Windows PowerShell 5.1, which is what the workflow
invokes: no pwsh-only syntax, and the file is kept pure ASCII as it was,
so 5.1 cannot mis-decode it.
v0.5.0
2026-07-27 08:42:48 +08:00
程序员阿江(Relakkes)
c64f69972d release: v0.5.0 2026-07-27 07:49:28 +08:00
程序员阿江(Relakkes)
137895f23e fix(test): isolate the appearance cache from a real CLAUDE_CONFIG_DIR
The same suite passed under `check:desktop` and failed under
`check:coverage` — 271 files and 3332 tests either way, with one case red
in the second: "returns null rather than throwing on a missing or corrupt
cache" read back `{isDark: true, background: '#201D17', ...}`, which is
exactly what the case above it writes.

`appearanceStatePath` resolves `env.CLAUDE_CONFIG_DIR` before falling back
to `app.getPath('home')`, and these cases passed no env, so they defaulted
to `process.env`. `check:coverage` runs its suites through
`createSandboxedTestEnvironment`, which sets CLAUDE_CONFIG_DIR
(scripts/pr/test-environment.ts:74). With it set, the per-case temp
directories from `makeApp()` stop deciding anything: every read and write
collapses onto one shared file, and `afterEach` only removes the temp
directories, never that file. So the round-trip case wrote it and the
missing-cache case read it. Only that one case is ordered to notice — the
others write before they read.

Nothing about the product is wrong here: defaulting to `process.env` is
what the shipped code should do, and a portable install setting
CLAUDE_CONFIG_DIR is a supported mode. The defect is that the tests never
opted out of it.

Each case now passes the isolated env `makeApp()` hands back, which is the
convention `windows.test.ts` already follows for the same path shape (it
threads `{}` or `{CLAUDE_CONFIG_DIR: tmp}` through every call). Verified
both ways: with CLAUDE_CONFIG_DIR set — the condition that reproduced the
failure — and without it, 23/23 each time. `check:coverage` now reports
5/5 suites, and `check:desktop` stays green.

`sidecarManager.ts` and `windows.ts` resolve paths the same way; their
suites were checked and already pass an explicit env.
2026-07-27 07:49:12 +08:00
程序员阿江(Relakkes)
fc8ed80068 fix(desktop): line the sidebar wordmark up with the nav icons
The header sat at 12px while everything below it started at 24px — the
new-session, scheduled and market icons, the search glyph, the settings
gear. The name hung out on a line of its own to the left, so pad it onto
theirs.

That spends 12px of header room, and the long form was already running
out around 244px. Rather than clip it mid-letter, carry a short form as
well and swap on a container query over the title region: no sidebar
width now shows a cut-off name.
2026-07-27 07:30:15 +08:00
程序员阿江(Relakkes)
fc9f5d554e feat(desktop): let a nine-row action sheet become a pet instead of demanding an exact atlas
Importing an animated pet required a file that was exactly 1536x2288, laid
out as 88 seamless cells, with the last two rows holding sixteen distinct
gaze angles. No image model emits that. Whatever a user got back from Jimeng
or ChatGPT was some fixed size like 1024x1536, so the path ended at "the
animation atlas must be exactly 1536x2288 pixels" every time. The third card
was worse: "AI-generate full animation" was hardcoded `disabled`, so the one
entry point named after what people actually wanted to do was dead.

The fix was already in the tree. `scripts/assemble-generated-pet-atlas.py`
landed in the same commit as the four built-in pets, which is to say the
built-ins were produced this way — it takes an action sheet at any size,
slices it on an 8x9 grid, fits each cell to 192x208, mirrors the run row to
make run-left, and reuses rows to reach eleven. That capability was never
wired to anything a user could reach.

`petAtlasNormalize.ts` reimplements it on a canvas in the renderer, so an
author draws nine rows and the app derives the rest. Verified against the
reference assembler by reversing dada-code's atlas into a nine-row sheet and
re-normalizing it: every difference lands on semi-transparent antialiased
edges (2314 pixels, max channel delta 14/255) and opaque regions are
identical. That residue is canvas premultiplied-alpha round-tripping, not a
slicing bug.

Three contract details worth stating. Row frame counts are now derived from
`PET_ANIMATION_DEFINITIONS` rather than typed out a fourth time; they come
out equal to the assembler's `(6,8,8,4,5,8,6,6,6,8,8)`. A sheet already at
1536x2288 passes through byte-for-byte instead of being resliced, because
resampling finished artwork buys nothing. And since the validator never
inspects the alpha channel, a flattened white background used to import
happily and render as a rectangle on the desktop — the renderer now rejects
sheets whose atlas is under 5% transparent (the built-ins sit near 78%) with
a message that names the actual problem.

The copy stops describing the implementation. "Animate one image" and
"Import professional animation atlas / exact 1536x2288 v2 PNG" become "use a
picture you already have" and "I already have an action sheet"; the dead AI
card becomes a three-step walkthrough carrying a copyable prompt, a labelled
8x9 reference grid that can be saved locally, and the checks that catch the
common failures. Reference images are generated by a script rather than hand-
placed, in both languages. All five locales move together.

Caught while reviewing the real dialog in Electron: after finishing the
walkthrough the form heading fell through to the atlas branch and announced
"I already have an action sheet" to someone who had just been walked through
drawing one. Covered by a test now.

Not done: docs/images/desktop_ui/15_pet_create_methods.png still shows the
old dialog and needs a fresh capture from a running app to match the styling
of the shots around it.
2026-07-27 07:20:09 +08:00
程序员阿江(Relakkes)
e972a362dc Merge branch 'fix/dialog-opaque-fill' 2026-07-27 06:26:20 +08:00
程序员阿江(Relakkes)
e43a70f9a1 fix(desktop): give dialogs an opaque fill instead of betting on the blur
The provider list behind the 860px "add provider" dialog was legible
straight through the panel — URLs and model names readable in the form's
empty space.

`.glass-panel` states a translucent fill and a blur in one rule, and reads
as frosted only when both land. The blur is the fragile half: where
`backdrop-filter` does not run there is no failure for CSS to report. The
declaration is skipped, the 0.84 fill is left standing on its own, and 16%
of the page comes through unscrambled. A reduced repro pins it — with the
blur live nothing inside the panel is readable; with it disabled the result
matches the report exactly.

Dialogs leave the coupling entirely. `--color-surface-dialog` is opaque by
construction (no alpha channel to walk back one decimal at a time), mapping
to `--cc-bg` on light themes and `--cc-s1` on the ink ones — ink runs its
background at the bottom of the ramp, so a lifted surface has to climb it
rather than reuse it. `.dialog-panel` carries fill, hairline and shadow and
never touches `backdrop-filter`.

Two things follow from an opaque panel. The scrim is now the only thing
separating the dialog from the page, so `Modal` moves to the heavier
`--color-modal-scrim` — the token that already existed for exactly this and
had only `GlobalSearchModal` as a user, while `Modal` sat on the non-modal
one. And the `:focus-within` ring goes: a dialog holds focus essentially
always, so it burned permanently rather than signalling anything.

The small floating layers keep the glass, but no longer depend on the blur
for legibility: the fill goes to 0.92/0.93. The `@supports` fallback added
alongside it is worth less than it looks — it catches engines that do not
implement `backdrop-filter`, not the failure seen here, where the query
returns true and the blur still never runs. Raising the density is the half
that actually covers the reported case.

Why the blur is inert on this machine is not established. Ruled out on the
code side: GPU switches, containment on `html`/`body`/`#root`, and CSS
`zoom` (UI scaling goes through Electron's `setZoomFactor`). The fix does
not depend on that answer.

Verified across warm-classic, dark and ink-blue: computed fills come back
as `rgb(...)` with no alpha, `backdrop-filter: none`, and both ink themes
render the panel lighter than the page behind it.
2026-07-27 06:09:50 +08:00
程序员阿江(Relakkes)
ff20817e3a feat(desktop): collapse the run location into one pill in the composer toolbar
Directory, branch and worktree were three separate buttons on a bar welded
under the composer. That bar forced the panel's squared bottom edge, so the
composer read as three stacked bands split by two divider lines, and the whole
row jumped outside the panel as a read-only chip the moment the first message
was sent.

They are one pill now, sized for the toolbar row it shares with "+" and the
model selector. The panel is fully rounded again and keeps a single divider.
The pill stays put for the life of the session: editable while the session is
a draft, read-only afterwards, same row either way.

Directory, branch and both worktree modes live in the pill's menu. The worktree
modes are one click from the root view; the branch list is a second view with
its own search and a way back. The nested directory picker portals its dropdown
to the body, so the menu exempts it from its own outside-click handling.

A truncated branch keeps its tail — `…use-native-on-main`, not `feature/comp…`
— because the end is what distinguishes it. `dir="rtl"` moves the ellipsis to
the front and `<bdi>` stops the RTL container from reordering the slashes.

H5 already took a different path here (`useCompactControls` keeps the controls
outside the panel), so the pill lands on its own line there at 40px for touch,
and uses the existing bottom sheet. That line is now a fixed single row: the
three buttons needed 447px against 338px available and wrapped to two, with the
row count following the branch name.

Sizing the pill exposed a layout bug: a long branch grew it until the
permission selector wrapped to two lines and the toolbar grew with it. Shrink
now falls on the pill alone.

The English label is "Location", not "Run location", which would have collided
with the Run button for anyone reading the row through a screen reader.
2026-07-27 06:09:44 +08:00