@guildai/cli 0.3.15 → 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.
@@ -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
- // Ctrl-C / Ctrl-D: Double-tap to exit
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 (isCtrlC || isCtrlD) {
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(keyName);
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
- setInput('');
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
- // Exit hint takes priority (temporary, 2s), otherwise show ctrl-t task hint
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 (activeTaskCount > 0 && currentOperation) {
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: setInput, handleSubmit: handleSubmit, trackedTasksSize: tasks.length, setShowTaskPanel: setShowTaskPanel, isActive: isActive })));
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 type SessionEvent = UserMessageEvent | RuntimeStartEvent | RuntimeRunningEvent | RuntimeWaitingEvent | RuntimeErrorEvent | RuntimeDoneEvent | AgentNotificationMessageEvent | AgentNotificationProgressEvent | AgentNotificationErrorEvent | AgentConsoleEvent | AgentInstallRequestEvent | CredentialsRequestEvent;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guildai/cli",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "Guild.ai CLI - Build, test, and deploy AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",