@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
@@ -0,0 +1,93 @@
1
+ import { render } from 'ink-testing-library';
2
+ import React from 'react';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import SessionPicker from '../SessionPicker.js';
6
+ import { shortSessionId } from '../short-session-id.js';
7
+
8
+ import type {
9
+ IResumableSessionSummary,
10
+ ISessionListingEntry,
11
+ } from '@robota-sdk/agent-interface-session';
12
+
13
+ function summary(id: string, name?: string): IResumableSessionSummary {
14
+ return {
15
+ id,
16
+ cwd: '/w',
17
+ updatedAt: '2026-09-26T10:00:00.000Z',
18
+ messageCount: 2,
19
+ preview: '',
20
+ ...(name !== undefined ? { name } : {}),
21
+ };
22
+ }
23
+
24
+ describe('session ids on screen', () => {
25
+ it('shortens an id to the part that tells sessions apart, not the prefix they share', () => {
26
+ expect(shortSessionId('session_07d291bc-4d43-4d26-9375-7d8ca0e60e6f')).toBe('07d291bc');
27
+ expect(shortSessionId('session_1773862349776_rnecma5lu')).toBe('17738623');
28
+ expect(shortSessionId('host-session-id')).toBe('host-ses');
29
+ expect(shortSessionId('session_')).toBe('session_');
30
+ });
31
+
32
+ it('lists unnamed sessions by distinct ids, and named ones by name', () => {
33
+ const view = render(
34
+ <SessionPicker
35
+ sessions={[
36
+ summary('session_07d291bc-4d43-4d26-9375-7d8ca0e60e6f'),
37
+ summary('session_15f05245-1f06-4375-8f29-db19e622afdd'),
38
+ summary('session_aaaaaaaa-0000-0000-0000-000000000000', 'auth work'),
39
+ ]}
40
+ onSelect={() => undefined}
41
+ onCancel={() => undefined}
42
+ />,
43
+ );
44
+ const frame = view.lastFrame() ?? '';
45
+ expect(frame).toContain('07d291bc');
46
+ expect(frame).toContain('15f05245');
47
+ expect(frame).toContain('auth work');
48
+ expect(frame).not.toMatch(/session_\s/);
49
+ view.unmount();
50
+ });
51
+ });
52
+
53
+ describe('live sessions in the picker (#3189)', () => {
54
+ function entry(id: string, name: string, extra: Partial<ISessionListingEntry>): ISessionListingEntry {
55
+ return { ...summary(id, name), ...extra };
56
+ }
57
+
58
+ it('marks a session the host runs now, and how many clients are on it', () => {
59
+ const view = render(
60
+ <SessionPicker
61
+ sessions={[
62
+ entry('session_a', 'busy one', { live: true, clients: 2 }),
63
+ entry('session_b', 'solo one', { live: true, clients: 1 }),
64
+ entry('session_c', 'stored one', { live: false, clients: 0 }),
65
+ ]}
66
+ onSelect={() => undefined}
67
+ onCancel={() => undefined}
68
+ />,
69
+ );
70
+ const lines = (view.lastFrame() ?? '').split('\n');
71
+ const lineOf = (name: string): string => lines.find((line) => line.includes(name)) ?? '';
72
+ expect(lineOf('busy one')).toContain('● live · 2 clients');
73
+ expect(lineOf('solo one')).toContain('● live · 1 client');
74
+ expect(lineOf('solo one')).not.toContain('1 clients');
75
+ expect(lineOf('stored one')).not.toContain('live');
76
+ expect(lineOf('stored one')).not.toContain('client');
77
+ view.unmount();
78
+ });
79
+
80
+ it('shows neither for an older host, which sends neither', () => {
81
+ const view = render(
82
+ <SessionPicker
83
+ sessions={[summary('session_a', 'plain')]}
84
+ onSelect={() => undefined}
85
+ onCancel={() => undefined}
86
+ />,
87
+ );
88
+ const frame = view.lastFrame() ?? '';
89
+ expect(frame).not.toContain('live');
90
+ expect(frame).not.toContain('client');
91
+ view.unmount();
92
+ });
93
+ });
@@ -47,6 +47,14 @@ describe('StatusBar', () => {
47
47
  }
48
48
  });
49
49
 
