From c5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sun, 21 Jun 2026 19:20:10 +0900 Subject: feat(cli): list, read, send commands (Wave 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI gains three new sub-commands: - dispatch list [--server] — list conversations (short ID + title + activity) - dispatch read [--server] — block until turn settles, print last AI message - dispatch send --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. --- packages/cli/src/render.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) (limited to 'packages/cli/src/render.ts') 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"); +} -- cgit v1.2.3