Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d5b8bb4
fix(agents): launch Codex with its idle animations off
mxrsv Sep 17, 2026
6216b5d
feat(feedback): persist moderated Google feedback and email updates
mxrsv Sep 17, 2026
4a96c4a
build(deps): move renderer-only packages to devDependencies
mxrsv Sep 17, 2026
3e3862b
fix(agents): pair Codex panes with exact process sessions
mxrsv Sep 18, 2026
c38940a
fix(agents): ignore Codex startup output until real input
mxrsv Sep 18, 2026
4e89c28
feat(dev): brand macOS development launcher as Deck Dev
mxrsv Sep 18, 2026
b3180fd
perf(dev): make Electron reload incremental and dependency-aware
mxrsv Sep 18, 2026
0c5d48c
feat(launcher): open agents from a transient stage page
mxrsv Sep 18, 2026
d2257fc
feat(agents): show agent context above each terminal pane
mxrsv Sep 18, 2026
af7e2a9
feat(sidebar): pair the collapse control with Deck identity
mxrsv Sep 18, 2026
7a67fb2
feat(theme): move the dark stage to neutral black
mxrsv Sep 18, 2026
313bcaa
chore(gallery): park the quick agent board study
mxrsv Sep 18, 2026
1da6e01
docs: record the launch page, pane header and sidebar identity
mxrsv Sep 18, 2026
cae639f
fix(rail): keep the checkout actions menu reachable for a bare folder
mxrsv Sep 18, 2026
556a230
fix(images): refuse a workspace favicon that resolves outside its folder
mxrsv Sep 18, 2026
ef50d35
fix(theme): follow the neutral-black ground on every host
mxrsv Sep 18, 2026
fc1e112
test(launcher): cover the launch paths that could strand the page
mxrsv Sep 18, 2026
8dea78b
docs(design-language): declare the launch page and pane header rules
mxrsv Sep 18, 2026
1db9db8
fix(shell): keep the stage's structural line on this branch
mxrsv Sep 18, 2026
af42735
style: satisfy the formatter on the three files CI flagged
mxrsv Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ the release PR, and frozen at the tag — never an auto-generated commit list.

## Unreleased

### Feedback

- The [feedback page](marketing/landing-prototype/src/feedback.js) supports Google sign-in,
durable private submissions, owner-approved public descriptions, older feedback pages,
and approval/progress email updates. Sending remains closed until service configuration
and rollout verification are complete.

### Usage

- **See remaining allowance before historical cost.** The redesigned
Expand All @@ -17,6 +24,16 @@ the release PR, and frozen at the tag — never an auto-generated commit list.

### Sidebar

- **Workspace favicons in the sidebar.** [Project headers](src/ui/agent-rail.tsx)
show the workspace favicon when available, falling back to the folder icon
when the image is missing or cannot be displayed.

- **Deck identity in the sidebar.** The [top row](src/ui/sidebar-toggle.tsx) now pairs
the collapse control on the left with the Deck logo, name and running version
on the right, separated from navigation by a border. The bordered `New Workspace`
button sits below it, above
the [scrolling project list](src/ui/agent-rail.tsx).

- **Agent usage replaces Unread.** The [compact sidebar summary](src/ui/usage/agent-usage-summary.tsx)
shows agent logos and remaining allowance, with reset times on hover.
Electron reads Codex limits through its CLI and Claude Code limits through a
Expand All @@ -25,6 +42,26 @@ the release PR, and frozen at the tag — never an auto-generated commit list.

### Agents

- **Agent context above each terminal pane.** The
[compact header](src/terminal/pane-agent-header.tsx) shows the agent logo and
the same latest message as the sidebar. Claude Code panes offer an Effort
button that opens the CLI's native model/effort picker. Use Left/Right to
choose effort and S to apply it to this session only; other agents have no
effort button.

- Fresh Codex panes no longer show busy bars while their startup screen is painting
before the first input, through the [input guard](src/terminal/agent-attention.ts).

