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