50
+ it('says an observing terminal is read only, and nothing of the kind otherwise', () => {
51
+ const observing = render(<StatusBar {...baseProps} readOnly />);
52
+ expect(observing.lastFrame()).toContain('Observing — read only');
53
+ observing.unmount();
54
+ const driving = render(<StatusBar {...baseProps} />);
55
+ expect(driving.lastFrame()).not.toContain('Observing');
56
+ });
57
+
50
58
  it('renders session name when provided', () => {
51
59
  const { lastFrame } = render(<StatusBar {...baseProps} sessionName="my-feature" />);
52
60
  const frame = lastFrame()!;
@@ -1,4 +1,8 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
6
  import type { IAIProvider } from '@robota-sdk/agent-core';
3
7
 
4
8
  vi.mock('@robota-sdk/agent-framework', async (importOriginal) => {
@@ -32,6 +36,10 @@ vi.mock('@robota-sdk/agent-framework', async (importOriginal) => {
32
36
 
33
37
  import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
34
38
 
39
+ /** The session's working directory: private to this run, never a fixed name under /tmp. */
40
+ const PROJECT_DIR = mkdtempSync(join(tmpdir(), 'robota-tui-init-failure-'));
41
+ afterAll(() => rmSync(PROJECT_DIR, { recursive: true, force: true }));
42
+
35
43
  describe('TuiInteractionChannel init failure surfacing', () => {
36
44
  beforeEach(() => {
37
45
  vi.useFakeTimers();
@@ -43,7 +51,7 @@ describe('TuiInteractionChannel init failure surfacing', () => {
43
51
 
44
52
  it('TC-05: a real init error records a session-init-error entry and sets error state', async () => {
45
53
  const channel = new TuiInteractionChannel({
46
- cwd: '/tmp/project',
54
+ cwd: PROJECT_DIR,
47
55
  provider: {} as IAIProvider,
48
56
  });
49
57
  await channel.start();
@@ -136,4 +136,14 @@ describe('TuiPermissionQueue', () => {
136
136
  queue.current?.resolve(true);
137
137
  await expect(second).resolves.toBe(true);
138
138
  });
139
+
140
+ it('issue #3288 §1: carries the requester onto the current entry', async () => {
141
+ const queue = new TuiPermissionQueue(vi.fn());
142
+ const requester = { kind: 'background-agent' as const, label: 'general-purpose', taskId: 'agent_1' };
143
+ const pending = queue.enqueue('Glob', { pattern: '**/*' }, 'permission-1', false, undefined, requester);
144
+
145
+ expect(queue.current?.requester).toEqual(requester);
146
+ queue.current?.resolve(false);
147
+ await expect(pending).resolves.toBe(false);
148
+ });
139
149
  });
@@ -71,6 +71,37 @@ describe('TuiStateManager', () => {
71
71
  expect(mgr.activeTools[0]!.result).toBe('success');
72
72
  });
73
73
 
74
+ it('#3288: two parallel same-named calls are attributed by executionId, not by name+running order', () => {
75
+ const mgr = new TuiStateManager();
76
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'a.ts', isRunning: true, executionId: 'exec-a' });
77
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'b.ts', isRunning: true, executionId: 'exec-b' });
78
+ // The FIRST-started call (a.ts) finishes first, while the SECOND (b.ts) is still running.
79
+ // `findLastIndex` by name+running alone would pick the most-recently-STARTED entry (b.ts) —
80
+ // the wrong one — because it is still running and sits last in the array.
81
+ mgr.onToolEnd({
82
+ toolName: 'Read',
83
+ firstArg: 'a.ts',
84
+ isRunning: false,
85
+ result: 'success',
86
+ executionId: 'exec-a',
87
+ });
88
+ const a = mgr.activeTools.find((t) => t.executionId === 'exec-a');
89
+ const b = mgr.activeTools.find((t) => t.executionId === 'exec-b');
90
+ expect(a?.isRunning).toBe(false);
91
+ expect(a?.firstArg).toBe('a.ts');
92
+ expect(b?.isRunning).toBe(true);
93
+ expect(b?.firstArg).toBe('b.ts');
94
+
95
+ mgr.onToolEnd({
96
+ toolName: 'Read',
97
+ firstArg: 'b.ts',
98
+ isRunning: false,
99
+ result: 'success',
100
+ executionId: 'exec-b',
101
+ });
102
+ expect(mgr.activeTools.every((t) => !t.isRunning)).toBe(true);
103
+ });
104
+
74
105
  it('clears tools on thinking=true (next execution start)', () => {
75
106
  const mgr = new TuiStateManager();
76
107
  mgr.onToolStart({ toolName: 'Read', firstArg: '', isRunning: true });