- **Fresh Codex panes keep their own conversation.** The rail
[requires an exact session identity](src/terminal/session-tail-store.ts), so a new pane
stays blank until its own conversation is available, even when another Codex is active
in the same folder.

- **Launch agents side by side.** The compact [agent launch page](src/launcher/agent-launch-page.tsx)
opens from checkout New agent controls or Cmd/Ctrl+T. Run adds a pane beside the target;
Back and Escape return without creating a terminal. Right-click actions keep their
existing new-tab and shell behavior.

- **Signals are off again after upgrading from 1.1.** Deck 1.1 saved every
agent's reporting switch as on, and 1.2.0 took that as your choice, so it kept
adding its hooks to your Claude and Codex settings. This update
Expand All @@ -34,6 +71,11 @@ the release PR, and frozen at the tag — never an auto-generated commit list.
- **Codex no longer asks you to review your own hooks again.** Deck's Codex
hooks [stay where they are](electron/agent-hooks/codex-hooks.ts) when Deck
starts, instead of moving behind hooks you added later.
- **Codex rows stop showing the busy bars while Codex is idle.** Codex 0.154
animates its prompt background, which Deck read as work in progress. Deck now
[launches Codex with its idle animations off](src/lib/agents/codex.ts); if you
wrote your own Codex command, add `-c tui.animations=false` or use the new
"No idle animations" switch under Settings → Agents.

### Updates

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ local agent storage.
## Built-in agents

