@robota-sdk/agent-ui-terminal 3.0.0-beta.86 → 3.0.0-beta.87
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 +54 -14
- package/README.md +2 -2
- package/dist/node/index.d.ts +8 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +14 -14
- package/dist/node/index.js.map +1 -1
- package/package.json +19 -20
- package/src/SupervisedSessionView.tsx +12 -2
- package/src/__tests__/SupervisedSessionView.test.tsx +9 -5
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +1 -1
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +1 -1
- package/src/__tests__/UpdateNotice.test.tsx +2 -2
- package/src/__tests__/attached-app-session-switch.test.tsx +1 -1
- package/src/__tests__/attention-bell-and-marks.test.ts +5 -5
- package/src/__tests__/channel-factory-integration.test.ts +3 -3
- package/src/__tests__/checkpoint-store-per-channel.test.ts +1 -1
- package/src/__tests__/cjk-fallback-render.test.tsx +2 -2
- package/src/__tests__/command-handoff-pty-e2e.test.ts +2 -2
- package/src/__tests__/command-result-handler.test.ts +3 -3
- package/src/__tests__/fork-attach.test.tsx +3 -3
- package/src/__tests__/helpers/terminal-profiles.ts +4 -4
- package/src/__tests__/humanize-tool-name.test.ts +1 -1
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +5 -5
- package/src/__tests__/product-display-name.test.tsx +5 -5
- package/src/__tests__/pty/ask-user-question.ptytest.ts +2 -2
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +3 -3
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +9 -9
- package/src/__tests__/pty/flag-tui.ptytest.ts +2 -2
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +53 -24
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +59 -14
- package/src/__tests__/pty/ime-cursor.ptytest.ts +4 -4
- package/src/__tests__/pty/isolated-home.ts +37 -4
- package/src/__tests__/pty/org-policy.ptytest.ts +3 -3
- package/src/__tests__/pty/provider-setup.ptytest.ts +2 -2
- package/src/__tests__/pty/pty-driver.ts +8 -6
- package/src/__tests__/pty/replay-conversation.ptytest.ts +2 -2
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +2 -2
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +4 -4
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +3 -3
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +2 -2
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +3 -3
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +3 -3
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +16 -16
- package/src/__tests__/pty/session-attach.ptytest.ts +66 -28
- package/src/__tests__/pty/settings-screen.ptytest.ts +2 -2
- package/src/__tests__/pty/spawn-pty.ts +3 -3
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +2 -2
- package/src/__tests__/pty/trust-prompt.ptytest.ts +5 -5
- package/src/__tests__/pty/tui-pty.ptytest.ts +2 -2
- package/src/__tests__/screen-reader-labels.test.ts +1 -1
- package/src/__tests__/screen-reader-pacing.test.ts +3 -3
- package/src/__tests__/screen-reader-render-options.test.ts +3 -3
- package/src/__tests__/session-switch-channel.test.tsx +1 -1
- package/src/__tests__/terminal-capabilities.test.ts +9 -9
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +1 -1
- package/src/__tests__/tui-channel-init-failure.test.ts +1 -1
- package/src/__tests__/tui-state-manager.test.ts +2 -2
- package/src/__tests__/wire-tui-channel.test.ts +2 -2
- package/src/keybindings/__tests__/keybinding-registry.test.ts +2 -2
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +8 -1
- package/src/keybindings/node-keybindings-source.ts +6 -6
- package/src/render.tsx +7 -1
- package/src/terminal-capabilities.ts +1 -1
- package/src/tui-channel-options.ts +2 -0
- package/src/tui-session-options.ts +4 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-ui-terminal",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.87",
|
|
4
4
|
"description": "Terminal UI package (React + Ink) for the Robota SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -23,15 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"./package.json": "./package.json"
|
|
25
25
|
},
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "https://github.com/woojubb/robota.git",
|
|
29
|
-
"directory": "packages/agent-ui-terminal"
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://docs.robota.io/en/packages/agent-ui-terminal/",
|
|
32
|
-
"bugs": {
|
|
33
|
-
"url": "https://github.com/woojubb/robota/issues"
|
|
34
|
-
},
|
|
35
26
|
"engines": {
|
|
36
27
|
"node": ">=22.12.0"
|
|
37
28
|
},
|
|
@@ -47,15 +38,15 @@
|
|
|
47
38
|
"marked-terminal": "^7.3.0",
|
|
48
39
|
"react": "19.2.8",
|
|
49
40
|
"string-width": "^8.2.2",
|
|
50
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
51
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
52
|
-
"@robota-sdk/agent-interface-analytics": "3.0.0-beta.
|
|
53
|
-
"@robota-sdk/agent-interface-command": "3.0.0-beta.
|
|
54
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
55
|
-
"@robota-sdk/agent-interface-session": "3.0.0-beta.
|
|
56
|
-
"@robota-sdk/agent-interface-transport": "3.0.0-beta.
|
|
57
|
-
"@robota-sdk/agent-interface-tui": "3.0.0-beta.
|
|
58
|
-
"@robota-sdk/agent-transport": "3.0.0-beta.
|
|
41
|
+
"@robota-sdk/agent-core": "3.0.0-beta.87",
|
|
42
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.87",
|
|
43
|
+
"@robota-sdk/agent-interface-analytics": "3.0.0-beta.87",
|
|
44
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.87",
|
|
45
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.87",
|
|
46
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.87",
|
|
47
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.87",
|
|
48
|
+
"@robota-sdk/agent-interface-tui": "3.0.0-beta.87",
|
|
49
|
+
"@robota-sdk/agent-transport": "3.0.0-beta.87"
|
|
59
50
|
},
|
|
60
51
|
"devDependencies": {
|
|
61
52
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
|
|
@@ -66,7 +57,7 @@
|
|
|
66
57
|
"tsx": "^4.23.1",
|
|
67
58
|
"typescript": "^6.0.3",
|
|
68
59
|
"vitest": "^3.2.6",
|
|
69
|
-
"@robota-sdk/agent-command": "3.0.0-beta.
|
|
60
|
+
"@robota-sdk/agent-command": "3.0.0-beta.87"
|
|
70
61
|
},
|
|
71
62
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
72
63
|
"publishConfig": {
|
|
@@ -75,6 +66,14 @@
|
|
|
75
66
|
"volta": {
|
|
76
67
|
"extends": "../../package.json"
|
|
77
68
|
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/woojubb/robota"
|
|
72
|
+
},
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/woojubb/robota/issues"
|
|
75
|
+
},
|
|
76
|
+
"homepage": "https://robota.io/",
|
|
78
77
|
"scripts": {
|
|
79
78
|
"build": "rimraf dist && tsdown",
|
|
80
79
|
"build:js": "pnpm run build",
|
|
@@ -71,6 +71,8 @@ export interface ISupervisedSessionViewProps {
|
|
|
71
71
|
readonly onStart?: (choice?: TSupervisedStartTrustChoice) => Promise<string>;
|
|
72
72
|
/** The question to ask when the folder a new session starts in is not trusted yet; none otherwise. */
|
|
73
73
|
readonly startTrustQuestion?: () => Promise<ISupervisedStartTrustQuestion | undefined>;
|
|
74
|
+
/** Product-owned command shown when a long trust question is shortened. */
|
|
75
|
+
readonly trustStatusCommand?: string;
|
|
74
76
|
readonly onOpenPr?: (id: string, url: string, generation: string) => Promise<void>;
|
|
75
77
|
readonly filteredByCwd?: boolean;
|
|
76
78
|
readonly filteredByName?: boolean;
|
|
@@ -89,10 +91,13 @@ const TRUST_SUMMARY =
|
|
|
89
91
|
"Trusting it loads the project's own settings, hooks, plugins, skills and MCP servers:";
|
|
90
92
|
|
|
91
93
|
/** `lines` in at most `room` lines, the last one saying how many more there are. */
|
|
92
|
-
function fitLines(lines: readonly string[], room: number): readonly string[] {
|
|
94
|
+
function fitLines(lines: readonly string[], room: number, trustStatusCommand?: string): readonly string[] {
|
|
93
95
|
if (lines.length <= room) return lines;
|
|
94
96
|
const shown = Math.max(1, room - 1);
|
|
95
|
-
|
|
97
|
+
const hint = trustStatusCommand === undefined
|
|
98
|
+
? 'check workspace trust in the terminal'
|
|
99
|
+
: `see ${trustStatusCommand}`;
|
|
100
|
+
return [...lines.slice(0, shown), `… ${lines.length - shown} more — ${hint}`];
|
|
96
101
|
}
|
|
97
102
|
type TGroup = (typeof GROUP_ORDER)[number];
|
|
98
103
|
type TDisplayLine =
|
|
@@ -227,6 +232,7 @@ export default function SupervisedSessionView({
|
|
|
227
232
|
onStop,
|
|
228
233
|
onStart,
|
|
229
234
|
startTrustQuestion,
|
|
235
|
+
trustStatusCommand,
|
|
230
236
|
onOpenPr,
|
|
231
237
|
onAttach,
|
|
232
238
|
filteredByCwd = false,
|
|
@@ -651,6 +657,7 @@ export default function SupervisedSessionView({
|
|
|
651
657
|
: fitLines(
|
|
652
658
|
[`Not trusted: ${trustQuestion.folder}`, TRUST_SUMMARY, ...trustQuestion.loads],
|
|
653
659
|
screenReader ? Number.POSITIVE_INFINITY : viewport,
|
|
660
|
+
trustStatusCommand,
|
|
654
661
|
);
|
|
655
662
|
const chromeWrap = screenReader ? {} : { wrap: 'truncate-end' as const };
|
|
656
663
|
// Attach and peek are offered only for a row this terminal could actually attach to.
|
|
@@ -829,6 +836,9 @@ export async function renderSupervisedSessionView(
|
|
|
829
836
|
});
|
|
830
837
|
}
|
|
831
838
|
let attach: ISupervisedAttachRequest | undefined;
|
|
839
|
+
// The attached App pauses stdin when it unmounts. A view reopened after detach must resume it
|
|
840
|
+
// before Ink subscribes again, otherwise the view paints but ignores q/Escape and other keys.
|
|
841
|
+
process.stdin.resume();
|
|
832
842
|
const instance = render(
|
|
833
843
|
<ScreenReaderProvider enabled={options.screenReader}>
|
|
834
844
|
<SupervisedSessionView
|
|
@@ -28,7 +28,7 @@ const THIRD: ISupervisedViewRow = {
|
|
|
28
28
|
|
|
29
29
|
const QUESTION = {
|
|
30
30
|
folder: '/work/repo',
|
|
31
|
-
loads: ['Project sources (metadata only; content not read):', ' [file]
|
|
31
|
+
loads: ['Project sources (metadata only; content not read):', ' [file] state/settings.json — settings'],
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
describe('supervised session view', () => {
|
|
@@ -403,7 +403,7 @@ describe('supervised session view', () => {
|
|
|
403
403
|
await vi.waitFor(() => expect(view.lastFrame()).toContain('r Start Restricted'));
|
|
404
404
|
// It names the folder and what trusting it would load.
|
|
405
405
|
expect(view.lastFrame()).toContain('Not trusted: /work/repo');
|
|
406
|
-
expect(view.lastFrame()).toContain('
|
|
406
|
+
expect(view.lastFrame()).toContain('state/settings.json');
|
|
407
407
|
expect(start).not.toHaveBeenCalled();
|
|
408
408
|
// n cancels: nothing starts.
|
|
409
409
|
view.stdin.write('n');
|
|
@@ -423,13 +423,16 @@ describe('supervised session view', () => {
|
|
|
423
423
|
}
|
|
424
424
|
});
|
|
425
425
|
|
|
426
|
-
it('
|
|
426
|
+
it.each(['alpha-agent', 'beta-agent'] as const)(
|
|
427
|
+
'shows the configured trust-status command when fitting a long question for %s',
|
|
428
|
+
async (cliName) => {
|
|
427
429
|
const loads = Array.from({ length: 32 }, (_, index) => ` [absent] source-${index} — a source`);
|
|
428
430
|
const view = render(
|
|
429
431
|
<SupervisedSessionView
|
|
430
432
|
loadRows={async () => [FIRST]}
|
|
431
433
|
onStart={async () => SECOND.id}
|
|
432
434
|
startTrustQuestion={async () => ({ folder: '/work/repo', loads })}
|
|
435
|
+
trustStatusCommand={`${cliName} trust status`}
|
|
433
436
|
/>,
|
|
434
437
|
);
|
|
435
438
|
try {
|
|
@@ -441,12 +444,13 @@ describe('supervised session view', () => {
|
|
|
441
444
|
expect(lines.length).toBeLessThanOrEqual(24);
|
|
442
445
|
expect(lines).toContain('Not trusted: /work/repo');
|
|
443
446
|
expect(lines).toContain('y Trust and start / r Start Restricted / n Cancel');
|
|
444
|
-
expect(frame).toMatch(
|
|
447
|
+
expect(frame).toMatch(new RegExp(`… \\d+ more — see ${cliName} trust status`));
|
|
445
448
|
expect(frame).toContain('source-0 — a source');
|
|
446
449
|
} finally {
|
|
447
450
|
view.unmount();
|
|
448
451
|
}
|
|
449
|
-
|
|
452
|
+
},
|
|
453
|
+
);
|
|
450
454
|
|
|
451
455
|
it('cancels the trust question on Escape in screen-reader mode, and keeps the view open', async () => {
|
|
452
456
|
const start = vi.fn(async () => SECOND.id);
|
|
@@ -81,7 +81,7 @@ import type { IAIProvider, IHistoryEntry } from '@robota-sdk/agent-core';
|
|
|
81
81
|
import type { IExecutionResult } from '@robota-sdk/agent-interface-session';
|
|
82
82
|
|
|
83
83
|
/** The session's working directory: private to this run, never a fixed name under /tmp. */
|
|
84
|
-
const SESSION_CWD = mkdtempSync(join(tmpdir(), '
|
|
84
|
+
const SESSION_CWD = mkdtempSync(join(tmpdir(), 'agent-test-tui-display-'));
|
|
85
85
|
afterAll(() => rmSync(SESSION_CWD, { recursive: true, force: true }));
|
|
86
86
|
|
|
87
87
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -131,7 +131,7 @@ function makeMockTransportRegistry(): {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/** The session's working directory: private to this run, never a fixed name under /tmp. */
|
|
134
|
-
const SESSION_CWD = mkdtempSync(join(tmpdir(), '
|
|
134
|
+
const SESSION_CWD = mkdtempSync(join(tmpdir(), 'agent-test-tui-lifecycle-'));
|
|
135
135
|
afterAll(() => rmSync(SESSION_CWD, { recursive: true, force: true }));
|
|
136
136
|
|
|
137
137
|
function makeChannel(opts?: {
|
|
@@ -6,10 +6,10 @@ import UpdateNotice from '../UpdateNotice.js';
|
|
|
6
6
|
describe('UpdateNotice', () => {
|
|
7
7
|
it('renders an update notice outside session history', () => {
|
|
8
8
|
const { lastFrame } = render(
|
|
9
|
-
<UpdateNotice message="
|
|
9
|
+
<UpdateNotice message="Fixture Agent update available. Run npm install -g '@robota-sdk/agent-cli@latest'." />,
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
-
expect(lastFrame()).toContain('
|
|
12
|
+
expect(lastFrame()).toContain('Fixture Agent update available');
|
|
13
13
|
expect(lastFrame()).toContain('npm install');
|
|
14
14
|
});
|
|
15
15
|
});
|
|
@@ -79,7 +79,7 @@ describe('the full App attached to a host over the wire', () => {
|
|
|
79
79
|
let cwd: string;
|
|
80
80
|
|
|
81
81
|
beforeEach(() => {
|
|
82
|
-
cwd = mkdtempSync(join(tmpdir(), '
|
|
82
|
+
cwd = mkdtempSync(join(tmpdir(), 'agent-attached-app-'));
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
afterEach(() => {
|
|
@@ -123,22 +123,22 @@ describe('TC-09: the OSC 133 turn marks', () => {
|
|
|
123
123
|
|
|
124
124
|
describe('supportsTurnMarks — the documented negatives', () => {
|
|
125
125
|
it('is off in WezTerm, which owns OSC 133 itself', () => {
|
|
126
|
-
vi.stubEnv('
|
|
126
|
+
vi.stubEnv('PRODUCT_TURN_MARKS', '');
|
|
127
127
|
vi.stubEnv('TERM_PROGRAM', 'WezTerm');
|
|
128
128
|
expect(supportsTurnMarks()).toBe(false);
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
it('honours the explicit opt-in and the kill switch over everything else', () => {
|
|
132
132
|
vi.stubEnv('TERM_PROGRAM', 'WezTerm');
|
|
133
|
-
vi.stubEnv('
|
|
133
|
+
vi.stubEnv('PRODUCT_TURN_MARKS', '1');
|
|
134
134
|
expect(supportsTurnMarks({ override: true })).toBe(true);
|
|
135
|
-
vi.stubEnv('
|
|
135
|
+
vi.stubEnv('PRODUCT_TURN_MARKS', '0');
|
|
136
136
|
expect(supportsTurnMarks({ override: false })).toBe(false);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
it('ignores an ambient
|
|
139
|
+
it('ignores an ambient Fixture Agent override without a host choice', () => {
|
|
140
140
|
vi.stubEnv('TERM_PROGRAM', 'WezTerm');
|
|
141
|
-
vi.stubEnv('
|
|
141
|
+
vi.stubEnv('PRODUCT_TURN_MARKS', '1');
|
|
142
142
|
expect(supportsTurnMarks()).toBe(false);
|
|
143
143
|
});
|
|
144
144
|
});
|
|
@@ -72,7 +72,7 @@ describe('channel factory restores persisted context (CLI-B11 TC-02)', () => {
|
|
|
72
72
|
let channel: TuiInteractionChannel | undefined;
|
|
73
73
|
|
|
74
74
|
beforeEach(() => {
|
|
75
|
-
cwd = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
75
|
+
cwd = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-b11-int-')));
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
afterEach(async () => {
|
|
@@ -82,7 +82,7 @@ describe('channel factory restores persisted context (CLI-B11 TC-02)', () => {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
it('createChannel(sessionId) over a real FileSessionStore yields usedTokens > 0', async () => {
|
|
85
|
-
const store = createNodeHostSessionStore(join(cwd, '
|
|
85
|
+
const store = createNodeHostSessionStore(join(cwd, 'state', 'sessions'));
|
|
86
86
|
const sessionId = 'b11-restore-session';
|
|
87
87
|
persistConversation(store, sessionId, cwd);
|
|
88
88
|
|
|
@@ -118,7 +118,7 @@ describe('channel factory restores persisted context (CLI-B11 TC-02)', () => {
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
it('a channel created WITHOUT resumeSessionId starts with an empty context (control)', async () => {
|
|
121
|
-
const store = createNodeHostSessionStore(join(cwd, '
|
|
121
|
+
const store = createNodeHostSessionStore(join(cwd, 'state', 'sessions'));
|
|
122
122
|
persistConversation(store, 'b11-other-session', cwd);
|
|
123
123
|
|
|
124
124
|
const scripted = createScriptedProvider([{ text: 'unused' }]);
|
|
@@ -57,7 +57,7 @@ describe('#3231: the in-process channel factory', () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it('gives each channel, including a switched-to one, its own checkpoint store', () => {
|
|
60
|
-
cwd = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
60
|
+
cwd = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-store-per-channel-')));
|
|
61
61
|
const createEditCheckpointStore = vi.fn(() => ({}) as EditCheckpointStore);
|
|
62
62
|
const options: IRenderOptions = {
|
|
63
63
|
cwd,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* CLI-062 — fallback-path pinning guard.
|
|
3
3
|
*
|
|
4
4
|
* When real-cursor positioning is NOT active (capability off — the default in this test process:
|
|
5
|
-
* stdout is not a TTY and
|
|
5
|
+
* stdout is not a TTY and PRODUCT_IME_CURSOR is unset), CjkTextInput's rendering must stay
|
|
6
6
|
* byte-identical to the pre-CLI-062 behavior (invariant I4: guard fail → today's visuals, drawn
|
|
7
7
|
* inverse cursor included). The expected strings below are literal captures of the component's
|
|
8
8
|
* output BEFORE the CLI-062 change (chalk.level forced to 3 so the inverse-cursor bytes are pinned,
|
|
@@ -31,7 +31,7 @@ describe('CLI-062 — fallback rendering is byte-identical to pre-change output'
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('sanity: capability is off in this test process (fallback path is the one being pinned)', () => {
|
|
34
|
-
expect(process.env['
|
|
34
|
+
expect(process.env['PRODUCT_IME_CURSOR']).toBeUndefined();
|
|
35
35
|
expect(Boolean(process.stdout.isTTY)).toBe(false);
|
|
36
36
|
});
|
|
37
37
|
|
|
@@ -45,7 +45,7 @@ describe('command handoff PTY E2E', () => {
|
|
|
45
45
|
it(
|
|
46
46
|
'/shell runs a one-shot command on the real terminal and returns its exit code',
|
|
47
47
|
async () => {
|
|
48
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
48
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-shell-pty-')));
|
|
49
49
|
tempDirs.push(dir);
|
|
50
50
|
const outputPath = join(dir, 'result.json');
|
|
51
51
|
const session = spawnPtyFixture(FIXTURE, {
|
|
@@ -77,7 +77,7 @@ describe('command handoff PTY E2E', () => {
|
|
|
77
77
|
it(
|
|
78
78
|
'/editor opens $EDITOR on the real terminal and round-trips the composed text',
|
|
79
79
|
async () => {
|
|
80
|
-
const dir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
80
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-editor-pty-')));
|
|
81
81
|
tempDirs.push(dir);
|
|
82
82
|
const outputPath = join(dir, 'result.json');
|
|
83
83
|
const session = spawnPtyFixture(FIXTURE, {
|
|
@@ -26,7 +26,7 @@ import { applySystemCommandResult } from '../hooks/command-result-handler.js';
|
|
|
26
26
|
const PLUGIN_SOURCE_NAME = 'plugin';
|
|
27
27
|
|
|
28
28
|
function reloadPluginCommandSource(registry: CommandRegistry): void {
|
|
29
|
-
const pluginsDir = join(process.env.HOME ?? homedir(), '
|
|
29
|
+
const pluginsDir = join(process.env.HOME ?? homedir(), 'state', 'plugins');
|
|
30
30
|
const loader = new BundlePluginLoader(pluginsDir);
|
|
31
31
|
try {
|
|
32
32
|
// allow-fallback: test helper — empty registry on load error is safe
|
|
@@ -119,10 +119,10 @@ describe('applySystemCommandResult', () => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
it('reloads plugin command source immediately when requested', () => {
|
|
122
|
-
const home = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
122
|
+
const home = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-plugin-reload-')));
|
|
123
123
|
const pluginDir = join(
|
|
124
124
|
home,
|
|
125
|
-
'
|
|
125
|
+
'state',
|
|
126
126
|
'plugins',
|
|
127
127
|
'cache',
|
|
128
128
|
'community',
|
|
@@ -191,16 +191,16 @@ describe('attach refuses rather than stranding the terminal (CLI-1994 TC-09)', (
|
|
|
191
191
|
notify: neutral,
|
|
192
192
|
});
|
|
193
193
|
expect(neutral.mock.calls[0]?.[0]).toContain(FORK_SESSION_ID);
|
|
194
|
-
expect(neutral.mock.calls[0]?.[0]).not.toContain('
|
|
194
|
+
expect(neutral.mock.calls[0]?.[0]).not.toContain('agent');
|
|
195
195
|
|
|
196
196
|
const product = vi.fn();
|
|
197
197
|
attachToForkedSession(entry, {
|
|
198
198
|
hasSessionRecord: storeWith([FORK_SESSION_ID]),
|
|
199
199
|
switchSession: vi.fn(),
|
|
200
200
|
notify: product,
|
|
201
|
-
formatResumeCommand: (id) => `
|
|
201
|
+
formatResumeCommand: (id) => `agent --resume ${id}`,
|
|
202
202
|
});
|
|
203
|
-
expect(product.mock.calls[0]?.[0]).toContain(`
|
|
203
|
+
expect(product.mock.calls[0]?.[0]).toContain(`agent --resume ${FORK_SESSION_ID}`);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
it('a missing session record is refused, naming the record and the task status', () => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The terminal matrix, as data.
|
|
3
3
|
*
|
|
4
4
|
* Checking IME cursor placement across terminals was a *manual* matrix ("iTerm2 + Terminal.app
|
|
5
|
-
* ±
|
|
5
|
+
* ±PRODUCT_IME_CURSOR, kitty/WezTerm/Ghostty/Windows Terminal/tmux on real hardware"). This module
|
|
6
6
|
* converts it into something a machine re-runs: each row is the environment handshake a terminal
|
|
7
7
|
* emulator hands the program it launches, which is the ONLY channel through which the emulator's
|
|
8
8
|
* identity reaches our code (`supportsImeCursorPositioning()` reads `TERM_PROGRAM`).
|
|
@@ -105,7 +105,7 @@ export const TERMINAL_PROFILES: readonly ITerminalProfile[] = [
|
|
|
105
105
|
},
|
|
106
106
|
];
|
|
107
107
|
|
|
108
|
-
/** `
|
|
108
|
+
/** `PRODUCT_IME_CURSOR` settings the matrix sweeps. `undefined` means the variable is absent. */
|
|
109
109
|
export const IME_CURSOR_SETTINGS: readonly (string | undefined)[] = [undefined, '1', '0'];
|
|
110
110
|
|
|
111
111
|
export const REPRESENTATIVE_IME_PTY_CELLS: readonly {
|
|
@@ -140,13 +140,13 @@ export function profileEnv(
|
|
|
140
140
|
): Record<string, string> {
|
|
141
141
|
return {
|
|
142
142
|
...profile.env,
|
|
143
|
-
...(override === undefined ? {} : {
|
|
143
|
+
...(override === undefined ? {} : { PRODUCT_IME_CURSOR: override }),
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/** Matrix label used in test names. */
|
|
148
148
|
export function cellLabel(profile: ITerminalProfile, override: string | undefined): string {
|
|
149
149
|
const setting =
|
|
150
|
-
override === undefined ? '
|
|
150
|
+
override === undefined ? 'PRODUCT_IME_CURSOR unset' : `PRODUCT_IME_CURSOR=${override}`;
|
|
151
151
|
return `${profile.label} / ${setting}`;
|
|
152
152
|
}
|
|
@@ -9,7 +9,7 @@ describe('humanizeToolName (SCREEN-012)', () => {
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('uses a host-selected prefix without changing other tool names', () => {
|
|
12
|
-
expect(humanizeToolName('
|
|
12
|
+
expect(humanizeToolName('fixture_agent_command_agent', 'fixture_agent_command_')).toBe('agent');
|
|
13
13
|
expect(humanizeToolName('acme_command_agent', 'acme_command_')).toBe('agent');
|
|
14
14
|
expect(humanizeToolName('acme_command_agent')).toBe('acme_command_agent');
|
|
15
15
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `toChannelOptions` builds the channel's options field by field. `orgPolicy` was not among them, so
|
|
5
5
|
* a policy forwarded from the shell arrived here and was dropped, and `blockedCommands` enforcement
|
|
6
|
-
* stayed dead on the plain `
|
|
6
|
+
* stayed dead on the plain `agent` path — the most common one — after two rounds of wiring it.
|
|
7
7
|
*
|
|
8
8
|
* The reason it was silent is worth keeping: the shell forwards with
|
|
9
9
|
* `...(orgPolicy === null ? {} : { orgPolicy })`, and a SPREAD BYPASSES TypeScript's excess-property
|
|
@@ -51,13 +51,13 @@ describe('CLI-083: the TUI projection carries the org policy', () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it('enforces a blocked command from a policy file in a real TUI-owned session', async () => {
|
|
54
|
-
const home = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
55
|
-
mkdirSync(join(home, '
|
|
56
|
-
writeFileSync(join(home, '
|
|
54
|
+
const home = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-tui-policy-')));
|
|
55
|
+
mkdirSync(join(home, 'state'));
|
|
56
|
+
writeFileSync(join(home, 'state', 'org-policy.json'), JSON.stringify(POLICY));
|
|
57
57
|
vi.stubEnv('HOME', home);
|
|
58
58
|
let channel: TuiInteractionChannel | undefined;
|
|
59
59
|
try {
|
|
60
|
-
const policy = loadOrgPolicy(join(home, '
|
|
60
|
+
const policy = loadOrgPolicy(join(home, 'state', 'org-policy.json'));
|
|
61
61
|
expect(policy).not.toBeNull();
|
|
62
62
|
channel = new TuiInteractionChannel(
|
|
63
63
|
toChannelOptions(renderOptions({ cwd: home, orgPolicy: policy ?? undefined })),
|
|
@@ -23,8 +23,8 @@ describe('host-selected terminal display name', () => {
|
|
|
23
23
|
|
|
24
24
|
expect(history.lastFrame()).toContain('Atlas:');
|
|
25
25
|
expect(streaming.lastFrame()).toContain('Atlas:');
|
|
26
|
-
expect(history.lastFrame()).not.toContain('
|
|
27
|
-
expect(streaming.lastFrame()).not.toContain('
|
|
26
|
+
expect(history.lastFrame()).not.toContain('Fixture Agent:');
|
|
27
|
+
expect(streaming.lastFrame()).not.toContain('Fixture Agent:');
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('uses a neutral name when the host does not choose one', () => {
|
|
@@ -34,15 +34,15 @@ describe('host-selected terminal display name', () => {
|
|
|
34
34
|
|
|
35
35
|
it('renders a projected command using the host-selected prefix', () => {
|
|
36
36
|
const { lastFrame } = render(
|
|
37
|
-
<ProductDisplayNameProvider name="Atlas" modelCommandToolPrefix="
|
|
37
|
+
<ProductDisplayNameProvider name="Atlas" modelCommandToolPrefix="fixture_agent_command_">
|
|
38
38
|
<StreamingIndicator
|
|
39
39
|
text=""
|
|
40
|
-
activeTools={[{ toolName: '
|
|
40
|
+
activeTools={[{ toolName: 'fixture_agent_command_echo', firstArg: '', isRunning: true }]}
|
|
41
41
|
/>
|
|
42
42
|
</ProductDisplayNameProvider>,
|
|
43
43
|
);
|
|
44
44
|
expect(lastFrame()).toContain('echo(');
|
|
45
|
-
expect(lastFrame()).not.toContain('
|
|
45
|
+
expect(lastFrame()).not.toContain('fixture_agent_command_echo');
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it('uses the host name in transport restart copy', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CMD-005 TC-03: a model-issued AskUserQuestion renders as the Ink dialog mid-turn and the picked
|
|
3
|
-
* answer returns to the model — end to end against the BUILT
|
|
3
|
+
* answer returns to the model — end to end against the BUILT CLI binary.
|
|
4
4
|
*
|
|
5
5
|
* Boots with `--session-log` (INFRA-018 replay, no model key): the replayed assistant turn calls the
|
|
6
6
|
* AskUserQuestion tool; the real TUI renders the CMD-004 PendingActionPrompt while the turn is
|
|
@@ -30,7 +30,7 @@ describe('AskUserQuestion dialog through the real binary (CMD-005)', () => {
|
|
|
30
30
|
let session: IPtySession | undefined;
|
|
31
31
|
|
|
32
32
|
beforeEach(() => {
|
|
33
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
33
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-ask-pty-')));
|
|
34
34
|
writeTuiProviderSettings(projectDir); // dummy provider profile; replay answers every call
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* TEST-010 / SCREEN-013: the background-work drill-in entry point, observed through a real PTY.
|
|
3
3
|
*
|
|
4
4
|
* Component tests cover the switcher selection + detail pane in isolation; this asserts the
|
|
5
|
-
* App-level wiring end to end against the BUILT
|
|
5
|
+
* App-level wiring end to end against the BUILT CLI binary: pressing `Ctrl+B` in the live TUI
|
|
6
6
|
* opens the execution-workspace switcher (the only way into a background task), and `Esc` returns to
|
|
7
7
|
* the prompt. The switcher always lists the main thread, so this is assertable without seeding
|
|
8
8
|
* background work. Runs in the dedicated PTY project (`pnpm --filter @robota-sdk/agent-ui-terminal
|
|
9
|
-
* test:pty`) against `packages/agent-cli/bin/
|
|
9
|
+
* test:pty`) against `packages/agent-cli/bin/agent.cjs`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
|
|
@@ -26,7 +26,7 @@ describe('Background-work drill-in entry point through a real PTY (TEST-010 / SC
|
|
|
26
26
|
let session: IPtySession | undefined;
|
|
27
27
|
|
|
28
28
|
beforeEach(() => {
|
|
29
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
29
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-bgwork-')));
|
|
30
30
|
writeTuiProviderSettings(projectDir);
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* BEHAVIOR-2437 TC-07 — `/git status`, `/git diff` and `/git commit` on the BUILT
|
|
2
|
+
* BEHAVIOR-2437 TC-07 — `/git status`, `/git diff` and `/git commit` on the BUILT CLI binary, in
|
|
3
3
|
* a real PTY, over a throwaway git repository. The four User Execution scenarios of the spec, as a
|
|
4
4
|
* machine runs them: read path (S1), commit path (S2), nothing staged (S3), headless cancel (S4).
|
|
5
5
|
*
|
|
@@ -15,11 +15,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
16
|
|
|
17
17
|
import { spawnTui } from './pty-driver.js';
|
|
18
|
+
import { createPtyEnv } from './isolated-home.js';
|
|
18
19
|
|
|
19
20
|
import type { IPtySession } from './pty-driver.js';
|
|
20
21
|
|
|
21
22
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
22
|
-
const
|
|
23
|
+
const AGENT_BIN = join(REPO_ROOT, 'packages/agent-cli/bin/agent.cjs');
|
|
23
24
|
|
|
24
25
|
const WAIT_MS = 20_000;
|
|
25
26
|
const CASE_TIMEOUT_MS = 120_000;
|
|
@@ -38,8 +39,7 @@ function gitEnv(home: string): NodeJS.ProcessEnv {
|
|
|
38
39
|
const globalConfig = join(home, 'gitconfig');
|
|
39
40
|
writeFileSync(globalConfig, '[commit]\n\tgpgsign = false\n');
|
|
40
41
|
return {
|
|
41
|
-
|
|
42
|
-
HOME: home,
|
|
42
|
+
...createPtyEnv({ HOME: home }),
|
|
43
43
|
GIT_CONFIG_NOSYSTEM: '1',
|
|
44
44
|
GIT_CONFIG_GLOBAL: globalConfig,
|
|
45
45
|
};
|
|
@@ -56,7 +56,7 @@ function git(fixture: IFixture, ...args: string[]): string {
|
|
|
56
56
|
|
|
57
57
|
/** The dummy provider profile under the isolated HOME — boot/slash/exit make zero model calls. */
|
|
58
58
|
function writeProviderSettings(home: string): void {
|
|
59
|
-
const dir = join(home, '
|
|
59
|
+
const dir = join(home, 'state');
|
|
60
60
|
mkdirSync(dir, { recursive: true });
|
|
61
61
|
writeFileSync(
|
|
62
62
|
join(dir, 'settings.json'),
|
|
@@ -75,7 +75,7 @@ function writeProviderSettings(home: string): void {
|
|
|
75
75
|
* `stage`), `notes.txt` edited and left unstaged, `scratch.log` created untracked.
|
|
76
76
|
*/
|
|
77
77
|
function makeFixture(stage: boolean): IFixture {
|
|
78
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
78
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), 'agent-git-pty-')));
|
|
79
79
|
const home = join(root, 'home');
|
|
80
80
|
const repo = join(root, 'repo');
|
|
81
81
|
mkdirSync(home, { recursive: true });
|
|
@@ -83,7 +83,7 @@ function makeFixture(stage: boolean): IFixture {
|
|
|
83
83
|
writeProviderSettings(home);
|
|
84
84
|
const fixture: IFixture = { root, repo, home, env: gitEnv(home) };
|
|
85
85
|
git(fixture, 'init', '-q', '-b', 'main');
|
|
86
|
-
git(fixture, 'config', 'user.name', '
|
|
86
|
+
git(fixture, 'config', 'user.name', 'Scenario Fixture');
|
|
87
87
|
git(fixture, 'config', 'user.email', 'scenario@example.com');
|
|
88
88
|
git(fixture, 'config', 'commit.gpgsign', 'false');
|
|
89
89
|
writeFileSync(join(repo, 'greeting.txt'), 'hello\n');
|
|
@@ -326,7 +326,7 @@ describe('/git through the real binary (BEHAVIOR-2437 TC-07)', () => {
|
|
|
326
326
|
() => {
|
|
327
327
|
fixture = makeFixture(true);
|
|
328
328
|
const env: NodeJS.ProcessEnv = { ...fixture.env, TERM: 'dumb' };
|
|
329
|
-
const trust = spawnSync(process.execPath, [
|
|
329
|
+
const trust = spawnSync(process.execPath, [AGENT_BIN, 'trust', '--yes'], {
|
|
330
330
|
cwd: fixture.repo,
|
|
331
331
|
env,
|
|
332
332
|
encoding: 'utf8',
|
|
@@ -334,7 +334,7 @@ describe('/git through the real binary (BEHAVIOR-2437 TC-07)', () => {
|
|
|
334
334
|
expect(trust.status).toBe(0);
|
|
335
335
|
const result = spawnSync(
|
|
336
336
|
process.execPath,
|
|
337
|
-
[
|
|
337
|
+
[AGENT_BIN, '-p', '/git commit feat: add greeting', '--bare', '--no-session-persistence'],
|
|
338
338
|
{ cwd: fixture.repo, env, encoding: 'utf8', timeout: 60_000 },
|
|
339
339
|
);
|
|
340
340
|
const output = `${result.stdout}${result.stderr}`;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The headless flag matrix (Phase 1) asserts request/output effects; this asserts the *rendered*
|
|
5
5
|
* effect a flag has on the live TUI. `--permission-mode <mode>` is shown in the status bar whenever it
|
|
6
6
|
* is not the default — so booting with `plan` / `acceptEdits` must render that mode. Runs in the
|
|
7
|
-
* dedicated PTY project against the BUILT
|
|
7
|
+
* dedicated PTY project against the BUILT CLI binary (`pnpm --filter @robota-sdk/agent-cli build`).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
|
|
@@ -22,7 +22,7 @@ describe('CLI flag → TUI rendering through a real PTY (TEST-009 Phase 3)', ()
|
|
|
22
22
|
let session: IPtySession | undefined;
|
|
23
23
|
|
|
24
24
|
beforeEach(() => {
|
|
25
|
-
projectDir = realpathSync(mkdtempSync(join(tmpdir(), '
|
|
25
|
+
projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-flagtui-')));
|
|
26
26
|
writeTuiProviderSettings(projectDir);
|
|
27
27
|
});
|
|
28
28
|
|