@guildai/cli 0.3.14 → 0.3.16
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/commands/chat.js
CHANGED
|
@@ -76,7 +76,11 @@ function CustomInput({ value, onChange, onSubmit, trackedTasksSize, setShowTaskP
|
|
|
76
76
|
const renderedValue = value + chalk.inverse(' ');
|
|
77
77
|
return React.createElement(Text, null, renderedValue);
|
|
78
78
|
}
|
|
79
|
-
function InputWrapper({ isReady, input, setInput, handleSubmit, trackedTasksSize, setShowTaskPanel, isActive, }) {
|
|
79
|
+
function InputWrapper({ isReady, isInterrupted, input, setInput, handleSubmit, trackedTasksSize, setShowTaskPanel, isActive, }) {
|
|
80
|
+
if (isInterrupted) {
|
|
81
|
+
return (React.createElement(Box, { height: 1 },
|
|
82
|
+
React.createElement(Text, { color: "gray" }, chalk.dim('Interrupted — this session cannot be resumed. Press Ctrl+C to exit.'))));
|
|
83
|
+
}
|
|
80
84
|
return (React.createElement(Box, { height: 1 },
|
|
81
85
|
React.createElement(Text, { color: isReady ? BRAND_COLOR : 'gray' }, "> "),
|
|
82
86
|
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...)')))));
|
|
@@ -229,11 +233,22 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
229
233
|
]
|
|
230
234
|
: []);
|
|
231
235
|
const [input, setInput] = useState('');
|
|
236
|
+
const inputTextRef = useRef('');
|
|
237
|
+
// Keep ref in sync so useInput handler can read current input text
|
|
238
|
+
const updateInput = (value) => {
|
|
239
|
+
inputTextRef.current = value;
|
|
240
|
+
setInput(value);
|
|
241
|
+
};
|
|
232
242
|
const [currentOperation, setCurrentOperation] = useState(resumeEvents ? '' : 'Waiting for response...');
|
|
233
243
|
const [exitHint, setExitHint] = useState(null);
|
|
244
|
+
const [isInterrupted, setIsInterrupted] = useState(false);
|
|
234
245
|
// Double-tap exit tracking (shared for Ctrl+C and Ctrl+D)
|
|
235
246
|
const exitKeyPressed = useRef(false);
|
|
236
247
|
const exitKeyTimeout = useRef(null);
|
|
248
|
+
// Interrupt tracking - prevents duplicate interrupt requests
|
|
249
|
+
const isInterrupting = useRef(false);
|
|
250
|
+
// Debounce Escape after mount so splash-skip doesn't trigger interrupt
|
|
251
|
+
const mountedAt = useRef(Date.now());
|
|
237
252
|
// Track terminal size for layout calculations
|
|
238
253
|
// Debounce resize to prevent spam during rapid resize events
|
|
239
254
|
const [terminalSize, setTerminalSize] = useState({
|
|
@@ -264,11 +279,55 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
264
279
|
}, []);
|
|
265
280
|
// Global keyboard shortcuts
|
|
266
281
|
useInput((input, key) => {
|
|
267
|
-
//
|
|
282
|
+
// Escape: Interrupt agent while processing
|
|
283
|
+
// Ignore Escape within 300ms of mount to prevent splash-skip from triggering interrupt
|
|
284
|
+
if (key.escape &&
|
|
285
|
+
client &&
|
|
286
|
+
session &&
|
|
287
|
+
currentOperation &&
|
|
288
|
+
!isInterrupting.current &&
|
|
289
|
+
Date.now() - mountedAt.current > 300) {
|
|
290
|
+
isInterrupting.current = true;
|
|
291
|
+
client
|
|
292
|
+
.post(`/sessions/${session.id}/interrupt`, {})
|
|
293
|
+
.then(() => {
|
|
294
|
+
debug('Session interrupted');
|
|
295
|
+
})
|
|
296
|
+
.catch((err) => {
|
|
297
|
+
debug('Interrupt failed (session may already be done):', err);
|
|
298
|
+
})
|
|
299
|
+
.finally(() => {
|
|
300
|
+
isInterrupting.current = false;
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Ctrl-C: Clear input on first press (if text present), double-tap to exit
|
|
268
305
|
const isCtrlC = input === '\x03' || (key.ctrl && input === 'c');
|
|
306
|
+
if (isCtrlC) {
|
|
307
|
+
// If there's text in the input, clear it instead of showing exit hint
|
|
308
|
+
if (inputTextRef.current && !exitKeyPressed.current) {
|
|
309
|
+
updateInput('');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (exitKeyPressed.current) {
|
|
313
|
+
if (preConnectedSession?.id && resumeCommand) {
|
|
314
|
+
printResumeHint(preConnectedSession.id, resumeCommand);
|
|
315
|
+
}
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
exitKeyPressed.current = true;
|
|
319
|
+
setExitHint('ctrl-c');
|
|
320
|
+
if (exitKeyTimeout.current)
|
|
321
|
+
clearTimeout(exitKeyTimeout.current);
|
|
322
|
+
exitKeyTimeout.current = setTimeout(() => {
|
|
323
|
+
exitKeyPressed.current = false;
|
|
324
|
+
setExitHint(null);
|
|
325
|
+
}, 2000);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Ctrl-D: Double-tap to exit
|
|
269
329
|
const isCtrlD = input === '\x04' || (key.ctrl && input === 'd');
|
|
270
|
-
if (
|
|
271
|
-
const keyName = isCtrlC ? 'ctrl-c' : 'ctrl-d';
|
|
330
|
+
if (isCtrlD) {
|
|
272
331
|
if (exitKeyPressed.current) {
|
|
273
332
|
if (preConnectedSession?.id && resumeCommand) {
|
|
274
333
|
printResumeHint(preConnectedSession.id, resumeCommand);
|
|
@@ -276,7 +335,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
276
335
|
process.exit(0);
|
|
277
336
|
}
|
|
278
337
|
exitKeyPressed.current = true;
|
|
279
|
-
setExitHint(
|
|
338
|
+
setExitHint('ctrl-d');
|
|
280
339
|
if (exitKeyTimeout.current)
|
|
281
340
|
clearTimeout(exitKeyTimeout.current);
|
|
282
341
|
exitKeyTimeout.current = setTimeout(() => {
|
|
@@ -539,6 +598,19 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
539
598
|
setCurrentOperation('');
|
|
540
599
|
}
|
|
541
600
|
}
|
|
601
|
+
else if (event.type === 'interrupted') {
|
|
602
|
+
// Session was interrupted — interrupted sessions are terminal on the backend
|
|
603
|
+
setMessages((prev) => [
|
|
604
|
+
...prev,
|
|
605
|
+
{
|
|
606
|
+
key: `interrupted-${Date.now()}`,
|
|
607
|
+
content: chalk.dim('⊘ Interrupted'),
|
|
608
|
+
type: 'assistant',
|
|
609
|
+
},
|
|
610
|
+
]);
|
|
611
|
+
setCurrentOperation('');
|
|
612
|
+
setIsInterrupted(true);
|
|
613
|
+
}
|
|
542
614
|
else if (isUnfulfilledAgentInstallRequest(event)) {
|
|
543
615
|
// Check for agent install requests that need user approval
|
|
544
616
|
if (!promptedEventIds.current.has(event.id) && !pendingInstallRequest) {
|
|
@@ -589,7 +661,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
589
661
|
},
|
|
590
662
|
]);
|
|
591
663
|
// Clear input right after showing the message
|
|
592
|
-
|
|
664
|
+
updateInput('');
|
|
593
665
|
try {
|
|
594
666
|
await client.post(`/sessions/${session.id}/events`, {
|
|
595
667
|
content: value,
|
|
@@ -657,18 +729,21 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
657
729
|
const showTasksHintText = '[ctrl-t to show tasks]';
|
|
658
730
|
const showTasksHint = `[${chalk.bold('ctrl-t')} to show tasks]`;
|
|
659
731
|
// Build status line with right-aligned hint
|
|
660
|
-
//
|
|
732
|
+
// Priority: exit hint > esc to interrupt > ctrl-t task hint
|
|
661
733
|
// Strip ANSI codes to get visible length (statusLine contains spinner with color codes)
|
|
662
734
|
const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
663
735
|
const statusWithHint = (() => {
|
|
664
|
-
// Pick which hint to show: exit hint takes priority
|
|
665
736
|
let hintText = null;
|
|
666
737
|
let hint = null;
|
|
667
738
|
if (exitHint) {
|
|
668
739
|
hintText = `[${exitHint} again to exit]`;
|
|
669
740
|
hint = `[${chalk.bold(exitHint)} again to exit]`;
|
|
670
741
|
}
|
|
671
|
-
else if (
|
|
742
|
+
else if (currentOperation) {
|
|
743
|
+
hintText = '(esc to interrupt)';
|
|
744
|
+
hint = `(${chalk.bold('esc')} to interrupt)`;
|
|
745
|
+
}
|
|
746
|
+
if (!hintText && activeTaskCount > 0 && currentOperation) {
|
|
672
747
|
hintText = showTaskPanel ? hideTasksHintText : showTasksHintText;
|
|
673
748
|
hint = showTaskPanel ? hideTasksHint : showTasksHint;
|
|
674
749
|
}
|
|
@@ -721,7 +796,7 @@ function ChatUIWithConnection({ initialPrompt, version: _version, versionId: _ve
|
|
|
721
796
|
React.createElement(Text, null, statusWithHint)),
|
|
722
797
|
React.createElement(Box, { height: 1 },
|
|
723
798
|
React.createElement(Text, { color: "gray" }, '─'.repeat(Math.max(1, terminalWidth - 2)))),
|
|
724
|
-
React.createElement(InputWrapper, { isReady: isReady, input: input, setInput:
|
|
799
|
+
React.createElement(InputWrapper, { isReady: isReady, isInterrupted: isInterrupted, input: input, setInput: updateInput, handleSubmit: handleSubmit, trackedTasksSize: tasks.length, setShowTaskPanel: setShowTaskPanel, isActive: isActive })));
|
|
725
800
|
}
|
|
726
801
|
export async function ensureAuthenticated() {
|
|
727
802
|
const token = await getAuthToken();
|
|
@@ -10,6 +10,7 @@ const STATUS_COLORS = {
|
|
|
10
10
|
WAITING: '#b8860b', // dark goldenrod (readable on light and dark terminals)
|
|
11
11
|
DONE: '#22c55e', // green (success)
|
|
12
12
|
ERROR: '#ef4444', // red (error)
|
|
13
|
+
INTERRUPTED: '#b8860b', // dark goldenrod (warning)
|
|
13
14
|
};
|
|
14
15
|
// Status icons (for completed/static states)
|
|
15
16
|
const STATUS_ICONS = {
|
|
@@ -19,6 +20,7 @@ const STATUS_ICONS = {
|
|
|
19
20
|
WAITING: '◐',
|
|
20
21
|
DONE: '✓',
|
|
21
22
|
ERROR: '✗',
|
|
23
|
+
INTERRUPTED: '⊘',
|
|
22
24
|
};
|
|
23
25
|
// Animation frames for running tasks (alternates between empty and filled)
|
|
24
26
|
const RUNNING_ANIMATION_FRAMES = ['○', '●'];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* These types match the backend event system from the messaging refactor (PR #819).
|
|
5
5
|
* All CLI commands should import these types instead of defining their own.
|
|
6
6
|
*/
|
|
7
|
-
export type TaskStatus = 'CREATED' | 'STARTED' | 'RUNNING' | 'WAITING' | 'DONE' | 'ERROR';
|
|
7
|
+
export type TaskStatus = 'CREATED' | 'STARTED' | 'RUNNING' | 'WAITING' | 'DONE' | 'ERROR' | 'INTERRUPTED';
|
|
8
8
|
export interface BaseTaskFields {
|
|
9
9
|
id: string;
|
|
10
10
|
status: TaskStatus;
|
|
@@ -143,7 +143,12 @@ export interface CredentialsRequestEvent extends BaseEvent {
|
|
|
143
143
|
export declare function isUnfulfilledAgentInstallRequest(event: SessionEvent): event is AgentInstallRequestEvent;
|
|
144
144
|
/** Check if event is an unfulfilled credentials request */
|
|
145
145
|
export declare function isUnfulfilledCredentialsRequest(event: SessionEvent): event is CredentialsRequestEvent;
|
|
146
|
-
export
|
|
146
|
+
export interface InterruptedEvent extends BaseEvent {
|
|
147
|
+
type: 'interrupted';
|
|
148
|
+
interrupted_at: string;
|
|
149
|
+
interrupted_by_id: string;
|
|
150
|
+
}
|
|
151
|
+
export type SessionEvent = UserMessageEvent | RuntimeStartEvent | RuntimeRunningEvent | RuntimeWaitingEvent | RuntimeErrorEvent | RuntimeDoneEvent | AgentNotificationMessageEvent | AgentNotificationProgressEvent | AgentNotificationErrorEvent | AgentConsoleEvent | AgentInstallRequestEvent | CredentialsRequestEvent | InterruptedEvent;
|
|
147
152
|
export interface Session {
|
|
148
153
|
id: string;
|
|
149
154
|
workspace_id?: string;
|
|
@@ -4,6 +4,24 @@
|
|
|
4
4
|
* and fetching/converting session events for resume.
|
|
5
5
|
*/
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { marked } from 'marked';
|
|
8
|
+
import { markedTerminal } from 'marked-terminal';
|
|
9
|
+
import { brand, code as codeColor } from './colors.js';
|
|
10
|
+
// Configure marked for terminal rendering (same config as chat.tsx)
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
marked.use(markedTerminal({}, { theme: {} }));
|
|
13
|
+
/**
|
|
14
|
+
* Render markdown text to ANSI terminal output.
|
|
15
|
+
* Matches the live message rendering pipeline in chat.tsx.
|
|
16
|
+
*/
|
|
17
|
+
function renderMarkdown(text) {
|
|
18
|
+
let rendered = marked.parse(text);
|
|
19
|
+
// Fix unrendered inline markdown in list items (marked-terminal bug)
|
|
20
|
+
rendered = rendered.replace(/\*\*([^*]+)\*\*/g, (_, content) => chalk.bold(content));
|
|
21
|
+
rendered = rendered.replace(/`([^`]+)`/g, (_, content) => codeColor(content));
|
|
22
|
+
rendered = rendered.replace(/(?<![\\w])_([^_]+)_(?![\\w])/g, (_, content) => chalk.italic(content));
|
|
23
|
+
return rendered;
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
26
|
* Print a dimmed resume hint to stderr on session exit.
|
|
9
27
|
* Only call when a session ID is available.
|
|
@@ -42,7 +60,7 @@ export function eventsToDisplayMessages(events) {
|
|
|
42
60
|
if (event.type === 'user_message') {
|
|
43
61
|
messages.push({
|
|
44
62
|
key: event.id,
|
|
45
|
-
content:
|
|
63
|
+
content: `${brand('>')} ${event.content}`,
|
|
46
64
|
type: 'user',
|
|
47
65
|
timestamp: event.created_at,
|
|
48
66
|
});
|
|
@@ -50,9 +68,11 @@ export function eventsToDisplayMessages(events) {
|
|
|
50
68
|
else if (event.type === 'agent_notification_message') {
|
|
51
69
|
const text = typeof event.content === 'string' ? event.content : event.content?.data || '';
|
|
52
70
|
if (text.trim()) {
|
|
71
|
+
const rendered = renderMarkdown(text);
|
|
72
|
+
const messageContent = `${chalk.green('●')} ${chalk.bold('assistant')}\n${rendered.trim()}`;
|
|
53
73
|
messages.push({
|
|
54
74
|
key: event.id,
|
|
55
|
-
content:
|
|
75
|
+
content: messageContent,
|
|
56
76
|
type: 'assistant',
|
|
57
77
|
timestamp: event.created_at,
|
|
58
78
|
});
|
|
@@ -63,7 +83,7 @@ export function eventsToDisplayMessages(events) {
|
|
|
63
83
|
if (text.trim()) {
|
|
64
84
|
messages.push({
|
|
65
85
|
key: event.id,
|
|
66
|
-
content: chalk.red(text)
|
|
86
|
+
content: `${chalk.red('●')} ${chalk.bold('assistant')}\n${chalk.red(text)}`,
|
|
67
87
|
type: 'assistant',
|
|
68
88
|
timestamp: event.created_at,
|
|
69
89
|
});
|