587 Commits

Author SHA1 Message Date
程序员阿江(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)
7ed30d08f5 fix(cli): avoid quadratic long-session normalization #1100 2026-07-28 22:22:44 +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)
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
5058efa44a
Merge pull request #1053 from leehom0123/fix/openai-responses-read-image
fix(proxy): 保留 Responses 工具结果中的图片
2026-07-28 00:22:36 +08:00
程序员阿江(Relakkes)
09120df85a fix(stats): count one assistant reply once instead of once per content block
Claude Code writes an assistant message as one JSONL line per content
block — thinking, text, and each tool_use separately — and every one of
those lines repeats the same complete `usage` object. Both stats paths
summed them, so a reply cost as many times as it had blocks. One real
message in this repo's own transcripts spans 52 lines carrying 50.8K
tokens, and was counted as 2.65M.

Scanning ~/.claude/projects with 1927 sources: the total drops from
9.76B to 4.59B tokens, and the busiest day from 2.60B to 1.46B. 39,290
of 69,306 usage lines were repeats.

Usage records now deduplicate on (message.id, requestId), scoped per
transcript, and survive an incremental read. Lines with no message id
are still counted, matching ccusage.

Two paths compute these stats — the local index reducer and the direct
scan in stats.ts — and a parity test pins them to identical output. Both
carried the bug, so the rules that decide what counts now live in one
module, `utils/usageAccounting.ts`, rather than being written twice and
drifting.

Also fixed, all surfaced while verifying the above:

Cost was dead code. `costUSD` and `webSearchRequests` were initialized
to 0 in the reducer and never assigned, so 0 travelled to SQLite and out
again. Now estimated from the rates in modelCost.ts — but with an
unknown model returning null instead of falling back to the default
model's rates, because 12.7% of the tokens here come from third-party
providers (k3, glm, MiniMax, deepseek, grok) that would otherwise be
billed at Claude prices. Those models keep their tokens in the activity
totals and are named in `unpricedModels` so the UI can say the dollar
figure is a floor.

`MODEL_COSTS` has no entry for claude-opus-5 — the canonical-name
resolver maps it to `claude-opus`, which isn't a key either — so the
largest model by usage priced through the unknown-model fallback. The
new module handles it explicitly. The CLI's own calculateUSDCost still
takes that fallback; left alone here, it reaches well beyond activity
stats.

"Longest task" measured `last - first`, a calendar span rather than a
duration: a session resumed the next morning reported the whole night as
time on task. The panel read 436 hours for a 17-message session, and the
worst case on this machine was 1137 hours. Sessions now accumulate
working time across gaps under 30 minutes, stored in a new
`active_duration_ms` column.

Workflow subagent transcripts were never indexed. Discovery read one
level of `subagents/`, but workflow agents nest a group deeper at
`subagents/workflows/<id>/`; 79 files here were invisible to both the
index and the direct scan, which additionally attributed them to a
session named "workflows".

Ties in "longest session" now break on session id. The two paths iterate
sessions in different orders, and ties became reachable once an
out-of-order session scored 0 rather than a distinct negative span.

The parser version bump is what makes any of this reach an existing
install: `detectSourceChange` rebuilds a source only when its stored
version differs.
2026-07-27 03:35:12 +08:00
程序员阿江(Relakkes)
cbef0360e2 fix(grok): align the Responses request with what xAI actually accepts
Four gaps against the official Grok CLI surface, found by comparing this
adapter with sub2api's and each confirmed by a probe against the live
endpoint.

The first is not a parity gap but a hard failure. A named tool_choice was
translated into Chat Completions syntax, `{type:'function',function:{name}}`,
which the Responses endpoint refuses:

    422 data did not match any variant of untagged enum ModelToolChoice

so every request where the caller pins a tool failed outright. Responses
names the function inline. Only the Responses transform changes; the Chat
transform keeps the nested form, which is correct there.

The rest:

