@robota-sdk/agent-ui-terminal 3.0.0-beta.82 → 3.0.0-beta.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +858 -0
  2. package/README.md +5 -1
  3. package/dist/node/index.d.ts +189 -54
  4. package/dist/node/index.d.ts.map +1 -1
  5. package/dist/node/index.js +21 -21
  6. package/dist/node/index.js.map +1 -1
  7. package/package.json +16 -21
  8. package/src/App.tsx +14 -3
  9. package/src/AppPresentation.tsx +1 -1
  10. package/src/PermissionPrompt.tsx +50 -17
  11. package/src/SessionPicker.tsx +27 -7
  12. package/src/SessionStatusBar.tsx +6 -2
  13. package/src/StatusBar.tsx +24 -0
  14. package/src/SupervisedSessionView.tsx +479 -157
  15. package/src/TuiInteractionChannel.ts +8 -53
  16. package/src/__tests__/SupervisedSessionView-render.test.tsx +28 -0
  17. package/src/__tests__/SupervisedSessionView.test.tsx +453 -82
  18. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +32 -22
  19. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +10 -2
  20. package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
  21. package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
  22. package/src/__tests__/command-handoff-pty-e2e.test.ts +2 -2
  23. package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +3 -1
  24. package/src/__tests__/fixtures/command-handoff-driver.tsx +8 -5
  25. package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
  26. package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
  27. package/src/__tests__/palette-consistency.test.ts +0 -1
  28. package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
  29. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +2 -1
  30. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +2 -0
  31. package/src/__tests__/pty/session-attach.ptytest.ts +144 -0
  32. package/src/__tests__/render-channel-options.test.ts +6 -6
  33. package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
  34. package/src/__tests__/screen-reader-menus.test.tsx +1 -1
  35. package/src/__tests__/session-picker.test.tsx +93 -0
  36. package/src/__tests__/status-bar.test.tsx +8 -0
  37. package/src/__tests__/tui-channel-init-failure.test.ts +10 -2
  38. package/src/__tests__/wire-tui-channel.test.ts +1445 -0
  39. package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
  40. package/src/app-view-model.ts +6 -2
  41. package/src/attached-session-connection.ts +14 -0
  42. package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
  43. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +13 -0
  44. package/src/hooks/useAppController.ts +6 -2
  45. package/src/hooks/useAppInputBindings.ts +20 -2
  46. package/src/hooks/useAppInteractionState.ts +3 -0
  47. package/src/hooks/useAppOverlays.ts +20 -4
  48. package/src/hooks/useNumberedSelection.ts +19 -2
  49. package/src/hooks/useTuiChannel.ts +13 -1
  50. package/src/index.ts +17 -3
  51. package/src/render.tsx +218 -27
  52. package/src/short-session-id.ts +9 -0
  53. package/src/slash-command-input.ts +11 -0
  54. package/src/tui-app-channel-port.ts +29 -1
  55. package/src/tui-channel-options.ts +11 -2
  56. package/src/tui-session-event-projector.ts +0 -2
  57. package/src/tui-session-events.ts +6 -0
  58. package/src/tui-session-options.ts +9 -0
  59. package/src/tui-state-manager.ts +17 -3
  60. package/src/waiting-loop-stop-notice.ts +23 -0
  61. package/src/wire-history-sync.ts +159 -0
  62. package/src/wire-tui-channel.ts +962 -0
  63. package/src/wire-tui-client-commands.ts +142 -0
  64. package/src/wire-tui-projection.ts +136 -0
  65. package/dist/node/index.cjs +0 -32
  66. package/dist/node/index.d.cts +0 -1066
  67. package/dist/node/index.d.cts.map +0 -1
@@ -6,16 +6,13 @@
6
6
  */
7
7
 
8
8
  import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
9
- import {
10
- CommandRegistry,
11
- buildRuntimeSession,
12
- generateSessionName,
13
- } from '@robota-sdk/agent-framework';
9
+ import { CommandRegistry, buildRuntimeSession } from '@robota-sdk/agent-framework';
14
10
 
15
11
  import { AttentionCoordinator } from './attention/attention-coordinator.js';
16
12
  import { MS_PER_SECOND } from './attention/time-units.js';
17
13
  import { createSessionInitPoller } from './flows/session-init-poller.js';
18
14
  import { applySystemCommandResult } from './hooks/command-result-handler.js';
