@dench.com/cli 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/agent.ts +95 -13
- package/chat-spawn.ts +65 -4
- package/chat.ts +134 -1
- package/crm.ts +188 -87
- package/dench.ts +118 -17
- package/lib/cli-args.ts +61 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,11 +195,14 @@ dench crm entries list people
|
|
|
195
195
|
|
|
196
196
|
Auth precedence:
|
|
197
197
|
|
|
198
|
-
1. `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
198
|
+
1. Explicit `--dev` flag → use the dev workspace env (`NEXT_PUBLIC_CONVEX_URL`,
|
|
199
199
|
`DENCH_WORKSPACE`, `DENCH_DEV_AGENT_KEY`).
|
|
200
200
|
2. A locally-stored `dench login` session for the current host.
|
|
201
201
|
3. `DENCH_API_KEY` + `CONVEX_URL` (sandbox / automation path).
|
|
202
|
-
|
|
202
|
+
|
|
203
|
+
Plain terminal usage does not silently fall back to dev env vars. Without
|
|
204
|
+
`--dev`, a saved session, or `DENCH_API_KEY` + `CONVEX_URL`, live commands
|
|
205
|
+
return `login_required`.
|
|
203
206
|
|
|
204
207
|
Commands that need to impersonate a specific agent (`dench memory`,
|
|
205
208
|
`dench task`, `dench claim`, `dench log`, `dench approval request`,
|
package/agent.ts
CHANGED
|
@@ -34,6 +34,20 @@ type CliCtx = {
|
|
|
34
34
|
convex: ConvexHttpClient;
|
|
35
35
|
args: string[];
|
|
36
36
|
jsonOutput: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Bearer token forwarded by the dispatcher in `cli/dench.ts`. Either a
|
|
39
|
+
* `dch_agent_*` agent session minted by `dench login` or a unified
|
|
40
|
+
* Dench API key (`DENCH_API_KEY`) when the CLI is running inside a
|
|
41
|
+
* sandbox. Used by `runAwait` to authenticate against
|
|
42
|
+
* `runs:getChildRunStatusForCli`.
|
|
43
|
+
*/
|
|
44
|
+
sessionToken?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Convex `runs._id` of the caller's own run, when authenticated via
|
|
47
|
+
* an API key (sandbox path). Required by `getChildRunStatusForCli`
|
|
48
|
+
* to scope the API key's reach to a specific organization.
|
|
49
|
+
*/
|
|
50
|
+
callerRunId?: string;
|
|
37
51
|
};
|
|
38
52
|
|
|
39
53
|
// Args helpers moved to cli/lib/cli-args.ts; we adapt shift to throw
|
|
@@ -137,10 +151,18 @@ async function postJson(
|
|
|
137
151
|
export async function runAgentCommand(opts: {
|
|
138
152
|
convex: ConvexHttpClient;
|
|
139
153
|
args: string[];
|
|
154
|
+
sessionToken?: string;
|
|
155
|
+
callerRunId?: string;
|
|
140
156
|
}): Promise<void> {
|
|
141
157
|
const args = [...opts.args];
|
|
142
158
|
const jsonOutput = hasFlag(args, "--json");
|
|
143
|
-
const ctx: CliCtx = {
|
|
159
|
+
const ctx: CliCtx = {
|
|
160
|
+
convex: opts.convex,
|
|
161
|
+
args,
|
|
162
|
+
jsonOutput,
|
|
163
|
+
sessionToken: opts.sessionToken,
|
|
164
|
+
callerRunId: opts.callerRunId,
|
|
165
|
+
};
|
|
144
166
|
const subcommand = args.shift();
|
|
145
167
|
if (!subcommand || subcommand === "help" || subcommand === "--help") {
|
|
146
168
|
agentHelp();
|
|
@@ -243,27 +265,81 @@ async function runAwait(ctx: CliCtx): Promise<void> {
|
|
|
243
265
|
"dench agent await requires --hook <token> or --children <csv>",
|
|
244
266
|
);
|
|
245
267
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
268
|
+
if (!ctx.sessionToken) {
|
|
269
|
+
throw new AgentCliError(
|
|
270
|
+
"dench agent await --children needs an authenticated session " +
|
|
271
|
+
"(run `dench login`, or set DENCH_API_KEY in a sandbox)",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const childIds = childIdsCsv
|
|
276
|
+
.split(",")
|
|
277
|
+
.map((id) => id.trim())
|
|
278
|
+
.filter(Boolean);
|
|
279
|
+
if (childIds.length === 0) {
|
|
280
|
+
throw new AgentCliError("--children requires at least one run id");
|
|
281
|
+
}
|
|
250
282
|
const start = Date.now();
|
|
251
283
|
const timeoutMs = parseInt(
|
|
252
284
|
getFlag(ctx.args, "--timeout-ms") ?? "3600000",
|
|
253
285
|
10,
|
|
254
286
|
);
|
|
287
|
+
const intervalMs = parseInt(
|
|
288
|
+
getFlag(ctx.args, "--interval-ms") ?? "2000",
|
|
289
|
+
10,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
type ChildSnapshot = {
|
|
293
|
+
runId: string;
|
|
294
|
+
status: string;
|
|
295
|
+
summary: string | null;
|
|
296
|
+
threadId: string | null;
|
|
297
|
+
terminal: boolean;
|
|
298
|
+
};
|
|
299
|
+
const getStatus = makeFunctionReference<"query">(
|
|
300
|
+
"functions/runs:getChildRunStatusForCli",
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// Poll each child's status until it reaches a terminal state. The
|
|
304
|
+
// server uses sessionToken auth (mirrors `dench chat list`) so this
|
|
305
|
+
// works both for `dch_agent_*` sessions (local dev) and for unified
|
|
306
|
+
// `DENCH_API_KEY`s (sandbox, must include `--caller-run-id`).
|
|
307
|
+
const completed: Record<string, ChildSnapshot> = {};
|
|
255
308
|
while (Object.keys(completed).length < childIds.length) {
|
|
256
309
|
if (Date.now() - start > timeoutMs) {
|
|
257
|
-
throw new AgentCliError(
|
|
310
|
+
throw new AgentCliError(
|
|
311
|
+
`dench agent await timed out after ${timeoutMs}ms with ${
|
|
312
|
+
Object.keys(completed).length
|
|
313
|
+
}/${childIds.length} children completed`,
|
|
314
|
+
);
|
|
258
315
|
}
|
|
259
316
|
for (const childId of childIds) {
|
|
260
317
|
if (completed[childId]) continue;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
318
|
+
try {
|
|
319
|
+
const snap = (await ctx.convex.query(getStatus, {
|
|
320
|
+
sessionToken: ctx.sessionToken,
|
|
321
|
+
runId: ctx.callerRunId ?? undefined,
|
|
322
|
+
childRunId: childId as never,
|
|
323
|
+
})) as ChildSnapshot | null;
|
|
324
|
+
if (snap?.terminal) {
|
|
325
|
+
completed[childId] = snap;
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
329
|
+
// Treat "not found" / access errors as terminal-with-error so
|
|
330
|
+
// we don't poll forever on a typo'd id.
|
|
331
|
+
completed[childId] = {
|
|
332
|
+
runId: childId,
|
|
333
|
+
status: "error",
|
|
334
|
+
summary: msg,
|
|
335
|
+
threadId: null,
|
|
336
|
+
terminal: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (Object.keys(completed).length < childIds.length) {
|
|
341
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
264
342
|
}
|
|
265
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
266
|
-
break; // v1 polling stub — placeholder
|
|
267
343
|
}
|
|
268
344
|
out(ctx, { children: completed });
|
|
269
345
|
}
|
|
@@ -367,12 +443,18 @@ function agentHelp(): void {
|
|
|
367
443
|
Spawn a chat-turn workflow (same as the web UI's chat panel):
|
|
368
444
|
dench agent new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
369
445
|
dench agent send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
370
|
-
|
|
446
|
+
dench agent follow <thread-id> [--json]
|
|
447
|
+
These are aliases for \`dench chat new\` / \`dench chat send\` / \`dench chat follow\`.
|
|
448
|
+
Use \`follow\` to tail the live stream of an existing thread (e.g.
|
|
449
|
+
a subagent spawned by \`spawn_agent\`) without sending a new message.
|
|
371
450
|
|
|
372
451
|
Subagents (autonomous Long Sessions, NOT chat threads):
|
|
373
452
|
dench agent spawn '<goal>' [--parent-run-id <id>] [--sandbox own|share_parent] [--time-budget-ms N]
|
|
374
453
|
dench agent await --hook <token>
|
|
375
|
-
dench agent await --children id1,id2,id3 [--timeout-ms N]
|
|
454
|
+
dench agent await --children id1,id2,id3 [--timeout-ms N] [--interval-ms N]
|
|
455
|
+
(polls runs:getChildRunStatusForCli
|
|
456
|
+
with the active session token until
|
|
457
|
+
every child is terminal)
|
|
376
458
|
|
|
377
459
|
Peer messaging:
|
|
378
460
|
dench agent message <toRunId> '<text...>' [--from-run-id <id>]
|
package/chat-spawn.ts
CHANGED
|
@@ -87,21 +87,47 @@ function buildSpawnUrl(host: string): string {
|
|
|
87
87
|
return `${host.replace(/\/+$/, "")}/api/chat/cli`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function
|
|
90
|
+
function buildStreamUrlByRunId(host: string, runId: string): string {
|
|
91
91
|
return `${host.replace(/\/+$/, "")}/api/chat/${encodeURIComponent(runId)}/stream`;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function buildStreamUrlByThreadId(host: string, threadId: string): string {
|
|
95
|
+
return `${host.replace(/\/+$/, "")}/api/chat/stream?sessionId=${encodeURIComponent(threadId)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
/**
|
|
95
99
|
* Subscribe to the durable chat stream and pretty-print useful chunks
|
|
96
100
|
* until the stream closes. Mirrors the chunk types the chat panel
|
|
97
101
|
* cares about (text deltas, reasoning, tool input/output, finish).
|
|
102
|
+
*
|
|
103
|
+
* Resolves the stream URL from EITHER:
|
|
104
|
+
* - `runId` (Workflow SDK run id, returned in `x-workflow-run-id`
|
|
105
|
+
* by `POST /api/chat/cli`), via `/api/chat/[runId]/stream`, OR
|
|
106
|
+
* - `threadId` (Convex `chatThreads._id`), via
|
|
107
|
+
* `/api/chat/stream?sessionId=...` which resolves the active run
|
|
108
|
+
* by following `chatThreads.activeRunId → runs.workflowRunId`.
|
|
109
|
+
*
|
|
110
|
+
* The thread-id path is what `dench chat follow <threadId>` uses, so
|
|
111
|
+
* we can tail any live thread (including a subagent created by
|
|
112
|
+
* `spawn_agent`) without needing to know its workflow run id ahead of
|
|
113
|
+
* time.
|
|
98
114
|
*/
|
|
99
115
|
async function followStream(args: {
|
|
100
116
|
host: string;
|
|
101
117
|
bearerToken: string;
|
|
102
|
-
runId
|
|
118
|
+
runId?: string;
|
|
119
|
+
threadId?: string;
|
|
103
120
|
}): Promise<void> {
|
|
104
|
-
const url =
|
|
121
|
+
const url = args.runId
|
|
122
|
+
? buildStreamUrlByRunId(args.host, args.runId)
|
|
123
|
+
: args.threadId
|
|
124
|
+
? buildStreamUrlByThreadId(args.host, args.threadId)
|
|
125
|
+
: null;
|
|
126
|
+
if (!url) {
|
|
127
|
+
throw new ChatSpawnError(
|
|
128
|
+
"followStream requires either { runId } or { threadId }",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
105
131
|
const response = await fetch(url, {
|
|
106
132
|
method: "GET",
|
|
107
133
|
headers: {
|
|
@@ -342,6 +368,37 @@ async function runChatSend(ctx: ChatSpawnContext): Promise<void> {
|
|
|
342
368
|
}
|
|
343
369
|
}
|
|
344
370
|
|
|
371
|
+
/**
|
|
372
|
+
* `dench chat follow <threadId>` — tail the live stream of an
|
|
373
|
+
* existing chat thread by Convex thread id.
|
|
374
|
+
*
|
|
375
|
+
* Useful for watching a subagent (a child thread spawned by a parent
|
|
376
|
+
* via `spawn_agent`) live, or for resuming the stream of any chat
|
|
377
|
+
* thread that's currently in flight. Resolves the active workflow run
|
|
378
|
+
* server-side via `chatThreads.activeRunId → runs.workflowRunId`, so
|
|
379
|
+
* the caller doesn't need to know any workflow ids.
|
|
380
|
+
*
|
|
381
|
+
* Exits when the stream closes (workflow finishes) or when the user
|
|
382
|
+
* hits Ctrl-C.
|
|
383
|
+
*/
|
|
384
|
+
async function runChatFollow(ctx: ChatSpawnContext): Promise<void> {
|
|
385
|
+
const positionals = ctx.args.filter((arg) => !arg.startsWith("--"));
|
|
386
|
+
const threadId = positionals.shift();
|
|
387
|
+
if (!threadId) {
|
|
388
|
+
throw new ChatSpawnError(
|
|
389
|
+
"Usage: dench chat follow <threadId> [--json]",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (!ctx.jsonOutput) {
|
|
393
|
+
console.error(`--- following stream for thread ${threadId} ---`);
|
|
394
|
+
}
|
|
395
|
+
await followStream({
|
|
396
|
+
host: ctx.runtime.host,
|
|
397
|
+
bearerToken: ctx.runtime.bearerToken,
|
|
398
|
+
threadId,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
345
402
|
/**
|
|
346
403
|
* Public entry point invoked by the chat dispatcher in cli/dench.ts
|
|
347
404
|
* (and the agent dispatcher when the user runs `dench agent new` /
|
|
@@ -351,7 +408,7 @@ async function runChatSend(ctx: ChatSpawnContext): Promise<void> {
|
|
|
351
408
|
export async function runChatSpawnCommand(opts: {
|
|
352
409
|
runtime: RuntimeBundle;
|
|
353
410
|
args: string[];
|
|
354
|
-
subcommand: "new" | "send";
|
|
411
|
+
subcommand: "new" | "send" | "follow";
|
|
355
412
|
}): Promise<void> {
|
|
356
413
|
const args = [...opts.args];
|
|
357
414
|
const jsonOutput = hasFlag(args, "--json");
|
|
@@ -364,6 +421,10 @@ export async function runChatSpawnCommand(opts: {
|
|
|
364
421
|
await runChatNew(ctx);
|
|
365
422
|
return;
|
|
366
423
|
}
|
|
424
|
+
if (opts.subcommand === "follow") {
|
|
425
|
+
await runChatFollow(ctx);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
367
428
|
await runChatSend(ctx);
|
|
368
429
|
}
|
|
369
430
|
|
package/chat.ts
CHANGED
|
@@ -111,7 +111,12 @@ Read messages from a specific past chat thread:
|
|
|
111
111
|
Spawn a new chat-turn workflow (same agent loop as the web UI):
|
|
112
112
|
dench chat new "<prompt>" [--model <m>] [--file-context <path>] [--visibility private|shared] [--title T] [--yolo] [--follow] [--json]
|
|
113
113
|
dench chat send <thread-id> "<message>" [--model <m>] [--yolo] [--follow] [--json]
|
|
114
|
-
|
|
114
|
+
dench chat follow <thread-id> [--json]
|
|
115
|
+
dench chat tree [<root-thread-id>] [--limit N] [--json]
|
|
116
|
+
Aliased as: dench agent new / dench agent send / dench agent follow.
|
|
117
|
+
Use \`follow\` to tail any live thread by id (e.g. a subagent
|
|
118
|
+
spawned by another chat). Use \`tree\` to print the parent-child
|
|
119
|
+
thread hierarchy from a root thread.
|
|
115
120
|
|
|
116
121
|
Approval policy:
|
|
117
122
|
Without --yolo the workflow uses the workspace policy from
|
|
@@ -156,6 +161,8 @@ export async function runChatCommand(opts: {
|
|
|
156
161
|
return await runChatSearchCommand(ctx);
|
|
157
162
|
case "read":
|
|
158
163
|
return await runChatReadCommand(ctx);
|
|
164
|
+
case "tree":
|
|
165
|
+
return await runChatTreeCommand(ctx);
|
|
159
166
|
default:
|
|
160
167
|
throw new ChatCliError(`Unknown chat subcommand: ${subcommand}`);
|
|
161
168
|
}
|
|
@@ -214,3 +221,129 @@ async function runChatReadCommand(ctx: ChatCliContext): Promise<void> {
|
|
|
214
221
|
}),
|
|
215
222
|
);
|
|
216
223
|
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `dench chat tree [<root-thread-id>] [--limit N] [--json]` — print the
|
|
227
|
+
* parent-child chat thread hierarchy rooted at the given thread (or
|
|
228
|
+
* roots = top-level chats if no id is passed).
|
|
229
|
+
*
|
|
230
|
+
* Uses `listPastChats` (which already includes nested threads in its
|
|
231
|
+
* results — `topLevelOnly` is a `listThreads`-only knob) and groups
|
|
232
|
+
* client-side by `parentThreadId`. With `--json`, prints the raw flat
|
|
233
|
+
* array; otherwise prints an indented tree with status + a short
|
|
234
|
+
* summary excerpt for each row.
|
|
235
|
+
*/
|
|
236
|
+
async function runChatTreeCommand(ctx: ChatCliContext): Promise<void> {
|
|
237
|
+
// Strip flag values from positionals so e.g. `tree --limit 30` doesn't
|
|
238
|
+
// misinterpret "30" as the rootThreadId. We treat "--key value" as a
|
|
239
|
+
// pair iff value doesn't itself start with "--".
|
|
240
|
+
const positionals: string[] = [];
|
|
241
|
+
for (let i = 0; i < ctx.args.length; i++) {
|
|
242
|
+
const arg = ctx.args[i];
|
|
243
|
+
if (arg.startsWith("--")) {
|
|
244
|
+
const next = ctx.args[i + 1];
|
|
245
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
246
|
+
i += 1; // skip the flag value
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
positionals.push(arg);
|
|
251
|
+
}
|
|
252
|
+
const rootThreadId = positionals[0]?.trim() || undefined;
|
|
253
|
+
const limit = parseLimit(getFlag(ctx.args, "--limit")) ?? 200;
|
|
254
|
+
|
|
255
|
+
const flat = (await callQuery(ctx, api.listPastChats, {
|
|
256
|
+
limit,
|
|
257
|
+
includeArchived: false,
|
|
258
|
+
})) as Array<{
|
|
259
|
+
threadId: string;
|
|
260
|
+
title: string;
|
|
261
|
+
status: string;
|
|
262
|
+
parentThreadId: string | null;
|
|
263
|
+
nestingDepth: number | null;
|
|
264
|
+
summary: string | null;
|
|
265
|
+
lastMessageAt: number;
|
|
266
|
+
createdAt: number;
|
|
267
|
+
activeRunId: string | null;
|
|
268
|
+
}>;
|
|
269
|
+
|
|
270
|
+
if (ctx.jsonOutput) {
|
|
271
|
+
out(ctx, flat);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Group by parent. Top-level rows = either no parent, or parent
|
|
276
|
+
// outside the loaded set (so we don't silently swallow children
|
|
277
|
+
// whose parent fell off the limit window).
|
|
278
|
+
const ids = new Set(flat.map((t) => t.threadId));
|
|
279
|
+
const childrenByParent = new Map<string, typeof flat>();
|
|
280
|
+
const roots: typeof flat = [];
|
|
281
|
+
for (const thread of flat) {
|
|
282
|
+
if (thread.parentThreadId && ids.has(thread.parentThreadId)) {
|
|
283
|
+
let list = childrenByParent.get(thread.parentThreadId);
|
|
284
|
+
if (!list) {
|
|
285
|
+
list = [];
|
|
286
|
+
childrenByParent.set(thread.parentThreadId, list);
|
|
287
|
+
}
|
|
288
|
+
list.push(thread);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (rootThreadId) {
|
|
292
|
+
if (thread.threadId === rootThreadId) roots.push(thread);
|
|
293
|
+
} else {
|
|
294
|
+
roots.push(thread);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// When the user passed an explicit `rootThreadId` but it didn't
|
|
298
|
+
// appear in the loaded set (e.g. it sits beyond `--limit`), pull
|
|
299
|
+
// it on its own so we still surface it as the single root.
|
|
300
|
+
if (rootThreadId && roots.length === 0) {
|
|
301
|
+
const explicit = flat.find((t) => t.threadId === rootThreadId);
|
|
302
|
+
if (explicit) roots.push(explicit);
|
|
303
|
+
}
|
|
304
|
+
// Sort children oldest-first within each parent (spawn order),
|
|
305
|
+
// roots newest-first (most recent root at the top).
|
|
306
|
+
for (const list of childrenByParent.values()) {
|
|
307
|
+
list.sort((a, b) => (a.createdAt || a.lastMessageAt) - (b.createdAt || b.lastMessageAt));
|
|
308
|
+
}
|
|
309
|
+
roots.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
|
|
310
|
+
|
|
311
|
+
if (roots.length === 0) {
|
|
312
|
+
if (rootThreadId) {
|
|
313
|
+
console.error(
|
|
314
|
+
`No thread found with id ${rootThreadId} (or it's outside the limit ` +
|
|
315
|
+
`${limit} window — increase --limit).`,
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
console.error("No chat threads in this workspace yet.");
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const stripe = (s: string, n: number) =>
|
|
324
|
+
s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
325
|
+
const print = (thread: (typeof flat)[number], depth: number): void => {
|
|
326
|
+
const indent = " ".repeat(depth);
|
|
327
|
+
const branch = depth > 0 ? "└─ " : "";
|
|
328
|
+
const status =
|
|
329
|
+
thread.status === "streaming"
|
|
330
|
+
? "▸ running"
|
|
331
|
+
: thread.status === "error"
|
|
332
|
+
? "✗ error"
|
|
333
|
+
: thread.summary
|
|
334
|
+
? "✓ done"
|
|
335
|
+
: "·";
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
`${indent}${branch}[${status}] ${stripe(thread.title || "Untitled", 60)} ` +
|
|
338
|
+
`(${thread.threadId})\n`,
|
|
339
|
+
);
|
|
340
|
+
if (thread.summary) {
|
|
341
|
+
process.stdout.write(
|
|
342
|
+
`${indent}${depth > 0 ? " " : ""}${stripe(thread.summary.replace(/\s+/g, " "), 80)}\n`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const direct = childrenByParent.get(thread.threadId) ?? [];
|
|
346
|
+
for (const child of direct) print(child, depth + 1);
|
|
347
|
+
};
|
|
348
|
+
for (const root of roots) print(root, 0);
|
|
349
|
+
}
|