- A tool_choice sent with no tools, or one whose target the BatchTool
  filter removed, is dropped rather than left selecting a tool the upstream
  never receives — and a tool list that filters down to empty is omitted
  instead of sent as `[]`.
- `prompt_cache_key` and the `x-grok-conv-id` header carry one identity per
  conversation, reusing resolvePromptCacheKey (the Claude Code session id)
  so a turn stops re-billing the whole prefix. Both survive the 401 refresh
  retry: header assembly is folded into one helper so the first attempt and
  the retry cannot drift apart.
- `x-grok-client-mode: interactive` matches what the official client sends.

Probes, in order: baseline 200; the three new fields together 200; the
inline tool_choice 200; the nested form 422.

Also lands the adapter-level regression test for the mid-stream socket
reset fixed in the previous commit — it drives a real reset through
buildGrokFetch and the SSE transform, so it needs both changes present.
2026-07-27 02:13:11 +08:00
程序员阿江(Relakkes)
c4560e76d0 fix(api): retry a stream the transport killed before any tool ran
A turn on the Grok provider died on `API Error: The socket connection was
closed unexpectedly` with partial text already on screen. That message is
Bun's, raised while reading a response body the peer reset: a raw TCP server
that sends chunked SSE headers plus one event and then terminates the socket
reproduces it exactly, as `Error{code:'ECONNRESET'}`.

The reset itself is the network — a stale pooled keep-alive socket, a
proxy/NAT dropping a reused connection, an edge resetting mid-flight — not
the payload. Ruled out against the live endpoint: twelve tools with a 21KB
skill tool_result, an 8-minute 11MB 47k-event generation, and a pooled
connection reused after 130s idle all complete normally.

What was broken is that nothing retried it. The error lands during stream
*consumption*, where all three recovery paths miss it:

- `withRetry` only wraps stream creation, so consumption never reaches it.
- `isRetryableStreamError` requires an APIError carrying `"type":"api_error"`
  inside a 200 SSE body. A dead socket is a bare Error with neither.
- The desktop injects `CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK=1`, so the
  one remaining branch rethrows and ends the turn.

`isRetryableStreamTransportError` therefore walks the cause chain for a
transport code (ECONNRESET/ECONNABORTED/EPIPE/UND_ERR_SOCKET/
ERR_STREAM_PREMATURE_CLOSE) instead of keying on the error type, and
queryModel re-establishes the stream through the existing withStreamRetry
budget.

