@guildai/cli 0.8.1 → 0.9.1

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.
Files changed (52) hide show
  1. package/dist/commands/agent/chat.d.ts +1 -0
  2. package/dist/commands/agent/chat.js +24 -1
  3. package/dist/commands/agent/init.js +1 -1
  4. package/dist/commands/agent/test.d.ts +1 -0
  5. package/dist/commands/agent/test.js +80 -28
  6. package/dist/commands/chat.d.ts +1 -0
  7. package/dist/commands/chat.js +178 -46
  8. package/dist/commands/integration/connect.js +1 -1
  9. package/dist/commands/integration/create.js +36 -36
  10. package/dist/commands/integration/operation/create.js +2 -1
  11. package/dist/commands/integration/operation/list.js +23 -15
  12. package/dist/commands/job/get-step.d.ts +3 -0
  13. package/dist/commands/job/{step-get.js → get-step.js} +3 -3
  14. package/dist/commands/mcp.js +2 -2
  15. package/dist/commands/session/create.js +1 -1
  16. package/dist/commands/session/events.js +16 -7
  17. package/dist/commands/session/list.js +1 -1
  18. package/dist/commands/session/send.js +1 -1
  19. package/dist/commands/workspace/agent/add.js +16 -3
  20. package/dist/commands/workspace/agent/list.js +14 -1
  21. package/dist/commands/workspace/agent/remove.js +14 -1
  22. package/dist/commands/workspace/clear.d.ts +3 -0
  23. package/dist/commands/workspace/clear.js +45 -0
  24. package/dist/commands/workspace/select.js +3 -1
  25. package/dist/index.js +58 -8
  26. package/dist/lib/api-types.d.ts +7 -0
  27. package/dist/lib/generated-types.d.ts +1 -1
  28. package/dist/lib/generated-types.js +1 -0
  29. package/dist/lib/guild-config.d.ts +13 -0
  30. package/dist/lib/guild-config.js +19 -0
  31. package/dist/lib/npmrc.js +6 -2
  32. package/dist/lib/output.js +25 -99
  33. package/dist/lib/polling.d.ts +7 -0
  34. package/dist/lib/polling.js +12 -3
  35. package/dist/lib/session-events.d.ts +32 -16
  36. package/dist/lib/session-events.js +22 -0
  37. package/dist/lib/session-polling.d.ts +4 -3
  38. package/dist/lib/session-polling.js +75 -17
  39. package/dist/lib/session-resume.js +4 -1
  40. package/dist/lib/stdin.d.ts +4 -0
  41. package/dist/lib/stdin.js +23 -0
  42. package/dist/lib/validate-input-schema.d.ts +19 -0
  43. package/dist/lib/validate-input-schema.js +208 -0
  44. package/dist/lib/version-helpers.js +38 -0
  45. package/dist/lib/workspace-helpers.d.ts +20 -0
  46. package/dist/lib/workspace-helpers.js +49 -0
  47. package/dist/mcp/tools.js +8 -52
  48. package/docs/CLI_WORKFLOW.md +1 -1
  49. package/docs/skills/agent-dev.md +192 -129
  50. package/docs/skills/integrations.md +1 -1
  51. package/package.json +2 -1
  52. package/dist/commands/job/step-get.d.ts +0 -3
@@ -1,3 +1,4 @@
1
1
  import { Command } from 'commander';
2
2
  export declare function createAgentChatCommand(): Command;
3
+ export declare function handleAgentChatAction(_promptArgs: string[], _options: Record<string, unknown>): Promise<void>;
3
4
  //# sourceMappingURL=chat.d.ts.map
@@ -19,6 +19,7 @@ import * as readline from 'readline';
19
19
  import { pollForResponse } from '../../lib/session-polling.js';
20
20
  import { readStdinAsJSON, ensureInteractiveStdin } from '../../lib/stdin.js';
21
21
  import { fetchSession, fetchSessionEvents } from '../../lib/session-resume.js';
22
+ import { getWorkspaceId, getWorkspaceSourceLabel } from '../../lib/guild-config.js';
22
23
  // ESM equivalent of __dirname
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
25
  // Read version from package.json
