Skip to content

genie-ui — dash-pattern PTY fleet + wish menu + hired agents + group chat (Model B) - #2609

Merged
namastex888 merged 22 commits into
devfrom
wish/genie-ui
Jul 21, 2026
Merged

namastex888 merged 22 commits into
devfrom
wish/genie-ui

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

The genie UI, council-ratified Model B: the wish is the workspace, each wish hires a crew of agents shown as their real terminals (dash pattern — render THE agent, no abstraction), with a wish-scoped group chat where read-only ACP chat faces collaborate. Full design + evidence: .genie/brainstorms/genie-ui/DESIGN.md (digest-stamped SHIP), .genie/wishes/genie-ui/WISH.md. New package: packages/genie-ui.

Groups (all SHIP — acceptance + quality + Fable final gate)

  • G1 shell floor — node-runtime PTY server (bun builds, node runs; bun's node-pty onData is dead — 40454-vs-0 isolated, regression-guarded), vanilla xterm client (tabs/splits/maximize), salvaged dash TerminalMirror replay, loopback+Origin-guarded ws, gates extended to cover packages/**.
  • G2 genie lane — wishes left-menu + hire roster + worktreeFor (reuses genie launch group worktrees, never mints); runtime-adaptive read-only sqlite (node:sqlite server / bun:sqlite tests), proven on 25 real wishes.
  • G3 chat + ACP backend — separable chat-backend (one lazy read-only ClientSideConnection per hired agent, @-mention-only, named fail-loud events) behind the isolation wall; capability-table badges (shared-memory = Hermes only). R1 ssh-ACP proven live (hermes acp → "PONG", rlmx acp transport over ssh localhost); Hermes two-faces state.db bridging demonstrated.
  • G4 — residue confirmed clean, two-faces contract doc (in .docs-vendor submodule).

Verification

  • 54 package tests; bun run check at the pre-existing 4-failure agent-sync baseline (zero genie-ui failures).
  • Final gate booted the composed server under node and drove PTY panes + lane + chat concurrently over one ws; all four isolation walls verified greppable; Origin guard held live (401 cross-origin); no chat-driven command injection.

Merge notes

  • Do not include a .docs-vendor pointer bump — the docs branch docs/genie-ui-two-faces (submodule commit b8942eb) must be pushed + PR'd to automagik-dev/docs first; recursive-submodule CI would break on an unmerged SHA. Bump the pointer after that merges.
  • Follow-ups (MINOR, non-blocking): AC2 "PTY spawns on tab-open" ships as eager startAll() (record or refine); conductor auto-advance is the next wish (register the blocks edge then).
  • Manual QA (no browser on the build box): browser split/maximize, reload-replay, per-pane badges, @hermes chat round-trip, non-mutation spot check — full checklist in the final-gate evidence.

🤖 Generated with Claude Code

felipehowit and others added 20 commits July 21, 2026 04:05
… G1-G4)

Author the executor-facing DESIGN for the genie-ui wish from the ratified
brainstorm (DRAFT/RESEARCH/COUNCIL): two channels (PTY viewing + ACP
control), Model-B-corrected contract verbatim, module/interface/isolation
architecture, the 8 council acceptance criteria verbatim, G1-G4 execution
groups, risks incl. council dissent as future work, and the OUT list
(conductor auto-pilot, write-promotion toggle, broadcast routing, Electron).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gate/build/residue tightenings)

Fix the design-review gaps without altering the ratified verbatim texts
(council contract, AC1-8):

- MAJOR: pin the exact wish<->worktree mapping. The coherence contract binds
  per hired agent, not per wish: an agent's two faces cd into that agent's
  ready-group worktree (reused from `genie launch`, one per group), never a
  parallel wish-level worktree. Wish-level coherence rides on the shared
  `.genie` git artifacts (git-tracked wishes + worktree-shared genie.db), not
  a single path. Reconciles the "compose, don't duplicate" promise. Updated
  worktree wall, knot gloss, Scope IN, chat prose, `worktreeFor(rosterEntry)`
  signature, and G2 validation.
