@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.
- package/CHANGELOG.md +893 -0
- package/README.md +34 -11
- package/dist/node/index.d.ts +205 -55
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +21 -21
- package/dist/node/index.js.map +1 -1
- package/package.json +19 -23
- package/src/App.tsx +14 -3
- package/src/AppPresentation.tsx +1 -1
- package/src/PermissionPrompt.tsx +73 -23
- package/src/SessionPicker.tsx +27 -7
- package/src/SessionStatusBar.tsx +6 -2
- package/src/StatusBar.tsx +24 -0
- package/src/SupervisedSessionView.tsx +568 -161
- package/src/TuiInteractionChannel.ts +17 -54
- package/src/__tests__/SupervisedSessionView-render.test.tsx +28 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +561 -82
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +32 -22
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +10 -2
- package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
- package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +2 -2
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +3 -1
- package/src/__tests__/fixtures/command-handoff-driver.tsx +8 -5
- package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
- package/src/__tests__/helpers/terminal-profiles.ts +4 -4
- package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
- package/src/__tests__/palette-consistency.test.ts +0 -1
- package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
- package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +11 -3
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +2 -1
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +2 -0
- package/src/__tests__/pty/session-attach.ptytest.ts +144 -0
- package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
- package/src/__tests__/render-channel-options.test.ts +6 -6
- package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
- package/src/__tests__/screen-reader-menus.test.tsx +1 -1
- package/src/__tests__/session-picker.test.tsx +93 -0
- package/src/__tests__/status-bar.test.tsx +8 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +10 -2
- package/src/__tests__/tui-interaction-queues.test.ts +10 -0
- package/src/__tests__/tui-state-manager.test.ts +31 -0
- package/src/__tests__/wire-tui-channel.test.ts +1457 -0
- package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
- package/src/app-view-model.ts +6 -2
- package/src/attached-session-connection.ts +14 -0
- package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +13 -0
- package/src/hooks/useAppController.ts +6 -2
- package/src/hooks/useAppInputBindings.ts +20 -2
- package/src/hooks/useAppInteractionState.ts +3 -0
- package/src/hooks/useAppOverlays.ts +20 -4
- package/src/hooks/useNumberedSelection.ts +19 -2
- package/src/hooks/useTuiChannel.ts +13 -1
- package/src/index.ts +19 -3
- package/src/render.tsx +218 -27
- package/src/short-session-id.ts +9 -0
- package/src/slash-command-input.ts +11 -0
- package/src/tui-app-channel-port.ts +29 -1
- package/src/tui-channel-options.ts +11 -2
- package/src/tui-interaction-queues.ts +9 -1
- package/src/tui-session-event-projector.ts +12 -3
- package/src/tui-session-events.ts +6 -0
- package/src/tui-session-options.ts +9 -0
- package/src/tui-state-manager.ts +25 -5
- package/src/types.ts +3 -1
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +963 -0
- package/src/wire-tui-client-commands.ts +142 -0
- package/src/wire-tui-projection.ts +136 -0
- package/dist/node/index.cjs +0 -32
- package/dist/node/index.d.cts +0 -1066
- 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 {
|
|
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:
|
|
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 });
|