@robota-sdk/agent-ui-web 3.3.0 → 3.3.1
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 +11 -0
- package/dist/index.cjs +81 -81
- package/dist/index.d.cts +61 -61
- package/dist/index.d.ts +61 -61
- package/dist/index.js +81 -81
- package/dist/index.js.map +1 -1
- package/docs/SPEC.md +1 -1
- package/package.json +6 -6
- package/src/client/ws-session-client.ts +1 -1
- package/src/clipboard.ts +2 -2
- package/src/components/AgentActivityPanel.tsx +8 -8
- package/src/components/AgentSwitcherSheet.tsx +2 -2
- package/src/components/Composer.tsx +27 -27
- package/src/components/ConversationView.tsx +14 -14
- package/src/components/Dialog.tsx +6 -6
- package/src/components/DiffLines.tsx +2 -2
- package/src/components/ExecutionDetailSheet.tsx +1 -1
- package/src/components/HelpSheet.tsx +2 -2
- package/src/components/PermissionPrompt.tsx +22 -22
- package/src/components/PopupMenu.tsx +1 -1
- package/src/components/ProjectPanel.tsx +2 -2
- package/src/components/SessionMonitor.tsx +1 -1
- package/src/components/SessionSidebar.tsx +8 -8
- package/src/components/SessionSurface.tsx +18 -18
- package/src/components/SessionSurfaceChrome.tsx +8 -8
- package/src/components/SettingsGeneralSection.tsx +1 -1
- package/src/components/SettingsMcpSection.tsx +1 -1
- package/src/components/SettingsPermissionsSection.tsx +1 -1
- package/src/components/SettingsPluginsSection.tsx +1 -1
- package/src/components/SettingsProvidersSection.tsx +3 -3
- package/src/components/SettingsScreen.tsx +3 -3
- package/src/components/StatusControls.tsx +12 -12
- package/src/components/composer-attachments.ts +5 -5
- package/src/driver-labels.ts +3 -3
- package/src/execution-entry-status.ts +1 -1
- package/src/goal-view.ts +1 -1
- package/src/hooks/command-menu.ts +2 -2
- package/src/hooks/excluded-commands.ts +2 -2
- package/src/hooks/prompt-state.ts +4 -4
- package/src/hooks/server-message-handling.ts +11 -11
- package/src/hooks/session-client-types.ts +29 -29
- package/src/hooks/ui-intent-state.ts +6 -6
- package/src/hooks/use-agent-switcher-state.ts +2 -2
- package/src/hooks/use-media-query.ts +1 -1
- package/src/hooks/use-model-list.ts +1 -1
- package/src/hooks/use-project-panel.ts +1 -1
- package/src/hooks/use-schedules-state.ts +4 -4
- package/src/hooks/use-session-directory.ts +4 -4
- package/src/hooks/use-settings-state.ts +4 -4
- package/src/hooks/useSessionClient.ts +44 -44
- package/src/index.ts +5 -5
- package/src/schedule-view.ts +1 -1
- package/src/styles/surface.css +1 -1
package/docs/SPEC.md
CHANGED
|
@@ -95,7 +95,7 @@ does not own session lifecycle, conversation history, or agent runtime state.
|
|
|
95
95
|
representation, when it has one, is only its disposable wake timer — cancelling that would leave
|
|
96
96
|
the loop itself running. Every other entry sends `cancel-background-task`.
|
|
97
97
|
- The Project panel (git status, one file's diff, project memory) correlates its own requests the
|
|
98
|
-
same way, and every file status it shows is a plain word, never a raw git XY code (
|
|
98
|
+
same way, and every file status it shows is a plain word, never a raw git XY code ("usable by
|
|
99
99
|
anyone"); a workspace that is not a git repository, or a diff whose path the host refused, is one
|
|
100
100
|
plain sentence, never left as a stuck loading state.
|
|
101
101
|
- A session error finalizes any partial streamed text and clears thinking/running-tool state
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-ui-web",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Browser-safe React session presentation, wire reducer, WebSocket client and runtime-host contract for consumer-owned agent renderers.",
|
|
5
5
|
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
6
6
|
"type": "module",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
"pretendard": "^1.3.9",
|
|
61
61
|
"react-markdown": "^10.1.0",
|
|
62
62
|
"remark-gfm": "^4.0.1",
|
|
63
|
-
"@robota-sdk/agent-interface-command": "^3.2.
|
|
64
|
-
"@robota-sdk/agent-interface-
|
|
65
|
-
"@robota-sdk/agent-
|
|
66
|
-
"@robota-sdk/agent-interface-
|
|
67
|
-
"@robota-sdk/agent-
|
|
63
|
+
"@robota-sdk/agent-interface-command": "^3.2.3",
|
|
64
|
+
"@robota-sdk/agent-interface-session": "^3.2.3",
|
|
65
|
+
"@robota-sdk/agent-transport": "^3.3.1",
|
|
66
|
+
"@robota-sdk/agent-interface-transport": "^3.2.3",
|
|
67
|
+
"@robota-sdk/agent-interface-execution": "^3.2.3"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"react": ">=18"
|
|
@@ -72,7 +72,7 @@ export function createWsSessionClient(
|
|
|
72
72
|
ws.onmessage = (event: MessageEvent): void => {
|
|
73
73
|
const data = event.data;
|
|
74
74
|
if (typeof data !== 'string') return;
|
|
75
|
-
//
|
|
75
|
+
// `raw → owner decoder → typed`. A malformed frame (invalid JSON or an invalid
|
|
76
76
|
// shape) must not throw inside the handler (it would break the socket's message pump and
|
|
77
77
|
// freeze the UI) and must not be cast onward; surface it via the normal path.
|
|
78
78
|
const decoded = decodeFrame(data, decodeServerMessage);
|
package/src/clipboard.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Copies `text` to the clipboard for the "Copy code" / "Copy message" buttons
|
|
4
|
+
* Copies `text` to the clipboard for the "Copy code" / "Copy message" buttons: the async
|
|
5
5
|
* Clipboard API first, and — for a page that denies it (no permission granted yet, a browser that
|
|
6
6
|
* never implements it, or a context that disallows it outright) — a same-tick `execCommand('copy')`
|
|
7
7
|
* fallback through a throwaway, invisible textarea. Rejects only once both have failed, so a caller
|
|
@@ -59,7 +59,7 @@ const FEEDBACK_MS = 2000;
|
|
|
59
59
|
/**
|
|
60
60
|
* The "Copy code" / "Copy message" buttons' shared feedback state: `copied` or `error` for a couple of
|
|
61
61
|
* seconds, then back to `idle` — drives both the button's own label swap and an `aria-live` region a
|
|
62
|
-
* screen reader announces without moving focus
|
|
62
|
+
* screen reader announces without moving focus.
|
|
63
63
|
*/
|
|
64
64
|
export function useCopyFeedback(): { status: TCopyStatus; copy: (text: string) => void } {
|
|
65
65
|
const [status, setStatus] = useState<TCopyStatus>('idle');
|
|
@@ -31,22 +31,22 @@ import type { IGoalState } from '@robota-sdk/agent-interface-session';
|
|
|
31
31
|
|
|
32
32
|
interface IAgentActivityPanelProps {
|
|
33
33
|
tasks: readonly IExecutionWorkspaceEntry[];
|
|
34
|
-
/**
|
|
34
|
+
/** The "Scheduled" group — every `kind: 'scheduled'` background task. */
|
|
35
35
|
schedules?: readonly IBackgroundTaskState<'scheduled'>[];
|
|
36
|
-
/**
|
|
36
|
+
/** The current goal (`/goal`), or null/absent when none is set. */
|
|
37
37
|
goal?: IGoalState | null;
|
|
38
38
|
onPauseSchedule?: (taskId: string) => void;
|
|
39
39
|
onResumeSchedule?: (taskId: string) => void;
|
|
40
40
|
onDeleteSchedule?: (taskId: string) => void;
|
|
41
41
|
onCancelGoal?: () => void;
|
|
42
42
|
className?: string;
|
|
43
|
-
/**
|
|
43
|
+
/** Open a non-main-thread entry's detail sheet. */
|
|
44
44
|
onSelect?: (entry: IExecutionWorkspaceEntry) => void;
|
|
45
|
-
/**
|
|
45
|
+
/** The main thread entry was clicked — the conversation beside this panel is it. */
|
|
46
46
|
onReturnToConversation?: () => void;
|
|
47
|
-
/**
|
|
47
|
+
/** Stop was clicked on a running/queued entry. */
|
|
48
48
|
onStop?: (entry: IExecutionWorkspaceEntry) => void;
|
|
49
|
-
/**
|
|
49
|
+
/** The entry whose detail sheet is currently open, for a highlighted row. */
|
|
50
50
|
selectedEntryId?: string;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -221,7 +221,7 @@ function AgentCard({
|
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
/**
|
|
224
|
+
/** One row of the "Scheduled" group — what it runs, when next, status, controls. */
|
|
225
225
|
function ScheduleCard({
|
|
226
226
|
task,
|
|
227
227
|
onPause,
|
|
@@ -281,7 +281,7 @@ function ScheduleCard({
|
|
|
281
281
|
);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
/**
|
|
284
|
+
/** The current goal — objective, plain-word status, and Cancel. */
|
|
285
285
|
function GoalCard({
|
|
286
286
|
goal,
|
|
287
287
|
onCancel,
|
|
@@ -8,12 +8,12 @@ import { Dialog } from './Dialog.js';
|
|
|
8
8
|
import type { IWsSessionState } from '../hooks/useSessionClient.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* The agent switcher: `show-agent-switcher` (`/agent` with no args) opens this
|
|
12
12
|
* sheet instead of the surface's old "not available" line. It lists every agent this session can
|
|
13
13
|
* run — name, one-line description, and where each is defined in plain words — with the current one
|
|
14
14
|
* checked. Choosing a row runs the SAME path as typing `/agent <name>` (a plain `command`, wired in
|
|
15
15
|
* `useAgentSwitcherState`); the reply is this sheet's own confirmation line, never a conversation
|
|
16
|
-
* card
|
|
16
|
+
* card.
|
|
17
17
|
*/
|
|
18
18
|
export function AgentSwitcherSheet({ state }: { state: IWsSessionState }): React.ReactElement | null {
|
|
19
19
|
if (!state.agentSwitcherOpen) return null;
|
|
@@ -28,7 +28,7 @@ export interface IComposerHandle {
|
|
|
28
28
|
focus: () => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** The draft as persisted: the typed text plus any attachment chips
|
|
31
|
+
/** The draft as persisted: the typed text plus any attachment chips. */
|
|
32
32
|
interface IStoredDraft {
|
|
33
33
|
readonly text: string;
|
|
34
34
|
readonly attachments: readonly IDraftAttachment[];
|
|
@@ -37,7 +37,7 @@ interface IStoredDraft {
|
|
|
37
37
|
const EMPTY_DRAFT: IStoredDraft = { text: '', attachments: [] };
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* A command whose whole job is opening a GUI screen — choosing it from the `/` menu runs
|
|
41
41
|
* it at once (its `ui_intent` opens the screen the normal way) instead of filling the draft and
|
|
42
42
|
* waiting for Enter, since there is nothing useful to type after it. `help` never reaches the
|
|
43
43
|
* session at all (`useSessionClient.ts`'s `send` opens the Help sheet locally). Every other command
|
|
@@ -55,14 +55,14 @@ function menuGroupLabel(item: ICommandMenuItem): 'Commands' | 'Skills' {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* The unsent draft survives a Chat → Usage → Chat switch (the composer unmounts), a page
|
|
59
59
|
* reload, and a desktop relaunch — `localStorage`, not `sessionStorage`, since only `localStorage`
|
|
60
60
|
* survives a closed window/tab being reopened. Namespaced (the configured draft prefix, matching
|
|
61
61
|
* the configured restore key in `use-session-directory.ts`) so a page hosting other state under the
|
|
62
62
|
* same origin does not collide. Per session id when one is known; a single fallback key before the
|
|
63
63
|
* first status arrives (the gap is brief and is reconciled once it does — see the effect below).
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* The stored value is now JSON (`IStoredDraft`), not the bare text string it used to be —
|
|
66
66
|
* `parseStoredDraft` treats anything that does not parse as that shape (including a draft saved
|
|
67
67
|
* before this change shipped) as plain text with no attachments, so an old stored draft still loads.
|
|
68
68
|
*/
|
|
@@ -141,38 +141,38 @@ const ComposerInstance = forwardRef<
|
|
|
141
141
|
catalog: TCommandCatalog | null;
|
|
142
142
|
status: TSessionStatus | null;
|
|
143
143
|
/**
|
|
144
|
-
* False while the transport is not `connected
|
|
144
|
+
* False while the transport is not `connected`: Enter and Send refuse to submit,
|
|
145
145
|
* and nothing typed is cleared or lost — the composer never sends into a socket that is not there.
|
|
146
|
-
*
|
|
146
|
+
* The status chips and the `/` command menu are disabled the same way.
|
|
147
147
|
*/
|
|
148
148
|
connected?: boolean;
|
|
149
|
-
/**
|
|
149
|
+
/** A turn (or a blocking command) is running — Send becomes Stop and Esc stops it too. */
|
|
150
150
|
running: boolean;
|
|
151
151
|
onStop: () => void;
|
|
152
|
-
/**
|
|
152
|
+
/** The prompt queued behind the running turn, or null when none is queued. */
|
|
153
153
|
queued: IQueuedPrompt | null;
|
|
154
154
|
onCancelQueue: () => void;
|
|
155
155
|
/**
|
|
156
156
|
* Opens the host's native multi-file dialog with real filesystem paths — present only on the
|
|
157
|
-
* desktop app
|
|
157
|
+
* desktop app. Its absence means the attach button falls back to a plain HTML file
|
|
158
158
|
* picker, whose picks a browser can never resolve to a path (rule 3: shown plainly, nothing
|
|
159
159
|
* attached).
|
|
160
160
|
*/
|
|
161
161
|
pickFiles?: () => Promise<readonly IPickedFile[]>;
|
|
162
162
|
/**
|
|
163
163
|
* Resolves a dropped or picked `File` to its real filesystem path — present only on the desktop
|
|
164
|
-
* app, via Electron's `webUtils.getPathForFile
|
|
164
|
+
* app, via Electron's `webUtils.getPathForFile`. Its absence means a drop can never
|
|
165
165
|
* become an `@`-reference either.
|
|
166
166
|
*/
|
|
167
167
|
getPathForFile?: (file: File) => string;
|
|
168
|
-
/**
|
|
168
|
+
/** The model control's pop-up menu — null until requested. */
|
|
169
169
|
modelList?: TModelListSnapshot | null;
|
|
170
170
|
onRequestModelList?: () => void;
|
|
171
171
|
/** Applies a model/mode/effort choice without a conversation card (the control's label confirms it). */
|
|
172
172
|
onSilentCommand?: (name: string, args?: string) => void;
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
175
|
-
* dispatching the bare `/provider` command (the
|
|
174
|
+
* "Manage providers…" opens Settings at the Providers & Models section. Defaults to
|
|
175
|
+
* dispatching the bare `/provider` command (the legacy behavior) for a caller that has not
|
|
176
176
|
* wired Settings — every shipped caller passes its own `state.openSettings('providers')`.
|
|
177
177
|
*/
|
|
178
178
|
onManageProviders?: () => void;
|
|
@@ -238,25 +238,25 @@ const ComposerInstance = forwardRef<
|
|
|
238
238
|
const [attachmentNotice, setAttachmentNotice] = useState<string | null>(null);
|
|
239
239
|
const [selected, setSelected] = useState(0);
|
|
240
240
|
const [dismissed, setDismissed] = useState(false);
|
|
241
|
-
//
|
|
241
|
+
// The slash menu is disabled while disconnected — none of its commands could run.
|
|
242
242
|
const menu = dismissed || !connected ? null : commandMenuFor(catalog, draft);
|
|
243
243
|
useEffect(() => {
|
|
244
244
|
setSelected(0);
|
|
245
245
|
setDismissed(false);
|
|
246
246
|
}, [draft]);
|
|
247
|
-
//
|
|
247
|
+
// The menu can hold more rows than fit in its scroll area — keyboard navigation (below)
|
|
248
248
|
// must keep the highlighted row in view, not just move the highlight off-screen.
|
|
249
249
|
const menuOptionRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
250
250
|
useEffect(() => {
|
|
251
251
|
menuOptionRefs.current[selected]?.scrollIntoView?.({ block: 'nearest' });
|
|
252
252
|
}, [selected, menu]);
|
|
253
|
-
/**
|
|
253
|
+
/** Every draft change is persisted at once, so a reload or relaunch loses nothing. */
|
|
254
254
|
const setDraft = (value: string): void => {
|
|
255
255
|
stateRef.current = { ...stateRef.current, text: value };
|
|
256
256
|
setDraftState(value);
|
|
257
257
|
if (draftMode === 'persistent') writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
|
|
258
258
|
};
|
|
259
|
-
/**
|
|
259
|
+
/** Attachment chips persist alongside the text, under the same per-session draft key. */
|
|
260
260
|
const setAttachments = (
|
|
261
261
|
updater: readonly IDraftAttachment[] | ((current: readonly IDraftAttachment[]) => readonly IDraftAttachment[]),
|
|
262
262
|
): void => {
|
|
@@ -265,7 +265,7 @@ const ComposerInstance = forwardRef<
|
|
|
265
265
|
setAttachmentsState(next);
|
|
266
266
|
if (draftMode === 'persistent') writeDraft(browserNamespace, sessionIdRef.current, stateRef.current);
|
|
267
267
|
};
|
|
268
|
-
//
|
|
268
|
+
// A session switch shows THAT session's own saved draft, never what was typed for
|
|
269
269
|
// another one. The session id becoming known for the very FIRST time (the fallback key was in use
|
|
270
270
|
// until now) instead carries over what is already typed, rather than discarding it. A transient
|
|
271
271
|
// `undefined` mid-switch, once a real id has already been seen, is not a change at all: stay bound
|
|
@@ -413,7 +413,7 @@ const ComposerInstance = forwardRef<
|
|
|
413
413
|
chooseMenuItem(item);
|
|
414
414
|
return true;
|
|
415
415
|
};
|
|
416
|
-
/**
|
|
416
|
+
/** Edit puts the queued text back in the draft and cancels the queue behind it (the wire
|
|
417
417
|
* has no per-message cancel — only a whole-queue clear). Offered only when exactly one prompt is
|
|
418
418
|
* queued: with more than one, `cancel-queue` would still drop every one of them, but only the
|
|
419
419
|
* shown prompt's text is known here, so "Edit" would silently lose the rest — the row offers only
|
|
@@ -432,7 +432,7 @@ const ComposerInstance = forwardRef<
|
|
|
432
432
|
onDragLeave={onDragLeave}
|
|
433
433
|
onDrop={onDrop}
|
|
434
434
|
>
|
|
435
|
-
{/*
|
|
435
|
+
{/* The drop target is announced even to someone who cannot drag a file —
|
|
436
436
|
the attach button beside the textarea is how they reach the same result. */}
|
|
437
437
|
<p id="composer-attach-hint" className="sr-only">
|
|
438
438
|
Drag files here, or use Attach files, to add them to your message.
|
|
@@ -493,7 +493,7 @@ const ComposerInstance = forwardRef<
|
|
|
493
493
|
className="gui-rise absolute bottom-full left-0 right-0 mb-2 max-h-[320px] overflow-y-auto rounded-2xl bg-popover p-1.5 shadow-2xl shadow-black/35"
|
|
494
494
|
>
|
|
495
495
|
{menu.map((item, index) => {
|
|
496
|
-
//
|
|
496
|
+
// "Commands" then "Skills" — a header appears once, above the first row of
|
|
497
497
|
// its group (the menu always lists every command before every skill, so a group's rows
|
|
498
498
|
// are contiguous).
|
|
499
499
|
const showGroupHeader = index === 0 || menuGroupLabel(menu[index - 1]!) !== menuGroupLabel(item);
|
|
@@ -590,10 +590,10 @@ const ComposerInstance = forwardRef<
|
|
|
590
590
|
value={draft}
|
|
591
591
|
onChange={(e) => setDraft(e.target.value)}
|
|
592
592
|
onKeyDown={(e) => {
|
|
593
|
-
//
|
|
593
|
+
// An Enter that only finishes an IME (Korean/Japanese/Chinese) composition must
|
|
594
594
|
// not submit or accept the menu — `isComposing` (or `keyCode` 229, on browsers that
|
|
595
595
|
// predate it) marks it; the keystroke is left alone so the browser commits the composition
|
|
596
|
-
// normally, matching the guard the free-text prompt field uses
|
|
596
|
+
// normally, matching the guard the free-text prompt field uses.
|
|
597
597
|
if (e.key === 'Enter' && (e.nativeEvent.isComposing || e.keyCode === 229)) return;
|
|
598
598
|
if (menu) {
|
|
599
599
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
@@ -612,9 +612,9 @@ const ComposerInstance = forwardRef<
|
|
|
612
612
|
return;
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
|
-
//
|
|
615
|
+
// Esc stops a running turn — but only once the menu (handled above) is out of
|
|
616
616
|
// the way, so dismissing the `/` menu never doubles as an abort. Not while disconnected:
|
|
617
|
-
// an abort could not reach the host either
|
|
617
|
+
// an abort could not reach the host either.
|
|
618
618
|
if (e.key === 'Escape' && running && connected) {
|
|
619
619
|
e.preventDefault();
|
|
620
620
|
onStop();
|
|
@@ -632,7 +632,7 @@ const ComposerInstance = forwardRef<
|
|
|
632
632
|
<button
|
|
633
633
|
type="button"
|
|
634
634
|
aria-label="Attach files"
|
|
635
|
-
//
|
|
635
|
+
// The attach button follows the same disconnected rule as the status
|
|
636
636
|
// chips and the `/` menu — a short "Reconnecting…" tooltip in place of its usual one.
|
|
637
637
|
title={connected ? 'Attach files' : 'Reconnecting…'}
|
|
638
638
|
disabled={!connected}
|
|
@@ -729,4 +729,4 @@ export function GoalBar({
|
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
// `StatusRow` (model/mode/effort pop-up menus) and its `ContextRing` moved to `StatusControls.tsx`
|
|
732
|
-
//
|
|
732
|
+
// — imported above.
|
|
@@ -44,7 +44,7 @@ function nodeText(node: React.ReactNode): string {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* "Copy code" / "Copy message"
|
|
47
|
+
* "Copy code" / "Copy message": shows on hover and keyboard focus, always on touch (no
|
|
48
48
|
* `hover` capability to gate on there). The transient "Copied" / "Couldn't copy" feedback lives in a
|
|
49
49
|
* visually hidden `aria-live` region that is a SIBLING of the button, not a descendant of it — ARIA
|
|
50
50
|
* name computation only ever looks at an element's own subtree, so a live region placed anywhere
|
|
@@ -96,7 +96,7 @@ function CopyButton({
|
|
|
96
96
|
/**
|
|
97
97
|
* A fenced code block: unwrapped and horizontally scrollable (a visible scrollbar via
|
|
98
98
|
* `.gui-code-scroll`, `tabIndex=0` and a name so a keyboard user without a mouse wheel can still reach
|
|
99
|
-
* the rest of a long line), with its own "Copy code" button revealed on hover/focus
|
|
99
|
+
* the rest of a long line), with its own "Copy code" button revealed on hover/focus.
|
|
100
100
|
*/
|
|
101
101
|
function CodeBlockRenderer({ children }: { children?: React.ReactNode }): React.ReactElement {
|
|
102
102
|
const text = nodeText(children).replace(/\n$/, '');
|
|
@@ -123,7 +123,7 @@ interface IConversationViewProps {
|
|
|
123
123
|
activeTools: IActiveTool[];
|
|
124
124
|
streamingText: string;
|
|
125
125
|
isThinking: boolean;
|
|
126
|
-
/** This connection's own driver id
|
|
126
|
+
/** This connection's own driver id, threaded to every message it renders. */
|
|
127
127
|
ownDriverId: TDriverId | null;
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -207,11 +207,11 @@ function UserBlock({
|
|
|
207
207
|
}: {
|
|
208
208
|
content: string;
|
|
209
209
|
author?: string;
|
|
210
|
-
/** This connection's own driver id
|
|
210
|
+
/** This connection's own driver id: its own turns never carry a "from" label. */
|
|
211
211
|
ownDriverId: TDriverId | null;
|
|
212
212
|
}): React.ReactElement {
|
|
213
|
-
//
|
|
214
|
-
// connection's own — in plain words, never the raw server-assigned id.
|
|
213
|
+
// Display-only: show WHO drove this turn when it wasn't this
|
|
214
|
+
// connection's own — in plain words, never the raw server-assigned id. AGENT_DRIVER_ID
|
|
215
215
|
// (never "the same surface" — see isSameSurface) reads "Automatic — loop": today it is only ever
|
|
216
216
|
// a loop's own injected turn (requestWakeup in interactive-session.ts).
|
|
217
217
|
const coDriver =
|
|
@@ -239,7 +239,7 @@ function AgentBlock({
|
|
|
239
239
|
{isStreaming && (
|
|
240
240
|
<span className="ml-1 inline-block h-2.5 w-2.5 animate-pulse rounded-full bg-foreground/70 align-middle" />
|
|
241
241
|
)}
|
|
242
|
-
{/*
|
|
242
|
+
{/* Every assistant message can copy its own Markdown source; a still-streaming one
|
|
243
243
|
has nothing settled yet to copy, so the button waits for it to finish. */}
|
|
244
244
|
{!isStreaming && (
|
|
245
245
|
<div className="flex opacity-0 transition-opacity focus-within:opacity-100 group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100">
|
|
@@ -264,7 +264,7 @@ function ToolIcon({ name, className }: { name: string; className?: string }): Re
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
267
|
+
* A long path's directory is shortened FROM THE LEFT so the filename — the part someone
|
|
268
268
|
* actually needs to recognise — is never the part that gets cut.
|
|
269
269
|
*/
|
|
270
270
|
const PATH_DISPLAY_MAX_CHARS = 52;
|
|
@@ -405,7 +405,7 @@ function ToolCard({ tool }: { tool: IActiveTool }): React.ReactElement {
|
|
|
405
405
|
Boolean(parsedResult?.structured);
|
|
406
406
|
const hasObservations = Boolean(tool.toolResultParts?.length);
|
|
407
407
|
const expandable = hasDiff || hasOutput || hasObservations;
|
|
408
|
-
//
|
|
408
|
+
// A projected `/command` tool shows its command, never the internal provider tool name.
|
|
409
409
|
const label = tool.commandName ? `Ran /${tool.commandName}` : tool.name;
|
|
410
410
|
|
|
411
411
|
return (
|
|
@@ -431,7 +431,7 @@ function ToolCard({ tool }: { tool: IActiveTool }): React.ReactElement {
|
|
|
431
431
|
{label}
|
|
432
432
|
</span>
|
|
433
433
|
{tool.displayPath ? (
|
|
434
|
-
//
|
|
434
|
+
// Server-computed, workspace-relative, and never middle/end-truncated — only the
|
|
435
435
|
// directory portion is shortened, from the left, so the filename always reads in full.
|
|
436
436
|
<span
|
|
437
437
|
className="min-w-0 flex-1 break-all font-mono text-[12.5px] text-subtle"
|
|
@@ -493,7 +493,7 @@ const COMMAND_TONE: Record<ICommandOutputEntry['tone'], { dot: string; text: str
|
|
|
493
493
|
};
|
|
494
494
|
|
|
495
495
|
/**
|
|
496
|
-
* A slash command's outcome, where it was typed: line breaks kept, long output folded.
|
|
496
|
+
* A slash command's outcome, where it was typed: line breaks kept, long output folded. The
|
|
497
497
|
* card itself still marks the command (the `/name` header stays monospace, a fixed-width label rather
|
|
498
498
|
* than prose), but its BODY reads as normal text — proportional, normal size — unless it is actually a
|
|
499
499
|
* long listing (the same measure that decides whether to fold it: many raw lines, or tall once
|
|
@@ -554,7 +554,7 @@ function CommandCard({ entry }: { entry: ICommandOutputEntry }): React.ReactElem
|
|
|
554
554
|
/** One line per finished turn's tool calls; the calls themselves open on demand. */
|
|
555
555
|
function ToolGroup({ tools }: { tools: readonly IActiveTool[] }): React.ReactElement | null {
|
|
556
556
|
const [open, setOpen] = useState(false);
|
|
557
|
-
//
|
|
557
|
+
// An internal signal tool (e.g. the goal-status tool) is never shown as a call — the goal
|
|
558
558
|
// bar shows progress instead. A group made up ENTIRELY of internal tools renders nothing at all.
|
|
559
559
|
const visible = tools.filter((tool) => !tool.internal);
|
|
560
560
|
const failed = visible.filter((tool) => tool.status === 'error').length;
|
|
@@ -605,7 +605,7 @@ function ToolGroup({ tools }: { tools: readonly IActiveTool[] }): React.ReactEle
|
|
|
605
605
|
);
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
/**
|
|
608
|
+
/** A turn that changed files ends with this compact row — a click opens that file's diff. */
|
|
609
609
|
function ChangedFilesRow({ files }: { files: readonly IChangedFileSummary[] }): React.ReactElement {
|
|
610
610
|
const [openPath, setOpenPath] = useState<string | null>(null);
|
|
611
611
|
return (
|
|
@@ -671,7 +671,7 @@ export function ConversationView({
|
|
|
671
671
|
};
|
|
672
672
|
|
|
673
673
|
/**
|
|
674
|
-
*
|
|
674
|
+
* Follows new content only while the person is already at the bottom: streaming text, a
|
|
675
675
|
* finished message, a tool starting or finishing. Expanding something already on screen (a tool row,
|
|
676
676
|
* a folded command card) touches none of these, so it never causes a jump — the effect simply does
|
|
677
677
|
* not run for it. An instant jump rather than a smooth one, so it can never itself be mistaken by
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import React, { useEffect, useId, useRef } from 'react';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The shared modal-shell primitive (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The shared modal-shell primitive (the model/mode controls and the
|
|
7
|
+
* session-delete confirmation both need a confirmation dialog). Both
|
|
8
|
+
* consumers share it rather than shipping a second copy.
|
|
9
9
|
*
|
|
10
10
|
* `role="dialog"` (or `"alertdialog"`, see `role` below), `aria-modal`, a labelled title, focus moved
|
|
11
11
|
* inside on open, a focus trap while open, Esc to close, and focus restored to whatever had it before
|
|
@@ -26,7 +26,7 @@ export interface IDialogProps {
|
|
|
26
26
|
/** Id of an element describing the dialog, wired to `aria-describedby`. */
|
|
27
27
|
describedById?: string;
|
|
28
28
|
/**
|
|
29
|
-
* Overrides what focus returns to when the dialog closes
|
|
29
|
+
* Overrides what focus returns to when the dialog closes. Default behavior captures
|
|
30
30
|
* `document.activeElement` when the dialog opens — which fails for a caller whose opener element
|
|
31
31
|
* is removed from the DOM in the SAME update that opens this dialog (e.g. a menu item whose menu
|
|
32
32
|
* closes as this dialog opens): by the time this dialog's open-effect runs, the browser has already
|
|
@@ -34,7 +34,7 @@ export interface IDialogProps {
|
|
|
34
34
|
*/
|
|
35
35
|
restoreFocusTo?: HTMLElement | null;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* `'alertdialog'` for a dialog that interrupts to demand a decision before anything else
|
|
38
38
|
* can happen (a destructive confirmation) — WAI-ARIA's distinction from a dialog the user can set
|
|
39
39
|
* aside. Default `'dialog'`.
|
|
40
40
|
*/
|
|
@@ -182,7 +182,7 @@ export interface IConfirmDialogProps {
|
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
184
|
* A title, a body, Cancel focused by default, and a confirm button that can be styled destructive.
|
|
185
|
-
*
|
|
185
|
+
* `destructive` also picks the dialog's ARIA role — `"alertdialog"` when it is (an action
|
|
186
186
|
* that demands a decision before anything else, e.g. deleting a session), `"dialog"` otherwise.
|
|
187
187
|
*/
|
|
188
188
|
export function ConfirmDialog({
|
|
@@ -5,7 +5,7 @@ import React, { useState } from 'react';
|
|
|
5
5
|
import type { IDiffLine } from '@robota-sdk/agent-interface-session';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Renders a unified diff already built server-side (the ONE diff builder, in
|
|
9
9
|
* `interactive-session-streaming.ts`) — a tool row's expanded result and the Edit/Write permission
|
|
10
10
|
* prompt both show the SAME diff through this, so approving a change and reviewing it afterwards
|
|
11
11
|
* look identical. This component never computes a diff itself, only displays one.
|
|
@@ -24,7 +24,7 @@ const DIFF_LINE_PREFIX: Record<IDiffLine['type'], string> = {
|
|
|
24
24
|
hunk: '',
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// The server already caps each diff side at 500 lines — this is a second,
|
|
28
28
|
// independent fold so a diff that size never renders into the DOM at once either.
|
|
29
29
|
const DIFF_FOLD_LINES = 40;
|
|
30
30
|
const DIFF_EXPANDED_CAP_LINES = 500;
|
|
@@ -28,7 +28,7 @@ const RECORD_KIND_LABEL: Record<IExecutionDetailRecord['kind'], string> = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Opening an Agents panel entry — status, what it was asked (one line, from its
|
|
32
32
|
* headline), its transcript so far (paginated via `onLoadMore`) and, once it has one, its result.
|
|
33
33
|
* `entry === null` renders nothing; the caller owns showing/hiding this over the panel.
|
|
34
34
|
*/
|
|
@@ -7,7 +7,7 @@ import { Dialog } from './Dialog.js';
|
|
|
7
7
|
|
|
8
8
|
import type { TCommandCatalog } from '../hooks/session-client-types.js';
|
|
9
9
|
|
|
10
|
-
/** One keyboard shortcut this surface answers
|
|
10
|
+
/** One keyboard shortcut this surface answers */
|
|
11
11
|
interface IShortcut {
|
|
12
12
|
readonly keys: string;
|
|
13
13
|
readonly description: string;
|
|
@@ -22,7 +22,7 @@ const SHORTCUTS: readonly IShortcut[] = [
|
|
|
22
22
|
];
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* The GUI's Help sheet
|
|
25
|
+
* The GUI's Help sheet, a `Dialog` opened by `/help` — client-only: `useSessionClient.ts`'s
|
|
26
26
|
* `send` catches `/help` before it ever reaches the session and opens this instead, so there is no
|
|
27
27
|
* round trip and nothing terminal-style to replace it with. It lists the same commands and skills the
|
|
28
28
|
* `/` menu offers (`commandMenuFor`, unfiltered by any query — an excluded command has no row here
|