Commit Graph

1641 Commits

Author SHA1 Message Date
程序员阿江(Relakkes) 2d39d2adaa fix(desktop): keep activity panel state across tab switches and window restore
The activity panel derived visibility from volatile per-session caches, and
three lossy paths could drain them: the connectToSession reset branch dropped
backgroundAgentTasks/agentTaskNotifications, reloadHistory replaced background
tasks with transcript-only records (running tasks are never persisted), and a
history load aborted by a concurrent task mutation was silently discarded.
ActiveSession's one-way auto-close then made any transient empty beat
permanent.

Preserve activity fields on reconnect reset, always merge unresolved
background tasks into reloaded history (transcript terminal records still win,
keeping the stopped-task reconcile intact), retry aborted empty history loads
with a bounded delay, and debounce the auto-close behind a history-ready grace
period.
2026-08-02 16:48:30 +08:00
程序员阿江(Relakkes) eacced7ad4 fix(desktop): keep expanded SubAgent run tool cards across live refresh
The SubAgent run detail tab polls every 2s while the run is live, and
mapHistoryMessagesToUiMessages minted a fresh nextId() for every
thinking/tool_use/tool_result block on each pass. The new ids changed
the ToolCallGroup/ToolCallTree React keys, remounting ToolCallBlock and
dropping its local expanded state, so any tool card the user expanded
collapsed again on the next poll.