@@ -103,9 +104,26 @@ export function createAgentChatCommand() {
103
104
  const sessionPrompt = options.mode === 'json' && inputData
104
105
  ? JSON.stringify(inputData)
105
106
  : initialPrompt;
106
- const session = await createSession(client, options.workspace, sessionPrompt, version.id);
107
+ // Pre-resolve workspace so we can show the source label in output.
108
+ // When --workspace is given explicitly, no source label is shown.
109
+ let resolvedWorkspaceId = options.workspace;
110
+ let workspaceSourceLabel;
111
+ if (!resolvedWorkspaceId) {
112
+ const wsResolved = await getWorkspaceId(resolvedPath);
113
+ resolvedWorkspaceId = wsResolved?.workspaceId;
114
+ workspaceSourceLabel = wsResolved
115
+ ? getWorkspaceSourceLabel(wsResolved.source)
116
+ : undefined;
117
+ }
118
+ const session = await createSession(client, resolvedWorkspaceId, sessionPrompt, version.id);
107
119
  if (!quiet) {
108
120
  console.log(`✓ Agent: ${config?.name || agentId}`);
121
+ if (session.workspace_id) {
122
+ const workspaceDisplay = workspaceSourceLabel
123
+ ? `${session.workspace_id} (from ${workspaceSourceLabel})`
124
+ : session.workspace_id;
125
+ console.log(`✓ Workspace: ${workspaceDisplay}`);
126
+ }
109
127
  const sessionLink = session.session_url
110
128
  ? hyperlink(session.id, session.session_url)
111
129
  : session.id;
@@ -252,4 +270,9 @@ export function createAgentChatCommand() {
252
270
  });
253
271
  return cmd;
254
272
  }
273
+ // Thin wrapper for lazy-loading from index.ts (avoids importing React at startup)
274
+ export async function handleAgentChatAction(_promptArgs, _options) {
275
+ const cmd = createAgentChatCommand();
276
+ await cmd.parseAsync(process.argv.slice(3), { from: 'user' });
277
+ }
255
278
  //# sourceMappingURL=chat.js.map
@@ -1,7 +1,6 @@
1
1
  // Copyright 2026 Guild.ai
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { Command } from 'commander';
4
- import inquirer from 'inquirer';
5
4
  import { GuildAPIClient } from '../../lib/api-client.js';
6
5
  import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
7
6
  import { createSteps, createSpinner, format } from '../../lib/progress.js';
@@ -61,6 +60,7 @@ async function promptForName(defaultName) {
61
60
  return ask();
62
61
  }
