Repository navigation
Weft 0.6.0
Everything since 0.4.0 (0.5.0 was never published on its own). Signed and notarized: it opens without a Gatekeeper warning.
Added
- Shared mini apps unfurl as themselves. A public app link now shows its own title, a one-line description and a branded card in Slack, X or iMessage, instead of "Shared app · Weft". The description comes from the app's
weft-app.json, else the page's own meta tags, else a sentence Haiku writes from the page. The card has the app's glyph, name, description and owner, with a screenshot of the app taken in a hidden window when you share it. An app or page that names its own picture uses that instead. The Share dialog shows the card, lets you rewrite the description and has Refresh preview. Private, email and password links keep Weft's generic card, so nothing about the app leaks (lib/app-preview.ts,lib/app-preview-capture.ts,lib/app-share-meta.ts,lib/app-card-image.tsx,electron/page-snapshot.cjs,components/ShareAppPreview.tsx,cloud_app_shares.preview). - Every tool drawer explains itself, once. The first time you open a drawer on the left rail — 🔐 secrets, 🪝 webhooks, ▲ Vercel, Supabase, Cloudflare, 🚀 BlastCP, GitHub, ✦ skills, 🖥 services — or the code quality dialog or the leagues, a short narrated video plays at the top of it: what it's for and what it can do. Eleven intros, one page each in
public/intros/, voiced bypnpm intros:narrate(Kokoro, the same local voice as the welcome tour, with its pad). ▶ beside the title plays it again, ↻ Replay appears over the frame once it ends, 🔊 shares the welcome tour's mute, and ✕ closes it. Each plays once and remembers that inlocalStorage; Settings → General's Reset tips plays them all again. It stands down for reduced motion, and a browser that refuses storage just plays them again (lib/feature-intros.ts,lib/use-feature-intro.ts,components/FeatureIntro.tsx, docs/architecture/feature-intros.md). - Weft's plans include BlastCP usage. weft.zip's pricing lists the BlastCP tier each plan comes with: Free has BlastCP Free (1 brand, 3 channels, 30 posts a month, $0.50 of AI generation once), and Pro and Team have BlastCP Creator (every channel, unlimited posts, $3 a month of AI generation). The tiers come from the catalog (
plan.blastcpinlib/billing/plans.ts). BlastCP doesn't apply them yet. - The limits chart looks back in a new week. In the first 3 days after a weekly reset, the status strip's 5hr / 7d hover also shows the last 3 days of the week before (shaded, labeled "last week", their weekly bars dimmed), so a fresh reset no longer leaves the chart empty (
lib/limit-chart.ts,components/LimitBurndown.tsx). - weft.zip plays the narrated tour. Under the board screenshot, "Take the tour, sound on." plays
public/brand/weft-tour.mp4(the welcome tourpnpm welcome:rendermakes), with its Welcome to Weft title card as the poster (public/brand/weft-tour-poster.webp) and nothing loaded until play (components/home/Tour.tsx). - BlastCP connects with your Weft account: no browser, an account made for you the first time, and a sign-in that renews itself (an RFC 8693 token exchange; BlastCP's side is its own change).
- Anonymous setup stats from the welcome. Nine onboarding events (started, each tour beat, mute, sign-in, CLI log in, each step, the projects answer, the first thread, skip) go to PostHog through weft.zip, so we can see where people drop off. Only allow-listed events and props, never prompts, project names, paths or code; the ID is a hash of the device ID. On by default with a switch in Settings → Security (
lib/telemetry*.ts,app/api/telemetry,components/StatsSwitch.tsx, docs/architecture/analytics.md). - Step through the welcome tour a beat at a time. A bar under Get ready's tour names the beat on screen ("3 of 9 · Connect your tools"), with a segment per beat to jump to, ‹ Back and Next › one beat at a time, and the mute; the voiceover picks up where a jump lands (
components/WelcomeTourBar.tsx,public/welcome/live.js). The beats now run build → ship → show: tools, panes, code quality, BlastCP, the Wefter Profile, open source. The anonymous-stats switch left Get ready; it stays in Settings → Security. - The welcome tour shows BlastCP and says Weft is open source. Two new beats before "Now let's get started": BlastCP built in, your social accounts connecting and a launch post getting its image, video and voice before it's scheduled ("so you can build in public"); then the
aimhuge/weftrepo and a pull request going Open → Merged, voiced "If something is broken, or you want to add a feature, please feel free to make a pull request" (public/welcome/beats.html, now 54.5 s; the tour is 96 s). - Plans on the home page, and a Team tier. weft.zip now prices itself between the pillars and the FAQ: Free, Pro and Team read straight from the billing catalog, so a price change moves the page (
components/home/Plans.tsx). Team is $9 per seat a month with Pro's allowance, and teams are now a paid capability — a Free workspace is a team of one and can't invite members (lib/billing/plan-gate.ts,lib/team-ops.tsrunInvite). The gate fails closed: an unreadable or unpaid subscription reads as Free. - The welcome's Get ready screen plays how Weft works. It opens on a "Welcome to Weft" title naming the agents it runs on (Claude Code, Codex, Puku AI;
public/welcome/intro.html) and ends on "Now let's get started", after which the next steps say "Allow Weft to see your projects" and "Now start a new thread". A 30-second loop with your own project names on the cards: a prompt starts a card, it asks you a question in Needs you, gets a key from the 🔐 vault after Touch ID without ever seeing it, shows its work in the browser pane, and lands in Done, which commits, refactors and deploys. It's a HyperFrames composition that plays live in the welcome and renders to an MP4 for weft.zip (public/welcome/loop.html,components/WelcomeLoop.tsx,pnpm welcome:render). Reduced motion keeps the still screenshot. After it the welcome plays a second composition, connect your tools, the Wefter Profile and code quality with the refactor scout (public/welcome/beats.html, handed off bypublic/welcome/live.js), with a beat on the pane: a thread's page as a live dashboard on vault keys and your data, shared as a weft.zip link; during the profile beat a real Sign in with GitHub button sits over it, and once you are signed in the beat shows your name and avatar. It plays over Weft's own scene pictures with a soft backing pad under the voice, and it is narrated, on by default with a mute button:pnpm welcome:narratevoices each caption with Kokoro, a local text-to-speech model, into one track per page (scripts/narrate-welcome.mjs), andpnpm welcome:renderbakes the voice into a 78-secondpublic/brand/weft-tour.mp4for weft.zip. - Settings has a Security page. The default permission mode (from Threads), trusted folders (from General) and sign-in expiry (from Accounts) now sit together (
components/SettingsSecurityPage.tsx). - The project bar's More menu has a filter. It has focus when the menu opens, matches the folder name and its
~path, and Enter picks the top match. It's the same field and rule as the 🔐 and skills filters, now shared (components/SearchField.tsx,lib/text-filter.ts). - Replay the welcome from your avatar's menu. It moved out of Settings → General (which keeps Test it and Reset tips) into the avatar menu and the command palette (
components/AccountButton.tsx). - The 🔐 drawer shows each vault's Vercel project. A vault whose folder is linked in ▲ gets a line under its name: the project, how many values are still to push, and Push to Vercel (one Touch ID) (
components/VaultVercel.tsx). - The ▲ Vercel drawer's 🔐 list has a filter. Past five variables, a name search and a menu (To push, On Vercel, Vercel only, No value, with counts) narrow the list (
components/VercelEnvRows.tsx,lib/vercel-env-filter.ts). - weft.zip measures the whole site and the download. GA4 now covers every weft.zip page (the FAQ, profiles, share links and the web board, not just the home), and PostHog runs alongside it. The download button sends
download_clickedwith its version to both, and New thread, replies, new projects and invites send their events through onetrack()(lib/analytics.ts, docs/architecture/analytics.md). - The ▲ Vercel drawer's 🔐 list has a filter once it runs past five variables: a name search and a menu for To push, On Vercel, Vercel only and No value.
- Threads ask the CLI for a 1-hour prompt cache (CLAUDE_CODE_PROMPT_CACHE_TTL), so a short break no longer re-sends the whole thread.
- Send fresh replaces "Continue in new card": a cold thread over 30k tokens sends your message to a new session in the same card, started from a brief (a Haiku summary written ahead of time, what's still open, and the diff of this thread's own edits) instead of re-sending the history; "Send with full history" is in the ▾ menu, and a divider in the thread shows the brief.
- An idle reply box has a Done button that sends /weft:done (fresh when the cache is cold).
- Stop is faster: it SIGTERMs the commands a thread is running at once, and ends the CLI after 1.5s (was 5s) if the interrupt is ignored.
- Errors and Restart errors filters on the Review column. Two chips in its header, each on its own: Errors keeps cards whose last line is a thread or build error, Restart errors keeps cards that hit the 5-hour session limit. An active chip shows its count; both off after a reload (
components/ReviewFilterChips.tsx,lib/board-view.ts). - The reply box's button says what Enter does. One mode at a time, named on a chip in the box and on the primary: Send, Queue (while a turn runs), Answer, Run (a
!command, in green with the folder), Replace (after ↑, in amber with the file edits that stay) or Send copy. Send now, Send later and Continue here moved into a ▾ beside it; ■ Stop keeps its own slot. The default follows the thread: New card when the prompt cache is cold (the stale-cache row above the box is gone; Send here is in the ▾), Send at when the 5-hour window is used up, and Continue only after a turn that stopped short (lib/reply-mode.ts,components/ReplyButtons.tsx,components/ReplyModeChip.tsx). - 🏆 Leagues in the header. A chip beside the streak shows your league and rank ("🏆 Linen #1") and opens every league: click any tier, Cotton to Gold Thread, to see its leagues this week and who's in them (
components/LeaguesDialog.tsx, theleague_browseRPC). The profile's League tab keeps just your own league. - Continue in new card. A thread whose prompt cache has gone cold shows a "stale cache" tag above its reply box with a Continue in new card button: Haiku summarizes the thread and a new card starts in the same folder and CLI with the summary, the files it touched and the message typed in the box (
lib/handoff.ts). The button is a solid accent button, and the panel switches to the new card as soon as it exists (lib/open-card.ts). - The project bar's More menu has Clone from GitHub…: type
owner/nameor a github.com URL and it clones into~/devand selects the project. - Confetti for achievements. A new badge or level opens a popup with a burst of confetti instead of a toast: one badge gets the stage, several share a list, and See your profile opens the Badges tab (
components/AchievementPopup.tsx,components/Confetti.tsx,lib/confetti.ts). - League emails. A welcome on the day you join a league, a Thursday and a Sunday nudge when a move is in play, and Monday's results, from a daily Vercel cron at 13:00 UTC; at most one a day, each sent once (
league_emails), with one-click unsubscribe (profiles.league_emails) (lib/league-mail*.ts,app/api/league/{emails,open,unsubscribe},supabase/migrations/20261017171804_league_emails.sql). - Tokens and plan-limit hits on your Wefter Profile. The You tab charts your tokens for the last 30 days, from every Claude Code and puku-cli transcript, with a dot over each day you hit your 5-hour or weekly limit; two new badges, ⏳ Hit the ceiling and 🗓️ Ran the week dry, unlock on your first hit of each (
lib/usage.ts,lib/usage-scan.ts,components/LoomUsage.tsx). - "Restart requested", in red. While a restart waits for the running threads, the title bar's chip reads "⟳ Restart requested · N" (or "Update requested") in red, and the New thread dialog's header shows a red Restart requested pill, since a thread started then begins only after the restart (
components/RestartChip.tsx). - Your weft.zip profile shows your whole Wefter Profile.
weft.zip/@usernamenow has your picture in a ring that fills toward the next level, your class and level title, your GitHub bio, location, company, site, followers and repos, this week's league and rank, big numbers (XP, cards shipped, streak, badges), the builder card's radar, attributes and skills, the activity cloth, a wall of the badges you've earned, tokens for the last 30 days with plan-limit hits, your records, and your public mini apps beside your public threads. While you're signed in, your Mac posts the card every 10 minutes on the league sync's timer (never prompts, code, project names or folders), and the page parses it again before drawing it (lib/profile-card.ts,lib/profile-card-sync.ts,components/profile/*,supabase/migrations/20261018135430_profile_cards.sql). - A welcome on first launch. Before any Claude Code session becomes a card, a two-screen overlay shows the Wefter Profile your sessions make (class, level, sessions, shipped, best streak, top attributes) and where each part came from, with Opt out / OK, then your projects to bring in, worktree, scratch and temp folders unticked and folded, with Start fresh / OK. OK is the default on ↵. A board that already has cards keeps working as before. Settings → General gets Count my Claude Code history, Bring them in for sessions left out, and Replay for the welcome (
lib/welcome.ts,lib/welcome-preview.ts,app/api/welcome,components/WelcomeDialog.tsx). - "Your loom" is now your Wefter Profile. The 🔥 chip's dialog has the new name; Loom stays a level title.
- Generate a random value in the 🔐 drawer. A 🎲 beside the value field, next to the eye, fills in a random 40-character string of letters and digits (about 238 bits, no quoting needed in a .env or shell) for the
CRON_SECRETs and signing secrets you make up yourself (lib/random-secret.ts,components/SecretValueField.tsx). - Filter secrets in the 🔐 drawer. A search box under the project filter narrows the shown vaults to the secrets whose name or blank hint holds every word typed (
stripe hookfindsSTRIPE_WEBHOOK_SECRET); projects with no match drop out, and Escape clears it (lib/vault-search.ts,components/VaultSearch.tsx). - Data mini apps, and 📈 Growth. A mini app can hold a folder (
~/.weft/apps/<slug>/:index.htmland aweft-app.jsonnaming the project folder it reads, the vault env it asks for, and its reads by name). The page asks for that env withwindow.weft.requestAccess()(a Touch ID naming the app, project, env and keys); Electron main then runs its named reads with the env's vault values, so no value, SQL or URL comes from or goes to the page: a Postgres function through Supabase's API as a read-only GET, or a GA4 report signed with a service-account key. A changed manifest asks again. Every mini app is also stored on prod (cloud_mini_apps, owner only, pages by content hash) and restored on a Mac that lacks it. 📈 Growth reads Weft's own accounts, signups, active accounts, weekly retention, activation and GA4 traffic (electron/app-data.cjs,electron/app-data-reads.cjs,electron/pane-preload.cjs,lib/app-cloud.ts,supabase/migrations/20261018054110_growth_reads.sql,supabase/migrations/20261018061014_cloud_mini_apps.sql). - Dependabot.
.github/dependabot.ymlchecks npm (pnpm) packages and GitHub Actions weekly, with minor and patch updates grouped into one PR per ecosystem. - Mini apps on your profile, and Share… for them. The 🔥 chip's You tab lists My mini apps under your wefter card: click one to open it in the pane, or its share button for a link. Share… (also on the tile's right-click menu) gives an app a
weft.zip/a/<id>link behind the same modes as a card's: your workspace, anyone, anyone who gives an email, or a password. A local page goes up as a sandboxed copy with key-shaped text masked, refreshed each time you open Share…; a public https site is framed as is; a localhost app says it can't be reached from the link. Removing the app takes its link down (lib/app-share.ts,components/ShareAppDialog.tsx,components/LoomApps.tsx,app/a/[id],supabase/migrations/20261018150000_app_shares.sql). - The show rail shows pictures, and the pane follows the chat. Each page in the rail beside the browser pane is now a small picture of what the pane showed (captured when the page or the thread settles, shrunk to a 144×180 WebP of a few KB,
~/.weft/shots/), with a short name under it; turns are lighter text rows between them. Scroll the chat and, once it settles, the pane shows the page that was on screen for the turn at its top; scroll back to the end and it returns to the newest page (lib/page-shots.ts,app/api/cards/[id]/shots,lib/use-page-shots.ts,lib/use-pane-follow.ts,components/ShowRailItems.tsx). - Done without a review tour. A Review card whose thread wrote no tour has a Done button beside Build review tour in its review bar; it runs
/weft:done, as a tour’s last step does (components/ReviewPanelNoTour.tsx). - Question cards: hold, retry, and a way out. A send, number key or click in the first 0.8 s after a question appears is held (with a note saying so), so Enter on a message you were finishing no longer answers the question with it and hides the card. An answer that doesn't reach the agent brings the card back with your text and the error, instead of leaving it greyed out. Chat about this and Skip are back on the card's hint row. Notes and previews now reach the agent from the desktop board and the copilot too; both had been dropping them (
components/QuestionForm.tsx). - Continue in the reply box (⌘. on the desktop, button on weft.zip). A ▶ Continue button next to Send sends the word
continuethrough the sameonSendpath a typed message uses — the same call the card-level ▶ makes (lib/resume-actions.ts). On the desktop, the button is hidden while a turn runs, a question waits, the box is a!shell command, or you're editing the last message; ⌘. does the same from the keyboard and the shortcut greys out under the same conditions; both show in the?sheet. The weft.zip web board's reply box gets the same ▶ button (no ⌘. — the web has no global keymap); it's hidden on a card a teammate's Mac runs (components/ReplyBox.tsx,components/ReplyBoxActions.tsx,components/ReplyButtons.tsx,components/web-board/WebReplyBox.tsx,lib/hotkeys.ts; new tests incomponents/ReplyBox-continue.test.tsxandcomponents/web-board/WebReplyBox.test.tsx). - Reorder cards in the Backlog. Drag a Backlog card onto a sibling to put it before or after — a 1px accent line on the top or bottom edge of the target shows where it'll land (top half = before, bottom = after). The order is saved as a half-step number on each card and wins over the column's chosen sort; ties fall back to it. Old cards backfill from
createdAton first load; other columns ignoreorderand keep their sort. If the half-step gap ever closes the column renumbers to evenly spaced values (lib/store.ts,lib/board-view.ts,lib/use-board-card-actions.ts,components/BoardColumn.tsx,app/api/cards/[id]/route.ts). - Backlog: archive, sprints and ⌘B. Backlog cards get the archive button on hover (Done's had stopped showing on the board; it's back too). The Backlog header gains Save as sprint (the flag) and Archive all (the box), which asks first and can save the lane as a sprint before archiving it. Click Backlog for the list of sprints, each with what became of its cards and Restore N for its archived ones; rename or delete them there. In the New thread dialog, ⌘B adds the thread to the Backlog instead of bolding (
lib/sprints.ts,lib/sprint-store.ts,app/api/sprints,components/BacklogActions.tsx,components/SprintsMenu.tsx). - FAQ, in the repo and on weft.zip.
docs/FAQ.mdanswers the questions people ask about Weft in plain words, from installing and the columns to CLIs, guardrails, the vault, connectors, the copilot, weft.zip, pricing, privacy and troubleshooting, with a visual tour of the real app. The home page now ends with six of them as open/close rows, each linking to its full answer onweft.zip/faq, which renders the same file with the home page's nav and footer (app/faq/page.tsx,components/home/FaqDoc.tsx,lib/faq.ts); the nav and footer link it too. - Dev servers in Services. The 🖥 Services drawer has a Dev servers section: each open card's folder with the command that starts its dev server (found in
package.jsonscripts, a Procfile, Rails, Django, deno, a Makefile or wrangler), a Start button, Stop and Restart once it runs, and whether it started or restarted, when, by which card (click its name to open that card), and if it answers yet. It sits first in the drawer, above the CPU and memory; Weft's own folder is listed last as the board itself, with no Start or Stop and a link to its address (:1024 ↗) that opens in your browser. Thread names in the services list open their card too (lib/dev-commands.ts,lib/dev-launchers.ts,components/DevLaunchers.tsx). - Mini apps. Any page in the browser pane becomes an app: the grid button in the pane's toolbar saves it (named after its title) to the left rail, one tile per app below 🖥 Services; click a tile to open it in the pane, right-click to rename, move, copy its address or remove it. With no apps the rail shows nothing new. Kept in
~/.weft/apps.json(lib/mini-apps.ts,lib/mini-app-store.ts,app/api/apps,components/AppsRail.tsx,components/SaveAppButton.tsx). - Limits chart on hover. Hover over
5hr/7din the status strip for the week from one weekly reset to the next: the 5-hour limit left as 168 green hour bars (left axis), the weekly limit left as 7 see-through red day bars in front whose tops burn down hour by hour (right axis), red lines where a limit was hit, and green lines at each 5-hour reset, stronger at the next one and strongest at the weekly reset. Threads ask the CLI for their limits after each turn (at most every 5 minutes) and log each change to~/.weft/limits.jsonl, resets rounded to the minute (lib/limit-history.ts,lib/limit-chart.ts,components/LimitBurndown.tsx). - Services summary that says something. The 🖥 drawer's "276% CPU · 7.9 GB" now reads "2.8 of 10 cores · 7.9 GB of 16 GB", with CPU and memory bars split by kind (threads, dev servers, Weft, terminals), and the busiest service and the biggest by memory named.
- Editor column. An open card is four columns: thread, editor, pane and glance, each closable (a thin strip reopens it, the ✏︎ in the thread's header for the editor, ⌥⌘1–4) and resizable by dragging the line between any two, remembered. Click a file path in the thread to open it: CodeMirror for code, TipTap for markdown (saves keep untouched lines byte for byte), a Changes view against git HEAD with per-chunk revert, ⌘S, ⌘P, live reload while the thread edits, and a conflict banner instead of a silent overwrite. Edit and Write rows in the thread show their diff (docs/architecture/editor.md).
- Leagues. The Loom's League tab is live: weekly leagues of up to 30 by XP, top 7 up and bottom 5 down, each builder's week (XP, shipped, streak, level, badges earned that week) and a link to their
weft.zip/@username; a signed-in Mac posts its week every 10 minutes (lib/league.ts,lib/league-sync.ts,app/api/league,components/LeagueTab.tsx,supabase/migrations/20261017162000_leagues.sql). - Public profiles. Every account has a page at
weft.zip/@username(your GitHub login, lowercased): picture, name, a GitHub link, when you joined and the threads you've shared publicly; never your email or workspace (app/u/[username],lib/public-profile.ts, docs/architecture/profiles.md). - Share the board. Share the board (the share icon beside your avatar, its menu, ⌘K, or Share in the weft.zip board's header) gives your workspace's web board link,
weft.zip/<workspace>/board, with Copy and Open, who can open it, and an email box: someone with a Weft account joins the workspace at once, someone without one gets an invite email to sign up and join (components/ShareBoardDialog.tsxandcomponents/web-board/WebShareBoard.tsxover onecomponents/BoardSharePanel.tsx, through weft.zip's team invite). - Workspace invites look like Weft. The invite email now has an HTML version in the Salt Flats scene: the hovering car on top, then "Join on Weft", who invited you and an Accept invite button in the scene palette; the plain text still goes with it (
lib/invite-email.ts, images inpublic/brand/email/). - Reply to a thread from weft.zip, and see it answer within a second. The open card on the web board has a reply box: what you send becomes a
replycommand for the Mac that runs the card, which sends it into the thread as if you'd typed it there (or after the running turn, shown under the box as "After this turn"). Each reply shows under the box until it lands ("Waiting for your Mac", "Sending on your Mac…", or why it couldn't, with Dismiss). A teammate's card stays read-only. The other way, a turn's items now go up within a second instead of every 10 s, and a message you send goes at once (lib/cloud-inbox.ts,supabase/migrations/20261017140000_reply_commands.sql). - An open card on weft.zip is the desktop's own thread panel. Same header, the same thread view with "Working…", paging as you scroll up and the pending request, and the same reply box (Markdown composer, a message held while the turn runs, Send now, ■ Stop). Answer a thread's permission request or question from the web: it becomes an
answercommand the Mac runs on that exact request, and Stop astop. Underneath, the panel, thread view and reply box read a transport (lib/thread-transport.ts), the desktop's local server by default and Supabase rows, Realtime and commands on the web (components/web-board/useCloudTransport.ts), instead of a second, web-only copy of each (supabase/migrations/20261017150000_answer_stop_commands.sql). - The weft.zip board looks like the desktop window. On
/<workspace>/boardthe workspace sidebar steps aside for the desktop's own chrome: its header (the Weft logo, "N running · M need you", the workspace, Live, New thread with N, and your avatar, whose menu switches workspaces and holds Team, Billing and Sign out), the 38px rail (★ starred filter, then Board, Team and Billing) and the desktop'sBoardColumn(with Add a thread in Backlog). A card opens framed over the board like on the Mac, the thread on the left with the desktop's header (session, live, stage, what's left; no more Mac id hash) and its browser pane on the right: a synced~/.weft/showpage runs sandboxed through a member-only route (app/api/web-board/[orgId]/page/[sha]), a site is framed with Open ↗, and a localhost page shows its last capture (components/web-board/WebBoardHeader.tsx,WebRail.tsx,WebAccountMenu.tsx,WebPane.tsx). - Services: everything Weft started, with CPU and memory. The rail's 🖥 drawer lists every process Weft spawned, one row per thread CLI (named by its card), dev server, terminal shell or other command, each with its CPU and memory and a total at the top; expand a row for its processes and end the whole service or one process (Weft itself is listed but can't be ended there). Pick how often it refreshes (every 1 to 30s, or paused); the icon counts what's running. Dev servers keep their url, Restart and Start: threads start them through Weft with
mcp__devservers__start(and/weft:previewdoes) instead of Bash in the background, so a server outlives its thread's CLI and no longer keeps it open; one a thread still starts the old way shows there too and leaves the thread's task strip (lib/services.ts,lib/services-snapshot.ts,lib/services-kill.ts,app/api/services,components/ServicesDrawer.tsx,lib/devserver.ts,lib/devserver-tools.ts).
Changed
- The code quality badge opens a dialog. Its suggestions (fix the checklist, the scout's findings, Re-scout) moved from a 320px dropdown to the shared
Modal, with each finding's fix shown in full (components/ScoutDialog.tsx). - A scoped coverage run no longer wipes the repo's coverage report. Vitest empties its reports folder before writing, so
pnpm test lib/x.test.ts --coveragereplacedcoverage/coverage-summary.jsonwith one file's numbers (or nothing, if cut off) and the header's Tests score fell to 0%. A run of named files now writes its report to a temp folder (scripts/run-tests.mjsscopedCoverageArgs); onlypnpm coveragewritescoverage/.--no-memory-gateand--no-watchdogare no longer passed on to Vitest, which refused them as unknown options (so the gate's own "run anyway" hint never worked), and the watchdog test fills its buffers so macOS counts them as resident. The open thread's handlers moved out ofcomponents/Board.tsxintolib/thread-handlers.ts(useThreadHandlers), tested beside it, andlib/codex-runner.tswent from 6% to 100% of lines covered. - The docs name only files that exist. Twelve migration paths in SOUL.md and ten architecture docs still carried the versions they were written with, not the ones Supabase stamped on apply, and SOUL.md named a
components/DevServersDrawer.tsxthat becamecomponents/ServicesDrawer.tsx; all now point at the real files. The rail's filter chip moved out ofcomponents/Board.tsxintocomponents/RailFilterChip.tsxwith its own tests, andlib/codex-input.ts,lib/server-status.ts,lib/onboarding-events.tsandlib/client-files.tsgot test files of their own. - One Keychain-token implementation for the four provider integrations. Vercel, Supabase and Cloudflare each had a byte-identical
token()reader and a persist step, and BlastCP had the reader plus anexpiresAtbeside it — four copies of the code that guards a secret file's mode. All four now read and write throughelectron/provider-token.cjs(readToken,readTokenRecord,requireKeychain,writeToken); each keeps its own error wording and itscreate*/register*/setup*shape (electron/provider-token.test.ts). - A token file's mode is now re-asserted on every write.
fs.writeFileSync'smodeonly applies when it creates a file, so a record that was ever loosened — a straychmod, a restore from a backup, a copy in from another machine — kept its loose mode through every later rewrite.writeTokenchmods after writing, so a token file is0600whatever happened to it before (found while extracting the shared module). - One read implementation for local and hosted mini apps. A data app's Supabase and GA4 reads ran in two copies, one in Electron main and one in the weft.zip route, and a security fix landed on only one of them (dde1126, closed in 25c1c99). Both now run
electron/app-read-core.cjs;electron/app-data-reads.cjsis main's re-export andlib/app-read.tsmaps its refusal kinds to the route's statuses.lib/app-read-token.tsis gone (electron/app-read-core.test.ts,lib/app-read.test.ts). - The reply box's queue is its own hook. Replies queued behind a running turn (the batch sent once as the turn ends, a failed send back on hold, a row's Send now) moved out of
components/ReplyBox.tsx(404 → 357 lines) intolib/use-reply-queue.ts, with its own tests at 100% and new ReplyBox tests for the queue's edges (components/ReplyBox-queue.test.tsx); no behaviour change. - A hosted mini app's read and fetch run one pipeline.
weft.readandweft.fetchon weft.zip (app/api/a/[id]/read,app/api/a/[id]/fetch) had each written their own body cap, gate, rate limiter, vault lookup and error mapper; both now runlib/app-route-pipeline.ts(parseBoundedBody→gateShare→requireService→rateLimitGuard→resolveValues→runLogged) and supply only their parser and upstream runner. Limits, statuses, bodies andno-storeheaders are unchanged. - One
clip, onefolderName, one settings shape. Three small duplications found by the refactor scout:cliphad four copies, two of which returnedmax + 1characters while promisingmax(lib/show.tsnow imports the shared one;lib/search.tsandelectron/menus.cjskeep theirs deliberately — an indexed prompt must not grow a trailing "…", and main can't import a.tsmodule — and say so in a comment);folderNamehad two implementations that answered identically on every input, so the project bar re-exports the one the provider views already use; andlib/settings.tshad 33 hand-written getter/setter pairs, now built onreadSetting/writeSetting, which is where the "absent means the default, and""is the read-time fallback" rule lives in one place instead of six. No behaviour change. - One "Link to…" picker for every provider drawer. ▲ Vercel, Supabase, Cloudflare and 🚀 BlastCP used to ship four near-identical searchable-list pickers; they now share
components/DbSelect.tsxfor the UI (search, loading/empty notes, row list, Cancel, "best fit" badge, optional "+ New project" slot) andlib/db-picker.tsrankItemsfor the shared filter-then-sort loop. Each picker keeps its own score (matchScorein its*-view.ts), so the row tiers don't change. Adding a fifth provider is one file that callsDbSelect, not another list UI to write. - Delete only before the first full turn. A thread's header shows the trash button only until the card finishes its first turn: deleting a wrong start stops its CLI and saves the tokens. After that the header offers only Archive (
hasFinishedTurnincomponents/CardActions.tsx). - No delete before the first turn. The thread header's trash button stays hidden until the card has finished a full turn (or holds an idle saved session, like an imported one), so a stray click can't delete a thread that's still starting (
hasFinishedTurnincomponents/CardActions.tsx). - Rail drawers slide out from under the board. Clicking 🏷, 🔐, 🪝, ▲, Supabase, Cloudflare, BlastCP, GitHub or 🖥 Services now pulls its drawer out from beneath the board, which moves over and lies on it as a sheet with a rounded, shadowed left edge; closing slides the drawer back under. Pin now means Esc leaves it out (
lib/use-slide-out.ts,.board-sheetinapp/globals.css). - Card titles aren't cut at four words. Haiku names a card in 3 to 6 words, and a title over six words that you didn't type (a thread's
create_cardtitle, an old one clipped with "…") is rewritten by Haiku after the card's next turn instead of being clipped (lib/retitle.ts,lib/format.ts). - Threads apply their own migrations, and ask with buttons. A thread that writes a migration for a linked Supabase project applies it in the same turn, renames the file to the version Supabase stamped and checks the advisors, instead of asking whether to; it asks first only when the migration drops or rewrites data. Any choice a thread still needs from you comes as AskUserQuestion buttons, not a question at the end of its reply, and the Supabase drawer's Migrate card asks for its OK the same way (
lib/decisions-prompt.ts,lib/supabase-migrate.ts). - The plan chart now shows the 5-hour limit where you can see it. Hovering the status strip's
5hr/7dopened a chart whose header said "5-hour 34% left" while the plot showed nothing for it: the 5-hour limit is drawn as green 15-minute bars whose height is the share left, so reading the number meant finding the current bar cluster at the "now" line and eyeballing its height off the left axis, under the red weekly overlay. The chart now draws that level: a green line across the open 5-hour window with a dot and the number ("34%") on it at now, so the header's figure is a thing you can see (components/LimitBurndown.tsxNowLevel,lib/limit-chart.tsfiveHourNow). A window that has already reset draws no level, so the plot never contradicts the header, andshareLeft()is the one place a used share becomes a share left. - The web board is the desktop board —
weft.zip/<workspace>/boardnow renders the desktop's own components instead of a hand-rolled lookalike. The header, project bar, column row, open-card frame and reply box are the desktop'sBoardHeader,BoardProjectBar,BoardColumns,CardFrameandReplyBox, with avariant="web"seam that hides only what the Mac owns (the browser and terminal panes, the local Settings, the project's filesystem). The web gained the reply box's modes, ↑ edit-last,!commands and the review panel's filter chips; the framed window is now a real namedrole="dialog". Files:components/BoardHeader.tsx,components/BoardProjectBar.tsx,components/ProjectBar.tsx,components/ProjectMenu.tsx,components/RailBoardButtons.tsx(new),components/BoardColumns.tsx,components/OpenCard.tsx,components/web-board/WebBoard.tsx; deletedcomponents/web-board/WebBoardHeader.tsx,WebProjectBar.tsxandWebReplyBox.tsx. - Weft 0.5.0. The app version moves from 0.4.0 to 0.5.0 in
package.json. Everything downstream reads it from there — the About and startup screens (app.getVersion()), the update feed and release assets (scripts/release-feed.mjs,.github/workflows/release.yml), and the packaged app — so the bump is the one edit the release needs. When the bump commits, Weft's own background changelog writer moves the Unreleased entries under## [0.5.0]and commits the notes alone; tagv0.5.0after that commit so the release feed finds its section.
Fixed
- The status strip shows the right CLI's limits and model. With no card open, the footer copied whichever card had changed last (its model, tokens and limits), so it flipped between a puku card and a Claude card on its own; and a card switched with Run on … kept the old CLI's numbers, so a puku card showed Claude's
5hr:0%hours after Claude's reset. The strip now has two parts: the open thread's ([claude · opus-5-5]from the model its session started on, context, cache, tokens), and the plan's, each CLI's newest 5-hour and weekly reading from~/.weft/limits.jsonl(a passed reset reads full). With no card open it lists every CLI's plan by name. A CLI switch clears the old session's numbers, readings are logged under the CLI that sent them, and the chart draws one CLI's plan again, since Claude Code and puku-cli are on different plans (lib/plan-limits.ts,app/api/limits/plans,lib/use-plan-limits.ts,components/StatusStrip.tsx, docs/architecture/limits-burndown.md). - Cloud sync drains again when Cloudflare blocks a row. Supabase's WAF answered one upload with an HTML "you have been blocked" page, and the outbox stalled behind it from 2026-10-18 (shared threads showed only a screenshot). A blocked request is now re-sent with its strings JSON-escaped (same value in Postgres, nothing for readers to decode); an item still blocked is skipped and named in the Sync status, and HTML error pages are named by their title instead of dumped into the log (
lib/cloud-sync-send.ts). - A shared data app loads its data on weft.zip. Growth showed "bridge.access is not a function": the web bridge lacked the Mac's
access/requestAccess/revoke, now answered byapp/api/a/[id]/access. And its read keys could never be uploaded, because Upload secrets only counted asecretsblock; the keys a manifest's reads need (lib/app-read-needs.ts) now count in the Share dialog, the push andapp/api/apps/secrets. - weft.zip's public pages draw, load and fail like a finished product. Every shared mini app (
/@user/apps/<slug>) crashed with React #441 (a function passed from the server page intoSharedApp); the secret form is nowSharedApp's own state. Every weft.zip link unfurled with no picture:/opengraph-image500'd because Satori can't read the variable Fraunces font, so it now draws with a static cut (public/fonts/fraunces-500.ttf) and lists its files for tracing (next.config.ts);app/opengraph-image.test.tsdraws it. Profiles, shared cards, mini apps and the web board load behind skeletons shaped like the page instead of a line of unstyled text (components/web-states/Skeletons.tsx); a crash on weft.zip shows a web error page with Try again and a reference instead of the desktop's "~/.weft" message (app/error.tsx,lib/local-board.ts), and a dead link gets a real 404 (app/not-found.tsx). A shared pane or app that doesn't load shows Loading, then "This page didn't load" with Try again and Open ↗ (components/share-view/PaneFrame.tsx). Visitor copy no longer names env vars,~/.weftor desktop-only paths. - New thread's Start now stays inside the form, and the pane no longer slides shut after you close it. The footer wraps beside the browser pane (
components/NewCardDialogFooter.tsx), and the board's pane holds its prior visibility while the splash restore lands (lib/use-new-thread-pane.ts). - Subagents on Opus 4.8 work on puku-cli. It refuses any subagent model outside
puku-ai-2.7,puku-ai-2.8andopus, so Weft now sendsopusfor Opus 4.8;/weft:done's Agent-tool fallback names a model each CLI accepts and treats an all-error Puku batch as a failure (lib/subagents.ts,plugin/skills/done/SKILL.md). - The replayed welcome closes like any other dialog. Escape, a click outside it, or the ✕ in its top bar all dismiss it; before, only Skip did. First launch still ignores all three — that screen is where your answer gets written, so drifting off it would lose it (
components/WelcomeDialog.tsx,components/WelcomeFooter.tsx). - A local Weft never calls home. PostHog's browser SDK, its server-side AI events (Haiku and scout prompts and replies) and its log exporter all started wherever the PostHog keys were set, the desktop board included, and printed a "variable required by PostHog is missing" error on every Haiku call when they weren't. They now run only on weft.zip, gated on the page's host in the browser and on
isDeployed()on the server (lib/analytics-gate.ts,lib/analytics-server.ts). - You can see and undo which folders threads trust. Settings → General has a Trusted folders row listing the folders outside any project that threads may use without asking, with an Add folder button (the same OS folder dialog the project and repo pickers use) and a Remove per folder. "Always" on a folder-guard prompt used to be a one-way door: nothing showed the list and nothing took a folder back out, so a folder trusted once stayed trusted. A revoke applies to new work; a thread already running keeps what it was granted until its turn ends (
lib/settings.tsunallowFolder,DELETE /api/folders,components/TrustedFolders.tsx). - Accounts says how long each sign-in lasts. Settings → Accounts has a Sign-in expiry row, so a lapsing sign-in is something you find in Settings rather than when a thread trips over it mid-run. BlastCP's 30-day sign-in is the one that runs out; it speaks up in its last week using the same note as its 🚀 drawer. Vercel, Supabase and Cloudflare tokens don't expire and say so. Only the expiry crosses from Electron main — the tokens themselves stay in the Keychain as before, and there's deliberately no silent auto-refresh (
components/TokenExpiry.tsx). - The edit lock covers Bash too. A Bash line that writes a file another busy card is editing (
sed -i,> file,prettier --write,cp,rm…) asks first like an edit does; whatever a Bash call wrote becomes the card's claim; claims drop as soon asgit statusshows the file clean (a thread's owngit commit, not just Done); and a failed typecheck, test or lint run is told which failures sit in files another card is mid-edit on, so it leaves them alone (lib/bash-claims.ts,lib/bash-writes.ts,lib/dirty-files.ts,lib/failure-owners.ts). - Laptop mode gets its own Settings page, and Always while threads run. Keep Working moved off General onto Settings → Laptop mode, with a switch (also in the menu bar's Keep Working menu) that turns it on by itself whenever a thread is running and off once they finish, an Only while online switch, and a battery floor you pick (5–50%) instead of a fixed 10% (
electron/keep-working.cjs,electron/keep-working-prefs.cjs,components/LaptopModeCard.tsx,components/LaptopModeRules.tsx). - Blanks show their whole name and hint, with links. A secret's name in the 🔐 drawer wraps instead of being cut off, a blank's hint shows in full, and any https link in it opens that page in the browser pane, where you're signed in; threads are asked to put the direct dashboard link in every
add_blankhint, now up to 400 characters (lib/hint-links.ts,lib/open-in-pane.ts). - Resizable rail drawers. Drag the board's edge over an open drawer (🔐, 🪝, ▲ and the rest) to widen or narrow it, 220 to 640px; the width is remembered and a double-click puts it back to 272px (
lib/use-drawer-width.ts). - The board explains itself. An empty board shows a Start your first thread panel with New thread and the welcome's four starters, and the first card to reach Needs you, Review and Done gets a one-time tip under that column's header (the last one opens your Wefter Profile). Settings → General → Reset tips shows them again (
lib/tips.ts,lib/use-onboarding-tips.ts,components/OnboardingTips.tsx,components/CoachTip.tsx,components/EmptyBoardStart.tsx). - A welcome that ends on a running thread. First launch is now three screens for everyone, history or not: Get ready (a real board screenshot and each agent CLI's login, with Log in), Bring in your projects (with the history switch that used to be its own screen) and Start your first thread (a project, a starter prompt, Start). The three drawn explainer screens are gone (
components/WelcomeDialog.tsx,components/WelcomeFirst.tsx,lib/onboarding-events.ts). - React mini apps, hosted under their owner. A folder with
App.tsxand nopackage.jsonbecomes a static page Weft bundles with its own React and rebuilds on save. Shared, an app lives atweft.zip/@you/apps/<slug>orweft.zip/<team>/apps/<slug>, every file of its folder served in a sandbox, and the pane shows its name and a Share button instead of afile://path. Apps reach other services throughweft.fetchwith{{secret:NAME}}placeholders that Weft fills outside the browser, only for the hosts, methods and pathsweft-app.jsonallows: the author's key, or each viewer's own, stored encrypted on weft.zip (supabase/migrations/20261019095618_app_hosting.sql,lib/app-bridge.ts,electron/app-fetch-policy.cjs,lib/app-build.ts, docs/architecture/mini-app-hosting.md). - Mini apps open on the left. With no card open, a mini app tile opens the browser pane between the rail and the columns, sliding in and out; a second click on the lit tile closes it. The page shows once the pane is fully open, and the pane's resize divider is its right edge (
components/StandaloneBrowserPane.tsx,components/AppsRail.tsx,components/BrowserPane.tsx). - Skill sets. Threads get only the skills their project uses. ✦ on the rail is the project's default set (Weft's skills, the project's, yours and your Claude Code plugins, each with a switch), seeded once from the skills its threads ran; ✦N in a card header changes that card's set alone, from its next message. Claude Code and puku threads get it as
skillOverridesandenabledPlugins, Codex and puku as a filtered skill index; hidden skills still run as/name(lib/skill-sets.ts,lib/skill-usage.ts,lib/skill-library.ts,lib/skill-set-actions.ts,app/api/skills/set,components/SkillsDrawer.tsx,components/SkillsChip.tsx). - Backup model + delete/rerun on a failed first message. A card whose very first turn came back with an error (a rate limit, mostly) now shows a banner with Rerun on and Delete. The backup model is a new Settings → Models row (
components/BackupModelPicker.tsx,backupModel); any model any installed CLI serves, plus an "Off — no backup" leading option. With no backup set, the Rerun button reads "Pick a backup model" and opens Settings → Models. The handler islib/backend-switch.tsrerunOnModel(clears the session, sets the model, replays the last user message); the route isPOST /api/cards/:id/rerun-model; the predicate that picks when the banner shows islib/first-message-failed.ts(components/FirstMessageErrorBanner.tsx,lib/settings.ts,app/api/settings/route.ts). - Fresh start after a model or CLI switch. When a card's session was reset (the model or CLI was just switched, or the card was never started), the reply box now defaults to Send fresh — a new session in the same card, started from a brief of the thread and its diff — instead of plain Send. The chip says "Fresh start · was on another model", the Continue button and ⌘⇧↵ are hidden (nothing to continue), and Codex falls back to plain Send with the same chip (
lib/reply-mode.ts,components/ReplyBox.tsx,components/ThreadFooter.tsx,lib/use-reply-continue.ts). scripts/release-channels.mjsgenerates a Homebrew cask and a signed apt repo from a release's dmg and deb. Not wired into the release workflow yet.- Limits chart: a 15-minute background poll (
lib/limit-poll.ts) keeps the chart filling in between thread turns, reusing a warm spare or opening a one-off session, and the 5-hour lane now shows 15-minute bars (4 per hour) instead of hourly. - Auto-resume parked cards when the limit resets. The same 15-minute poll now watches the 5-hour and 7-day windows for the drop from 100% to <100%; when it sees a reset, every
needs_youcard withrateLimitHiton that backend replays its last user message (the same pathswitchBackendtakes on a manual "Run on …"). The card'slastLinereads "Auto-resumed after 5-hour reset" (orweekly) and the chip on the card shows "Auto-resumed · 4:12am" with a tip. Busy cards are skipped; the next cycle picks them up (lib/limit-history.tssetOnLimitReset,lib/limit-resume.ts,lib/limit-poll.ts,components/CardMarks.tsx,lib/types-card-detail.ts). - Cockpit scene. A slate-grey space theme: the windshield of a spaceship over a starfield with a sliver of dashboard, the splash screen's steel weave emblem lit on the console and cyan screens as the accent. Picture rendered with BlastCP; Settings → Appearance or ⌘K "Theme: Cockpit". A scene can now set
--scene-posto move the picture's crop (lib/theme.ts,app/globals.css,electron/assets/backdrop-cockpit.jpg). pnpm dist:linuxpackages Weft as an AppImage and deb (run it on a Linux machine; the toolchain wrappers andnode-ptybuild are per-OS). Only packaging so far: the vault unlock, Keep Working and process cleanup are still Mac-only.- Many teams, and projects in a team. Settings → Teams (it was "Team") opens on Your teams, every team you're in with your role, the one you're viewing lit; click another to manage it below (hovering a row preloads it, and the list stays up while it loads), and New team at the foot makes one you own. A team you own has a pencil beside its name that renames it in place (
renameop, owners only; the slug stays, so links and synced boards keep working), and while you're in another team too, a trash that deletes it once you type its name (deleteop,lib/team-delete.ts: refused while a paid plan still bills; everything under the team cascades, and this Mac looks its sync team up again). The pick is remembered. The invite form's role descriptions are Weft's own now, not BlastCP's ("posts", "channels") from@aimhuge/teams(lib/team-types.tsROLE_DESCRIPTIONS,components/team/TeamList.tsx,components/team/TeamPanel.tsx). A team has many projects (GitHub repos): owners and admins Add project (from the board's repos orowner/name) and Remove them. People on your projects lists the repos' GitHub collaborators with an Invite button. Its table is theorg_projectsmigration (supabase/migrations/20261019100423_org_projects.sql, applied). Cards still sync to your first owned team; per-project routing is next (components/TeamSettings.tsx,components/team/Projects.tsx,lib/team-projects.ts,lib/team-create.ts,lib/github-collaborators.ts). - Icon-only header buttons. Copilot, Browser and New thread in the top bar are icons now (names and shortcuts stay in the tooltip), saving width on a 13" screen (
components/BoardHeader.tsx,components/CopilotButton.tsx). - Three intro screens in the welcome. Before the Wefter Profile and the projects, the first-launch welcome shows an open card with the browser pane explaining threads and the copilot, an empty board with each swimlane explained (Review as a walkthrough, Done as commit, refactor and docs), and the left bar. Drawn as mockups with the app's colors; Next / Back, five dots (
components/WelcomeIntro.tsx). - ServerErrorBar when the dev server's restart budget trips at runtime. When the supervisor's 4-in-2-min crash budget trips while the board is up (no longer only at startup), a small bar slides in at the bottom-left with Restart server, Open logs, and Fix with a thread — the third queues a
weft://new?…link pre-filled with the fatal and the last 4 KB ofserver.logso the New thread dialog opens ready to start once the server is back. The board stays on the last data the server returned instead of being replaced by the splash; startup fatals (server never came up) still splash-fail as before. The supervisor'sonNotifycarries the event toappView.webContents(electron/server.cjs,electron/server-ipc.cjs,electron/main.cjs,electron/preload.cjs,lib/server-status.ts,components/ServerErrorBar.tsx). - Settings → General → The welcome → Test it: runs the real first-launch welcome again, even on a board that was adopted with cards, and saves your answers.
- Continue is back in the reply box beside Done, whenever the thread is idle, and ⌘⇧↵ sends it (it was ⌘. before).
- Renamed the last "threadboard" names to Weft: the write header is
x-weft-token(the launch token is accepted in its place), the env vars areWEFT_*, and the desktop bridge iswindow.weft. Old~/.threadboarddata is still moved to~/.weft. pnpm dist:remote(scripts/package-remote.sh): builds the standalone app on m1mini or m2mini over the LAN, then packs and signs here with the Flexbike Developer ID (--checktests the builder and identity first).- A whole-suite
pnpm testno longer takes Weft down with it. The memory cap added inf9140cfdid nothing: Vitest'svmMemoryLimitis a no-op onpool: "forks"(itsgetMemoryLimit()returns null for any pool butvmForks/vmThreads), reads a bare number as bytes rather than MB, and only measures V8'sheapUsedafter each test file finishes — so it could not see, let alone stop, the off-heap growth that crashed us. An RSS watchdog now polls the vitest process tree every 2s and ends a run whose real resident memory crosses 3072 MB (WEFT_TEST_MAX_RSS_MB,--no-watchdogto bypass), wired throughrunHolding's newwatchdogoption. A run of named files is never gated or watched (bug-fixes/027). - A shared mini app's link no longer says "Not found" for a folder share. The Mac can share an app as a whole folder, and the deployed page route only knew single-page rows; it now serves the folder's entry file through the same gate and sandbox (bug-fixes/024).
- The "+N/-N" counter's Review N cards button can be clicked: its bubble stays open while the mouse crosses into it (
Tip's newinteractivemode) instead of closing the moment you leave the counter. - A multi-select question's
[ ]no longer wraps into "[" over "]", and the board footer's limits line no longer peeks out under an open card. - "Review all" and "Resume N" stay once the board loads. Backlog's column appears after the cards load and narrows every column below 230px, where the header hid all its actions. Narrow columns now keep them as icons (✓, ▶ N) and hide the row only under 150px, beside the open copilot lane (
components/ColumnHeader.tsx). - "Show N more" no longer jumps while the board loads. Until a card's component loaded, the column drew nothing, so "Show 5 more" floated to the top and then dropped below the cards. Each card now draws a card-sized blank first (
CardSkeletonincomponents/BoardColumn.tsx). file.ts:42in a thread jumps to line 42. The editor column opened the file at the top: the request's line never reachedCodeEditor'slineprop. The tab now carries it (setLineinlib/editor-tabs.ts).- Resting the mouse on the copilot pill peeks. The hidden lane's pill took an
onPeekit never called; hovering it for 350ms now opens the peek digest, a pass over it doesn't (components/CopilotHider.tsx). - No more
Date.now()prerender errors on weft.zip's cold starts. /login, /team, /[orgSlug]/team, /[orgSlug]/billing and /[orgSlug]/board logged "encountered the unstable valueDate.now()while prerendering" whenever a fresh lambda built its first Supabase client:cookies()alone doesn't move a render past that stage, onlyconnection()does.webClient()inlib/web-session.tsnow awaitsconnection()first, which covers every gate (requireTeam,currentWorkspaceSlug,readSessionIdentity); /s, /a and /u, which reach the service client without the gate, call it themselves.lib/prerender-check.tsnow flags a page or layout that callsserviceClient()beforeconnection(),webClient()orwebUser(). - The weft.zip invite page works when you're signed in. On a cold start, building the Supabase client read
Date.now()before anything request-bound, which cacheComponents refuses, so the page sat dimmed on "Checking your invite…";app/invite/[token]/page.tsxnow callsconnection()first. The Join button also sits inside the invite card instead of bare on the dark page. - A Done card's Archive button is back.
CardViewdrew it only when givenonArchive, which the board never passed; the card's handlers now come fromlib/card-handlers.ts, archive included. The Done header's Archive all icon istext-mutedso it no longer vanishes over a scene. - Second pentest pass (2026-10-18), six findings. F-1:
electron/pane-routes.cjs, whichelectron/main.cjsrequires, was lost to a stash and the shell couldn't start; restored with its test. F-2: the share unlock rate limit (lib/share-unlock-rate.ts) is one atomic Postgres call,share_unlock_bump(supabase/migrations/20261018045842_share_unlock_bump.sql), so racing attempts can't pass the 10-per-10-minutes cap. F-3: the terminal's IPC answers the board window only, and each shell only its opener (electron/terminal.cjs, the M-3 fix that never landed). F-4: Keep Working's IPC answers the board window only (electron/keep-working-ipc.cjs). F-5:weft://new?folder=documented inelectron/new-thread-link.cjs(the Start click is the gate). F-6:POST /api/s/[id]/unlockanswers 503 off Vercel, wherex-real-ipis caller-controlled. pnpm auditcleanups, four of six advisories fixed. Bumpedvitestand@vitest/coverage-v8to 4.1.11 (clearstinypool,vitest, and@vitest/mockeradvisories); addedpnpm.overridesfortinypool,bracesandsprintf-jsto pin patched transitive versions. The two remaining (braces,sprintf-js) have no upstream patch beyond the override. Also wired the web board's reply box (aWebReplyBoxthat had been written but never rendered), fixed route test pollution incloud-sync-routes/vercel-routes(lib/folders.tsresolveFoldernow throws on empty input instead of silently usingos.homedir()), and ran the suite onpool: forksto dodge vitest 4's threads-pool worker-spawn timeouts (vitest.config.ts).- Task notifications stay in the chat. When a background command or subagent ends, the CLI tells the agent with a
<task-notification>message that can start a turn of its own; the SDK streams only atask_notificationevent, which the runner ignored, so that turn’s reply seemed to answer nothing. The thread now shows a<task-notification>row with the task’s summary (red if it failed); foreground tasks, whose tool call already reports them, stay out (lib/runner-tasks.ts). - A wrap-up that can't finish stays in Review. When
/weft:doneends withcolumn=review(it couldn't commit, the push was rejected, tests failed), the card stays on the board in Review with what's left, instead of being filed into Done and archived as if it had shipped (lib/runner-turn.ts). And a thread now reportscolumn=doneon its own only when none of its file changes are left uncommitted; with changes it reports Review, so your move to Done commits and pushes them (lib/settings.ts). - Done commits and deploys every time.
/weft:donecommits files other cards also edited whole instead of stopping to ask or building its own git index, always pushes, and then checks the push deploys:plugin/skills/done/deploy.mjsconfirms the linked Vercel project builds from origin's repo (by id), then waits for HEAD's build to finish behind any deploys already running. A failed build comes back with the tail of its log: the wrap-up fixes and pushes again (twice at most) when the cause is its own, else starts a card to fix the build and stays in Review. Weft's own Vercel project had stayed linked to the renamedaimhuge/weft-historysince 2026-10-16, so 55 pushes never deployed; it's relinked toaimhuge/weft. - Orphaned background tasks read as cut off. A resumed CLI reports the tasks a previous process exit left behind as "stopped … Orphaned by a previous Claude Code process exit"; the task strip now shows them as cut off, with a hint to send a message, and the thread is told about them on its next message (once).
- Orphaned vitest workers. A test run killed mid-way (a thread's Bash command stopped or timed out, a card stopped) left its
node (vitest N)workers running under PID 1. Twelve of them hung the Mac on 2026-10-17. Workers now end themselves when their run dies (lib/orphan-guard.tsas a vitest setup file), stopping a card ends the process groups its Bash commands run in (lib/process-tree.ts), whole-suitepnpm testruns take turns and pass a stop on to vitest (scripts/run-tests.mjs), and the server reaps any orphaned worker in a project folder every 5 minutes (lib/worker-reaper.ts; docs/architecture/threads-and-sessions.md). - Security audit findings, fixed. The first pass of
weft.zip's security audit found 3 medium and 5 low issues; all 8 are now resolved: the share-gate unlock rate limit moved to a Supabaseshare_unlock_attemptstable keyed on a peppered SHA-256 of the client IP, with the route taking the real remote address over anX-Forwarded-Forcheck (lib/share-unlock-rate.ts,app/api/s/[id]/unlock/route.ts,supabase/migrations/20261018120000_share_unlock_attempts.sql); the share-thread redactor now scrubs high-entropy strings (40+ char runs above 4.7 bits/char) so API tokens in code aren't surfaced to a viewer (lib/share-redact-entropy.ts,lib/share-redact.ts); the terminal drawer scopes every spawned shell to a single session id (lib/terminalroutes);cloud_commandsrows on Supabase are now claimed by the device that should run them, with aBEFORE INSERTtrigger that requires the row'sdevice_idto be the same account's signed-in device, andcard_idis set only by the receiver after it creates the card (supabase/migrations/20261018130000_cloud_commands_device_ownership.sql,lib/cloud-inbox.ts);weft://new?prompt=…now opens the New Thread dialog pre-filled instead of starting the card itself, so a clickjacked link can't fire a thread (electron/new-thread-link.cjs); the dev-server launcher parses the command into argv and runsspawnwithshell: false, refusing any command with shell metacharacters (lib/devserver.ts); and the browser pane'sopenIPC handler now only answers the board window's sender, so a compromised renderer can't navigate the pane to an arbitrary URL (electron/main.cjs,electron/browser-pane.cjs,electron/pane-origins.cjs). - New thread's browser pane fills the window. The pane beside the New thread form used to keep the width you'd dragged it to in an open card, which left an empty strip on the right. Now it fills everything beside the form (
components/NewThreadWindow.tsx).
Security
- A local process can no longer read the board. Once the desktop app writes a launch token, every API route needs it — reads included, not just writes. Before, a read passed on the localhost Host check alone, so any process that reached the port (a browser extension, a malicious
postinstall, another local app) could read/api/webhooks(project paths and the names of the vault's secrets),/api/projects,/api/game,/api/githuband 15 more. Only Weft's own window is given the token (electron/api-token.cjs). The per-routesensitiveflag is gone: it existed to mark the six routes that leaked and no longer changes any verdict (lib/guard.ts; pentest 2026-10-23, P-1). - A thread can no longer approve itself through the board view.
mcp__board_view__clickandtype_textrefuse anything insidedata-human-only: the permission panel (Allow, folder This card / Always), the reply box while a question waits, the mode picker and Settings. Screenshots and reads still work (electron/board-view.cjs; pentest 2026-10-20, P-1). - Synced threads are redacted before they leave the Mac. Cards, thread items and long tool outputs go through the share redactor (
findSecret+ the entropy pass) before the upsert, so a key that reached a thread never lands in rows every workspace member can read; the raw text stays local (lib/cloud-sync-redact.ts; P-2). - "Always allow" on a vault command lasts only until the code changes. A
secret_runrule now also pins a fingerprint of the folder's tree (HEAD, the diff against it, untracked files' bytes), so a thread editing a file the approved script runs makes Weft ask again; outside a git repo it always asks. Dev-server grants are unchanged (electron/run-rules.cjs; P-3). - Power writes need the launch token. Answering a permission, changing settings, allowing a folder, running
!shell, loosening a card's mode or granting it a read scope now refuse the barex-weft-token: 1a thread'scurlcan send; ordinary card moves and creates still take it (lib/guard.tselevated; P-4). - A webhook relay delivery can no longer climb out of its target's path (
..,%2e%2e) to another local route, and the relay dropsx-threadboardfrom a stranger's request. Account, settings and services reads need a per-launch token the board window adds itself (electron/api-token.cjs).weft://links only act from a page under~/.weft, not anyfile:page. - The email forwarder scrubs CR/LF/NUL from the parsed From display name, the subject, the alias and the round-tripped
X-Forwarded-*headers (and every entry in the reply-to list), so an attacker mailing to@weft.zipwith a craftedFrom:can't smuggle extra headers into the forwarded mail. Five new tests inlib/email-forward.test.tscover the cases (pentest 2026-10-20, F-7;lib/email-forward.ts). - A hosted mini app's manifest can no longer point its Supabase read at a host of its choosing. A named Supabase read sends the app author's real
SUPABASE_SECRET_KEYas a bearer token to the URLweft-app.jsondeclares, and that URL was copied into the share row with only atypeofcheck — so a manifest naming an attacker's host had weft.zip hand the key over on the first read.supabaseUrlmust now be anhttpsSupabase project URL (.supabase.co/.supabase.in, no credentials, no port), checked both when the manifest is reduced and again at read time so rows written earlier can't leak either (lib/app-folder.tssafeSupabaseUrl,app/api/a/[id]/read/route.ts; pentest 2026-10-20, F-11). - …and neither can a saved local one. That fix reached only the hosted route; the local read path is a second implementation of the same read, and it had no such check — a data app saved from a folder could name any host and collect the project's
SUPABASE_SECRET_KEYasapikey+Beareron the first read.supabaseUrlis now judged by the same rule in both places, at manifest load and again before the secret is read, and the request is built on the validated origin rather than the raw string (electron/app-data-reads.cjssafeSupabaseUrl,electron/app-data-frame.cjs). A parity test asserts the CJS copy and the.tsoriginal agree on every case, so the next fix can't land on half the code again (electron/app-data-reads.test.ts).