Derive the UI message id from the transcript identity instead:
msg.id (always set by the server-side entriesToMessages boundary) plus
the block index is deterministic across remaps and unique within the
source message, so polling keeps keys stable and expansion survives.
2026-08-02 16:48:30 +08:00
程序员阿江(Relakkes) 52dce5917a fix(desktop): render images in workspace markdown preview
The MarkdownRenderer security hardening strips every img src that is not
blob:/data:, which is right for untrusted assistant output but also broke
the workbench file preview: relative images lost their src and remote
badges never loaded. The document preview is user-owned local content, so
give MarkdownRenderer an opt-in resolveImageSrc hook and wire it in the
workspace MarkdownSurface: relative sources resolve against the markdown
file's directory and are served through the sandboxed /preview-fs (or
/local-file for absolute paths and workspace escapes), http(s) URLs pass
through to CSP, and other schemes stay stripped.
2026-08-02 16:48:30 +08:00
程序员阿江(Relakkes) bbc0405f65 fix(provider): normalize model reasoning effort by capability 2026-08-02 16:48:30 +08:00
程序员阿江(Relakkes) 637b3e5339 fix(desktop): 引导未配置模型的用户完成设置
在无可用供应商或官方登录时显示配置入口,并在发送前阻止创建无认证会话。
同时识别桌面端 Claude OAuth,并补充回归测试与多语言文案。
2026-08-02 16:48:30 +08:00
程序员阿江(Relakkes) c7b6522d79 feat(provider): 新增玄枢API 供应商并加入 README 赞助商
- provider presets 新增 xuanshuapi(Anthropic 兼容,auth_token 鉴权)
- 默认模型 claude-opus-5 / claude-sonnet-5 / claude-haiku-4-5
- README 中英文赞助商表格新增玄枢API 行及 logo,专属链接注册赠 5 美元
- docs/start/models.md 双语预设清单同步
2026-08-02 16:48:30 +08:00
Relakkes Yang f332057b88 fix(windows): handle tab edits and missing Git Bash #1168
Preserve file indentation across tabs and spaces, route command tools to an available Windows shell, and make the source CLI launcher cross-platform.
2026-08-02 16:45:19 +08:00
程序员阿江(Relakkes) 5f8bae08db feat(desktop): support batch element selection #1164 2026-08-01 14:32:19 +08:00
程序员阿江(Relakkes) fde63d94e7 feat(desktop): add model selector search 2026-08-01 13:21:04 +08:00
程序员阿江(Relakkes) 2fbaae30a7 fix(mcp): respect project scope in desktop settings #1165 2026-08-01 13:08:40 +08:00
程序员阿江(Relakkes) d8c6456c77 feat(chat): streamline conversation navigation #1068 2026-08-01 12:30:45 +08:00
程序员阿江(Relakkes) 888a5241f4 fix(activity): reconcile stopped agents and task updates 2026-08-01 12:16:46 +08:00
程序员阿江(Relakkes) 124f876d1f fix(chat): prevent stopped turns from reviving #1161 2026-08-01 01:48:19 +08:00
程序员阿江(Relakkes) 7d9a4954b0 fix: avoid double-counting fork token usage #1159 2026-08-01 01:36:37 +08:00
程序员阿江(Relakkes) 44137ccb91 fix(desktop): stop built-in updates from tripping the installer legacy-data guard #1160
The NSIS installer spawned by quitAndInstall() inherits the app process
environment, including the app-managed CLAUDE_CONFIG_DIR /
CC_HAHA_APP_PORTABLE_DIR pair that applyStartupPortableMode() derives
from app-mode.json. The installer's recovery helper then re-validated
that snapshot against the persisted mode it reads via its own APPDATA
and blocked the whole upgrade on any disagreement (mode switched without
a restart, APPDATA differing from the app's known-folder view), even
though an active data directory outside every install directory cannot
be touched by removing the old version. Manually launched setups never
saw the variables, which is why they kept working.

- clear the app-managed portable env before handing off to the spawned
  installer (shared with the existing app.relaunch() cleanup), so
  built-in updates present the same clean environment as a manual setup
- downgrade the recovery helper's managed-mode consistency check from
  fail-closed to treating the directory as externally managed; the
  install-contained legacy data guard below it still refuses unsafe
  removals, and matching persisted modes behave exactly as before
2026-08-01 01:35:50 +08:00
程序员阿江(Relakkes) 867745c5ae fix(compact): stop pinning small-context models at 1M so auto-compact fires #1162
Auto-compact never fired for models like k3-256k once the window was
resolved as 1M: the threshold (window - 20K - 13K) sat at 967K while the
provider hard-capped at 256K, and the overflow surfaced as a misleading
401 "Please run /login" with no way to recover short of a new session.

- Per-model configured context windows now win over the [1m] marker;
  the marker still wins over built-in table entries so official 1M
  models keep working.
- CLAUDE_CODE_AUTO_COMPACT_WINDOW can only lower models with a known
  window (configured/built-in/[1m]/Codex catalog); unknown models keep
  the raise-above-default behavior. Drop the hardcoded 1M from the
  deepseek/zhipuglm/minimax preset defaultEnv — their 1M models are
  already covered by modelContextWindows.
- tokenCountWithEstimation skips placeholder all-zero usage instead of
  anchoring on it (a ~300K conversation used to count as ~1 token when
  a proxy emitted zeroed usage).
- Recognize third-party context-overflow wordings, including
  401-wrapped ones, as prompt-too-long: /compact's head-truncation
  retry now applies and such 401s are no longer retried 10 times.
- Desktop: the 1M checkbox gets a tooltip and hint, unticking rolls the
  auto-filled 1,000,000 back to the preset window, and the auto-compact
  window description matches the only-lowers semantics. The context
  input helpers move to lib/providerModelContext.ts with unit tests.
2026-07-31 22:15:09 +08:00
程序员阿江(Relakkes) aa2b6d6e69 fix(provider): match MiniMax regional endpoints #1163 2026-07-31 20:26:23 +08:00
程序员阿江-Relakkes 902350606d Merge pull request #1163 from octo-patch/octo/20260731-parameter-refresh-recvqKlqBpA3Ku
fix(provider): add MiniMax regional endpoints
2026-07-31 20:21:01 +08:00
octo-patch 85cf108140 fix(provider): add MiniMax regional endpoints 2026-07-31 19:44:04 +08:00
OpenClawBot eb6558a086 docs: share English browser preview across locales 2026-07-31 19:17:41 +08:00
OpenClawBot 7e31914564 docs: restore original browser previews 2026-07-31 18:53:44 +08:00
OpenClawBot 107bfc86bc docs: refresh bilingual product screenshots 2026-07-31 18:14:54 +08:00
程序员阿江(Relakkes) 3df7f23b25 docs: add bilingual desktop screenshots 2026-07-31 07:32:07 +08:00
程序员阿江(Relakkes) 8280eab844 release: prepare v0.5.1 v0.5.1 2026-07-31 05:30:28 +08:00
程序员阿江(Relakkes) dfb3e350d8 fix(security): restore features blocked by request hardening 2026-07-31 05:13:38 +08:00
程序员阿江(Relakkes) db8cae2b77 fix(desktop): open the run-location menu on the view that has something in it
Choosing a directory in a fresh session cost two clicks. The first opened the
run-location pill's menu, the second opened a directory picker nested inside
it — and in that state the menu in between held exactly one row.

That menu was built around a repo it already had: directory, branch and both
worktree modes. With no workDir yet `isGitReady` is false, the latter two are
gone, and the root view collapses to a lone "Directory" row whose only job is
to open a second dropdown. A directory that is not a repo collapses the same
way.

The directory list is a view of this menu now, symmetric with the branch list:
the menu opens on `root` when there is a repo to describe and on `directory`
when there is not. Its back crumb appears only once `isGitReady`, because
going back without one lands on the single-row shell this exists to skip.

Picking a repo holds the menu open and swings back to the root view, where the
branch row and worktree cards have just appeared. They were invisible before
to anyone who did not go looking for them, since picking a directory closed
the whole menu. A plain folder has no next step, so it closes instead, and a
skeleton holds the height while the context request is in flight.

Resolving that pending pick needs the identity check `context.workDir ===
revealAfterPick`. On the render right after a pick, `context` still describes
the previous directory and `loading` has not flipped yet, so a bare `!!context`
judges the new folder by the repo it replaced — and clears the pending state,
so nothing ever corrects it.

Extracting the list as `RecentProjectsPanel` also drops the nested-picker
outside-click exemption (no second portal left to exempt) and the `menuitem`
variant (no callers left). The native folder dialog now funnels through the
same selection path it was bypassing: it called `onChange` directly and left
the recent-project cache stale.
2026-07-31 04:24:42 +08:00
程序员阿江(Relakkes) 1eae54ad5c feat(desktop): send from a round arrow button in the composer
The composer's send button was a 112px pill reading "run" with a trailing
arrow, in a toolbar where every other control is already icon-only and where a
model picker and a location chip have to fit beside it. It is now the round
up-arrow the rest of the category uses. The arrow points into the transcript
the message is going to, which is what lets it read as send with no word next
to it; the name lives in aria-label and the tooltip, at every width.

Both composers change together — the in-session one and EmptySession's. They
were already deliberate mirrors of each other, and the note in ChatInput
recording why is now the note explaining the shape.

Adds shape="circle" to Button rather than a tone to IconButton, which already
has a circle and forces a label. Two things ruled IconButton out. It dims to
opacity-50 when disabled, and half-transparent ink over the page ground reads
as "still loading" rather than "not available" — the exact reason primary
carries an opaque disabled fill, and the send button sits disabled whenever
the input is empty. And --color-inverse-surface is already btn-primary's own
--cc-t1, so reproducing primary's colors there would have forked that
definition in two, past the token contrast guards.

No new colors: primary is --cc-t1 on --cc-bg, which inverts on its own. The
four paper themes give a dark ink circle with a pale arrow, dark and ink-blue
a pale circle with a dark one. Measured across all six, arrow-on-circle runs
12.70:1 to 16.98:1, and hover resolves to --color-brand throughout.

The radius moves out of BASE_CLASSES so exactly one rounded-* is ever emitted;
the component does no Tailwind conflict resolution, so keeping both would have
left the shape to stylesheet order rather than to the prop (AGENTS.md 3.6).
A test pins that.

Drops iconOnlyAction and the 610px threshold behind it. It existed to shed the
run button's label before the location degraded, and there is no label left to
shed, so the location is now the only thing that degrades.

Also covers the stop button, which had no test at all: send and stop are one
control that swaps role, so a round send becoming a pill on stop would shift
the whole toolbar every time a turn starts.
2026-07-31 04:18:01 +08:00
程序员阿江(Relakkes) a7e9074d07 fix(desktop): keep the active tab whole when the strip resizes under it
The rightmost tab lost its right edge whenever it was the active one, close
button included. Not merely hidden either: the button's centre sat past the
strip, so elementFromPoint there returned the toolbar's terminal button and
that tab could not be closed at all.

The activation scroll is the cause, but only together with the chevrons. They
are w-7 siblings of the scroll region, so the moment updateScrollState decides
the strip overflows they take 28px each out of a flex-1 region — after
scrollIntoView has already landed on a scrollLeft computed without them, and
scrollLeft does not follow a layout change. Measured on a 1280px window with
seven tabs: the scroll stopped at 108 when the reachable end had moved to 164,
and the tab lost exactly those 56px.

So keeping the active tab whole cannot be a one-shot on activation; it is an
invariant the strip has to re-establish whenever its own width changes. It now
runs off the ResizeObserver that was already there, which covers window
resizes, sidebar drags and the toolbar's conditional buttons for free.

Guarded on whether the user has driven the strip themselves, not on the strip
getting narrower. Width was tried first and is wrong: a chevron retires when
its end is reached and rejoins when it is left, so a plain chevron press
narrows the strip mid-flight and is indistinguishable from the layout event
being guarded against. Measured, that snapped the view straight back and made
the left end unreachable. Switching tabs hands the position back — the user
has just named a tab they want to see.

Four tests, two of them pinning the guard rather than the fix. Sentinel runs
confirm both halves: removing the realign reddens two, removing the guard
reddens the one written for it. The ResizeObserver mock now records its
callback, since jsdom lays nothing out and the geometry has to be stubbed
alongside it.

Walked through against a live strip: rightmost tab clip 56px -> 0, close
button reachable again, left end reachable, sidebar expand re-aligns by 136px.
2026-07-31 04:17:35 +08:00
程序员阿江(Relakkes) 358f3e5806 fix(desktop): put the settings rail on paper so the tab meets its content
The rounded tab's whole premise is that its paper fill runs unbroken into
the view it opens onto. Settings was the one place that did not hold: the
selected white tab sat directly on a grey panel and read as a card
stranded on it.

The cause is not in the tab strip. The settings rail is painted
--color-surface-container-low, which resolves to the same --cc-s0 the
strip's trough does, so the trough appeared to wrap around the corner and
continue down the left of the page with the tab marooned in it. Every
other page — chat, market, scheduled, trace, subagent — is already paper
at the top, which is why only this one showed.

So the rail moves onto paper and the existing border-r does the
separating, which is what every other secondary panel in the app already
does (the workbench, the diff split). The rail was the odd one out.

That also fixes something nobody had flagged: the selected rail entry is
--color-surface-hover, which against the old grey was 1.06:1 — a
highlight you cannot see. On paper it is 1.15:1.

ring-offset moves with it. It is a painted ring, not a transparent one,
so naming the old fill would have drawn a grey collar around the focus
ring on a white rail.

Two alternatives rejected: --color-surface-container (#FAFAF8) measures
1.045:1 against paper — indistinguishable, so it buys a special case and
nothing else; and having the tab's fill follow whatever page is below it
would leave Settings' tab with nothing but an outline, dropping the
selected state entirely.
2026-07-31 03:13:00 +08:00
程序员阿江(Relakkes) 21efd1cf92 feat(desktop): round the tab corners and outline the selected tab
Tabs were hard to tell apart. The strip, the selected tab and the content
below it sit within 1.05–1.10:1 of each other in all six themes, so the
only thing marking selection was a 3px terracotta rule along the bottom —
and nothing at all separated two idle tabs from each other.

The obvious fix is Chrome's: darken the strip into a trough so the paper
tab pops out of it. That is the wrong trade here. The strip is continuous
with the sidebar, and darkening it turns the titlebar into a separate band
running across the top of the window instead of the same surface the
session list is already on. So the trough stays exactly --cc-s0, the fill
contrast stays where it was, and the shape is carried by two new per-theme
tokens instead:

- --cc-tab-edge (1.35:1 against the trough) outlines the selected tab.
  Without it the 8px top corners do not resolve at all — a curve is drawn
  by a colour boundary, and on 素白 there is none. This was measured, not
  assumed: a radius-only build is indistinguishable from the flat strip.
- --cc-tab-sep (1.22:1) is the hairline between two neighbouring idle tabs,
  and the same rule now divides the toolbar off (it was a full-height
  border-l, which read as a different kind of divider next to 16px ones).

Neither can reuse --color-border or --color-outline. Both are calibrated
against paper; on the trough they land at 1.12:1 (invisible) and 1.36–2.01
(a drawn box), in opposite directions between the light and ink families.
Both new tokens go *lighter* than the trough on dark and ink-blue for the
same structural reason --color-surface-hover was already banned here:
their paper is dark enough that pure black is only ~1.26:1 against it, so
there is no room downward.

Hover gets the weaker of the two outlines rather than none, giving three
legible tiers — no outline, hairline, full edge — because the same
1.05–1.10:1 that hides the selected tab's corners hides a hovered tab's.

Two things had to go for the corners to mean anything. The terracotta rule
and the strip's border-b both cut across the bottom edge that now has to
run unbroken into the content the tab opens onto; a rounded tab sitting on
a ruled line is just a clipped rectangle.

Chat tabs also lose the chat_bubble glyph. #1123 asked for icons and got
one on every kind including session, which is most of the strip, so the
row filled with identical bubbles saying nothing the titles did not. The
glyph now means "this tab is not a conversation". The slot animates its
own width rather than collapsing outright, so a session starting up still
does not jump its own title sideways — the bug the fixed slot was added
for. Spacing moved to per-child margin for the same reason: flex gap is
charged between children whatever their width.

contrast.test.ts gains 18 assertions pinning both tokens per theme,
including an upper bound on the hairline — too strong and the strip reads
as a table of cells. The three TabBar guards written against the old
square shape (no radius, 52px tab, the terracotta rule) are rewritten
rather than deleted, since what they were protecting — this is a document
tab, not a floating pill — still holds.
2026-07-31 02:50:03 +08:00
程序员阿江(Relakkes) dccd6e1394 chore(desktop): drop the long sidebar wordmark for cc-haha
The header rendered both "Claude Code Haha" and "cc-haha" and hid one
with a container query, so the app answered to two names depending on how
far the sidebar had been dragged. Keep the short form at every width and
remove the node rather than hide it — a display-hidden copy still reaches
screen readers and in-page search.
2026-07-31 02:16:20 +08:00
程序员阿江(Relakkes) b6c2241532 fix(chat): key composer toolbar layout to column width
The composer took `compact` straight from "is the workspace panel open",
so opening the panel dropped the run location to a second line and shrank
the permission mode to a bare icon on columns with hundreds of pixels to
spare. The panel is resizable and the window is not fixed, so its open
state says nothing about the width the composer actually got.

Measure the shell instead and degrade in two steps: the run button gives
up its label at 610px, the location leaves the toolbar at 530px. The
numbers come off the shipped toolbar with the longest mode label. Within
the location chip the branch now yields width before the project name,
which used to truncate both at once into `cc-…/…n`.
2026-07-31 02:16:20 +08:00
程序员阿江(Relakkes) ad532c4dc8 fix(desktop): stop dropping confirmed preview selections
Confirming the edit bubble did nothing: the page emitted picker-exited
before selection, so both host-side guards added by 8ec8833be disarmed
first and threw the selection away — the main process returned before
even capturing, and the renderer's pickerActive check would have dropped
it too. selection already implies "this pick is over" and both hosts
reset their picker state on it, so the confirm path now only cleans up
locally; picker-exited stays on the cancel/abort paths that produce no
selection. The authorization semantics are unchanged.

The bubble is also rebuilt: colour fields get a picker swatch plus hex
(with a chequerboard for transparent), opacity becomes a slider, font
becomes a select, styles move to a constructable stylesheet so a strict
style-src CSP can no longer blank them, and the panel follows the system
colour scheme. Text editing now reads and writes .value on form controls
— it was always blank on inputs — and is withheld from containers where
it would flatten the subtree.

Also fixes opacity '0' being swallowed as falsy in applyEdit, and a
phantom diff when the picker re-picked an already-set colour.

The confirm path had no test at all, which is how the ordering bug
shipped; add page-level coverage of the real message sequence plus
ordering contracts on both hosts.
2026-07-31 02:07:50 +08:00
程序员阿江(Relakkes) d7081c4245 feat(chat): link file references in message text #1146
A file path the assistant printed in prose was inert text, so the only
way to reach the file it named was to retype the path into the file tree.

The reported request — "recognise file paths in the body" — understates
what was missing. src/constants/prompts.ts:437 already instructs the
model to write file_path:line_number "to allow the user to easily
navigate to the source code location", and :438 to write owner/repo#123
"so they render as clickable links". The model has been holding up both
ends all along; neither had an implementation on the desktop side.

This is the second half of #1145 and reuses its four layers: boundaries
live in a pure filePathBoundary module beside urlBoundary, marked
integration joins markdownAutolink, clicks inherit openPreviewLink
unchanged, and the styling follows prose-a and md-code-link.

Boundaries invert #1145's approach. A URL is matched permissively then
trimmed, while a path segment is an allow list, so prose punctuation
cannot leak into a path the way a sentence leaked into an href. CJK is
excluded from that list on purpose, which costs 文档/说明.md: allowing it
would make 修改了lib/foo.ts match from 修 and drag the verb into the path,
and Chinese running flush against an ASCII path is far more common than a
CJK filename. Extensions are gated by one set shared with
previewLinkRouter, so anything underlined in the prose is guaranteed to
have a route that opens it — the old private list in that module was
missing .yml and .ps1, which classified as ignored and did nothing when
clicked. A bare foo.bar additionally requires an extension that cannot be
read as a property access, since console.log, array.map and process.env
are otherwise indistinguishable from filenames.

Windows had no working path link at all. new URL('C:\\src\\app.ts')
succeeds with protocol 'c:', so every drive path fell through to ignored
— on the platform this issue was filed from. The drive check now runs
before URL parsing.

References carry their target in data-* rather than href. The sanitizer's
ALLOWED_URI_REGEXP reads a leading word: as an unknown scheme, which
strips exactly two shapes: foo.ts:42 and C:\src\app.ts. Widening that
pattern is not an option — it is what keeps javascript: out — so the
anchor gets role and tabindex for the affordances an href would have
provided, and fileRefFromElement rebuilds the reference on click, leaving
classifyPreviewLink the single parser for both these anchors and
hand-written markdown destinations.

A reference becomes a link only where a click can be handled. Eleven of
this component's thirteen callers — release notes, agent prompts, thinking
blocks, plan previews, the markdown file preview — pass no handler, and a
link there looks live and does nothing. That same condition closes the
streaming gap: MessageList's streaming renderer gets no sessionId, hence
no handler, so a paragraph the model finished before calling a tool stays
plain even though chatState has already left 'streaming'.

Bare paths additionally wait for the text to be final. A path has no
closing delimiter, so mid-stream desktop/src/lib/foo.ts is itself a valid
reference that changes again as x and :42 arrive, flickering through three
targets on one line. This is why recognition runs on the DOM after
sanitizing instead of as a marked tokenizer: marked.use is module-level
and cannot be gated per render. Working on the DOM also puts URLs out of
reach, since they are already anchors by then, so the github.com/a/b.ts
inside an href can never be re-matched as a path. Inline code needs no
such wait — an unclosed backtick is not a codespan, so the reference is
whole by the time it renders — and because renderCodespan sits on marked's
shared renderer and cannot see whether a surface handles clicks, its
anchors are unwrapped on the surfaces that do not.

Clicking reveals the referenced line rather than only opening the file.
The preview truncates past WORKSPACE_PREVIEW_LINE_LIMIT, so a reference
beyond the fold points at a row that was never rendered and would
silently do nothing; the reveal expands first, and its effect is declared
after the reset effect so a reload cannot collapse it again. The mark's
load-bearing part is an inset rule in --color-brand: measured across all
six themes, every soft fill lands between 1.02 and 1.11 against
--color-code-bg — 1.05 for --color-brand-soft in warm-classic, the
default — so no tint in this palette can carry it alone. contrast.test.ts
guards the rule, the same way #1145 guards the underline.

Right-clicking a reference opens the menu the output cards and file tree
already use, extended with copy-path and copy-contents. The file tree
passes omitCopyPath because it renders its own pair above that block. The
store wiring those two call sites each spelled out is now a single
openWithMenuItems; only the wiring is shared, not how targets are fetched,
since the tree holds them from a selector and must stay synchronous or its
menu renders a frame short.

Mistyped references are not pre-validated, so nothing is silently left
unlinked; a path that does not exist opens and reports "file not found",
which the preview already handled.
2026-07-30 21:11:47 +08:00
程序员阿江(Relakkes) 224a076ba7 chore(sponsor): 下线胜算云赞助,退役预设改为 deprecated 墓碑
胜算云不再赞助,从 README 与「添加服务商」入口下架。与 558124914 下线接口AI
的做法不同,这次不删预设条目——纯删会静默降级已配置该预设的 provider:

- defaultEnv 从不随 provider 持久化,每次运行都从预设重算(胜算云的
  API_TIMEOUT_MS、CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 与
  ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES=none)
- getManagedEnvKeys() 用所有预设的 defaultEnv 键构建 settings.json 擦除清单,
  而 API_TIMEOUT_MS 与 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 仅由胜算云声明,
  删掉后这两个键永久脱离清单,陈旧值会泄漏进此后激活的每个 provider
- provider 卡片 Badge 渲染预设名,预设消失则已配置的卡片标签消失
- 较早的存档可能连 authStrategy / modelContextWindows 都未持久化,同样回退到预设

改动:

- providerPresets.json:胜算云标 deprecated,摘掉 featured / promoText / apiKeyUrl;
  一并补回 jiekouai 墓碑条目,修掉 558124914 留下的同类回归(该提交未进任何 tag,
  回归尚未发布)
- 过滤只加在新增路径:桌面端预设 chips 源,以及粘贴 settings JSON 后按
  ANTHROPIC_BASE_URL 自动切预设那处;按 presetId 反查的路径一律保持全量
- GET /api/providers/presets 仍返回全量并携带 deprecated 标记,由消费方自行过滤
- ccSwitchImport 的 matchPresetId 有意继续匹配退役预设:导入映射的是用户既有配置,
  继承该预设的 env 比落到 custom 更正确
- README 中英文移除胜算云赞助整行与推广链接,删除 shengsuanyun-logo.svg,
  中英文 docs 模型页的中转服务商清单去掉胜算云

测试:settings.json 擦除清单不变量、退役预设仍解析出完整运行时 env、退役预设不得
残留推广字段、接口AI 的第三方 Sonnet 护栏(capabilities=none 与 1M 上下文窗口)、
桌面端 selectableProviderPresets 过滤。
2026-07-30 21:11:47 +08:00
程序员阿江(Relakkes) 12c01b7c17 fix(desktop): echo shell command output in the transcript #1149
Bash/PowerShell cards printed the command three times (collapsed header,
terminal card, Tool Input JSON) and never printed its output. Echo the
command's output into the terminal card and drop the duplicated JSON.

- Render shell output as plain preformatted text, head-windowed to 12
  lines behind a toggle. Errors are never windowed, preserving #625.
- Recognise the CLI's `(<Tool> completed with no output)` substitution
  (toolResultStorage.ts, inc-4586) instead of testing for empty content:
  empty content never reaches the desktop, so the marker would otherwise
  be rendered to the user verbatim.
- Show only the non-echoed input keys as JSON, so `timeout` and
  `run_in_background` stay visible without reprinting the command. An
  all-or-nothing rule left the command triplicated for the ~20% of real
  calls that carry a `timeout`.
- Resolve terminal control sequences: carriage-return overwrites, and the
  full CSI and OSC families. Stripping only SGR colour codes left erase
  and cursor sequences to print literally, and a carriage return left at
  a collapse boundary added a blank line on Windows.
- Distinguish an image/structured result from a silent one, so an
  image-only command is not labelled as having printed nothing.
- Add a duration badge. Note this measures the transcript gap, which
  begins when the model starts streaming the tool input; the CLI does not
  report execution time over the wire, and parallel calls in one batch
  share a single tool_result timestamp.

Read, Edit and Write results stay suppressed as before.

Tested: bun run check:desktop (281 files, 3540 passed, 1 skipped, build ok)
Tested: mutation-tested the new assertions to confirm they fail when the
  corresponding logic is reverted
Tested: real-render walkthrough against live CLI payload shapes (marker
  string, CRLF output, ANSI progress frames)
Confidence: high
Scope-risk: narrow
(cherry picked from commit 618a9a063e189d49efa7f27d36c2b33cc67c7aed)
2026-07-30 21:11:47 +08:00
程序员阿江(Relakkes) ba7e3b0094 fix(chat): link bare URLs in message text #1145
A URL the assistant printed in prose was either not a link at all or a
link pointing somewhere else, and the only clickable copy lived in the
output card below the reply. Five separate causes, four of which only
show up in Chinese text.

The reported cause — "gfm autolink is off" — was not one of them; it has
always been on. What is wrong is where GFM ends an autolink: the spec
trims only ASCII trailing punctuation, so a full-width mark or a Han
character right after the URL is treated as part of it. "打开
http://localhost:5173,然后刷新页面" became a single link whose href
carried the rest of the sentence, and clicking it loaded a 404 in the
workbench browser. The output card was unaffected because
assistantOutputTargets already excluded CJK punctuation — which is
exactly why the card looked like the only thing that worked.

Boundaries now come from a shared urlBoundary module built on two
structural rules: after the authority a URL can only continue with / ? #,
so a Han character there ends it; and inside the path CJK letters are
legal — /文档.html is a real path — while CJK punctuation is not. It is
installed by overriding marked's `url` tokenizer, which falls back to the
built-in one for schemeless www. hosts and bare email addresses. IPv6
literals are left to that fallback and stay plain text as before:
marked's cleanUrl percent-encodes the brackets, and the resulting href
fails new URL(), so classifyPreviewLink would route it nowhere.

The prompt bubble rendered raw text, so a URL the user typed was never
clickable. It now splits bare URLs out and wraps those, without going
through the markdown renderer — a prompt is literal text, and `**`, `#`
and file paths have to survive as typed.

Inline code that is nothing but a URL is now a link and keeps its code
chip. `curl http://localhost:3000` is a command, not a link, and stays
plain code.

Links also could not be recognised as links. The accent measures only
1.95–2.55:1 against body text across all six themes, so in dense Chinese
prose an unadorned link is indistinguishable from the sentence around it
— the reported "same colour as the body text" was literal. Links now
carry a resting 1px underline in the accent, and theme/contrast.test.ts
guards that it stays visible: the first attempt used
--color-primary-fixed-dim, which measures 1.49–1.76:1 against the page
and would have shipped an invisible fix.

On the CLI the OSC 8 machinery was already in place; bare URLs just never
reached it. Markdown's fast path skips marked.lexer entirely when it sees
no markdown markers, and a plain Chinese sentence carries none — so the
URL rendered as dead text, while the same sentence in English usually
contained a hyphen and linked fine. Content carrying a scheme now always
reaches the lexer, scanned across the whole string rather than the
500-character sample, since a summary commonly puts its dev-server URL in
the closing line. OutputLine's linkifier had the same ASCII-only
boundary flaw and now shares urlBoundary.

Routing is unchanged: the markdown body's anchors already went through
handlePreviewLink, so loopback URLs open the workbench browser and remote
ones the system browser. The store wiring that AssistantMessage and
AssistantOutputTargetCard each spelled out separately is now a single
openPreviewLink, which the prompt bubble reuses.
2026-07-30 21:11:47 +08:00
程序员阿江(Relakkes) 3e57b8c834 feat(chat): stamp finished turns with completion time and duration #1151
Reading "when did this finish" and "how long did it take" required
hovering the reply, which a touch user cannot do at all and which nobody
thinks to try on a turn that ran for twelve minutes.

The turn's last reply now carries an always-visible stamp under it —
"Done 15:20 · took 12m 19s" — while every other message keeps the
hover-only timestamp it had. That reply drops its own hover chip, since
the stamp already states the same time a line above it.

A turn is stamped only when it ends on a reply. Turns whose last reply is
a mid-turn aside followed by more tool calls are left alone: the stamp
renders under the reply, so it would sit above the work it introduced.
Trailing task summaries and background-task cards are the exception —
the summary is written on the next send and background work is detached,
so neither means the answer was still coming. The running turn is never
stamped, queued prompts do not close the turn that is still streaming,
and a span above a day drops the duration and keeps only the end time
(that is a session resumed the next day, not a model that thought for
eighteen hours).

Durations reuse the existing chat.duration.* strings rather than a
fourth private formatter, and StreamingIndicator now shares them so the
same turn does not read as "12m 19s" while running and "12 分 19 秒"
once done. Those strings gained an hours tier: a long turn used to
render as "75 分 30 秒".

History goes through the same derivation as live turns, so reopening an
old session shows the stamps too.
2026-07-30 21:11:47 +08:00
Relakkes Yang 486fa1d1ff test(desktop): verify Unicode Windows install paths 2026-07-30 18:02:22 +08:00
Relakkes Yang 77046ee485 fix(desktop): ignore 2px WebView resize jitter (#1144)
Treat the full two-pixel WebView2 resize oscillation as layout jitter while preserving accumulated follow behavior for real content growth. Add a regression covering stepwise one-pixel observations across both edges.
2026-07-30 17:47:08 +08:00
程序员阿江(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