diff options
| author | Adam Malczewski <[email protected]> | 2026-06-21 19:20:10 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-21 19:20:10 +0900 |
| commit | c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4 (patch) | |
| tree | 809bd93eaa25646f237fb4b1ddd3719e25aaca90 /packages/cli/src/render.ts | |
| parent | ea0e938eca3072649dc8707c999ec00cf87b986a (diff) | |
| download | dispatch-c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4.tar.gz dispatch-c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4.zip | |
feat(cli): list, read, send commands (Wave 3)
CLI gains three new sub-commands:
- dispatch list [--server] — list conversations (short ID + title + activity)
- dispatch read <id> [--server] — block until turn settles, print last AI message
- dispatch send <id> --text [--queue] [--open] [--cwd] [--effort] [--server]
- Default: blocking (consumes NDJSON stream, prints accumulated text + conv ID)
- --queue: non-blocking (POST /conversations/:id/queue, exit immediately)
- --open: signals frontend to open the conversation tab (POST /conversations/:id/open)
Short-ID resolution: 4+ char prefix → GET /conversations?q= → resolve to full ID.
32+ char input is treated as a full UUID (no resolution). Errors on 0 or >1 matches.
48 new tests (108 total in cli). Pure arg parser + HTTP client functions, zero vi.mock.
Diffstat (limited to 'packages/cli/src/render.ts')
| -rw-r--r-- | packages/cli/src/render.ts | 49 |
1 files changed, 48 insertions, 1 deletions
diff --git a/packages/cli/src/render.ts b/packages/cli/src/render.ts index 1853963..9f25dd3 100644 --- a/packages/cli/src/render.ts +++ b/packages/cli/src/render.ts @@ -5,7 +5,7 @@ * Consumers write these to process.stdout / process.stderr. */ -import type { AgentEvent } from "@dispatch/transport-contract"; +import type { AgentEvent, ConversationMeta } from "@dispatch/transport-contract"; interface RenderOpts { readonly showReasoning: boolean; @@ -43,3 +43,50 @@ export function renderEvent(e: AgentEvent, opts: RenderOpts): RenderOutput | und return undefined; } } + +/** + * Accumulate ALL `text-delta` events' `delta` text into one string — the full + * assistant reply assembled from a turn's event stream. Pure: input → output, + * no I/O. Returns the empty string when the stream had no text deltas. + */ +export function extractLastText(events: readonly AgentEvent[]): string { + let text = ""; + for (const e of events) { + if (e.type === "text-delta") { + text += e.delta; + } + } + return text; +} + +/** + * Render an epoch-ms timestamp as a short relative phrase ("just now", "5m ago", + * "3h ago", "2d ago") or, past a week, the `YYYY-MM-DD` date. `now` is injected + * (clock is an outermost edge) so the function is pure and testable. + */ +export function formatRelativeTime(epochMs: number, now: number): string { + const delta = now - epochMs; + if (delta < 60_000) return "just now"; + const minutes = Math.floor(delta / 60_000); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d ago`; + return new Date(epochMs).toISOString().slice(0, 10); +} + +/** + * Format the conversation list as a table — one row per conversation: + * `shortId | title | lastActivity (relative)`. Empty input yields the empty + * string. `now` is injected for `formatRelativeTime` (pure + testable). + */ +export function formatConversationList( + conversations: readonly ConversationMeta[], + now: number, +): string { + if (conversations.length === 0) return ""; + return conversations + .map((c) => `${c.id.slice(0, 8)} | ${c.title} | ${formatRelativeTime(c.lastActivityAt, now)}`) + .join("\n"); +} |
