@robota-sdk/agent-ui-terminal 3.0.0-beta.81 → 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.
- package/CHANGELOG.md +858 -0
- package/README.md +5 -1
- package/dist/node/index.d.ts +189 -54
- 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 +16 -21
- package/src/App.tsx +14 -3
- package/src/AppPresentation.tsx +1 -1
- package/src/PermissionPrompt.tsx +50 -17
- package/src/SessionPicker.tsx +27 -7
- package/src/SessionStatusBar.tsx +6 -2
- package/src/StatusBar.tsx +24 -0
- package/src/SupervisedSessionView.tsx +479 -157
- package/src/TuiInteractionChannel.ts +8 -53
- package/src/__tests__/SupervisedSessionView-render.test.tsx +28 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +453 -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__/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__/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__/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__/wire-tui-channel.test.ts +1445 -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 +17 -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-session-event-projector.ts +0 -2
- package/src/tui-session-events.ts +6 -0
- package/src/tui-session-options.ts +9 -0
- package/src/tui-state-manager.ts +17 -3
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +962 -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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's history as a terminal attached over the wire knows it, read one page at a time.
|
|
3
|
+
*
|
|
4
|
+
* The transcript is committed to the terminal once and counted (Ink `<Static>`), so what this shows
|
|
5
|
+
* must only ever grow at its end. Two rules keep it so:
|
|
6
|
+
* - a history read is shown when its last page arrives, never half-read;
|
|
7
|
+
* - a prompt's echo is held back while the host has entries this terminal has not read yet, since
|
|
8
|
+
* the echo would take the place one of them belongs in. It is shown once the read catches up,
|
|
9
|
+
* unless the host has recorded the prompt by then.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
13
|
+
import type { IWireHistoryEntry, TServerMessage } from '@robota-sdk/agent-transport/client';
|
|
14
|
+
|
|
15
|
+
type THistoryPage = Extract<TServerMessage, { type: 'history' }>;
|
|
16
|
+
|
|
17
|
+
export interface IWireHistorySyncOptions {
|
|
18
|
+
/** Ask the host for the page from `fromIndex`; false when it could not be sent. */
|
|
19
|
+
readonly request: (fromIndex: number) => boolean;
|
|
20
|
+
/** Show the host's history as known, then the echoes of prompts the host has not recorded yet. */
|
|
21
|
+
readonly show: (entries: readonly IHistoryEntry[], echoes: readonly IHistoryEntry[]) => void;
|
|
22
|
+
/** A page's entries as the terminal holds them. */
|
|
23
|
+
readonly revive: (entries: readonly IWireHistoryEntry[]) => IHistoryEntry[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface IHistoryRead {
|
|
27
|
+
/** From the start of the history, replacing what is known; otherwise after what is known. */
|
|
28
|
+
readonly reload: boolean;
|
|
29
|
+
readonly entries: IHistoryEntry[];
|
|
30
|
+
/** Where the next page must start. */
|
|
31
|
+
next: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface IPromptEcho {
|
|
35
|
+
readonly entry: IHistoryEntry;
|
|
36
|
+
readonly content: string;
|
|
37
|
+
/** How many host entries were known when the prompt was echoed: the host records it after them. */
|
|
38
|
+
readonly after: number;
|
|
39
|
+
shown: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isUserEntryWith(entry: IHistoryEntry, content: string): boolean {
|
|
43
|
+
if (entry.category !== 'chat' || entry.type !== 'user') return false;
|
|
44
|
+
const data = entry.data;
|
|
45
|
+
return typeof data === 'object' && data !== null && 'content' in data && data.content === content;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class WireHistorySync {
|
|
49
|
+
private known: IHistoryEntry[] = [];
|
|
50
|
+
private read: IHistoryRead | undefined;
|
|
51
|
+
/** What to read once the current read ends; a reload covers a tail read. */
|
|
52
|
+
private followUp: 'tail' | 'reload' | undefined;
|
|
53
|
+
private echoes: IPromptEcho[] = [];
|
|
54
|
+
|
|
55
|
+
constructor(private readonly options: IWireHistorySyncOptions) {}
|
|
56
|
+
|
|
57
|
+
/** The host has entries this terminal has not read yet. */
|
|
58
|
+
get behind(): boolean {
|
|
59
|
+
return this.read !== undefined || this.followUp !== undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The host added entries after the ones known: read from there. */
|
|
63
|
+
readTail(): void {
|
|
64
|
+
if (this.read !== undefined) {
|
|
65
|
+
this.followUp ??= 'tail';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.start(false);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The host's history may differ anywhere: read all of it again. */
|
|
72
|
+
reload(): void {
|
|
73
|
+
if (this.read !== undefined) {
|
|
74
|
+
this.followUp = 'reload';
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
this.start(true);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read all of it again now, giving up on a read in flight: its reply may never come (frames were
|
|
82
|
+
* lost). A reply that still comes is told apart by where it starts.
|
|
83
|
+
*/
|
|
84
|
+
restart(): void {
|
|
85
|
+
this.read = undefined;
|
|
86
|
+
this.followUp = undefined;
|
|
87
|
+
this.start(true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Another session, or none: nothing known, echoed or being read belongs to what comes next. */
|
|
91
|
+
reset(): void {
|
|
92
|
+
this.known = [];
|
|
93
|
+
this.read = undefined;
|
|
94
|
+
this.followUp = undefined;
|
|
95
|
+
this.echoes = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One page from the host. Only the page the current read asked for is taken. Any page is the
|
|
100
|
+
* host's history as of when it was sent, so one that answers a read given up on and happens to
|
|
101
|
+
* start where this read needs is as good as its own answer.
|
|
102
|
+
*/
|
|
103
|
+
onPage(page: THistoryPage): void {
|
|
104
|
+
const read = this.read;
|
|
105
|
+
if (read === undefined) return;
|
|
106
|
+
if (page.startIndex !== read.next) {
|
|
107
|
+
// The host holds fewer entries than this read counted on: its history is not the one known.
|
|
108
|
+
if (page.total < read.next) this.restart();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
read.entries.push(...this.options.revive(page.entries));
|
|
112
|
+
read.next += page.entries.length;
|
|
113
|
+
if (read.next < page.total && page.entries.length > 0) {
|
|
114
|
+
if (!this.options.request(read.next)) this.read = undefined;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.read = undefined;
|
|
118
|
+
this.known = read.reload ? read.entries : [...this.known, ...read.entries];
|
|
119
|
+
const followUp = this.followUp;
|
|
120
|
+
this.followUp = undefined;
|
|
121
|
+
if (followUp !== undefined) this.start(followUp === 'reload');
|
|
122
|
+
this.show();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A prompt a turn started with, shown at once unless the host has entries not read yet. */
|
|
126
|
+
echo(entry: IHistoryEntry, content: string): void {
|
|
127
|
+
this.echoes.push({ entry, content, after: this.known.length, shown: false });
|
|
128
|
+
this.show();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private start(reload: boolean): void {
|
|
132
|
+
const from = reload ? 0 : this.known.length;
|
|
133
|
+
this.read = { reload, entries: [], next: from };
|
|
134
|
+
if (!this.options.request(from)) this.read = undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private show(): void {
|
|
138
|
+
this.echoes = this.unrecordedEchoes();
|
|
139
|
+
if (!this.behind) for (const echo of this.echoes) echo.shown = true;
|
|
140
|
+
this.options.show(
|
|
141
|
+
this.known,
|
|
142
|
+
this.echoes.filter((echo) => echo.shown).map((echo) => echo.entry),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The echoes whose prompt the host has not recorded, as far as this terminal has read. */
|
|
147
|
+
private unrecordedEchoes(): IPromptEcho[] {
|
|
148
|
+
let searchFrom = 0;
|
|
149
|
+
return this.echoes.filter((echo) => {
|
|
150
|
+
const from = Math.max(searchFrom, echo.after);
|
|
151
|
+
const index = this.known.findIndex(
|
|
152
|
+
(entry, position) => position >= from && isUserEntryWith(entry, echo.content),
|
|
153
|
+
);
|
|
154
|
+
if (index === -1) return true;
|
|
155
|
+
searchFrom = index + 1;
|
|
156
|
+
return false;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|