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

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 (75) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/README.md +34 -11
  3. package/dist/node/index.d.ts +205 -55
  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 +19 -23
  8. package/src/App.tsx +14 -3
  9. package/src/AppPresentation.tsx +1 -1
  10. package/src/PermissionPrompt.tsx +73 -23
  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 +568 -161
  15. package/src/TuiInteractionChannel.ts +17 -54
  16. package/src/__tests__/SupervisedSessionView-render.test.tsx +28 -0
  17. package/src/__tests__/SupervisedSessionView.test.tsx +561 -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__/helpers/terminal-profiles.ts +4 -4
  27. package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
  28. package/src/__tests__/palette-consistency.test.ts +0 -1
  29. package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
  30. package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
  31. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +11 -3
  32. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +2 -1
  33. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +2 -0
  34. package/src/__tests__/pty/session-attach.ptytest.ts +144 -0
  35. package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
  36. package/src/__tests__/render-channel-options.test.ts +6 -6
  37. package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
  38. package/src/__tests__/screen-reader-menus.test.tsx +1 -1
  39. package/src/__tests__/session-picker.test.tsx +93 -0
  40. package/src/__tests__/status-bar.test.tsx +8 -0
  41. package/src/__tests__/tui-channel-init-failure.test.ts +10 -2
  42. package/src/__tests__/tui-interaction-queues.test.ts +10 -0
  43. package/src/__tests__/tui-state-manager.test.ts +31 -0
  44. package/src/__tests__/wire-tui-channel.test.ts +1457 -0
  45. package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
  46. package/src/app-view-model.ts +6 -2
  47. package/src/attached-session-connection.ts +14 -0
  48. package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
  49. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +13 -0
  50. package/src/hooks/useAppController.ts +6 -2
  51. package/src/hooks/useAppInputBindings.ts +20 -2
  52. package/src/hooks/useAppInteractionState.ts +3 -0
  53. package/src/hooks/useAppOverlays.ts +20 -4
  54. package/src/hooks/useNumberedSelection.ts +19 -2
  55. package/src/hooks/useTuiChannel.ts +13 -1
  56. package/src/index.ts +19 -3
  57. package/src/render.tsx +218 -27
  58. package/src/short-session-id.ts +9 -0
  59. package/src/slash-command-input.ts +11 -0
  60. package/src/tui-app-channel-port.ts +29 -1
  61. package/src/tui-channel-options.ts +11 -2
  62. package/src/tui-interaction-queues.ts +9 -1
  63. package/src/tui-session-event-projector.ts +12 -3
  64. package/src/tui-session-events.ts +6 -0
  65. package/src/tui-session-options.ts +9 -0
  66. package/src/tui-state-manager.ts +25 -5
  67. package/src/types.ts +3 -1
  68. package/src/waiting-loop-stop-notice.ts +23 -0
  69. package/src/wire-history-sync.ts +159 -0
  70. package/src/wire-tui-channel.ts +963 -0
  71. package/src/wire-tui-client-commands.ts +142 -0
  72. package/src/wire-tui-projection.ts +136 -0
  73. package/dist/node/index.cjs +0 -32
  74. package/dist/node/index.d.cts +0 -1066
  75. 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,14 +100,21 @@ 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
- requestPermission: (toolName, toolArgs, id, canPersistProjectPermission, requestedByPeer) =>
103
+ requestPermission: (
104
+ toolName,
105
+ toolArgs,
106
+ id,
107
+ canPersistProjectPermission,
108
+ requestedByPeer,
109
+ requester,
110
+ ) =>
108
111
  this.permissions.enqueue(
109
112
  toolName,
110
113
  toolArgs,
111
114
  id,
112
115
  canPersistProjectPermission,
113
116
  requestedByPeer,
117
+ requester,
114
118
  ),
115
119
  askUser: (request, id) => this.askUser(request, id),
116
120
  dismissPrompt: (id) => this.dismissPromptById(id),
@@ -324,34 +328,11 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
324
328
  this.stateManager.setPendingPrompt(null);
325
329
  }
326
330
 
331
+ /** Esc on an idle prompt: the session decides which waiting loop, if any, stops. */
327
332
  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
- }
333
+ const outcome = await this.interactiveSession.stopWaitingSelfPacedLoop(WAITING_LOOP_STOP_REASON);
334
+ const notice = waitingLoopStopNotice(outcome);
335
+ if (notice !== undefined) this.addEntry(messageToHistoryEntry(createSystemMessage(notice)));
355
336
  }
356
337
 
357
338
  async shutdown(options?: { reason?: TSessionEndReason; timeoutMs?: number }): Promise<void> {
@@ -407,9 +388,7 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
407
388
  }
408
389
 
409
390
  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(' ');
391
+ const { name: cmd, args } = parseSlashCommandInput(input);
413
392
 
414
393
  const result = await this.interactiveSession.executeCommand(cmd, args);
415
394
  if (result) {
@@ -463,22 +442,6 @@ export class TuiInteractionChannel implements ITuiAppChannelPort {
463
442
  this.permissions.cancelAll();
464
443
  }
465
444
 
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
445
  private syncRestoredHistory(): void {
483
446
  if (this.stateManager.history.length === 0) {
484
447
  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
  });