3 Commits

Author SHA1 Message Date
你的姓名
062978ec97 release: v0.5.8 2026-06-11 02:03:00 +08:00
小橙子
fe4bf78448
fix(provider): auto-detect & fall back from thinking-incompatible providers (#6)
When a third-party Anthropic-compatible gateway (typical: Bedrock-backed
proxies like the user's mimo setup) can't relay Anthropic's `thinking`
field, it returns 400s like
"该模型不支持 'additionalModelRequestFields' 字段" — the gateway has
wrapped the unknown Anthropic param into AWS Bedrock's
`additionalModelRequestFields`, and Bedrock rejects on the target
model. The user reported a 4.5 model on mimo failing this way while the
same model on kiro-account-manager 1.7.5 (which doesn't go through
Bedrock) was fine.

End-to-end fix: detect the failure pattern, sticky-mark the provider,
auto-restart the sidecar with `CLAUDE_CODE_DISABLE_THINKING=1`, and
surface a Settings badge so the user knows what happened. Mirrors the
desktop providerCompatStore pattern from PR #4 (fake tool_use detection)
— two compat dimensions, same re-arm-on-edit semantics, same UX shape.

Server side
-----------
- `SavedProvider` schema gains `thinkingIncompatible?: boolean` and
  `thinkingIncompatibleReason?: string` (max 500 chars). Fully
  back-compat: missing field reads as undefined.
- `ProviderService.markThinkingIncompatible(id, reason)` writes the
  flag, idempotent for identical reasons (no thrash on a burst of the
  same error). Returns null for unknown ids and openai-official.
- `ProviderService.updateProvider` auto-clears the flag on any edit —
  user changing config = "I'm fixing it" → fresh chance.
- `buildProviderManagedEnv` injects `CLAUDE_CODE_DISABLE_THINKING=1`
  when the flag is true, so the next sidecar launch goes out without
  thinking entirely.
- WS handler runs `detectThinkingIncompatMessage` on every error
  ServerMessage forwarded to the desktop. On match, fires:
    1. `markThinkingIncompatible` (persists)
    2. `provider_compat_event` WS message to the desktop
    3. `enqueueRuntimeTransition` → `scheduleRestartSessionWithRuntimeConfig`
       (same graceful path used by set_runtime_config — never tears
       down a streaming response mid-flight)
  Process-local dedup so a burst of identical errors doesn't restart
  the sidecar repeatedly.
- Detection regex deliberately narrow:
    /additionalModelRequestFields/i
    /\bthinking\b[^.]*\b(not supported|unsupported|invalid|
                          disabled|rejected)\b/i
    /unknown.{0,40}\bthinking\b/i
  False positives would permanently disable thinking on the wrong
  provider until the user edited its config.

Desktop side
------------
- New ServerMessage variant `provider_compat_event` plumbed through
  both server and desktop chat type unions.
- `providerCompatStore` gains `thinkingIncompatibleProviderIds:
  Set<string>`, `recordThinkingIncompatible(id, reason)` action,
  `hasProviderThinkingIncompatible(id)` helper. Persisted to the
  same localStorage key as the fake-tool_use counter, alongside
  it. Dedup: re-firing for an already-flagged provider doesn't
  re-toast.
- `clearProvider(id)` now clears BOTH dimensions, matching the
  server-side updateProvider behavior.
- `chatStore` switch dispatches the new event to the store.
- `Settings → Provider` row renders a separate "思考不兼容" badge
  (psychology_alt icon) next to the existing "工具调用异常" badge.
  Both badges can show simultaneously when a provider has both
  problems; both are cleared by the same edit-and-save action.
- 5 locales (en/zh/zh-TW/jp/kr) get 3 new keys each: badge label,
  badge tooltip, and toast message.

Tests
-----
- `src/server/ws/thinkingIncompat.test.ts` (NEW, 6 cases): regex
  pinning — Bedrock additionalModelRequestFields rejections /
  thinking-rejection phrases / unrelated 4xx / passing mentions of
  thinking / null-empty defensive / case-insensitive.
- `src/server/__tests__/providers.test.ts` (5 added): markThinkingIncompatible
  persists / truncates 500-char reason / idempotent / null-for-unknown-id
  / null-for-openai-official / updateProvider auto-clears.
- `src/server/__tests__/provider-runtime-env.test.ts` (2 added):
  `CLAUDE_CODE_DISABLE_THINKING=1` injected when flag true / absent
  when flag false (back-compat with v0.5.7 providers.json).
- `desktop/src/stores/providerCompatStore.test.ts` (6 added):
  records flag + one-time toast / persists set / hydrates from
  localStorage / clearProvider clears both dimensions / null-empty
  defensive / hasProviderThinkingIncompatible reflects current state.

Folded into the unreleased v0.5.8 release notes — fold both this fix
and the PR #5 (handoff fixes) into the same 0.5.8 release.

Tested:
- bun run lint (desktop) clean
- bun test src/server/{ws/thinkingIncompat,__tests__/providers,
  __tests__/provider-runtime-env}.test.ts → 87 + 6 = 93 pass
- bunx vitest run src/stores/providerCompatStore.test.ts → 14 pass
- bunx vitest run src/components/chat/AssistantMessage.faketooluse.test.tsx
  → 6 pass (regression check, no breakage from the new chatStore
  dispatcher case)

Confidence: high
Scope-risk: narrow — additive types throughout (back-compat with
v0.5.7 providers.json), narrow detection regex, dedup guard prevents
restart loops, edit-to-clear gives users an easy escape hatch.

Co-authored-by: 你的姓名 <you@example.com>
2026-06-11 01:43:21 +08:00
小橙子
2b6202d381
fix(handoff): reuse empty session + ship raw tail for v0.5.8 (#5)
Two bugs in the v0.5.7 "Continue from here" hand-off path landed in
the wild and the user caught them while exercising 0.5.8 prep:

1. **Stray empty session.** When the user previously opened a "New
   session in X" tab from the sidebar, then closed the tab to
   declutter, the freshly-created empty session was already on disk
   and visible in the sidebar. Coming back to the welcome screen
   (activeTabId = null → EmptySession route) and clicking "Continue
   from here" called `createSession` unconditionally, minting yet
   another empty session next to the now-stale one. The user ended up
   with `Untitled Session 27 minutes ago` lingering in the sidebar
   while the hand-off ran in a completely separate fresh session.

   Extract the picker into `desktop/src/lib/sessionReuse.ts` —
   `pickReusableEmptySession(sessions, workDir, excludeSessionId?)`
   filters by exact-workDir match, `messageCount === 0`, excludes the
   previous session being handed off FROM, and sorts by `modifiedAt`
   desc. Caller in `EmptySession.onAutoHandoff` runs this before
   `createSession`; on hit, openTab on the existing sessionId and let
   ContentRouter switch to ActiveSession naturally; on miss, create
   fresh as before.

   7 unit tests cover the boundaries: workDir mismatch, non-zero
   messageCount, excludeSessionId honored, null workDir treated as
   "no candidates" (so we never silently merge home-dir sessions
   into a project hand-off), empty workDir argument, empty session
   list, freshness sort order.

2. **Summary too abstract; AI doesn't know specific recent state.**
   The previous implementation only injected the LLM-summarized
   `main` + `recent` paragraphs into the next session's system
   prompt. That summarization tends to wash out exact wording, file
   paths, error messages, and the user's literal last question — the
   user reported "AI doesn't know what we just hit a wall on."

   Add `recentRaw?: string` to `SessionSummary`. New helper
   `buildRecentRawSlice` keeps the LAST ~12 turns (env-tunable via
   `CLAUDE_CODE_HANDOFF_RAW_TURNS`, range 0–50, default 12), each
   truncated to 400 chars preserving the `USER:` / `ASSISTANT:`
   role prefix, total capped at ~8000 chars (env-tunable via
   `CLAUDE_CODE_HANDOFF_RAW_CHARS`, range 500–30000, default 8000).
   Older turns drop first when the total cap is busted by dense
   recent turns.

   `formatHandoffSystemPrompt` renders the raw slice inside a fenced
   code block AFTER the abstracted recent summary, so the next
   session sees both the digest and the literal text. Section is
   omitted entirely when `recentRaw` is absent — v0.5.7 caches still
   load and produce the exact same prompt bytes (back-compat).

   8 unit tests cover the formatter's back-compat path, the raw
   block placement, fence rendering, tail-N selection, all-fits
   case, per-turn truncation with role prefix preservation, total
   cap busting (older drops first), and the `RAW_TURNS=0` opt-out.

Both fixes fold into the unreleased v0.5.8 — release-notes/v0.5.8.md
gets two new fix bullets and the 范围 / 验证 sections call out the
new files and 15 added tests.

desktop/package.json bump to 0.5.8 stays as-is (preset earlier in
the same worktree before this commit).

Tested:
- bun run lint (desktop) clean
- bunx vitest run sessionReuse.test.ts → 7/7 pass
- bun test sessionSummaryService.test.ts → 8/8 pass
- bunx vitest run EmptySession.test.tsx → 22/22 pass (regression check
  after the helper extraction — the existing tests don't go through
  the hand-off branch but exercise the rest of the file)

Confidence: high
Scope-risk: narrow (no shape changes to any persistence — recentRaw
is an additive field, old caches deserialize cleanly because
`tryParseSummaryResponse` already only required main+recent)

Co-authored-by: 你的姓名 <you@example.com>
2026-06-10 20:46:46 +08:00