summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 11:27:43 +0900
committerAdam Malczewski <[email protected]>2026-06-25 11:27:43 +0900
commitc1bc7bfaaca7bdf4d9b2973f5dc88605217a7866 (patch)
tree204de76d526a052712bba0a97bfe8a5d8935ea64
parent59c481dc96aa40b8c8f0a81bb294a77d4e5aa533 (diff)
downloaddispatch-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.ts35
-rw-r--r--packages/cli/src/args.ts6
-rw-r--r--packages/cli/src/http.test.ts30
-rw-r--r--packages/cli/src/http.ts2
-rw-r--r--packages/cli/src/main.ts3
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());