15
+ import { parseSlashCommandInput } from './slash-command-input.js';
19
16
  import {
20
17
  TuiChannelLifecycleCoordinator,
21
18
  TuiChannelStartRollbackError,
@@ -24,6 +21,7 @@ import { TuiPermissionQueue, TuiUserActionQueue } from './tui-interaction-queues
24
21
  import { TuiSessionEventProjector } from './tui-session-event-projector.js';
25
22
  import { buildTuiSessionOptions } from './tui-session-options.js';
26
23
  import { TuiStateManager } from './tui-state-manager.js';
24
+ import { WAITING_LOOP_STOP_REASON, waitingLoopStopNotice } from './waiting-loop-stop-notice.js';
27
25
 
28
26
  import type { IAttentionSource } from './attention/attention-tracker.js';
29
27
  import type { ISessionInitPoller, TSessionInitFailure } from './flows/session-init-poller.js';
@@ -78,7 +76,6 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
78
76
  availableCommands: ICommandInfo[] = [];
79
77
  sessionName: string | undefined;
80
78
 
81
- private autoNameTriggered = false;
82
79
  private initPoller: ISessionInitPoller | null = null;
83
80
 
84
81
  /** TERM-002: the App registers its Ink suspend/resume hooks into this controller. */
@@ -103,7 +100,6 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
103
100
  session: this.interactiveSession,
104
101
  manager: this.stateManager,
105
102
  ...(this.attention ? { attention: this.attention } : {}),
106
- onUserMessage: (content) => this.handleAutoNaming(content),
107
103
  requestPermission: (toolName, toolArgs, id, canPersistProjectPermission, requestedByPeer) =>
108
104
  this.permissions.enqueue(
109
105
  toolName,
@@ -324,34 +320,11 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
324
320
  this.stateManager.setPendingPrompt(null);
325
321
  }
326
322
 
323
+ /** Esc on an idle prompt: the session decides which waiting loop, if any, stops. */
327
324
  async stopWaitingSelfPacedLoop(): Promise<void> {
328
- const waiting = this.interactiveSession
329
- .listSelfPacedLoops()
330
- .filter((loop) => loop.phase === 'waiting');
331
- if (waiting.length === 0) return;
332
- if (waiting.length > 1) {
333
- this.addEntry(
334
- messageToHistoryEntry(
335
- createSystemMessage(
336
- 'Several self-paced loops are waiting. Use /loop list and /loop stop <id> to choose one.',
337
- ),
338
- ),
339
- );
340
- return;
341
- }
342
- const loopId = waiting[0]!.loopId;
343
- try {
344
- await this.interactiveSession.stopSelfPacedLoop(loopId, 'Loop stopped by Esc');
345
- this.addEntry(messageToHistoryEntry(createSystemMessage(`Loop ${loopId} stopped by Esc.`)));
346
- } catch (error) {
347
- this.addEntry(
348
- messageToHistoryEntry(
349
- createSystemMessage(
350
- `Could not stop loop ${loopId}: ${error instanceof Error ? error.message : String(error)}`,
351
- ),
352
- ),
353
- );
354
- }
325
+ const outcome = await this.interactiveSession.stopWaitingSelfPacedLoop(WAITING_LOOP_STOP_REASON);
326
+ const notice = waitingLoopStopNotice(outcome);
327
+ if (notice !== undefined) this.addEntry(messageToHistoryEntry(createSystemMessage(notice)));
355
328
  }
356
329
 
357
330
  async shutdown(options?: { reason?: TSessionEndReason; timeoutMs?: number }): Promise<void> {
@@ -407,9 +380,7 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
407
380
  }
408
381
 
409
382
  private async handleSlashCommand(input: string): Promise<void> {
410
- const parts = input.slice(1).split(/\s+/);
411
- const cmd = parts[0]?.toLowerCase() ?? '';
412
- const args = parts.slice(1).join(' ');
383
+ const { name: cmd, args } = parseSlashCommandInput(input);
413
384
 
414
385
  const result = await this.interactiveSession.executeCommand(cmd, args);
415
386
  if (result) {
@@ -463,22 +434,6 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
463
434
  this.permissions.cancelAll();
464
435
  }
465
436
 
466
- private handleAutoNaming(content: string): void {
467
- if (this.autoNameTriggered) return;
468
- if (this.opts.sessionName || this.interactiveSession.getName()) return;
469
- this.autoNameTriggered = true;
470
- generateSessionName(this.opts.provider, content)
471
- .then((name) => {
472
- this.interactiveSession.setName(name);
473
- this.sessionName = name;
474
- this.opts.onAutoNamed?.(name);
475
- this.onChange?.();
476
- })
477
- .catch(() => {
478
- this.autoNameTriggered = false;
479
- });
480
- }
481
-
482
437
  private syncRestoredHistory(): void {
483
438
  if (this.stateManager.history.length === 0) {
484
439
  const restored = this.interactiveSession.getFullHistory();
@@ -21,4 +21,32 @@ describe('production supervised view renderer', () => {
21
21
  expect(wrapper.props.children.props.onOpenPr).toBe(onOpenPr);
22
22
  expect(wrapper.props.children.props.filteredByPr).toBe(true);
23
23
  });
24
+
25
+ it('returns the attach the user confirmed so the host can run it and come back', async () => {
26
+ vi.mocked(render).mockClear();
27
+ let exitView: () => void = () => undefined;
28
+ vi.mocked(render).mockImplementationOnce(() => ({
29
+ waitUntilExit: () => new Promise<void>((resolve) => { exitView = resolve; }),
30
+ unmount: () => undefined,
31
+ }) as never);
32
+ const ended = renderSupervisedSessionView({ loadRows: async () => [], screenReader: false });
33
+ const wrapper = vi.mocked(render).mock.calls[0]?.[0] as React.ReactElement<{
34
+ children: React.ReactElement<{ onAttach?: (request: unknown) => void }>;
35
+ }>;
36
+ wrapper.props.children.props.onAttach?.({ id: 'x', generation: 'g', mode: 'observe', groupByDirectory: true });
37
+ exitView();
38
+ expect(await ended).toEqual({ kind: 'attach', id: 'x', generation: 'g', mode: 'observe', groupByDirectory: true });
39
+ });
40
+
41
+ it('prints the screen-reader line only when asked to announce', async () => {
42
+ const out = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
43
+ try {
44
+ await renderSupervisedSessionView({ loadRows: async () => [], screenReader: true, screenReaderChannel: 'flag', announce: false });
45
+ expect(out.mock.calls.map(([text]) => String(text)).join('')).not.toContain('Screen reader mode');
46
+ await renderSupervisedSessionView({ loadRows: async () => [], screenReader: true, screenReaderChannel: 'flag' });
47
+ expect(out.mock.calls.map(([text]) => String(text)).join('')).toContain('Screen reader mode: on via flag');
48
+ } finally {
49
+ out.mockRestore();
50
+ }
51
+ });
24
52
  });