SSE has no resume, so the retry is a full re-send and is gated on the same
side-effect boundary the watchdog retry uses: StreamAssistantCommitBuffer
holds completed thinking/text unpublished, so they are discarded with the
attempt, but once a tool_use block completes or server-side tool activity
starts the disconnect surfaces to the user instead (#766 / inc-4258).
Keep-alive is disabled only when nothing arrived at all — no `message_start`
means the socket was already dead when the request went out, which is the
stale-pool signature, while a connection that broke later leaves the pool
trusted.

This preempts the non-streaming fallback for these disconnects, the same
trade the existing transient-error branch already makes.

Covered by the guard matrix (each transport code, bare and wrapped in
APIConnectionError; tool boundary crossed; watchdog abort; user abort;
non-transport error) and by withStreamRetry recovering a transport-flavoured
RetriableStreamError — the first one whose payload is not an SDK error
object — including the exhaustion branch that has to render it.
2026-07-27 02:12:33 +08:00
程序员阿江(Relakkes)
eecc873535 fix(mcp): delete servers declared in a parent .mcp.json (#1107)
Project scope reads and writes disagreed about which file owns a server.
getMcpConfigsByScope('project') walks from the cwd up to the filesystem root
and inherits every .mcp.json it finds, so a server declared in an ancestor
directory shows up for a nested project -- with canRemove: true, because the
DTO derives that from the scope alone. removeMcpConfig('project') only ever
read the cwd's own .mcp.json, so it threw a plain Error for exactly those
servers, and deleteServer let it fall through to the errorHandler's 500 catch.
That is the reported pair of symptoms: the delete does nothing and the toast
says "An unexpected error occurred". Windows hits it easily, since dropping
.mcp.json in the home directory (mistaking it for global config) makes every
project below inherit servers that none of them can remove.

removeMcpConfig now resolves the file that actually declares the server, using
the same nearest-first order the reader uses, and edits that file. The edit is
applied to the raw parsed JSON rather than to a rebuilt McpJsonConfig: the old
rebuild ran through parseMcpConfigFromFilePath with expandVars: true, so
rewriting the file baked resolved ${VAR} values into it and dropped every
top-level key outside mcpServers, since the zod schema strips them. Lookup
matches on the raw object too, because validation is all-or-nothing per file --
one malformed entry would otherwise make every server in that file unremovable,
and would leave lookup and rewrite working off different views.

addMcpConfig had the same rebuild, so adding any project-scoped server rewrote
its siblings the same way. Measured against a live server with MY_MCP_TOKEN
set, "Bearer ${MY_MCP_TOKEN}" came back as "Bearer super-secret-value" -- a
plaintext credential written into a file that is normally committed. It now
inserts into the raw object as well, and its duplicate-name check reads the raw
file so a name sitting in an otherwise-invalid .mcp.json is still reported as a
conflict instead of being silently overwritten.

Two smaller corrections follow from the same asymmetry. deleteServer reports
why a removal failed (400 with the underlying message) instead of an opaque
500, and rejects unknown or non-editable scopes with a reason. configLocation
pointed at join(getCwd(), '.mcp.json') for every project-scoped server, naming
a file that does not exist for inherited ones; it now names the declaring file,
which is what tells a user where the server came from.

getProjectMcpConfigsFromCwd() has no callers left. Kept rather than deleted --
it is vendored upstream code and removing it widens future sync conflicts --
with its comment corrected, since it claimed to serve add/remove.

Verified over real HTTP against a server with an isolated CLAUDE_CONFIG_DIR,
not only through handleMcpApi: before the change the delete returned 500 with
the generic message and left the file untouched; after it returns 200, removes
the entry from the parent file, creates no .mcp.json in the cwd, and preserves
both $schema and the unexpanded ${MY_MCP_TOKEN} of a sibling server. The add
path was confirmed the same way. check:server: 226 files, 2405 tests, 0 failed.
2026-07-27 00:47:51 +08:00
程序员阿江(Relakkes)
ef71b9f622 fix(server): return tool denials to the model instead of aborting the turn (#1051)
Desktop denials answered the CLI with `interrupt: true`, which
permissionPromptToolResultToPermissionDecision turns into
toolUseContext.abortController.abort(). query.ts then hits its post-tool abort
check, yields the interruption marker and returns `aborted_tools` -- the denial
was written to the transcript, but no follow-up request was ever made. The
model never learned it had been denied and never got to reply, so a rejected
tool ended the turn in silence.

#1051 named the root cause directly, and it was not a missing abort: the denial
text was 'User denied via UI', a debug string the model cannot act on, where
upstream sends REJECT_MESSAGE ("... STOP what you are doing and wait for the
user to tell you how to proceed."). Aborting suppressed the reported symptom --
the model could not go looking for a workaround because it was never asked
again -- while leaving in place a denial it still could not read. Send the copy
the CLI itself uses and the instruction lands where it belongs.

ExitPlanMode carries the opposite meaning: rejecting a plan is "keep planning",
so it must not receive REJECT_MESSAGE's STOP. It now gets copy saying the plan
was rejected and should be revised, in place of that same debug string.
Deliberately not PLAN_REJECTION_PREFIX -- its contract is the prefix followed
by the full plan text, and both renderers already read the plan from the tool
input (the CLI's renderToolUseRejectedMessage, the desktop's
extractPlanPreview), so reusing it would only echo the model's own plan back
into context. Plan feedback gets the same framing instead of being passed
through bare, where "add rollback steps" reads as a go-ahead.

REJECT_MESSAGE and REJECT_MESSAGE_WITH_REASON_PREFIX move to the
dependency-free constants/messages.ts so the server can share the CLI's exact
wording without pulling utils/messages.ts and its analytics deps into the
sidecar. utils/messages.ts re-exports them, leaving existing callers untouched.

Verified end to end against a mock upstream driving the real server and a real
CLI subprocess: the model is asked a second time, that request carries the
denial as tool_result, and the model replies. Restoring `interrupt: true` leaves
the upstream with a single request and no closing reply, which is the reported
symptom. A denied Write reaches the filesystem in neither case.
permissionPromptToolResultToPermissionDecision had no coverage at the point
where it decides to abort; it does now.
2026-07-26 02:39:51 +08:00
程序员阿江(Relakkes)
7d2a8a3cdb fix(server): keep loopback trusted without the desktop process token
4c3b08c0 made `isLocalTrustedRequest` return early on
`localAccessTokenConfigured`, so once the desktop shell injects
CC_HAHA_LOCAL_ACCESS_TOKEN the loopback address stops being evidence of
anything -- only the token grants trust. The system browser never has it.
`shell.open`ing the Grok OAuth success page landed on
{"error":"Unauthorized","message":"Missing H5 access token"} instead of
the success HTML, and /preview-fs links, /local-file links and plain curl
went the same way. With H5 access off it was worse: 403 "H5 access is
disabled", which no H5 token can clear.

The classifier had no way to tell the two apart. A scenario matrix over
the policy functions returned the identical verdict for "system browser
opens the OAuth page" and "malicious site fetches 127.0.0.1" -- the rule
bought its safety by refusing every request that is not the app itself.

What that early return actually added over the checks below it was one
case: a reverse proxy on this machine perfectly mimicking loopback. A LAN
client is already rejected on clientAddress, a cross-site fetch on Origin,
and a tunnel forwarding its original Host on isLoopbackHost(url.hostname).
Reaching the remaining case means running a process on the user's box,
where ~/.claude is readable anyway. Loopback goes back to being trusted on
its own.

The boundary worth keeping is narrower than the whole surface, so
requiresLocalAccessCredential pins it to /api/h5-access: another program
on this machine must not be able to publish the user's sessions to the
network. /api/h5-access/verify stays open -- checking a token the phone
already holds is not a control-plane change -- and a server with no token
configured does not lock itself out.

Restoring loopback trust re-exposed an older hole that the blanket rule
had been covering: `if (!origin) return true` also accepts cross-site
subresource loads, since <img src="http://127.0.0.1:3456/api/...">
carries no Origin. Fetch Metadata separates them -- a real navigation
sends Sec-Fetch-Mode: navigate, a subresource does not -- and clients
that send no Fetch Metadata at all (curl, adapters, the CLI subprocess)
are not a browser CSRF vector, so they stay trusted. This is net new
protection; it existed neither before 4c3b08c0 nor after.

desktopRuntime resolved the token inside a Promise.all with getServerUrl,
so an IPC failure took the whole startup down with it. It degrades to
null now, which is the same principle: the token raises what the shell
may do, it is not a precondition for running.

Verified end to end against a server started with the token injected: the
OAuth success page returns 200 and its HTML, /api/status, /api/sessions
and /api/adapters return 200 tokenless, while the control plane, an <img>
subresource, a cross-site fetch and a proxy hop all return 403.
check:server is 224 files / 2379 passing, desktop 225 files / 2522.
2026-07-26 01:08:10 +08:00
程序员阿江(Relakkes)
4be4d3a4ca fix(skills): resolve a skill name to one file across every root
A skill name is a single slash command, but three code paths disagreed on
which file it pointed at once the same name existed in more than one root.

The CLI loader deduped by `${scopeKey}\0${name}`, so it only collapsed a
collision inside one scope. Across scopes both entries survived, and the
slash command menu has no dedup of its own -- typing `/` listed two identical
rows, and only the first was ever executable.

The desktop menu builds its list separately, in listSkillSlashCommands, where
a plain `byName.set()` made the last writer win. Its input runs user roots
before project roots, so the project copy took the name. The kernel resolves
the same call through findCommand, which takes the first match and therefore
picked the user copy. A user with `deploy` in both ~/.agents/skills and the
repository's .claude/skills read the project description in the menu and ran
the .agents body on click.

Both paths now share outranksClaimedSkill: `.claude` outranks `.agents` in
every scope, including across them, and any other pairing leaves the incumbent
in place so the existing root order still decides. This repository is the
Claude Code CLI, so a skill installed for us stays authoritative over one
another client dropped into the shared .agents space. Plugin and legacy
/commands/ entries carry no flavor and neither displace nor get displaced on
flavor alone -- which also settles a second disagreement, where the desktop
list let a plugin skill take a name the CLI gave to the user's own.

The --bare branch loads only --add-dir roots and had its own scoped helper;
it now runs the same rule, so two added directories holding one name collapse
like everywhere else.

Behavior change: a name repeated across scopes used to produce two entries,
and a regression test pinned that. Two identical rows give the reader no way
to tell them apart while only one can run, so the pair now collapses; the
test is replaced by the cross-scope cases that assert the surviving copy.

Verified end to end in a sandbox HOME plus project: for same-scope,
cross-flavor, and cross-scope-same-flavor collisions, the desktop menu, the
CLI menu, and findCommand all land on one file and agree on which.
2026-07-25 17:25:07 +08:00
程序员阿江(Relakkes)
2b89d4a678 fix(skills): correct the .agents badge, doctor, and listing safety
Follow-ups to 9ef170bb, where the discovery path itself is sound but three
surfaces around it are not.

The desktop badge showed the same tooltip for both scopes, and that tooltip
names ~/.agents/skills. A skill that ships inside a checked-out repository was
therefore described as one the user had installed in their own home directory
-- the least trusted source presented as the most trusted. Project-scope
skills now get their own copy, in all five locales.

The doctor listed both .agents targets unconditionally, so a user who set
disableAgentSkillsDirectory (or the env var) still saw them reported, with an
entry count if the directory had contents. That reads as "these skills are in
use" when the loader is ignoring them.

Skill directory names went into the catalog the model sees verbatim, one
`- name: description` line per skill, with no validation. A name containing a
line break writes entries of its own -- and .agents/skills is a directory
other clients and repositories write into, so the name is not necessarily the
user's. Names carrying control characters are now skipped. Deliberately
narrower than the Agent Skills spec's lowercase-and-hyphens rule: existing
.claude/skills directories predate it, and silently dropping one of those
would be the worse failure.

Also collapses the two spellings of the scoped dedup key -- the main loader
joined with a raw NUL byte embedded in the source, the --add-dir path with a
space -- into one helper, so the comment claiming both apply the same rule is
true and the invisible byte is gone.

Adds coverage for the two scopes 9ef170bb claimed but left untested: the
hot-reload watcher (no test file existed for it at all) and on-demand monorepo
discovery. Both were already correct; nothing was holding them that way.
2026-07-25 15:56:55 +08:00
程序员阿江(Relakkes)
5b4e224f2e fix(desktop): address shadcn migration review findings
- Prevent EmptySession crash when settings have not loaded: fall back
  to the default permission-mode display and an empty model list, and
  stop undefined models responses from poisoning the settings store
- Sync Tauri terminal hardening with Electron: owner binding, input
  limits, window-destroy session cleanup, malformed config fallback
- Fix Sheet/MobileBottomSheet z-order so dialogs render above sheets
- Migrate ConfirmPopover to the shadcn Button; remove dead legacy
  shared form components and unused mockup pages
- Keep underscores readable in shared diagnostics, redact provider
  models only on secret-scanner hits, clean stale WhatsApp login
  staging dirs, and add recovery hints for corrupt computer-use config
- Token-ize find-in-page highlight colors, restore the indeterminate
  progress slide animation, and tidy a11y/displayName details
2026-07-25 15:07:00 +08:00
程序员阿江(Relakkes)
9ef170bb42 feat(skills): discover the cross-client .agents/skills convention #1093
Skills installed by Codex, Cursor, Gemini CLI, or opencode live under
`.agents/skills/` -- the convention documented at agentskills.io -- so users
had to keep a duplicate copy in `.claude/skills/` before cc-haha could see
them. The spec only constrains SKILL.md itself, which we already parse, so
this is purely a directory-discovery gap.

Scan both conventions at every scope: user (`~/.agents/skills`), project
(every level up to the repo root), `--add-dir` roots, on-demand monorepo
discovery, and the hot-reload watcher. skillRoots.ts is the single source of
truth for those paths; the REST API now reports which convention a skill came
from and the desktop list badges the `.agents` ones.

`.claude` is always ordered first, so first-wins dedup keeps a user's own
skills authoritative rather than letting an externally installed same-named
skill shadow them. Name collisions are collapsed per scope, which covers
copy-based sync -- symlink-based sync was already handled by the existing
realpath dedup. Collisions across different scopes, such as a user skill and
a project skill sharing a name, keep their existing behavior, and the shadowed
file's identity is recorded so overlapping roots (CLAUDE_CONFIG_DIR pointing
inside the project) cannot reintroduce it under the other scope.

getProjectDirsUpToHome is untouched -- it is shared by commands, agents,
output-styles, and workflows. Only its directory walk is extracted so the
skill resolver reuses the same submodule/worktree stop boundary. Writes stay
where they were: market installs and /skillify still target
`~/.claude/skills`.

`.agents` is anchored at $HOME rather than CLAUDE_CONFIG_DIR, since it is a
space shared with other tools that read $HOME. Note that Bun snapshots
os.homedir() at startup while Node re-reads $HOME per call, so the resolver
reads the env var explicitly.

Opt out with `disableAgentSkillsDirectory` in settings.json or
CLAUDE_CODE_DISABLE_AGENT_SKILLS_DIR=1; no `.agents` root is produced at all
then, so there is not even a failed stat.

(cherry picked from commit ba9338f85d6beaba237753e42e823c87b59633d4)
2026-07-25 02:50:21 +08:00
程序员阿江(Relakkes)
ad597ffe0b feat(desktop): unify UI with shadcn components (#1089) 2026-07-24 23:59:48 +08:00
程序员阿江(Relakkes)
ca0342d252 Merge remote-tracking branch 'origin/main' 2026-07-23 00:31:40 +08:00
Relakkes Yang
3b2e750c6e fix(desktop): polish pet status and agent tools 2026-07-23 00:24:18 +08:00
Relakkes Yang
20b7753281 fix: sync pet status and finish agent editor 2026-07-22 22:12:27 +08:00
Relakkes Yang
a07e68839c fix: harden desktop runtime and pet interactions 2026-07-22 20:46:34 +08:00
程序员阿江(Relakkes)
29b17c8028 fix(models): expose current Claude official lineup 2026-07-22 17:42:19 +08:00
程序员阿江(Relakkes)
91829e91a4 fix(chat): preserve attachment context across resume 2026-07-21 04:05:01 +08:00
程序员阿江(Relakkes)
7f6fdadaa3 feat(agents): add per-agent model and effort controls #1062 2026-07-21 00:16:11 +08:00
程序员阿江(Relakkes)
1270ced4b4 fix(tasks): keep task state reliable across providers #1075 2026-07-20 23:53:46 +08:00
程序员阿江(Relakkes)
7d272e0f96 fix(server): handle tool denial interruption #1070 2026-07-20 20:38:45 +08:00
程序员阿江(Relakkes)
6b725a0b6f fix(desktop): show live SubAgent run details #1077 2026-07-20 20:30:33 +08:00
程序员阿江(Relakkes)
8995f56d1f fix(server): preserve context estimates for empty usage #1063 2026-07-20 20:20:25 +08:00
程序员阿江(Relakkes)
71dc2fd4cf fix(provider): update Kimi Code preset to K3 2026-07-20 19:25:20 +08:00
程序员阿江(Relakkes)
36560c09d5 feat(pets): add interactive desktop companions 2026-07-20 19:20:18 +08:00
程序员阿江(Relakkes)
c60d3780b8 fix(server): close release QA regressions (#1019, #1047, #1051) 2026-07-18 15:38:32 +08:00
程序员阿江(Relakkes)
ef841c2962 fix(server): flush context before runtime restart (#1033) 2026-07-17 23:17:05 +08:00
程序员阿江(Relakkes)
0f41501d53 test(server): harden async trace timing (#1014) 2026-07-17 22:58:27 +08:00
程序员阿江(Relakkes)
d500497b9d fix(server): honor configured current model (#1024) 2026-07-17 22:57:13 +08:00
程序员阿江(Relakkes)
1d8597fb7b fix(server): bound late permission waits (#1019) 2026-07-17 22:57:13 +08:00
程序员阿江(Relakkes)
948eb36b67 fix(server): bound retained SDK diagnostics (#1049) 2026-07-17 22:54:43 +08:00
程序员阿江(Relakkes)
b6bbbd5ff8 perf(server): decouple proxy traces from responses (#1014) 2026-07-17 22:49:48 +08:00
程序员阿江(Relakkes)
15e554a565 fix(server): bound abandoned permission waits (#1019) 2026-07-17 22:48:16 +08:00
程序员阿江(Relakkes)
b7f1dd6d80 fix(server): include configured role models (#1024) 2026-07-17 22:48:16 +08:00
程序员阿江(Relakkes)
a6ebcfb00c fix(server): interrupt turns after tool denial (#1051) 2026-07-17 18:33:32 +08:00
程序员阿江(Relakkes)
0bbfc62907 fix(context): normalize attachments in status analysis (#1022) 2026-07-17 18:33:19 +08:00
Li Hong
4b1e2fdecf fix(proxy): 保留 Responses 工具结果中的图片
修正 Anthropic 多模态内容到 OpenAI Responses 的转换,避免 Read 等工具返回的图片被过滤为空字符串,并保持纯文本兼容与内容顺序。
2026-07-17 12:46:44 +08:00
程序员阿江(Relakkes)
c06f958e7c fix(proxy): route AI sessions through dynamic system proxy #953 2026-07-16 17:33:33 +08:00
程序员阿江(Relakkes)
53a15098c0 perf(desktop): add SQLite-backed local data index #1016
Project sessions, activity, scheduled runs, traces, and searchable content into rebuildable managed databases while preserving canonical-file fallbacks. Add diagnostics, recovery, parity coverage, and performance acceptance checks.
2026-07-16 04:16:02 +08:00
程序员阿江(Relakkes)
7e998e0365 fix(search): bundle cross-platform ripgrep #923 2026-07-14 22:11:22 +08:00
程序员阿江(Relakkes)
86393ac179 fix(desktop): search unopened workspace files #1023 2026-07-14 21:31:39 +08:00
程序员阿江(Relakkes)
d048a3b3c4 fix(provider): restore desktop OpenAI proxy auth #1025 2026-07-14 21:22:20 +08:00
程序员阿江(Relakkes)
4c3b08c0a4 fix(security): harden desktop request isolation 2026-07-14 02:12:03 +08:00
程序员阿江-Relakkes
d3bf146c46
Merge pull request #1015 from toBerlinWay/feat/security-hardening
feat: harden local server access policy and isolate remote preview permissions
2026-07-14 01:54:02 +08:00
程序员阿江(Relakkes)
5178d7ceec feat(provider): add Grok official OAuth support 2026-07-13 21:24:55 +08:00