| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
add talking mascot | 2 个月前 | |
fix(x): Spaces — cached roster and thread snapshots end the member-id flash and reply-open delay (#975) Two symptoms, one cause: messages cache aggressively (module store + localStorage, hydrated in render) but the roster and thread replies did not, so every pane mount painted cached messages next to raw member ids, and every thread open was a cold listThread round trip. - New use-space-members store: roster persisted per space, hydrated synchronously in render (names resolve in the first frame), refreshed stale-while-revalidate with a throttle that also drops the redundant refetch on every resubscribe. Sidebar hover warms it beside the stream. - Thread cache in use-space-chat: bounded per-space snapshots (root, topic, replies tail, hasMore) persisted like the stream tail. ThreadPane seeds from it on mount and writes back what it settles. - Live replies are kept instead of dropped: the WS bus folds reply bodies into the cache (grafting a partial snapshot onto the stream's root for never-opened threads) before bumping the chip denorm. - Hover prefetch on both paths into a thread: the replies chip and the rail's discussion rows. Renderer-only; no wire or main/server changes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 23 小时前 | |
feat(apps): M3 UI — catalog, D18 install dialog, publish dialog, detail actions - Catalog tab: registry search/list, stale-cache refresh, install flow with the D18 capability-disclosure dialog (plain-language capability lines + bundled agents, explicit confirm), install-from-URL with preview and 'updates unavailable' notice for non-GitHub sources - Publish dialog: GitHub device-flow sign-in (user code + polling), §11.2 step progress via apps:progress pushes, success links, name_taken rename-and-retry hint - App detail actions: check-for-update / update (new_capabilities and modified_files confirmation flows), rollback, uninstall (names data/ and bundled agents), publish / publish update - docs/publishing-apps.md (§11.5): bundle format, two-asset requirement, tag convention, registry record, monorepo latest-release constraint | 1 个月前 | |
fix(x): code mode — marking a session done kills its terminal PTY (#974) * fix(x): code mode — marking a session done kills its terminal PTY Done means nothing keeps running on the session's behalf: setDone(true) and merge-back auto-done now dispose the session's terminal (killing the shell and anything it runs — dev servers, watchers). Reopen and failed merges leave the terminal alone; the pane respawns a fresh shell via terminal:ensure on the next attach. Lives in CodeSessionService (core) so both hosts — Electron main and rowboat-server — get the behavior through the one setDone/mergeBack path. The kill is best-effort via dynamic import, keeping native node-pty off the service's static module graph. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(x): terminal dispose kills the whole process tree, not just the shell disposeTerminal relied on pty.kill() = one SIGHUP to the shell. zsh puts every job in its own process group, and any job trapping SIGHUP — which most dev servers do, to survive terminal disconnects — kept running (reproduced empirically: HUP-trapping foreground and background jobs both survived dispose). So "mark done kills the terminal" silently leaked exactly the processes users care about. killProcessTree snapshots the shell's descendants while it is still alive (orphans become untraceable after), TERMs them (+CONT for stopped jobs), and SIGKILLs survivors after a grace period. Group signalling is limited to groups whose leader is inside the tree — a group inherited from above the root is never swept, so the sweep can't hit the app or its siblings; foreign-group members are matched per-pid, re-validated against pid+pgid at KILL time to dodge pid reuse. Windows uses taskkill /T /F. Verified with real PTY trees: SIGHUP/SIGTERM-trapping parents and grandchildren, foreground and backgrounded, all reaped on dispose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 22 小时前 | |
feat(x): sandboxed dev instances — npm run dev:sandbox (#954) Lets several dev instances run at once, alongside the production app and from inside Rowboat code mode: - node-guard.ts (main.ts's first import): fail fast with a clear error when ELECTRON_RUN_AS_NODE leaks in (code-mode agent shells inherit it from the ACP adapter spawn) instead of the cryptic TypeError mid-bundle; apps/main start script strips the var with env -u - ROWBOAT_SERVER_PORT override in apps/server config (explicit test opts.port still wins; split-brain guard untouched) - ROWBOAT_APPS_PORT override for the hardcoded 3210; appOrigin() follows - per-instance Electron userData (<workdir>/.electron-data) when ROWBOAT_WORKDIR is set in dev, so instances don't fight over profile locks - scripts/dev-sandbox.mjs: isolated workdir at ~/.rowboat-dev/<id>, auto-picked free ports, optional --seed-config; docs in DEV_SANDBOX.md Claude-Session: https://claude.ai/code/session_01AV11vrsywmfcrvCUQyxvHJ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 天前 | |
ci(x): drop the Windows-only .npmrc that broke installs on Linux/macOS runners apps/x/.npmrc pinned pnpm's script-shell to a Git-for-Windows bash path. On ubuntu/macos runners that shell does not exist, so every install-time script (node-pty, electron, esbuild, uiohook-napi) failed and `pnpm install --frozen-lockfile` exited 254. The setting is a local convenience: untracked now and ignored, so a developer can keep one without CI ever seeing it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> | 3 天前 | |
feat(spaces): real-time collaborative Excalidraw whiteboards (#936) * feat(spaces): real-time collaborative Excalidraw whiteboards Every space gets shared boards: a Board button (⌘4) in the space header opens a full-bleed Excalidraw canvas where all members draw together live — named cursors, selection highlights, per-element last-writer-wins sync — with the scene persisted as a normal space asset that agents can read and draw on through the existing MCP face. Harbor (contract amendment 2026-08-31): - New ephemeral `whiteboard` Client/ServerFrame pair on the live face — membership-checked, content-blind relay (payload is z.unknown()), never persisted or replayed; mirrors the presence path. Relay + forbidden tests on a real socket. apps/x plumbing (both modes, per the horizontal rule): - core: SpacesLive.whiteboard() sender; the receive path needed nothing — subscriptions already deliver every space-scoped frame, and spaces:events is in PUSH_CHANNELS so remote/child server mode relays frames untouched. Round-trip test through the real stub. - shared: spaces:whiteboard IPC channel + the app-side payload vocabulary (scene / scene_request / files / cursor / idle, keyed by per-pane clientId) and whiteboards/*.excalidraw conventions. - main + apps/server: the handler in both surfaces + RPC_CHANNELS. Renderer: - whiteboard-pane.tsx: Excalidraw's collab model on our transport — version-gated diff broadcasts, 20s full-scene self-heal, reconcile + CaptureUpdateAction.NEVER on receive, 33ms cursor frames, collaborator TTL/heartbeats, image sync via space blobs over app://space-blob. - Snapshots save throttled as single-line .excalidraw JSON TEXT assets (< ~900KB; blob fallback above) — agents read/write boards with plain read_asset/propose_change, and one-line JSON means line-merge can never mangle a scene: concurrent saves conflict cleanly and the pane reconciles + retries (excalidraw.com's merge-on-save in our contract). - Header Board button, Whiteboards rail section (boards hidden from the file tree), lazy chunk (~1MB) so the main bundle stays flat, and self-hosted fonts via a Vite plugin (EXCALIDRAW_ASSET_PATH) so the packaged app:// origin never falls back to the CDN. Design doc: apps/x/SPACES_WHITEBOARD.md (§9 = what shipped + manual QA). Verified: harbor 211, core spaces 39, renderer 365, server app 27 tests passing; typecheck + lint clean; production build splits and copies fonts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spaces): whiteboard polish — image-as-asset uploads, no raw JSON in doc pane, own the editor menu Three fixes from first dogfood: - Images now persist as regular space assets at whiteboards/images/<fileId> (blob upload + proposeChange) instead of bare blobs: visible in the file tree, resolvable by ANYONE from the deterministic path alone (peers, cold loads, agents via read_asset), and snapshot JSON no longer embeds bytes — so boards stay under the 1MB text cap and agent-readable even with images. Upload failures now toast + console.warn instead of vanishing silently. - Board files never render as raw JSON in the document pane: the Split/Read default-document fallback excludes whiteboards/*.excalidraw, and a board reached through any file-shaped selection (artifact link, deep link, history) routes to the whiteboard surface instead. - The editor menu is ours: replaced Excalidraw's stock MainMenu (which carries their GitHub/X/Discord/help links) with just the canvas actions — export image, clear canvas, canvas background. The feature is "Whiteboard"; Excalidraw stays an implementation detail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spaces): whiteboard doc — image-as-asset storage revision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * debug(spaces): whiteboard image-pipeline trace logs Server+core path verified working (PNG blob upload, image asset filing, list, read-back all pass against the stub) — the failure is renderer-side. These [whiteboard] console traces mark each stage: file entering the editor store, upload start, asset filed — so one paste pinpoints the failing hop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(spaces): whiteboard v1 is shapes+text only — remove image pipeline The image upload path proved flaky in dogfood while the collab core is solid, so v1 ships without images rather than with a broken upload. UIOptions.tools.image=false disables the toolbar button, paste, and drag-drop inside Excalidraw, so no image can enter a scene; the whole sync pipeline (whiteboards/images assets, {t:'files'} frames, missing- file fetch, debug traces) goes with it. Snapshots stay small one-line agent-readable text with zero side-band bytes. The image-as-asset design stays recorded in SPACES_WHITEBOARD.md §4.3 + git history for when images return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * fix(spaces): rail '+' creates the whiteboard asset immediately Previously '+' only set the selection — the board existed as an asset only after the first stroke's save, so the rail list showed no new row, nothing highlighted, and a board you never drew on silently vanished on navigation. Now the '+' proposes an empty single-line snapshot (shared EMPTY_WHITEBOARD_CONTENT, byte-identical to what the pane saves for an empty scene) at baseVersion 0, so the board is listed and highlighted the moment it's named; a taken name just opens that board. The header Board button now shows the open board's name as confirmation. The ⌘4 default-board path still creates on first stroke — peeking at the surface shouldn't spawn assets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * feat(spaces): Rowboat skin for the whiteboard — de-Excalidraw the look Documented theming surface only, no internal-layout surgery: - whiteboard.css maps the editor's CSS custom properties to the app's design tokens (islands, popups, inputs, radius, shadows; light+dark) and swaps the signature purple for a calm blue accent - new elements default to crisp architect strokes + Nunito; the sketchy style and hand-drawn font remain in the style picker - live collaborator avatars in the top-right slot (renderTopRightUI), per-client hue shared with their cursor via Collaborator.color; the Library trigger (external excalidraw.com libraries) is hidden - help dialog keeps the shortcut sheet, loses the external-links header - loadScene/saveToActiveFile/toggleTheme canvas actions off — the space owns persistence and the app owns theme - custom WelcomeScreen empty state in the app's voice Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * feat(spaces): Split docks the whiteboard beside chat Split (⌘3) with a board open now keeps the board: chat on the left, the live board in the document slot on the right, resizable with the shared divider (min 420px — room for Excalidraw's compact UI). Talk and Read still leave the board; full-bleed stays the default and the too-narrow fallback lands on full-bleed. The board pane keeps one tree position for full ⇄ split (flex order moves it right of chat), so toggling never remounts the live collab session — no rejoin, no cursor flicker. Mode highlight: full-bleed board stays mode-less, board-split lights up Split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * fix(spaces): stable Board button; board name chip + switcher on the canvas The header button no longer morphs into the board's name after opening (a control that changes label and click-meaning has no identity) — it is always the pen + 'Board', active state via highlight, click toggles. Which-board now lives where your eyes are: a name chip in the editor's top-right slot, left of the collaborator avatars. The chip is also the quick switcher — a popover listing the space's boards (current one checked) plus a 'New board…' input, so switching or creating never needs the rail. Board-name cleaning is now one shared helper (whiteboardPathForName) used by both the rail and the chip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * docs(spaces): whiteboard doc — name chip / switcher note Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 4 天前 | |
Code mode: make packaged builds work via managed engine provisioning (#625) * fix(code-mode): make packaged code mode work via on-demand engine provisioning Packaged builds could never run code mode: the Claude/Codex ACP adapters are spawned as separate `node <entry>` processes resolved at runtime, but esbuild can't inline a dynamic spawn target and Forge strips the workspace node_modules, so every release threw `Cannot find module '@agentclientprotocol/...'`. Dev worked only because of the pnpm symlink. Rather than bundle the ~400 MB of native engines (one claude + one codex binary per OS), provision them on demand: - forge.config.cjs: stage the two ACP adapters + their JS dependency closure into .package/acp/node_modules (npm-style nested layout, native engines skipped), exempt .package from the node_modules ignore rule, and only sign/notarize when APPLE_ID is set so unsigned local/CI builds can package. - agents.ts: resolve the adapter from the staged location first (node_modules fallback in dev); provision the pinned engine and point the adapter at it via CLAUDE_CODE_EXECUTABLE / CODEX_PATH. No dependence on a user's global install. - engine-provisioner.ts: ensureEngine() downloads the per-platform engine package from npm AT THE EXACT VERSION THE ADAPTER WAS BUILT AGAINST, verifies its sha512 integrity, extracts atomically into ~/.rowboat/engines/<agent>/<version>/, and caches it. Version-pinning keeps the ACP handshake compatible. - engine-manifest.ts + scripts/gen-engine-manifest.mjs: committed manifest of tarball URLs + integrity for all platforms, regenerated from the adapters' pinned versions on a bump. Verified on macOS arm64: both engines provision and run, and both adapters complete the ACP initialize handshake from the packaged .app against the provisioned engines. Installer drops from ~790 MB to 390 MB. * feat(code-mode): explicit per-agent Enable in Settings; no silent chat download Code mode now requires the user to explicitly enable an agent before use, instead of silently downloading a ~200 MB engine on the first chat message. - Settings → Code Mode: each agent shows "Not enabled" + an Enable button that downloads its engine with a live progress indicator (download % → verify → install), then flips to "Engine ready". Driven by a new codeMode:provisionEngine IPC call + a codeMode:engineProgress push channel. The section now states the prerequisite explicitly: the agent must be installed (Enable) and logged in (claude login / codex login — code mode reuses that saved credential). - Chat path no longer auto-downloads: getProvisionedEnginePath() returns the enabled engine or throws a clear "enable it in Settings → Code Mode" error, so there's never a surprise mid-conversation download. getAgentLaunchSpec is sync again. - Agent status: `installed` now means "engine provisioned" (downloaded), driving the Enable/Ready state; the new-session dialog shows "Enable in Settings" and disables un-enabled agents. Dropped the dead PATH-probing for a global CLI. Verified: empty cache -> status installed=false and the chat path throws the enable-in-Settings error (no download); core, renderer, and main typecheck/build; no new lint errors. * fix(code-mode): show only percentage during engine download in Settings * feat(code-mode): prune superseded engine versions after install After a successful provision, remove any other version dirs (and their .meta) for that agent so old ~200 MB engines don't accumulate across version bumps. Best-effort; never fails a good install. Verified: a planted stale version dir + meta are both removed after provisioning the current version. * fix(code-mode): keep showing engine download % after reopening Settings Provisioning state lived in the row component, which unmounts when the Settings dialog closes — so reopening mid-download showed the Enable button again even though the download was still running in the main process. Move provisioning state to a module-level store with one persistent listener on codeMode:engineProgress, so a row remounting (dialog reopened) reflects the live % and resolves to Ready on completion. * fix(code-mode): flip Enable row straight to Ready after install (no Enable flash) On successful provision the in-flight flag was cleared before the async status refresh completed, so the row briefly (or until reopen) showed the Enable button again. Await the status refresh before clearing the flag so it transitions directly to Ready. * fix(code-mode): optimistically show Ready right after Enable completes Awaiting the status refresh wasn't enough — setStatus re-renders the parent separately from the row, leaving a window where the in-flight flag was cleared but the status prop was stale, so the row flashed/stuck on the Enable button until reopen. Track just-enabled agents in a module-level set and treat them as installed immediately; loadStatus still syncs the real status in the background. * fix(code-mode): graft login-shell PATH + add startup deadline #1 (the gh/git "command not found" in packaged builds): GUI/Finder launches inherit launchd's stripped PATH (/usr/bin:/bin:...), so tools the engine spawns — gh, git, rg, bash — fail even though they work from a terminal (e.g. Homebrew's /opt/homebrew/bin/gh). Probe the user's login-shell PATH and graft it onto the engine's env before spawn (shell-env.ts; no-op on Windows / probe failure). #2: add a 60s startup deadline (initialize / session create+load) so a wedged engine fails with a clear, stderr-enriched error instead of an infinite "(pending...)". Overridable via ROWBOAT_ACP_STARTUP_TIMEOUT_MS. Manager now disposes the client on startup failure so the spawned adapter doesn't leak. Verified: getAgentLaunchSpec's env.PATH now includes /opt/homebrew/bin (where gh lives); core builds; no new lint errors. * chore(code-mode): comment out signing/notarization for local builds Revert to the explicit comment-out approach for osxSign/osxNotarize: uncomment them (with APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID) for a signed release build. * chore(code-mode): keep signing/notarization active in committed config The repo's forge.config ships with osxSign/osxNotarize enabled (release-ready). Developers comment them out locally for unsigned test builds and don't commit that. * chore: approve workspace build scripts so packaging runs non-interactively The allowBuilds entries were left as "set this to true or false" placeholders, so `pnpm install` / the pre-build deps check aborted with ERR_PNPM_IGNORED_BUILDS and `npm run package` failed. Set them to true (and add node-pty, used by the code-mode embedded terminal) so build scripts are approved and packaging works without a manual `pnpm approve-builds`. | 2 个月前 | |
feat(x): sandboxed dev instances — npm run dev:sandbox (#954) Lets several dev instances run at once, alongside the production app and from inside Rowboat code mode: - node-guard.ts (main.ts's first import): fail fast with a clear error when ELECTRON_RUN_AS_NODE leaks in (code-mode agent shells inherit it from the ACP adapter spawn) instead of the cryptic TypeError mid-bundle; apps/main start script strips the var with env -u - ROWBOAT_SERVER_PORT override in apps/server config (explicit test opts.port still wins; split-brain guard untouched) - ROWBOAT_APPS_PORT override for the hardcoded 3210; appOrigin() follows - per-instance Electron userData (<workdir>/.electron-data) when ROWBOAT_WORKDIR is set in dev, so instances don't fight over profile locks - scripts/dev-sandbox.mjs: isolated workdir at ~/.rowboat-dev/<id>, auto-picked free ports, optional --seed-config; docs in DEV_SANDBOX.md Claude-Session: https://claude.ai/code/session_01AV11vrsywmfcrvCUQyxvHJ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 天前 | |
feat(x): resident app — launch at login + menu bar tray (Granola parity phase 1) - Register as an OS login item on first packaged run (default on); the OS registry stays the source of truth afterward, with a toggle in Settings > Appearance > System - Menu bar tray with template icon (derived from the app glyph, embedded as base64): Open Rowboat / Start-Stop meeting notes / Quit - App keeps running with no windows while the tray exists; launches at login start hidden (menu bar only), Dock/tray reveals the window - Tray menu reflects live recording state via meeting:setRecordingState; tray-initiated toggles survive window-loading races through a pending command drained on renderer mount (same pattern as deep links) - GRANOLA_PARITY.md: research doc — how Granola works, current-state audit, gap analysis, phased parity plan | 1 个月前 | |
docs(x): design docs and prompt catalogs follow the runtime/ move Path pass over turn-runtime-design.md, session-design.md, VIDEO_MODE.md, and LIVE_NOTE.md: every source pointer updated to the runtime/ layout, the turn doc's suggested-module-layout section (§28) refreshed to the real tree (it predated the event hub, bridges, and composer), and the session doc's §14 layout fixed including its old §11/§14 contradiction about where the headless runner lives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat(mini-apps): copilot builder — build-mini-app skill + install/data tools Lets the copilot build a Mini App into ~/.rowboat/apps from chat. - build-mini-app skill: intent gate (confirm when ambiguous), verify Composio wiring by actually calling tools before building, writer branch (Code Mode via code-with-agents vs Copilot), agent-backed data pipeline, install - mini-app-install tool: write an app folder (manifest + dist/index.html) - mini-app-set-data tool: atomic deterministic data.json write (agent returns content; path/write handled in code) - serve canonical bridge shim at app://miniapp/__bridge__.js | 2 个月前 | |
arch: client–server separation — merge to main (Phase 9) (#929) * feat(x): rowboat-server vertical slice + iOS companion app (#758) * feat(x): rowboat-server vertical slice + iOS companion app Implements the phone-client slice of the server/client RFC (#700): - apps/server (@x/server): HTTP transport generated from ipcSchemas (POST /rpc/{channel}, allowlisted subset), server-key bearer auth, WS /events hub (per-connection seq, durable broadcast + per-turn delta subscriptions, hello handshake), authenticated workspace file route, headless standalone entrypoint with pid lockfile - main hosts the transport in-process on its single core instance and forwards migrated channels over localhost HTTP (strangler-fig, ROWBOAT_FORWARD_MIGRATED kill switch, on by default in dev) - Phone-app settings tab: QR pairing, LAN opt-in, key rotation - packages/client (@x/client): portable typed RPC + reconnecting WS client (seq-gap detection, refcounted delta subs, 401/4401 handling) - apps/mobile (@x/mobile): Expo iOS app — QR/manual/dev-link pairing, sessions list, live chat (shared turn-follower + reduceTurn, delta streaming, permission/ask-human prompts), read-only notes browser, PostHog events (platform=mobile), EAS build profiles - turn-follower moves from renderer into @x/shared (unchanged), so desktop and phone share the join/streaming protocol verbatim * fix(x): build @x/server and @x/client in CI build chains The new packages are gitignored dist consumers like shared/core, but the forge generateAssets hook and the root test/typecheck scripts didn't build them: the Electron package smoke test failed resolving @x/server from main, and the server vitest suite failed importing @x/client. * fix(x): typecheck:server needs @x/core built — add it to the typecheck chain * fix(x): review — forward migrated channels in packaged builds, refetch turns on WS reconnect - rpc-forwarder: forwarding defaults ON everywhere (packaged builds included) so production desktops exercise the HTTP path daily; ROWBOAT_FORWARD_MIGRATED=0 stays as the kill switch - turn-follower: followTurn now returns { stop, refetch }. refetch() forces a fresh snapshot — required for droppable transports where a turn that finished during an outage never emits another event, so offset-gap detection alone can leave it frozen mid-stream. The phone calls it from the events client's onResync; renderer behavior is unchanged. Covered by a new turn-follower test. * chore: retrigger CI * fix(x): rowboat-server hardening — two-sided lock, loud port collision, Host guard, symlink guard, WS header auth (#887) - the workdir lock moves into createRowboatServer itself, so the Electron-hosted transport and the standalone entrypoint contend for the same server.lock; EPERM on the pid probe counts as a live holder - EADDRINUSE on the configured port now probes the occupant's /health: another rowboat-server is a hard error, not a silent port+1 fallback - every HTTP request and WS upgrade validates the Host header against the machine's own names/addresses (DNS-rebinding defense-in-depth) - GET /workspace/{path} and the app://workspace protocol realpath the target and require containment in the workspace (symlink escape) - @x/client sends the WS token as an Authorization header on React Native; ?token= remains only for clients that cannot set headers * docs(x): client-server separation execution plan * feat(x): migrate Phase 1 read-only channels to rowboat-server (31 channels) (#888) SEPARATION_PLAN.md Phase 1: every pure-read query outside the connector, code-mode, apps, and client-local buckets now answers over the server — mcp/runs/sessions-queue/models/llm/rowboat/granola/knowledge-sources/ onboarding/agent-schedule/voice-config/live-note/todo/home/bg-task/ billing/credits/notifications/turn-limits/retention reads. Handlers are verbatim lifts from apps/main/src/ipc.ts; the desktop forwards them over localhost HTTP automatically (48 channels total on the frontier now). Verified live: per-channel smoke against the running app (200s across the board; domain errors surface as typed 500s), desktop UI driving models/ todos/home through the forwarded path. * feat(x): migrate Phase 2 write channels to rowboat-server (38 channels) (#889) SEPARATION_PLAN.md Phase 2: workspace writes (write/mkdir/rename/copy/ remove), knowledge version history + restore, knowledge-sources upsert, deck generation, the todo/home surface (add/run/chat/dismiss/archive/ planner), onboarding completion, and the notifications/turn-limits/ retention setters. workspace:pickImage and workspace:exportCopy stay client-local (native dialogs). 86 channels now on the frontier. Handlers are verbatim lifts from apps/main/src/ipc.ts. Verified live over HTTP against the running app: note write→read→rename→history→ remove round-trip, todo add→list→dismiss round-trip, settings get→set round-trip. * feat(x): migrate Phase 3a connector data channels to rowboat-server (45 channels) (#890) SEPARATION_PLAN.md Phase 3a: the connector data surface — gmail (all 23: inbox/search/drafts/labels/instructions/archive/importance), slack config/status/workspaces/channels/messages, channels (WhatsApp/Telegram) config+status+logout, chatgpt:getStatus, google-docs status/import/sync/ link, githubAuth poll/status/signOut, composio-google migration check. 131 channels now on the frontier. The Slack home parsing/enrichment helpers move from apps/main/src/ipc.ts into @x/core (slack/home-parse.ts) so main and the server share one implementation. Deliberately NOT in this PR (Phase 3b — flow relocation): oauth:* and composio:* (main-local oauth-handler/composio-handler with the browser + loopback dance), chatgpt:signIn/cancelSignIn/signOut, githubAuth:start (opens browser), google-docs:pickViaManaged. Slack desktop-import channels stay client-local permanently (they read the local Slack app — an edge capability). Verified live over the forwarded path: real Gmail inbox/search/labels, Slack workspaces/status, Google Docs status, channels status, GitHub auth status all answering through the server. * feat(x): Phase 3b — relocate OAuth/connector flows into core, migrate 19 flow channels (#898) SEPARATION_PLAN.md Phase 3b (RFC Q10): the interactive sign-in machinery leaves Electron main — - auth-server.ts → core auth/loopback-server.ts (pure node http) - oauth-handler.ts → core auth/oauth-flows.ts - chatgpt-signin.ts → core auth/chatgpt-signin.ts - composio-handler.ts → core composio/flows.ts - google-picker-managed.ts → core knowledge/google-picker-managed.ts Two new core seams replace the Electron touchpoints: - auth/url-opener.ts: registerUrlOpener / openExternalUrl / focusClient — opening a browser is a client capability (RFC Q14 in DI form; becomes a WS capability request after the flip). Main registers shell.openExternal + main-window focus. - auth/connector-events.ts: oauth/composio/chatgpt buses. Main relays them to windows (startConnectorEventsWatcher); the WS hub broadcasts them to network clients (new push channels oauth:didConnect, composio:didConnect, chatgpt:statusChanged). Migrated channels (150 total on the frontier now): oauth:connect/ disconnect/list-providers/getState, all 10 composio flow channels, chatgpt:signIn/cancelSignIn/signOut, githubAuth:start, google-docs:pickViaManaged. Verified end-to-end on a packaged build: disconnect + reconnect Google — server-side orchestration, browser via the seam, deep-link completion, tokens stored, gmail:getConnectionStatus green with full scopes. Note: the rowboat-mode deep-link return remains packaged-app-only in dev (pre-existing macOS protocol-registration behavior, unchanged here). * feat(x): migrate Phase 4 feature channels to rowboat-server (53 channels) (#899) SEPARATION_PLAN.md Phase 4: models/llm (test/generate/setProvider/ removeProvider/updateConfig), the whole apps surface (catalog/install/ update/publishUpdate/rollback/stars), legacy runs, session queue/steer (sendOrQueueMessage/editQueued/removeQueued), agent-schedule updates, search, meeting summarize, inline tasks, voice:synthesize, live-note run/set/setActive/delete/stop, bg-task run/create/patch/delete/stop, granola:setConfig, mcp:executeTool. 203 channels now on the frontier. Client-machine channels stay in main: recording/PTT/capture state, native notify, TTS streaming (event.sender-scoped), mic/camera permission prompts, save dialogs, apps:publish (window-bound flow). The two apps memo caches (install previews, catalog fingerprint) are replicated process-locally in core-deps — memoization only, no cross-process invariants. Verified live over the forwarded path: apps catalog + status, model connection test, one-shot llm:generate, search, meeting summarize, inline-task classification, live-note run, bg-task create/delete round-trip, queue/steer + runs error paths returning typed domain errors. * feat(x): Phase 5 — migrate code-mode & terminal to rowboat-server (23 channels) (#903) SEPARATION_PLAN.md Phase 5 (RFC Q13): the PTY moves with core — the terminal must show the machine code sessions execute on. - apps/main/src/terminal.ts → core terminal/terminal.ts; the window broadcast becomes subscribeTerminalEvents, relayed by main to windows (startTerminalEventsWatcher) and by the WS hub to network clients (new push channels terminal:data / terminal:exit, broadcast-to-all per Q12) - node-pty becomes a core dependency (stays external to the esbuild bundle as before) - migrated: codeMode config/status/model-options, codeProject add/remove/ list, codeSession create/list/update/delete/stop/gitStatus/fileDiff/ readdir/readFile/mergeBack/cleanupWorktree, codeRun:resolvePermission, terminal ensure/input/resize/dispose. codeMode:provisionEngine stays client-local (sender-scoped progress stream). 226 channels on the frontier. Verified live: PTY spawned via terminal:ensure over HTTP, 'echo hello-p5' typed via terminal:input, output received over the WS terminal:data feed, disposed cleanly; code-mode config/status/list channels answering. * feat(x): Phase 6 — schedulers and sync services boot in the standalone server (#905) SEPARATION_PLAN.md Phase 6: the headless-safe half of Rowboat's boot moves into core (boot/services.ts) — data repairs (runs migration, code-session backfill), retention sweep, mobile channels, live-note + bg-task schedulers, skills watcher, event processor + consumers, all knowledge sync services (gmail/outlook/calendars/fireflies/granola), graph builder, note tagging, inline tasks, agent runner, agent notes, calendar notifications, meeting prep, chrome-extension sync. Electron main and the standalone rowboat-server now call the same prepareCoreData()/initCoreServices() in the same order — main keeps only client-machine concerns (windows, tray, meeting detection, quick-ask, updater, capture) plus its window fan-out watchers. The standalone entrypoint is no longer a stub: it runs the full core, which is the child process the Phase 7 flip will spawn. Verified: standalone server on an isolated ROWBOAT_WORKDIR boots every scheduler/sync with no Electron (health + RPC answering, clean shutdown); Electron main boots identically to before on the shared module; split-brain lock correctly refused a second host during testing. * feat(x): Phase 7a — child-server mode + reverse-call protocol (flag-gated) (#906) * feat(x): Phase 7a — child-server mode behind ROWBOAT_CHILD_SERVER, reverse-call protocol The flip's machinery, default off until 7b parity: - WS reverse calls (RFC Q14): clients declare capabilities in hello; the hub routes capability-request to one capable client and awaits capability-response (30s default timeout; fire-and-forget variant for notifications). @x/client answers via a capabilities handler map. - The capability broker (apps/server/src/capabilities.ts) backs core's DI seams in the standalone server: notifications broadcast to capable clients, url-opener/focus and browser-control route as requests — failing loudly when no capable client is connected. - ROWBOAT_CHILD_SERVER=1: main spawns standalone.js as a child (ELECTRON_RUN_AS_NODE), waits for health, and becomes a client — the existing HTTP forwarder for calls, a WS events bridge relaying all push channels to windows, renderer delta subscriptions mirrored onto the wire, and Electron capability handlers (notifications, open-url, focus-client, browser-control). Core boot (data repairs, session index, schedulers, apps server) is skipped in main and owned by the child; the apps server moves into the shared boot module. Verified live under the flag: child spawns and runs every scheduler, main boots core-free, a real LLM turn completes end-to-end in the child over the forwarded path. Reverse-call protocol covered by an integration test (request/reply, no-capable-client rejection, handler error propagation). 19 server tests green; typecheck + lint clean. Known 7b gaps before flipping the default: streaming TTS and other sender-scoped handlers still call core in-process, packaged builds need the standalone entry shipped as a second artifact, and main-side core imports need a shadow-state audit. * fix(x): build @x/client in the forge packaging hook (main imports it now) * feat(x): Phase 7b — child-server mode becomes the default (#907) The flip completes: main spawns the standalone rowboat-server and runs as a pure client by default. ROWBOAT_CHILD_SERVER=0 is the kill switch back to in-process hosting for one release cycle. Parity work that made default-on possible: - streaming TTS moves server-side: voice:synthesizeStreamStart/Cancel are RPC channels; chunks publish on a core tts bus and fan out as the voice:tts-chunk push channel (main relays to windows in both modes, the WS hub to network clients; renderers already filter by requestId) - screen-pointer and text-insert become reverse-call capabilities: the standalone server registers broker-backed DI impls; Electron main advertises and answers them (sync isShareActive/isSupported degrade to capable-client presence — point/insert report truthfully) - packaged builds ship the server as a second esbuild artifact (.package/dist/rowboat-server.cjs, node-pty external as ever); launchChild resolves it in packaged mode, apps/server/dist in dev Known gap carried to Phase 8: token encryption at rest — the child has no safeStorage, so github/chatgpt token ciphers fall back to core's plaintext path (same file, same machine; encryption-at-rest lands with the remote-hosting hardening). Verified live with no flag: child spawns and owns core, a real LLM turn completes end-to-end, TTS stream RPC answers, kill-switch mode still boots in-process. 19 server tests green; typecheck + lint clean. * feat(x): Phase 8a — remote server mode, headless distribution, server-side workspace watcher (#909) - ROWBOAT_REMOTE_SERVER=<url> + ROWBOAT_REMOTE_TOKEN point the desktop at a rowboat-server on another machine: no child spawn, no local core; RPC, WS events, capabilities, and app://workspace file serving all target the remote - workspace watcher (debounced chokidar feed) moves from Electron main into the standalone server; workspace:didChange + knowledge:didCommit now ride the WS relay in child and remote modes (fixes knowledge:didCommit being dead since the 7b flip) - npm run build:headless (apps/server): self-contained rowboat-server.cjs + package.json for plain Node 22 on Linux; node-pty installed on the target - REMOTE_SERVER.md: EC2 + Tailscale setup, systemd unit, known 8b gaps * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay (#911) * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay for remote servers - file cipher (apps/server/file-cipher.ts): AES-256-GCM under a random key at <workdir>/cipher-key (0600); standalone wires it into the github/chatgpt token stores, replacing the plaintext fallback on headless hosts - loopback host seam in core (auth/loopback-server.ts): OAuth flows call openLoopback() instead of binding directly; a registered host can delegate the 127.0.0.1 callback listener to the machine whose browser gets the redirect. No host / no capable client → local bind, unchanged behaviour - loopback relay (apps/server/loopback-relay.ts): the standalone server asks a loopback-capable client over the WS reverse-call channel to bind the port ('loopback-bind'/'loopback-close'); the client relays each callback hit via the new oauth:deliverLoopbackCallback RPC, which runs the flow's validate/error/callback logic and answers with the page to render - desktop client hosts the relay listener (server-host.ts capabilities), reusing core's dual-family loopback binding and page rendering All OAuth flows (BYOK providers, composio, ChatGPT sign-in) now work with a remote rowboat-server: the browser opens on the client, the redirect lands on the client, the code reaches the server. * fix(x): build @x/core before test suites — apps/server tests now import its dist * fix(x): pass the relay option through createAuthServer createAuthServer rebuilds its CallbackHandlingOpts and dropped `relay`, so the client-side relay listener rendered a success page without forwarding the callback — the server's flow hung on 'waiting for sign in'. Found in the live AWS remote test; regression test added. * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle (#927) * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle The bundled child crashed at module init in packaged builds — core's skills module resolves a path from import.meta.url, which is undefined in the CJS bundle without the polyfill main.cjs already uses. Dev (unbundled) and the headless build were unaffected. Found by the packaged-app checklist. * fix(x): child server shutdown — destroy keep-alive sockets, hard-exit fallback, parent-death watch Quit left the child orphaned: httpServer.close() waited forever on the forwarder's keep-alive sockets, so the SIGTERM from before-quit never finished. close() now destroys connections; standalone adds a 5s hard-exit fallback and exits on its own when the parent pid disappears (crash / force-quit). Verified: AppleScript quit → no orphan + lock released; kill -9 of the app → child self-exits within 10s. * feat(x): Connect-to-server settings UI Remote mode no longer needs env vars: Settings → Phone gains a 'Connect to a server' section — enter the server's address and access code (~/.rowboat/server-key on that machine), the connection is validated (health + authed RPC), saved to config/client.json, and the app switches live (local child stopped, windows reloaded). Disconnect switches back. Env vars still win and lock the section when set. * feat(x): re-home Spaces into the split architecture Spaces landed on main mid-separation, built for in-process core. It is core-coupled (the topic agent runs turns through the session runtime; org tokens, mention offsets, and the blob cache live in the workdir), so it moves where core runs: - 35 spaces channels join the RPC allowlist, handlers lifted verbatim into apps/server/spaces-deps.ts; browser opens ride the url-opener seam - live space frames push over the WS hub as 'spaces:events' (desktop relays to windows); mention watcher starts with core services and notifies through the notification seam - new spaces:bounceLive channel: the desktop's powerMonitor wake signal reaches the server that owns the WebSockets - client-local pieces stay in main: save dialogs, link previews, blob thumbnails, the app://space-blob protocol - apps installer staging cleanup joins core boot Verified: full build/typecheck/lint/tests green; dev app boots and serves in child mode and against the AWS remote server. * fix(x): review findings — relay all renderer feeds over the WS bridge; base64 for pasted blob bytes 1. Ten renderer push feeds (todo, runs, code runs, code-session status, home threads, services, live-note/bg-task agents, channels status, credit activations) published to buses inside the child while the desktop relayed none of them — the renderer went quiet for anything it didn't refetch itself. They now ride the WS hub via one multiplexed EventSources feed (core-deps subscribeFeedEvents, which also starts the two trackers server-side), and the channel roster is a single shared const (@x/shared push-channels.ts) that the hub, the client type, and the desktop relay all derive from — the three lists can't drift again. Verified live: todo:addItem over RPC → todo:events observed on a WS probe. 2. spaces:uploadBlob pastes sent an ArrayBuffer, which JSON.stringify turns into '{}' on the /rpc hop — a zero-byte blob uploaded silently (drag-drop/picker take the filePath route and were unaffected). Bytes now cross as base64, decoded where core runs; the schema says so instead of z.custom<ArrayBuffer>() accepting anything. | 5 天前 | |
arch: client–server separation — merge to main (Phase 9) (#929) * feat(x): rowboat-server vertical slice + iOS companion app (#758) * feat(x): rowboat-server vertical slice + iOS companion app Implements the phone-client slice of the server/client RFC (#700): - apps/server (@x/server): HTTP transport generated from ipcSchemas (POST /rpc/{channel}, allowlisted subset), server-key bearer auth, WS /events hub (per-connection seq, durable broadcast + per-turn delta subscriptions, hello handshake), authenticated workspace file route, headless standalone entrypoint with pid lockfile - main hosts the transport in-process on its single core instance and forwards migrated channels over localhost HTTP (strangler-fig, ROWBOAT_FORWARD_MIGRATED kill switch, on by default in dev) - Phone-app settings tab: QR pairing, LAN opt-in, key rotation - packages/client (@x/client): portable typed RPC + reconnecting WS client (seq-gap detection, refcounted delta subs, 401/4401 handling) - apps/mobile (@x/mobile): Expo iOS app — QR/manual/dev-link pairing, sessions list, live chat (shared turn-follower + reduceTurn, delta streaming, permission/ask-human prompts), read-only notes browser, PostHog events (platform=mobile), EAS build profiles - turn-follower moves from renderer into @x/shared (unchanged), so desktop and phone share the join/streaming protocol verbatim * fix(x): build @x/server and @x/client in CI build chains The new packages are gitignored dist consumers like shared/core, but the forge generateAssets hook and the root test/typecheck scripts didn't build them: the Electron package smoke test failed resolving @x/server from main, and the server vitest suite failed importing @x/client. * fix(x): typecheck:server needs @x/core built — add it to the typecheck chain * fix(x): review — forward migrated channels in packaged builds, refetch turns on WS reconnect - rpc-forwarder: forwarding defaults ON everywhere (packaged builds included) so production desktops exercise the HTTP path daily; ROWBOAT_FORWARD_MIGRATED=0 stays as the kill switch - turn-follower: followTurn now returns { stop, refetch }. refetch() forces a fresh snapshot — required for droppable transports where a turn that finished during an outage never emits another event, so offset-gap detection alone can leave it frozen mid-stream. The phone calls it from the events client's onResync; renderer behavior is unchanged. Covered by a new turn-follower test. * chore: retrigger CI * fix(x): rowboat-server hardening — two-sided lock, loud port collision, Host guard, symlink guard, WS header auth (#887) - the workdir lock moves into createRowboatServer itself, so the Electron-hosted transport and the standalone entrypoint contend for the same server.lock; EPERM on the pid probe counts as a live holder - EADDRINUSE on the configured port now probes the occupant's /health: another rowboat-server is a hard error, not a silent port+1 fallback - every HTTP request and WS upgrade validates the Host header against the machine's own names/addresses (DNS-rebinding defense-in-depth) - GET /workspace/{path} and the app://workspace protocol realpath the target and require containment in the workspace (symlink escape) - @x/client sends the WS token as an Authorization header on React Native; ?token= remains only for clients that cannot set headers * docs(x): client-server separation execution plan * feat(x): migrate Phase 1 read-only channels to rowboat-server (31 channels) (#888) SEPARATION_PLAN.md Phase 1: every pure-read query outside the connector, code-mode, apps, and client-local buckets now answers over the server — mcp/runs/sessions-queue/models/llm/rowboat/granola/knowledge-sources/ onboarding/agent-schedule/voice-config/live-note/todo/home/bg-task/ billing/credits/notifications/turn-limits/retention reads. Handlers are verbatim lifts from apps/main/src/ipc.ts; the desktop forwards them over localhost HTTP automatically (48 channels total on the frontier now). Verified live: per-channel smoke against the running app (200s across the board; domain errors surface as typed 500s), desktop UI driving models/ todos/home through the forwarded path. * feat(x): migrate Phase 2 write channels to rowboat-server (38 channels) (#889) SEPARATION_PLAN.md Phase 2: workspace writes (write/mkdir/rename/copy/ remove), knowledge version history + restore, knowledge-sources upsert, deck generation, the todo/home surface (add/run/chat/dismiss/archive/ planner), onboarding completion, and the notifications/turn-limits/ retention setters. workspace:pickImage and workspace:exportCopy stay client-local (native dialogs). 86 channels now on the frontier. Handlers are verbatim lifts from apps/main/src/ipc.ts. Verified live over HTTP against the running app: note write→read→rename→history→ remove round-trip, todo add→list→dismiss round-trip, settings get→set round-trip. * feat(x): migrate Phase 3a connector data channels to rowboat-server (45 channels) (#890) SEPARATION_PLAN.md Phase 3a: the connector data surface — gmail (all 23: inbox/search/drafts/labels/instructions/archive/importance), slack config/status/workspaces/channels/messages, channels (WhatsApp/Telegram) config+status+logout, chatgpt:getStatus, google-docs status/import/sync/ link, githubAuth poll/status/signOut, composio-google migration check. 131 channels now on the frontier. The Slack home parsing/enrichment helpers move from apps/main/src/ipc.ts into @x/core (slack/home-parse.ts) so main and the server share one implementation. Deliberately NOT in this PR (Phase 3b — flow relocation): oauth:* and composio:* (main-local oauth-handler/composio-handler with the browser + loopback dance), chatgpt:signIn/cancelSignIn/signOut, githubAuth:start (opens browser), google-docs:pickViaManaged. Slack desktop-import channels stay client-local permanently (they read the local Slack app — an edge capability). Verified live over the forwarded path: real Gmail inbox/search/labels, Slack workspaces/status, Google Docs status, channels status, GitHub auth status all answering through the server. * feat(x): Phase 3b — relocate OAuth/connector flows into core, migrate 19 flow channels (#898) SEPARATION_PLAN.md Phase 3b (RFC Q10): the interactive sign-in machinery leaves Electron main — - auth-server.ts → core auth/loopback-server.ts (pure node http) - oauth-handler.ts → core auth/oauth-flows.ts - chatgpt-signin.ts → core auth/chatgpt-signin.ts - composio-handler.ts → core composio/flows.ts - google-picker-managed.ts → core knowledge/google-picker-managed.ts Two new core seams replace the Electron touchpoints: - auth/url-opener.ts: registerUrlOpener / openExternalUrl / focusClient — opening a browser is a client capability (RFC Q14 in DI form; becomes a WS capability request after the flip). Main registers shell.openExternal + main-window focus. - auth/connector-events.ts: oauth/composio/chatgpt buses. Main relays them to windows (startConnectorEventsWatcher); the WS hub broadcasts them to network clients (new push channels oauth:didConnect, composio:didConnect, chatgpt:statusChanged). Migrated channels (150 total on the frontier now): oauth:connect/ disconnect/list-providers/getState, all 10 composio flow channels, chatgpt:signIn/cancelSignIn/signOut, githubAuth:start, google-docs:pickViaManaged. Verified end-to-end on a packaged build: disconnect + reconnect Google — server-side orchestration, browser via the seam, deep-link completion, tokens stored, gmail:getConnectionStatus green with full scopes. Note: the rowboat-mode deep-link return remains packaged-app-only in dev (pre-existing macOS protocol-registration behavior, unchanged here). * feat(x): migrate Phase 4 feature channels to rowboat-server (53 channels) (#899) SEPARATION_PLAN.md Phase 4: models/llm (test/generate/setProvider/ removeProvider/updateConfig), the whole apps surface (catalog/install/ update/publishUpdate/rollback/stars), legacy runs, session queue/steer (sendOrQueueMessage/editQueued/removeQueued), agent-schedule updates, search, meeting summarize, inline tasks, voice:synthesize, live-note run/set/setActive/delete/stop, bg-task run/create/patch/delete/stop, granola:setConfig, mcp:executeTool. 203 channels now on the frontier. Client-machine channels stay in main: recording/PTT/capture state, native notify, TTS streaming (event.sender-scoped), mic/camera permission prompts, save dialogs, apps:publish (window-bound flow). The two apps memo caches (install previews, catalog fingerprint) are replicated process-locally in core-deps — memoization only, no cross-process invariants. Verified live over the forwarded path: apps catalog + status, model connection test, one-shot llm:generate, search, meeting summarize, inline-task classification, live-note run, bg-task create/delete round-trip, queue/steer + runs error paths returning typed domain errors. * feat(x): Phase 5 — migrate code-mode & terminal to rowboat-server (23 channels) (#903) SEPARATION_PLAN.md Phase 5 (RFC Q13): the PTY moves with core — the terminal must show the machine code sessions execute on. - apps/main/src/terminal.ts → core terminal/terminal.ts; the window broadcast becomes subscribeTerminalEvents, relayed by main to windows (startTerminalEventsWatcher) and by the WS hub to network clients (new push channels terminal:data / terminal:exit, broadcast-to-all per Q12) - node-pty becomes a core dependency (stays external to the esbuild bundle as before) - migrated: codeMode config/status/model-options, codeProject add/remove/ list, codeSession create/list/update/delete/stop/gitStatus/fileDiff/ readdir/readFile/mergeBack/cleanupWorktree, codeRun:resolvePermission, terminal ensure/input/resize/dispose. codeMode:provisionEngine stays client-local (sender-scoped progress stream). 226 channels on the frontier. Verified live: PTY spawned via terminal:ensure over HTTP, 'echo hello-p5' typed via terminal:input, output received over the WS terminal:data feed, disposed cleanly; code-mode config/status/list channels answering. * feat(x): Phase 6 — schedulers and sync services boot in the standalone server (#905) SEPARATION_PLAN.md Phase 6: the headless-safe half of Rowboat's boot moves into core (boot/services.ts) — data repairs (runs migration, code-session backfill), retention sweep, mobile channels, live-note + bg-task schedulers, skills watcher, event processor + consumers, all knowledge sync services (gmail/outlook/calendars/fireflies/granola), graph builder, note tagging, inline tasks, agent runner, agent notes, calendar notifications, meeting prep, chrome-extension sync. Electron main and the standalone rowboat-server now call the same prepareCoreData()/initCoreServices() in the same order — main keeps only client-machine concerns (windows, tray, meeting detection, quick-ask, updater, capture) plus its window fan-out watchers. The standalone entrypoint is no longer a stub: it runs the full core, which is the child process the Phase 7 flip will spawn. Verified: standalone server on an isolated ROWBOAT_WORKDIR boots every scheduler/sync with no Electron (health + RPC answering, clean shutdown); Electron main boots identically to before on the shared module; split-brain lock correctly refused a second host during testing. * feat(x): Phase 7a — child-server mode + reverse-call protocol (flag-gated) (#906) * feat(x): Phase 7a — child-server mode behind ROWBOAT_CHILD_SERVER, reverse-call protocol The flip's machinery, default off until 7b parity: - WS reverse calls (RFC Q14): clients declare capabilities in hello; the hub routes capability-request to one capable client and awaits capability-response (30s default timeout; fire-and-forget variant for notifications). @x/client answers via a capabilities handler map. - The capability broker (apps/server/src/capabilities.ts) backs core's DI seams in the standalone server: notifications broadcast to capable clients, url-opener/focus and browser-control route as requests — failing loudly when no capable client is connected. - ROWBOAT_CHILD_SERVER=1: main spawns standalone.js as a child (ELECTRON_RUN_AS_NODE), waits for health, and becomes a client — the existing HTTP forwarder for calls, a WS events bridge relaying all push channels to windows, renderer delta subscriptions mirrored onto the wire, and Electron capability handlers (notifications, open-url, focus-client, browser-control). Core boot (data repairs, session index, schedulers, apps server) is skipped in main and owned by the child; the apps server moves into the shared boot module. Verified live under the flag: child spawns and runs every scheduler, main boots core-free, a real LLM turn completes end-to-end in the child over the forwarded path. Reverse-call protocol covered by an integration test (request/reply, no-capable-client rejection, handler error propagation). 19 server tests green; typecheck + lint clean. Known 7b gaps before flipping the default: streaming TTS and other sender-scoped handlers still call core in-process, packaged builds need the standalone entry shipped as a second artifact, and main-side core imports need a shadow-state audit. * fix(x): build @x/client in the forge packaging hook (main imports it now) * feat(x): Phase 7b — child-server mode becomes the default (#907) The flip completes: main spawns the standalone rowboat-server and runs as a pure client by default. ROWBOAT_CHILD_SERVER=0 is the kill switch back to in-process hosting for one release cycle. Parity work that made default-on possible: - streaming TTS moves server-side: voice:synthesizeStreamStart/Cancel are RPC channels; chunks publish on a core tts bus and fan out as the voice:tts-chunk push channel (main relays to windows in both modes, the WS hub to network clients; renderers already filter by requestId) - screen-pointer and text-insert become reverse-call capabilities: the standalone server registers broker-backed DI impls; Electron main advertises and answers them (sync isShareActive/isSupported degrade to capable-client presence — point/insert report truthfully) - packaged builds ship the server as a second esbuild artifact (.package/dist/rowboat-server.cjs, node-pty external as ever); launchChild resolves it in packaged mode, apps/server/dist in dev Known gap carried to Phase 8: token encryption at rest — the child has no safeStorage, so github/chatgpt token ciphers fall back to core's plaintext path (same file, same machine; encryption-at-rest lands with the remote-hosting hardening). Verified live with no flag: child spawns and owns core, a real LLM turn completes end-to-end, TTS stream RPC answers, kill-switch mode still boots in-process. 19 server tests green; typecheck + lint clean. * feat(x): Phase 8a — remote server mode, headless distribution, server-side workspace watcher (#909) - ROWBOAT_REMOTE_SERVER=<url> + ROWBOAT_REMOTE_TOKEN point the desktop at a rowboat-server on another machine: no child spawn, no local core; RPC, WS events, capabilities, and app://workspace file serving all target the remote - workspace watcher (debounced chokidar feed) moves from Electron main into the standalone server; workspace:didChange + knowledge:didCommit now ride the WS relay in child and remote modes (fixes knowledge:didCommit being dead since the 7b flip) - npm run build:headless (apps/server): self-contained rowboat-server.cjs + package.json for plain Node 22 on Linux; node-pty installed on the target - REMOTE_SERVER.md: EC2 + Tailscale setup, systemd unit, known 8b gaps * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay (#911) * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay for remote servers - file cipher (apps/server/file-cipher.ts): AES-256-GCM under a random key at <workdir>/cipher-key (0600); standalone wires it into the github/chatgpt token stores, replacing the plaintext fallback on headless hosts - loopback host seam in core (auth/loopback-server.ts): OAuth flows call openLoopback() instead of binding directly; a registered host can delegate the 127.0.0.1 callback listener to the machine whose browser gets the redirect. No host / no capable client → local bind, unchanged behaviour - loopback relay (apps/server/loopback-relay.ts): the standalone server asks a loopback-capable client over the WS reverse-call channel to bind the port ('loopback-bind'/'loopback-close'); the client relays each callback hit via the new oauth:deliverLoopbackCallback RPC, which runs the flow's validate/error/callback logic and answers with the page to render - desktop client hosts the relay listener (server-host.ts capabilities), reusing core's dual-family loopback binding and page rendering All OAuth flows (BYOK providers, composio, ChatGPT sign-in) now work with a remote rowboat-server: the browser opens on the client, the redirect lands on the client, the code reaches the server. * fix(x): build @x/core before test suites — apps/server tests now import its dist * fix(x): pass the relay option through createAuthServer createAuthServer rebuilds its CallbackHandlingOpts and dropped `relay`, so the client-side relay listener rendered a success page without forwarding the callback — the server's flow hung on 'waiting for sign in'. Found in the live AWS remote test; regression test added. * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle (#927) * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle The bundled child crashed at module init in packaged builds — core's skills module resolves a path from import.meta.url, which is undefined in the CJS bundle without the polyfill main.cjs already uses. Dev (unbundled) and the headless build were unaffected. Found by the packaged-app checklist. * fix(x): child server shutdown — destroy keep-alive sockets, hard-exit fallback, parent-death watch Quit left the child orphaned: httpServer.close() waited forever on the forwarder's keep-alive sockets, so the SIGTERM from before-quit never finished. close() now destroys connections; standalone adds a 5s hard-exit fallback and exits on its own when the parent pid disappears (crash / force-quit). Verified: AppleScript quit → no orphan + lock released; kill -9 of the app → child self-exits within 10s. * feat(x): Connect-to-server settings UI Remote mode no longer needs env vars: Settings → Phone gains a 'Connect to a server' section — enter the server's address and access code (~/.rowboat/server-key on that machine), the connection is validated (health + authed RPC), saved to config/client.json, and the app switches live (local child stopped, windows reloaded). Disconnect switches back. Env vars still win and lock the section when set. * feat(x): re-home Spaces into the split architecture Spaces landed on main mid-separation, built for in-process core. It is core-coupled (the topic agent runs turns through the session runtime; org tokens, mention offsets, and the blob cache live in the workdir), so it moves where core runs: - 35 spaces channels join the RPC allowlist, handlers lifted verbatim into apps/server/spaces-deps.ts; browser opens ride the url-opener seam - live space frames push over the WS hub as 'spaces:events' (desktop relays to windows); mention watcher starts with core services and notifies through the notification seam - new spaces:bounceLive channel: the desktop's powerMonitor wake signal reaches the server that owns the WebSockets - client-local pieces stay in main: save dialogs, link previews, blob thumbnails, the app://space-blob protocol - apps installer staging cleanup joins core boot Verified: full build/typecheck/lint/tests green; dev app boots and serves in child mode and against the AWS remote server. * fix(x): review findings — relay all renderer feeds over the WS bridge; base64 for pasted blob bytes 1. Ten renderer push feeds (todo, runs, code runs, code-session status, home threads, services, live-note/bg-task agents, channels status, credit activations) published to buses inside the child while the desktop relayed none of them — the renderer went quiet for anything it didn't refetch itself. They now ride the WS hub via one multiplexed EventSources feed (core-deps subscribeFeedEvents, which also starts the two trackers server-side), and the channel roster is a single shared const (@x/shared push-channels.ts) that the hub, the client type, and the desktop relay all derive from — the three lists can't drift again. Verified live: todo:addItem over RPC → todo:events observed on a WS probe. 2. spaces:uploadBlob pastes sent an ArrayBuffer, which JSON.stringify turns into '{}' on the /rpc hop — a zero-byte blob uploaded silently (drag-drop/picker take the filePath route and were unaffected). Bytes now cross as base64, decoded where core runs; the schema says so instead of z.custom<ArrayBuffer>() accepting anything. | 5 天前 | |
fix(x): whiteboard saves can no longer persist an empty scene over a real board (#948) A board's snapshot reached {"elements":[]} in prod after holding drawings: the pane read the scene through the Excalidraw API at save time, and three Excalidraw behaviors make that read unsafe — the excalidrawAPI callback fires from the App constructor while the scene is still empty, initialData hydrates async and fires onChange itself (so every open marked the board dirty), and componentWillUnmount swaps in a fresh empty scene the API keeps answering for. Open a board, close it, and the unmount flush proposed the destroyed scene's [] over the stored content. Snapshot persistence now lives in lib/whiteboard-saver.ts, a pure state machine the pane wires up only after the snapshot loads: it accepts a local change only when getSceneVersion advances past the hydrated scene's (the hydration echo and pre-hydration empty scenes gate out; clear-canvas still advances because deletion bumps element versions), serializes the last scene it accepted instead of re-reading the editor, and dispose() blocks pending timers and in-flight continuations so a dead pane never writes. Regression tests cover the wipe scenarios, the conflict/merged retry loop, and the legitimate clear. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 4 天前 | |
fix(x): one hover companion — delete the summoned ask bar, no flash, no dead summons (#878) * fix(x): hover companion — no flash of the old bar, no dead summons, one flow for every entry point The ⌥⇧Space hover mode (quick access) had three real bugs: 1. The first summon created the companion window and show()ed it while the page was still loading: index.html painted a theme slab, then the renderer's default summoned layout (the old text bar), then the Skipper. Re-summons flashed the summoned layout for a frame too (the hidden window keeps it, and show() fired before the re-render). 2. startHoverCall held its in-flight guard across the sticky screen share; getDisplayMedia hangs forever when Screen Recording is denied, so after the first summon every later chord was a silent no-op. 3. QuickAskBar called useMemo after conditional early returns — a pill ⇄ card switch (camera toggle) would throw React's hook-count error and blank the window. Fixes: - Reveal protocol: quick-ask:mode carries a seq, the renderer acks quickAsk:modeApplied two frames after committing that role, and main orders the window in at opacity 0 and only makes it opaque / focuses on the ack. Fold pushes first and shrinks on the ack. The renderer paints nothing until its role is known; index.html gives #quick-ask and #screen-pointer a transparent background from the very first paint. - startScreenShare is time-boxed (10s) and fails cleanly; the hover guard is released right after startCall and the share is fire-and-forget; a session that fails to start falls back to the text card. - ONE hover flow: chord, tray, tuck handle, toast "Try it", composer call button and the "Share screen" preset all end in startHoverCall(). A summon with no (or a still-loading) app window recreates it hidden and re-relays on a quickAsk:appReady handshake; closing the app window mid-call unpins the companion. Call-state cache survives fullscreen ⇄ popout (camera calls return as the pill) and is cleared by an explicit idle push at call end. Hooks hoisted above the early returns. - [companion] mode-transition breadcrumbs in main; VIDEO_MODE.md updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(x): a summon right after app start waits for the voice probe instead of answering with the text card The remaining "old quick access comes up instead of hover mode" case: voiceAvailable/ttsAvailable start false and are filled by an async probe (voice:getConfig + oauth:getState) at mount. A chord in the first seconds after reopening the app read that not-yet-resolved false as "no voice configured" and answered with quickAsk:show — the old text card, with no Skipper ever arriving. Reproduced deterministically by delaying the probe: main logged `hidden → summoned (text card (fresh window))`. With this change the same run logs `hidden → pinned (call surface (summoned))`. - App: mirror the probe result into refs (written inside the probe, so a waiter doesn't depend on a re-render), keep the in-flight promise, and have startHoverCall await it (capped at 4s, starting one if nothing has probed yet) before deciding. The in-flight guard now covers that wait, and a call started meanwhile just re-asserts its floating surface. A genuinely unconfigured voice still falls back to the text card. - Bar: the tuck handle is optimistic until its own probe answers — the app window owns the authoritative check and its own fallback, so an early click relays instead of dying as a dimmed no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(x): delete the summoned ask bar — the hover companion is the only surface The old Spotlight-style quick-ask bar is gone, not just avoided. It was only ever a fallback surface, and every "hover mode is glitchy" report was that bar appearing where the Skipper belonged. With one role left there is no second layout to flash, race, or get stuck in. Removed: - main: the `summoned` mode, its geometry (positionSummoned, FRAME_*, BOTTOM_MARGIN), showSummonedCard/showQuickAsk/hideQuickAsk, the blur-to-dismiss handler, and the trigger enum. `toggleQuickAsk()` now has exactly one outcome: fold/unfold the Skipper's text when it's up, else relay a summon. An unanswered relay is re-sent once by a watchdog and then logged — never answered with a different surface. - IPC: quickAsk:show / quickAsk:hide / quickAsk:stop + quick-ask:stop / quickAsk:state + quick-ask:state / quickAsk:setOptions + quick-ask:set-options / quickAsk:optionsState + quick-ask:options-state / quick-ask:summoned. CompanionMode is now 'hidden' | 'pinned'. - renderer: the bar layout, its answer panel, local dictation and the ⌥⇧Space hold-to-talk chord, the voice-out and share-without-a-call toggles, dismiss/stage-click-to-dismiss, and the tuck handle's start-a-call role. The window paints NOTHING unless it is pinned. - App: quickAskActiveRef / quickAskOptionsRef / quickAskStartedAtRef / speakTurnRef and the answer-mirror effect. Speech now follows the question's modality alone. A summon that can't become a session (voice unconfigured, or the engine failed to start) is explained in the APP window — brought to the front with a toast that opens Settings — instead of silently becoming a different floating thing. Verified on the dev build: earliest-possible summon after an app start lands as the Skipper (no other layout at any frame), fold/unfold/end/ re-summon cycle clean, hidden window renders 0 bytes, and the no-voice path shows the toast with the companion staying hidden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(x): retire the last references to the deleted ask bar Comment- and copy-only follow-up to 5eeb56c9: several comments still described the summoned bar's status line, voice toggle, destination chip and geometry, and two user-facing strings still called the feature "Quick Ask". Nothing behavioural. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 16 天前 | |
add gif | 1 个月前 | |
arch: client–server separation — merge to main (Phase 9) (#929) * feat(x): rowboat-server vertical slice + iOS companion app (#758) * feat(x): rowboat-server vertical slice + iOS companion app Implements the phone-client slice of the server/client RFC (#700): - apps/server (@x/server): HTTP transport generated from ipcSchemas (POST /rpc/{channel}, allowlisted subset), server-key bearer auth, WS /events hub (per-connection seq, durable broadcast + per-turn delta subscriptions, hello handshake), authenticated workspace file route, headless standalone entrypoint with pid lockfile - main hosts the transport in-process on its single core instance and forwards migrated channels over localhost HTTP (strangler-fig, ROWBOAT_FORWARD_MIGRATED kill switch, on by default in dev) - Phone-app settings tab: QR pairing, LAN opt-in, key rotation - packages/client (@x/client): portable typed RPC + reconnecting WS client (seq-gap detection, refcounted delta subs, 401/4401 handling) - apps/mobile (@x/mobile): Expo iOS app — QR/manual/dev-link pairing, sessions list, live chat (shared turn-follower + reduceTurn, delta streaming, permission/ask-human prompts), read-only notes browser, PostHog events (platform=mobile), EAS build profiles - turn-follower moves from renderer into @x/shared (unchanged), so desktop and phone share the join/streaming protocol verbatim * fix(x): build @x/server and @x/client in CI build chains The new packages are gitignored dist consumers like shared/core, but the forge generateAssets hook and the root test/typecheck scripts didn't build them: the Electron package smoke test failed resolving @x/server from main, and the server vitest suite failed importing @x/client. * fix(x): typecheck:server needs @x/core built — add it to the typecheck chain * fix(x): review — forward migrated channels in packaged builds, refetch turns on WS reconnect - rpc-forwarder: forwarding defaults ON everywhere (packaged builds included) so production desktops exercise the HTTP path daily; ROWBOAT_FORWARD_MIGRATED=0 stays as the kill switch - turn-follower: followTurn now returns { stop, refetch }. refetch() forces a fresh snapshot — required for droppable transports where a turn that finished during an outage never emits another event, so offset-gap detection alone can leave it frozen mid-stream. The phone calls it from the events client's onResync; renderer behavior is unchanged. Covered by a new turn-follower test. * chore: retrigger CI * fix(x): rowboat-server hardening — two-sided lock, loud port collision, Host guard, symlink guard, WS header auth (#887) - the workdir lock moves into createRowboatServer itself, so the Electron-hosted transport and the standalone entrypoint contend for the same server.lock; EPERM on the pid probe counts as a live holder - EADDRINUSE on the configured port now probes the occupant's /health: another rowboat-server is a hard error, not a silent port+1 fallback - every HTTP request and WS upgrade validates the Host header against the machine's own names/addresses (DNS-rebinding defense-in-depth) - GET /workspace/{path} and the app://workspace protocol realpath the target and require containment in the workspace (symlink escape) - @x/client sends the WS token as an Authorization header on React Native; ?token= remains only for clients that cannot set headers * docs(x): client-server separation execution plan * feat(x): migrate Phase 1 read-only channels to rowboat-server (31 channels) (#888) SEPARATION_PLAN.md Phase 1: every pure-read query outside the connector, code-mode, apps, and client-local buckets now answers over the server — mcp/runs/sessions-queue/models/llm/rowboat/granola/knowledge-sources/ onboarding/agent-schedule/voice-config/live-note/todo/home/bg-task/ billing/credits/notifications/turn-limits/retention reads. Handlers are verbatim lifts from apps/main/src/ipc.ts; the desktop forwards them over localhost HTTP automatically (48 channels total on the frontier now). Verified live: per-channel smoke against the running app (200s across the board; domain errors surface as typed 500s), desktop UI driving models/ todos/home through the forwarded path. * feat(x): migrate Phase 2 write channels to rowboat-server (38 channels) (#889) SEPARATION_PLAN.md Phase 2: workspace writes (write/mkdir/rename/copy/ remove), knowledge version history + restore, knowledge-sources upsert, deck generation, the todo/home surface (add/run/chat/dismiss/archive/ planner), onboarding completion, and the notifications/turn-limits/ retention setters. workspace:pickImage and workspace:exportCopy stay client-local (native dialogs). 86 channels now on the frontier. Handlers are verbatim lifts from apps/main/src/ipc.ts. Verified live over HTTP against the running app: note write→read→rename→history→ remove round-trip, todo add→list→dismiss round-trip, settings get→set round-trip. * feat(x): migrate Phase 3a connector data channels to rowboat-server (45 channels) (#890) SEPARATION_PLAN.md Phase 3a: the connector data surface — gmail (all 23: inbox/search/drafts/labels/instructions/archive/importance), slack config/status/workspaces/channels/messages, channels (WhatsApp/Telegram) config+status+logout, chatgpt:getStatus, google-docs status/import/sync/ link, githubAuth poll/status/signOut, composio-google migration check. 131 channels now on the frontier. The Slack home parsing/enrichment helpers move from apps/main/src/ipc.ts into @x/core (slack/home-parse.ts) so main and the server share one implementation. Deliberately NOT in this PR (Phase 3b — flow relocation): oauth:* and composio:* (main-local oauth-handler/composio-handler with the browser + loopback dance), chatgpt:signIn/cancelSignIn/signOut, githubAuth:start (opens browser), google-docs:pickViaManaged. Slack desktop-import channels stay client-local permanently (they read the local Slack app — an edge capability). Verified live over the forwarded path: real Gmail inbox/search/labels, Slack workspaces/status, Google Docs status, channels status, GitHub auth status all answering through the server. * feat(x): Phase 3b — relocate OAuth/connector flows into core, migrate 19 flow channels (#898) SEPARATION_PLAN.md Phase 3b (RFC Q10): the interactive sign-in machinery leaves Electron main — - auth-server.ts → core auth/loopback-server.ts (pure node http) - oauth-handler.ts → core auth/oauth-flows.ts - chatgpt-signin.ts → core auth/chatgpt-signin.ts - composio-handler.ts → core composio/flows.ts - google-picker-managed.ts → core knowledge/google-picker-managed.ts Two new core seams replace the Electron touchpoints: - auth/url-opener.ts: registerUrlOpener / openExternalUrl / focusClient — opening a browser is a client capability (RFC Q14 in DI form; becomes a WS capability request after the flip). Main registers shell.openExternal + main-window focus. - auth/connector-events.ts: oauth/composio/chatgpt buses. Main relays them to windows (startConnectorEventsWatcher); the WS hub broadcasts them to network clients (new push channels oauth:didConnect, composio:didConnect, chatgpt:statusChanged). Migrated channels (150 total on the frontier now): oauth:connect/ disconnect/list-providers/getState, all 10 composio flow channels, chatgpt:signIn/cancelSignIn/signOut, githubAuth:start, google-docs:pickViaManaged. Verified end-to-end on a packaged build: disconnect + reconnect Google — server-side orchestration, browser via the seam, deep-link completion, tokens stored, gmail:getConnectionStatus green with full scopes. Note: the rowboat-mode deep-link return remains packaged-app-only in dev (pre-existing macOS protocol-registration behavior, unchanged here). * feat(x): migrate Phase 4 feature channels to rowboat-server (53 channels) (#899) SEPARATION_PLAN.md Phase 4: models/llm (test/generate/setProvider/ removeProvider/updateConfig), the whole apps surface (catalog/install/ update/publishUpdate/rollback/stars), legacy runs, session queue/steer (sendOrQueueMessage/editQueued/removeQueued), agent-schedule updates, search, meeting summarize, inline tasks, voice:synthesize, live-note run/set/setActive/delete/stop, bg-task run/create/patch/delete/stop, granola:setConfig, mcp:executeTool. 203 channels now on the frontier. Client-machine channels stay in main: recording/PTT/capture state, native notify, TTS streaming (event.sender-scoped), mic/camera permission prompts, save dialogs, apps:publish (window-bound flow). The two apps memo caches (install previews, catalog fingerprint) are replicated process-locally in core-deps — memoization only, no cross-process invariants. Verified live over the forwarded path: apps catalog + status, model connection test, one-shot llm:generate, search, meeting summarize, inline-task classification, live-note run, bg-task create/delete round-trip, queue/steer + runs error paths returning typed domain errors. * feat(x): Phase 5 — migrate code-mode & terminal to rowboat-server (23 channels) (#903) SEPARATION_PLAN.md Phase 5 (RFC Q13): the PTY moves with core — the terminal must show the machine code sessions execute on. - apps/main/src/terminal.ts → core terminal/terminal.ts; the window broadcast becomes subscribeTerminalEvents, relayed by main to windows (startTerminalEventsWatcher) and by the WS hub to network clients (new push channels terminal:data / terminal:exit, broadcast-to-all per Q12) - node-pty becomes a core dependency (stays external to the esbuild bundle as before) - migrated: codeMode config/status/model-options, codeProject add/remove/ list, codeSession create/list/update/delete/stop/gitStatus/fileDiff/ readdir/readFile/mergeBack/cleanupWorktree, codeRun:resolvePermission, terminal ensure/input/resize/dispose. codeMode:provisionEngine stays client-local (sender-scoped progress stream). 226 channels on the frontier. Verified live: PTY spawned via terminal:ensure over HTTP, 'echo hello-p5' typed via terminal:input, output received over the WS terminal:data feed, disposed cleanly; code-mode config/status/list channels answering. * feat(x): Phase 6 — schedulers and sync services boot in the standalone server (#905) SEPARATION_PLAN.md Phase 6: the headless-safe half of Rowboat's boot moves into core (boot/services.ts) — data repairs (runs migration, code-session backfill), retention sweep, mobile channels, live-note + bg-task schedulers, skills watcher, event processor + consumers, all knowledge sync services (gmail/outlook/calendars/fireflies/granola), graph builder, note tagging, inline tasks, agent runner, agent notes, calendar notifications, meeting prep, chrome-extension sync. Electron main and the standalone rowboat-server now call the same prepareCoreData()/initCoreServices() in the same order — main keeps only client-machine concerns (windows, tray, meeting detection, quick-ask, updater, capture) plus its window fan-out watchers. The standalone entrypoint is no longer a stub: it runs the full core, which is the child process the Phase 7 flip will spawn. Verified: standalone server on an isolated ROWBOAT_WORKDIR boots every scheduler/sync with no Electron (health + RPC answering, clean shutdown); Electron main boots identically to before on the shared module; split-brain lock correctly refused a second host during testing. * feat(x): Phase 7a — child-server mode + reverse-call protocol (flag-gated) (#906) * feat(x): Phase 7a — child-server mode behind ROWBOAT_CHILD_SERVER, reverse-call protocol The flip's machinery, default off until 7b parity: - WS reverse calls (RFC Q14): clients declare capabilities in hello; the hub routes capability-request to one capable client and awaits capability-response (30s default timeout; fire-and-forget variant for notifications). @x/client answers via a capabilities handler map. - The capability broker (apps/server/src/capabilities.ts) backs core's DI seams in the standalone server: notifications broadcast to capable clients, url-opener/focus and browser-control route as requests — failing loudly when no capable client is connected. - ROWBOAT_CHILD_SERVER=1: main spawns standalone.js as a child (ELECTRON_RUN_AS_NODE), waits for health, and becomes a client — the existing HTTP forwarder for calls, a WS events bridge relaying all push channels to windows, renderer delta subscriptions mirrored onto the wire, and Electron capability handlers (notifications, open-url, focus-client, browser-control). Core boot (data repairs, session index, schedulers, apps server) is skipped in main and owned by the child; the apps server moves into the shared boot module. Verified live under the flag: child spawns and runs every scheduler, main boots core-free, a real LLM turn completes end-to-end in the child over the forwarded path. Reverse-call protocol covered by an integration test (request/reply, no-capable-client rejection, handler error propagation). 19 server tests green; typecheck + lint clean. Known 7b gaps before flipping the default: streaming TTS and other sender-scoped handlers still call core in-process, packaged builds need the standalone entry shipped as a second artifact, and main-side core imports need a shadow-state audit. * fix(x): build @x/client in the forge packaging hook (main imports it now) * feat(x): Phase 7b — child-server mode becomes the default (#907) The flip completes: main spawns the standalone rowboat-server and runs as a pure client by default. ROWBOAT_CHILD_SERVER=0 is the kill switch back to in-process hosting for one release cycle. Parity work that made default-on possible: - streaming TTS moves server-side: voice:synthesizeStreamStart/Cancel are RPC channels; chunks publish on a core tts bus and fan out as the voice:tts-chunk push channel (main relays to windows in both modes, the WS hub to network clients; renderers already filter by requestId) - screen-pointer and text-insert become reverse-call capabilities: the standalone server registers broker-backed DI impls; Electron main advertises and answers them (sync isShareActive/isSupported degrade to capable-client presence — point/insert report truthfully) - packaged builds ship the server as a second esbuild artifact (.package/dist/rowboat-server.cjs, node-pty external as ever); launchChild resolves it in packaged mode, apps/server/dist in dev Known gap carried to Phase 8: token encryption at rest — the child has no safeStorage, so github/chatgpt token ciphers fall back to core's plaintext path (same file, same machine; encryption-at-rest lands with the remote-hosting hardening). Verified live with no flag: child spawns and owns core, a real LLM turn completes end-to-end, TTS stream RPC answers, kill-switch mode still boots in-process. 19 server tests green; typecheck + lint clean. * feat(x): Phase 8a — remote server mode, headless distribution, server-side workspace watcher (#909) - ROWBOAT_REMOTE_SERVER=<url> + ROWBOAT_REMOTE_TOKEN point the desktop at a rowboat-server on another machine: no child spawn, no local core; RPC, WS events, capabilities, and app://workspace file serving all target the remote - workspace watcher (debounced chokidar feed) moves from Electron main into the standalone server; workspace:didChange + knowledge:didCommit now ride the WS relay in child and remote modes (fixes knowledge:didCommit being dead since the 7b flip) - npm run build:headless (apps/server): self-contained rowboat-server.cjs + package.json for plain Node 22 on Linux; node-pty installed on the target - REMOTE_SERVER.md: EC2 + Tailscale setup, systemd unit, known 8b gaps * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay (#911) * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay for remote servers - file cipher (apps/server/file-cipher.ts): AES-256-GCM under a random key at <workdir>/cipher-key (0600); standalone wires it into the github/chatgpt token stores, replacing the plaintext fallback on headless hosts - loopback host seam in core (auth/loopback-server.ts): OAuth flows call openLoopback() instead of binding directly; a registered host can delegate the 127.0.0.1 callback listener to the machine whose browser gets the redirect. No host / no capable client → local bind, unchanged behaviour - loopback relay (apps/server/loopback-relay.ts): the standalone server asks a loopback-capable client over the WS reverse-call channel to bind the port ('loopback-bind'/'loopback-close'); the client relays each callback hit via the new oauth:deliverLoopbackCallback RPC, which runs the flow's validate/error/callback logic and answers with the page to render - desktop client hosts the relay listener (server-host.ts capabilities), reusing core's dual-family loopback binding and page rendering All OAuth flows (BYOK providers, composio, ChatGPT sign-in) now work with a remote rowboat-server: the browser opens on the client, the redirect lands on the client, the code reaches the server. * fix(x): build @x/core before test suites — apps/server tests now import its dist * fix(x): pass the relay option through createAuthServer createAuthServer rebuilds its CallbackHandlingOpts and dropped `relay`, so the client-side relay listener rendered a success page without forwarding the callback — the server's flow hung on 'waiting for sign in'. Found in the live AWS remote test; regression test added. * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle (#927) * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle The bundled child crashed at module init in packaged builds — core's skills module resolves a path from import.meta.url, which is undefined in the CJS bundle without the polyfill main.cjs already uses. Dev (unbundled) and the headless build were unaffected. Found by the packaged-app checklist. * fix(x): child server shutdown — destroy keep-alive sockets, hard-exit fallback, parent-death watch Quit left the child orphaned: httpServer.close() waited forever on the forwarder's keep-alive sockets, so the SIGTERM from before-quit never finished. close() now destroys connections; standalone adds a 5s hard-exit fallback and exits on its own when the parent pid disappears (crash / force-quit). Verified: AppleScript quit → no orphan + lock released; kill -9 of the app → child self-exits within 10s. * feat(x): Connect-to-server settings UI Remote mode no longer needs env vars: Settings → Phone gains a 'Connect to a server' section — enter the server's address and access code (~/.rowboat/server-key on that machine), the connection is validated (health + authed RPC), saved to config/client.json, and the app switches live (local child stopped, windows reloaded). Disconnect switches back. Env vars still win and lock the section when set. * feat(x): re-home Spaces into the split architecture Spaces landed on main mid-separation, built for in-process core. It is core-coupled (the topic agent runs turns through the session runtime; org tokens, mention offsets, and the blob cache live in the workdir), so it moves where core runs: - 35 spaces channels join the RPC allowlist, handlers lifted verbatim into apps/server/spaces-deps.ts; browser opens ride the url-opener seam - live space frames push over the WS hub as 'spaces:events' (desktop relays to windows); mention watcher starts with core services and notifies through the notification seam - new spaces:bounceLive channel: the desktop's powerMonitor wake signal reaches the server that owns the WebSockets - client-local pieces stay in main: save dialogs, link previews, blob thumbnails, the app://space-blob protocol - apps installer staging cleanup joins core boot Verified: full build/typecheck/lint/tests green; dev app boots and serves in child mode and against the AWS remote server. * fix(x): review findings — relay all renderer feeds over the WS bridge; base64 for pasted blob bytes 1. Ten renderer push feeds (todo, runs, code runs, code-session status, home threads, services, live-note/bg-task agents, channels status, credit activations) published to buses inside the child while the desktop relayed none of them — the renderer went quiet for anything it didn't refetch itself. They now ride the WS hub via one multiplexed EventSources feed (core-deps subscribeFeedEvents, which also starts the two trackers server-side), and the channel roster is a single shared const (@x/shared push-channels.ts) that the hub, the client type, and the desktop relay all derive from — the three lists can't drift again. Verified live: todo:addItem over RPC → todo:events observed on a WS probe. 2. spaces:uploadBlob pastes sent an ArrayBuffer, which JSON.stringify turns into '{}' on the /rpc hop — a zero-byte blob uploaded silently (drag-drop/picker take the filePath route and were unaffected). Bytes now cross as base64, decoded where core runs; the schema says so instead of z.custom<ArrayBuffer>() accepting anything. | 5 天前 | |
feat(x): sandboxed dev instances — npm run dev:sandbox (#954) Lets several dev instances run at once, alongside the production app and from inside Rowboat code mode: - node-guard.ts (main.ts's first import): fail fast with a clear error when ELECTRON_RUN_AS_NODE leaks in (code-mode agent shells inherit it from the ACP adapter spawn) instead of the cryptic TypeError mid-bundle; apps/main start script strips the var with env -u - ROWBOAT_SERVER_PORT override in apps/server config (explicit test opts.port still wins; split-brain guard untouched) - ROWBOAT_APPS_PORT override for the hardcoded 3210; appOrigin() follows - per-instance Electron userData (<workdir>/.electron-data) when ROWBOAT_WORKDIR is set in dev, so instances don't fight over profile locks - scripts/dev-sandbox.mjs: isolated workdir at ~/.rowboat-dev/<id>, auto-picked free ports, optional --seed-config; docs in DEV_SANDBOX.md Claude-Session: https://claude.ai/code/session_01AV11vrsywmfcrvCUQyxvHJ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 3 天前 | |
feat(spaces): real-time collaborative Excalidraw whiteboards (#936) * feat(spaces): real-time collaborative Excalidraw whiteboards Every space gets shared boards: a Board button (⌘4) in the space header opens a full-bleed Excalidraw canvas where all members draw together live — named cursors, selection highlights, per-element last-writer-wins sync — with the scene persisted as a normal space asset that agents can read and draw on through the existing MCP face. Harbor (contract amendment 2026-08-31): - New ephemeral `whiteboard` Client/ServerFrame pair on the live face — membership-checked, content-blind relay (payload is z.unknown()), never persisted or replayed; mirrors the presence path. Relay + forbidden tests on a real socket. apps/x plumbing (both modes, per the horizontal rule): - core: SpacesLive.whiteboard() sender; the receive path needed nothing — subscriptions already deliver every space-scoped frame, and spaces:events is in PUSH_CHANNELS so remote/child server mode relays frames untouched. Round-trip test through the real stub. - shared: spaces:whiteboard IPC channel + the app-side payload vocabulary (scene / scene_request / files / cursor / idle, keyed by per-pane clientId) and whiteboards/*.excalidraw conventions. - main + apps/server: the handler in both surfaces + RPC_CHANNELS. Renderer: - whiteboard-pane.tsx: Excalidraw's collab model on our transport — version-gated diff broadcasts, 20s full-scene self-heal, reconcile + CaptureUpdateAction.NEVER on receive, 33ms cursor frames, collaborator TTL/heartbeats, image sync via space blobs over app://space-blob. - Snapshots save throttled as single-line .excalidraw JSON TEXT assets (< ~900KB; blob fallback above) — agents read/write boards with plain read_asset/propose_change, and one-line JSON means line-merge can never mangle a scene: concurrent saves conflict cleanly and the pane reconciles + retries (excalidraw.com's merge-on-save in our contract). - Header Board button, Whiteboards rail section (boards hidden from the file tree), lazy chunk (~1MB) so the main bundle stays flat, and self-hosted fonts via a Vite plugin (EXCALIDRAW_ASSET_PATH) so the packaged app:// origin never falls back to the CDN. Design doc: apps/x/SPACES_WHITEBOARD.md (§9 = what shipped + manual QA). Verified: harbor 211, core spaces 39, renderer 365, server app 27 tests passing; typecheck + lint clean; production build splits and copies fonts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(spaces): whiteboard polish — image-as-asset uploads, no raw JSON in doc pane, own the editor menu Three fixes from first dogfood: - Images now persist as regular space assets at whiteboards/images/<fileId> (blob upload + proposeChange) instead of bare blobs: visible in the file tree, resolvable by ANYONE from the deterministic path alone (peers, cold loads, agents via read_asset), and snapshot JSON no longer embeds bytes — so boards stay under the 1MB text cap and agent-readable even with images. Upload failures now toast + console.warn instead of vanishing silently. - Board files never render as raw JSON in the document pane: the Split/Read default-document fallback excludes whiteboards/*.excalidraw, and a board reached through any file-shaped selection (artifact link, deep link, history) routes to the whiteboard surface instead. - The editor menu is ours: replaced Excalidraw's stock MainMenu (which carries their GitHub/X/Discord/help links) with just the canvas actions — export image, clear canvas, canvas background. The feature is "Whiteboard"; Excalidraw stays an implementation detail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(spaces): whiteboard doc — image-as-asset storage revision Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * debug(spaces): whiteboard image-pipeline trace logs Server+core path verified working (PNG blob upload, image asset filing, list, read-back all pass against the stub) — the failure is renderer-side. These [whiteboard] console traces mark each stage: file entering the editor store, upload start, asset filed — so one paste pinpoints the failing hop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(spaces): whiteboard v1 is shapes+text only — remove image pipeline The image upload path proved flaky in dogfood while the collab core is solid, so v1 ships without images rather than with a broken upload. UIOptions.tools.image=false disables the toolbar button, paste, and drag-drop inside Excalidraw, so no image can enter a scene; the whole sync pipeline (whiteboards/images assets, {t:'files'} frames, missing- file fetch, debug traces) goes with it. Snapshots stay small one-line agent-readable text with zero side-band bytes. The image-as-asset design stays recorded in SPACES_WHITEBOARD.md §4.3 + git history for when images return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * fix(spaces): rail '+' creates the whiteboard asset immediately Previously '+' only set the selection — the board existed as an asset only after the first stroke's save, so the rail list showed no new row, nothing highlighted, and a board you never drew on silently vanished on navigation. Now the '+' proposes an empty single-line snapshot (shared EMPTY_WHITEBOARD_CONTENT, byte-identical to what the pane saves for an empty scene) at baseVersion 0, so the board is listed and highlighted the moment it's named; a taken name just opens that board. The header Board button now shows the open board's name as confirmation. The ⌘4 default-board path still creates on first stroke — peeking at the surface shouldn't spawn assets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * feat(spaces): Rowboat skin for the whiteboard — de-Excalidraw the look Documented theming surface only, no internal-layout surgery: - whiteboard.css maps the editor's CSS custom properties to the app's design tokens (islands, popups, inputs, radius, shadows; light+dark) and swaps the signature purple for a calm blue accent - new elements default to crisp architect strokes + Nunito; the sketchy style and hand-drawn font remain in the style picker - live collaborator avatars in the top-right slot (renderTopRightUI), per-client hue shared with their cursor via Collaborator.color; the Library trigger (external excalidraw.com libraries) is hidden - help dialog keeps the shortcut sheet, loses the external-links header - loadScene/saveToActiveFile/toggleTheme canvas actions off — the space owns persistence and the app owns theme - custom WelcomeScreen empty state in the app's voice Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * feat(spaces): Split docks the whiteboard beside chat Split (⌘3) with a board open now keeps the board: chat on the left, the live board in the document slot on the right, resizable with the shared divider (min 420px — room for Excalidraw's compact UI). Talk and Read still leave the board; full-bleed stays the default and the too-narrow fallback lands on full-bleed. The board pane keeps one tree position for full ⇄ split (flex order moves it right of chat), so toggling never remounts the live collab session — no rejoin, no cursor flicker. Mode highlight: full-bleed board stays mode-less, board-split lights up Split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * fix(spaces): stable Board button; board name chip + switcher on the canvas The header button no longer morphs into the board's name after opening (a control that changes label and click-meaning has no identity) — it is always the pen + 'Board', active state via highlight, click toggles. Which-board now lives where your eyes are: a name chip in the editor's top-right slot, left of the collaborator avatars. The chip is also the quick switcher — a popover listing the space's boards (current one checked) plus a 'New board…' input, so switching or creating never needs the rail. Board-name cleaning is now one shared helper (whiteboardPathForName) used by both the rail and the chip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN * docs(spaces): whiteboard doc — name chip / switcher note Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AiySLgofbK9fXiSw3Kh1nN --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 4 天前 | |
arch: client–server separation — merge to main (Phase 9) (#929) * feat(x): rowboat-server vertical slice + iOS companion app (#758) * feat(x): rowboat-server vertical slice + iOS companion app Implements the phone-client slice of the server/client RFC (#700): - apps/server (@x/server): HTTP transport generated from ipcSchemas (POST /rpc/{channel}, allowlisted subset), server-key bearer auth, WS /events hub (per-connection seq, durable broadcast + per-turn delta subscriptions, hello handshake), authenticated workspace file route, headless standalone entrypoint with pid lockfile - main hosts the transport in-process on its single core instance and forwards migrated channels over localhost HTTP (strangler-fig, ROWBOAT_FORWARD_MIGRATED kill switch, on by default in dev) - Phone-app settings tab: QR pairing, LAN opt-in, key rotation - packages/client (@x/client): portable typed RPC + reconnecting WS client (seq-gap detection, refcounted delta subs, 401/4401 handling) - apps/mobile (@x/mobile): Expo iOS app — QR/manual/dev-link pairing, sessions list, live chat (shared turn-follower + reduceTurn, delta streaming, permission/ask-human prompts), read-only notes browser, PostHog events (platform=mobile), EAS build profiles - turn-follower moves from renderer into @x/shared (unchanged), so desktop and phone share the join/streaming protocol verbatim * fix(x): build @x/server and @x/client in CI build chains The new packages are gitignored dist consumers like shared/core, but the forge generateAssets hook and the root test/typecheck scripts didn't build them: the Electron package smoke test failed resolving @x/server from main, and the server vitest suite failed importing @x/client. * fix(x): typecheck:server needs @x/core built — add it to the typecheck chain * fix(x): review — forward migrated channels in packaged builds, refetch turns on WS reconnect - rpc-forwarder: forwarding defaults ON everywhere (packaged builds included) so production desktops exercise the HTTP path daily; ROWBOAT_FORWARD_MIGRATED=0 stays as the kill switch - turn-follower: followTurn now returns { stop, refetch }. refetch() forces a fresh snapshot — required for droppable transports where a turn that finished during an outage never emits another event, so offset-gap detection alone can leave it frozen mid-stream. The phone calls it from the events client's onResync; renderer behavior is unchanged. Covered by a new turn-follower test. * chore: retrigger CI * fix(x): rowboat-server hardening — two-sided lock, loud port collision, Host guard, symlink guard, WS header auth (#887) - the workdir lock moves into createRowboatServer itself, so the Electron-hosted transport and the standalone entrypoint contend for the same server.lock; EPERM on the pid probe counts as a live holder - EADDRINUSE on the configured port now probes the occupant's /health: another rowboat-server is a hard error, not a silent port+1 fallback - every HTTP request and WS upgrade validates the Host header against the machine's own names/addresses (DNS-rebinding defense-in-depth) - GET /workspace/{path} and the app://workspace protocol realpath the target and require containment in the workspace (symlink escape) - @x/client sends the WS token as an Authorization header on React Native; ?token= remains only for clients that cannot set headers * docs(x): client-server separation execution plan * feat(x): migrate Phase 1 read-only channels to rowboat-server (31 channels) (#888) SEPARATION_PLAN.md Phase 1: every pure-read query outside the connector, code-mode, apps, and client-local buckets now answers over the server — mcp/runs/sessions-queue/models/llm/rowboat/granola/knowledge-sources/ onboarding/agent-schedule/voice-config/live-note/todo/home/bg-task/ billing/credits/notifications/turn-limits/retention reads. Handlers are verbatim lifts from apps/main/src/ipc.ts; the desktop forwards them over localhost HTTP automatically (48 channels total on the frontier now). Verified live: per-channel smoke against the running app (200s across the board; domain errors surface as typed 500s), desktop UI driving models/ todos/home through the forwarded path. * feat(x): migrate Phase 2 write channels to rowboat-server (38 channels) (#889) SEPARATION_PLAN.md Phase 2: workspace writes (write/mkdir/rename/copy/ remove), knowledge version history + restore, knowledge-sources upsert, deck generation, the todo/home surface (add/run/chat/dismiss/archive/ planner), onboarding completion, and the notifications/turn-limits/ retention setters. workspace:pickImage and workspace:exportCopy stay client-local (native dialogs). 86 channels now on the frontier. Handlers are verbatim lifts from apps/main/src/ipc.ts. Verified live over HTTP against the running app: note write→read→rename→history→ remove round-trip, todo add→list→dismiss round-trip, settings get→set round-trip. * feat(x): migrate Phase 3a connector data channels to rowboat-server (45 channels) (#890) SEPARATION_PLAN.md Phase 3a: the connector data surface — gmail (all 23: inbox/search/drafts/labels/instructions/archive/importance), slack config/status/workspaces/channels/messages, channels (WhatsApp/Telegram) config+status+logout, chatgpt:getStatus, google-docs status/import/sync/ link, githubAuth poll/status/signOut, composio-google migration check. 131 channels now on the frontier. The Slack home parsing/enrichment helpers move from apps/main/src/ipc.ts into @x/core (slack/home-parse.ts) so main and the server share one implementation. Deliberately NOT in this PR (Phase 3b — flow relocation): oauth:* and composio:* (main-local oauth-handler/composio-handler with the browser + loopback dance), chatgpt:signIn/cancelSignIn/signOut, githubAuth:start (opens browser), google-docs:pickViaManaged. Slack desktop-import channels stay client-local permanently (they read the local Slack app — an edge capability). Verified live over the forwarded path: real Gmail inbox/search/labels, Slack workspaces/status, Google Docs status, channels status, GitHub auth status all answering through the server. * feat(x): Phase 3b — relocate OAuth/connector flows into core, migrate 19 flow channels (#898) SEPARATION_PLAN.md Phase 3b (RFC Q10): the interactive sign-in machinery leaves Electron main — - auth-server.ts → core auth/loopback-server.ts (pure node http) - oauth-handler.ts → core auth/oauth-flows.ts - chatgpt-signin.ts → core auth/chatgpt-signin.ts - composio-handler.ts → core composio/flows.ts - google-picker-managed.ts → core knowledge/google-picker-managed.ts Two new core seams replace the Electron touchpoints: - auth/url-opener.ts: registerUrlOpener / openExternalUrl / focusClient — opening a browser is a client capability (RFC Q14 in DI form; becomes a WS capability request after the flip). Main registers shell.openExternal + main-window focus. - auth/connector-events.ts: oauth/composio/chatgpt buses. Main relays them to windows (startConnectorEventsWatcher); the WS hub broadcasts them to network clients (new push channels oauth:didConnect, composio:didConnect, chatgpt:statusChanged). Migrated channels (150 total on the frontier now): oauth:connect/ disconnect/list-providers/getState, all 10 composio flow channels, chatgpt:signIn/cancelSignIn/signOut, githubAuth:start, google-docs:pickViaManaged. Verified end-to-end on a packaged build: disconnect + reconnect Google — server-side orchestration, browser via the seam, deep-link completion, tokens stored, gmail:getConnectionStatus green with full scopes. Note: the rowboat-mode deep-link return remains packaged-app-only in dev (pre-existing macOS protocol-registration behavior, unchanged here). * feat(x): migrate Phase 4 feature channels to rowboat-server (53 channels) (#899) SEPARATION_PLAN.md Phase 4: models/llm (test/generate/setProvider/ removeProvider/updateConfig), the whole apps surface (catalog/install/ update/publishUpdate/rollback/stars), legacy runs, session queue/steer (sendOrQueueMessage/editQueued/removeQueued), agent-schedule updates, search, meeting summarize, inline tasks, voice:synthesize, live-note run/set/setActive/delete/stop, bg-task run/create/patch/delete/stop, granola:setConfig, mcp:executeTool. 203 channels now on the frontier. Client-machine channels stay in main: recording/PTT/capture state, native notify, TTS streaming (event.sender-scoped), mic/camera permission prompts, save dialogs, apps:publish (window-bound flow). The two apps memo caches (install previews, catalog fingerprint) are replicated process-locally in core-deps — memoization only, no cross-process invariants. Verified live over the forwarded path: apps catalog + status, model connection test, one-shot llm:generate, search, meeting summarize, inline-task classification, live-note run, bg-task create/delete round-trip, queue/steer + runs error paths returning typed domain errors. * feat(x): Phase 5 — migrate code-mode & terminal to rowboat-server (23 channels) (#903) SEPARATION_PLAN.md Phase 5 (RFC Q13): the PTY moves with core — the terminal must show the machine code sessions execute on. - apps/main/src/terminal.ts → core terminal/terminal.ts; the window broadcast becomes subscribeTerminalEvents, relayed by main to windows (startTerminalEventsWatcher) and by the WS hub to network clients (new push channels terminal:data / terminal:exit, broadcast-to-all per Q12) - node-pty becomes a core dependency (stays external to the esbuild bundle as before) - migrated: codeMode config/status/model-options, codeProject add/remove/ list, codeSession create/list/update/delete/stop/gitStatus/fileDiff/ readdir/readFile/mergeBack/cleanupWorktree, codeRun:resolvePermission, terminal ensure/input/resize/dispose. codeMode:provisionEngine stays client-local (sender-scoped progress stream). 226 channels on the frontier. Verified live: PTY spawned via terminal:ensure over HTTP, 'echo hello-p5' typed via terminal:input, output received over the WS terminal:data feed, disposed cleanly; code-mode config/status/list channels answering. * feat(x): Phase 6 — schedulers and sync services boot in the standalone server (#905) SEPARATION_PLAN.md Phase 6: the headless-safe half of Rowboat's boot moves into core (boot/services.ts) — data repairs (runs migration, code-session backfill), retention sweep, mobile channels, live-note + bg-task schedulers, skills watcher, event processor + consumers, all knowledge sync services (gmail/outlook/calendars/fireflies/granola), graph builder, note tagging, inline tasks, agent runner, agent notes, calendar notifications, meeting prep, chrome-extension sync. Electron main and the standalone rowboat-server now call the same prepareCoreData()/initCoreServices() in the same order — main keeps only client-machine concerns (windows, tray, meeting detection, quick-ask, updater, capture) plus its window fan-out watchers. The standalone entrypoint is no longer a stub: it runs the full core, which is the child process the Phase 7 flip will spawn. Verified: standalone server on an isolated ROWBOAT_WORKDIR boots every scheduler/sync with no Electron (health + RPC answering, clean shutdown); Electron main boots identically to before on the shared module; split-brain lock correctly refused a second host during testing. * feat(x): Phase 7a — child-server mode + reverse-call protocol (flag-gated) (#906) * feat(x): Phase 7a — child-server mode behind ROWBOAT_CHILD_SERVER, reverse-call protocol The flip's machinery, default off until 7b parity: - WS reverse calls (RFC Q14): clients declare capabilities in hello; the hub routes capability-request to one capable client and awaits capability-response (30s default timeout; fire-and-forget variant for notifications). @x/client answers via a capabilities handler map. - The capability broker (apps/server/src/capabilities.ts) backs core's DI seams in the standalone server: notifications broadcast to capable clients, url-opener/focus and browser-control route as requests — failing loudly when no capable client is connected. - ROWBOAT_CHILD_SERVER=1: main spawns standalone.js as a child (ELECTRON_RUN_AS_NODE), waits for health, and becomes a client — the existing HTTP forwarder for calls, a WS events bridge relaying all push channels to windows, renderer delta subscriptions mirrored onto the wire, and Electron capability handlers (notifications, open-url, focus-client, browser-control). Core boot (data repairs, session index, schedulers, apps server) is skipped in main and owned by the child; the apps server moves into the shared boot module. Verified live under the flag: child spawns and runs every scheduler, main boots core-free, a real LLM turn completes end-to-end in the child over the forwarded path. Reverse-call protocol covered by an integration test (request/reply, no-capable-client rejection, handler error propagation). 19 server tests green; typecheck + lint clean. Known 7b gaps before flipping the default: streaming TTS and other sender-scoped handlers still call core in-process, packaged builds need the standalone entry shipped as a second artifact, and main-side core imports need a shadow-state audit. * fix(x): build @x/client in the forge packaging hook (main imports it now) * feat(x): Phase 7b — child-server mode becomes the default (#907) The flip completes: main spawns the standalone rowboat-server and runs as a pure client by default. ROWBOAT_CHILD_SERVER=0 is the kill switch back to in-process hosting for one release cycle. Parity work that made default-on possible: - streaming TTS moves server-side: voice:synthesizeStreamStart/Cancel are RPC channels; chunks publish on a core tts bus and fan out as the voice:tts-chunk push channel (main relays to windows in both modes, the WS hub to network clients; renderers already filter by requestId) - screen-pointer and text-insert become reverse-call capabilities: the standalone server registers broker-backed DI impls; Electron main advertises and answers them (sync isShareActive/isSupported degrade to capable-client presence — point/insert report truthfully) - packaged builds ship the server as a second esbuild artifact (.package/dist/rowboat-server.cjs, node-pty external as ever); launchChild resolves it in packaged mode, apps/server/dist in dev Known gap carried to Phase 8: token encryption at rest — the child has no safeStorage, so github/chatgpt token ciphers fall back to core's plaintext path (same file, same machine; encryption-at-rest lands with the remote-hosting hardening). Verified live with no flag: child spawns and owns core, a real LLM turn completes end-to-end, TTS stream RPC answers, kill-switch mode still boots in-process. 19 server tests green; typecheck + lint clean. * feat(x): Phase 8a — remote server mode, headless distribution, server-side workspace watcher (#909) - ROWBOAT_REMOTE_SERVER=<url> + ROWBOAT_REMOTE_TOKEN point the desktop at a rowboat-server on another machine: no child spawn, no local core; RPC, WS events, capabilities, and app://workspace file serving all target the remote - workspace watcher (debounced chokidar feed) moves from Electron main into the standalone server; workspace:didChange + knowledge:didCommit now ride the WS relay in child and remote modes (fixes knowledge:didCommit being dead since the 7b flip) - npm run build:headless (apps/server): self-contained rowboat-server.cjs + package.json for plain Node 22 on Linux; node-pty installed on the target - REMOTE_SERVER.md: EC2 + Tailscale setup, systemd unit, known 8b gaps * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay (#911) * feat(x): Phase 8b — token encryption at rest + OAuth loopback relay for remote servers - file cipher (apps/server/file-cipher.ts): AES-256-GCM under a random key at <workdir>/cipher-key (0600); standalone wires it into the github/chatgpt token stores, replacing the plaintext fallback on headless hosts - loopback host seam in core (auth/loopback-server.ts): OAuth flows call openLoopback() instead of binding directly; a registered host can delegate the 127.0.0.1 callback listener to the machine whose browser gets the redirect. No host / no capable client → local bind, unchanged behaviour - loopback relay (apps/server/loopback-relay.ts): the standalone server asks a loopback-capable client over the WS reverse-call channel to bind the port ('loopback-bind'/'loopback-close'); the client relays each callback hit via the new oauth:deliverLoopbackCallback RPC, which runs the flow's validate/error/callback logic and answers with the page to render - desktop client hosts the relay listener (server-host.ts capabilities), reusing core's dual-family loopback binding and page rendering All OAuth flows (BYOK providers, composio, ChatGPT sign-in) now work with a remote rowboat-server: the browser opens on the client, the redirect lands on the client, the code reaches the server. * fix(x): build @x/core before test suites — apps/server tests now import its dist * fix(x): pass the relay option through createAuthServer createAuthServer rebuilds its CallbackHandlingOpts and dropped `relay`, so the client-side relay listener rendered a success page without forwarding the callback — the server's flow hung on 'waiting for sign in'. Found in the live AWS remote test; regression test added. * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle (#927) * fix(x): polyfill import.meta.url in the packaged rowboat-server bundle The bundled child crashed at module init in packaged builds — core's skills module resolves a path from import.meta.url, which is undefined in the CJS bundle without the polyfill main.cjs already uses. Dev (unbundled) and the headless build were unaffected. Found by the packaged-app checklist. * fix(x): child server shutdown — destroy keep-alive sockets, hard-exit fallback, parent-death watch Quit left the child orphaned: httpServer.close() waited forever on the forwarder's keep-alive sockets, so the SIGTERM from before-quit never finished. close() now destroys connections; standalone adds a 5s hard-exit fallback and exits on its own when the parent pid disappears (crash / force-quit). Verified: AppleScript quit → no orphan + lock released; kill -9 of the app → child self-exits within 10s. * feat(x): Connect-to-server settings UI Remote mode no longer needs env vars: Settings → Phone gains a 'Connect to a server' section — enter the server's address and access code (~/.rowboat/server-key on that machine), the connection is validated (health + authed RPC), saved to config/client.json, and the app switches live (local child stopped, windows reloaded). Disconnect switches back. Env vars still win and lock the section when set. * feat(x): re-home Spaces into the split architecture Spaces landed on main mid-separation, built for in-process core. It is core-coupled (the topic agent runs turns through the session runtime; org tokens, mention offsets, and the blob cache live in the workdir), so it moves where core runs: - 35 spaces channels join the RPC allowlist, handlers lifted verbatim into apps/server/spaces-deps.ts; browser opens ride the url-opener seam - live space frames push over the WS hub as 'spaces:events' (desktop relays to windows); mention watcher starts with core services and notifies through the notification seam - new spaces:bounceLive channel: the desktop's powerMonitor wake signal reaches the server that owns the WebSockets - client-local pieces stay in main: save dialogs, link previews, blob thumbnails, the app://space-blob protocol - apps installer staging cleanup joins core boot Verified: full build/typecheck/lint/tests green; dev app boots and serves in child mode and against the AWS remote server. * fix(x): review findings — relay all renderer feeds over the WS bridge; base64 for pasted blob bytes 1. Ten renderer push feeds (todo, runs, code runs, code-session status, home threads, services, live-note/bg-task agents, channels status, credit activations) published to buses inside the child while the desktop relayed none of them — the renderer went quiet for anything it didn't refetch itself. They now ride the WS hub via one multiplexed EventSources feed (core-deps subscribeFeedEvents, which also starts the two trackers server-side), and the channel roster is a single shared const (@x/shared push-channels.ts) that the hub, the client type, and the desktop relay all derive from — the three lists can't drift again. Verified live: todo:addItem over RPC → todo:events observed on a WS probe. 2. spaces:uploadBlob pastes sent an ArrayBuffer, which JSON.stringify turns into '{}' on the /rpc hop — a zero-byte blob uploaded silently (drag-drop/picker take the filePath route and were unaffected). Bytes now cross as base64, decoded where core runs; the schema says so instead of z.custom<ArrayBuffer>() accepting anything. | 5 天前 | |
bootstrap new electron app | 7 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 2 个月前 | ||
| 23 小时前 | ||
| 1 个月前 | ||
| 22 小时前 | ||
| 3 天前 | ||
| 3 天前 | ||
| 4 天前 | ||
| 2 个月前 | ||
| 3 天前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 5 天前 | ||
| 5 天前 | ||
| 4 天前 | ||
| 16 天前 | ||
| 1 个月前 | ||
| 5 天前 | ||
| 3 天前 | ||
| 4 天前 | ||
| 5 天前 | ||
| 7 个月前 |