Commit Graph

86 Commits

Author SHA1 Message Date
程序员阿江-Relakkes a2d2ca15ad fix(desktop): retry Windows data migration publication locks (#1473) 2026-10-08 23:28:28 +08:00
程序员阿江-Relakkes 3754d1f2a5 feat(desktop): replace the settings trace list with an in-session trajectory view (#1443)
Sessions get a Chat / Trajectory switch in the header. The trajectory is a
dense one-line-per-event ledger (system prompt, user, injected context such
as skills and system reminders, assistant responses, tool calls) with a
three-lane minimap, turn folding, search, and a detail panel per row.

- CLI: record a deduplicated prompt snapshot sidecar for desktop sessions
  (system prompt, tool catalog, user context) without touching the transcript.
- Server: project transcript records into trajectory rows with bounded,
  cursor-paged reads, live appends, a turn index, row detail by byte range,
  snapshot blobs, and a time-window lookup into the raw trace capture.
- Desktop: windowed ledger, minimap, detail panel with a raw request summary,
  chat <-> trajectory navigation, subagent drill-in.
- Remove the Settings trace list, trace tabs and the standalone trace
  window; migrate persisted trace tabs to session tabs.
2026-10-04 17:14:07 +08:00
程序员阿江-Relakkes 7e50c31986 fix(desktop): composite the workspace browser with the app UI (#1437)
The workspace browser was a native WebContentsView, which always paints
above the DOM. Switching to the skills or settings page left the page
floating over it, and menus over the page needed a screenshot swap that
showed up clipped and flashed white.

Pages are now <webview> guests kept in a layer inside the session panel
that is never unmounted, so other pages, menus and dialogs draw over
them like any element. The main process adopts each guest and keeps all
in-page behaviour: navigation, history, find, zoom, capture, PDF,
downloads, shortcuts and annotation.

- Guard every attach in the main window: browser partition only, start
  at about:blank, preload and sandbox pinned, reported ids validated.
- Match the native page: drop the blank history entry, keep page zoom
  independent of app zoom, allow popups so they still become tabs.
- Keep app drags working over a page and close menus on a click into it.
- Tell the side dock it is off screen when the session page is hidden.
- Remove the overlay snapshot machinery and native bounds syncing.
2026-10-04 03:24:50 +08:00
程序员阿江-Relakkes 795ff9d4df feat(desktop): add safe data directory migration (#1433) 2026-10-04 00:39:20 +08:00
程序员阿江(Relakkes) 6d75fac69e feat(desktop): add local voice dictation to the chat composer
Adds a microphone button beside the composer. Click to record, click to stop; the audio is resampled to 16 kHz mono PCM16 WAV, posted to the local server and transcribed by a SenseVoice worker process, and the text lands in the draft without being sent. If the draft changed or an IME is composing, the result is held behind an insert button instead of overwriting the user's text.

Server: a small provider registry behind /api/voice/* (catalog, preferences, prepare/cancel/status/remove, transcribe). The engine and model are downloaded at runtime to <config>/cc-haha/voice with pinned sha256/sha512, HuggingFace plus hf-mirror and npm plus npmmirror, HTTP Range resume, automatic retry after interruptions, and a partial file kept across cancels. Recognition runs in a separate worker process (sidecar --voice-worker), started on demand and reclaimed when idle.

Desktop: an independent Voice input settings tab with enable switch, model download progress and resume, language, microphone selection and a transcription test with a live waveform. Uses the shared Dropdown/Card/Button components; adds a danger-ghost Button variant. Preferences live in desktop-ui.json (schemaVersion 6); the microphone device id stays in localStorage.

Electron: main-window media permission handler limited to app pages, main frame and audio only, plus the audio-input entitlement and NSMicrophoneUsageDescription.

Scope: Electron desktop only. Not verified on Windows or Linux, with a real microphone, or in a signed and notarized package.
2026-10-01 15:23:21 +08:00
程序员阿江(Relakkes) a4cd0306fa fix(desktop): keep ngrok start consent validation in sync with the shared version
PUBLIC_ACCESS_CONSENT_VERSION moved to 2 when remote provider management
landed, but the Electron IPC validator still accepted only the literal 1.
The renderer sends the constant, so publicAccessStart was rejected as an
invalid payload in the preload guard and again in the main-process handler,
before PublicAccessManager saw the request at all. That rejection bypasses
the manager's error classification, so the settings page could only show the
generic "operation failed" line while the state stayed disabled: public
access could not be enabled, and autoStart could never become eligible
because consent v2 was never persisted.

Validate against the shared constant instead of a copy of its value, and
stop pinning the stale literal in the tests that let this drift through:
capabilities.test.ts and electronHost.test.ts now send the constant and
reject the previous, next and non-numeric versions.
2026-09-15 17:54:59 +08:00
程序员阿江(Relakkes) 8d7b5ea56b feat(h5): persist pairing and add secure mobile settings
Add mobile provider and General settings while preserving desktop behavior.
Harden remote credential handling, ngrok session ownership and consent upgrades.
2026-09-14 00:38:55 +08:00
程序员阿江(Relakkes) 9fce822df3 feat: add secure ngrok remote access with device pairing 2026-09-13 23:28:30 +08:00
程序员阿江(Relakkes) 69e8c014af fix(desktop): keep workspace browser visible beneath native menus
Use a native Electron menu with validated host actions and lifecycle cancellation. Preserve browser state, persistent annotations, and disabled page actions on blank tabs.
2026-09-13 22:01:39 +08:00
程序员阿江(Relakkes) 6f0650c5cb fix(workspace): align resource panels and complete backend state flows
Unify workspace controls and resource tabs, refine browser, file and diff
interactions, and remove superseded panels. Preserve resource lifecycles,
refresh Git comparisons after external changes, and correlate terminal
startup events across IPC.
2026-09-13 16:02:58 +08:00
程序员阿江(Relakkes) dbc1e483ca feat(desktop): rebuild the workspace as a unified tab controller
Replaces the two-mode right-side panel (files ↔ browser) with one controller
that owns layout, navigation and resource lifetime for four content kinds:
files, browser pages, Git review and terminals. Follows the Codex desktop
reference captured in the workspace-refactor plan.

The old panel conflated three things that have different lifetimes: a UI tab,
the content it shows, and the process behind it. That is why switching modes
destroyed a live page, why a second link overwrote the first, and why the
toolbar button reported "show workspace" while the workspace was already open.
Splitting them is the whole change:

- workspaceStore    layout, docks, tab order, preview/pinned, focus
- workspaceContentStore / workspaceReviewStore   file and diff data
- host services     webContents and PTYs, keyed by resource id, never by tab id

Consequences that fall out of the split:

- Hiding the panel, switching tabs and switching tasks keep every page and PTY
  alive; only closing a tab releases them.
- A terminal moves between the side and bottom docks without restarting.
- Pages live in a shared persistent partition with native navigation history;
  popups become sibling tabs in the task that opened them.
- Review names both sides of every comparison and performs real index and
  working-tree writes, guarded by a snapshot token and a backup-first revert.

Also adds versioned workspace persistence with a forward migration: terminals
come back stopped and restartable, pages reload lazily, and nothing holding
content, cookies or handles is written to storage.

The previous implementation is no longer reachable but is left in place: the new
file tab does not yet reproduce workspace search, quick-open or the changed-file
view, so removing it now would lose those. Real three-platform Electron
acceptance (plan item A10) has not been performed; all evidence here is
deterministic tests, type checks and a packaging smoke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 17:17:39 +08:00
程序员阿江(Relakkes) 7af6c53ff8 fix(desktop): restore macOS background computer use and app shutdown
Keep occluded renderers live with display-filtered window streams, refresh
capture regions after geometry changes, and preserve the paste read window.
Allow application shutdown to finish even when resource cleanup throws.

Validated with check:native and an isolated desktop MCP workflow covering
background search, playback, lyrics, and repeated text entry.
2026-09-06 03:50:49 +08:00
程序员阿江(Relakkes) 814c02a786 feat(im): add scan-to-create Feishu bots plus WeCom, QQ and Slack adapters
Connecting Feishu meant creating a bot by hand on the open platform and
pasting an App ID and App Secret back. Feishu also exposes an RFC 8628
device-authorization flow, so the desktop can now render a QR code, and
confirming it in the app creates the bot and stores its credentials
directly. `adapters/feishu/registration.ts` implements that protocol
rather than importing `registerApp` from `@larksuiteoapi/node-sdk@1.73`:
the repository pins 1.60 for the chat client, and the SDK runs the whole
poll inside one un-cancellable promise where the desktop needs the
stateless begin/poll pair the DingTalk registration already uses. The
scan is create-only, so it can never rewrite the configuration of a bot
the user already runs, and it pre-fills exactly the scopes, events and
callbacks this adapter calls. International tenants finish on Lark's
domain, which is now persisted and honoured by the client.

WeCom, QQ and Slack join the same session model. WeCom and QQ bind by
scanning; Slack has no scan flow, so it uses an app manifest that
pre-fills the scopes and Socket Mode. All three run over long
connections, so no public callback URL is needed, and all three accept
private chats only — pairing authorizes one person, and answering in a
group would extend that authorization to everyone else in the room.

They are built on a new `adapters/common/chat-runtime.ts` instead of a
fourth copy of the loop the five existing adapters each carry. A platform
supplies a `ChatPort` — how to say something, how to open a streaming
reply, optionally how to send an image — and the runtime owns pairing,
command routing, session restore, permission bookkeeping and the
translation of the server's stream. The existing five are deliberately
left on their own copies; migrating them is a separate change with its
own regression surface.

Attachments are downloaded through a deferred loader that runs after the
pairing gate and inside the per-chat queue. Resolving them eagerly would
let an unpaired stranger make the adapter fetch bytes and write them
under ~/.claude/im-downloads — on Slack with the bot token attached —
and would let a slow attachment overtake a text message sent after it.

The sidecar launcher's per-adapter branches become one table. It is
declared above the mode dispatch on purpose: `runAdapters` is hoisted and
runs at module top level, so a table declared below it is still in its
temporal dead zone when the adapters mode reads it — which type checks,
lints and unit tests all miss, and only the compiled binary reveals.

Verified with the checks `check:impact` selects: adapters, server,
desktop, electron, policy, chat-contract, agent-flow, docs, native
(sidecar compile, packaging and an adapters-mode smoke against the real
binary) and coverage. The scan flows themselves are not verified against
live platforms — that needs real WeCom, QQ and Slack accounts and would
create real bots.

Claude-Session: https://claude.ai/code/session_01CCGoP316AK7wdQG3Ms6Uwq
2026-09-05 23:46:50 +08:00
程序员阿江(Relakkes) 3e3ae6510a fix(desktop): await sidecar cleanup before quit 2026-09-02 10:18:27 +08:00
程序员阿江(Relakkes) 5001502882 fix(desktop): recover safely from sidecar crashes #1227 2026-08-16 00:18:09 +08:00
Relakkes Yang 4f93e20cd5 fix(desktop): stabilize Windows pet and chat rendering (#1187, #1144) 2026-08-05 18:44:51 +08:00
程序员阿江(Relakkes) 4c39fb0629 fix(desktop): reap terminal PTYs on committed navigation, not on started
Review caught a way the previous listener could kill a user's running shells with
no reload at all. installMainWindowNavigationGuards cancels external http(s)
navigation in `will-navigate` and hands it to the system browser, but Chromium
dispatches DidStartNavigation before the throttle that cancellation runs in — so
a blocked navigation still emits `did-start-navigation`. Dropping a URL anywhere
outside the composer (useComposerFileDrop only preventDefaults over the composer
panel with files attached) would have started a top-level navigation, had it
cancelled, and taken every `npm run dev` and its children with it while the app
stayed on screen.

`did-navigate` fires only once a main-frame navigation has committed, so a
cancelled one never reaches it, and Electron does not emit it for in-page
navigation — which also removes the need for the same-document predicate. The
reload paths this cleanup exists for all commit, so they still reap.

rendererNavigation.ts goes with the predicate: previewLifecycle.ts was its only
remaining caller, so it returns to the standalone form it had before.

Mutating the listener back to `did-start-navigation` reddens the cancelled
navigation case, which is what shows the test pins the commit boundary rather
than merely the existence of a listener.
2026-08-04 16:33:00 +08:00
程序员阿江(Relakkes) a554622086 Merge main into worktree-20260803001-qa-reactor
Two real conflicts, one of them structural.

desktop/src/pages/Settings.tsx — git offered the whole 4128-line pre-split file
as "theirs" against the 183-line shell, which is not a merge anyone can review.
Resolved by keeping the split and porting main's nine hunks to where that code
now lives: the rail width, its comment and TabButton's padding stay in
Settings.tsx; the ModelIdCombobox import and the five ProviderFormModal changes
(the canFetchModels split into hasModelsBaseUrl/hasModelsApiKey, modelPickerItems
becoming modelPickerGroups, the two new hint branches, and the Input+Dropdown pair
collapsing into ModelIdCombobox) go to settings/ProviderSettings.tsx.

Verified rather than assumed: every line main added is present somewhere in the
split, every construct it removed is gone (modelPickerItems, canFetchModels =
Boolean(...), the supplementary Dropdown), and the import specifier gained the
level the new directory needs.

desktop/package.json — taking main's version wholesale dropped the eslint setup
from f36c9cb49. Reconstructed with both sides: main's six prosemirror packages and
the three eslint devDependencies, with lint back to eslint + tsc.

Everything else merged clean, including the files both sides touched:
src/server/ws/handler.ts (main's four title-generation changes all sit in code the
three splits left behind), desktop/src/stores/chatStore.ts (main's mention/
repository-launch state alongside the turn-scoped replay guard), and the five
locales — 2526 - 9 removed + 5 added = 2522, still aligned across all languages.

Checks: desktop lint + 4049 tests + build, check:electron, check:policy all green.
The one server failure, workspace-service.test.ts "rejects a file outside the
workdir", fails identically on main — confirmed against a temporary worktree at
main, which fails it plus three more. It passes 3/3 in isolation; the registry it
asserts on is a module-level Set shared across test files.
2026-08-04 15:46:41 +08:00
程序员阿江(Relakkes) dbe63bed64 fix(desktop): reap terminal PTYs when the renderer reloads, not only when destroyed
webContents.once('destroyed') was the terminal service's only lifecycle hook, and a
reload does not emit it. The app reloads the renderer deliberately on
render-process-gone and on sustained unresponsive (rendererLifecycle.ts), and both
ErrorBoundary and StartupErrorView give the user a reload button. The renderer's
session map is module state wiped by that reload, and kill() needs a session id the
reloaded renderer no longer has — so every shell kept running with its children
(dev servers, watchers, builds), invisible and unkillable, until before-quit.

Subscribe to did-start-navigation as well and run the same teardown, mirroring
installPreviewCleanupOnRendererNavigation, which already solved exactly this for
the preview.

The main-frame / same-document predicate moves into rendererNavigation.ts and both
callers anchor on it. Electron has shipped this event in two shapes — details
object and trailing positional args — and reading only one silently disables the
cleanup on whichever build uses the other; that is not a rule worth spelling out
twice.

`off` rather than a second `removeListener` overload: a target type with two call
signatures is not structurally assignable from Electron's overloaded
removeListener, so the obvious spelling rejects the real WebContents.

Three mutations verified against the new tests: dropping the subscription (2 red),
ignoring isSameDocument so in-page routing kills live shells (2 red), and skipping
the detach so listeners accumulate per terminal (1 red).
2026-08-04 13:47:03 +08:00
程序员阿江(Relakkes) 63c81e63ab fix(desktop): restore native right-click for selected text 2026-08-04 09:54:23 +08:00
程序员阿江(Relakkes) 0f1ebf8d1f fix(desktop): shift project menu around browser preview 2026-08-03 02:35:58 +08:00
程序员阿江(Relakkes) f6f2a6a6f7 fix(desktop): render project menu in native popup 2026-08-03 01:50:18 +08:00
程序员阿江(Relakkes) 5f8bae08db feat(desktop): support batch element selection #1164 2026-08-01 14:32:19 +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) dfb3e350d8 fix(security): restore features blocked by request hardening 2026-07-31 05:13:38 +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) 8ec8833bec fix(security): harden local runtime boundaries 2026-07-29 18:37:11 +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) d92ac7f2c0 feat(desktop): detect and persist the display locale 2026-07-29 13:09:03 +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) 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) 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) 5b891151ae feat(desktop): follow the system dark/light appearance (#1106)
An Auto-dark-mode user reported being flashed by a white window every
evening, then switching to a dark palette by hand. That is two defects,
and only one of them is the missing feature.

The flash fired even for someone who had already saved a dark palette.
`index.html` hardcoded `data-theme="white"` while the code that reads the
stored theme, `initializeTheme()`, only runs after the app bundle's
dynamic imports resolve. So every launch painted white first. A
synchronous inline script now resolves the theme before any stylesheet is
parsed, and Electron seeds `backgroundColor` from a cached appearance so
the window is not white before the renderer's first frame either.

Following the system is a switch in Settings -> General rather than a
seventh palette. The OS only reports dark/light while the app ships six
palettes, so each ground carries its own preference: the picker splits
into "use in light mode" (the four paper grounds) and "use in dark mode"
(the two ink ones), and a pick lands in the preference for its own ground.
Choosing ink-blue at noon is therefore remembered for that night rather
than fighting the OS. Detection goes through `prefers-color-scheme`
because the same renderer runs under Electron, the Tauri shell and the
browser entry, and the media query is the only signal all three share.
New installs follow the system; existing ones keep their fixed palette
until they opt in, so an update never silently repaints someone's app.

`nativeTheme.themeSource` is deliberately left alone, which is the part
most likely to be "fixed" later. Pinning it to the user's palette would
make context menus and the macOS frame agree with the app, but it is a
process-wide override of `prefers-color-scheme` — the very signal this
feature reads. Re-enabling the switch would then resolve against the
pinned value instead of the real OS setting, and the override also leaks
into the preview WebContentsView, forcing third-party pages to the app's
theme. A test fails on any assignment to it.

The OS-flip listener reads the preferences from storage rather than from
its own store. The pet and trace windows run the same bootstrap with
their own store instance over one shared localStorage, so after the main
window turns the switch off their in-memory copy still says "on" — acting
on it wrote the user's choice straight back out. A `storage` listener
catches the other windows up.

`settingsStore.theme` is gone. It was a copy that only refreshed on an
explicit `setTheme`, so an OS flip left the Settings picker highlighting
a palette that was no longer on screen. uiStore owns the theme; the copy
had no remaining readers.

The 「纸·墨·印」 rename reaches the new keys too: `light` -> `warm-classic`
now migrates for the per-ground preferences, not just the applied theme,
so the palette daytime returns to is not silently reset.

Guards, each verified by breaking what it protects: the inline script is
extracted from `index.html` and run verbatim against `resolveAppliedTheme`
over every stored combination — including dirty values, which are
reachable because it runs before the persistence migrations, and
cross-ground values like a dark palette stored as the light preference;
the three copies of the palette grounds (CSS `--cc-bg`, `index.html`,
main process) are pinned to each other and to `THEME_MODES`, so a seventh
palette cannot ship without a pre-paint color; the two grounds are proven
to cover every palette at compile time; and the IPC payload is held to a
literal 6-digit hex because `setBackgroundColor` also accepts
`#AARRGGBB`, where a translucent window means click-through and overlay
spoofing.
2026-07-27 02:15:37 +08:00
程序员阿江(Relakkes) 8f3a2f092c fix(desktop): let the dragged pet reach the macOS menu bar
The mascot sits at the bottom of a mostly transparent 384x400 window, so
clamping a drag against the mascot rather than against the whole window
deliberately asks for a negative y that pushes the padding above it off-screen.
macOS runs a visible window's frame through
-[NSWindow constrainFrameRect:toScreen:], which rewrites any y above the work
area back down to its top edge -- silently. The request was refused, the
controller never noticed, and the mascot stranded a padding-height below the
menu bar: 208px of dead band on this display, with
~/.claude/cc-haha/pet-window.json recording y=-175 for a window that never
left y=33.

Measured on Electron 42.7.0 / Darwin 25.4: only the top edge is constrained.
Off-screen x and off-screen bottom y are kept verbatim, which is why the other
three edges always worked. Nothing else escapes it -- not window level (all
eight, up to screen-saver), movable, frame, transparent, panel type, and not
positioning the window while hidden, since showInactive re-runs the constraint.
enableLargerThanScreen is the one switch that skips the method, and it leaves
size, getContentBounds and ordinary moves untouched. clampPetWindowPosition is
already a complete four-edge bound, so opting out makes it the only clamp
rather than removing one.

Restoring then exposed a second problem the constraint had been hiding. A saved
position deliberately leaves the padding off-screen, but the window is created
before the renderer reports any region, so getPetWindowBounds clamped it against
the whole window and opened a padding-height lower -- a jump of zero while the
window was pinned, 208px once it is not. Persist the mascot box next to the
position so the first frame lands where the drag left it. State written without
one still restores the way it always did, and an empty box is dropped rather
than trusted.

The fake window accepted every y, so no top-edge assertion could fail and the
suite stayed green through all of this -- the same blind spot that let the
Windows DIP bugs hide. It now applies the constraint the way AppKit does, only
while visible and only when the constructor options did not opt out, so the
assertions prove the fix rather than the platform. Reverting either half turns
the new cases red.
2026-07-26 02:38:03 +08:00
程序员阿江(Relakkes) ced096c894 fix(desktop): keep the dragged pet at the display edge across restarts
8a3ea62b taught dragging to clamp against the mascot rather than the whole
window, so a saved edge position deliberately leaves the transparent padding
off-screen. Recreating the window re-clamps that position against the whole
window again, and the reposition-on-restore step that undoes it was gated on
darwin -- so on Windows and Linux the mascot walked inwards by the padding
width on every hide/show, not just on restart. The gate only existed because
visibleDragRegion used to be darwin-only; the region now arrives everywhere,
so the repositioning runs everywhere.

e24d59fe restated the recorded window size on every drag tick to stop
setPosition from growing the window, but sampled that size from getBounds().
Chromium converts window rects between physical pixels and DIP with ceil in
both directions, so the sampled value has already been rounded up once and
restating it rounds up again: stable within a drag, a pixel per drag across
them. The window is created non-resizable at the nominal size, so restating
the constants is idempotent instead -- and heals a window that already drifted.
Both call sites now go through movePetWindow.

petWindowContentExtent fell back to the nominal constants when
getContentBounds returned an empty rect, which silently reinstates the exact
clamp it exists to avoid. Fall through to the live window box first.

The fake window in the tests returned one object for both getBounds and
getContentBounds, and modelled setBounds as inherently size-neutral -- so
neither "clamp to the content box" nor "size-neutral drag" was actually
proven: reverting either left all 33 tests green. It now models the ceil/ceil
DIP round trip and carries a distinct content box, and covers the previously
untested paths: cross-platform restore, multi-region shapes, the region
normalization bounds, and the getContentBounds fallbacks.
2026-07-25 15:56:39 +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) 8a3ea62b9b fix(desktop): let the dragged pet reach display edges on Windows
setInteractiveRegions only recorded visibleDragRegion inside the darwin
branch, so on Windows and Linux it stayed null and dragging fell back to
clamping against the whole PET_WINDOW_WIDTH x PET_WINDOW_HEIGHT window. That
window is mostly transparent padding around a mascot anchored bottom center,
so the mascot stopped short of every display edge by the width of that
padding and could not be dragged into a corner. Record the region on all
platforms; the reposition-on-restore step stays darwin-only.

Normalizing the region also clamped against the nominal constants. Renderer
regions are measured against the live viewport, so clamp against the real
content box instead -- the same petWindowContentExtent the shape clamp
already uses -- and let the shape clamp reuse that one normalizer.
2026-07-25 02:38:19 +08:00
程序员阿江(Relakkes) e24d59fe92 fix(desktop): stop clipping the dragged pet on Windows #1099 #1104
The Windows shape clamp used the nominal PET_WINDOW_HEIGHT constant. Renderer
regions are measured against the live viewport, so once the content area was
taller than that constant the mascot -- which sits flush with the viewport
bottom -- got sliced off from below, while the task badge kept its own
unclamped rect and stayed visible. Clamp the shape to the real content box.

Dragging also moved the window with setPosition, which Windows resolves as
getSize() + setBounds(); that DIP round trip grows the window a pixel at a
time on fractional display scaling, and the drag timer fires every 16ms.
Restate the recorded size on every tick so dragging stays size-neutral.
2026-07-25 02:12:10 +08:00
程序员阿江(Relakkes) cc768b87c2 merge: unify desktop UI with shadcn components (#1089)
合入 shadcn/ui 全量迁移。该提交同时包含:
- 桌面端手写组件 -> shadcn/ui (radix-ui + Tailwind v4) 全量替换
- Electron 壳层加固: 终端 owner 绑定/IPC 校验/诊断写盘防逃逸
- server/utils 安全加固与修复: 诊断脱敏、Memory 乐观锁、Computer Use fail-closed、stats UTC 等

合并前已经 merge-tree 预演 + 落盘实证: 与 #1086/#1091 无文本冲突, 功能代码零损失。
2026-07-25 00:49:46 +08:00
程序员阿江(Relakkes) ad597ffe0b feat(desktop): unify UI with shadcn components (#1089) 2026-07-24 23:59:48 +08:00
程序员阿江(Relakkes) c2774fc170 fix(desktop): support pasted file attachments #1086 2026-07-24 05:30:38 +08:00
程序员阿江(Relakkes) 8e9c104514 fix(desktop): handle missing Windows taskkill #1091 2026-07-24 05:13:36 +08:00
程序员阿江(Relakkes) 2240a07088 feat(desktop): improve pet motion and interactions 2026-07-23 03:03:05 +08:00
Relakkes Yang 3b2e750c6e fix(desktop): polish pet status and agent tools 2026-07-23 00:24:18 +08:00
Relakkes Yang a07e68839c fix: harden desktop runtime and pet interactions 2026-07-22 20:46:34 +08:00
程序员阿江(Relakkes) 727bcea077 fix(desktop): harden Windows crash recovery #1064 #1065 #1071 2026-07-20 22:06:34 +08:00
程序员阿江(Relakkes) 36560c09d5 feat(pets): add interactive desktop companions 2026-07-20 19:20:18 +08:00
程序员阿江(Relakkes) 26e863902f fix(desktop): reveal renderer startup failures (#1032) 2026-07-17 22:54:43 +08:00
程序员阿江(Relakkes) cdc0d0faff fix(desktop): handle proxy response resets (#1050, #1056) 2026-07-17 18:33:36 +08:00
程序员阿江(Relakkes) c06f958e7c fix(proxy): route AI sessions through dynamic system proxy #953 2026-07-16 17:33:33 +08:00