- **[Claude Code](https://claude.com/claude-code):** `claude --dangerously-skip-permissions`
- **[Codex](https://developers.openai.com/codex/cli):** `codex --dangerously-bypass-approvals-and-sandbox`
- **[Codex](https://developers.openai.com/codex/cli):** `codex --dangerously-bypass-approvals-and-sandbox -c tui.animations=false`
(the last flag stops Codex's idle animations from reading as a working agent)
- **[OpenCode](https://opencode.ai):** `opencode`
- **[Antigravity](https://antigravity.google):** `agy --dangerously-skip-permissions`
- **[Gemini CLI](https://github.com/google-gemini/gemini-cli):** `gemini --yolo`
Expand Down
107 changes: 81 additions & 26 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Specification and decisions: [DECK-1](https://linear.app/mxrsv/issue/DECK-1) `decided`.
This directory is deployed independently from the desktop and landing. It shares no database
with sibling SpaceVibe products. Entry point: [Worker](src/worker.mjs) `current`;
[deployment configuration](wrangler.jsonc) `current`; [privacy notice](../marketing/public/privacy/2026-09-12/index.html) `current`.
[deployment configuration](wrangler.jsonc) `current`; [privacy notice](../marketing/public/privacy/2026-09-18/index.html) `current`.

## Run and deploy

Expand Down Expand Up @@ -37,7 +37,8 @@ prevents stale retries from reinserting identifiers already removed by retention

204 means the D1 upsert completed. 400/413 are terminal, 429/503 retryable. Other routes
return 404 and non-POST ingest returns 405. No route reads or exports usage data; the one
read route, the feedback board below, never touches D1. A shared
read route, the feedback board below, reads only feedback tables through the
[feedback repository](src/feedback-repository.mjs). A shared
1000-request/minute, per-Cloudflare-location limiter bounds writes without reading IPs;
it is best effort, not a strict global spending cap. Its namespace is service-specific.

Expand All @@ -55,35 +56,89 @@ and [the update-counter column](migrations/0002-update-counters.sql).

## Feedback

The landing's `/feedback` page ([DECK-101](https://linear.app/mxrsv/issue/DECK-101)) talks to
[`/v1/feedback`](src/feedback-routes.mjs), which keeps nothing in D1: Linear is the only store.
`POST` [validates](src/feedback-payload.mjs) a title, body, category and optional draft UUID (up
to 16 KB, so 2,000 characters fit in any script), drops a filled honeypot with a silent 204,
applies its own `FEEDBACK_LIMITER`, then creates an issue in SpaceVibe-Deck with the `Feedback`
label in **Backlog**. The visitor's text sits in a fenced block under `## User report`, with
linear.app links defused so a paste cannot mention anyone; `Other` gets `Needs decision` instead
of a Type label. The draft UUID becomes the issue id, so a resend after a lost answer finds the
issue that already landed instead of creating a second one. `GET` returns the board — identifier,
title, category, column and update time, never the description — edge-cached for 60 seconds per
Cloudflare location. CORS allows only `deck.spacevibe.dev` and the local landing preview.

Backlog is the moderation gate: nothing there is public. Moving an issue to Todo publishes its
title, so rewrite a title in Linear before publishing it. The [board query](src/linear-feedback.mjs)
asks Linear only for published state types and maps by type: unstarted → pending, started →
review, completed → done (30 most recent). Every connection names its own `first`: Linear's
complexity budget is per user and shared with every other tool using the key owner's account.

With logging off, the `17 * * * *` [probe](src/linear-feedback.mjs) is the only signal that
submissions fail: it rejects when the key, team, labels or Backlog state are missing or archived,
so check that cron's invocations after any key or workflow change. The landing keeps sending
closed (`SUBMISSIONS_OPEN` in [feedback-api.js](../marketing/landing-prototype/src/feedback-api.js))
until the Worker ships; flip it only after a probe passes. Team, label and Backlog IDs are `vars`
in [wrangler.jsonc](wrangler.jsonc); the key is a secret, and without it both routes answer 503:
[DECK-101](https://linear.app/mxrsv/issue/DECK-101) uses [D1 storage](src/feedback-repository.mjs)
as its source of truth. [POST /v1/feedback](src/feedback-routes.mjs) requires a
[verified Google ID token](src/feedback-auth.mjs), validates the report and required draft UUID,
and returns 201 only after saving a private pending record. A repeat of the same payload and
Google subject/draft ID returns the existing receipt; changed content returns 409. Public and
Linear issue IDs are generated by the server. A filled honeypot is rejected, never acknowledged
as saved. Neither a Linear outage nor an email failure can discard accepted feedback.

The [minute job](src/feedback-sync.mjs) creates a Backlog issue and retries using its stable
server-generated UUID. The owner approves in Linear: Todo publishes Pending; In Progress,
Blocked and Ready for Review map to the middle column; Done remains public. Backlog, cancellation,
duplicate, manual archive or removal of the Feedback label hide the report. A signed
[Issue webhook](src/feedback-webhook.mjs) persists refresh requests and hides deleted issues.
Only D1-owned issue IDs are processed. A missing/inaccessible API response is not treated as
proof of deletion. Webhook delivery and polling are complementary: five records rotate per minute,
so synchronization can take longer than a minute as the backlog grows.

[GET /v1/feedback](src/feedback-routes.mjs) needs no login. It returns original submitted title,
description, category, status, dates and identifiers; never email, Google subject or internal
Linear description. Editing Linear text does not rewrite the original public report: hide an
inappropriate report rather than assuming a Linear edit redacts it. Reads use no-store and an
opaque pagination cursor; there is no permanent cap on old Done items. Feedback has no automatic
expiry (including automatically archived Linear Done issues) and is excluded from [analytics retention](src/usage-repository.mjs). Hiding or deleting
an issue hides the public card but retains the D1 record; it is not a personal-data erasure.

Publication and notification outbox entries commit in one D1 transaction. The first publication
and first In Progress milestone each receive one [Resend notification](src/feedback-mail.mjs).
Signed intermediate In Progress events are retained even when the next poll already sees Done.
Email payloads and idempotency keys remain stable on retries. Ambiguous deliveries stop retrying
after 23 hours (before Resend's 24-hour key expiry) and set `needs_review = 1`; check provider
receipts before manually reconciling them. This avoids claiming exactly-once email delivery.
Hidden reports do not start pending email deliveries; a provider request already in flight cannot
be recalled. Email recipient and title are stored privately in the outbox.

### Configuration and opening intake

The [deployment defaults](wrangler.jsonc) keep both `FEEDBACK_SYNC_ENABLED` and
`FEEDBACK_SUBMISSIONS_OPEN` false. After approval, apply [0003-feedback.sql](migrations/0003-feedback.sql)
with the existing migration command. The migration adds tables without rewriting usage data.
Configure a Google web client with `https://deck.spacevibe.dev` as an authorized JavaScript origin
(and the documented localhost preview origins if needed), then set `GOOGLE_CLIENT_ID` as a Worker
variable. Sign-in accepts verified Gmail and Google Workspace email; no Gmail inbox permission,
Google client secret, refresh token or stored browser credential is needed.

Configure these Worker secrets using Wrangler's interactive prompts; never paste values into
an issue, source file or command argument:

```sh
npx wrangler secret put LINEAR_API_KEY
npx wrangler secret put LINEAR_WEBHOOK_SECRET
npx wrangler secret put RESEND_API_KEY
```

Set `FEEDBACK_EMAIL_FROM` to a verified Resend sender. Create a team-scoped Linear Issue webhook
at `https://api.deck.spacevibe.dev/v1/feedback/linear-webhook`; its signing secret must match.
Check `FEEDBACK_TEAM_ID`, `FEEDBACK_LABEL_ID`, `FEEDBACK_BACKLOG_STATE_ID` and
`FEEDBACK_PROGRESS_STATE_ID` against the workflow. Google client ID and sender are configuration,
not fabricated defaults. The [configuration endpoint](src/feedback-routes.mjs) refuses to open
intake when a required value or synchronization is absent.

After deploying the Worker, enable synchronization and verify the hourly
[configuration probe](src/linear-feedback.mjs). Review the dated privacy notice before publishing
it. Enable `FEEDBACK_BOARD_OPEN` in the [landing API module](../marketing/landing-prototype/src/feedback-api.js)
for public reads. Open the Worker submission switch and landing `SUBMISSIONS_OPEN` only as part of
an authorized acceptance run: sign in, submit, approve, move to In Progress, confirm both emails,
then hide the report. A local build or mocked provider test does not establish these results.
The [dev-only demo](../marketing/landing-prototype/src/feedback-demo.js) remains available for
visual review without production credentials.

For recovery, close intake while keeping synchronization and public reads enabled. Preserve D1
records and pending jobs; do not revert to the old direct-to-Linear writer or drop tables.
Check scheduled invocation outcomes and count-only outbox queries (no email/body exports):

```sql
SELECT count(*) AS unsynced FROM feedback WHERE linear_synced = 0 AND deleted_at IS NULL;
SELECT count(*) AS pending_mail FROM feedback_mail WHERE sent_at IS NULL AND needs_review = 0;
SELECT count(*) AS mail_needing_review FROM feedback_mail WHERE needs_review = 1;
```

There is no external failure alert configured. A failed minute invocation needs investigation;
a skipped lease means another invocation owns the bounded batch. Provider acceptance is not proof
of inbox delivery. Pending runtime and owner acceptance are tracked on the issue.

## Operations and privacy

Worker logging, invocation logging, tracing and Logpush are disabled. Do not use `wrangler
Expand Down
43 changes: 43 additions & 0 deletions backend/migrations/0003-feedback.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- Feedback has owner-controlled retention, independent of usage_days cleanup.
CREATE TABLE feedback (
id TEXT PRIMARY KEY,
draft_id TEXT NOT NULL,
google_sub TEXT NOT NULL,
email TEXT NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
category TEXT NOT NULL CHECK (category IN ('bug', 'idea', 'other')),
status TEXT NOT NULL DEFAULT 'private' CHECK (status IN ('private', 'pending', 'review', 'done', 'hidden')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
linear_identifier TEXT,
linear_synced INTEGER NOT NULL DEFAULT 0,
source_updated_at INTEGER NOT NULL DEFAULT 0,
checked_at INTEGER NOT NULL DEFAULT 0,
deleted_at INTEGER,
UNIQUE (google_sub, draft_id)
);
CREATE INDEX feedback_public ON feedback(status, created_at DESC, id DESC);
CREATE INDEX feedback_sync ON feedback(linear_synced, checked_at);

CREATE TABLE feedback_mail (
id TEXT PRIMARY KEY,
feedback_id TEXT NOT NULL REFERENCES feedback(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK (kind IN ('approved', 'progress')),
recipient TEXT NOT NULL,
title TEXT NOT NULL,
created_at INTEGER NOT NULL,
first_attempt_at INTEGER,
next_attempt_at INTEGER NOT NULL DEFAULT 0,
sent_at INTEGER,
needs_review INTEGER NOT NULL DEFAULT 0,
UNIQUE (feedback_id, kind)
);
CREATE INDEX feedback_mail_pending ON feedback_mail(sent_at, needs_review, next_attempt_at);

CREATE TABLE feedback_jobs (
name TEXT PRIMARY KEY,
lease_token TEXT,
lease_until INTEGER NOT NULL DEFAULT 0
);
INSERT INTO feedback_jobs(name) VALUES ('sync');
12 changes: 9 additions & 3 deletions backend/src/contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { build } from "esbuild";
import { FEEDBACK_SYNC_CRON } from "./feedback-sync.mjs";
import { FEEDBACK_PROBE_CRON } from "./linear-feedback.mjs";
import { AGENT_KEYS, SURFACE_KEYS, UPDATE_KEYS, COUNTER_CAP, validPayload } from "./payload.mjs";

Expand Down Expand Up @@ -80,14 +81,15 @@ test("deployment disables logs, traces, public preview URLs and exposes only the
// drifted schedule would keep raw rows past it and nothing else would notice,
// because logs and traces are off by design.
// The second cron is the hourly feedback probe; the Worker dispatches on it.
assert.deepEqual(config.triggers.crons, ["0 3 * * *", FEEDBACK_PROBE_CRON]);
assert.deepEqual(config.triggers.crons, ["0 3 * * *", FEEDBACK_PROBE_CRON, FEEDBACK_SYNC_CRON]);
});

test("privacy routes publish the dated notice and include its source in the deployment", async () => {
const config = JSON.parse(await readFile(new URL("../../vercel.json", import.meta.url), "utf8"));
// `/privacy` serves the newest notice; every earlier dated copy stays reachable.
for (const [source, notice] of [
["/privacy", "2026-09-12"],
["/privacy", "2026-09-18"],
["/privacy/2026-09-18", "2026-09-18"],
["/privacy/2026-09-12", "2026-09-12"],
["/privacy/2026-09-07", "2026-09-07"],
]) {
Expand All @@ -101,7 +103,7 @@ test("privacy routes publish the dated notice and include its source in the depl
const ignore = await readFile(new URL("../../.vercelignore", import.meta.url), "utf8");
assert.ok(ignore.includes("!/marketing/public"));
const html = await readFile(
new URL("../../marketing/public/privacy/2026-09-12/index.html", import.meta.url),
new URL("../../marketing/public/privacy/2026-09-18/index.html", import.meta.url),
"utf8",
);
assert.doesNotMatch(html, /anonymous/i);
Expand All @@ -116,6 +118,10 @@ test("privacy routes publish the dated notice and include its source in the depl
"no in-app opt-out",
"1.0.0",
"Share usage stats",
"Google",
"Resend",
"private until",
"do not automatically expire",
]) {
assert.ok(html.replace(/\s+/g, " ").includes(term), term);
}
Expand Down
Loading
Loading