@guildai/cli 0.11.0 → 0.12.0

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 (198) hide show
  1. package/dist/auth-CRMO5O3N.js +29 -0
  2. package/dist/auth-CRMO5O3N.js.map +7 -0
  3. package/dist/chat-5VX2WJH2.js +303 -0
  4. package/dist/chat-5VX2WJH2.js.map +7 -0
  5. package/dist/chat-SIKDYZQK.js +31 -0
  6. package/dist/chat-SIKDYZQK.js.map +7 -0
  7. package/dist/chunk-56YCMGL3.js +522 -0
  8. package/dist/chunk-56YCMGL3.js.map +7 -0
  9. package/dist/chunk-6EX6E7WP.js +7042 -0
  10. package/dist/chunk-6EX6E7WP.js.map +7 -0
  11. package/dist/chunk-B7VAF5UG.js +532 -0
  12. package/dist/chunk-B7VAF5UG.js.map +7 -0
  13. package/dist/chunk-DOIYVBNY.js +3057 -0
  14. package/dist/chunk-DOIYVBNY.js.map +7 -0
  15. package/dist/chunk-ENKEEJ45.js +17 -0
  16. package/dist/chunk-ENKEEJ45.js.map +7 -0
  17. package/dist/chunk-IBRKVGMZ.js +97041 -0
  18. package/dist/chunk-IBRKVGMZ.js.map +7 -0
  19. package/dist/chunk-LFMQJOKC.js +19778 -0
  20. package/dist/chunk-LFMQJOKC.js.map +7 -0
  21. package/dist/chunk-M347HP6M.js +22896 -0
  22. package/dist/chunk-M347HP6M.js.map +7 -0
  23. package/dist/chunk-OYQ476FQ.js +44 -0
  24. package/dist/chunk-OYQ476FQ.js.map +7 -0
  25. package/dist/chunk-PNCUR4OB.js +257 -0
  26. package/dist/chunk-PNCUR4OB.js.map +7 -0
  27. package/dist/chunk-RIG2HZWM.js +317 -0
  28. package/dist/chunk-RIG2HZWM.js.map +7 -0
  29. package/dist/chunk-SPZPZXUN.js +826 -0
  30. package/dist/chunk-SPZPZXUN.js.map +7 -0
  31. package/dist/chunk-VVSOU6ON.js +53 -0
  32. package/dist/chunk-VVSOU6ON.js.map +7 -0
  33. package/dist/chunk-X3ADGWOF.js +3643 -0
  34. package/dist/chunk-X3ADGWOF.js.map +7 -0
  35. package/dist/commands/skill/create.d.ts +3 -0
  36. package/dist/commands/skill/get.d.ts +3 -0
  37. package/dist/commands/skill/list.d.ts +3 -0
  38. package/dist/commands/skill/update.d.ts +3 -0
  39. package/dist/commands/skill/version/create.d.ts +3 -0
  40. package/dist/commands/skill/version/get.d.ts +3 -0
  41. package/dist/commands/skill/version/list.d.ts +3 -0
  42. package/dist/devtools-AO7YSDOD.js +67 -0
  43. package/dist/devtools-AO7YSDOD.js.map +7 -0
  44. package/dist/dist-4CBK6X5H.js +1566 -0
  45. package/dist/dist-4CBK6X5H.js.map +7 -0
  46. package/dist/esm-FRAVZP4J.js +13 -0
  47. package/dist/esm-FRAVZP4J.js.map +7 -0
  48. package/dist/execa-XQMWSABC.js +35 -0
  49. package/dist/execa-XQMWSABC.js.map +7 -0
  50. package/dist/index.js +8230 -263
  51. package/dist/index.js.map +7 -0
  52. package/dist/lib/api-types.d.ts +44 -0
  53. package/dist/lib/config.d.ts +9 -0
  54. package/dist/lib/errors.d.ts +1 -1
  55. package/dist/lib/output.d.ts +11 -1
  56. package/dist/lib/session-events.d.ts +1 -1
  57. package/dist/lib/session-polling.d.ts +24 -1
  58. package/dist/lib/websocket-client.d.ts +46 -0
  59. package/dist/open-RF4X5MOP.js +13 -0
  60. package/dist/open-RF4X5MOP.js.map +7 -0
  61. package/dist/server-JYVH64FD.js +27659 -0
  62. package/dist/server-JYVH64FD.js.map +7 -0
  63. package/dist/test-SNIYRJ32.js +692 -0
  64. package/dist/test-SNIYRJ32.js.map +7 -0
  65. package/docs/skills/codex-agent-dev.md +2 -2
  66. package/package.json +8 -12
  67. package/dist/commands/agent/chat.js +0 -281
  68. package/dist/commands/agent/clone.js +0 -118
  69. package/dist/commands/agent/code.js +0 -87
  70. package/dist/commands/agent/fork.js +0 -220
  71. package/dist/commands/agent/get.js +0 -37
  72. package/dist/commands/agent/grep.js +0 -107
  73. package/dist/commands/agent/init.js +0 -403
  74. package/dist/commands/agent/list.js +0 -110
  75. package/dist/commands/agent/logs.js +0 -62
  76. package/dist/commands/agent/owners.js +0 -74
  77. package/dist/commands/agent/publish.js +0 -91
  78. package/dist/commands/agent/pull.js +0 -194
  79. package/dist/commands/agent/revalidate.js +0 -56
  80. package/dist/commands/agent/save.js +0 -345
  81. package/dist/commands/agent/search.js +0 -61
  82. package/dist/commands/agent/tags/add.js +0 -73
  83. package/dist/commands/agent/tags/list.js +0 -43
  84. package/dist/commands/agent/tags/remove.js +0 -84
  85. package/dist/commands/agent/tags/set.js +0 -71
  86. package/dist/commands/agent/test.js +0 -489
  87. package/dist/commands/agent/unpublish.js +0 -64
  88. package/dist/commands/agent/update.js +0 -118
  89. package/dist/commands/agent/versions.js +0 -55
  90. package/dist/commands/agent/workspaces.js +0 -54
  91. package/dist/commands/auth/login.js +0 -31
  92. package/dist/commands/auth/logout.js +0 -24
  93. package/dist/commands/auth/status.js +0 -38
  94. package/dist/commands/auth/token.js +0 -19
  95. package/dist/commands/chat.js +0 -1416
  96. package/dist/commands/config/get.js +0 -64
  97. package/dist/commands/config/list.js +0 -46
  98. package/dist/commands/config/path.js +0 -37
  99. package/dist/commands/config/set.js +0 -132
  100. package/dist/commands/credentials/endpoint-list.js +0 -88
  101. package/dist/commands/credentials/list.js +0 -50
  102. package/dist/commands/credentials/policy-create.js +0 -66
  103. package/dist/commands/credentials/policy-delete.js +0 -33
  104. package/dist/commands/credentials/policy-list.js +0 -45
  105. package/dist/commands/credentials/policy-update.js +0 -66
  106. package/dist/commands/doctor.js +0 -233
  107. package/dist/commands/integration/connect.js +0 -76
  108. package/dist/commands/integration/create.js +0 -298
  109. package/dist/commands/integration/get.js +0 -95
  110. package/dist/commands/integration/list.js +0 -62
  111. package/dist/commands/integration/operation/create.js +0 -164
  112. package/dist/commands/integration/operation/list.js +0 -92
  113. package/dist/commands/integration/update.js +0 -139
  114. package/dist/commands/integration/version/build.js +0 -86
  115. package/dist/commands/integration/version/create.js +0 -45
  116. package/dist/commands/integration/version/get.js +0 -72
  117. package/dist/commands/integration/version/list.js +0 -45
  118. package/dist/commands/integration/version/publish.js +0 -79
  119. package/dist/commands/integration/version/test.js +0 -104
  120. package/dist/commands/job/get-step.js +0 -40
  121. package/dist/commands/job/get.js +0 -44
  122. package/dist/commands/mcp.js +0 -34
  123. package/dist/commands/session/create.js +0 -59
  124. package/dist/commands/session/events.js +0 -56
  125. package/dist/commands/session/get.js +0 -33
  126. package/dist/commands/session/interrupt.js +0 -33
  127. package/dist/commands/session/list.js +0 -59
  128. package/dist/commands/session/send.js +0 -54
  129. package/dist/commands/session/tasks.js +0 -45
  130. package/dist/commands/setup.js +0 -260
  131. package/dist/commands/trigger/activate.js +0 -41
  132. package/dist/commands/trigger/create.js +0 -197
  133. package/dist/commands/trigger/deactivate.js +0 -41
  134. package/dist/commands/trigger/get.js +0 -33
  135. package/dist/commands/trigger/list.js +0 -57
  136. package/dist/commands/trigger/sessions.js +0 -48
  137. package/dist/commands/trigger/update.js +0 -128
  138. package/dist/commands/version.js +0 -24
  139. package/dist/commands/workspace/agent/add.js +0 -114
  140. package/dist/commands/workspace/agent/list.js +0 -78
  141. package/dist/commands/workspace/agent/remove.js +0 -78
  142. package/dist/commands/workspace/clear.js +0 -45
  143. package/dist/commands/workspace/context/edit.js +0 -107
  144. package/dist/commands/workspace/context/get.js +0 -47
  145. package/dist/commands/workspace/context/list.js +0 -51
  146. package/dist/commands/workspace/context/publish.js +0 -42
  147. package/dist/commands/workspace/create.js +0 -51
  148. package/dist/commands/workspace/current.js +0 -63
  149. package/dist/commands/workspace/get.js +0 -39
  150. package/dist/commands/workspace/list.js +0 -70
  151. package/dist/commands/workspace/select.js +0 -184
  152. package/dist/components/AgentInstallPrompt.js +0 -97
  153. package/dist/components/SplashAnimation.js +0 -321
  154. package/dist/components/TaskView.js +0 -268
  155. package/dist/lib/agent-helpers.js +0 -306
  156. package/dist/lib/alternate-screen.js +0 -59
  157. package/dist/lib/api-client.js +0 -154
  158. package/dist/lib/api-types.js +0 -10
  159. package/dist/lib/auth.js +0 -284
  160. package/dist/lib/braille-canvas.js +0 -321
  161. package/dist/lib/colors.js +0 -46
  162. package/dist/lib/config-cache.js +0 -45
  163. package/dist/lib/config.js +0 -153
  164. package/dist/lib/did-you-mean.js +0 -144
  165. package/dist/lib/errors.js +0 -375
  166. package/dist/lib/event-filter.js +0 -91
  167. package/dist/lib/generated-types.js +0 -56
  168. package/dist/lib/git.js +0 -176
  169. package/dist/lib/gk.js +0 -91
  170. package/dist/lib/guild-config.js +0 -178
  171. package/dist/lib/iap.js +0 -117
  172. package/dist/lib/integration-helpers.js +0 -38
  173. package/dist/lib/loading-messages.js +0 -72
  174. package/dist/lib/logo.js +0 -141
  175. package/dist/lib/lottie-serverside.js +0 -181
  176. package/dist/lib/markdown.js +0 -38
  177. package/dist/lib/npmrc.js +0 -59
  178. package/dist/lib/output-mode.js +0 -54
  179. package/dist/lib/output.js +0 -622
  180. package/dist/lib/owner-helpers.js +0 -112
  181. package/dist/lib/polling.js +0 -76
  182. package/dist/lib/progress.js +0 -324
  183. package/dist/lib/session-events-fetch.js +0 -25
  184. package/dist/lib/session-events.js +0 -126
  185. package/dist/lib/session-polling.js +0 -166
  186. package/dist/lib/session-resume.js +0 -229
  187. package/dist/lib/spinners.js +0 -770
  188. package/dist/lib/splash.js +0 -42
  189. package/dist/lib/stdin.js +0 -91
  190. package/dist/lib/svg-to-braille.js +0 -76
  191. package/dist/lib/table.js +0 -59
  192. package/dist/lib/update-check.js +0 -65
  193. package/dist/lib/validate-input-schema.js +0 -208
  194. package/dist/lib/version-helpers.js +0 -137
  195. package/dist/lib/workspace-helpers.js +0 -49
  196. package/dist/mcp/resources.js +0 -67
  197. package/dist/mcp/server.js +0 -64
  198. package/dist/mcp/tools.js +0 -753
