@robota-sdk/agent-ui-terminal 3.0.0-beta.82 → 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
|
@@ -5,7 +5,10 @@ import stringWidth from 'string-width';
|
|
|
5
5
|
import { useNumberedSelection } from './hooks/useNumberedSelection.js';
|
|
6
6
|
import { formatNumberedSelectionPrompt, numberedRowPrefix } from './numbered-list.js';
|
|
7
7
|
import { Text } from './SafeText.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
writeScreenReaderAnnouncement,
|
|
10
|
+
type TScreenReaderChannel,
|
|
11
|
+
} from './screen-reader-announcement.js';
|
|
9
12
|
import { ScreenReaderProvider, useScreenReader } from './screen-reader-context.js';
|
|
10
13
|
|
|
11
14
|
export interface ISupervisedViewRow {
|
|
@@ -16,68 +19,128 @@ export interface ISupervisedViewRow {
|
|
|
16
19
|
readonly nextLoopAt?: string;
|
|
17
20
|
readonly name?: string;
|
|
18
21
|
readonly cwd?: string;
|
|
19
|
-
readonly pr?: {
|
|
20
|
-
readonly
|
|
22
|
+
readonly pr?: {
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly host: string;
|
|
25
|
+
readonly number: number;
|
|
26
|
+
readonly kind: 'pull' | 'merge-request';
|
|
27
|
+
};
|
|
21
28
|
readonly problem?: 'invalid-registration';
|
|
29
|
+
/** The verified registration generation; a row without one never offers an action. Never rendered. */
|
|
30
|
+
readonly generation?: string;
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
/** An attach the user confirmed in the view, bound to the process start the row showed. */
|
|
34
|
+
export interface ISupervisedAttachRequest {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly generation: string;
|
|
37
|
+
readonly mode: 'drive' | 'observe';
|
|
38
|
+
/** How the view was grouped, so it can reopen the same way after the attach. */
|
|
39
|
+
readonly groupByDirectory: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** How the view ended: closed, or handed to the host to attach and then come back. */
|
|
43
|
+
export type TSupervisedViewExit =
|
|
44
|
+
| { readonly kind: 'closed' }
|
|
45
|
+
| ({ readonly kind: 'attach' } & ISupervisedAttachRequest);
|
|
46
|
+
|
|
47
|
+
const ROLE: Readonly<Record<ISupervisedAttachRequest['mode'], string>> = {
|
|
48
|
+
drive: 'drive (send prompts, answer its questions)',
|
|
49
|
+
observe: 'observe (read only)',
|
|
50
|
+
};
|
|
51
|
+
|
|
24
52
|
export interface ISupervisedSessionViewProps {
|
|
25
53
|
readonly loadRows: (signal: AbortSignal) => Promise<readonly ISupervisedViewRow[]>;
|
|
26
|
-
|
|
54
|
+
/** Called once the user confirmed; the view then closes so the host can attach this terminal. */
|
|
55
|
+
readonly onAttach?: (request: ISupervisedAttachRequest) => void;
|
|
56
|
+
/** Receives the generation the row displayed, so a session restarted under the same id is refused. */
|
|
57
|
+
readonly onStop?: (id: string, generation: string) => Promise<void>;
|
|
27
58
|
readonly onStart?: () => Promise<string>;
|
|
28
|
-
readonly onOpenPr?: (id: string, url: string) => Promise<void>;
|
|
59
|
+
readonly onOpenPr?: (id: string, url: string, generation: string) => Promise<void>;
|
|
29
60
|
readonly filteredByCwd?: boolean;
|
|
30
61
|
readonly filteredByName?: boolean;
|
|
31
62
|
readonly filteredByPr?: boolean;
|
|
32
63
|
readonly stateFilter?: TGroup;
|
|
33
64
|
readonly refreshMs?: number;
|
|
65
|
+
/** The row to select first, when it is still listed — the one an attach left from. */
|
|
66
|
+
readonly initialSelectedId?: string;
|
|
67
|
+
readonly initialGroupByDirectory?: boolean;
|
|
34
68
|
}
|
|
35
69
|
|
|
36
70
|
const GROUP_ORDER = ['needs-input', 'working', 'idle', 'unknown', 'unverified', 'dead'] as const;
|
|
37
|
-
type TGroup = typeof GROUP_ORDER[number];
|
|
71
|
+
type TGroup = (typeof GROUP_ORDER)[number];
|
|
38
72
|
type TDisplayLine =
|
|
39
73
|
| { readonly kind: 'group'; readonly label: string }
|
|
40
74
|
| { readonly kind: 'row'; readonly row: ISupervisedViewRow; readonly index: number };
|
|
41
75
|
|
|
76
|
+
function isControllable(
|
|
77
|
+
row: ISupervisedViewRow | undefined,
|
|
78
|
+
): row is ISupervisedViewRow & { readonly generation: string } {
|
|
79
|
+
return (
|
|
80
|
+
row?.liveness === 'alive' && row.control === 'available' && row.generation !== undefined
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
function groupOf(row: ISupervisedViewRow): TGroup {
|
|
43
85
|
if (row.liveness === 'dead') return 'dead';
|
|
44
|
-
if (row
|
|
86
|
+
if (!isControllable(row)) return 'unverified';
|
|
45
87
|
return row.activity;
|
|
46
88
|
}
|
|
47
89
|
|
|
48
90
|
function sortedRows(rows: readonly ISupervisedViewRow[]): readonly ISupervisedViewRow[] {
|
|
49
|
-
return [...rows].sort(
|
|
50
|
-
|
|
91
|
+
return [...rows].sort(
|
|
92
|
+
(a, b) =>
|
|
93
|
+
GROUP_ORDER.indexOf(groupOf(a)) - GROUP_ORDER.indexOf(groupOf(b)) || a.id.localeCompare(b.id),
|
|
94
|
+
);
|
|
51
95
|
}
|
|
52
96
|
|
|
53
97
|
function sortedDirectoryRows(rows: readonly ISupervisedViewRow[]): readonly ISupervisedViewRow[] {
|
|
54
|
-
return [...rows].sort(
|
|
55
|
-
(a
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
return [...rows].sort(
|
|
99
|
+
(a, b) =>
|
|
100
|
+
(a.cwd === undefined ? 1 : 0) - (b.cwd === undefined ? 1 : 0) ||
|
|
101
|
+
(a.cwd ?? '').localeCompare(b.cwd ?? '') ||
|
|
102
|
+
GROUP_ORDER.indexOf(groupOf(a)) - GROUP_ORDER.indexOf(groupOf(b)) ||
|
|
103
|
+
a.id.localeCompare(b.id),
|
|
104
|
+
);
|
|
58
105
|
}
|
|
59
106
|
|
|
60
|
-
function uniqueDirectorySuffix(
|
|
107
|
+
function uniqueDirectorySuffix(
|
|
108
|
+
cwd: string,
|
|
109
|
+
directories: readonly string[],
|
|
110
|
+
budget: number,
|
|
111
|
+
): string {
|
|
61
112
|
if (cwd === '/') return cwd;
|
|
62
113
|
let longestSharedSuffix = 0;
|
|
63
114
|
for (const other of directories) {
|
|
64
115
|
if (other === cwd) continue;
|
|
65
116
|
let shared = 0;
|
|
66
|
-
while (
|
|
67
|
-
|
|
117
|
+
while (
|
|
118
|
+
shared < cwd.length &&
|
|
119
|
+
shared < other.length &&
|
|
120
|
+
cwd[cwd.length - shared - 1] === other[other.length - shared - 1]
|
|
121
|
+
)
|
|
122
|
+
shared++;
|
|
68
123
|
longestSharedSuffix = Math.max(longestSharedSuffix, shared);
|
|
69
124
|
}
|
|
70
125
|
const basenameLength = cwd.split(/[\\/]/u).filter(Boolean).at(-1)?.length ?? cwd.length;
|
|
71
126
|
const uniqueLength = Math.min(cwd.length, Math.max(basenameLength, longestSharedSuffix + 3));
|
|
72
127
|
const suffixStart = cwd.length - uniqueLength;
|
|
73
|
-
const boundary = Math.max(
|
|
128
|
+
const boundary = Math.max(
|
|
129
|
+
cwd.lastIndexOf('/', suffixStart - 1),
|
|
130
|
+
cwd.lastIndexOf('\\', suffixStart - 1),
|
|
131
|
+
);
|
|
74
132
|
const componentSuffix = cwd.slice(boundary + 1);
|
|
75
|
-
return stringWidth(displayPath(componentSuffix)) <= budget
|
|
133
|
+
return stringWidth(displayPath(componentSuffix)) <= budget
|
|
134
|
+
? componentSuffix
|
|
135
|
+
: cwd.slice(suffixStart);
|
|
76
136
|
}
|
|
77
137
|
|
|
78
138
|
function pathTokens(path: string): readonly string[] {
|
|
79
|
-
return Array.from(path, (character) =>
|
|
80
|
-
|
|
139
|
+
return Array.from(path, (character) =>
|
|
140
|
+
/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(character)
|
|
141
|
+
? `\\u{${character.codePointAt(0)!.toString(16)}}`
|
|
142
|
+
: character,
|
|
143
|
+
);
|
|
81
144
|
}
|
|
82
145
|
|
|
83
146
|
function displayPath(path: string): string {
|
|
@@ -103,20 +166,34 @@ function compactPath(path: string, budget: number): string {
|
|
|
103
166
|
}
|
|
104
167
|
|
|
105
168
|
function sameRows(a: readonly ISupervisedViewRow[], b: readonly ISupervisedViewRow[]): boolean {
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
169
|
+
return (
|
|
170
|
+
a.length === b.length &&
|
|
171
|
+
a.every((row, index) => {
|
|
172
|
+
const other = b[index];
|
|
173
|
+
return (
|
|
174
|
+
other !== undefined &&
|
|
175
|
+
row.id === other.id &&
|
|
176
|
+
row.liveness === other.liveness &&
|
|
177
|
+
row.control === other.control &&
|
|
178
|
+
row.activity === other.activity &&
|
|
179
|
+
row.problem === other.problem &&
|
|
180
|
+
row.nextLoopAt === other.nextLoopAt &&
|
|
181
|
+
row.name === other.name &&
|
|
182
|
+
row.cwd === other.cwd &&
|
|
183
|
+
row.pr?.url === other.pr?.url &&
|
|
184
|
+
row.generation === other.generation
|
|
185
|
+
);
|
|
186
|
+
})
|
|
187
|
+
);
|
|
113
188
|
}
|
|
114
189
|
|
|
115
190
|
function loopWaitLabel(nextLoopAt: string, observedAtMs: number): string {
|
|
116
191
|
const remaining = Date.parse(nextLoopAt) - observedAtMs;
|
|
117
192
|
if (!Number.isFinite(remaining)) return '';
|
|
118
193
|
if (remaining <= 0) return 'loop eligible now';
|
|
119
|
-
return remaining < 60_000
|
|
194
|
+
return remaining < 60_000
|
|
195
|
+
? 'loop eligible in <1m'
|
|
196
|
+
: `loop eligible in ${Math.ceil(remaining / 60_000)}m`;
|
|
120
197
|
}
|
|
121
198
|
|
|
122
199
|
export default function SupervisedSessionView({
|
|
@@ -124,11 +201,14 @@ export default function SupervisedSessionView({
|
|
|
124
201
|
onStop,
|
|
125
202
|
onStart,
|
|
126
203
|
onOpenPr,
|
|
204
|
+
onAttach,
|
|
127
205
|
filteredByCwd = false,
|
|
128
206
|
filteredByName = false,
|
|
129
207
|
filteredByPr = false,
|
|
130
208
|
stateFilter,
|
|
131
209
|
refreshMs = 2_000,
|
|
210
|
+
initialSelectedId,
|
|
211
|
+
initialGroupByDirectory = false,
|
|
132
212
|
}: ISupervisedSessionViewProps): React.ReactElement {
|
|
133
213
|
const { exit } = useApp();
|
|
134
214
|
const { stdout } = useStdout();
|
|
@@ -137,40 +217,65 @@ export default function SupervisedSessionView({
|
|
|
137
217
|
const [rows, setRows] = useState<readonly ISupervisedViewRow[]>([]);
|
|
138
218
|
const [observedAtMs, setObservedAtMs] = useState(Date.now);
|
|
139
219
|
const [status, setStatus] = useState<'loading' | 'ready' | 'unavailable'>('loading');
|
|
140
|
-
const [
|
|
220
|
+
const [chosenId, setSelectedId] = useState<string | undefined>(initialSelectedId);
|
|
141
221
|
const [showHelp, setShowHelp] = useState(false);
|
|
142
|
-
const [groupByDirectory, setGroupByDirectory] = useState(
|
|
143
|
-
|
|
144
|
-
const [
|
|
222
|
+
const [groupByDirectory, setGroupByDirectory] = useState(initialGroupByDirectory);
|
|
223
|
+
// One confirmation at a time, for the one registration the row showed when the key was pressed.
|
|
224
|
+
const [confirmStop, setConfirmStop] = useState<
|
|
225
|
+
| {
|
|
226
|
+
readonly id: string;
|
|
227
|
+
readonly generation: string;
|
|
228
|
+
readonly action: 'stop' | 'attach';
|
|
229
|
+
readonly mode?: ISupervisedAttachRequest['mode'];
|
|
230
|
+
}
|
|
231
|
+
| undefined
|
|
232
|
+
>();
|
|
233
|
+
const confirmStopId = confirmStop?.id;
|
|
234
|
+
const [attachStatus, setAttachStatus] = useState<'idle' | 'unavailable'>('idle');
|
|
235
|
+
const [stopStatus, setStopStatus] = useState<
|
|
236
|
+
'idle' | 'unavailable' | 'stopping' | 'stopped' | 'failed'
|
|
237
|
+
>('idle');
|
|
145
238
|
const [lastStoppedId, setLastStoppedId] = useState<string | undefined>();
|
|
146
239
|
const stoppingRef = useRef(false);
|
|
147
|
-
const [startStatus, setStartStatus] = useState<'idle' | 'starting' | 'started' | 'failed'>(
|
|
240
|
+
const [startStatus, setStartStatus] = useState<'idle' | 'starting' | 'started' | 'failed'>(
|
|
241
|
+
'idle',
|
|
242
|
+
);
|
|
148
243
|
const [lastStartedId, setLastStartedId] = useState<string | undefined>();
|
|
149
244
|
const startingRef = useRef(false);
|
|
150
|
-
const [prOpenStatus, setPrOpenStatus] = useState<
|
|
245
|
+
const [prOpenStatus, setPrOpenStatus] = useState<
|
|
246
|
+
'idle' | 'opening' | 'opened' | 'failed' | 'unavailable'
|
|
247
|
+
>('idle');
|
|
151
248
|
const openingPrRef = useRef(false);
|
|
152
249
|
const mountedRef = useRef(true);
|
|
153
250
|
const ordered = useMemo(() => {
|
|
154
|
-
const filtered = rows.filter(
|
|
251
|
+
const filtered = rows.filter(
|
|
252
|
+
(row) => stateFilter === undefined || groupOf(row) === stateFilter,
|
|
253
|
+
);
|
|
155
254
|
return groupByDirectory ? sortedDirectoryRows(filtered) : sortedRows(filtered);
|
|
156
255
|
}, [rows, stateFilter, groupByDirectory]);
|
|
157
256
|
const displayLines = useMemo((): readonly TDisplayLine[] => {
|
|
158
257
|
const lines: TDisplayLine[] = [];
|
|
159
|
-
const directories = [
|
|
258
|
+
const directories = [
|
|
259
|
+
...new Set(ordered.map((row) => row.cwd).filter((cwd): cwd is string => cwd !== undefined)),
|
|
260
|
+
];
|
|
160
261
|
let previousGroup: string | undefined;
|
|
161
262
|
let directoryNumber = 0;
|
|
162
263
|
ordered.forEach((row, index) => {
|
|
163
|
-
const group = groupByDirectory ? row.cwd ?? 'unverified' : groupOf(row);
|
|
264
|
+
const group = groupByDirectory ? (row.cwd ?? 'unverified') : groupOf(row);
|
|
164
265
|
if (group !== previousGroup) {
|
|
165
266
|
const label = groupByDirectory
|
|
166
|
-
? row.cwd === undefined
|
|
267
|
+
? row.cwd === undefined
|
|
268
|
+
? 'Directory: unverified'
|
|
167
269
|
: (() => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
270
|
+
const prefix = `Dir ${++directoryNumber}: `;
|
|
271
|
+
const budget = Math.max(4, columns - prefix.length);
|
|
272
|
+
const suffix = compactPath(
|
|
273
|
+
uniqueDirectorySuffix(row.cwd, directories, budget),
|
|
274
|
+
budget,
|
|
275
|
+
);
|
|
276
|
+
const full = `${prefix}${suffix} — ${displayPath(row.cwd)}`;
|
|
277
|
+
return screenReader || stringWidth(full) <= columns ? full : `${prefix}${suffix}`;
|
|
278
|
+
})()
|
|
174
279
|
: `${group}:`;
|
|
175
280
|
lines.push({ kind: 'group', label });
|
|
176
281
|
}
|
|
@@ -189,14 +294,24 @@ export default function SupervisedSessionView({
|
|
|
189
294
|
const next = sortedRows(await loadRows(controller.signal));
|
|
190
295
|
if (!mounted || controller.signal.aborted) return;
|
|
191
296
|
const nowMs = Date.now();
|
|
192
|
-
setRows((previous) => sameRows(previous, next) ? previous : next);
|
|
193
|
-
setObservedAtMs((previous) =>
|
|
194
|
-
|
|
297
|
+
setRows((previous) => (sameRows(previous, next) ? previous : next));
|
|
298
|
+
setObservedAtMs((previous) =>
|
|
299
|
+
next.some(
|
|
300
|
+
(row) =>
|
|
301
|
+
row.nextLoopAt !== undefined &&
|
|
302
|
+
loopWaitLabel(row.nextLoopAt, previous) !== loopWaitLabel(row.nextLoopAt, nowMs),
|
|
303
|
+
)
|
|
304
|
+
? nowMs
|
|
305
|
+
: previous,
|
|
306
|
+
);
|
|
195
307
|
setStatus('ready');
|
|
196
308
|
} catch {
|
|
197
309
|
if (mounted && !controller.signal.aborted) setStatus('unavailable');
|
|
198
310
|
} finally {
|
|
199
|
-
if (mounted && !controller.signal.aborted)
|
|
311
|
+
if (mounted && !controller.signal.aborted)
|
|
312
|
+
timer = setTimeout(() => {
|
|
313
|
+
void poll();
|
|
314
|
+
}, refreshMs);
|
|
200
315
|
}
|
|
201
316
|
};
|
|
202
317
|
void poll();
|
|
@@ -209,39 +324,96 @@ export default function SupervisedSessionView({
|
|
|
209
324
|
|
|
210
325
|
useEffect(() => {
|
|
211
326
|
mountedRef.current = true;
|
|
212
|
-
return () => {
|
|
327
|
+
return () => {
|
|
328
|
+
mountedRef.current = false;
|
|
329
|
+
};
|
|
213
330
|
}, []);
|
|
214
331
|
|
|
332
|
+
// Derived during render, so no frame ever shows loaded rows without a selection. The effect then
|
|
333
|
+
// records the fallback with a functional update, so it never overwrites a choice made meanwhile,
|
|
334
|
+
// and the selected row stays selected when regrouping reorders the list.
|
|
335
|
+
const selectedId =
|
|
336
|
+
chosenId !== undefined && ordered.some((row) => row.id === chosenId)
|
|
337
|
+
? chosenId
|
|
338
|
+
: ordered[0]?.id;
|
|
215
339
|
useEffect(() => {
|
|
216
|
-
|
|
217
|
-
|
|
340
|
+
// Before the first rows arrive there is nothing to fall back to; the choice waits for them.
|
|
341
|
+
setSelectedId((current) =>
|
|
342
|
+
ordered.length === 0 ||
|
|
343
|
+
(current !== undefined && ordered.some((row) => row.id === current))
|
|
344
|
+
? current
|
|
345
|
+
: ordered[0]?.id,
|
|
346
|
+
);
|
|
218
347
|
}, [ordered]);
|
|
219
348
|
|
|
220
349
|
useInput((input, key) => {
|
|
221
350
|
if (stoppingRef.current) return;
|
|
222
|
-
if (
|
|
223
|
-
|
|
351
|
+
if (
|
|
352
|
+
openingPrRef.current &&
|
|
353
|
+
input !== 'q' &&
|
|
354
|
+
!(!screenReader && key.escape) &&
|
|
355
|
+
!(key.ctrl && input === 'c')
|
|
356
|
+
)
|
|
357
|
+
return;
|
|
358
|
+
if (
|
|
359
|
+
startingRef.current &&
|
|
360
|
+
input !== 'q' &&
|
|
361
|
+
!(!screenReader && key.escape) &&
|
|
362
|
+
!(key.ctrl && input === 'c')
|
|
363
|
+
)
|
|
364
|
+
return;
|
|
224
365
|
if (confirmStopId !== undefined) {
|
|
225
366
|
if (input === 'n' || key.escape) {
|
|
226
|
-
|
|
367
|
+
setConfirmStop(undefined);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (input === 'y' && confirmStop?.action === 'attach') {
|
|
371
|
+
const target = confirmStop;
|
|
372
|
+
const row = ordered.find((candidate) => candidate.id === target.id);
|
|
373
|
+
setConfirmStop(undefined);
|
|
374
|
+
if (
|
|
375
|
+
status !== 'ready' ||
|
|
376
|
+
!isControllable(row) ||
|
|
377
|
+
row.generation !== target.generation ||
|
|
378
|
+
target.mode === undefined ||
|
|
379
|
+
onAttach === undefined
|
|
380
|
+
) {
|
|
381
|
+
setAttachStatus('unavailable');
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
onAttach({ id: target.id, generation: target.generation, mode: target.mode, groupByDirectory });
|
|
385
|
+
exit();
|
|
227
386
|
return;
|
|
228
387
|
}
|
|
229
388
|
if (input === 'y') {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
389
|
+
const target = confirmStop;
|
|
390
|
+
const row = ordered.find((candidate) => candidate.id === target?.id);
|
|
391
|
+
setConfirmStop(undefined);
|
|
392
|
+
// The confirmation names one registration; a restart under the same id is a different session.
|
|
393
|
+
if (
|
|
394
|
+
status !== 'ready' ||
|
|
395
|
+
target === undefined ||
|
|
396
|
+
!isControllable(row) ||
|
|
397
|
+
row.generation !== target.generation ||
|
|
398
|
+
onStop === undefined
|
|
399
|
+
) {
|
|
233
400
|
setStopStatus('unavailable');
|
|
234
401
|
return;
|
|
235
402
|
}
|
|
236
403
|
stoppingRef.current = true;
|
|
237
404
|
setStopStatus('stopping');
|
|
238
|
-
void onStop(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
405
|
+
void onStop(target.id, target.generation)
|
|
406
|
+
.then(() => {
|
|
407
|
+
if (!mountedRef.current) return;
|
|
408
|
+
setLastStoppedId(target.id);
|
|
409
|
+
setStopStatus('stopped');
|
|
410
|
+
})
|
|
411
|
+
.catch(() => {
|
|
412
|
+
if (mountedRef.current) setStopStatus('failed');
|
|
413
|
+
})
|
|
414
|
+
.finally(() => {
|
|
415
|
+
stoppingRef.current = false;
|
|
416
|
+
});
|
|
245
417
|
}
|
|
246
418
|
return;
|
|
247
419
|
}
|
|
@@ -261,43 +433,74 @@ export default function SupervisedSessionView({
|
|
|
261
433
|
if (input === 'n' && onStart !== undefined) {
|
|
262
434
|
startingRef.current = true;
|
|
263
435
|
setStartStatus('starting');
|
|
264
|
-
void Promise.resolve()
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
436
|
+
void Promise.resolve()
|
|
437
|
+
.then(onStart)
|
|
438
|
+
.then((id) => {
|
|
439
|
+
if (!mountedRef.current) return;
|
|
440
|
+
setLastStartedId(id);
|
|
441
|
+
setStartStatus('started');
|
|
442
|
+
})
|
|
443
|
+
.catch(() => {
|
|
444
|
+
if (mountedRef.current) setStartStatus('failed');
|
|
445
|
+
})
|
|
446
|
+
.finally(() => {
|
|
447
|
+
startingRef.current = false;
|
|
448
|
+
});
|
|
271
449
|
return;
|
|
272
450
|
}
|
|
273
|
-
if (input === 'p' &&
|
|
451
|
+
if ((input === 'a' || input === 'p') && onAttach !== undefined) {
|
|
274
452
|
const row = ordered.find((candidate) => candidate.id === selectedId);
|
|
275
|
-
if (status !== 'ready' ||
|
|
453
|
+
if (status !== 'ready' || !isControllable(row)) {
|
|
454
|
+
setAttachStatus('unavailable');
|
|
455
|
+
} else {
|
|
456
|
+
setAttachStatus('idle');
|
|
457
|
+
setStopStatus('idle');
|
|
458
|
+
setConfirmStop({
|
|
459
|
+
id: row.id,
|
|
460
|
+
generation: row.generation,
|
|
461
|
+
action: 'attach',
|
|
462
|
+
mode: input === 'a' ? 'drive' : 'observe',
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (input === 'o' && onOpenPr !== undefined) {
|
|
468
|
+
const row = ordered.find((candidate) => candidate.id === selectedId);
|
|
469
|
+
if (status !== 'ready' || !isControllable(row) || !row.pr) {
|
|
276
470
|
setPrOpenStatus('unavailable');
|
|
277
471
|
return;
|
|
278
472
|
}
|
|
279
473
|
openingPrRef.current = true;
|
|
280
474
|
setPrOpenStatus('opening');
|
|
281
|
-
void onOpenPr(row.id, row.pr.url
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
475
|
+
void onOpenPr(row.id, row.pr.url, row.generation)
|
|
476
|
+
.then(() => {
|
|
477
|
+
if (mountedRef.current) setPrOpenStatus('opened');
|
|
478
|
+
})
|
|
479
|
+
.catch(() => {
|
|
480
|
+
if (mountedRef.current) setPrOpenStatus('failed');
|
|
481
|
+
})
|
|
482
|
+
.finally(() => {
|
|
483
|
+
openingPrRef.current = false;
|
|
484
|
+
});
|
|
286
485
|
return;
|
|
287
486
|
}
|
|
288
487
|
if (input === 's' && onStop !== undefined) {
|
|
289
488
|
const row = ordered.find((candidate) => candidate.id === selectedId);
|
|
290
|
-
if (status !== 'ready' || row
|
|
489
|
+
if (status !== 'ready' || !isControllable(row)) {
|
|
291
490
|
setStopStatus('unavailable');
|
|
292
491
|
} else {
|
|
293
492
|
setStopStatus('idle');
|
|
294
|
-
|
|
493
|
+
setAttachStatus('idle');
|
|
494
|
+
setConfirmStop({ id: row.id, generation: row.generation, action: 'stop' });
|
|
295
495
|
}
|
|
296
496
|
return;
|
|
297
497
|
}
|
|
298
498
|
if (screenReader || ordered.length === 0) return;
|
|
299
499
|
if (key.upArrow || key.downArrow) {
|
|
300
|
-
const index = Math.max(
|
|
500
|
+
const index = Math.max(
|
|
501
|
+
0,
|
|
502
|
+
ordered.findIndex((row) => row.id === selectedId),
|
|
503
|
+
);
|
|
301
504
|
const next = Math.max(0, Math.min(ordered.length - 1, index + (key.downArrow ? 1 : -1)));
|
|
302
505
|
setSelectedId(ordered[next]?.id);
|
|
303
506
|
}
|
|
@@ -312,17 +515,22 @@ export default function SupervisedSessionView({
|
|
|
312
515
|
setSelectedId(ordered[index]?.id);
|
|
313
516
|
setStopStatus('idle');
|
|
314
517
|
},
|
|
315
|
-
onCancel: () => {
|
|
518
|
+
onCancel: () => {
|
|
519
|
+
if (!stoppingRef.current) exit();
|
|
520
|
+
},
|
|
316
521
|
});
|
|
317
522
|
|
|
318
523
|
const selectedRow = ordered.find((row) => row.id === selectedId);
|
|
319
|
-
const selectedName =
|
|
320
|
-
|
|
321
|
-
const selectedLoopStatus =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
selectedRow.
|
|
524
|
+
const selectedName =
|
|
525
|
+
status === 'ready' && isControllable(selectedRow) ? selectedRow.name : undefined;
|
|
526
|
+
const selectedLoopStatus =
|
|
527
|
+
status === 'ready' &&
|
|
528
|
+
isControllable(selectedRow) &&
|
|
529
|
+
selectedRow.activity === 'idle' &&
|
|
530
|
+
selectedRow.nextLoopAt
|
|
531
|
+
? loopWaitLabel(selectedRow.nextLoopAt, observedAtMs)
|
|
532
|
+
: '';
|
|
533
|
+
const selectedPr = status === 'ready' && isControllable(selectedRow) ? selectedRow.pr : undefined;
|
|
326
534
|
|
|
327
535
|
const height = Math.max(8, stdout.rows ?? 24);
|
|
328
536
|
const helpLines = [
|
|
@@ -331,9 +539,10 @@ export default function SupervisedSessionView({
|
|
|
331
539
|
's Request stop',
|
|
332
540
|
'g Group state/dir',
|
|
333
541
|
...(onStart === undefined ? [] : ['n New session']),
|
|
334
|
-
...(
|
|
335
|
-
'
|
|
336
|
-
'
|
|
542
|
+
...(onAttach === undefined ? [] : ['a Attach (drive)', 'p Peek (read only)']),
|
|
543
|
+
...(onOpenPr === undefined ? [] : ['o Open linked PR']),
|
|
544
|
+
'y Confirm',
|
|
545
|
+
'n/Esc Cancel',
|
|
337
546
|
'q/Esc/Ctrl+C Close',
|
|
338
547
|
'? Toggle help',
|
|
339
548
|
'Activity ≠ liveness',
|
|
@@ -342,84 +551,179 @@ export default function SupervisedSessionView({
|
|
|
342
551
|
];
|
|
343
552
|
// Reserve all fixed chrome plus both possible overflow indicators before choosing row lines.
|
|
344
553
|
const helpVisible = showHelp && confirmStopId === undefined && stopStatus !== 'stopping';
|
|
345
|
-
const fixedLines =
|
|
346
|
-
+
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
554
|
+
const fixedLines =
|
|
555
|
+
2 +
|
|
556
|
+
(stateFilter === undefined ? 0 : 1) +
|
|
557
|
+
(status === 'loading' ||
|
|
558
|
+
status === 'unavailable' ||
|
|
559
|
+
(status === 'ready' && ordered.length === 0)
|
|
560
|
+
? 1
|
|
561
|
+
: 0) +
|
|
562
|
+
(selectedId === undefined ? 0 : 1) +
|
|
563
|
+
(selectedName === undefined ? 0 : 1) +
|
|
564
|
+
(selectedPr === undefined ? 0 : 2) +
|
|
565
|
+
(selectedLoopStatus ? 1 : 0) +
|
|
566
|
+
(prOpenStatus === 'idle' ? 0 : 1) +
|
|
567
|
+
(startStatus === 'idle' ? 0 : 1) +
|
|
568
|
+
(confirmStopId !== undefined ? (screenReader ? 1 : 2) : stopStatus !== 'idle' ? 1 : 0) +
|
|
569
|
+
(confirmStopId === undefined && attachStatus !== 'idle' ? 1 : 0) +
|
|
570
|
+
1 +
|
|
571
|
+
(helpVisible ? helpLines.length : 0) +
|
|
572
|
+
2;
|
|
355
573
|
const viewport = Math.max(1, height - fixedLines);
|
|
356
|
-
const selectedLine = Math.max(
|
|
357
|
-
|
|
574
|
+
const selectedLine = Math.max(
|
|
575
|
+
0,
|
|
576
|
+
displayLines.findIndex((line) => line.kind === 'row' && line.row.id === selectedId),
|
|
577
|
+
);
|
|
578
|
+
const start = Math.min(
|
|
579
|
+
Math.max(0, selectedLine - Math.floor(viewport / 2)),
|
|
580
|
+
Math.max(0, displayLines.length - viewport),
|
|
581
|
+
);
|
|
358
582
|
const visible = screenReader ? displayLines : displayLines.slice(start, start + viewport);
|
|
359
583
|
const chromeWrap = screenReader ? {} : { wrap: 'truncate-end' as const };
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
584
|
+
// Attach and peek are offered only for a row this terminal could actually attach to.
|
|
585
|
+
const attachable = onAttach !== undefined && status === 'ready' && isControllable(selectedRow);
|
|
586
|
+
const footer =
|
|
587
|
+
stopStatus === 'stopping'
|
|
588
|
+
? 'Stop in progress; wait for result.'
|
|
589
|
+
: confirmStopId !== undefined
|
|
590
|
+
? `Confirm ${confirmStop?.action === 'attach' ? 'attach' : 'stop'} or cancel before closing.`
|
|
591
|
+
: screenReader
|
|
592
|
+
? `Type a number and Enter to select; s Stop;${attachable ? ' a Attach; p Peek;' : ''}${onStart ? ' n New;' : ''}${onOpenPr ? ' o Open PR;' : ''} g Group; Escape to close; ? Help.`
|
|
593
|
+
: `↑↓ Navigate s Stop${attachable ? ' a Attach p Peek' : ''}${onStart ? ' n New' : ''}${onOpenPr ? ' o Open PR' : ''} g Group ? Help q/Esc Close`;
|
|
594
|
+
const confirmQuestion = (id: string): string =>
|
|
595
|
+
confirmStop?.action === 'attach' && confirmStop.mode !== undefined
|
|
596
|
+
? `${confirmStop.mode === 'drive' ? 'Attach to' : 'Peek at'} ${id} to ${ROLE[confirmStop.mode]}?`
|
|
597
|
+
: `Stop ${id}?`;
|
|
364
598
|
return (
|
|
365
599
|
<Box flexDirection="column" {...(screenReader ? {} : { height })}>
|
|
366
600
|
<Text {...chromeWrap}>
|
|
367
|
-
{filteredByCwd
|
|
601
|
+
{filteredByCwd
|
|
602
|
+
? 'Background sessions in selected directory'
|
|
603
|
+
: 'Background sessions across projects'}
|
|
368
604
|
{filteredByName ? ' · name filter active' : ''}
|
|
369
605
|
{filteredByPr ? ' · PR filter active' : ''}
|
|
370
606
|
</Text>
|
|
371
|
-
<Text {...chromeWrap}>
|
|
607
|
+
<Text {...chromeWrap}>
|
|
608
|
+
{ordered.length} supervised session(s). Foreground peers and saved records are separate.
|
|
609
|
+
</Text>
|
|
372
610
|
{stateFilter !== undefined && <Text {...chromeWrap}>State: {stateFilter}</Text>}
|
|
373
611
|
{status === 'loading' && <Text {...chromeWrap}>Loading supervised sessions...</Text>}
|
|
374
|
-
{status === 'unavailable' &&
|
|
375
|
-
|
|
612
|
+
{status === 'unavailable' && (
|
|
613
|
+
<Text {...chromeWrap}>
|
|
614
|
+
Supervised session discovery unavailable; last verified rows remain below.
|
|
615
|
+
</Text>
|
|
616
|
+
)}
|
|
617
|
+
{status === 'ready' && ordered.length === 0 && (
|
|
618
|
+
<Text {...chromeWrap}>No supervised sessions.</Text>
|
|
619
|
+
)}
|
|
376
620
|
{start > 0 && !screenReader && <Text>{start} more above</Text>}
|
|
377
|
-
{visible.map((line) =>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
621
|
+
{visible.map((line) =>
|
|
622
|
+
line.kind === 'group' ? (
|
|
623
|
+
<Text key={`group-${line.label}`} {...chromeWrap}>
|
|
624
|
+
{line.label}
|
|
625
|
+
</Text>
|
|
626
|
+
) : (
|
|
627
|
+
<Text
|
|
628
|
+
key={`row-${line.row.id}`}
|
|
629
|
+
{...(screenReader ? {} : { wrap: 'truncate-end' as const })}
|
|
630
|
+
>
|
|
631
|
+
{screenReader
|
|
632
|
+
? numberedRowPrefix(line.index)
|
|
633
|
+
: line.row.id === selectedId
|
|
634
|
+
? '> '
|
|
635
|
+
: ' '}
|
|
636
|
+
{line.row.name && isControllable(line.row)
|
|
637
|
+
? screenReader
|
|
638
|
+
? `${line.row.name} (${line.row.id})`
|
|
639
|
+
: `${Array.from(line.row.name).slice(0, 24).join('')}${Array.from(line.row.name).length > 24 ? '…' : ''} [${line.row.id.slice(-8)}]`
|
|
640
|
+
: line.row.id}{' '}
|
|
641
|
+
activity {line.row.activity} liveness {line.row.liveness} control {line.row.control}
|
|
642
|
+
{status === 'ready' &&
|
|
643
|
+
isControllable(line.row) &&
|
|
644
|
+
line.row.activity === 'idle' &&
|
|
645
|
+
line.row.nextLoopAt
|
|
646
|
+
? ` ${loopWaitLabel(line.row.nextLoopAt, observedAtMs)}`
|
|
647
|
+
: ''}
|
|
648
|
+
{line.row.problem ? ` ${line.row.problem}` : ''}
|
|
649
|
+
{status === 'ready' && isControllable(line.row) && line.row.pr
|
|
650
|
+
? ` ${line.row.pr.host} ${line.row.pr.kind === 'pull' ? '#' : '!'}${line.row.pr.number}`
|
|
651
|
+
: ''}
|
|
652
|
+
</Text>
|
|
653
|
+
),
|
|
654
|
+
)}
|
|
655
|
+
{!screenReader && start + visible.length < displayLines.length && (
|
|
656
|
+
<Text>{displayLines.length - start - visible.length} more below</Text>
|
|
657
|
+
)}
|
|
395
658
|
{selectedId !== undefined && <Text {...chromeWrap}>Selected {selectedId}</Text>}
|
|
396
659
|
{selectedName !== undefined && <Text {...chromeWrap}>Name: {selectedName}</Text>}
|
|
397
|
-
{selectedPr !== undefined &&
|
|
660
|
+
{selectedPr !== undefined && (
|
|
661
|
+
<Text {...chromeWrap}>
|
|
662
|
+
PR: {selectedPr.host} {selectedPr.kind === 'pull' ? '#' : '!'}
|
|
663
|
+
{selectedPr.number}
|
|
664
|
+
</Text>
|
|
665
|
+
)}
|
|
398
666
|
{selectedPr !== undefined && <Text {...chromeWrap}>URL: {selectedPr.url}</Text>}
|
|
399
667
|
{selectedLoopStatus && <Text {...chromeWrap}>{selectedLoopStatus}</Text>}
|
|
400
668
|
{prOpenStatus === 'opening' && <Text {...chromeWrap}>Opening linked PR...</Text>}
|
|
401
669
|
{prOpenStatus === 'opened' && <Text {...chromeWrap}>Opened linked PR.</Text>}
|
|
402
|
-
{prOpenStatus === 'failed' &&
|
|
403
|
-
|
|
670
|
+
{prOpenStatus === 'failed' && (
|
|
671
|
+
<Text {...chromeWrap}>PR link changed or could not be opened.</Text>
|
|
672
|
+
)}
|
|
673
|
+
{prOpenStatus === 'unavailable' && (
|
|
674
|
+
<Text {...chromeWrap}>No verified PR link is available.</Text>
|
|
675
|
+
)}
|
|
404
676
|
{startStatus === 'starting' && <Text {...chromeWrap}>Starting a background session...</Text>}
|
|
405
|
-
{startStatus === 'started' &&
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
677
|
+
{startStatus === 'started' && (
|
|
678
|
+
<Text {...chromeWrap}>
|
|
679
|
+
Started {lastStartedId}
|
|
680
|
+
{filteredByName || stateFilter !== undefined ? ' (may be hidden by filter)' : ''}
|
|
681
|
+
</Text>
|
|
682
|
+
)}
|
|
683
|
+
{startStatus === 'failed' && (
|
|
684
|
+
<Text {...chromeWrap}>
|
|
685
|
+
Start failed; check workspace trust or run session start --background for details.
|
|
686
|
+
</Text>
|
|
687
|
+
)}
|
|
688
|
+
{confirmStopId !== undefined &&
|
|
689
|
+
(screenReader ? (
|
|
690
|
+
<Text>{confirmQuestion(confirmStopId)} y Yes / n No</Text>
|
|
691
|
+
) : (
|
|
692
|
+
<>
|
|
693
|
+
<Text {...chromeWrap}>{confirmQuestion(`…${confirmStopId.slice(-8)}`)}</Text>
|
|
694
|
+
<Text {...chromeWrap}>y Yes / n No</Text>
|
|
695
|
+
</>
|
|
696
|
+
))}
|
|
697
|
+
{confirmStopId === undefined && stopStatus === 'unavailable' && (
|
|
698
|
+
<Text {...chromeWrap}>This session cannot be stopped from the view.</Text>
|
|
699
|
+
)}
|
|
700
|
+
{confirmStopId === undefined && attachStatus === 'unavailable' && (
|
|
701
|
+
<Text {...chromeWrap}>This session cannot be attached to from the view.</Text>
|
|
702
|
+
)}
|
|
703
|
+
{stopStatus === 'stopping' && (
|
|
704
|
+
<Text {...chromeWrap}>Stopping selected session; wait for confirmation.</Text>
|
|
705
|
+
)}
|
|
416
706
|
{stopStatus === 'stopped' && <Text {...chromeWrap}>Stopped {lastStoppedId}</Text>}
|
|
417
|
-
{stopStatus === 'failed' &&
|
|
418
|
-
|
|
419
|
-
|
|
707
|
+
{stopStatus === 'failed' && (
|
|
708
|
+
<Text {...chromeWrap}>Stop failed; session remains listed until verified otherwise.</Text>
|
|
709
|
+
)}
|
|
710
|
+
{screenReader &&
|
|
711
|
+
ordered.length > 0 &&
|
|
712
|
+
confirmStopId === undefined &&
|
|
713
|
+
stopStatus !== 'stopping' && (
|
|
714
|
+
<Text>
|
|
715
|
+
{formatNumberedSelectionPrompt(ordered.length, true)}
|
|
716
|
+
{numbered.buffer ? ` ${numbered.buffer}` : ''}
|
|
717
|
+
</Text>
|
|
718
|
+
)}
|
|
420
719
|
{screenReader && numbered.invalid && <Text>Selection out of range.</Text>}
|
|
421
720
|
<Text {...chromeWrap}>{footer}</Text>
|
|
422
|
-
{helpVisible &&
|
|
721
|
+
{helpVisible &&
|
|
722
|
+
helpLines.map((line) => (
|
|
723
|
+
<Text key={line} {...chromeWrap}>
|
|
724
|
+
{line}
|
|
725
|
+
</Text>
|
|
726
|
+
))}
|
|
423
727
|
</Box>
|
|
424
728
|
);
|
|
425
729
|
}
|
|
@@ -429,21 +733,38 @@ export async function renderSupervisedSessionView(
|
|
|
429
733
|
readonly screenReader: boolean;
|
|
430
734
|
readonly screenReaderChannel?: TScreenReaderChannel;
|
|
431
735
|
readonly screenReaderHint?: boolean;
|
|
736
|
+
/** Print the screen-reader line; false when this process already printed it. */
|
|
737
|
+
readonly announce?: boolean;
|
|
432
738
|
},
|
|
433
|
-
): Promise<
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
739
|
+
): Promise<TSupervisedViewExit> {
|
|
740
|
+
if (options.announce !== false) {
|
|
741
|
+
writeScreenReaderAnnouncement({
|
|
742
|
+
enabled: options.screenReader,
|
|
743
|
+
channel: options.screenReaderChannel,
|
|
744
|
+
hint: options.screenReaderHint,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
let attach: ISupervisedAttachRequest | undefined;
|
|
439
748
|
const instance = render(
|
|
440
749
|
<ScreenReaderProvider enabled={options.screenReader}>
|
|
441
|
-
<SupervisedSessionView
|
|
750
|
+
<SupervisedSessionView
|
|
751
|
+
loadRows={options.loadRows}
|
|
752
|
+
onStop={options.onStop}
|
|
442
753
|
onStart={options.onStart}
|
|
443
754
|
onOpenPr={options.onOpenPr}
|
|
444
|
-
|
|
755
|
+
onAttach={(request) => {
|
|
756
|
+
attach = request;
|
|
757
|
+
}}
|
|
758
|
+
filteredByCwd={options.filteredByCwd}
|
|
759
|
+
filteredByName={options.filteredByName}
|
|
445
760
|
filteredByPr={options.filteredByPr}
|
|
446
|
-
stateFilter={options.stateFilter}
|
|
761
|
+
stateFilter={options.stateFilter}
|
|
762
|
+
refreshMs={options.refreshMs}
|
|
763
|
+
{...(options.initialSelectedId !== undefined ? { initialSelectedId: options.initialSelectedId } : {})}
|
|
764
|
+
{...(options.initialGroupByDirectory !== undefined
|
|
765
|
+
? { initialGroupByDirectory: options.initialGroupByDirectory }
|
|
766
|
+
: {})}
|
|
767
|
+
/>
|
|
447
768
|
</ScreenReaderProvider>,
|
|
448
769
|
{ isScreenReaderEnabled: options.screenReader, exitOnCtrlC: false },
|
|
449
770
|
);
|
|
@@ -452,4 +773,5 @@ export async function renderSupervisedSessionView(
|
|
|
452
773
|
} finally {
|
|
453
774
|
instance.unmount();
|
|
454
775
|
}
|
|
776
|
+
return attach === undefined ? { kind: 'closed' } : { kind: 'attach', ...attach };
|
|
455
777
|
}
|