@guildai/cli 0.9.0 → 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 (41) 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 +92 -34
  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/mcp.js +1 -1
  13. package/dist/commands/session/events.js +16 -7
  14. package/dist/commands/session/send.js +1 -1
  15. package/dist/commands/workspace/agent/add.js +16 -3
  16. package/dist/commands/workspace/agent/list.js +14 -1
  17. package/dist/commands/workspace/agent/remove.js +14 -1
  18. package/dist/commands/workspace/clear.d.ts +3 -0
  19. package/dist/commands/workspace/clear.js +45 -0
  20. package/dist/commands/workspace/select.js +3 -1
  21. package/dist/index.js +53 -6
  22. package/dist/lib/api-types.d.ts +1 -0
  23. package/dist/lib/generated-types.d.ts +1 -1
  24. package/dist/lib/generated-types.js +1 -0
  25. package/dist/lib/guild-config.d.ts +13 -0
  26. package/dist/lib/guild-config.js +19 -0
  27. package/dist/lib/npmrc.js +6 -2
  28. package/dist/lib/session-events.d.ts +32 -16
  29. package/dist/lib/session-events.js +22 -0
  30. package/dist/lib/session-polling.d.ts +4 -3
  31. package/dist/lib/session-polling.js +75 -17
  32. package/dist/lib/session-resume.js +4 -1
  33. package/dist/lib/stdin.d.ts +4 -0
  34. package/dist/lib/stdin.js +23 -0
  35. package/dist/lib/validate-input-schema.d.ts +19 -0
  36. package/dist/lib/validate-input-schema.js +208 -0
  37. package/dist/lib/workspace-helpers.d.ts +20 -0
  38. package/dist/lib/workspace-helpers.js +49 -0
  39. package/dist/mcp/tools.js +8 -52
  40. package/docs/skills/agent-dev.md +191 -128
  41. package/package.json +2 -1