@@ -1,1416 +0,0 @@
1
- // Copyright 2026 Guild.ai
2
- // SPDX-License-Identifier: Apache-2.0
3
- import React, { useState, useEffect, useRef } from 'react';
4
- import { Box, Text, Static, render, useInput, useApp } from 'ink';
5
- import { Command, Option } from 'commander';
6
- import { getAuthToken } from '../lib/auth.js';
7
- import { GuildAPIClient } from '../lib/api-client.js';
8
- import { handleAxiosError, ErrorCodes, debug, isDebugMode, retry, } from '../lib/errors.js';
9
- import { createSpinner, format } from '../lib/progress.js';
10
- import { marked } from 'marked';
11
- import { markedTerminal } from 'marked-terminal';
12
- import chalk from 'chalk';
13
- import { readFileSync } from 'fs';
14
- import path from 'path';
15
- import { fileURLToPath } from 'url';
16
- import { isUnfulfilledAgentInstallRequest, isFilteredTaskName, getTaskDisplayName, getAgentName, getAgentNotificationText, applyResponseStreamText, isDoneResponseStreamEvent, isResponseStreamEvent, isRootTaskEvent, } from '../lib/session-events.js';
17
- import { printResumeHint, fetchSession, fetchSessionEvents, prepareSessionResumeDisplay, } from '../lib/session-resume.js';
18
- import { DEFAULT_EVENT_TYPES, parseEventFilter, shouldShowEvent, } from '../lib/event-filter.js';
19
- import { fetchEvents, fetchTasks } from '../lib/session-events-fetch.js';
20
- import { AgentInstallPrompt } from '../components/AgentInstallPrompt.js';
21
- import { getWorkspaceId, getWorkspaceSourceLabel } from '../lib/guild-config.js';
22
- import { ensureInteractiveStdin } from '../lib/stdin.js';
23
- import { brand, BRAND_COLOR, code as codeColor, hyperlink } from '../lib/colors.js';
24
- import { SplashAnimation } from '../components/SplashAnimation.js';
25
- import { LOADING_TIMINGS } from '../lib/loading-messages.js';
26
- import { suppressScrollbackClear } from '../lib/alternate-screen.js';
27
- import open from 'open';
28
- import { TaskView, hasActiveTasks } from '../components/TaskView.js';
29
- import { getOutputMode, isQuietMode } from '../lib/output-mode.js';
30
- // ESM equivalent of __dirname
31
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
- // Read version from package.json
33
- const packageJson = JSON.parse(readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
34
- // ---------------------------------------------------------------------------
35
- // Workspace error types
36
- // ---------------------------------------------------------------------------
37
- /** Thrown when no workspace is configured (no --workspace flag, no config). */
38
- export class WorkspaceNotConfiguredError extends Error {
39
- constructor() {
40
- super('No workspace configured.');
41
- this.name = 'WorkspaceNotConfiguredError';
42
- }
43
- }
44
- /** Thrown when the specified workspace ID is not found in the backend. */
45
- export class WorkspaceNotFoundError extends Error {
46
- workspaceId;
47
- constructor(workspaceId) {
48
- super(`Workspace ${workspaceId} not found.`);
49
- this.name = 'WorkspaceNotFoundError';
50
- this.workspaceId = workspaceId;
51
- }
52
- }
53
- /** User-facing error messages for workspace resolution failures. */
54
- const WORKSPACE_NOT_CONFIGURED_MSG = 'No workspace configured. Pass a --workspace <id_or_name> argument or run guild workspace select';
55
- const WORKSPACE_NOT_FOUND_MSG = "The workspace doesn't exist.";
56
- // Configure marked for terminal
57
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
- marked.use(markedTerminal({}, { theme: {} }));
59
- /**
60
- * Post-process markdown to fix unrendered inline markdown in list items.
61
- * marked-terminal has a bug where it uses parse() instead of parseInline()
62
- * for list items, leaving **bold** and `code` unrendered.
63
- */
64
- function fixListItemMarkdown(text) {
65
- // Handle bold/strong: **text** -> bold text
66
- text = text.replace(/\*\*([^*]+)\*\*/g, (_, content) => chalk.bold(content));
67
- // Handle inline code: `text` -> highlighted text
68
- text = text.replace(/`([^`]+)`/g, (_, content) => codeColor(content));
69
- // Handle italic: _text_ -> italic (underscore style, less ambiguous)
70
- text = text.replace(/(?<![\\w])_([^_]+)_(?![\\w])/g, (_, content) => chalk.italic(content));
71
- return text;
72
- }
73
- function extractMessageText(text) {
74
- const trimmed = text.trim();
75
- if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
76
- return text;
77
- try {
78
- const parsed = JSON.parse(trimmed);
79
- if (typeof parsed === 'object' && parsed !== null && 'message' in parsed) {
80
- return typeof parsed.message === 'string' ? parsed.message : text;
81
- }
82
- }
83
- catch {
84
- // If parsing fails, use the content as-is.
85
- }
86
- return text;
87
- }
88
- function extractRuntimeDoneText(content) {
89
- if (content === null || content === undefined)
90
- return null;
91
- if (typeof content === 'string')
92
- return content.trim() ? content : null;
93
- if (typeof content !== 'object' || Array.isArray(content)) {
94
- return JSON.stringify(content);
95
- }
96
- const record = content;
97
- if (Object.keys(record).length === 0)
98
- return null;
99
- if (record.type === 'text') {
100
- if (typeof record.text === 'string')
101
- return record.text;
102
- if (typeof record.data === 'string')
103
- return record.data;
104
- }
105
- if (typeof record.message === 'string')
106
- return record.message;
107
- // runtime_done content is agent output, not notification content. Preserve
108
- // structured outputs rather than trying to render notification-only shapes.
109
- return JSON.stringify(content);
110
- }
111
- function renderAssistantMessage(text, taskName) {
112
- const rendered = fixListItemMarkdown(marked.parse(text));
113
- return `${chalk.green('●')} ${chalk.bold(taskName)}\n${rendered.trim()}`;
114
- }
115
- function applyResponseStreamContentsInSequence(contents) {
116
- return [...contents]
117
- .sort((left, right) => left.sequence - right.sequence)
118
- .reduce((currentText, content) => applyResponseStreamText(currentText, content), '');
119
- }
120
- /**
121
- * Output the result of a --once mode session.
122
- * Handles both JSON and human-readable output formats.
123
- */
124
- async function outputOnceResult(sessionId, events, mode) {
125
- if (mode === 'json') {
126
- console.log(JSON.stringify({ session_id: sessionId, events }, null, 2));
127
- }
128
- else {
129
- const finalAgentMessages = events.filter((e) => e.type === 'agent_notification_message' &&
130
- isRootTaskEvent(e) &&
131
- !isResponseStreamEvent(e));
132
- const responseStreamContents = new Map();
133
- const doneStreamIds = [];
134
- for (const event of events) {
135
- if (!isResponseStreamEvent(event) || !isRootTaskEvent(event))
136
- continue;
137
- const streamContents = responseStreamContents.get(event.content.stream_id) ?? [];
138
- streamContents.push(event.content);
139
- responseStreamContents.set(event.content.stream_id, streamContents);
140
- if (isDoneResponseStreamEvent(event)) {
141
- doneStreamIds.push(event.content.stream_id);
142
- }
143
- }
144
- if (finalAgentMessages.length > 0) {
145
- const messageContent = extractMessageText(getAgentNotificationText(finalAgentMessages[finalAgentMessages.length - 1]));
146
- const rendered = fixListItemMarkdown(await marked(messageContent));
147
- console.log(rendered.trim());
148
- return;
149
- }
150
- const runtimeDoneEvents = events.filter((e) => e.type === 'runtime_done' && isRootTaskEvent(e) && e.content);
151
- const runtimeDoneWithContent = runtimeDoneEvents[runtimeDoneEvents.length - 1];
152
- if (runtimeDoneWithContent?.type === 'runtime_done') {
153
- const runtimeDoneText = extractRuntimeDoneText(runtimeDoneWithContent.content);
154
- if (runtimeDoneText) {
155
- console.log(runtimeDoneText);
156
- return;
157
- }
158
- }
159
- const lastDoneStreamId = doneStreamIds[doneStreamIds.length - 1];
160
- const streamFallbackText = lastDoneStreamId !== undefined
161
- ? applyResponseStreamContentsInSequence(responseStreamContents.get(lastDoneStreamId) ?? [])
162
- : null;
163
- if (streamFallbackText !== null) {
164
- const rendered = fixListItemMarkdown(await marked(streamFallbackText));
165
- console.log(rendered.trim());
166
- }
167
- }
168
- }
169
- import { createSpinner as createAnimatedSpinner, createRandomSpinner, } from '../lib/spinners.js';
170
- function CustomInput({ value, onChange, onSubmit, trackedTasksSize, setShowTaskPanel, isActive, }) {
171
- useInput((input, key) => {
172
- // Ctrl-T: toggle task panel
173
- if (key.ctrl && input === 't' && trackedTasksSize > 0) {
174
- setShowTaskPanel((prev) => !prev);
175
- return;
176
- }
177
- // Enter: submit
178
- if (key.return) {
179
- onSubmit(value);
180
- return;
181
- }
182
- // Backspace: delete character
183
- if (key.backspace || key.delete) {
184
- onChange(value.slice(0, -1));
185
- return;
186
- }
187
- // Ctrl+C, Ctrl+D: handled at parent level
188
- if (key.ctrl && (input === 'c' || input === 'd')) {
189
- return;
190
- }
191
- // Regular character: append to input
192
- if (!key.ctrl && !key.meta && input) {
193
- onChange(value + input);
194
- }
195
- }, { isActive });
196
- // Render cursor using chalk.inverse() like ink-text-input does
197
- // This creates a block cursor that adapts to the user's terminal theme
198
- const renderedValue = value + chalk.inverse(' ');
199
- return React.createElement(Text, null, renderedValue);
200
- }
201
- function InputWrapper({ isReady, isInterrupted, input, setInput, handleSubmit, trackedTasksSize, setShowTaskPanel, isActive, }) {
202
- if (isInterrupted) {
203
- return (React.createElement(Box, { height: 1 },
204
- React.createElement(Text, { color: "gray" }, chalk.dim('Interrupted — this session cannot be resumed. Press Ctrl+C to exit.'))));
205
- }
206
- return (React.createElement(Box, { height: 1 },
207
- React.createElement(Text, { color: isReady ? BRAND_COLOR : 'gray' }, "> "),
208
- isReady && isActive ? (React.createElement(CustomInput, { value: input, onChange: setInput, onSubmit: handleSubmit, trackedTasksSize: trackedTasksSize, setShowTaskPanel: setShowTaskPanel, isActive: isActive })) : isReady ? (React.createElement(Text, null, input)) : (React.createElement(Text, null, chalk.dim('(connecting...)')))));
209
- }
210
- export function ChatApp({ initialPrompt, version, workspaceId, versionId, agentName, showSplash = true, resumeSession, resumeEvents, resumeCommand, openDashboard, eventFilter, }) {
211
- const { exit } = useApp();
212
- const isResuming = !!resumeSession;
213
- const [phase, setPhase] = useState(isResuming || !showSplash ? 'chat' : 'splash');
214
- const [splashStatus, setSplashStatus] = useState('Initializing');
215
- const [connectedSession, setConnectedSession] = useState(resumeSession || null);
216
- const [connectedClient, setConnectedClient] = useState(isResuming ? new GuildAPIClient() : null);
217
- const [firstMessageReceived, setFirstMessageReceived] = useState(isResuming);
218
- const connectionAttempted = useRef(isResuming);
219
- const splashStartTime = useRef(Date.now());
220
- // Transition from finalizing to chat after a brief delay to clear screen
221
- useEffect(() => {
222
- if (phase === 'finalizing') {
223
- const timeout = setTimeout(() => {
224
- setPhase('chat');
225
- }, 50); // Brief delay to render the blank frame
226
- return () => clearTimeout(timeout);
227
- }
228
- }, [phase]);
229
- // Transition from splash to finalizing when first message is received
230
- useEffect(() => {
231
- if (!firstMessageReceived || phase !== 'splash')
232
- return;
233
- // Ensure minimum splash duration for animation to complete
234
- const elapsed = Date.now() - splashStartTime.current;
235
- const remainingTime = Math.max(0, LOADING_TIMINGS.minLoadingDwell - elapsed);
236
- setTimeout(() => {
237
- setPhase('finalizing');
238
- }, remainingTime);
239
- }, [firstMessageReceived, phase]);
240
- // Timeout: transition to chat 2 seconds after session is created
241
- // Don't make users stare at splash for long-running prompts
242
- useEffect(() => {
243
- if (!connectedSession || phase === 'chat')
244
- return;
245
- const timeout = setTimeout(() => {
246
- // If still on splash after 2 seconds of session creation, switch to finalizing
247
- // so user can see progress logs and task activity
248
- if (phase === 'splash') {
249
- setPhase('finalizing');
250
- }
251
- }, 2000);
252
- return () => clearTimeout(timeout);
253
- }, [connectedSession, phase]);
254
- // Start connection attempt during splash
255
- useEffect(() => {
256
- if (connectionAttempted.current)
257
- return;
258
- connectionAttempted.current = true;
259
- const connect = async () => {
260
- try {
261
- setSplashStatus('Connecting to Guild servers');
262
- const client = new GuildAPIClient();
263
- setSplashStatus('Creating session');
264
- const session = await retry(() => createSession(client, workspaceId, initialPrompt, versionId, setSplashStatus), {
265
- maxAttempts: 20,
266
- initialDelay: 500,
267
- maxDelay: 5000,
268
- backoffMultiplier: 1.5,
269
- shouldRetry: (error) => {
270
- if (typeof error === 'object' && error !== null) {
271
- const err = error;
272
- return (err.code === 'ECONNREFUSED' ||
273
- err.code === 'ETIMEDOUT' ||
274
- err.code === 'ENOTFOUND' ||
275
- err.code === 'ECONNABORTED');
276
- }
277
- return false;
278
- },
279
- });
280
- setConnectedClient(client);
281
- setConnectedSession(session);
282
- setSplashStatus('Waiting for response');
283
- // Open dashboard in browser if requested
284
- if (openDashboard && session.session_url) {
285
- open(session.session_url).catch(() => {
286
- // Ignore errors opening browser
287
- });
288
- }
289
- // Don't transition to chat yet - wait for first message from ChatUIWithConnection
290
- }
291
- catch (error) {
292
- debug('Connection error during splash:', error);
293
- // Exit Ink first to stop rendering, then clear screen.
294
- // Use ESC[H + ESC[J (cursor home + erase below) instead of ESC[2J
295
- // to avoid pushing the last splash frame into scrollback.
296
- exit();
297
- process.stdout.write('\x1b[H\x1b[J');
298
- // Get error details and provide clear, actionable guidance
299
- const formattedError = handleAxiosError(error);
300
- const details = formattedError.details.toLowerCase();
301
- // One message, one action - use consistent format from progress.ts
302
- if (formattedError.code === ErrorCodes.AUTH_REQUIRED ||
303
- formattedError.code === ErrorCodes.AUTH_TOKEN_INVALID) {
304
- format.error('Not authenticated. Run: guild auth login');
305
- }
306
- else if (error instanceof WorkspaceNotConfiguredError) {
307
- format.error(WORKSPACE_NOT_CONFIGURED_MSG);
308
- }
309
- else if (error instanceof WorkspaceNotFoundError) {
310
- format.error(WORKSPACE_NOT_FOUND_MSG);
311
- }
312
- else if (details.includes('agent')) {
313
- format.error('Agent not found in workspace.');
314
- }
315
- else {
316
- format.error(formattedError.details);
317
- }
318
- process.exit(1);
319
- }
320
- };
321
- connect();
322
- }, [workspaceId, initialPrompt, versionId]);
323
- const chatInstanceKey = `${connectedSession?.id ?? 'pending'}:${resumeEvents?.[resumeEvents.length - 1]?.id ?? 'live'}`;
324
- // Render both splash and chat, but only show one at a time
325
- // ChatUIWithConnection is always mounted (when connected) so it can stream events in background
326
- return (React.createElement(React.Fragment, null,
327
- (phase === 'splash' || phase === 'finalizing') && showSplash && (React.createElement(SplashAnimation, { status: splashStatus, version: version, isFinalizing: phase === 'finalizing', onComplete: () => {
328
- // Animation complete callback (not used for transition anymore)
329
- }, onEscapePress: () => {
330
- // User pressed Esc to skip splash screen - only allow if connected
331
- if (connectedSession && connectedClient) {
332
- process.stdout.write('\x1b[H\x1b[J'); // Clear screen without pushing to scrollback
333
- setPhase('chat');
334
- }
335
- // If not connected yet, ignore escape (let connection complete)
336
- } })),
337
- phase === 'chat' && (React.createElement(ChatUIWithConnection, { key: chatInstanceKey, initialPrompt: initialPrompt, version: version, versionId: versionId, agentName: agentName, client: connectedClient, session: connectedSession, onFirstMessage: () => setFirstMessageReceived(true), resumeEvents: resumeEvents, resumeCommand: resumeCommand, eventFilter: eventFilter })),
338
- (phase === 'splash' || phase === 'finalizing') &&
339
- connectedSession &&
340
- connectedClient && (React.createElement(Box, { display: "none" },
341
- React.createElement(ChatUIWithConnection, { initialPrompt: initialPrompt, version: version, versionId: versionId, agentName: agentName, client: connectedClient, session: connectedSession, onFirstMessage: () => setFirstMessageReceived(true), isActive: false, eventFilter: eventFilter })))));
342
- }
343
- function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _versionId, agentName, client: preConnectedClient, session: preConnectedSession, onFirstMessage, isActive = true, resumeEvents, resumeCommand, eventFilter, }) {
344
- const activeFilter = eventFilter ?? DEFAULT_EVENT_TYPES;
345
- // Note: We handle SIGINT directly via process.on, not using useApp().exit
346
- // Task panel state - managed at this level to handle keyboard input before TextInput
347
- // Default to showing task panel (Ctrl-T to toggle)
348
- const [showTaskPanel, setShowTaskPanel] = useState(true);
349
- const [tasks, setTasks] = useState([]);
350
- // Interactive prompt state for agent install requests
351
- const [pendingInstallRequest, setPendingInstallRequest] = useState(null);
352
- const promptedEventIds = useRef(new Set());
353
- // No header needed - version is shown in splash finale
354
- // Only include initial prompt when active (not during splash)
355
- // Static component writes to stdout even with display="none"
356
- // When resuming, show past events instead of initial prompt
357
- const [resumeDisplay] = useState(() => resumeEvents ? prepareSessionResumeDisplay(resumeEvents) : null);
358
- const resumeDisplayMessages = resumeDisplay?.displayMessages ?? null;
359
- const responseStreamResumeState = resumeDisplay?.responseStreamState ?? null;
360
- const sessionLinkMessage = isActive && preConnectedSession?.session_url
361
- ? [
362
- {
363
- key: 'session-link',
364
- content: chalk.dim(`Session: ${hyperlink(preConnectedSession.id, preConnectedSession.session_url)}`),
365
- type: 'progress',
366
- },
367
- ]
368
- : [];
369
- const [messages, setMessages] = useState(resumeDisplayMessages
370
- ? resumeDisplayMessages
371
- : isActive
372
- ? [
373
- ...sessionLinkMessage,
374
- {
375
- key: 'initial',
376
- content: `${brand('>')} ${initialPrompt}`,
377
- type: 'user',
378
- },
379
- ]
380
- : []);
381
- const [input, setInput] = useState('');
382
- const inputTextRef = useRef('');
383
- // Keep ref in sync so useInput handler can read current input text
384
- const updateInput = (value) => {
385
- inputTextRef.current = value;
386
- setInput(value);
387
- };
388
- const [currentOperation, setCurrentOperation] = useState(resumeEvents ? '' : 'Waiting for response...');
389
- const [exitHint, setExitHint] = useState(null);
390
- const [isInterrupted, setIsInterrupted] = useState(false);
391
- // Double-tap exit tracking (shared for Ctrl+C and Ctrl+D)
392
- const exitKeyPressed = useRef(false);
393
- const exitKeyTimeout = useRef(null);
394
- // Interrupt tracking - prevents duplicate interrupt requests
395
- const isInterrupting = useRef(false);
396
- // Debounce Escape after mount so splash-skip doesn't trigger interrupt
397
- const mountedAt = useRef(Date.now());
398
- // Track terminal size for layout calculations
399
- // Debounce resize to prevent spam during rapid resize events
400
- const [terminalSize, setTerminalSize] = useState({
401
- width: process.stdout.columns || 80,
402
- height: process.stdout.rows || 24,
403
- });
404
- const resizeTimeout = useRef(null);
405
- useEffect(() => {
406
- const handleResize = () => {
407
- // Debounce: only update after resize events stop for 100ms
408
- if (resizeTimeout.current) {
409
- clearTimeout(resizeTimeout.current);
410
- }
411
- resizeTimeout.current = setTimeout(() => {
412
- setTerminalSize({
413
- width: process.stdout.columns || 80,
414
- height: process.stdout.rows || 24,
415
- });
416
- }, 100);
417
- };
418
- process.stdout.on('resize', handleResize);
419
- return () => {
420
- process.stdout.off('resize', handleResize);
421
- if (resizeTimeout.current) {
422
- clearTimeout(resizeTimeout.current);
423
- }
424
- };
425
- }, []);
426
- // Global keyboard shortcuts
427
- useInput((input, key) => {
428
- // Escape: Interrupt agent while processing
429
- // Ignore Escape within 300ms of mount to prevent splash-skip from triggering interrupt
430
- if (key.escape &&
431
- client &&
432
- session &&
433
- currentOperation &&
434
- !isInterrupting.current &&
435
- Date.now() - mountedAt.current > 300) {
436
- isInterrupting.current = true;
437
- client
438
- .post(`/sessions/${session.id}/interrupt`, {})
439
- .then(() => {
440
- debug('Session interrupted');
441
- })
442
- .catch((err) => {
443
- debug('Interrupt failed (session may already be done):', err);
444
- })
445
- .finally(() => {
446
- isInterrupting.current = false;
447
- });
448
- return;
449
- }
450
- // Ctrl-C: Clear input on first press (if text present), double-tap to exit
451
- const isCtrlC = input === '\x03' || (key.ctrl && input === 'c');
452
- if (isCtrlC) {
453
- // If there's text in the input, clear it instead of showing exit hint
454
- if (inputTextRef.current && !exitKeyPressed.current) {
455
- updateInput('');
456
- return;
457
- }
458
- if (exitKeyPressed.current) {
459
- if (preConnectedSession?.id && resumeCommand) {
460
- printResumeHint(preConnectedSession.id, resumeCommand);
461
- }
462
- process.exit(0);
463
- }
464
- exitKeyPressed.current = true;
465
- setExitHint('ctrl-c');
466
- if (exitKeyTimeout.current)
467
- clearTimeout(exitKeyTimeout.current);
468
- exitKeyTimeout.current = setTimeout(() => {
469
- exitKeyPressed.current = false;
470
- setExitHint(null);
471
- }, 2000);
472
- return;
473
- }
474
- // Ctrl-D: Double-tap to exit
475
- const isCtrlD = input === '\x04' || (key.ctrl && input === 'd');
476
- if (isCtrlD) {
477
- if (exitKeyPressed.current) {
478
- if (preConnectedSession?.id && resumeCommand) {
479
- printResumeHint(preConnectedSession.id, resumeCommand);
480
- }
481
- process.exit(0);
482
- }
483
- exitKeyPressed.current = true;
484
- setExitHint('ctrl-d');
485
- if (exitKeyTimeout.current)
486
- clearTimeout(exitKeyTimeout.current);
487
- exitKeyTimeout.current = setTimeout(() => {
488
- exitKeyPressed.current = false;
489
- setExitHint(null);
490
- }, 2000);
491
- return;
492
- }
493
- });
494
- // Clean up exit key timeout on unmount
495
- useEffect(() => {
496
- return () => {
497
- if (exitKeyTimeout.current) {
498
- clearTimeout(exitKeyTimeout.current);
499
- }
500
- };
501
- }, []);
502
- // Use pre-established connection directly from props (no state needed)
503
- // This ensures we react to parent updates if user escapes splash before connection completes
504
- const client = preConnectedClient;
505
- const session = preConnectedSession;
506
- const isReady = session !== null;
507
- // Stateful spinner instance
508
- const animatedSpinner = useRef(null);
509
- const spinnerSwapInterval = useRef(null);
510
- const spinnerTheme = process.env.GUILD_SPINNER_THEME;
511
- const isRandomMode = spinnerTheme === 'random';
512
- if (!animatedSpinner.current) {
513
- animatedSpinner.current = createAnimatedSpinner();
514
- }
515
- const [spinnerFrame, setSpinnerFrame] = useState(() => animatedSpinner.current?.tick() || '');
516
- const sentMessages = useRef(new Set());
517
- const pollInterval = useRef(null);
518
- const spinnerInterval = useRef(null);
519
- const lastEventIdRef = useRef(resumeEvents?.length ? resumeEvents[resumeEvents.length - 1].id : undefined);
520
- const isPolling = useRef(false);
521
- const receivedResponseSinceLastInput = useRef(false);
522
- const firstMessageNotified = useRef(!!resumeEvents);
523
- const responseStreamKeys = useRef(responseStreamResumeState?.keys ?? new Map());
524
- const responseStreamContents = useRef(responseStreamResumeState?.contents ?? new Map());
525
- const responseStreamTexts = useRef(responseStreamResumeState?.texts ?? new Map());
526
- const responseStreamTimestamps = useRef(responseStreamResumeState?.timestamps ?? new Map());
527
- const responseStreamStatuses = useRef(responseStreamResumeState?.statuses ??
528
- new Map());
529
- const responseStreamKeysByTask = useRef(responseStreamResumeState?.keysByTask ?? new Map());
530
- const clearResponseStreamsForTask = (taskId, options = {}) => {
531
- if (!taskId)
532
- return;
533
- const keys = responseStreamKeysByTask.current.get(taskId);
534
- if (!keys?.size)
535
- return;
536
- // Removed keys disappear from the transcript; detached keys stay rendered
537
- // but are no longer tracked as active streams for future cleanup.
538
- const removedKeys = new Set();
539
- const detachedKeys = new Set();
540
- for (const [streamId, key] of responseStreamKeys.current.entries()) {
541
- if (keys.has(key)) {
542
- if (options.preserveContinued &&
543
- responseStreamStatuses.current.get(streamId) === 'continued' &&
544
- responseStreamTexts.current.get(streamId) !== options.finalText) {
545
- responseStreamKeys.current.delete(streamId);
546
- responseStreamContents.current.delete(streamId);
547
- responseStreamTexts.current.delete(streamId);
548
- responseStreamTimestamps.current.delete(streamId);
549
- responseStreamStatuses.current.delete(streamId);
550
- detachedKeys.add(key);
551
- continue;
552
- }
553
- responseStreamKeys.current.delete(streamId);
554
- responseStreamContents.current.delete(streamId);
555
- responseStreamTexts.current.delete(streamId);
556
- responseStreamTimestamps.current.delete(streamId);
557
- responseStreamStatuses.current.delete(streamId);
558
- removedKeys.add(key);
559
- }
560
- }
561
- const inactiveKeys = new Set([...removedKeys, ...detachedKeys]);
562
- const remainingKeys = new Set([...keys].filter((key) => !inactiveKeys.has(key)));
563
- if (remainingKeys.size > 0) {
564
- responseStreamKeysByTask.current.set(taskId, remainingKeys);
565
- }
566
- else {
567
- responseStreamKeysByTask.current.delete(taskId);
568
- }
569
- setMessages((prev) => prev.filter((message) => !removedKeys.has(message.key)));
570
- };
571
- const upsertResponseStreamMessage = (event) => {
572
- if (!isResponseStreamEvent(event))
573
- return;
574
- if (!isRootTaskEvent(event))
575
- return;
576
- const streamId = event.content.stream_id;
577
- const taskId = event.task?.id;
578
- const existingKey = responseStreamKeys.current.get(streamId);
579
- if (event.content.status === 'aborted') {
580
- responseStreamContents.current.delete(streamId);
581
- responseStreamTexts.current.delete(streamId);
582
- responseStreamTimestamps.current.delete(streamId);
583
- responseStreamStatuses.current.delete(streamId);
584
- if (existingKey) {
585
- responseStreamKeys.current.delete(streamId);
586
- if (taskId) {
587
- const keys = responseStreamKeysByTask.current.get(taskId);
588
- keys?.delete(existingKey);
589
- if (keys?.size === 0)
590
- responseStreamKeysByTask.current.delete(taskId);
591
- }
592
- setMessages((prev) => prev.filter((message) => message.key !== existingKey));
593
- }
594
- return;
595
- }
596
- const streamContents = responseStreamContents.current.get(streamId) ?? [];
597
- const existingContentIndex = streamContents.findIndex((content) => content.sequence === event.content.sequence);
598
- if (existingContentIndex === -1) {
599
- streamContents.push(event.content);
600
- }
601
- else {
602
- streamContents[existingContentIndex] = event.content;
603
- }
604
- responseStreamContents.current.set(streamId, streamContents);
605
- const text = applyResponseStreamContentsInSequence(streamContents);
606
- if (!text.trim())
607
- return;
608
- const key = existingKey ?? `response-stream-${streamId}`;
609
- responseStreamKeys.current.set(streamId, key);
610
- responseStreamTexts.current.set(streamId, text);
611
- responseStreamStatuses.current.set(streamId, event.content.status);
612
- if (taskId) {
613
- const keys = responseStreamKeysByTask.current.get(taskId) ?? new Set();
614
- keys.add(key);
615
- responseStreamKeysByTask.current.set(taskId, keys);
616
- }
617
- const taskName = agentName || 'assistant';
618
- const messageContent = renderAssistantMessage(text, taskName);
619
- const timestamp = responseStreamTimestamps.current.get(streamId) ?? new Date().toLocaleTimeString();
620
- responseStreamTimestamps.current.set(streamId, timestamp);
621
- setMessages((prev) => {
622
- const index = prev.findIndex((message) => message.key === key);
623
- const message = {
624
- key,
625
- content: messageContent,
626
- type: 'assistant',
627
- timestamp,
628
- };
629
- if (index === -1)
630
- return [...prev, message];
631
- const next = [...prev];
632
- next[index] = message;
633
- return next;
634
- });
635
- if (!firstMessageNotified.current && onFirstMessage) {
636
- firstMessageNotified.current = true;
637
- onFirstMessage();
638
- }
639
- if (event.content.status === 'done') {
640
- receivedResponseSinceLastInput.current = true;
641
- setCurrentOperation('');
642
- }
643
- };
644
- // Mark initial prompt as sent (skip for resume — we already have the events)
645
- useEffect(() => {
646
- if (!resumeEvents) {
647
- sentMessages.current.add(initialPrompt);
648
- receivedResponseSinceLastInput.current = false;
649
- }
650
- }, [initialPrompt, resumeEvents]);
651
- // Check if there are active tasks (for spinner logic)
652
- const hasActiveTasksNow = hasActiveTasks(tasks);
653
- // Spinner animation - run when there's an operation OR active tasks
654
- useEffect(() => {
655
- if (!currentOperation && !hasActiveTasksNow)
656
- return;
657
- spinnerInterval.current = setInterval(() => {
658
- if (animatedSpinner.current) {
659
- setSpinnerFrame(animatedSpinner.current.tick());
660
- }
661
- }, 50);
662
- return () => {
663
- if (spinnerInterval.current) {
664
- clearInterval(spinnerInterval.current);
665
- spinnerInterval.current = null;
666
- }
667
- };
668
- }, [currentOperation, hasActiveTasksNow]);
669
- // Random mode: swap spinners periodically
670
- useEffect(() => {
671
- if (!isRandomMode)
672
- return;
673
- spinnerSwapInterval.current = setInterval(() => {
674
- animatedSpinner.current = createRandomSpinner();
675
- }, 5000 + Math.random() * 5000);
676
- return () => {
677
- if (spinnerSwapInterval.current) {
678
- clearInterval(spinnerSwapInterval.current);
679
- spinnerSwapInterval.current = null;
680
- }
681
- };
682
- }, [isRandomMode]);
683
- // Poll for tasks (like web's useGetSessionTasksQuery)
684
- const isPollingTasks = useRef(false);
685
- useEffect(() => {
686
- if (!client || !session || !isReady)
687
- return;
688
- const pollTasks = async () => {
689
- if (isPollingTasks.current)
690
- return;
691
- isPollingTasks.current = true;
692
- try {
693
- const tasksList = await fetchTasks(client, session.id);
694
- setTasks(tasksList);
695
- }
696
- catch (error) {
697
- debug('Tasks poll error:', error);
698
- }
699
- finally {
700
- isPollingTasks.current = false;
701
- }
702
- };
703
- // Poll immediately and then every 2 seconds
704
- pollTasks();
705
- const interval = setInterval(pollTasks, 2000);
706
- return () => {
707
- clearInterval(interval);
708
- };
709
- }, [client, session, isReady]);
710
- // Poll for messages
711
- useEffect(() => {
712
- if (!client || !session || !isReady)
713
- return;
714
- const poll = async () => {
715
- if (isPolling.current)
716
- return;
717
- isPolling.current = true;
718
- debug(`poll() called, session=${session.id}, fromId=${lastEventIdRef.current}`);
719
- try {
720
- const newEvents = await fetchEvents(client, session.id, {
721
- fromId: lastEventIdRef.current,
722
- });
723
- debug(`Events: fromId=${lastEventIdRef.current}, new=${newEvents.length}`);
724
- if (newEvents.length > 0) {
725
- lastEventIdRef.current = newEvents[newEvents.length - 1].id;
726
- for (const event of newEvents) {
727
- // Track task from event if present
728
- const taskInfo = event.task;
729
- // Debug: log ALL events with task info
730
- if (taskInfo) {
731
- const taskName = 'agent' in taskInfo
732
- ? getAgentName(taskInfo.agent)
733
- : 'tool_name' in taskInfo
734
- ? taskInfo.tool_name || 'task'
735
- : 'unknown';
736
- debug(`Event: ${event.type}, task=${taskName}:${taskInfo.id.substring(0, 8)}, status=${taskInfo.status}`);
737
- }
738
- else {
739
- debug(`Event: ${event.type} (no task info)`);
740
- }
741
- // Process events that affect the chat UI (task state comes from tasks poll)
742
- if (event.type === 'runtime_error') {
743
- clearResponseStreamsForTask(taskInfo?.id);
744
- // Always clear the spinner on runtime errors so the UI doesn't get stuck
745
- setCurrentOperation('');
746
- // Show runtime errors in the chat (gated on --events filter)
747
- if (shouldShowEvent('runtime_error', activeFilter)) {
748
- const errorText = typeof event.content === 'string' ? event.content : 'Unknown error';
749
- const taskName = agentName || 'assistant';
750
- setMessages((prev) => [
751
- ...prev,
752
- {
753
- key: `error-${Date.now()}`,
754
- content: `${chalk.red('●')} ${chalk.bold(taskName)}\n${chalk.red(`Error: ${errorText}`)}`,
755
- type: 'assistant',
756
- },
757
- ]);
758
- }
759
- }
760
- else if (event.type === 'runtime_start') {
761
- if (shouldShowEvent('runtime_start', activeFilter)) {
762
- setMessages((prev) => [
763
- ...prev,
764
- {
765
- key: `runtime-start-${Date.now()}`,
766
- content: chalk.dim('[runtime/start]'),
767
- type: 'assistant',
768
- },
769
- ]);
770
- }
771
- }
772
- else if (event.type === 'runtime_running') {
773
- if (shouldShowEvent('runtime_running', activeFilter)) {
774
- setMessages((prev) => [
775
- ...prev,
776
- {
777
- key: `runtime-running-${Date.now()}`,
778
- content: chalk.dim('[runtime/running]'),
779
- type: 'assistant',
780
- },
781
- ]);
782
- }
783
- }
784
- else if (event.type === 'runtime_waiting') {
785
- if (shouldShowEvent('runtime_waiting', activeFilter)) {
786
- setMessages((prev) => [
787
- ...prev,
788
- {
789
- key: `runtime-waiting-${Date.now()}`,
790
- content: chalk.dim('[runtime/waiting]'),
791
- type: 'assistant',
792
- },
793
- ]);
794
- }
795
- }
796
- else if (event.type === 'trigger_message') {
797
- if (shouldShowEvent('trigger_message', activeFilter)) {
798
- const triggerText = typeof event.content === 'object' ? event.content?.data || '' : '';
799
- setMessages((prev) => [
800
- ...prev,
801
- {
802
- key: `trigger-${Date.now()}`,
803
- content: `${chalk.cyan('[trigger]')} ${triggerText}`,
804
- type: 'assistant',
805
- },
806
- ]);
807
- }
808
- }
809
- else if (event.type === 'system_error') {
810
- if (shouldShowEvent('system_error', activeFilter)) {
811
- const errText = typeof event.content === 'object' ? event.content?.data || '' : '';
812
- setMessages((prev) => [
813
- ...prev,
814
- {
815
- key: `system-error-${Date.now()}`,
816
- content: `${chalk.red('[system_error]')} ${errText}`,
817
- type: 'assistant',
818
- },
819
- ]);
820
- }
821
- }
822
- else if (event.type === 'llm_start') {
823
- if (shouldShowEvent('llm_start', activeFilter)) {
824
- setMessages((prev) => [
825
- ...prev,
826
- {
827
- key: `llm-start-${Date.now()}`,
828
- content: chalk.dim(`[llm_start] provider:${event.provider}`),
829
- type: 'assistant',
830
- },
831
- ]);
832
- }
833
- }
834
- else if (event.type === 'llm_done') {
835
- if (shouldShowEvent('llm_done', activeFilter)) {
836
- setMessages((prev) => [
837
- ...prev,
838
- {
839
- key: `llm-done-${Date.now()}`,
840
- content: chalk.dim(`[llm_done] HTTP ${event.status_code}`),
841
- type: 'assistant',
842
- },
843
- ]);
844
- }
845
- }
846
- else if (event.type === 'agent_notification_progress') {
847
- // Update status line with progress text (task tracking is done by tasks poll)
848
- const rawProgressText = typeof event.content === 'string'
849
- ? event.content
850
- : event.content?.data || '';
851
- // Skip internal task names as progress (e.g., "ui_prompt" when waiting for input)
852
- if (rawProgressText && !isFilteredTaskName(rawProgressText)) {
853
- setCurrentOperation(rawProgressText);
854
- }
855
- }
856
- else if (event.type === 'agent_notification_message') {
857
- if (isResponseStreamEvent(event)) {
858
- upsertResponseStreamMessage(event);
859
- continue;
860
- }
861
- const text = extractMessageText(getAgentNotificationText(event));
862
- if (text.trim()) {
863
- clearResponseStreamsForTask(taskInfo?.id, {
864
- preserveContinued: true,
865
- finalText: text,
866
- });
867
- const taskName = agentName || 'assistant';
868
- const messageContent = renderAssistantMessage(text, taskName);
869
- setMessages((prev) => [
870
- ...prev,
871
- {
872
- key: `msg-${Date.now()}-${Math.random()}`,
873
- content: messageContent,
874
- type: 'assistant',
875
- timestamp: new Date().toLocaleTimeString(),
876
- },
877
- ]);
878
- // Notify parent that first message has arrived (for splash screen transition)
879
- if (!firstMessageNotified.current && onFirstMessage) {
880
- firstMessageNotified.current = true;
881
- onFirstMessage();
882
- }
883
- }
884
- receivedResponseSinceLastInput.current = true;
885
- setCurrentOperation('');
886
- }
887
- else if (event.type === 'agent_notification_error') {
888
- clearResponseStreamsForTask(taskInfo?.id);
889
- // Show error in chat (task status is updated via tasks poll)
890
- const errorText = typeof event.content === 'string'
891
- ? event.content
892
- : event.content?.data || 'Unknown error';
893
- const taskName = agentName || 'assistant';
894
- setMessages((prev) => [
895
- ...prev,
896
- {
897
- key: `error-${Date.now()}`,
898
- content: `${chalk.red('●')} ${chalk.bold(taskName)}\n${chalk.red(`Error: ${errorText}`)}`,
899
- type: 'assistant',
900
- },
901
- ]);
902
- setCurrentOperation('');
903
- }
904
- else if (event.type === 'agent_console') {
905
- // Show console logs when enabled via --events or --debug
906
- // --debug continues to show console logs for backwards compatibility
907
- if (shouldShowEvent('agent_console', activeFilter) || isDebugMode()) {
908
- const content = typeof event.content === 'string' ? event.content : '';
909
- setMessages((prev) => [
910
- ...prev,
911
- {
912
- key: `console-${Date.now()}-${Math.random()}`,
913
- content: chalk.dim(`[console.${event.level}] ${content}`),
914
- type: 'assistant',
915
- },
916
- ]);
917
- }
918
- }
919
- else if (event.type === 'runtime_done') {
920
- if (shouldShowEvent('runtime_done', activeFilter)) {
921
- // Show runtime_done as a system event when enabled via --events
922
- setMessages((prev) => [
923
- ...prev,
924
- {
925
- key: `runtime-done-${Date.now()}`,
926
- content: chalk.dim('[runtime/done]'),
927
- type: 'assistant',
928
- },
929
- ]);
930
- }
931
- if (!receivedResponseSinceLastInput.current &&
932
- event.content !== undefined &&
933
- taskInfo &&
934
- 'agent' in taskInfo) {
935
- // One-shot agents may complete with runtime_done without sending
936
- // agent_notification_message. Display the output if we haven't
937
- // already shown a response for this input cycle.
938
- const contentStr = typeof event.content === 'string'
939
- ? event.content
940
- : JSON.stringify(event.content);
941
- clearResponseStreamsForTask(taskInfo.id, {
942
- preserveContinued: true,
943
- finalText: contentStr,
944
- });
945
- if (contentStr && contentStr !== '{}' && contentStr !== 'null') {
946
- const rendered = fixListItemMarkdown(marked.parse(contentStr));
947
- const taskName = agentName || 'assistant';
948
- const messageContent = `${chalk.green('●')} ${chalk.bold(taskName)}\n${rendered.trim()}`;
949
- setMessages((prev) => [
950
- ...prev,
951
- {
952
- key: `msg-${Date.now()}-${Math.random()}`,
953
- content: messageContent,
954
- type: 'assistant',
955
- timestamp: new Date().toLocaleTimeString(),
956
- },
957
- ]);
958
- if (!firstMessageNotified.current && onFirstMessage) {
959
- firstMessageNotified.current = true;
960
- onFirstMessage();
961
- }
962
- receivedResponseSinceLastInput.current = true;
963
- setCurrentOperation('');
964
- }
965
- }
966
- }
967
- else if (event.type === 'interrupted') {
968
- clearResponseStreamsForTask(taskInfo?.id);
969
- // Session was interrupted — interrupted sessions are terminal on the backend
970
- setMessages((prev) => [
971
- ...prev,
972
- {
973
- key: `interrupted-${Date.now()}`,
974
- content: chalk.dim('⊘ Interrupted'),
975
- type: 'assistant',
976
- },
977
- ]);
978
- setCurrentOperation('');
979
- setIsInterrupted(true);
980
- }
981
- else if (isUnfulfilledAgentInstallRequest(event)) {
982
- // Check for agent install requests that need user approval
983
- if (!promptedEventIds.current.has(event.id) && !pendingInstallRequest) {
984
- debug(`Found unfulfilled agent install request: ${event.id}`);
985
- promptedEventIds.current.add(event.id);
986
- setPendingInstallRequest(event);
987
- }
988
- }
989
- }
990
- }
991
- }
992
- catch (error) {
993
- debug('Polling error:', error);
994
- }
995
- finally {
996
- isPolling.current = false;
997
- }
998
- };
999
- pollInterval.current = setInterval(poll, 2000);
1000
- poll();
1001
- return () => {
1002
- if (pollInterval.current) {
1003
- clearInterval(pollInterval.current);
1004
- pollInterval.current = null;
1005
- }
1006
- };
1007
- }, [client, session, isReady, pendingInstallRequest]);
1008
- // Handle message submission
1009
- const handleSubmit = async (value) => {
1010
- const text = value.trim();
1011
- if (text === 'exit' || text === 'quit') {
1012
- if (preConnectedSession?.id && resumeCommand) {
1013
- printResumeHint(preConnectedSession.id, resumeCommand);
1014
- }
1015
- process.exit(0);
1016
- }
1017
- if (!text || !client || !session || !isReady)
1018
- return;
1019
- // Track sent messages to filter echoes from backend
1020
- sentMessages.current.add(value);
1021
- // Add user message to display immediately
1022
- setMessages((prev) => [
1023
- ...prev,
1024
- {
1025
- key: `user-${Date.now()}`,
1026
- content: `${brand('>')} ${value}`,
1027
- type: 'user',
1028
- },
1029
- ]);
1030
- // Clear input right after showing the message
1031
- updateInput('');
1032
- try {
1033
- await client.post(`/sessions/${session.id}/events`, {
1034
- content: value,
1035
- });
1036
- receivedResponseSinceLastInput.current = false;
1037
- setCurrentOperation('Waiting for response...');
1038
- }
1039
- catch (error) {
1040
- debug('Send error:', error);
1041
- setMessages((prev) => [
1042
- ...prev,
1043
- {
1044
- key: `error-${Date.now()}`,
1045
- content: chalk.red('Failed to send message'),
1046
- type: 'assistant',
1047
- },
1048
- ]);
1049
- }
1050
- };
1051
- // Active tasks for spinner (excluding filtered internal tasks like ui_prompt)
1052
- const activeTasksList = tasks.filter((t) => (t.status === 'CREATED' ||
1053
- t.status === 'STARTED' ||
1054
- t.status === 'RUNNING' ||
1055
- t.status === 'WAITING') &&
1056
- !isFilteredTaskName(getTaskDisplayName(t)));
1057
- const activeTaskCount = activeTasksList.length;
1058
- // Calculate status line - only show when we have an active operation
1059
- const statusLine = (() => {
1060
- // No operation = idle = no spinner
1061
- if (!currentOperation) {
1062
- return '';
1063
- }
1064
- if (activeTaskCount > 0) {
1065
- // Find the most recently updated active task
1066
- const mostRecentTask = activeTasksList.reduce((a, b) => new Date(b.updated_at) > new Date(a.updated_at) ? b : a);
1067
- if (!mostRecentTask) {
1068
- return `${brand(spinnerFrame)} ${currentOperation}`;
1069
- }
1070
- const taskName = 'agent' in mostRecentTask
1071
- ? getAgentName(mostRecentTask.agent)
1072
- : 'tool_name' in mostRecentTask
1073
- ? mostRecentTask.tool_name || 'task'
1074
- : 'assistant';
1075
- // Hide elapsed time for root tasks (no parent) - they run for entire session
1076
- const isRootTask = !mostRecentTask.parent_task;
1077
- const elapsed = Math.floor((Date.now() - new Date(mostRecentTask.created_at).getTime()) / 1000);
1078
- const elapsedText = isRootTask ? '' : ` ${chalk.dim(`(${elapsed}s)`)}`;
1079
- // Avoid redundancy: if operation already mentions the task, just show operation
1080
- const showTaskName = !currentOperation.includes(taskName);
1081
- if (showTaskName) {
1082
- return `${brand(spinnerFrame)} ${taskName} · ${currentOperation}${elapsedText}`;
1083
- }
1084
- return `${brand(spinnerFrame)} ${currentOperation}${elapsedText}`;
1085
- }
1086
- // No active tasks but we have an operation (e.g., "Waiting for response...")
1087
- return `${brand(spinnerFrame)} ${currentOperation}`;
1088
- })();
1089
- const terminalWidth = terminalSize.width;
1090
- // Content always uses full width - drawer overlays on top
1091
- const contentWidth = terminalWidth;
1092
- // Drawer doesn't need fixed height - it will match the message container
1093
- // Ctrl-T hints for toggling task view
1094
- const hideTasksHintText = '[ctrl-t to hide tasks]';
1095
- const hideTasksHint = `[${chalk.bold('ctrl-t')} to hide tasks]`;
1096
- const showTasksHintText = '[ctrl-t to show tasks]';
1097
- const showTasksHint = `[${chalk.bold('ctrl-t')} to show tasks]`;
1098
- // Build status line with right-aligned hint
1099
- // Priority: exit hint > esc to interrupt > ctrl-t task hint
1100
- // Strip ANSI codes to get visible length (statusLine contains spinner with color codes)
1101
- const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
1102
- const statusWithHint = (() => {
1103
- let hintText = null;
1104
- let hint = null;
1105
- if (exitHint) {
1106
- hintText = `[${exitHint} again to exit]`;
1107
- hint = `[${chalk.bold(exitHint)} again to exit]`;
1108
- }
1109
- else if (currentOperation) {
1110
- hintText = '(esc to interrupt)';
1111
- hint = `(${chalk.bold('esc')} to interrupt)`;
1112
- }
1113
- if (!hintText && activeTaskCount > 0 && currentOperation) {
1114
- hintText = showTaskPanel ? hideTasksHintText : showTasksHintText;
1115
- hint = showTaskPanel ? hideTasksHint : showTasksHint;
1116
- }
1117
- if (hintText && hint) {
1118
- const visibleStatusLength = stripAnsi(statusLine).length;
1119
- const hintLength = hintText.length;
1120
- const totalNeeded = visibleStatusLength + hintLength + 4; // +4 for spacing buffer
1121
- if (totalNeeded <= contentWidth) {
1122
- // Right-align hint with padding
1123
- const padding = contentWidth - visibleStatusLength - hintLength - 2; // -2 for buffer
1124
- return statusLine + ' '.repeat(Math.max(1, padding)) + chalk.dim(hint);
1125
- }
1126
- }
1127
- return statusLine;
1128
- })();
1129
- // Handler for approving agent installation
1130
- const handleApproveInstall = async () => {
1131
- if (!pendingInstallRequest || !client || !session?.workspace_id) {
1132
- throw new Error('Missing required data for agent installation');
1133
- }
1134
- const agentId = pendingInstallRequest.requested_agent.id;
1135
- const eventId = pendingInstallRequest.id;
1136
- const workspaceId = session.workspace_id;
1137
- debug(`Approving agent install: agent=${agentId}, event=${eventId}, workspace=${workspaceId}`);
1138
- await client.post(`/workspaces/${workspaceId}/agents`, {
1139
- agent_id: agentId,
1140
- event_id: eventId,
1141
- });
1142
- };
1143
- // Handler for declining agent installation
1144
- const handleDeclineInstall = async () => {
1145
- if (!pendingInstallRequest || !client) {
1146
- throw new Error('Missing required data for declining installation');
1147
- }
1148
- const eventId = pendingInstallRequest.id;
1149
- debug(`Declining agent install: event=${eventId}`);
1150
- await client.delete(`/events/${eventId}`);
1151
- };
1152
- // Handler for when install prompt is complete
1153
- const handleInstallComplete = () => {
1154
- setPendingInstallRequest(null);
1155
- };
1156
- return (React.createElement(Box, { flexDirection: "column" },
1157
- React.createElement(Static, { items: messages }, (msg, index) => (React.createElement(Box, { key: msg.key, flexDirection: "column", marginTop: msg.type === 'user' && index > 0 ? 1 : 0 },
1158
- React.createElement(Text, null, msg.content)))),
1159
- showTaskPanel && tasks.length > 0 && currentOperation && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
1160
- React.createElement(TaskView, { tasks: tasks }))),
1161
- pendingInstallRequest && (React.createElement(AgentInstallPrompt, { event: pendingInstallRequest, onApprove: handleApproveInstall, onDecline: handleDeclineInstall, onComplete: handleInstallComplete })),
1162
- React.createElement(Box, { height: 1, marginTop: 1 },
1163
- React.createElement(Text, null, statusWithHint)),
1164
- React.createElement(Box, { height: 1 },
1165
- React.createElement(Text, { color: "gray" }, '─'.repeat(Math.max(1, terminalWidth - 2)))),
1166
- React.createElement(InputWrapper, { isReady: isReady, isInterrupted: isInterrupted, input: input, setInput: updateInput, handleSubmit: handleSubmit, trackedTasksSize: tasks.length, setShowTaskPanel: setShowTaskPanel, isActive: isActive })));
1167
- }
1168
- export async function ensureAuthenticated() {
1169
- const token = await getAuthToken();
1170
- if (!token) {
1171
- format.error('Not authenticated. Run: guild auth login');
1172
- process.exit(1);
1173
- }
1174
- // Validate token against the server to catch expired/invalid tokens
1175
- // before any UI (splash, Ink) renders.
1176
- try {
1177
- const client = new GuildAPIClient();
1178
- await client.get('/me');
1179
- }
1180
- catch {
1181
- // Token is expired or invalid — clear it and exit
1182
- const { clearAuthToken } = await import('../lib/auth.js');
1183
- await clearAuthToken();
1184
- format.error('Session expired. Run: guild auth login');
1185
- process.exit(1);
1186
- }
1187
- return token;
1188
- }
1189
- export async function createSession(client, workspaceId, initialPrompt, versionId, onProgress) {
1190
- const progress = onProgress || (() => { });
1191
- if (!workspaceId) {
1192
- // Check for workspace in local (guild.json) or global (~/.guild/config.json) config
1193
- progress('Loading config');
1194
- const resolved = await getWorkspaceId();
1195
- if (resolved) {
1196
- workspaceId = resolved.workspaceId;
1197
- const sourceLabel = getWorkspaceSourceLabel(resolved.source);
1198
- if (sourceLabel) {
1199
- progress(`Using workspace from ${sourceLabel}`);
1200
- }
1201
- }
1202
- }
1203
- if (!workspaceId) {
1204
- throw new WorkspaceNotConfiguredError();
1205
- }
1206
- progress('Creating session');
1207
- const sessionData = {
1208
- initial_prompt: initialPrompt,
1209
- session_type: 'chat',
1210
- };
1211
- if (versionId) {
1212
- // API field is "agent_id" but the server expects a version identifier
1213
- // (resolved by gen_agent_version_id_for). Raw agent UUIDs are not supported.
1214
- sessionData.agent_id = versionId;
1215
- }
1216
- let response;
1217
- try {
1218
- response = await client.post(`/workspaces/${workspaceId}/sessions`, sessionData);
1219
- }
1220
- catch (error) {
1221
- const err = handleAxiosError(error);
1222
- if (err.code === ErrorCodes.NOT_FOUND) {
1223
- throw new WorkspaceNotFoundError(workspaceId);
1224
- }
1225
- throw error;
1226
- }
1227
- if (!response) {
1228
- throw new Error('Failed to create session');
1229
- }
1230
- // Include the workspace_id we used to create the session
1231
- // (backend may return workspace object instead of workspace_id string)
1232
- return { ...response, workspace_id: workspaceId };
1233
- }
1234
- export function createChatCommand() {
1235
- const cmd = new Command('chat');
1236
- cmd
1237
- .description('Chat with an agent (default: Guild assistant)')
1238
- .argument('[prompt...]', 'Optional initial prompt (multiple words)')
1239
- .option('--agent <identifier>', 'Agent ID or full name, e.g., foo~bar (default: assistant)')
1240
- .option('--once', 'One-shot mode: send message, wait for response, exit (non-interactive)')
1241
- .option('--workspace <identifier>', 'Workspace ID or full name (e.g., owner/workspace-name)')
1242
- .option('--no-splash', 'Skip the splash screen animation')
1243
- .option('--resume <session-id>', 'Resume an existing session')
1244
- .option('--events <types>', 'Event types to show (default: user). Shorthands: none, user, system, all, or comma-separated type names (e.g. agent_console,llm_start)')
1245
- // Accept --mode so `guild chat --mode json` works when re-parsed.
1246
- // The actual value is read from process.argv by getOutputMode().
1247
- .addOption(new Option('--mode <format>').hideHelp())
1248
- .addHelpText('after', '\nTo chat with a local agent under development: guild agent chat')
1249
- .action(async (promptArgs, options) => {
1250
- const initialPrompt = promptArgs.length > 0 ? promptArgs.join(' ') : 'Hello';
1251
- const eventFilter = options.events
1252
- ? parseEventFilter(options.events)
1253
- : DEFAULT_EVENT_TYPES;
1254
- if (options.once) {
1255
- // --once mode: use old spinner-based approach
1256
- const spinner = createSpinner('Connecting to Guild servers...');
1257
- spinner.start();
1258
- try {
1259
- await ensureAuthenticated();
1260
- const client = new GuildAPIClient();
1261
- const session = await createSession(client, options.workspace, initialPrompt, options.agent, (status) => {
1262
- spinner.text = status;
1263
- });
1264
- spinner.succeed('Connected');
1265
- if (session.session_url) {
1266
- const sessionLink = hyperlink(session.id, session.session_url);
1267
- console.error(chalk.dim(`Session: ${sessionLink}`));
1268
- }
1269
- console.error('');
1270
- // Non-interactive mode: send message, wait for response, output JSON
1271
- // Poll for messages until we get an agent MESSAGE response
1272
- // Timeout after 5 minutes of INACTIVITY (no new messages)
1273
- // Agent initialization can take 1-2 minutes with no events
1274
- const inactivityTimeoutMs = 300000; // 5 minutes
1275
- const pollIntervalMs = 2000;
1276
- const maxInactivityAttempts = inactivityTimeoutMs / pollIntervalMs;
1277
- let lastSeenEventId;
1278
- const allEvents = [];
1279
- let inactivityCounter = 0;
1280
- while (true) {
1281
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1282
- const newEvents = await fetchEvents(client, session.id, {
1283
- fromId: lastSeenEventId,
1284
- });
1285
- if (newEvents.length > 0) {
1286
- for (const evt of newEvents) {
1287
- const taskInfo = evt.task;
1288
- if (taskInfo) {
1289
- debug(`Event: ${evt.type}, task=${taskInfo.agent || taskInfo.tool_name || 'unknown'}:${(taskInfo.id || '').substring(0, 8)}, status=${taskInfo.status}`);
1290
- }
1291
- else {
1292
- debug(`Event: ${evt.type} (no task info)`);
1293
- }
1294
- }
1295
- allEvents.push(...newEvents);
1296
- lastSeenEventId = newEvents[newEvents.length - 1].id;
1297
- inactivityCounter = 0;
1298
- }
1299
- else {
1300
- inactivityCounter++;
1301
- }
1302
- // Check if we got a completion response from the root agent.
1303
- // Stream done events are only a rendering fallback; wait for the
1304
- // final root message or runtime completion so child drafts cannot
1305
- // terminate --once early.
1306
- const hasRootTaskDone = allEvents.some((e) => e.type === 'runtime_done' && isRootTaskEvent(e));
1307
- const hasAgentMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
1308
- isRootTaskEvent(e) &&
1309
- !isResponseStreamEvent(e));
1310
- const hasRootTaskError = allEvents.some((e) => e.type === 'runtime_error' && isRootTaskEvent(e));
1311
- // Check for a ui_prompt request... that ends the game.
1312
- const hasUIPromptMessage = allEvents.some((e) => e.type === 'agent_notification_message' &&
1313
- !isResponseStreamEvent(e) &&
1314
- e.task?.tool_name === 'ui_prompt');
1315
- if (hasRootTaskError) {
1316
- debug('Found error event from root agent, exiting --once mode');
1317
- const errorEvents = allEvents.filter((e) => e.type === 'runtime_error' || e.type === 'agent_notification_error');
1318
- const outputMode = getOutputMode();
1319
- if (errorEvents.length > 0 && outputMode === 'interactive') {
1320
- const lastError = errorEvents[errorEvents.length - 1];
1321
- const content = lastError.content;
1322
- if (content?.data) {
1323
- console.error(chalk.red(`Error: ${content.data}`));
1324
- }
1325
- else {
1326
- console.error(chalk.red('Agent failed to start'));
1327
- }
1328
- }
1329
- else if (outputMode === 'json') {
1330
- console.log(JSON.stringify({
1331
- session_id: session.id,
1332
- events: allEvents,
1333
- error: true,
1334
- }, null, 2));
1335
- }
1336
- process.exit(1);
1337
- }
1338
- if (hasRootTaskDone || hasAgentMessage || hasUIPromptMessage) {
1339
- debug('Found completion event from root agent, exiting --once mode');
1340
- await outputOnceResult(session.id, allEvents, getOutputMode());
1341
- process.exit(0);
1342
- }
1343
- // Timeout if no activity for too long
1344
- if (inactivityCounter >= maxInactivityAttempts) {
1345
- debug(`Inactivity timeout reached (${maxInactivityAttempts} attempts with no new events)`);
1346
- debug(`Exiting with ${allEvents.length} events total`);
1347
- await outputOnceResult(session.id, allEvents, getOutputMode());
1348
- process.exit(0);
1349
- }
1350
- }
1351
- }
1352
- catch (error) {
1353
- spinner.fail('Connection failed');
1354
- console.error('');
1355
- if (error instanceof WorkspaceNotConfiguredError) {
1356
- format.error(WORKSPACE_NOT_CONFIGURED_MSG);
1357
- process.exit(1);
1358
- }
1359
- if (error instanceof WorkspaceNotFoundError) {
1360
- format.error(WORKSPACE_NOT_FOUND_MSG);
1361
- process.exit(1);
1362
- }
1363
- const formattedError = handleAxiosError(error);
1364
- console.error(`Error: ${formattedError.error}`);
1365
- console.error(formattedError.details);
1366
- // Show suggestions if available
1367
- if (formattedError.suggestions && formattedError.suggestions.length > 0) {
1368
- console.error('');
1369
- console.error('Suggestions:');
1370
- formattedError.suggestions.forEach((suggestion) => {
1371
- console.error(` • ${suggestion}`);
1372
- });
1373
- }
1374
- process.exit(1);
1375
- }
1376
- }
1377
- else {
1378
- // Interactive mode - check auth first, then render UI with splash animation
1379
- ensureInteractiveStdin('guild chat');
1380
- await ensureAuthenticated();
1381
- // Build the resume command string for exit hints
1382
- let resumeCommand = 'guild chat';
1383
- if (options.agent)
1384
- resumeCommand += ` --agent ${options.agent}`;
1385
- // Handle --resume: fetch existing session + events
1386
- let resumeSession;
1387
- let resumeSessionEvents;
1388
- if (options.resume) {
1389
- const client = new GuildAPIClient();
1390
- resumeSession = await fetchSession(client, options.resume);
1391
- resumeSessionEvents = await fetchSessionEvents(client, options.resume);
1392
- }
1393
- // Only show splash screen in truly interactive mode
1394
- // (not --json, not --quiet, not --no-splash, not --resume)
1395
- const isInteractive = getOutputMode() === 'interactive' && !isQuietMode();
1396
- const shouldShowSplash = isInteractive && options.splash !== false && !options.resume;
1397
- // Strip ESC[3J (erase scrollback) from Ink's output to prevent
1398
- // iTerm2 "clear scrollback" warning during fullscreen splash.
1399
- if (shouldShowSplash) {
1400
- suppressScrollbackClear();
1401
- }
1402
- const { waitUntilExit } = render(React.createElement(ChatApp, { initialPrompt: initialPrompt, version: packageJson.version, workspaceId: options.workspace, versionId: options.agent, showSplash: shouldShowSplash, resumeSession: resumeSession, resumeEvents: resumeSessionEvents, resumeCommand: resumeCommand, eventFilter: eventFilter }), {
1403
- exitOnCtrlC: false, // We handle Ctrl-C in useInput (raw mode)
1404
- });
1405
- await waitUntilExit();
1406
- }
1407
- });
1408
- return cmd;
1409
- }
1410
- // Thin wrapper for lazy-loading from index.ts (avoids importing React at startup)
1411
- export async function handleChatAction(_promptArgs, _options) {
1412
- const cmd = createChatCommand();
1413
- // Re-parse the original argv so the command's own action handler runs
1414
- await cmd.parseAsync(process.argv.slice(2), { from: 'user' });
1415
- }
1416
- //# sourceMappingURL=chat.js.map