63
62
  async function promptForTemplate() {
63
+ const { default: inquirer } = await import('inquirer');
64
64
  const { template } = await inquirer.prompt([
65
65
  {
66
66
  type: 'list',
@@ -1,3 +1,4 @@
1
1
  import { Command } from 'commander';
2
2
  export declare function createAgentTestCommand(): Command;
3
+ export declare function handleAgentTestAction(_options: Record<string, unknown>): Promise<void>;
3
4
  //# sourceMappingURL=test.d.ts.map
@@ -12,14 +12,14 @@ import { hyperlink } from '../../lib/colors.js';
12
12
  import { GuildAPIClient } from '../../lib/api-client.js';
13
13
  import { handleAxiosError, ErrorCodes } from '../../lib/errors.js';
14
14
  import { format } from '../../lib/progress.js';
15
- import * as readline from 'readline';
16
15
  import { parseEventFilter } from '../../lib/event-filter.js';
17
16
  import { isQuietMode, getOutputMode } from '../../lib/output-mode.js';
18
17
  import { pollForResponse, pollForResponseWithEvents, } from '../../lib/session-polling.js';
19
- import { readStdinAsJSON, ensureInteractiveStdin } from '../../lib/stdin.js';
20
- import { loadLocalConfig, getWorkspaceId } from '../../lib/guild-config.js';
18
+ import { readStdinAsJSON, readStdinAsText, ensureInteractiveStdin, } from '../../lib/stdin.js';
19
+ import { loadLocalConfig, getWorkspaceId, getWorkspaceSourceLabel, } from '../../lib/guild-config.js';
21
20
  import { GitError, formatGitError } from '../../lib/git.js';
22
21
  import { readAgentFiles, buildEphemeralVersion, buildBundledVersion, BundleNotFoundError, BuildTimeoutError, BuildFailedError, } from '../../lib/agent-helpers.js';
22
+ import { validateInputSchema } from '../../lib/validate-input-schema.js';
23
23
  import { fetchSession, fetchSessionEvents } from '../../lib/session-resume.js';
24
24
  import { ChatApp, ensureAuthenticated } from '../chat.js';
25
25
  import { suppressScrollbackClear } from '../../lib/alternate-screen.js';
@@ -83,9 +83,10 @@ export function createAgentTestCommand() {
83
83
  console.error(' guild agent clone <agent-id>');
84
84
  process.exit(1);
85
85
  }
86
- // If using JSON input, read and validate it early (before auth/session creation)
87
- // This provides better UX - fail fast on bad JSON without needing auth
86
+ // If using JSON/JSONL input, read and validate it early (before auth/session creation)
87
+ // This provides better UX - fail fast on bad input without needing auth
88
88
  let inputData;
89
+ let jsonlInputs;
89
90
  if (options.mode === 'json') {
90
91
  try {
91
92
  inputData = await readStdinAsJSON();
@@ -100,6 +101,57 @@ export function createAgentTestCommand() {
100
101
  process.exit(1);
101
102
  }
102
103
  }
104
+ else if (options.mode === 'jsonl') {
105
+ try {
106
+ const stdinContent = await readStdinAsText();
107
+ jsonlInputs = [];
108
+ const lines = stdinContent.split('\n');
109
+ for (let i = 0; i < lines.length; i++) {
110
+ const line = lines[i].trim();
111
+ if (!line)
112
+ continue;
113
+ try {
114
+ jsonlInputs.push(JSON.parse(line));
115
+ }
116
+ catch {
117
+ console.error(`Error: Invalid JSON on line ${i + 1}`);
118
+ console.error('');
119
+ console.error('Each line must be valid JSON.');
120
+ console.error(' cat inputs.jsonl | guild agent test --mode jsonl');
121
+ process.exit(1);
122
+ }
123
+ }
124
+ if (jsonlInputs.length === 0) {
125
+ console.error('Error: No JSON input provided');
126
+ console.error('');
127
+ console.error('Example usage:');
128
+ console.error(' cat inputs.jsonl | guild agent test --mode jsonl');
129
+ process.exit(1);
130
+ }
131
+ }
132
+ catch (error) {
133
+ const err = error;
134
+ console.error(`Error: ${err.message}`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ // Validate input against agent's schema locally (before build)
139
+ // Skip when --agent-version is set since local source may differ from that version
140
+ const inputsToValidate = inputData !== undefined ? [inputData] : (jsonlInputs ?? []);
141
+ if (inputsToValidate.length > 0 && !options.agentVersion) {
142
+ const validationResult = await validateInputSchema(cwd, inputsToValidate);
143
+ if (!validationResult.valid) {
144
+ console.error('Input validation failed:');
145
+ for (const err of validationResult.errors) {
146
+ const pathStr = err.path?.length ? ` (at ${err.path.join('.')})` : '';
147
+ console.error(` - ${err.message}${pathStr}`);
148
+ }
149
+ process.exit(1);
150
+ }
151
+ if ('skipped' in validationResult && validationResult.skipped) {
152
+ format.warn(`Skipping local validation: ${validationResult.reason}`);
153
+ }
154
+ }
103
155
  // If using bundle, verify the file exists early (before auth/session creation)
104
156
  // so we fail fast without needing auth if the path is wrong.
105
157
  if (options.bundle) {
@@ -117,9 +169,13 @@ export function createAgentTestCommand() {
117
169
  }
118
170
  // Determine workspace (priority: flag > local config > global config)
119
171
  let workspaceId = options.workspace;
172
+ let workspaceSourceLabel;
120
173
  if (!workspaceId) {
121
174
  const resolved = await getWorkspaceId(cwd);
122
175
  workspaceId = resolved?.workspaceId;
176
+ workspaceSourceLabel = resolved
177
+ ? getWorkspaceSourceLabel(resolved.source)
178
+ : undefined;
123
179
  if (!workspaceId) {
124
180
  console.error('Error: No default workspace configured');
125
181
  console.error('');
@@ -251,7 +307,10 @@ export function createAgentTestCommand() {
251
307
  ? 'ephemeral (cached, no changes)'
252
308
  : 'ephemeral (working directory)';
253
309
  console.log(`✓ Version: ${versionDisplay}`);
254
- console.log(`✓ Workspace: ${workspaceId}`);
310
+ const workspaceDisplay = workspaceSourceLabel
311
+ ? `${workspaceId} (from ${workspaceSourceLabel})`
312
+ : workspaceId;
313
+ console.log(`✓ Workspace: ${workspaceDisplay}`);
255
314
  const sessionLink = session.session_url
256
315
  ? hyperlink(session.id, session.session_url)
257
316
  : session.id;
@@ -325,25 +384,16 @@ export function createAgentTestCommand() {
325
384
  process.exit(1);
326
385
  }
327
386
  }
328
- else if (options.mode === 'jsonl') {
329
- // JSONL input mode: line-by-line processing
330
- const rl = readline.createInterface({
331
- input: process.stdin,
332
- output: process.stdout,
333
- terminal: false,
334
- });
335
- let lineNumber = 0;
387
+ else if (options.mode === 'jsonl' && jsonlInputs) {
388
+ // JSONL input mode: line-by-line processing (inputs already parsed and validated)
336
389
  let processedCount = 0;
337
390
  let lastEventId;
338
- for await (const line of rl) {
339
- lineNumber++;
340
- if (!line.trim())
341
- continue; // Skip empty lines
391
+ for (let inputIndex = 0; inputIndex < jsonlInputs.length; inputIndex++) {
392
+ const jsonInput = jsonlInputs[inputIndex];
393
+ if (!quiet) {
394
+ console.error(`Processing input ${inputIndex + 1}/${jsonlInputs.length}...`);
395
+ }
342
396
  try {
343
- const jsonInput = JSON.parse(line);
344
- if (!quiet) {
345
- console.error(`Processing line ${lineNumber}...`);
346
- }
347
397
  // Send message
348
398
  await client.post(`/sessions/${session.id}/events`, {
349
399
  mode: 'json',
@@ -355,13 +405,12 @@ export function createAgentTestCommand() {
355
405
  : await pollForResponse(client, session.id, lastEventId, 180000);
356
406
  lastEventId = result.lastEventId;
357
407
  if (!result.response) {
358
- console.error(`Timeout: No response for line ${lineNumber}`);
408
+ console.error(`Timeout: No response for input ${inputIndex + 1}`);
359
409
  continue;
360
410
  }
361
411
  const response = result.response;
362
412
  // Output response
363
413
  if (quiet) {
364
- // In quiet mode, output just the message content (or as JSON if --json is used)
365
414
  console.log(response);
366
415
  }
367
416
  else {
@@ -371,10 +420,8 @@ export function createAgentTestCommand() {
371
420
  processedCount++;
372
421
  }
373
422
  catch (error) {
374
- const err = error;
375
- console.error(`Error: Invalid JSON on line ${lineNumber}`);
376
- console.error(`Failed to parse JSON: ${err.message}`);
377
- console.error('Skipping line and continuing...');
423
+ const formattedErr = handleAxiosError(error);
424
+ console.error(`Error processing input ${inputIndex + 1}: ${formattedErr.details}`);
378
425
  }
379
426
  }
380
427
  if (!quiet) {
@@ -431,4 +478,9 @@ export function createAgentTestCommand() {
431
478
  });
432
479
  return cmd;
433
480
  }
481
+ // Thin wrapper for lazy-loading from index.ts (avoids importing React at startup)
482
+ export async function handleAgentTestAction(_options) {
483
+ const cmd = createAgentTestCommand();
484
+ await cmd.parseAsync(process.argv.slice(3), { from: 'user' });
485
+ }
434
486
  //# sourceMappingURL=test.js.map
@@ -23,4 +23,5 @@ export declare function ChatApp({ initialPrompt, version, workspaceId, versionId
23
23
  export declare function ensureAuthenticated(): Promise<string>;
24
24
  export declare function createSession(client: GuildAPIClient, workspaceId: string | undefined, initialPrompt: string, versionId?: string, onProgress?: (status: string) => void): Promise<Session>;
25
25
  export declare function createChatCommand(): Command;
26
+ export declare function handleChatAction(_promptArgs: string[], _options: Record<string, unknown>): Promise<void>;
26
27
  //# sourceMappingURL=chat.d.ts.map
@@ -13,12 +13,12 @@ import chalk from 'chalk';
13
13
  import { readFileSync } from 'fs';
14
14
  import path from 'path';
15
15
  import { fileURLToPath } from 'url';
16
- import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName, getAgentName, } from '../lib/session-events.js';
16
+ import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName, getAgentName, getAgentNotificationText, isDoneResponseStreamEvent, isResponseStreamEvent, isRootTaskEvent, } from '../lib/session-events.js';
17
17
  import { printResumeHint, fetchSession, fetchSessionEvents, eventsToDisplayMessages, } from '../lib/session-resume.js';
18
18
  import { DEFAULT_EVENT_TYPES, parseEventFilter, shouldShowEvent, } from '../lib/event-filter.js';
19
19
  import { fetchEvents, fetchTasks } from '../lib/session-events-fetch.js';
20
20
  import { AgentInstallPrompt } from '../components/AgentInstallPrompt.js';
21
- import { getWorkspaceId } from '../lib/guild-config.js';
21
+ import { getWorkspaceId, getWorkspaceSourceLabel } from '../lib/guild-config.js';
22
22
  import { ensureInteractiveStdin } from '../lib/stdin.js';
23
23
  import { brand, BRAND_COLOR, code as codeColor, hyperlink } from '../lib/colors.js';
24
24
  import { SplashAnimation } from '../components/SplashAnimation.js';
@@ -48,6 +48,48 @@ function fixListItemMarkdown(text) {
48
48
  text = text.replace(/(?<![\\w])_([^_]+)_(?![\\w])/g, (_, content) => chalk.italic(content));
49
49
  return text;
50
50
  }
51
+ function extractMessageText(text) {
52
+ const trimmed = text.trim();
53
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
54
+ return text;
55
+ try {
56
+ const parsed = JSON.parse(trimmed);
57
+ if (typeof parsed === 'object' && parsed !== null && 'message' in parsed) {
58
+ return typeof parsed.message === 'string' ? parsed.message : text;
59
+ }
60
+ }
61
+ catch {
62
+ // If parsing fails, use the content as-is.
63
+ }
64
+ return text;
65
+ }
66
+ function extractRuntimeDoneText(content) {
67
+ if (content === null || content === undefined)
68
+ return null;
69
+ if (typeof content === 'string')
70
+ return content.trim() ? content : null;
71
+ if (typeof content !== 'object' || Array.isArray(content)) {
72
+ return JSON.stringify(content);
73
+ }
74
+ const record = content;
75
+ if (Object.keys(record).length === 0)
76
+ return null;
77
+ if (record.type === 'text') {
78
+ if (typeof record.text === 'string')
79
+ return record.text;
80
+ if (typeof record.data === 'string')
81
+ return record.data;
82
+ }
83
+ if (typeof record.message === 'string')
84
+ return record.message;
85
+ // runtime_done content is agent output, not notification content. Preserve
86
+ // structured outputs rather than trying to render notification-only shapes.
87
+ return JSON.stringify(content);
88
+ }
89
+ function renderAssistantMessage(text, taskName) {
90
+ const rendered = fixListItemMarkdown(marked.parse(text));
91
+ return `${chalk.green('●')} ${chalk.bold(taskName)}\n${rendered.trim()}`;
92
+ }
51
93
  /**
52
94
  * Output the result of a --once mode session.
53
95
  * Handles both JSON and human-readable output formats.
@@ -57,21 +99,29 @@ async function outputOnceResult(sessionId, events, mode) {
57
99
  console.log(JSON.stringify({ session_id: sessionId, events }, null, 2));
58
100
  }
59
101
  else {
60
- const agentMessages = events.filter((e) => e.type === 'agent_notification_message');
61
- if (agentMessages.length > 0) {
62
- let messageContent = agentMessages[agentMessages.length - 1].content.data;
63
- const trimmed = messageContent.trim();
64
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
65
- try {
66
- const parsed = JSON.parse(trimmed);
67
- if (typeof parsed === 'object' && parsed !== null && 'message' in parsed) {
68
- messageContent = parsed.message;
69
- }
70
- }
71
- catch {
72
- // If parsing fails, use content as-is
73
- }
102
+ const finalAgentMessages = events.filter((e) => e.type === 'agent_notification_message' &&
103
+ isRootTaskEvent(e) &&
104
+ !isResponseStreamEvent(e));
105
+ const streamFallbacks = events.filter((e) => e.type === 'agent_notification_message' &&
106
+ isRootTaskEvent(e) &&
107
+ isDoneResponseStreamEvent(e));
108
+ if (finalAgentMessages.length > 0) {
109
+ const messageContent = extractMessageText(getAgentNotificationText(finalAgentMessages[finalAgentMessages.length - 1]));
110
+ const rendered = fixListItemMarkdown(await marked(messageContent));
111
+ console.log(rendered.trim());
112
+ return;
113
+ }
114
+ const runtimeDoneEvents = events.filter((e) => e.type === 'runtime_done' && isRootTaskEvent(e) && e.content);
115
+ const runtimeDoneWithContent = runtimeDoneEvents[runtimeDoneEvents.length - 1];
116
+ if (runtimeDoneWithContent?.type === 'runtime_done') {
117
+ const runtimeDoneText = extractRuntimeDoneText(runtimeDoneWithContent.content);
118
+ if (runtimeDoneText) {
119
+ console.log(runtimeDoneText);
120
+ return;
74
121
  }
122
+ }
123
+ if (streamFallbacks.length > 0) {
124
+ const messageContent = extractMessageText(getAgentNotificationText(streamFallbacks[streamFallbacks.length - 1]));
75
125
  const rendered = fixListItemMarkdown(await marked(messageContent));
76
126
  console.log(rendered.trim());
77
127
  }
@@ -427,6 +477,83 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
427
477
  const isPolling = useRef(false);
428
478
  const receivedResponseSinceLastInput = useRef(false);
429
479
  const firstMessageNotified = useRef(!!resumeEvents);
480
+ const responseStreamKeys = useRef(new Map());
481
+ const responseStreamTimestamps = useRef(new Map());
482
+ const responseStreamKeysByTask = useRef(new Map());
483
+ const clearResponseStreamsForTask = (taskId) => {
484
+ if (!taskId)
485
+ return;
486
+ const keys = responseStreamKeysByTask.current.get(taskId);
487
+ if (!keys?.size)
488
+ return;
489
+ for (const [streamId, key] of responseStreamKeys.current.entries()) {
490
+ if (keys.has(key)) {
491
+ responseStreamKeys.current.delete(streamId);
492
+ responseStreamTimestamps.current.delete(streamId);
493
+ }
494
+ }
495
+ responseStreamKeysByTask.current.delete(taskId);
496
+ setMessages((prev) => prev.filter((message) => !keys.has(message.key)));
497
+ };
498
+ const upsertResponseStreamMessage = (event) => {
499
+ if (!isResponseStreamEvent(event))
500
+ return;
501
+ if (!isRootTaskEvent(event))
502
+ return;
503
+ const streamId = event.content.stream_id;
504
+ const taskId = event.task?.id;
505
+ const existingKey = responseStreamKeys.current.get(streamId);
506
+ if (event.content.status === 'aborted') {
507
+ if (existingKey) {
508
+ responseStreamKeys.current.delete(streamId);
509
+ responseStreamTimestamps.current.delete(streamId);
510
+ if (taskId) {
511
+ const keys = responseStreamKeysByTask.current.get(taskId);
512
+ keys?.delete(existingKey);
513
+ if (keys?.size === 0)
514
+ responseStreamKeysByTask.current.delete(taskId);
515
+ }
516
+ setMessages((prev) => prev.filter((message) => message.key !== existingKey));
517
+ }
518
+ return;
519
+ }
520
+ const text = event.content.text;
521
+ if (!text.trim())
522
+ return;
523
+ const key = existingKey ?? `response-stream-${streamId}`;
524
+ responseStreamKeys.current.set(streamId, key);
525
+ if (taskId) {
526
+ const keys = responseStreamKeysByTask.current.get(taskId) ?? new Set();
527
+ keys.add(key);
528
+ responseStreamKeysByTask.current.set(taskId, keys);
529
+ }
530
+ const taskName = agentName || 'assistant';
531
+ const messageContent = renderAssistantMessage(text, taskName);
532
+ const timestamp = responseStreamTimestamps.current.get(streamId) ?? new Date().toLocaleTimeString();
533
+ responseStreamTimestamps.current.set(streamId, timestamp);
534
+ setMessages((prev) => {
535
+ const index = prev.findIndex((message) => message.key === key);
536
+ const message = {
537
+ key,
538
+ content: messageContent,
539
+ type: 'assistant',
540
+ timestamp,
541
+ };
542
+ if (index === -1)
543
+ return [...prev, message];
544
+ const next = [...prev];
545
+ next[index] = message;
546
+ return next;
547
+ });
548
+ if (!firstMessageNotified.current && onFirstMessage) {
549
+ firstMessageNotified.current = true;
550
+ onFirstMessage();
551
+ }
552
+ if (event.content.status === 'done') {
553
+ receivedResponseSinceLastInput.current = true;
554
+ setCurrentOperation('');
555
+ }
556
+ };
430
557
  // Mark initial prompt as sent (skip for resume — we already have the events)
431
558
  useEffect(() => {
432
559
  if (!resumeEvents) {
@@ -526,6 +653,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
526
653
  }
527
654
  // Process events that affect the chat UI (task state comes from tasks poll)
528
655
  if (event.type === 'runtime_error') {
656
+ clearResponseStreamsForTask(taskInfo?.id);
529
657
  // Always clear the spinner on runtime errors so the UI doesn't get stuck
530
658
  setCurrentOperation('');
531
659
  // Show runtime errors in the chat (gated on --events filter)
@@ -639,32 +767,15 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
639
767
  }
640
768
  }
641
769
  else if (event.type === 'agent_notification_message') {
642
- let text = typeof event.content === 'string'
643
- ? event.content
644
- : event.content?.data || '';
645
- // Extract message content, handling JSON responses with "message" field
646
- const trimmed = text.trim();
647
- if (trimmed) {
648
- // Check if content looks like JSON object with "message" field
649
- if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
650
- try {
651
- const parsed = JSON.parse(trimmed);
652
- // If JSON has a "message" field, extract it (some agents wrap responses this way)
653
- if (typeof parsed === 'object' &&
654
- parsed !== null &&
655
- 'message' in parsed) {
656
- text = parsed.message;
657
- }
658
- // Otherwise use full JSON content as-is (legitimate JSON responses)
659
- }
660
- catch {
661
- // If parsing fails, use the content as-is
662
- }
663
- }
664
- const rendered = fixListItemMarkdown(marked.parse(text));
665
- // Add attribution with task name
770
+ if (isResponseStreamEvent(event)) {
771
+ upsertResponseStreamMessage(event);
772
+ continue;
773
+ }
774
+ const text = extractMessageText(getAgentNotificationText(event));
775
+ if (text.trim()) {
776
+ clearResponseStreamsForTask(taskInfo?.id);
666
777
  const taskName = agentName || 'assistant';
667
- const messageContent = `${chalk.green('●')} ${chalk.bold(taskName)}\n${rendered.trim()}`;
778
+ const messageContent = renderAssistantMessage(text, taskName);
668
779
  setMessages((prev) => [
669
780
  ...prev,
670
781
  {
@@ -684,6 +795,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
684
795
  setCurrentOperation('');
685
796
  }
686
797
  else if (event.type === 'agent_notification_error') {
798
+ clearResponseStreamsForTask(taskInfo?.id);
687
799
  // Show error in chat (task status is updated via tasks poll)
688
800
  const errorText = typeof event.content === 'string'
689
801
  ? event.content
@@ -730,6 +842,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
730
842
  event.content !== undefined &&
731
843
  taskInfo &&
732
844
  'agent' in taskInfo) {
845
+ clearResponseStreamsForTask(taskInfo.id);
733
846
  // One-shot agents may complete with runtime_done without sending
734
847
  // agent_notification_message. Display the output if we haven't
735
848
  // already shown a response for this input cycle.
@@ -759,6 +872,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
759
872
  }
760
873
  }
761
874
  else if (event.type === 'interrupted') {
875
+ clearResponseStreamsForTask(taskInfo?.id);
762
876
  // Session was interrupted — interrupted sessions are terminal on the backend
763
877
  setMessages((prev) => [
764
878
  ...prev,
@@ -987,6 +1101,10 @@ export async function createSession(client, workspaceId, initialPrompt, versionI
987
1101
  const resolved = await getWorkspaceId();
988
1102
  if (resolved) {
989
1103
  workspaceId = resolved.workspaceId;
1104
+ const sourceLabel = getWorkspaceSourceLabel(resolved.source);
1105
+ if (sourceLabel) {
1106
+ progress(`Using workspace from ${sourceLabel}`);
1107
+ }
990
1108
  }
991
1109
  }
992
1110
  if (!workspaceId) {
@@ -1045,7 +1163,9 @@ export function createChatCommand() {
1045
1163
  try {
1046
1164
  await ensureAuthenticated();
1047
1165
  const client = new GuildAPIClient();
1048
- const session = await createSession(client, options.workspace, initialPrompt, options.agent);
1166
+ const session = await createSession(client, options.workspace, initialPrompt, options.agent, (status) => {
1167
+ spinner.text = status;
1168
+ });
1049
1169
  spinner.succeed('Connected');
1050
1170
  if (session.session_url) {
1051
1171
  const sessionLink = hyperlink(session.id, session.session_url);
@@ -1084,12 +1204,18 @@ export function createChatCommand() {
1084
1204
  else {
1085
1205
  inactivityCounter++;
1086
1206
  }
1087
- const isRootTask = (e) => !e.task?.parent_task;
1088
- const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' && isRootTask(e));
1089
- const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' && isRootTask(e));
1090
- const hasRootTaskError = allEvents.some((e) => e.type === 'runtime_error' && isRootTask(e));
1207
+ // Check if we got a completion response from the root agent.
1208
+ // Stream done events are only a rendering fallback; wait for the
1209
+ // final root message or runtime completion so child drafts cannot
1210
+ // terminate --once early.
1211
+ const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' && isRootTaskEvent(e));
1212
+ const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
1213
+ isRootTaskEvent(e) &&
1214
+ !isResponseStreamEvent(e));
1215
+ const hasRootTaskError = allEvents.some((e) => e.type === 'runtime_error' && isRootTaskEvent(e));
1091
1216
  // Check for a ui_prompt request... that ends the game.
1092
1217
  const hasUIPromptMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
1218
+ !isResponseStreamEvent(e) &&
1093
1219
  e.task?.tool_name === 'ui_prompt');
1094
1220
  if (hasRootTaskError) {
1095
1221
  debug('Found error event from root agent, exiting --once mode');
@@ -1177,4 +1303,10 @@ export function createChatCommand() {
1177
1303
  });
1178
1304
  return cmd;
1179
1305
  }
1306
+ // Thin wrapper for lazy-loading from index.ts (avoids importing React at startup)
1307
+ export async function handleChatAction(_promptArgs, _options) {
1308
+ const cmd = createChatCommand();
1309
+ // Re-parse the original argv so the command's own action handler runs
1310
+ await cmd.parseAsync(process.argv.slice(2), { from: 'user' });
1311
+ }
1180
1312
  //# sourceMappingURL=chat.js.map
@@ -2,7 +2,6 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
- import inquirer from 'inquirer';
6
5
  import { GuildAPIClient } from '../../lib/api-client.js';
7
6
  import { getAuthToken } from '../../lib/auth.js';
8
7
  import { handleAxiosError } from '../../lib/errors.js';
@@ -40,6 +39,7 @@ export function createIntegrationConnectCommand() {
40
39
  }
41
40
  let apiKey = options.token;
42
41
  if (!apiKey) {
42
+ const { default: inquirer } = await import('inquirer');
43
43
  const answer = await inquirer.prompt([
44
44
  {
45
45
  type: 'password',