- MINOR: G1 must wire `packages/**` into the gates (tsconfig include,
  complexity-budget scope, biome complexity override) or typecheck + the
  budget silently skip the package.
- MINOR: "zero build step" scoped to the server; G1 replaces the prototype's
  Vite client transpile with a bun-native serve path (Bun.serve + import map).
- MINOR: soften the rlmx-acp "all four exist today" overclaim (in flight).
- MINOR: reframe G4/AC8 to verify no LIVE refs remain (already true) and
  preserve historical CHANGELOG/wish records rather than scrub them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Curated from .genie/brainstorms/genie-ui/DESIGN.md (SHIP, digest-verified):
the fleet floor (PTY viewing) + genie lane + wish group chat (ACP control).
8 ratified ACs, D1-D13, R1-R11, four sequential waves G1->G2->G3->G4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make CLAUDE.md's "src/** AND packages/**" complexity claim true and stop
typecheck + the complexity budget silently skipping the new package before
any feature code lands.

- tsconfig: add packages/genie-ui project reference; the package has its own
  tsconfig (DOM lib for the client) so the typecheck script runs both.
- biome: add a packages/** override (noExcessiveCognitiveComplexity warn @ 25,
  matching src/**).
- complexity-budget: extend the suppression grep scope from src to src+packages
  (biome check . already reports packages/** complexity warnings).
- knip: add the package entries + project glob so dead-code resolves the new
  runtime imports instead of flagging them unused.
- deps: node-pty, ws, @xterm/{xterm,addon-fit,addon-clipboard,headless,addon-serialize}
  land in root package.json (no workspaces field in the repo); node-pty added to
  trustedDependencies so a fresh install builds its native binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the A/B "fresh" substrate into packages/genie-ui/server as TypeScript:

- fleet-config: loadFleet() -> PaneSpec[], grown with the genie seams wishId + role.
- pty-session: PtySession + PtySessionManager (startAll/spawn/kill/restart/write/
  resize/replay/list/killAll; events data/exit/status = idle/running/exited). THE
  single node-pty importer.
- transport: the ws protocol codec (FLEET/DATA/EXIT/STATUS/REPLAY/INPUT/RESIZE/
  SPAWN/KILL/RESTART/LIST) as a typed pure codec.
- reused/TerminalMirror.ts: salvaged verbatim from dash (syv-ai/dash, MIT) as the
  replay path, replacing fresh's 256 KB raw-byte ring. Verified under bun: the
  createRequire loader for @xterm/headless + @xterm/addon-serialize round-trips
  serialize() with SGR styling (design R4) — no loader change, no ring fallback.
  Imported only by pty-session.
- index: thin composition root + the bun-native serve path (no Vite) — one node
  http server serves the client (Bun.build of the single entry) and upgrades to
  the bare ws PTY protocol on the same port.

Colocated bun:test: pty-session (spawn/kill/restart + status/exit events) and the
TerminalMirror serialize/replay round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The browser client, served by the bun-native path (no Vite):

- pane: one @xterm/xterm 5.5 Terminal + FitAddon per pane, ClipboardAddon wired
  through the salvaged Utf8Base64 (UTF-8 OSC-52), and the salvaged FitScheduler
  debouncing refits via a ResizeObserver. Each pane is a split-cell with a header
  (role badge, status dot, per-pane spawn/kill/restart/split/max/close).
- layout: vanilla reimplementation of Lane A's grid concept — tabs (swap the
  visible agent), horizontal splits (2+ side-by-side cells), maximize. Panes stay
  mounted; the layout only toggles visibility so scrollback survives switching.
- transport: typed ws client, same-origin (server serves assets + ws on one port),
  reconnecting.
- main: roster-driven composition; talks to the server exclusively over transport.
- reused/{Utf8Base64,FitScheduler}.ts: salvaged verbatim from dash (MIT).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README: module map + provenance, the pinned no-Vite serve decision, the
  TerminalMirror-under-bun (R4) outcome, and the no-workspaces deps call.
- qa.md: the AC2-substrate manual QA — local pane render -> TerminalMirror ->
  reattach-identical replay (proven), ssh localhost key auth (proven), the
  bun-native serve path + ws FLEET handshake (proven), and the lifecycle events.
  Records the nested-sandbox PTY-SIGHUP limitation that blocks live btop/ssh -t
  frames + browser splits here, flagged operator-confirm on the real box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MSG.INPUT feeds arbitrary keystrokes into live login shells, so the ws
upgrade is a remote-control surface. Close it by default:

- server.listen now binds HOST (default 127.0.0.1); LAN reach (mac +
  phone) is opt-in via HOST=0.0.0.0, not the implicit all-interfaces bind.
- WebSocketServer gains verifyClient: a browser Origin must be same-origin
  with the page host (any LAN hostname, no config) or on the explicit
  GENIE_UI_ALLOWED_ORIGINS allowlist. Browsers always send Origin, so this
  defeats a cross-origin drive-by into the shells; no-Origin CLI/test
  clients stay gated by the loopback bind.

Records the trust boundary as an explicit decision in the package README
(the seam G2/G3 build on) and adds colocated verifyClient regression tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refreshTab and renderMeta interpolated PaneInfo fields (name, role,
command, args, wishId) straight into innerHTML. Config-trusted today, but
the SEAM comment marks this tab strip as where G2 feeds genie state (wish
slugs, markdown-derived roles) — a wish title must not inject markup. Add
an esc() helper and wrap every string interpolation before G2 lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uctor substrate)

The wish group chat's ACP control channel: one lazily-spawned, read-only ACP
ClientSideConnection per hired agent, @-mention-only routing (message + room
transcript -> streamed reply), a checked-in capability table -> minimal badges,
and named fail-loud spawn/delivery events.

- chat-backend.ts: the pool + router. Lazy spawn on first @-mention, seeded with
  wish context + room transcript; non-mutating chat face (fs write caps off +
  cancels every permission request); per-agent prompt serialization (sidesteps
  rlmx's single-session -32600). The load-bearing wall (AC6/D7): imports NOTHING
  from pty-session/TerminalMirror/transport/client — greppable test enforces it.
  The vendor SDK is reached via acp.ts so the wall grep stays honest.
- capability-table.ts: checked-in per-harness data -> "shared memory" badge for
  Hermes only, where demonstrated. v1 rides session/prompt + session/update only.
- chat-room.ts: composition-root glue binding chat-backend <-> transport (per-wish
  transcript + roster) so neither imports the other; wired into index.ts beside
  the PTY manager. fleet-config grows the `harness` seam.
- client chat-drawer.ts: wish-scoped roster + badges, streamed replies, named
  fail-loud lines ("@codex could not start: ...; check PATH"), never silence.
- @agentclientprotocol/sdk@0.26.0 (matches rlmx's pin).

R1 proven live over `ssh localhost ... acp`: hermes (clean streamed PONG) and rlmx
(handshake + streamed reply). AC4b: Hermes session bridging demonstrated via
~/.hermes/state.db (the read-only chat face's turn persisted to the same db the
terminal face reads). v1 default is the co-located fallback; launcher is injectable.

Colocated bun:test: 14 chat-backend + 4 chat-room, all green. bun run check green
with no new failures vs the pre-existing 4-failure agent-sync baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ents

The codex ACP default launched `codex acp`, but the installed codex has no
`acp` subcommand — the Zed adapter is the `codex-acp` binary (DESIGN D9 names
`codex-acp exited …`; mirrors the claude `claude-code-acp` shape). @codex can
now connect when codex-acp is on PATH; a missing binary still fails loud.

failFace lacked a face.failed guard, so one child failure fired spawn-failed
twice (child 'error' → onChildDown → failFace, then the handshake catch →
failFace). Add the same idempotency guard onChildDown uses so exactly one named
event surfaces per failure. First-failure-is-loud behavior unchanged.

Colocated tests assert exactly one spawn-failed per failure and that the codex
default resolves to codex-acp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4f1884e6-69e3-48b6-a319-42c0f7871cb8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wish/genie-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces packages/genie-ui, a browser-served, PTY-faithful terminal UI that integrates a fleet of coding agents with genie's wish and board state, alongside a wish-scoped inter-agent group chat. The implementation features a Node-based PTY server, a vanilla TypeScript client, a read-only database layer for wish contexts, and an isolated ACP-based chat backend. The review feedback highlights several critical robustness improvements: preventing cross-talk of active chat streams across different wishes in the UI, allowing failed agent faces to self-heal and recreate on subsequent mentions, wrapping PTY spawning in a try-catch block to prevent server crashes from missing directories, and defensively guarding ResizeObserver entries against empty lists.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +68 to +82
addLine(line: ChatLine): void {
const kind = line.from === 'human' ? 'human' : 'agent';
this.commit(line.wish, { from: line.from, text: line.text, kind });
if (kind === 'agent') this.live.delete(line.from); // committed → drop its live bubble
if (line.wish === this.wish) this.render();
}

/** A streamed chat event: a chunk to a live bubble, or a NAMED fail-loud system line (D9). */
applyEvent(wish: string, event: ChatEventWire): void {
if (event.kind === 'message-chunk') this.live.set(event.agentId, (this.live.get(event.agentId) ?? '') + event.text);
else if (event.kind === 'spawn-failed' || event.kind === 'delivery-failed')
this.commit(wish, { from: event.agentId, text: event.message, kind: 'error' });
// reply-done needs no action here — the committed line arrives as a chat-message.
if (wish === this.wish) this.render();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential cross-talk/leakage bug where active streams in this.live are shared across all wishes. Since this.live is a single map, applyEvent and addLine modify it without checking if the event or line belongs to the currently active wish. This can cause in-flight streams from other wishes to render in the active wish's chat log. Guarding these updates with wish === this.wish prevents this cross-talk.

Suggested change
addLine(line: ChatLine): void {
const kind = line.from === 'human' ? 'human' : 'agent';
this.commit(line.wish, { from: line.from, text: line.text, kind });
if (kind === 'agent') this.live.delete(line.from); // committed → drop its live bubble
if (line.wish === this.wish) this.render();
}
/** A streamed chat event: a chunk to a live bubble, or a NAMED fail-loud system line (D9). */
applyEvent(wish: string, event: ChatEventWire): void {
if (event.kind === 'message-chunk') this.live.set(event.agentId, (this.live.get(event.agentId) ?? '') + event.text);
else if (event.kind === 'spawn-failed' || event.kind === 'delivery-failed')
this.commit(wish, { from: event.agentId, text: event.message, kind: 'error' });
// reply-done needs no action here — the committed line arrives as a chat-message.
if (wish === this.wish) this.render();
}
addLine(line: ChatLine): void {
const kind = line.from === 'human' ? 'human' : 'agent';
this.commit(line.wish, { from: line.from, text: line.text, kind });
if (kind === 'agent' && line.wish === this.wish) this.live.delete(line.from); // committed → drop its live bubble
if (line.wish === this.wish) this.render();
}
/** A streamed chat event: a chunk to a live bubble, or a NAMED fail-loud system line (D9). */
applyEvent(wish: string, event: ChatEventWire): void {
if (event.kind === 'message-chunk' && wish === this.wish) this.live.set(event.agentId, (this.live.get(event.agentId) ?? '') + event.text);
else if (event.kind === 'spawn-failed' || event.kind === 'delivery-failed')
this.commit(wish, { from: event.agentId, text: event.message, kind: 'error' });
// reply-done needs no action here — the committed line arrives as a chat-message.
if (wish === this.wish) this.render();
}

Comment on lines +332 to +353
/** A child that dies before it was ready is a spawn failure; after ready it is a delivery failure. */
private onChildDown(face: AgentFace, spawnMessage: string): void {
if (face.failed) return;
if (face.conn) {
face.failed = true;
this.dispatch({
type: 'delivery-failed',
agentId: face.reg.agentId,
message: `@${face.reg.agentId} disconnected`,
});
} else {
this.failFace(face, spawnMessage);
}
}

private failFace(face: AgentFace, message: string): void {
if (face.failed) return; // idempotency: one child-down + one handshake-catch must emit ONE spawn-failed
face.failed = true;
face.child?.kill('SIGKILL');
this.dispatch({ type: 'spawn-failed', agentId: face.reg.agentId, harness: face.reg.harness, message });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Once face.failed is set to true, any future @mention to that agent will silently do nothing because deliverMessage returns early without attempting to recreate the face. Deleting the face from this.faces on failure allows a fresh process to be spawned on the next mention, enabling self-healing and recovery from transient crashes or disconnections.

  private onChildDown(face: AgentFace, spawnMessage: string): void {
    if (face.failed) return;
    if (face.conn) {
      face.failed = true;
      this.faces.delete(face.reg.agentId); // Allow recreation on next message
      this.dispatch({
        type: 'delivery-failed',
        agentId: face.reg.agentId,
        message: '@' + face.reg.agentId + ' disconnected',
      });
    } else {
      this.failFace(face, spawnMessage);
    }
  }

  private failFace(face: AgentFace, message: string): void {
    if (face.failed) return; // idempotency: one child-down + one handshake-catch must emit ONE spawn-failed
    face.failed = true;
    face.child?.kill('SIGKILL');
    this.faces.delete(face.reg.agentId); // Allow recreation on next message
    this.dispatch({ type: 'spawn-failed', agentId: face.reg.agentId, harness: face.reg.harness, message });
  }

Comment on lines +37 to +71

start(): void {
if (this.proc) return; // already running
// Fresh screen on (re)start: drop the previous mirror so replay never shows
// pre-restart content (the sync equivalent of fresh's `buffer = ''`).
this.mirror.dispose();
this.mirror = new TerminalMirror(this.cols, this.rows);
const env = { ...process.env, ...this.spec.env } as Record<string, string>;
this.proc = pty.spawn(this.spec.command, this.spec.args, {
name: env.TERM || 'xterm-256color',
cols: this.cols,
rows: this.rows,
cwd: this.spec.cwd,
env,
});
this.status = 'running';
this.lastExitCode = null;

this.proc.onData((data) => {
this.mirror.write(data);
this.emit('data', data);
});

this.proc.onExit(({ exitCode }) => {
this.status = 'exited';
this.lastExitCode = exitCode;
this.proc = null;
// Emit `status:exited` BEFORE `exit`: restart() listens on `exit` and calls
// start() synchronously, which emits `status:running`. Emitting status first
// keeps the observable sequence honest (exited -> running), never reversed.
this.emit('status', this.status);
this.emit('exit', exitCode);
});

this.emit('status', this.status);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

pty.spawn throws synchronously if the cwd directory does not exist or is inaccessible (e.g., if a worktree was deleted). Wrapping it in a try-catch block prevents the entire server from crashing and allows the error to be written directly to the terminal mirror so the user is informed of the failure.

  start(): void {
    if (this.proc) return; // already running
    // Fresh screen on (re)start: drop the previous mirror so replay never shows
    // pre-restart content (the sync equivalent of fresh's 'buffer = ""').
    this.mirror.dispose();
    this.mirror = new TerminalMirror(this.cols, this.rows);
    const env = { ...process.env, ...this.spec.env } as Record<string, string>;
    try {
      this.proc = pty.spawn(this.spec.command, this.spec.args, {
        name: env.TERM || 'xterm-256color',
        cols: this.cols,
        rows: this.rows,
        cwd: this.spec.cwd,
        env,
      });
      this.status = 'running';
      this.lastExitCode = null;

      this.proc.onData((data) => {
        this.mirror.write(data);
        this.emit('data', data);
      });

      this.proc.onExit(({ exitCode }) => {
        this.status = 'exited';
        this.lastExitCode = exitCode;
        this.proc = null;
        // Emit `status:exited` BEFORE `exit`: restart() listens on `exit` and calls
        // start() synchronously, which emits `status:running`. Emitting status first
        // keeps the observable sequence honest (exited -> running), never reversed.
        this.emit('status', this.status);
        this.emit('exit', exitCode);
      });

      this.emit('status', this.status);
    } catch (err) {
      this.status = 'exited';
      this.lastExitCode = -1;
      this.proc = null;
      this.mirror.write('Error spawning PTY: ' + (err as Error).message + '\r\n');
      this.emit('status', this.status);
      this.emit('exit', -1);
    }
  }

Comment on lines +66 to +68
new ResizeObserver((entries) => {
this.scheduler.onResize(entries[0].contentRect.height);
}).observe(this.mount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In some environments or browsers, ResizeObserver can occasionally fire with an empty entries list. Adding a defensive guard for entries[0] prevents potential runtime errors.

Suggested change
new ResizeObserver((entries) => {
this.scheduler.onResize(entries[0].contentRect.height);
}).observe(this.mount);
new ResizeObserver((entries) => {
if (entries[0]) this.scheduler.onResize(entries[0].contentRect.height);
}).observe(this.mount);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 332f1e51ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/** Extract @-mentioned ids from a message (deduped, order-preserving). @-mention-only (D4). */
export function parseMentions(text: string): string[] {
const seen = new Set<string>();
const re = /(?:^|[^\w@])@([a-zA-Z0-9][\w.-]*)/g;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trim trailing punctuation from mention IDs

When a user writes a normal sentence like please ask @codex., this regex captures codex. as the agent id. ChatRoom then looks up that exact id, treats it as an unknown mention, and silently skips delivery, which is especially confusing because the @-mention-only path intentionally does not surface unknown mentions as errors.

Useful? React with 👍 / 👎.

const face = this.faces.get(agentId) ?? this.createFace(reg);
face.queue = face.queue.then(async () => {
await face.ready;
if (face.failed || !face.conn || !face.sessionId) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not swallow retries after a chat face fails

After a missing adapter, unlaunched worktree, or later disconnect marks face.failed, any subsequent @mention to that rostered agent reaches this return without emitting another spawn-failed/delivery-failed event or attempting a respawn. The human line is still recorded, so retries after fixing PATH or restarting the adapter appear as silence in the drawer.

Useful? React with 👍 / 👎.

if (visible && firstVisible === null) firstVisible = id;
}
const activeInfo = activeId ? infos.get(activeId) : undefined;
if (firstVisible && (!activeInfo || !paneInWish(activeInfo))) layout.solo(firstVisible);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide the terminal when a selected wish has no panes

When the operator selects a wish that has no matching PaneInfo.wishId entries, firstVisible remains null, all tabs are hidden, but the LayoutManager keeps rendering the previously active terminal. Since the drawer and context now point at the newly selected wish, this can leave the user typing into an unrelated agent shell under the wrong wish context; clear the active layout or render an empty state in this branch.

Useful? React with 👍 / 👎.

@namastex888
namastex888 merged commit a3211fa into dev Jul 21, 2026
16 of 17 checks passed
lirazsiri pushed a commit to lirazsiri/genie that referenced this pull request Jul 28, 2026
packages/genie-ui was the original fresh-substrate UI (PR automagik-si#2609), superseded
2026-07-21 when Felipe rejected the substrate live and the UI direction moved
to genie-ui-dash in the private khal-os/genie-desktop repo. it was never
promoted to main; the docs superseded it but the code kept squatting on dev.

removes the standalone package (@automagik/genie-ui, private) and its 9
root-package deps that nothing in src/ imports (@agentclientprotocol/sdk,
node-pty-prebuilt-multiarch, 5x @xterm/*, ws, @types/ws), plus the gate
touchpoints: typecheck leg, tsconfig reference, biome packages/** override,
knip entries/project, complexity-budget scan scope.

the sanctioned genie ui-bridge command (src/term-commands/ui-bridge.ts) stays
— it is the stdio MCP channel the private UI spawns, not the shell.

gate: typecheck/lint/dead-code/complexity-budget clean; 2343 tests pass (the
single ui-bridge 'ss' failure is a pre-existing linux-only test, absent on
darwin, byte-unchanged here).
@automagik-genie
automagik-genie deleted the wish/genie-ui branch September 25, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants