mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat: add the TUI session model, classification and cursor
Rows are the ones GET /api/sessions/unified already returns and blocked states are the items the approvals inbox already parsed, both imported as types only so a CLI process pulls in neither the server nor node-pty. Classification speaks the web UI's language (red blocked, yellow waiting, green working) so a user with both surfaces open never has to translate between them. Groups order by how long a session has been in its state, which is why WORKING anchors on the pane's last Enter: a working pane repaints about once a second, so its last-activity stamp always says "now". Selection is tracked by session id, never by row index: rows re-sort under the cursor whenever a session starts working or an approval lands, and an index-tracked cursor would quietly move the selection to another session between two keystrokes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @fileoverview Shared types for the `codeman tui` pure core.
|
||||
*
|
||||
* The TUI is a client of the server, never a second brain: its rows are the
|
||||
* rows `GET /api/sessions/unified` already returns (`UnifiedSessionItem`) and
|
||||
* its blocked states are the items `GET /api/approvals` already parsed
|
||||
* (`ApprovalItem`). Both are imported as TYPES only, so nothing here pulls the
|
||||
* server, node-pty or the utils barrel into a CLI process.
|
||||
*
|
||||
* Everything in `src/tui/*` except `tui-app.ts` / `tui-client.ts` is pure:
|
||||
* deterministic outputs from inputs, no `process.*`, no timers, no IO.
|
||||
*
|
||||
* @module tui/tui-types
|
||||
*/
|
||||
|
||||
import type { UnifiedSessionItem } from '../services/unified-session-service.js';
|
||||
import type { ApprovalItem } from '../web/approval-inbox.js';
|
||||
|
||||
/**
|
||||
* A unified-list row plus the few live-only extras the dashboard shows.
|
||||
*
|
||||
* The unified list is the spine (it is the only source that carries history
|
||||
* rows), but it has no token counters and no turn-start stamp, so the client
|
||||
* merges those from the live session payload (`GET /api/sessions` /
|
||||
* `session_updated` SSE) when a row is live. History rows simply lack them.
|
||||
*/
|
||||
export interface TuiSessionRow extends UnifiedSessionItem {
|
||||
/**
|
||||
* Wall-clock ms of the pane's last Enter (`SessionState.lastSubmitAt`). The
|
||||
* only usable "working since" anchor: a working pane repaints about once a
|
||||
* second, so its `lastActivityAt` is always "now".
|
||||
*/
|
||||
lastSubmitAt?: number;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Row state, in the web UI's vocabulary so both surfaces read the same.
|
||||
*
|
||||
* There is deliberately no `error` member: an errored session is something a
|
||||
* human has to look at, so it classifies as `waiting` and lands in NEEDS YOU
|
||||
* rather than growing a fifth color nobody designed.
|
||||
*/
|
||||
export type TuiSessionState = 'blocked-question' | 'blocked-permission' | 'waiting' | 'working' | 'idle' | 'recent';
|
||||
|
||||
/** The four display groups, in display order. */
|
||||
export type TuiGroupKey = 'needs-you' | 'working' | 'idle' | 'recent';
|
||||
|
||||
/** A classified session: what the cursor moves over and the renderer paints. */
|
||||
export interface TuiRow {
|
||||
session: TuiSessionRow;
|
||||
state: TuiSessionState;
|
||||
group: TuiGroupKey;
|
||||
/** The pending prompt that blocks this session, when it has one. */
|
||||
approval?: ApprovalItem;
|
||||
/** Epoch ms the session entered `state`; the intra-group sort key. 0 when unknown. */
|
||||
since: number;
|
||||
}
|
||||
|
||||
export interface TuiGroup {
|
||||
key: TuiGroupKey;
|
||||
label: string;
|
||||
rows: TuiRow[];
|
||||
}
|
||||
|
||||
/** How the client currently sees the server. */
|
||||
export type TuiConnectionStatus = 'connected' | 'reconnecting' | 'degraded' | 'down';
|
||||
|
||||
/** Which overlay (if any) owns the keyboard. */
|
||||
export type TuiUiMode = 'list' | 'help' | 'confirm-kill' | 'prompt' | 'search' | 'message';
|
||||
|
||||
/**
|
||||
* Glyph capability tier. Detection is env-driven and therefore lives in a tiny
|
||||
* function the app layer calls (`detectGlyphTier`); the renderer only ever
|
||||
* takes the resolved tier as an input.
|
||||
*/
|
||||
export type TuiGlyphTier = 'nerd' | 'unicode' | 'ascii';
|
||||
|
||||
/** Header facts, all optional: the header degrades to just the product name. */
|
||||
export interface TuiHeaderInfo {
|
||||
hostname?: string;
|
||||
instance?: string;
|
||||
version?: string;
|
||||
/** Plan-usage chip text, e.g. `5h 32% · wk 61%`. */
|
||||
planUsage?: string;
|
||||
}
|
||||
|
||||
/** The selected session's terminal tail, already run through `toDisplayLines()`. */
|
||||
export interface TuiPreview {
|
||||
sessionId: string;
|
||||
/** Display lines, oldest first. */
|
||||
lines: string[];
|
||||
/** Set instead of lines when the tail could not be fetched. */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface TuiMessage {
|
||||
text: string;
|
||||
tone: 'info' | 'warn' | 'err';
|
||||
}
|
||||
|
||||
/** Typed-confirmation state for `x` (kill): the user retypes the session name. */
|
||||
export interface TuiConfirmState {
|
||||
sessionId: string;
|
||||
name: string;
|
||||
typed: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* What `renderFrame()` reads. The store implements it; a test can hand-build
|
||||
* one, which is what keeps the renderer testable without the model.
|
||||
*/
|
||||
export interface TuiRenderModel {
|
||||
groups(): TuiGroup[];
|
||||
readonly selectedId: string | null;
|
||||
readonly connection: TuiConnectionStatus;
|
||||
readonly mode: TuiUiMode;
|
||||
readonly header: TuiHeaderInfo;
|
||||
readonly preview: TuiPreview | null;
|
||||
readonly message: TuiMessage | null;
|
||||
readonly confirm: TuiConfirmState | null;
|
||||
/** Live sessions only (RECENT rows are history, not sessions you have open). */
|
||||
readonly sessionCount: number;
|
||||
}
|
||||
Reference in New Issue
Block a user