You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Issue cards now stand out, and a project can work them itself. Each project picks where its imported GitHub issue cards land, in the GitHub drawer under the sync switch: Backlog (what it did before — a cold card, yours to pick up), Needs you (the card is made there, so a fresh import is visible at a glance and still follows its issue until you run it: a new label reaches it, a closed issue archives it), or Run (the card's thread starts at once and the agent works the issue, parking in Needs you when it needs a decision and going to Review when it finishes, exactly as any other thread does). A Run card can be checked before any work happens: Checks with names the skill that runs as the card's first turn, and Works as names the agent that works it. Design the agent starts a new thread that designs that agent for you: Weft does not ask the questions, the thread does — it asks what it needs to know, one question at a time, then writes the soul and saves it as a real agent whose soul opens every system prompt its threads get. The thread opens on its own page: the brief it is running on and what happens next, written before the turn starts so the pane has something current the moment it opens. The workflow is per project and saved with your settings; changing it changes where the next cards land, and the cards already on the board stay where they are. The landing and its skill and agent live in lib/issue-landing.ts and lib/issue-run.ts, the designing thread in lib/agent-thread.ts with its brief page in lib/agent-brief-page.ts, the drawer in components/IssueWorkflowPicker.tsx, and the route in PATCH /api/github/issues and POST /api/agents/design.
The Weft Runner is designed: a headless agent runner on a host that does the compute for a card, while the board stays on your laptop or on the web at <workspace>/board. Reading the code first turned up the fact the design rests on: Weft already has a remote control plane (the web board commands a Mac across the network through cloud_commands) but no remote compute plane (the CLI always spawns on the board's own machine). A runner is therefore not a new transport but a second implementation of the executor behind that same command bus — the run kind carries a Spec (lib/runner-state.ts), and the turn comes back through the same handle(id, session, msg: SDKMessage) seam the Codex and Goose runners already use (lib/runner-turn.ts:36), so the thread rendering, the reply box, the permission prompts and the web transport are all unchanged. A cloud_hosts registry with heartbeats turns the web's "Waiting for your Mac" into a real liveness signal, and command leases replace the rows a crashed runner would leave claimed forever. The design also names the twelve places a host is not "the Mac, but headless" — the guards and cross-card claims are in-process today and become distributed, paths and repos need host-relative identities, the pending permissions have to become state rather than a promise, the worktrees need a Linux implementation, and the 🔐 vault needs a host-scoped key. The phases: the daemon alone (a Mac card running on a server) → addressing and liveness → shared claims → host-side MCP and secrets → the host's own board, where this converges with headless server mode. The design is in docs/architecture/runner-host.md; the ROADMAP's "Remote runners" moves from P3 to P2.
Changed
The Esc view hint is now a loud toast — it was a grey box in the bar's own grey, 11.5px, gone in 2.6 seconds, and it read as a tooltip rather than an announcement. It is now big type, an accent tile carrying the view's icon, a bright accent border and a wide glow, with a short pop-in so it reads as an announcement. It stays up for 4 seconds, and says what the next Esc does ("List view · hit Esc again to switch to board view") instead of a vague "Esc goes back". It is one row and never wraps. An absolutely-positioned box with no width shrank to its content and then wrapped its text into a narrow column, which left the icon adrift from the words and stranded the ✕ between them, so it now carries w-max and whitespace-nowrap. The next Esc puts the toast away instead of flipping the view again — the toast advertises that Esc does something, so Esc has to do it. The flip itself still works the same: the toast is driven by the board's own Escape layer, so it appears on exactly the Escape that moves the view. Every flip pops the toast afresh. The flip's id counted the flips but nothing read it, so a second Esc left the toast sitting there already grown, with its entrance long over. It is keyed on the id now, so each flip remounts it and the pop-in plays again. And the toast's ✕ was wired through an optional prop that defaulted to a no-op swallowing the click, though the board has always passed it; it is required now. The toast is components/ViewHint.tsx with its state and timer in lib/use-view-hint.ts, the flip's hook lib/use-escape-view.ts (whose extra inputs are one options object, not a list of six), the pop-in and glow in app/globals.css, and the bar that hangs it under the toggle in components/ProjectBar.tsx.
Fixed
The dictation key answers to Command+Option+' and stops the mic. The rebindable binding is a set of modifiers rather than an exact combination, so the default ⌥' now fires on ⌘⌥' too — the combination people reach for when the bare one is taken. Pressing the key again while the microphone is still opening cancels the recording that is starting, instead of leaving it running with nobody listening or opening a second microphone on top of it (lib/shortcuts.ts, lib/use-dictation.ts, lib/use-composer-keys.ts).
The packaged app starts again — it had been dying on launch with "Cannot find module '@supabase/supabase-js'". The shared vault added a Supabase client to Electron main, but electron-builder's files list drops the whole top-level node_modules (!node_modules${/*}), and the package was never named back on the list. It was in package.json's dependencies and in the Next server's standalone bundle, so the dev build and every dependency check were happy — but the shipped app's node_modules is only the allow-list, and the client wasn't on it. Every packaged build since died in the main process before a window opened, and no test could see it, because the tests run against the repo's node_modules, where the package is present. The list is now derived rather than hand-kept.scripts/electron-runtime-deps.mjs computes the closure of packages Electron main needs at runtime — the bare require()s out of electron/*.cjs, then the runtime requires out of each package it finds, transitively — and pnpm dist writes it into build.files before packaging. A new require() ships with its package instead of waiting for someone to remember. Both the CommonJS and the ESM entry are read, because a package's main can vendor a dependency its module build imports (realtime-js's dist/main vendors its own phoenix, its dist/module imports @supabase/phoenix). The built app is checked before it ships.scripts/verify-packaged-app.cjs reads the archive itself — a package counts as shipped only if app.asar holds it, never by resolving from the repo's node_modules, which has everything and would hide a broken build — and then loads the packaged account-client.cjs out of the archive and builds a Supabase client from it. electron/packaged-deps.test.ts fails if build.files drifts from the closure. The packaged app's bundled pnpm (which already ships in Contents/Resources/toolchain) is checked too: scripts/verify-packaged-toolchain.cjs confirms it runs, matches packageManager, and can install a project's dependencies. The packaging section of docs/architecture/desktop-shell.md says what the allow-list is and how it is derived. The app now checks its own bundle before anything needs it. The derivation and the build check catch a missing package before it ships, but a bundle can still arrive broken another way — an interrupted install, a half-copied update — so at boot the app reads its own app.asar and says what is missing: "Weft couldn't find some of its own files (missing from the app bundle: @supabase/supabase-js). Reinstall Weft to repair it." instead of dying on a require with a native "A JavaScript error occurred in the main process" dialog nobody could act on. The check reads the bundle, not the machine: inside app.asar Node's resolution walks out of the archive and finds the real node_modules next to the app, so a plain require.resolve() would report "found" for a package the bundle doesn't have — the same trap verify-packaged-app.cjs documents, and the first version of the check fell into it and called a broken bundle good. The Supabase client is now required lazily, so a bundle missing it still opens a window and can say so. An auto-update runs pnpm install — for the one tree that needs it. Squirrel.Mac replaces the whole .app bundle, so an update brings the app's own dependencies with it: app.asar's packages and the Next standalone bundle are already installed when the app restarts, and re-installing them would write a node_modules folder nothing reads. The one tree installed separately from the bundle is the toolchain's puku tree, so that is what pnpm install runs against — after an update, and only then, in the background, because the board never waits for it. The app knows an update landed by the version changing since the last launch (~/.weft/updated-version), so an ordinary relaunch installs nothing and only checks the tree. The check and the install live in electron/runtime-deps.cjs, electron/toolchain-install.cjs, electron/updated-since.cjs and electron/boot-deps.cjs, with tests beside them; docs/architecture/updates.md says what an update brings and what it installs.
Two bugs from GitHub are fixed: the achievements screen can always be dismissed, and logging in with Claude no longer leaves the app stuck.The achievements screen keeps its buttons on screen. The pop-up had no height cap and nothing to scroll: the hero picture, the medal and the badge list pushed the buttons below the fold, and on a short window they fell off the edge entirely, so the screen could not be dismissed at all. The panel is capped now, the badges scroll above, and the buttons sit in a pinned footer that stays put at any window height (components/AchievementPopup.tsx). Logging in with Claude no longer leaves the app stuck. Three things went wrong at once. The panel read the logins once and then went on saying "Not logged in" after a successful login, so pressing Log in again felt necessary. Pressing it again was the freeze: the second login replaced the first, killing its auth login process mid-flow, so the credential never landed and the two logins fought each other — the copilot's "could not be refreshed" was the CLI trying to reuse the stale token. The server now joins a login that is already under way instead of replacing it, so a login is never killed (lib/login.ts). The panel re-reads the logins every two seconds, so a login finished in the browser shows up on its own, and the Log in button is replaced by a "Logging in" line while one is under way, so there is nothing to press twice (components/CliLogins.tsx). The welcome screen's own Log in button is safe for the same reason: the server joins the running login instead of starting a second one. components/CliLogins.test.tsx is new — the panel had no test at all — and lib/login.test.ts and components/AchievementPopup.test.tsx cover the rest. Each new test was checked against the unfixed code: it fails there and passes here.