@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.
- package/dist/auth-CRMO5O3N.js +29 -0
- package/dist/auth-CRMO5O3N.js.map +7 -0
- package/dist/chat-5VX2WJH2.js +303 -0
- package/dist/chat-5VX2WJH2.js.map +7 -0
- package/dist/chat-SIKDYZQK.js +31 -0
- package/dist/chat-SIKDYZQK.js.map +7 -0
- package/dist/chunk-56YCMGL3.js +522 -0
- package/dist/chunk-56YCMGL3.js.map +7 -0
- package/dist/chunk-6EX6E7WP.js +7042 -0
- package/dist/chunk-6EX6E7WP.js.map +7 -0
- package/dist/chunk-B7VAF5UG.js +532 -0
- package/dist/chunk-B7VAF5UG.js.map +7 -0
- package/dist/chunk-DOIYVBNY.js +3057 -0
- package/dist/chunk-DOIYVBNY.js.map +7 -0
- package/dist/chunk-ENKEEJ45.js +17 -0
- package/dist/chunk-ENKEEJ45.js.map +7 -0
- package/dist/chunk-IBRKVGMZ.js +97041 -0
- package/dist/chunk-IBRKVGMZ.js.map +7 -0
- package/dist/chunk-LFMQJOKC.js +19778 -0
- package/dist/chunk-LFMQJOKC.js.map +7 -0
- package/dist/chunk-M347HP6M.js +22896 -0
- package/dist/chunk-M347HP6M.js.map +7 -0
- package/dist/chunk-OYQ476FQ.js +44 -0
- package/dist/chunk-OYQ476FQ.js.map +7 -0
- package/dist/chunk-PNCUR4OB.js +257 -0
- package/dist/chunk-PNCUR4OB.js.map +7 -0
- package/dist/chunk-RIG2HZWM.js +317 -0
- package/dist/chunk-RIG2HZWM.js.map +7 -0
- package/dist/chunk-SPZPZXUN.js +826 -0
- package/dist/chunk-SPZPZXUN.js.map +7 -0
- package/dist/chunk-VVSOU6ON.js +53 -0
- package/dist/chunk-VVSOU6ON.js.map +7 -0
- package/dist/chunk-X3ADGWOF.js +3643 -0
- package/dist/chunk-X3ADGWOF.js.map +7 -0
- package/dist/commands/agent/logs.d.ts +3 -0
- package/dist/commands/setup.d.ts +16 -0
- package/dist/commands/skill/create.d.ts +3 -0
- package/dist/commands/skill/get.d.ts +3 -0
- package/dist/commands/skill/list.d.ts +3 -0
- package/dist/commands/skill/update.d.ts +3 -0
- package/dist/commands/skill/version/create.d.ts +3 -0
- package/dist/commands/skill/version/get.d.ts +3 -0
- package/dist/commands/skill/version/list.d.ts +3 -0
- package/dist/devtools-AO7YSDOD.js +67 -0
- package/dist/devtools-AO7YSDOD.js.map +7 -0
- package/dist/dist-4CBK6X5H.js +1566 -0
- package/dist/dist-4CBK6X5H.js.map +7 -0
- package/dist/esm-FRAVZP4J.js +13 -0
- package/dist/esm-FRAVZP4J.js.map +7 -0
- package/dist/execa-XQMWSABC.js +35 -0
- package/dist/execa-XQMWSABC.js.map +7 -0
- package/dist/index.js +8231 -253
- package/dist/index.js.map +7 -0
- package/dist/lib/api-types.d.ts +44 -0
- package/dist/lib/auth.d.ts +1 -1
- package/dist/lib/config.d.ts +9 -0
- package/dist/lib/errors.d.ts +1 -1
- package/dist/lib/output-mode.d.ts +9 -2
- package/dist/lib/output.d.ts +17 -1
- package/dist/lib/session-events.d.ts +14 -3
- package/dist/lib/session-polling.d.ts +24 -1
- package/dist/lib/session-resume.d.ts +15 -1
- package/dist/lib/stdin.d.ts +5 -1
- package/dist/lib/websocket-client.d.ts +46 -0
- package/dist/open-RF4X5MOP.js +13 -0
- package/dist/open-RF4X5MOP.js.map +7 -0
- package/dist/server-JYVH64FD.js +27659 -0
- package/dist/server-JYVH64FD.js.map +7 -0
- package/dist/test-SNIYRJ32.js +692 -0
- package/dist/test-SNIYRJ32.js.map +7 -0
- package/docs/skills/codex-agent-dev.md +2 -2
- package/package.json +8 -12
- package/dist/commands/agent/chat.js +0 -278
- package/dist/commands/agent/clone.js +0 -116
- package/dist/commands/agent/code.js +0 -87
- package/dist/commands/agent/fork.js +0 -218
- package/dist/commands/agent/get.js +0 -37
- package/dist/commands/agent/grep.js +0 -107
- package/dist/commands/agent/init.js +0 -390
- package/dist/commands/agent/list.js +0 -110
- package/dist/commands/agent/owners.js +0 -74
- package/dist/commands/agent/publish.js +0 -91
- package/dist/commands/agent/pull.js +0 -198
- package/dist/commands/agent/revalidate.js +0 -56
- package/dist/commands/agent/save.js +0 -346
- package/dist/commands/agent/search.js +0 -61
- package/dist/commands/agent/tags/add.js +0 -73
- package/dist/commands/agent/tags/list.js +0 -43
- package/dist/commands/agent/tags/remove.js +0 -84
- package/dist/commands/agent/tags/set.js +0 -71
- package/dist/commands/agent/test.js +0 -486
- package/dist/commands/agent/unpublish.js +0 -64
- package/dist/commands/agent/update.js +0 -110
- package/dist/commands/agent/versions.js +0 -55
- package/dist/commands/agent/workspaces.js +0 -54
- package/dist/commands/auth/login.js +0 -33
- package/dist/commands/auth/logout.js +0 -24
- package/dist/commands/auth/status.js +0 -38
- package/dist/commands/auth/token.js +0 -19
- package/dist/commands/chat.js +0 -1345
- package/dist/commands/config/get.js +0 -64
- package/dist/commands/config/list.js +0 -47
- package/dist/commands/config/path.js +0 -38
- package/dist/commands/config/set.js +0 -132
- package/dist/commands/credentials/endpoint-list.js +0 -88
- package/dist/commands/credentials/list.js +0 -50
- package/dist/commands/credentials/policy-create.js +0 -66
- package/dist/commands/credentials/policy-delete.js +0 -33
- package/dist/commands/credentials/policy-list.js +0 -45
- package/dist/commands/credentials/policy-update.js +0 -66
- package/dist/commands/doctor.js +0 -233
- package/dist/commands/integration/connect.js +0 -76
- package/dist/commands/integration/create.js +0 -298
- package/dist/commands/integration/get.js +0 -95
- package/dist/commands/integration/list.js +0 -62
- package/dist/commands/integration/operation/create.js +0 -164
- package/dist/commands/integration/operation/list.js +0 -92
- package/dist/commands/integration/update.js +0 -139
- package/dist/commands/integration/version/build.js +0 -86
- package/dist/commands/integration/version/create.js +0 -45
- package/dist/commands/integration/version/get.js +0 -72
- package/dist/commands/integration/version/list.js +0 -45
- package/dist/commands/integration/version/publish.js +0 -79
- package/dist/commands/integration/version/test.js +0 -104
- package/dist/commands/job/get-step.js +0 -40
- package/dist/commands/job/get.js +0 -44
- package/dist/commands/mcp.js +0 -34
- package/dist/commands/session/create.js +0 -59
- package/dist/commands/session/events.js +0 -56
- package/dist/commands/session/get.js +0 -33
- package/dist/commands/session/interrupt.js +0 -33
- package/dist/commands/session/list.js +0 -59
- package/dist/commands/session/send.js +0 -54
- package/dist/commands/session/tasks.js +0 -45
- package/dist/commands/setup.js +0 -230
- package/dist/commands/trigger/activate.js +0 -41
- package/dist/commands/trigger/create.js +0 -197
- package/dist/commands/trigger/deactivate.js +0 -41
- package/dist/commands/trigger/get.js +0 -33
- package/dist/commands/trigger/list.js +0 -57
- package/dist/commands/trigger/sessions.js +0 -48
- package/dist/commands/trigger/update.js +0 -128
- package/dist/commands/version.js +0 -24
- package/dist/commands/workspace/agent/add.js +0 -114
- package/dist/commands/workspace/agent/list.js +0 -78
- package/dist/commands/workspace/agent/remove.js +0 -78
- package/dist/commands/workspace/clear.js +0 -45
- package/dist/commands/workspace/context/edit.js +0 -107
- package/dist/commands/workspace/context/get.js +0 -47
- package/dist/commands/workspace/context/list.js +0 -51
- package/dist/commands/workspace/context/publish.js +0 -42
- package/dist/commands/workspace/create.js +0 -51
- package/dist/commands/workspace/current.js +0 -63
- package/dist/commands/workspace/get.js +0 -39
- package/dist/commands/workspace/list.js +0 -70
- package/dist/commands/workspace/select.js +0 -184
- package/dist/components/AgentInstallPrompt.js +0 -97
- package/dist/components/SplashAnimation.js +0 -321
- package/dist/components/TaskView.js +0 -268
- package/dist/lib/agent-helpers.js +0 -306
- package/dist/lib/alternate-screen.js +0 -59
- package/dist/lib/api-client.js +0 -154
- package/dist/lib/api-types.js +0 -10
- package/dist/lib/auth.js +0 -284
- package/dist/lib/braille-canvas.js +0 -321
- package/dist/lib/colors.js +0 -46
- package/dist/lib/config-cache.js +0 -45
- package/dist/lib/config.js +0 -153
- package/dist/lib/did-you-mean.js +0 -144
- package/dist/lib/errors.js +0 -375
- package/dist/lib/event-filter.js +0 -91
- package/dist/lib/generated-types.js +0 -56
- package/dist/lib/git.js +0 -176
- package/dist/lib/gk.js +0 -91
- package/dist/lib/guild-config.js +0 -178
- package/dist/lib/iap.js +0 -117
- package/dist/lib/integration-helpers.js +0 -38
- package/dist/lib/loading-messages.js +0 -72
- package/dist/lib/logo.js +0 -141
- package/dist/lib/lottie-serverside.js +0 -181
- package/dist/lib/markdown.js +0 -38
- package/dist/lib/npmrc.js +0 -59
- package/dist/lib/output-mode.js +0 -33
- package/dist/lib/output.js +0 -591
- package/dist/lib/owner-helpers.js +0 -112
- package/dist/lib/polling.js +0 -76
- package/dist/lib/progress.js +0 -324
- package/dist/lib/session-events-fetch.js +0 -25
- package/dist/lib/session-events.js +0 -112
- package/dist/lib/session-polling.js +0 -160
- package/dist/lib/session-resume.js +0 -96
- package/dist/lib/spinners.js +0 -770
- package/dist/lib/splash.js +0 -41
- package/dist/lib/stdin.js +0 -84
- package/dist/lib/svg-to-braille.js +0 -76
- package/dist/lib/table.js +0 -59
- package/dist/lib/update-check.js +0 -65
- package/dist/lib/validate-input-schema.js +0 -208
- package/dist/lib/version-helpers.js +0 -121
- package/dist/lib/workspace-helpers.js +0 -49
- package/dist/mcp/resources.js +0 -67
- package/dist/mcp/server.js +0 -64
- package/dist/mcp/tools.js +0 -753
package/dist/commands/chat.js
DELETED
|
@@ -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
|