@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
|
@@ -19,19 +19,19 @@ import type { TActionResponse } from '@robota-sdk/agent-interface-transport';
|
|
|
19
19
|
import type { TDriverId } from '@robota-sdk/agent-interface-session';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Renders the owner's pending permission/ask prompts
|
|
22
|
+
* Renders the owner's pending permission/ask prompts. Under local == remote the paired
|
|
23
23
|
* browser owner answers its OWN prompts; the first pending prompt is shown. A gated tool call blocks
|
|
24
24
|
* until answered, so this is on the critical path — not decorative.
|
|
25
25
|
*
|
|
26
26
|
* `modal` covers the page; `dock` sits above the composer, as desktop agent apps place a pending
|
|
27
27
|
* question next to where the answer is typed. Once armed and focused, its keys answer it: 1–9 picks
|
|
28
28
|
* an option, Esc cancels a question or denies a permission — but it never takes that focus away from
|
|
29
|
-
* a field the person is typing in
|
|
29
|
+
* a field the person is typing in. A click on a button always answers at once. An ask whose
|
|
30
30
|
* request sets `allowFreeText` also shows a text field (masked when `masked` is set); Enter there
|
|
31
|
-
* submits it, and its own keys never reach the prompt's shortcuts
|
|
31
|
+
* submits it, and its own keys never reach the prompt's shortcuts. The field's typed value
|
|
32
32
|
* lives only in `FreeTextField`, keyed by `prompt.id`: a cancelled or answered prompt's text can
|
|
33
33
|
* never reach the next prompt's render, masked or not, because the field is a fresh component
|
|
34
|
-
* instance rather than a value carried over in this component's own state
|
|
34
|
+
* instance rather than a value carried over in this component's own state.
|
|
35
35
|
*
|
|
36
36
|
* An ask whose `maxSelect` is above 1 is a multi-select: each option is a toggle (a click or 1–9 ticks
|
|
37
37
|
* it, nothing answers yet), and Submit — or Enter at the prompt, once the selection is allowed —
|
|
@@ -43,7 +43,7 @@ interface IPermissionPromptProps {
|
|
|
43
43
|
prompts: readonly TPendingPrompt[];
|
|
44
44
|
onAnswerPermission: (id: string, result: boolean) => void;
|
|
45
45
|
onAnswerAsk: (id: string, response: TActionResponse) => void;
|
|
46
|
-
/** This connection's own driver id
|
|
46
|
+
/** This connection's own driver id: a prompt raised by its own turn shows no requester. */
|
|
47
47
|
ownDriverId?: TDriverId | null;
|
|
48
48
|
layout?: 'modal' | 'dock';
|
|
49
49
|
/** How long a new prompt waits before its keys answer it; tests pass their own. */
|
|
@@ -72,7 +72,7 @@ function isEditableElement(element: Element | null): boolean {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* The one destructive option in the provider profile action menu (the ask built
|
|
76
76
|
* by `askProviderProfileAction` in `agent-command`'s `provider-command-profile.ts`, `request.id`
|
|
77
77
|
* `'provider-profile-action'`): "Delete" answers to no digit and renders apart from Switch/Edit/Test/
|
|
78
78
|
* Duplicate, styled destructive — the exact audited bug ("Delete looks the same as Switch, answers to
|
|
@@ -132,7 +132,7 @@ export function PermissionPrompt({
|
|
|
132
132
|
const promptId = prompt?.id;
|
|
133
133
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
134
134
|
const fieldRef = useRef<HTMLInputElement>(null);
|
|
135
|
-
// A question with a text field
|
|
135
|
+
// A question with a text field: the field is the prompt's primary control, so focus goes
|
|
136
136
|
// there instead of the container wherever this component would otherwise focus the container itself.
|
|
137
137
|
const hasFreeText = prompt?.kind === 'ask' && prompt.request.allowFreeText === true;
|
|
138
138
|
const [armedId, setArmedId] = useState<string | undefined>(undefined);
|
|
@@ -162,8 +162,8 @@ export function PermissionPrompt({
|
|
|
162
162
|
// Answering one prompt renders the next into the same buttons, so a button focused to answer the
|
|
163
163
|
// last one would take Enter, Space or a held key as an answer to this one. Focus goes back to the
|
|
164
164
|
// prompt itself, whose keys wait for the prompt to arm. A reused button is still there to find this
|
|
165
|
-
// way — but the free-text field is its own component, remounted (not updated) for the new prompt.id
|
|
166
|
-
//
|
|
165
|
+
// way — but the free-text field is its own component, remounted (not updated) for the new prompt.id,
|
|
166
|
+
// and removing a focused DOM node drops focus to <body> as an intrinsic side
|
|
167
167
|
// effect, before this effect ever runs. `pendingFocusReturnRef` (set at answer time, and not yet
|
|
168
168
|
// cleared — the effect that clears it is a passive one, ordered after this layout effect) still
|
|
169
169
|
// remembers that focus was in this prompt, so that case is reclaimed too.
|
|
@@ -178,7 +178,7 @@ export function PermissionPrompt({
|
|
|
178
178
|
}
|
|
179
179
|
}, [promptId]);
|
|
180
180
|
// Once armed the prompt takes focus, so its keys answer it rather than type wherever the person
|
|
181
|
-
// already is — UNLESS that is an editable field, whose keystrokes must never be stolen
|
|
181
|
+
// already is — UNLESS that is an editable field, whose keystrokes must never be stolen.
|
|
182
182
|
useEffect(() => {
|
|
183
183
|
if (!armed) return;
|
|
184
184
|
if (isEditableElement(document.activeElement)) return;
|
|
@@ -199,8 +199,8 @@ export function PermissionPrompt({
|
|
|
199
199
|
}, [promptId, onFocusReturn]);
|
|
200
200
|
if (!prompt) return null;
|
|
201
201
|
|
|
202
|
-
//
|
|
203
|
-
// `provider-command-profile-lifecycle.ts`) is now built on the shared `ConfirmDialog`
|
|
202
|
+
// The provider-delete confirmation (`buildProviderDelete` in agent-command's
|
|
203
|
+
// `provider-command-profile-lifecycle.ts`) is now built on the shared `ConfirmDialog`
|
|
204
204
|
// instead of the generic ask grid — a destructive-styled "Delete", Cancel focused by default, and
|
|
205
205
|
// no accidental backdrop-click dismissal. `confirmAction`'s own `CONFIRM_YES`/`CONFIRM_NO` values
|
|
206
206
|
// ('yes'/'no', from `@robota-sdk/agent-core`) are answered directly; this bypasses the rest of this
|
|
@@ -220,7 +220,7 @@ export function PermissionPrompt({
|
|
|
220
220
|
);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// Display-only: the prompt belongs to the driver whose turn raised it. Shown so the owner
|
|
224
224
|
// can tell a co-driver's tool-gate from their own — it NEVER changes who is authorized to answer (owner).
|
|
225
225
|
const requesterLabel =
|
|
226
226
|
prompt.requesterDriverId && !isSameSurface(prompt.requesterDriverId, ownDriverId)
|
|
@@ -301,7 +301,7 @@ export function PermissionPrompt({
|
|
|
301
301
|
if (index < 2) answerPermission(prompt.id, index === 0);
|
|
302
302
|
return;
|
|
303
303
|
}
|
|
304
|
-
//
|
|
304
|
+
// A destructive option (Delete) is excluded from the digit-answerable list entirely —
|
|
305
305
|
// no stray digit, including the one it happened to render at before this menu was reordered, can
|
|
306
306
|
// ever answer it.
|
|
307
307
|
const digitOptions = (prompt.request.options ?? []).filter(
|
|
@@ -313,7 +313,7 @@ export function PermissionPrompt({
|
|
|
313
313
|
if (multi) toggleOption(option.value);
|
|
314
314
|
else answerAsk(prompt.id, { type: 'answer', values: [option.value] });
|
|
315
315
|
};
|
|
316
|
-
//
|
|
316
|
+
// An Edit/Write request carries the same server-built diff `tool_end` would show.
|
|
317
317
|
const hasDiff = prompt.kind === 'permission' && (prompt.diffLines?.length ?? 0) > 0;
|
|
318
318
|
const shellCommand =
|
|
319
319
|
prompt.kind === 'permission' && typeof prompt.toolArgs['command'] === 'string'
|
|
@@ -322,7 +322,7 @@ export function PermissionPrompt({
|
|
|
322
322
|
const isShellCommand = prompt.kind === 'permission' && prompt.toolName === 'Bash' && shellCommand !== undefined;
|
|
323
323
|
const askRequestId = prompt.kind === 'ask' ? prompt.request.id : undefined;
|
|
324
324
|
const askOptions = prompt.kind === 'ask' ? (prompt.request.options ?? []) : [];
|
|
325
|
-
//
|
|
325
|
+
// Delete (in the provider profile action menu) renders apart from the rest, destructive-
|
|
326
326
|
// styled and with no digit shortcut — every other ask's options are unaffected (`destructiveAskOption`
|
|
327
327
|
// stays undefined, so `primaryAskOptions` is just `askOptions`, same order, same shortcuts).
|
|
328
328
|
const destructiveAskOption = askOptions.find((opt) => isDestructiveAskOption(askRequestId, opt.value));
|
|
@@ -381,10 +381,10 @@ export function PermissionPrompt({
|
|
|
381
381
|
</>
|
|
382
382
|
)}
|
|
383
383
|
</p>
|
|
384
|
-
{/*
|
|
384
|
+
{/* An Edit/Write request with a server-built diff preview shows "Edit
|
|
385
385
|
<path>" (or "Background agent <label> wants to edit <path>") and the diff, in
|
|
386
386
|
place of "Allow <tool> to run?" and raw arguments.
|
|
387
|
-
|
|
387
|
+
A background agent's own request ALWAYS names it — whether or
|
|
388
388
|
not there's a diff to show — so this never reads as an unattributed ask for
|
|
389
389
|
exactly the riskiest calls (a diff-bearing Edit/Write). */}
|
|
390
390
|
{prompt.requester?.kind === 'background-agent' ? (
|
|
@@ -505,7 +505,7 @@ export function PermissionPrompt({
|
|
|
505
505
|
))}
|
|
506
506
|
</div>
|
|
507
507
|
)}
|
|
508
|
-
{/*
|
|
508
|
+
{/* Delete sits apart at the bottom, styled destructive, with no digit
|
|
509
509
|
shortcut — the audited bug was that it looked and behaved just like Switch. */}
|
|
510
510
|
{destructiveAskOption && (
|
|
511
511
|
<div className="flex flex-wrap items-start gap-3">
|
|
@@ -579,7 +579,7 @@ const BUTTON =
|
|
|
579
579
|
const PRIMARY_BUTTON = `${BUTTON} bg-primary text-primary-foreground hover:opacity-90`;
|
|
580
580
|
const SECONDARY_BUTTON = `${BUTTON} bg-raised text-foreground hover:bg-hover`;
|
|
581
581
|
const GHOST_BUTTON = `${BUTTON} text-muted-foreground hover:bg-hover hover:text-foreground`;
|
|
582
|
-
/**
|
|
582
|
+
/** A destructive ask option (Delete) — visually distinct from Switch/Edit/Test/Duplicate. */
|
|
583
583
|
const DESTRUCTIVE_BUTTON = `${BUTTON} bg-destructive/12 text-destructive hover:bg-destructive/20`;
|
|
584
584
|
|
|
585
585
|
/** Imperative escape hatch a parent can use to wipe the field's value without owning it. */
|
|
@@ -628,7 +628,7 @@ const FreeTextField = forwardRef<IFreeTextFieldHandle, IFreeTextFieldProps>(func
|
|
|
628
628
|
onCancel();
|
|
629
629
|
};
|
|
630
630
|
/**
|
|
631
|
-
* A key that reaches the field types into it — the prompt's own shortcuts never see it
|
|
631
|
+
* A key that reaches the field types into it — the prompt's own shortcuts never see it.
|
|
632
632
|
* The Enter that only finishes an IME composition (Korean/Japanese/Chinese) must not submit the
|
|
633
633
|
* still-uncommitted text — `isComposing` (or `keyCode` 229, on browsers that predate it) marks that
|
|
634
634
|
* Enter; the keystroke is left alone so the browser can commit the composition normally.
|
|
@@ -738,7 +738,7 @@ function ToolArgs({ args }: { args: unknown }): React.ReactElement | null {
|
|
|
738
738
|
}
|
|
739
739
|
|
|
740
740
|
/**
|
|
741
|
-
*
|
|
741
|
+
* A Shell request shows just its command — and the working directory only when the server
|
|
742
742
|
* says it differs from the workspace — instead of every raw argument.
|
|
743
743
|
*/
|
|
744
744
|
function ShellCommandPreview({
|
|
@@ -22,7 +22,7 @@ export interface IPopupMenuSection {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* A HIG pop-up menu
|
|
25
|
+
* A HIG pop-up menu: `role="menu"`, its choices as `menuitemradio` with `aria-checked`
|
|
26
26
|
* (or `menuitem` for a plain action, e.g. "Manage providers…"), grouped into optional labelled
|
|
27
27
|
* sections. Arrow keys move a roving focus, Enter/Space activates the focused item, and Esc — or a
|
|
28
28
|
* click outside — closes the menu and returns focus to the trigger that opened it, so the person
|
|
@@ -13,8 +13,8 @@ import type {
|
|
|
13
13
|
} from '../hooks/session-client-types.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* readable text. The owner's design principle 4
|
|
16
|
+
* The Project panel: git status (Changes), one file's diff on click, and project memory as
|
|
17
|
+
* readable text. The owner's design principle 4: everything the host implements is usable from
|
|
18
18
|
* the GUI through organized screens — this is git status/diff and project memory's screen. Checkpoints
|
|
19
19
|
* (rewind) are deliberately absent (see `agent-gui-web/docs/SPEC.md`'s design decisions): edit
|
|
20
20
|
* checkpoints only work on a host that can prove a write stays inside the project
|
|
@@ -102,7 +102,7 @@ export function SessionMonitor({ wsUrl, className }: ISessionMonitorProps): Reac
|
|
|
102
102
|
ownDriverId={ownDriverId}
|
|
103
103
|
/>
|
|
104
104
|
) : (
|
|
105
|
-
//
|
|
105
|
+
// This renders in the same slot as `ConversationView` (never
|
|
106
106
|
// together), so it carries this page's `main` landmark here too — nothing above this
|
|
107
107
|
// slot supplies one.
|
|
108
108
|
<main className="flex h-full items-center justify-center" aria-label="Conversation">
|
|
@@ -32,7 +32,7 @@ export function formatUpdatedAt(updatedAt: string, now: number = Date.now()): st
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* A session's title
|
|
35
|
+
* A session's title: its name, else its stable first-message title, else "New session".
|
|
36
36
|
* Never `preview` — that field is the raw latest reply, which is what changes every turn and is what
|
|
37
37
|
* this title exists to stop showing.
|
|
38
38
|
*/
|
|
@@ -75,12 +75,12 @@ interface IMenuAnchor {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
78
|
+
* This workspace's sessions on the left of the conversation, as in Claude Code Desktop:
|
|
79
79
|
* start a new one, or click another to make it current. Rows the host could not read are listed,
|
|
80
80
|
* disabled, so a damaged session never looks deleted. A refused switch comes back as a notice.
|
|
81
81
|
* A host that keeps several sessions live marks the rows running now and counts who else is on them.
|
|
82
82
|
*
|
|
83
|
-
*
|
|
83
|
+
* Each row also carries a "More" menu (or right-click) with Rename (inline) and Delete
|
|
84
84
|
* (confirmed). Rename on the current row reuses the `/rename` command, which also updates the live
|
|
85
85
|
* session's own name and title bar; rename on any other row writes the stored record directly.
|
|
86
86
|
*/
|
|
@@ -99,7 +99,7 @@ export function SessionSidebar({
|
|
|
99
99
|
const unreadable = listing?.unreadableSessionIds ?? [];
|
|
100
100
|
const lockedLine = lockedSessionsLine(listing?.lockedSessionCount);
|
|
101
101
|
const failed = state.sessionsError?.code === 'list_failed' ? state.sessionsError.message : null;
|
|
102
|
-
//
|
|
102
|
+
// Session switching is disabled while disconnected, like the status chips and
|
|
103
103
|
// the `/` command menu — a switch could not reach the host either.
|
|
104
104
|
const connected = state.status === 'connected';
|
|
105
105
|
|
|
@@ -203,7 +203,7 @@ export function SessionSidebar({
|
|
|
203
203
|
const isCurrent = session.id === current;
|
|
204
204
|
const others = otherClients(session, isCurrent);
|
|
205
205
|
const isRenaming = renamingId === session.id;
|
|
206
|
-
//
|
|
206
|
+
// The explicit `aria-label` below replaces name-from-content outright, so
|
|
207
207
|
// the live dot's and "N other(s)" span's own text no longer reaches the accessible name —
|
|
208
208
|
// folded back in here as a description instead, alongside the relative time.
|
|
209
209
|
const statusParts = [
|
|
@@ -238,12 +238,12 @@ export function SessionSidebar({
|
|
|
238
238
|
<button
|
|
239
239
|
type="button"
|
|
240
240
|
aria-current={isCurrent ? 'true' : undefined}
|
|
241
|
-
//
|
|
241
|
+
// An explicit name + description — the row's own visible text has no
|
|
242
242
|
// whitespace between its parts, so an unlabelled button reads as one run-together
|
|
243
243
|
// string ("Title validation fixjust now") to a screen reader.
|
|
244
244
|
aria-label={sessionTitle(session)}
|
|
245
245
|
aria-describedby={describedBy}
|
|
246
|
-
//
|
|
246
|
+
// While disconnected, the tooltip says why instead of previewing.
|
|
247
247
|
title={connected ? session.preview || session.id : 'Reconnecting…'}
|
|
248
248
|
disabled={!connected}
|
|
249
249
|
onClick={() => {
|
|
@@ -409,7 +409,7 @@ export function SessionSidebar({
|
|
|
409
409
|
|
|
410
410
|
/** The collapsed sidebar: a narrow rail that opens it again. */
|
|
411
411
|
export function SessionSidebarRail({ state }: { state: IWsSessionState }): React.ReactElement {
|
|
412
|
-
//
|
|
412
|
+
// Starting a session is disabled while disconnected, matching the open sidebar.
|
|
413
413
|
const connected = state.status === 'connected';
|
|
414
414
|
return (
|
|
415
415
|
<div className="flex w-12 flex-shrink-0 flex-col items-center gap-1 bg-sidebar py-2.5">
|
|
@@ -38,7 +38,7 @@ const APP_VIEW_HOST_VERSION = '1.0.0';
|
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Designed empty state shown before the first turn. Renders in the same slot as `ConversationView`
|
|
41
|
-
* (never together), so it carries the same `main` landmark and label
|
|
41
|
+
* (never together), so it carries the same `main` landmark and label — nothing
|
|
42
42
|
* above this slot supplies one.
|
|
43
43
|
*/
|
|
44
44
|
function EmptyState(): React.ReactElement {
|
|
@@ -59,14 +59,14 @@ function EmptyState(): React.ReactElement {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Shown in place of the conversation while `sessionStatus.setupRequired` holds: the
|
|
63
63
|
* session is running (the GUI itself connected fine), but has no provider to reply with yet. "Set up
|
|
64
64
|
* provider" runs `/provider add` as a command; its questions dock above where the composer would be
|
|
65
65
|
* (the same `PermissionPrompt` any ask uses), and the panel clears itself once the session reports a
|
|
66
66
|
* real provider, live — no restart, no reload.
|
|
67
67
|
*
|
|
68
68
|
* Renders in the same slot as `ConversationView`/`EmptyState` (never together), so it carries the
|
|
69
|
-
* page's `main` landmark here too
|
|
69
|
+
* page's `main` landmark here too — nothing above this slot supplies one.
|
|
70
70
|
*/
|
|
71
71
|
function SetupPanel({
|
|
72
72
|
onSetUp,
|
|
@@ -121,17 +121,17 @@ export function SessionSurface({
|
|
|
121
121
|
/** Secure desktop discards unsent text and attachment paths when the session closes. */
|
|
122
122
|
draftMode?: 'persistent' | 'volatile';
|
|
123
123
|
/**
|
|
124
|
-
* Present only when the host can restart the runtime after the connection is lost for good (
|
|
125
|
-
*
|
|
124
|
+
* Present only when the host can restart the runtime after the connection is lost for good (the
|
|
125
|
+
* desktop app); absent in the browser, where the banner says how to reopen it instead.
|
|
126
126
|
*/
|
|
127
127
|
onReconnect?: () => Promise<void>;
|
|
128
|
-
/** Forwarded to the composer's attach button — present only on the desktop app
|
|
128
|
+
/** Forwarded to the composer's attach button — present only on the desktop app. */
|
|
129
129
|
pickFiles?: () => Promise<readonly IPickedFile[]>;
|
|
130
|
-
/** Forwarded to the composer's drag-and-drop — present only on the desktop app
|
|
130
|
+
/** Forwarded to the composer's drag-and-drop — present only on the desktop app. */
|
|
131
131
|
getPathForFile?: (file: File) => string;
|
|
132
132
|
/**
|
|
133
133
|
* The Project panel's memory "Open in editor" action — present only when the host can open a file
|
|
134
|
-
* in an external editor (the desktop app
|
|
134
|
+
* in an external editor (the desktop app). Absent in the browser, where the action
|
|
135
135
|
* itself is hidden (only shown "if the host supports it").
|
|
136
136
|
*/
|
|
137
137
|
onOpenMemoryInEditor?: (path: string) => void;
|
|
@@ -158,7 +158,7 @@ export function SessionSurface({
|
|
|
158
158
|
const tasks = state.executionWorkspace?.entries ?? [];
|
|
159
159
|
// The main thread alone is this conversation; the rail earns its width only for work beside it.
|
|
160
160
|
const hasTasks = tasks.some((entry) => entry.kind !== 'main_thread');
|
|
161
|
-
//
|
|
161
|
+
// The entry open in the detail sheet, resolved fresh from the live snapshot each render
|
|
162
162
|
// (so it reflects the entry's own updates, e.g. a Stop taking effect) rather than a stale copy.
|
|
163
163
|
const openEntry = tasks.find((entry) => entry.id === state.openEntryId) ?? null;
|
|
164
164
|
// A loop's `cancel` means "stop the loop" (`/loop stop <id>`) — never cancel-background-task,
|
|
@@ -171,9 +171,9 @@ export function SessionSurface({
|
|
|
171
171
|
state.send({ type: 'cancel-background-task', taskId: entry.sourceId });
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
|
-
//
|
|
175
|
-
// background task/loop beside the conversation — "Work in progress" covers all four
|
|
176
|
-
//
|
|
174
|
+
// The rail also earns its width for a schedule or a goal, even with no running
|
|
175
|
+
// background task/loop beside the conversation — "Work in progress" covers all four, not only
|
|
176
|
+
// execution-workspace entries.
|
|
177
177
|
const currentGoal = state.sessionStatus?.goal ?? null;
|
|
178
178
|
const hasWorkInProgress = hasTasks || state.scheduledTasks.length > 0 || currentGoal !== null;
|
|
179
179
|
const isEmpty =
|
|
@@ -181,7 +181,7 @@ export function SessionSurface({
|
|
|
181
181
|
!state.streamingText &&
|
|
182
182
|
!state.isThinking &&
|
|
183
183
|
state.activeTools.length === 0;
|
|
184
|
-
//
|
|
184
|
+
// No provider is configured — the session is running (a placeholder stands in for the
|
|
185
185
|
// real one), but there is nothing to reply with yet.
|
|
186
186
|
const setupRequired = state.sessionStatus?.setupRequired === true;
|
|
187
187
|
// Switching, starting, renaming and deleting sessions are driver actions: an observer has no sidebar.
|
|
@@ -195,8 +195,8 @@ export function SessionSurface({
|
|
|
195
195
|
const currentRow = listing?.sessions.find((session) => session.id === listing.currentSessionId);
|
|
196
196
|
// A rename seen on this page wins; otherwise the host's listing names the current session.
|
|
197
197
|
const title = state.sessionName ?? (currentRow ? sessionTitle(currentRow) : null);
|
|
198
|
-
//
|
|
199
|
-
//
|
|
198
|
+
// Below `md` the open sidebar is a sheet over the conversation (the shared `Dialog`)
|
|
199
|
+
// rather than a column beside it: a dimmed backdrop, Esc, an outside click or choosing a
|
|
200
200
|
// session all close it again, and it traps Tab inside it while open.
|
|
201
201
|
const isNarrow = useMediaQuery(NARROW_WINDOW_QUERY);
|
|
202
202
|
const sheetActive = sidebarOpen && isNarrow;
|
|
@@ -370,7 +370,7 @@ export function SessionSurface({
|
|
|
370
370
|
onFocusReturn={() => composerRef.current?.focus()}
|
|
371
371
|
/>
|
|
372
372
|
)}
|
|
373
|
-
{/*
|
|
373
|
+
{/* Hidden while there is no provider to send to — the setup panel's own
|
|
374
374
|
button is the only way in, so nobody types into a composer that goes nowhere. */}
|
|
375
375
|
{readOnly ? (
|
|
376
376
|
<p
|
|
@@ -403,7 +403,7 @@ export function SessionSurface({
|
|
|
403
403
|
modelList={state.modelList}
|
|
404
404
|
onRequestModelList={() => state.requestModelList()}
|
|
405
405
|
onSilentCommand={(name, args) => state.sendCommandSilently(name, args)}
|
|
406
|
-
//
|
|
406
|
+
// "Manage providers…" now opens Settings at the Providers & Models
|
|
407
407
|
// section, replacing the model menu's former server-driven profile flow.
|
|
408
408
|
onManageProviders={() => state.openSettings('providers')}
|
|
409
409
|
onSubmit={(prompt) => {
|
|
@@ -497,7 +497,7 @@ export function SessionSurface({
|
|
|
497
497
|
|
|
498
498
|
/**
|
|
499
499
|
* A centered chrome frame for the pre-session (loading) and fatal states — reused by app shells. Its
|
|
500
|
-
* content area is this page's `main` landmark
|
|
500
|
+
* content area is this page's `main` landmark: nothing above `CenteredChrome`'s
|
|
501
501
|
* three call sites (starting, fatal, the trust question) supplies one, and none of the three ever
|
|
502
502
|
* render alongside `SessionSurface`'s own. `header` stays a sibling of `main`, not inside it, so it
|
|
503
503
|
* keeps its own implicit `banner` role.
|
|
@@ -14,7 +14,7 @@ const STATUS: Record<string, { dot: string; label: string }> = {
|
|
|
14
14
|
error: { dot: 'bg-destructive', label: 'Connection error' },
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/** Every view the primary nav can show — 'project' is always offered (desktop and
|
|
18
18
|
* browser alike); 'usage' only where the host opted the surface into it. */
|
|
19
19
|
export type TSessionView = 'chat' | 'usage' | 'project';
|
|
20
20
|
|
|
@@ -25,7 +25,7 @@ const VIEW_LABEL: Readonly<Record<TSessionView, string>> = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* The folder this session works in, wherever `document.title` is settable: a page (the
|
|
29
29
|
* browser tab) and the desktop window that loads it (Electron follows the page's title unless told
|
|
30
30
|
* otherwise, and nothing in `apps/agent-app/electron/main.ts` overrides it — see its own SPEC note).
|
|
31
31
|
* Absent while the host has not said, so a not-yet-connected surface keeps whatever title it loaded
|
|
@@ -58,14 +58,14 @@ export function SessionTitleBar({
|
|
|
58
58
|
status: string;
|
|
59
59
|
surface?: string;
|
|
60
60
|
title?: string | null;
|
|
61
|
-
/** The folder this session works in
|
|
61
|
+
/** The folder this session works in; absent when the host has not said. */
|
|
62
62
|
workspace?: { readonly name: string; readonly path: string };
|
|
63
63
|
showBrand: boolean;
|
|
64
64
|
view: TSessionView;
|
|
65
65
|
onView: (view: TSessionView) => void;
|
|
66
66
|
personalUsageEnabled: boolean;
|
|
67
67
|
/**
|
|
68
|
-
*
|
|
68
|
+
* Mirrors `ConnectionBanner`'s own "give-up" flag (`useWsSession`'s
|
|
69
69
|
* `connectionLost`) so this corner indicator can tell exactly when that banner is on screen, to
|
|
70
70
|
* never say the same thing twice.
|
|
71
71
|
*/
|
|
@@ -73,7 +73,7 @@ export function SessionTitleBar({
|
|
|
73
73
|
}): React.ReactElement {
|
|
74
74
|
const state = STATUS[status] ?? STATUS.disconnected;
|
|
75
75
|
useWorkspaceDocumentTitle(workspace);
|
|
76
|
-
//
|
|
76
|
+
// The same "has this surface ever connected" rule `ConnectionBanner` uses to
|
|
77
77
|
// decide whether it has anything to show — kept in step here so the corner text disappears exactly
|
|
78
78
|
// when, and only when, the banner takes over saying it (never both, never neither).
|
|
79
79
|
const everConnectedRef = useRef(false);
|
|
@@ -81,7 +81,7 @@ export function SessionTitleBar({
|
|
|
81
81
|
if (status === 'connected') everConnectedRef.current = true;
|
|
82
82
|
}, [status]);
|
|
83
83
|
const bannerShown = status !== 'connected' && (connectionLost || everConnectedRef.current);
|
|
84
|
-
//
|
|
84
|
+
// Everything the agent implements is usable from the GUI through
|
|
85
85
|
// organized screens — the Project panel (git status/diff, project memory) is one, offered next to
|
|
86
86
|
// Chat on every surface, desktop and browser alike; Usage stays surface-gated.
|
|
87
87
|
const items: readonly TSessionView[] = personalUsageEnabled
|
|
@@ -142,7 +142,7 @@ export function SessionTitleBar({
|
|
|
142
142
|
const BANNER_COLUMN = 'mx-auto w-full max-w-[760px] px-6';
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* A connection that drops after a working session
|
|
145
|
+
* A connection that drops after a working session: a banner directly above the
|
|
146
146
|
* conversation — never a full-screen replacement — so the conversation and its history stay visible
|
|
147
147
|
* and scrollable throughout. Shown as soon as the drop happens; says nothing on the very first
|
|
148
148
|
* connect, because nothing has been lost yet.
|
|
@@ -234,7 +234,7 @@ function displayProvider(provider: string | undefined): string {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
|
-
* A provider/session error classified at the wire boundary
|
|
237
|
+
* A provider/session error classified at the wire boundary as one plain sentence naming
|
|
238
238
|
* what happened and, where there is one, the next step — `null` for a notice this classification does
|
|
239
239
|
* not cover, which keeps showing its own `message` instead.
|
|
240
240
|
*
|
|
@@ -6,7 +6,7 @@ import type { ISettingsSnapshot, TSettingsPatch } from '@robota-sdk/agent-interf
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Language, output style and preset — reusing the exact choices and descriptions the runtime
|
|
9
|
-
* already builds for `/language`, `/output-style` and `/preset
|
|
9
|
+
* already builds for `/language`, `/output-style` and `/preset`. Never internal ids:
|
|
10
10
|
* every pop-up shows the runtime's human labels.
|
|
11
11
|
*/
|
|
12
12
|
export function SettingsGeneralSection({
|
|
@@ -92,7 +92,7 @@ function ServerRow({
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* The MCP Servers section
|
|
95
|
+
* The MCP Servers section: every configured server's name, where it is
|
|
96
96
|
* configured, its live connection status and tool count, an enable/disable switch (approve/reject),
|
|
97
97
|
* an expandable list of its tool names, and a section-level Reload. Adding or editing a server is
|
|
98
98
|
* left out — the runtime has no command path that writes MCP config (recorded in
|
|
@@ -48,7 +48,7 @@ function PermissionRuleRow({
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Default permission mode, the rules `/permissions` shows, and the sandbox switch
|
|
51
|
+
* Default permission mode, the rules `/permissions` shows, and the sandbox switch.
|
|
52
52
|
* "Skip all checks" as the mode and a preset that would set it both go through
|
|
53
53
|
* `onRequestModeChange`'s parent-owned confirmation before anything is sent.
|
|
54
54
|
*/
|
|
@@ -60,7 +60,7 @@ function PluginRow({
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* The Plugins section
|
|
63
|
+
* The Plugins section: every installed plugin's name, description and enabled
|
|
64
64
|
* state, an enable/disable switch, a section-level Reload, and — local surfaces only — install and
|
|
65
65
|
* uninstall, since installing runs third-party code. `canInstall` (from the snapshot) mirrors the
|
|
66
66
|
* refusal `/plugin install|uninstall` itself gives a remote device or an observer.
|
|
@@ -14,7 +14,7 @@ const PRIMARY_BUTTON = `${BUTTON} bg-primary text-primary-foreground hover:opaci
|
|
|
14
14
|
const SECONDARY_BUTTON = `${BUTTON} bg-raised text-foreground hover:bg-hover`;
|
|
15
15
|
const DESTRUCTIVE_BUTTON = `${BUTTON} text-destructive hover:bg-destructive/10`;
|
|
16
16
|
|
|
17
|
-
/** The "Model" action: a `PopupMenu` of this ONE profile's catalog models
|
|
17
|
+
/** The "Model" action: a `PopupMenu` of this ONE profile's catalog models. */
|
|
18
18
|
function ProviderModelButton({
|
|
19
19
|
profile,
|
|
20
20
|
modelList,
|
|
@@ -142,7 +142,7 @@ function ProviderProfileRow({
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
* The configured provider profiles
|
|
145
|
+
* The configured provider profiles, each row a plain provider name, its model's label,
|
|
146
146
|
* a checkmark on the one in use, and a plain connection state when one is known. Every action goes
|
|
147
147
|
* through the same path the equivalent command does: Use/Model apply through `state.updateSettings`
|
|
148
148
|
* (the same `field` patterns Preset/Mode/Sandbox use); Edit/Test/Duplicate/Add dispatch the raw
|
|
@@ -169,7 +169,7 @@ export function SettingsProvidersSection({
|
|
|
169
169
|
onRequestDelete: (profileName: string) => void;
|
|
170
170
|
}): React.ReactElement {
|
|
171
171
|
// The "Model" button needs each profile's catalog as soon as this section is visible, not only
|
|
172
|
-
// once a person opens one — `list-models` is a cheap, observer-safe read
|
|
172
|
+
// once a person opens one — `list-models` is a cheap, observer-safe read.
|
|
173
173
|
// Intentionally empty deps: fetch once per mount, not on every render.
|
|
174
174
|
useEffect(() => {
|
|
175
175
|
onRequestModelList();
|
|
@@ -19,7 +19,7 @@ interface ISettingsSectionDefinition {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* The Settings screen's sections
|
|
22
|
+
* The Settings screen's sections. Adding a later section (Advisor) is one entry here plus
|
|
23
23
|
* one component in the switch below — nothing else in this file changes.
|
|
24
24
|
*/
|
|
25
25
|
const SECTIONS: readonly ISettingsSectionDefinition[] = [
|
|
@@ -36,7 +36,7 @@ type TPendingPluginAction =
|
|
|
36
36
|
| { kind: 'uninstall'; plugin: ISettingsPlugin };
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* A large modal sheet with a section sidebar on the left and content on the right
|
|
39
|
+
* A large modal sheet with a section sidebar on the left and content on the right — the
|
|
40
40
|
* GUI's Settings screen, opened by the sidebar's gear button, the desktop app's ⌘, menu item, or
|
|
41
41
|
* `/settings` (its `show-settings` `ui_intent` calls `state.openSettings()`, wired in
|
|
42
42
|
* `useSessionClient`). Every change applies at once; there is no Save button. Esc and the Close
|
|
@@ -56,7 +56,7 @@ export function SettingsScreen({ state }: { state: IWsSessionState }): React.Rea
|
|
|
56
56
|
const [pendingPluginAction, setPendingPluginAction] = useState<TPendingPluginAction | null>(null);
|
|
57
57
|
const [mcpReloading, setMcpReloading] = useState(false);
|
|
58
58
|
const [pluginsReloading, setPluginsReloading] = useState(false);
|
|
59
|
-
//
|
|
59
|
+
// The profile a pending "Delete…" confirms, or null when none is pending.
|
|
60
60
|
const [pendingProviderDelete, setPendingProviderDelete] = useState<string | null>(null);
|
|
61
61
|
|
|
62
62
|
// Each time the screen opens, land on the section the opener asked for (`/plugin` → Plugins,
|