Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
88 changes: 76 additions & 12 deletions apps/streamdeck/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,71 @@ and runs it. The plugin holds no Tower state and never edits files directly.
- **Auth**: reads `~/.agent-farm/local-key` and sends it as the `codev-web-key`
header (never generates it — Tower owns the key).

Architecture and design decisions live in the pre-migration repo's `PLAN.md`
(see [History](#history)).
The **[Design](#design)** section explains why the plugin is shaped this way; the
[History](#history) section records where it came from.

## Design

The rest of this README is the *what*; this section is the *why*. These decisions
shape everything above.

**A stateless controller, not a second client.** The plugin holds no Tower state
and edits no files. It is a remote: it reads the overview Tower already computes
and POSTs verbs Tower already routes. Everything authoritative lives elsewhere.
Tower owns workspace, builder, and gate state plus the auth key; VSCode owns the
working tree, the review queue, and the composer. The deck only projects that
state onto keys and dials and fires intents back. The payoff is that the device
is disposable: unplug it, restart it, or run two of them, and nothing is lost or
forked, because there was never a second copy of the truth to reconcile. It also
means every surface (deck, VSCode sidebar, web dashboard) reflects one state, so a
change made on any of them shows up on all of them.

**Canonical verbs over a command relay, not direct editor control.** The plugin
never speaks to VSCode directly. It POSTs a small, fixed vocabulary of canonical
verbs to Tower's command relay (`/api/command`), and the focused VSCode window
maps each verb to a `codev.*` command. The decoupling is deliberate: the deck does
not need to know how many editors are open, which one is focused, or how a command
is implemented. Tower routes the verb to the focused window, and each verb is
stamped with the active workspace, so one Tower serving several workspaces sends
each command to the right place. New editor behavior is a new `codev.*` handler,
and the deck's verb set barely moves.

**One shared selection, because the board is a single instrument.** Row 1 selects a
builder, Row 2 acts on it, and the dials review it, with all three pointed at the
same builder. That coherence is the design, not a coincidence: a Row 1 press is
select-and-open in one gesture, and focusing a builder's diff or canvas in VSCode
moves the deck's selection back to it (via the `builder-active` activity hook).
Without one binding selection the three zones would drift and every press would
carry a "which builder?" ambiguity. With it, the board reads as one instrument
aimed at one target.

**Keys commit, dials review, mapped to the hardware.** The interaction model splits
along the SD+'s two physical controls. A key is a discrete, labelled surface, so
keys carry commits: select, approve, run, flush. An encoder is a continuous cursor,
so dials carry review, the one task that is inherently "scan an ordered list and
pick a target." Reviewing a diff walks files, then hunks, then changes; reviewing a
spec walks headings, then blocks. That is a rotate-to-cursor, push-to-act motion,
exactly what a dial is for and what a grid of keys is not. So the dials are
phase-aware review cursors and the keys are the commit buttons: "dials collect,
keys commit."

**The canvas owns composer state; the deck stays mode-neutral.** A dial press does
not decide what happens to the feedback it submits. It relays a mode-neutral verb
(`feedback-file`, `feedback-hunk`, `feedback-selection`); VSCode forwards it to the
builder now or queues it, per the `codev.diffCodelensMode` workspace setting, and
**Send Feedback** flushes a queue. All of that (the queue, the delivery mode, the
composed text) lives in VSCode, the surface that actually renders the artifact. The
deck renders no artifact content and holds no composer state, so it cannot fall out
of sync with what you are reading; the touch strip only names the live mode so a
press is never a surprise. Keeping composer state where the canvas is, and off the
device, is what lets the deck stay a stateless remote.

**Gate approval is never silent, by design.** The deck can surface a gate's approval
modal in VSCode, but it can never approve on the device. Approving a spec, plan, or
PR is a human decision with consequences, so it always lands in front of you in the
editor, where the artifact is, rather than behind a one-touch key on a desk
peripheral. Silent one-touch approval is deliberately out of scope for that reason,
not for want of a spare key.

## Hardware

Expand Down Expand Up @@ -260,17 +323,18 @@ Functional — build/type/unit-verified and validated end-to-end on physical
hardware (Stream Deck +, live Tower; versioned in lockstep with the codev
workspace since 3.3.0). The dial touch strips render title + a live value via
`setFeedback`; a richer SVG/icon render layer (badges, colour by state) is still
out of scope. Also deliberately out of scope (see the pre-migration `PLAN.md`):
silent one-touch gate approval. (Editor scrolling, originally out of scope there,
was since implemented as the Scroll dial.) The `/api/command` route inherits
out of scope. Silent one-touch gate approval is also deliberately out of scope
(see [Design](#design) for the reason). (Editor scrolling, once out of scope, was
since implemented as the Scroll dial.) The `/api/command` route inherits
Tower's current auth posture; a Tower-auth follow-up is tracked separately.

## History

This plugin was imported into the monorepo (issue #1347) from
[`cluesmith/codev-integrations`](https://github.com/cluesmith/codev-integrations)
at commit `77be3d0` (`packages/streamdeck`), as part of the #1189 SDK
consolidation: its `@cluesmith/codev-client` dependency was absorbed into
`@cluesmith/codev-sdk`, and the plugin became the sdk's first
outside-the-original-trio consumer. Pre-migration history (including the
original `PLAN.md` design document) lives in that repo.
This plugin was imported into the monorepo under issue #1347 from the
pre-migration repository at commit `77be3d0` (`packages/streamdeck`), as part of
the #1189 SDK consolidation: its `@cluesmith/codev-client` dependency was absorbed
into `@cluesmith/codev-sdk`, and the plugin became the sdk's first
outside-the-original-trio consumer. The design rationale that once lived only in
that repository's planning document now lives in-tree, under [Design](#design)
(issue #1390); the pre-migration repository, which holds the earlier development
history, is slated for retirement after the sdk's first npm publish.
34 changes: 34 additions & 0 deletions apps/streamdeck/src/__tests__/readme-design.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

/**
* The plugin's design rationale must live in-tree, not depend on the
* pre-migration repository's planning document (issue #1390). That repository is
* slated for retirement after the sdk's first npm publish, so a README that
* defers "why the plugin is shaped this way" to a `PLAN.md` over there is a live
* documentation dependency on a soon-archived source. These pins fail loudly if
* the Design section is dropped or the external design-doc pointer creeps back.
*/
describe('streamdeck README carries its own design rationale', () => {
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const readme = readFileSync(join(root, 'README.md'), 'utf-8');

it('has an in-tree Design section', () => {
expect(readme).toMatch(/^## Design$/m);
});

it('does not defer design to the pre-migration planning document', () => {
expect(readme).not.toContain('PLAN.md');
});

it('does not name the pre-migration repository in committed prose', () => {
expect(readme).not.toContain('codev-integrations');
});

it('keeps provenance in the History section', () => {
expect(readme).toMatch(/^## History$/m);
expect(readme).toContain('pre-migration repository');
});
});
22 changes: 22 additions & 0 deletions codev/projects/1390-fold-the-stream-deck-plugin-s-/status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
id: '1390'
title: fold-the-stream-deck-plugin-s-
protocol: air
phase: pr
plan_phases: []
current_plan_phase: null
gates:
pr:
status: approved
requested_at: '2026-08-14T00:52:20.798Z'
approved_at: '2026-08-15T02:16:58.623Z'
iteration: 1
build_complete: false
history: []
started_at: '2026-08-14T00:27:47.743Z'
updated_at: '2026-08-15T02:16:58.623Z'
pr_history:
- phase: pr
pr_number: 1452
branch: builder/air-1390
created_at: '2026-08-14T00:52:10.780Z'
pr_ready_for_human: false
41 changes: 41 additions & 0 deletions codev/state/air-1390_thread.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# air-1390 — Fold the Stream Deck plugin's design rationale in-tree

**Protocol:** AIR (strict). **Issue:** #1390. **Scope:** docs + a doc-invariant test.

## What the issue asked

`apps/streamdeck/README.md` deferred "architecture and design decisions" to the
pre-migration repository's `PLAN.md`, a live dependency on a repository whose
retirement is a tracked follow-up (gated on the sdk's first npm publish). Move the
still-relevant rationale in-tree; keep provenance in the History section only.

## Decisions

- **Home = README Design section** (architect's steer; issue left README-vs-arch.md
to me). Co-located with the code, free of the COLD arch.md hot/cold tier discipline.
- **Reconstructed the rationale from what's in-tree** (the README, `store.ts` /
`plugin.ts`, `codev/plans/1347-*`, `codev/reviews/1347-*`, arch.md's Spec 1410
coherence section), not the external `PLAN.md` — did not assume I could read the
pre-migration repo, and the architect confirmed I likely can't. Wrote the *why*
behind the system as it actually ships: stateless controller, canonical-verb relay,
one shared selection, keys-commit/dials-review hardware mapping, canvas-owns-composer
state, and why silent gate approval is out of scope.
- **Naming:** scrubbed the external repo name/URL (`cluesmith/codev-integrations`,
github.com link) and every `PLAN.md` mention from committed prose, per the standing
"describe in codev's own terms" rule and the architect's naming instruction.
Provenance survives in History described as "the pre-migration repository" (commit
`77be3d0`, `packages/streamdeck`, #1347/#1189 kept as codev's own reference terms).

## Changes

- `apps/streamdeck/README.md`: new `## Design` section (the *why*); intro pointer now
points at Design + History; Status/roadmap and History rewritten to drop the three
`PLAN.md` references and the external repo name.
- `apps/streamdeck/src/__tests__/readme-design.test.ts` (new): pins the Design section,
no `PLAN.md` deferral, no external repo name, provenance retained in History. Mirrors
the existing `vendored-ui-lib.test.ts` doc-invariant pattern.

## Verification

`pnpm --filter @cluesmith/codev-sdk build && ...streamdeck check-types && ...test`:
check-types clean; 166/166 tests pass across 9 files (4 new).
Loading