diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 11:27:43 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 11:27:43 +0900 |
| commit | c1bc7bfaaca7bdf4d9b2973f5dc88605217a7866 (patch) | |
| tree | 204de76d526a052712bba0a97bfe8a5d8935ea64 | |
| parent | 59c481dc96aa40b8c8f0a81bb294a77d4e5aa533 (diff) | |
| download | dispatch-c1bc7bfaaca7bdf4d9b2973f5dc88605217a7866.tar.gz dispatch-c1bc7bfaaca7bdf4d9b2973f5dc88605217a7866.zip | |
feat(cli): add --workspace filter to 'dispatch list'
The backend already supported GET /conversations?workspaceId= but the CLI
never sent it. Wire the list command to that filter:
- args.ts: parse --workspace / -w on 'list' (placed before the --catch-all
so the single-dash -w shorthand isn't taken for a positional prefix);
add workspaceId? to the list ParsedCommand.
- http.ts: add workspaceId? to FetchConversationsOpts; send ?workspaceId=
(after q/status, preserving URLSearchParams order).
- main.ts: forward parsed.workspaceId into fetchConversations; update USAGE.
Composable with --status and the <prefix> short-id arg. 'Open conversations
in workspace X' is now: dispatch list --workspace X (status defaults to
active,idle). No contract changes — purely additive CLI wiring.
Tests: +4 args (incl. composability + missing-value error), +2 http
(exact ?workspaceId= URL + combined status/workspaceId with %2C encoding).
typecheck EXIT 0, biome clean (364 files), full suite 1558 passed.
Live-verified against an isolated server.
| -rw-r--r-- | packages/cli/src/args.test.ts | 35 | ||||
| -rw-r--r-- | packages/cli/src/args.ts | 6 | ||||
| -rw-r--r-- | packages/cli/src/http.test.ts | 30 | ||||
| -rw-r--r-- | packages/cli/src/http.ts | 2 | ||||
| -rw-r--r-- | packages/cli/src/main.ts | 3 |
5 files changed, 75 insertions, 1 deletions
diff --git a/packages/cli/src/args.test.ts b/packages/cli/src/args.test.ts index 7a45c02..e613f31 100644 --- a/packages/cli/src/args.test.ts +++ b/packages/cli/src/args.test.ts @@ -254,6 +254,41 @@ describe("parseArgs", () => { }); }); + it("parses 'list' with --workspace", () => { + expect(parseArgs(["list", "--workspace", "proj"], { defaultServer })).toEqual({ + kind: "list", + server: "http://localhost:24203", + workspaceId: "proj", + all: false, + }); + }); + + it("parses 'list' with -w shorthand", () => { + const result = parseArgs(["list", "-w", "ws"], { defaultServer }); + expect(result.kind).toBe("list"); + if (result.kind === "list") expect(result.workspaceId).toBe("ws"); + }); + + it("parses 'list' with --workspace, --status, and a prefix together", () => { + const result = parseArgs(["list", "abc", "--status", "active", "--workspace", "proj"], { + defaultServer, + }); + expect(result).toEqual({ + kind: "list", + server: "http://localhost:24203", + query: "abc", + status: "active", + workspaceId: "proj", + all: false, + }); + }); + + it("errors when --workspace has no value (list)", () => { + const result = parseArgs(["list", "--workspace"], { defaultServer }); + expect(result.kind).toBe("error"); + if (result.kind === "error") expect(result.message).toContain("--workspace requires a value"); + }); + it("parses 'list' with --all", () => { expect(parseArgs(["list", "--all"], { defaultServer })).toEqual({ kind: "list", diff --git a/packages/cli/src/args.ts b/packages/cli/src/args.ts index aaad2de..74cc56a 100644 --- a/packages/cli/src/args.ts +++ b/packages/cli/src/args.ts @@ -33,6 +33,7 @@ export type ParsedCommand = readonly server: string; readonly query?: string; readonly status?: string; + readonly workspaceId?: string; readonly all: boolean; } | { readonly kind: "compact"; readonly server: string; readonly conversationId: string } @@ -85,6 +86,7 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma let server = opts.defaultServer; let query: string | undefined; let status: string | undefined; + let workspaceId: string | undefined; let all = false; for (let i = 1; i < argv.length; i++) { const arg = argv[i] as string; @@ -94,6 +96,9 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma } else if (arg === "--status") { if (i + 1 >= argv.length) return { kind: "error", message: "--status requires a value" }; status = argv[++i]; + } else if (arg === "--workspace" || arg === "-w") { + if (i + 1 >= argv.length) return { kind: "error", message: "--workspace requires a value" }; + workspaceId = argv[++i]; } else if (arg === "--all") { all = true; } else if (arg.startsWith("--")) { @@ -109,6 +114,7 @@ export function parseArgs(argv: readonly string[], opts: ParseOpts): ParsedComma server, ...(query !== undefined && { query }), ...(status !== undefined && { status }), + ...(workspaceId !== undefined && { workspaceId }), all, }; } diff --git a/packages/cli/src/http.test.ts b/packages/cli/src/http.test.ts index 2aa61e9..ab39813 100644 --- a/packages/cli/src/http.test.ts +++ b/packages/cli/src/http.test.ts @@ -289,6 +289,36 @@ describe("fetchConversations", () => { expect(calledUrl).toBe("http://localhost:24203/conversations?q=abc+def"); }); + it("appends ?workspaceId=<value> when a workspaceId is given", async () => { + let calledUrl: string | undefined; + const fakeFetch = (async (url: string | URL | Request): Promise<Response> => { + calledUrl = String(url); + return new Response(JSON.stringify({ conversations: [] }), { status: 200 }); + }) as unknown as typeof fetch; + + await fetchConversations( + { fetchImpl: fakeFetch }, + { server: "http://localhost:24203", workspaceId: "proj" }, + ); + expect(calledUrl).toBe("http://localhost:24203/conversations?workspaceId=proj"); + }); + + it("combines ?status= and ?workspaceId= when both are given", async () => { + let calledUrl: string | undefined; + const fakeFetch = (async (url: string | URL | Request): Promise<Response> => { + calledUrl = String(url); + return new Response(JSON.stringify({ conversations: [] }), { status: 200 }); + }) as unknown as typeof fetch; + + await fetchConversations( + { fetchImpl: fakeFetch }, + { server: "http://localhost:24203", status: "active,idle", workspaceId: "proj" }, + ); + expect(calledUrl).toBe( + "http://localhost:24203/conversations?status=active%2Cidle&workspaceId=proj", + ); + }); + it("throws on non-OK status", async () => { const fakeFetch = (async (): Promise<Response> => new Response("boom", { status: 500 })) as unknown as typeof fetch; diff --git a/packages/cli/src/http.ts b/packages/cli/src/http.ts index 42fcfec..e13842a 100644 --- a/packages/cli/src/http.ts +++ b/packages/cli/src/http.ts @@ -98,6 +98,7 @@ interface FetchConversationsOpts { readonly server: string; readonly query?: string; readonly status?: string; + readonly workspaceId?: string; } export async function fetchConversations( @@ -107,6 +108,7 @@ export async function fetchConversations( const params = new URLSearchParams(); if (opts.query !== undefined) params.set("q", opts.query); if (opts.status !== undefined) params.set("status", opts.status); + if (opts.workspaceId !== undefined) params.set("workspaceId", opts.workspaceId); const qs = params.toString(); const url = qs.length > 0 ? `${opts.server}/conversations?${qs}` : `${opts.server}/conversations`; const res = await deps.fetchImpl(url); diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index b61be07..cba0de7 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -24,7 +24,7 @@ import { extractLastText, formatConversationList, renderEvent } from "./render.j const USAGE = `Usage: dispatch models [--server <url>] - dispatch list [<prefix>] [--status <active|idle|closed>] [--all] [--server <url>] + dispatch list [<prefix>] [--status <active|idle|closed>] [--workspace <id>] [--all] [--server <url>] dispatch stop <conversationId> [--server <url>] dispatch compact <conversationId> [--server <url>] dispatch read <conversationId> [--server <url>] @@ -61,6 +61,7 @@ async function main(): Promise<void> { server: parsed.server, ...(parsed.query !== undefined && { query: parsed.query }), ...(status !== undefined && { status }), + ...(parsed.workspaceId !== undefined && { workspaceId: parsed.workspaceId }), }, ); const table = formatConversationList(result.conversations, Date.now()); |
