@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
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.83",
|
|
4
4
|
"description": "Terminal UI package (React + Ink) for the Robota SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -12,23 +12,16 @@
|
|
|
12
12
|
"import": {
|
|
13
13
|
"types": "./dist/node/index.d.ts",
|
|
14
14
|
"default": "./dist/node/index.js"
|
|
15
|
-
},
|
|
16
|
-
"require": {
|
|
17
|
-
"types": "./dist/node/index.d.cts",
|
|
18
|
-
"default": "./dist/node/index.cjs"
|
|
19
15
|
}
|
|
20
16
|
},
|
|
21
17
|
"default": {
|
|
22
18
|
"import": {
|
|
23
19
|
"types": "./dist/node/index.d.ts",
|
|
24
20
|
"default": "./dist/node/index.js"
|
|
25
|
-
},
|
|
26
|
-
"require": {
|
|
27
|
-
"types": "./dist/node/index.d.cts",
|
|
28
|
-
"default": "./dist/node/index.cjs"
|
|
29
21
|
}
|
|
30
22
|
}
|
|
31
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
32
25
|
},
|
|
33
26
|
"repository": {
|
|
34
27
|
"type": "git",
|
|
@@ -39,11 +32,12 @@
|
|
|
39
32
|
"url": "https://github.com/woojubb/robota/issues"
|
|
40
33
|
},
|
|
41
34
|
"engines": {
|
|
42
|
-
"node": ">=22.
|
|
35
|
+
"node": ">=22.12.0"
|
|
43
36
|
},
|
|
44
37
|
"files": [
|
|
45
38
|
"dist",
|
|
46
|
-
"src"
|
|
39
|
+
"src",
|
|
40
|
+
"CHANGELOG.md"
|
|
47
41
|
],
|
|
48
42
|
"dependencies": {
|
|
49
43
|
"chalk": "^5.6.2",
|
|
@@ -52,14 +46,15 @@
|
|
|
52
46
|
"marked-terminal": "^7.3.0",
|
|
53
47
|
"react": "19.2.8",
|
|
54
48
|
"string-width": "^8.2.2",
|
|
55
|
-
"@robota-sdk/agent-core": "3.0.0-beta.
|
|
56
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
57
|
-
"@robota-sdk/agent-interface-analytics": "3.0.0-beta.
|
|
58
|
-
"@robota-sdk/agent-interface-command": "3.0.0-beta.
|
|
59
|
-
"@robota-sdk/agent-interface-execution": "3.0.0-beta.
|
|
60
|
-
"@robota-sdk/agent-interface-session": "3.0.0-beta.
|
|
61
|
-
"@robota-sdk/agent-interface-transport": "3.0.0-beta.
|
|
62
|
-
"@robota-sdk/agent-
|
|
49
|
+
"@robota-sdk/agent-core": "3.0.0-beta.83",
|
|
50
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.83",
|
|
51
|
+
"@robota-sdk/agent-interface-analytics": "3.0.0-beta.83",
|
|
52
|
+
"@robota-sdk/agent-interface-command": "3.0.0-beta.83",
|
|
53
|
+
"@robota-sdk/agent-interface-execution": "3.0.0-beta.83",
|
|
54
|
+
"@robota-sdk/agent-interface-session": "3.0.0-beta.83",
|
|
55
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.83",
|
|
56
|
+
"@robota-sdk/agent-transport": "3.0.0-beta.83",
|
|
57
|
+
"@robota-sdk/agent-interface-tui": "3.0.0-beta.83"
|
|
63
58
|
},
|
|
64
59
|
"devDependencies": {
|
|
65
60
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
|
|
@@ -70,7 +65,7 @@
|
|
|
70
65
|
"tsx": "^4.23.1",
|
|
71
66
|
"typescript": "^6.0.3",
|
|
72
67
|
"vitest": "^3.2.6",
|
|
73
|
-
"@robota-sdk/agent-command": "3.0.0-beta.
|
|
68
|
+
"@robota-sdk/agent-command": "3.0.0-beta.83"
|
|
74
69
|
},
|
|
75
70
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
76
71
|
"publishConfig": {
|
package/src/App.tsx
CHANGED
|
@@ -17,6 +17,12 @@ interface IProps {
|
|
|
17
17
|
cwd: string;
|
|
18
18
|
/** The composition root narrows every concrete channel to this port before React receives it. */
|
|
19
19
|
createChannel: (resumeSessionId?: string) => ITuiAppChannelPort;
|
|
20
|
+
/**
|
|
21
|
+
* The session lives on a host this terminal is attached to: a switch is asked of the host, and the
|
|
22
|
+
* channel follows the host's `session_switched`, so the App keeps its channel. Absent ⇒ the App
|
|
23
|
+
* switches by building a channel for the chosen session.
|
|
24
|
+
*/
|
|
25
|
+
requestSessionSwitch?: (sessionId: string) => Promise<void>;
|
|
20
26
|
providerOverride?: string | undefined;
|
|
21
27
|
providerType?: string | undefined;
|
|
22
28
|
modelId?: string;
|
|
@@ -76,9 +82,14 @@ function useActiveChannel(props: IProps): IActiveChannel {
|
|
|
76
82
|
setIsSwitching(true);
|
|
77
83
|
try {
|
|
78
84
|
setShowPicker(false);
|
|
79
|
-
|
|
85
|
+
if (props.requestSessionSwitch) {
|
|
86
|
+
await props.requestSessionSwitch(sessionId);
|
|
87
|
+
} else {
|
|
88
|
+
await state.channel.stop();
|
|
89
|
+
if (!mounted.current) return;
|
|
90
|
+
setState({ channel: props.createChannel(sessionId), sessionId });
|
|
91
|
+
}
|
|
80
92
|
if (!mounted.current) return;
|
|
81
|
-
setState({ channel: props.createChannel(sessionId), sessionId });
|
|
82
93
|
setFailedTarget(undefined);
|
|
83
94
|
setError(undefined);
|
|
84
95
|
} catch (cause) {
|
|
@@ -90,7 +101,7 @@ function useActiveChannel(props: IProps): IActiveChannel {
|
|
|
90
101
|
setIsSwitching(false);
|
|
91
102
|
}
|
|
92
103
|
},
|
|
93
|
-
[props.createChannel, state.channel],
|
|
104
|
+
[props.createChannel, props.requestSessionSwitch, state.channel],
|
|
94
105
|
);
|
|
95
106
|
const retrySwitch = useCallback((): void => {
|
|
96
107
|
if (failedTarget !== undefined) void switchSession(failedTarget);
|
package/src/AppPresentation.tsx
CHANGED
|
@@ -26,7 +26,7 @@ import type { IAppViewModel } from './app-view-model.js';
|
|
|
26
26
|
|
|
27
27
|
function Transcript({ model }: { model: IAppViewModel }): React.ReactElement {
|
|
28
28
|
return (
|
|
29
|
-
<Static items={model.staticItems}>
|
|
29
|
+
<Static key={model.transcriptKey} items={model.staticItems}>
|
|
30
30
|
{(item) =>
|
|
31
31
|
item.kind === 'banner' ? (
|
|
32
32
|
<AppBanner key="logo" version={item.version} />
|
package/src/PermissionPrompt.tsx
CHANGED
|
@@ -27,8 +27,18 @@ import type { TToolArgs } from '@robota-sdk/agent-core';
|
|
|
27
27
|
|
|
28
28
|
interface IProps {
|
|
29
29
|
request: IPendingPermissionRequest;
|
|
30
|
+
/** How long a new request ignores its answer keys; tests pass their own. */
|
|
31
|
+
armDelayMs?: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The prompt replaces the composer while the user may still be typing, and its keys are ordinary
|
|
36
|
+
* letters and digits (`a` allows for the session). For this long after a request appears its keys
|
|
37
|
+
* answer nothing, so a keystroke meant for the composer cannot grant a permission. The prompt shows
|
|
38
|
+
* no selection cursor until it is armed.
|
|
39
|
+
*/
|
|
40
|
+
export const PERMISSION_PROMPT_ARM_DELAY_MS = 450;
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
43
|
* Footer for the permission prompt — identical to the confirm prompt's (same horizontal row, same
|
|
34
44
|
* reducer). Esc is deliberately suppressed by the flow (`escape: false` — a permission ask must
|
|
@@ -47,7 +57,10 @@ function formatArgs(args: TToolArgs): string {
|
|
|
47
57
|
.join(', ');
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
export default function PermissionPrompt({
|
|
60
|
+
export default function PermissionPrompt({
|
|
61
|
+
request,
|
|
62
|
+
armDelayMs = PERMISSION_PROMPT_ARM_DELAY_MS,
|
|
63
|
+
}: IProps): React.ReactElement {
|
|
51
64
|
const palette = usePalette();
|
|
52
65
|
const canPersistProjectPermission = request.canPersistProjectPermission !== false;
|
|
53
66
|
const [state, setState] = React.useState<ISelectionFlowState>(() => createSelectionFlowState());
|
|
@@ -61,6 +74,14 @@ export default function PermissionPrompt({ request }: IProps): React.ReactElemen
|
|
|
61
74
|
setState(nextState);
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
const [armedRequest, setArmedRequest] = React.useState<IPendingPermissionRequest | undefined>();
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (armDelayMs <= 0) return undefined;
|
|
80
|
+
const timer = setTimeout(() => setArmedRequest(request), armDelayMs);
|
|
81
|
+
return () => clearTimeout(timer);
|
|
82
|
+
}, [request, armDelayMs]);
|
|
83
|
+
const armed = armDelayMs <= 0 || armedRequest === request;
|
|
84
|
+
|
|
64
85
|
const applyAction = React.useCallback(
|
|
65
86
|
(action: TPermissionPromptInputAction): void => {
|
|
66
87
|
const result = applyPermissionPromptInput(
|
|
@@ -83,7 +104,8 @@ export default function PermissionPrompt({ request }: IProps): React.ReactElemen
|
|
|
83
104
|
canPersistProjectPermission,
|
|
84
105
|
);
|
|
85
106
|
const numbered = useNumberedSelection({
|
|
86
|
-
|
|
107
|
+
// Off while arming, so a digit typed then is not buffered for a later Enter to commit.
|
|
108
|
+
enabled: screenReader && armed,
|
|
87
109
|
itemCount: options.length,
|
|
88
110
|
onSelect: (index) => {
|
|
89
111
|
const result = applyPermissionPromptInput(
|
|
@@ -99,10 +121,14 @@ export default function PermissionPrompt({ request }: IProps): React.ReactElemen
|
|
|
99
121
|
if (result.effect.type === 'resolve') request.resolve(result.effect.decision);
|
|
100
122
|
},
|
|
101
123
|
});
|
|
124
|
+
// Digits typed for one request are not an answer to the next.
|
|
125
|
+
const clearNumbered = numbered.clear;
|
|
126
|
+
React.useEffect(() => clearNumbered(), [request, clearNumbered]);
|
|
102
127
|
|
|
103
128
|
useKeybindingActions(
|
|
104
129
|
'permission-prompt',
|
|
105
130
|
(actions) => {
|
|
131
|
+
if (!armed) return;
|
|
106
132
|
const shortcutIndex = {
|
|
107
133
|
'allow-once': 0,
|
|
108
134
|
'allow-session': 1,
|
|
@@ -147,12 +173,15 @@ export default function PermissionPrompt({ request }: IProps): React.ReactElemen
|
|
|
147
173
|
<Box
|
|
148
174
|
flexDirection="column"
|
|
149
175
|
borderStyle="round"
|
|
150
|
-
borderColor={palette.border.attention}
|
|
176
|
+
borderColor={armed ? palette.border.attention : palette.border.muted}
|
|
151
177
|
paddingX={1}
|
|
152
178
|
>
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
179
|
+
<Box>
|
|
180
|
+
<Text color={palette.text.warning} bold>
|
|
181
|
+
[Permission Required]
|
|
182
|
+
</Text>
|
|
183
|
+
{armed ? null : <Text dimColor> keys answer in a moment</Text>}
|
|
184
|
+
</Box>
|
|
156
185
|
<Text>
|
|
157
186
|
Tool:{' '}
|
|
158
187
|
<Text color={palette.text.accent} bold>
|
|
@@ -168,17 +197,21 @@ export default function PermissionPrompt({ request }: IProps): React.ReactElemen
|
|
|
168
197
|
{/* Issue #2351: the "always" options carry the consent scope, so they no longer fit one row
|
|
169
198
|
of the prompt box — a row wrapped `Allow [y]` across two lines. One option per line. */}
|
|
170
199
|
<Box marginTop={1} flexDirection="column">
|
|
171
|
-
{options.map((opt, i) =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
{options.map((opt, i) => {
|
|
201
|
+
const selected = armed && i === state.selectedIndex;
|
|
202
|
+
return (
|
|
203
|
+
<Box key={opt}>
|
|
204
|
+
<Text
|
|
205
|
+
color={selected ? palette.text.accent : undefined}
|
|
206
|
+
bold={selected}
|
|
207
|
+
dimColor={!armed}
|
|
208
|
+
>
|
|
209
|
+
{selected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
|
|
210
|
+
{opt}
|
|
211
|
+
</Text>
|
|
212
|
+
</Box>
|
|
213
|
+
);
|
|
214
|
+
})}
|
|
182
215
|
</Box>
|
|
183
216
|
<KeyHintFooter hints={footerHints} />
|
|
184
217
|
</Box>
|
package/src/SessionPicker.tsx
CHANGED
|
@@ -9,15 +9,28 @@ import React from 'react';
|
|
|
9
9
|
import { SELECTION_INDICATOR, SELECTION_INDICATOR_NONE } from './key-hint-footer.js';
|
|
10
10
|
import ListPicker from './ListPicker.js';
|
|
11
11
|
import { Text } from './SafeText.js';
|
|
12
|
+
import { shortSessionId } from './short-session-id.js';
|
|
12
13
|
import { usePalette } from './theme/index.js';
|
|
13
14
|
|
|
14
|
-
import type {
|
|
15
|
+
import type { ISessionListingEntry } from '@robota-sdk/agent-interface-session';
|
|
15
16
|
|
|
16
|
-
const SESSION_ID_DISPLAY_LENGTH = 8;
|
|
17
17
|
const SESSION_PREVIEW_DISPLAY_LENGTH = 60;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* `● live · 2 clients` on a host that keeps sessions live: the session runs there now, and that many
|
|
21
|
+
* clients are on it. Null for a stored session, and for a host that says neither.
|
|
22
|
+
*/
|
|
23
|
+
export function sessionLiveBadge(session: ISessionListingEntry): string | null {
|
|
24
|
+
const parts: string[] = [];
|
|
25
|
+
if (session.live === true) parts.push('● live');
|
|
26
|
+
if (session.clients !== undefined && session.clients > 0) {
|
|
27
|
+
parts.push(`${session.clients} ${session.clients === 1 ? 'client' : 'clients'}`);
|
|
28
|
+
}
|
|
29
|
+
return parts.length > 0 ? parts.join(' · ') : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
interface IProps {
|
|
20
|
-
sessions: readonly
|
|
33
|
+
sessions: readonly ISessionListingEntry[];
|
|
21
34
|
onSelect: (sessionId: string) => void;
|
|
22
35
|
onCancel: () => void;
|
|
23
36
|
}
|
|
@@ -33,9 +46,10 @@ export default function SessionPicker({
|
|
|
33
46
|
<Text bold color={palette.text.accent}>
|
|
34
47
|
Select a session to resume:
|
|
35
48
|
</Text>
|
|
36
|
-
<ListPicker<
|
|
49
|
+
<ListPicker<ISessionListingEntry>
|
|
37
50
|
items={[...sessions]}
|
|
38
|
-
renderItem={(session:
|
|
51
|
+
renderItem={(session: ISessionListingEntry, isSelected: boolean) => {
|
|
52
|
+
const badge = sessionLiveBadge(session);
|
|
39
53
|
const preview = session.preview
|
|
40
54
|
? session.preview.slice(0, SESSION_PREVIEW_DISPLAY_LENGTH) +
|
|
41
55
|
(session.preview.length > SESSION_PREVIEW_DISPLAY_LENGTH ? '...' : '')
|
|
@@ -43,7 +57,7 @@ export default function SessionPicker({
|
|
|
43
57
|
return (
|
|
44
58
|
<Text>
|
|
45
59
|
{isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
|
|
46
|
-
<Text bold>{session.name ?? session.id
|
|
60
|
+
<Text bold>{session.name ?? shortSessionId(session.id)}</Text>
|
|
47
61
|
{' '}
|
|
48
62
|
<Text dimColor>
|
|
49
63
|
{new Date(session.updatedAt).toLocaleString(undefined, {
|
|
@@ -55,6 +69,12 @@ export default function SessionPicker({
|
|
|
55
69
|
</Text>
|
|
56
70
|
{' '}
|
|
57
71
|
<Text dimColor>msgs: {session.messageCount}</Text>
|
|
72
|
+
{badge !== null ? (
|
|
73
|
+
<>
|
|
74
|
+
{' '}
|
|
75
|
+
<Text color={palette.text.success}>{badge}</Text>
|
|
76
|
+
</>
|
|
77
|
+
) : null}
|
|
58
78
|
{preview ? (
|
|
59
79
|
<>
|
|
60
80
|
{'\n '}
|
|
@@ -64,7 +84,7 @@ export default function SessionPicker({
|
|
|
64
84
|
</Text>
|
|
65
85
|
);
|
|
66
86
|
}}
|
|
67
|
-
onSelect={(session:
|
|
87
|
+
onSelect={(session: ISessionListingEntry) => onSelect(session.id)}
|
|
68
88
|
onCancel={onCancel}
|
|
69
89
|
/>
|
|
70
90
|
</Box>
|
package/src/SessionStatusBar.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
import StatusBar from './StatusBar.js';
|
|
3
|
+
import StatusBar, { ReadOnlyText } from './StatusBar.js';
|
|
4
4
|
import { useTuiCliAdapter } from './tui-cli-adapter-context.js';
|
|
5
5
|
|
|
6
6
|
import type { TModelEffortSelection, TPermissionMode } from '@robota-sdk/agent-core';
|
|
@@ -23,6 +23,7 @@ interface IProps {
|
|
|
23
23
|
activePresetId?: string;
|
|
24
24
|
effort?: TModelEffortSelection;
|
|
25
25
|
gitRefreshToken?: number;
|
|
26
|
+
readOnly?: boolean;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export default function SessionStatusBar({
|
|
@@ -42,6 +43,7 @@ export default function SessionStatusBar({
|
|
|
42
43
|
activePresetId,
|
|
43
44
|
effort,
|
|
44
45
|
gitRefreshToken,
|
|
46
|
+
readOnly = false,
|
|
45
47
|
}: IProps): React.ReactElement | null {
|
|
46
48
|
const cliAdapter = useTuiCliAdapter();
|
|
47
49
|
const gitBranch = useMemo(() => cliAdapter.getGitBranch(cwd), [cliAdapter, cwd, gitRefreshToken]);
|
|
@@ -50,7 +52,8 @@ export default function SessionStatusBar({
|
|
|
50
52
|
providerType !== undefined ? cliAdapter.getProviderDisplayName(providerType) : undefined,
|
|
51
53
|
[cliAdapter, providerType],
|
|
52
54
|
);
|
|
53
|
-
|
|
55
|
+
// With the status line off, an observer is still told it cannot change the session.
|
|
56
|
+
if (!settings.enabled) return readOnly ? <ReadOnlyText /> : null;
|
|
54
57
|
|
|
55
58
|
return (
|
|
56
59
|
<StatusBar
|
|
@@ -71,6 +74,7 @@ export default function SessionStatusBar({
|
|
|
71
74
|
activeAgentLabel={activeAgentLabel}
|
|
72
75
|
activePresetId={activePresetId}
|
|
73
76
|
effort={effort}
|
|
77
|
+
readOnly={readOnly}
|
|
74
78
|
/>
|
|
75
79
|
);
|
|
76
80
|
}
|
package/src/StatusBar.tsx
CHANGED
|
@@ -35,6 +35,8 @@ interface IProps {
|
|
|
35
35
|
activeAgentLabel?: string;
|
|
36
36
|
activePresetId?: string;
|
|
37
37
|
effort?: TModelEffortSelection;
|
|
38
|
+
/** This terminal only observes the session. */
|
|
39
|
+
readOnly?: boolean;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
interface IStatusLeftProps {
|
|
@@ -53,8 +55,12 @@ interface IStatusLeftProps {
|
|
|
53
55
|
showGitBranch: boolean;
|
|
54
56
|
activePresetId?: string;
|
|
55
57
|
effort?: TModelEffortSelection;
|
|
58
|
+
readOnly: boolean;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/** What an observing terminal shows first on its status line. */
|
|
62
|
+
export const READ_ONLY_STATUS = 'Observing — read only';
|
|
63
|
+
|
|
58
64
|
/** Return the color for the context percentage indicator */
|
|
59
65
|
function getContextColor(percentage: number, palette: IThemeColors): string {
|
|
60
66
|
if (percentage >= CONTEXT_RED_THRESHOLD) return palette.text.error;
|
|
@@ -104,6 +110,15 @@ function ContextText({
|
|
|
104
110
|
);
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
export function ReadOnlyText(): React.ReactElement {
|
|
114
|
+
const palette = usePalette();
|
|
115
|
+
return (
|
|
116
|
+
<Text color={palette.text.warning} bold>
|
|
117
|
+
{READ_ONLY_STATUS}
|
|
118
|
+
</Text>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
function ModeText({ permissionMode }: { permissionMode: TPermissionMode }): React.ReactElement {
|
|
108
123
|
const palette = usePalette();
|
|
109
124
|
return (
|
|
@@ -183,6 +198,13 @@ function StatusLeft(props: IStatusLeftProps): React.ReactElement {
|
|
|
183
198
|
const showActivePreset = shouldShowActivePreset(activePresetId);
|
|
184
199
|
return (
|
|
185
200
|
<Text>
|
|
201
|
+
{props.readOnly && (
|
|
202
|
+
<>
|
|
203
|
+
<ReadOnlyText />
|
|
204
|
+
{/* In screen-reader mode the next field brings its own separator. */}
|
|
205
|
+
{!screenReader && SEP}
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
186
208
|
{!screenReader && (
|
|
187
209
|
<StatusActivityText
|
|
188
210
|
isThinking={props.isThinking}
|
|
@@ -253,6 +275,7 @@ export default function StatusBar({
|
|
|
253
275
|
activeAgentLabel,
|
|
254
276
|
activePresetId,
|
|
255
277
|
effort,
|
|
278
|
+
readOnly = false,
|
|
256
279
|
}: IProps): React.ReactElement {
|
|
257
280
|
const palette = usePalette();
|
|
258
281
|
return (
|
|
@@ -273,6 +296,7 @@ export default function StatusBar({
|
|
|
273
296
|
showGitBranch={showGitBranch}
|
|
274
297
|
activePresetId={activePresetId}
|
|
275
298
|
effort={effort}
|
|
299
|
+
readOnly={readOnly}
|
|
276
300
|
/>
|
|
277
301
|
{activeAgentLabel !== undefined && (
|
|
278
302
|
<Text color={palette.text.warning} bold>
|