@@ -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
- import { pollForResponseWithEvents } from '../../lib/session-polling.js';
19
- import { readStdinAsJSON, ensureInteractiveStdin } from '../../lib/stdin.js';
20
- import { loadLocalConfig, getWorkspaceId } from '../../lib/guild-config.js';
17
+ import { pollForResponse, pollForResponseWithEvents, } from '../../lib/session-polling.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';
@@ -36,13 +36,15 @@ export function createAgentTestCommand() {
36
36
  .option('--agent-version <id>', 'Test a specific version (UUID or version number)')
37
37
  .option('--resume <session-id>', 'Resume an existing test session')
38
38
  .option('--open', 'Open session in web dashboard')
39
- .option('--events <types>', 'Event types to stream (default: all). Shorthands: none, user, system, all, or comma-separated type names')
39
+ .option('--events <types>', 'Event types to stream (default: user). Shorthands: none, user, system, all, or comma-separated type names')
40
40
  .option('--bundle <file>', 'Path to a pre-built gzip+base64 bundle file')
41
41
  .option('--no-cache', 'Skip ephemeral build cache (force a fresh build)')
42
42
  .action(async (options) => {
43
43
  const cwd = process.cwd();
44
- // Parse --events filter once, before any branching (default: all)
45
- const eventFilter = parseEventFilter(options.events ?? 'all');
44
+ // Parse --events filter once, before any branching
45
+ const eventFilter = options.events
46
+ ? parseEventFilter(options.events)
47
+ : undefined;
46
48
  try {
47
49
  // Handle --resume: skip build, fetch existing session, hand off to ChatApp
48
50
  if (options.resume) {
@@ -81,9 +83,10 @@ export function createAgentTestCommand() {
81
83
  console.error(' guild agent clone <agent-id>');
82
84
  process.exit(1);
83
85
  }
84
- // If using JSON input, read and validate it early (before auth/session creation)
85
- // 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
86
88
  let inputData;
89
+ let jsonlInputs;
87
90
  if (options.mode === 'json') {
88
91
  try {
89
92
  inputData = await readStdinAsJSON();
@@ -98,6 +101,57 @@ export function createAgentTestCommand() {
98
101
  process.exit(1);
99
102
  }
100
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
+ }
101
155
  // If using bundle, verify the file exists early (before auth/session creation)
102
156
  // so we fail fast without needing auth if the path is wrong.
103
157
  if (options.bundle) {
@@ -115,9 +169,13 @@ export function createAgentTestCommand() {
115
169
  }
116
170
  // Determine workspace (priority: flag > local config > global config)
117
171
  let workspaceId = options.workspace;
172
+ let workspaceSourceLabel;
118
173
  if (!workspaceId) {
119
174
  const resolved = await getWorkspaceId(cwd);
120
175
  workspaceId = resolved?.workspaceId;
176
+ workspaceSourceLabel = resolved
177
+ ? getWorkspaceSourceLabel(resolved.source)
178
+ : undefined;
121
179
  if (!workspaceId) {
122
180
  console.error('Error: No default workspace configured');
123
181
  console.error('');
@@ -249,7 +307,10 @@ export function createAgentTestCommand() {
249
307
  ? 'ephemeral (cached, no changes)'
250
308
  : 'ephemeral (working directory)';
251
309
  console.log(`✓ Version: ${versionDisplay}`);
252
- console.log(`✓ Workspace: ${workspaceId}`);
310
+ const workspaceDisplay = workspaceSourceLabel
311
+ ? `${workspaceId} (from ${workspaceSourceLabel})`
312
+ : workspaceId;
313
+ console.log(`✓ Workspace: ${workspaceDisplay}`);
253
314
  const sessionLink = session.session_url
254
315
  ? hyperlink(session.id, session.session_url)
255
316
  : session.id;
@@ -270,7 +331,9 @@ export function createAgentTestCommand() {
270
331
  });
271
332
  // Poll for response (starting from beginning)
272
333
  // 3 minutes - allow time for agents that use LLM calls for input parsing
273
- const { response } = await pollForResponseWithEvents(client, session.id, eventFilter, undefined, 180000);
334
+ const { response } = eventFilter
335
+ ? await pollForResponseWithEvents(client, session.id, eventFilter, undefined, 180000)
336
+ : await pollForResponse(client, session.id, undefined, 180000);
274
337
  if (!response) {
275
338
  console.error('Error: No response received from agent within timeout');
276
339
  console.error('');
@@ -321,41 +384,33 @@ export function createAgentTestCommand() {
321
384
  process.exit(1);
322
385
  }
323
386
  }
324
- else if (options.mode === 'jsonl') {
325
- // JSONL input mode: line-by-line processing
326
- const rl = readline.createInterface({
327
- input: process.stdin,
328
- output: process.stdout,
329
- terminal: false,
330
- });
331
- let lineNumber = 0;
387
+ else if (options.mode === 'jsonl' && jsonlInputs) {
388
+ // JSONL input mode: line-by-line processing (inputs already parsed and validated)
332
389
  let processedCount = 0;
333
390
  let lastEventId;
334
- for await (const line of rl) {
335
- lineNumber++;
336
- if (!line.trim())
337
- 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
+ }
338
396
  try {
339
- const jsonInput = JSON.parse(line);
340
- if (!quiet) {
341
- console.error(`Processing line ${lineNumber}...`);
342
- }
343
397
  // Send message
344
398
  await client.post(`/sessions/${session.id}/events`, {
345
399
  mode: 'json',
346
400
  content: jsonInput,
347
401
  });
348
402
  // Wait for response (looking for events after last seen)
349
- const result = await pollForResponseWithEvents(client, session.id, eventFilter, lastEventId, 180000);
403
+ const result = eventFilter
404
+ ? await pollForResponseWithEvents(client, session.id, eventFilter, lastEventId, 180000)
405
+ : await pollForResponse(client, session.id, lastEventId, 180000);
350
406
  lastEventId = result.lastEventId;
351
407
  if (!result.response) {
352
- console.error(`Timeout: No response for line ${lineNumber}`);
408
+ console.error(`Timeout: No response for input ${inputIndex + 1}`);
353
409
  continue;
354
410
  }
355
411
  const response = result.response;
356
412
  // Output response
357
413
  if (quiet) {
358
- // In quiet mode, output just the message content (or as JSON if --json is used)
359
414
  console.log(response);
360
415
  }
361
416
  else {
@@ -365,10 +420,8 @@ export function createAgentTestCommand() {
365
420
  processedCount++;
366
421
  }
367
422
  catch (error) {
368
- const err = error;
369
- console.error(`Error: Invalid JSON on line ${lineNumber}`);
370
- console.error(`Failed to parse JSON: ${err.message}`);
371
- console.error('Skipping line and continuing...');
423
+ const formattedErr = handleAxiosError(error);
424
+ console.error(`Error processing input ${inputIndex + 1}: ${formattedErr.details}`);
372
425
  }
373
426
  }
374
427
  if (!quiet) {
@@ -425,4 +478,9 @@ export function createAgentTestCommand() {
425
478
  });
426
479
  return cmd;
427
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
+ }
428
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',