summaryrefslogtreecommitdiffhomepage
path: root/packages/cli/src/render.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 19:20:10 +0900
committerAdam Malczewski <[email protected]>2026-06-21 19:20:10 +0900
commitc5e9fd6cd6565b55fab1bf2b9d8dacf8ba72a9f4 (patch)
tree809bd93eaa25646f237fb4b1ddd3719e25aaca90 /packages/cli/src/render.ts
parentea0e938eca3072649dc8707c999ec00cf87b986a (diff)
downloaddispatch-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.ts49
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");
+}