@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/index.cjs +81 -81
  3. package/dist/index.d.cts +61 -61
  4. package/dist/index.d.ts +61 -61
  5. package/dist/index.js +81 -81
  6. package/dist/index.js.map +1 -1
  7. package/docs/SPEC.md +1 -1
  8. package/package.json +6 -6
  9. package/src/client/ws-session-client.ts +1 -1
  10. package/src/clipboard.ts +2 -2
  11. package/src/components/AgentActivityPanel.tsx +8 -8
  12. package/src/components/AgentSwitcherSheet.tsx +2 -2
  13. package/src/components/Composer.tsx +27 -27
  14. package/src/components/ConversationView.tsx +14 -14
  15. package/src/components/Dialog.tsx +6 -6
  16. package/src/components/DiffLines.tsx +2 -2
  17. package/src/components/ExecutionDetailSheet.tsx +1 -1
  18. package/src/components/HelpSheet.tsx +2 -2
  19. package/src/components/PermissionPrompt.tsx +22 -22
  20. package/src/components/PopupMenu.tsx +1 -1
  21. package/src/components/ProjectPanel.tsx +2 -2
  22. package/src/components/SessionMonitor.tsx +1 -1
  23. package/src/components/SessionSidebar.tsx +8 -8
  24. package/src/components/SessionSurface.tsx +18 -18
  25. package/src/components/SessionSurfaceChrome.tsx +8 -8
  26. package/src/components/SettingsGeneralSection.tsx +1 -1
  27. package/src/components/SettingsMcpSection.tsx +1 -1
  28. package/src/components/SettingsPermissionsSection.tsx +1 -1
  29. package/src/components/SettingsPluginsSection.tsx +1 -1
  30. package/src/components/SettingsProvidersSection.tsx +3 -3
  31. package/src/components/SettingsScreen.tsx +3 -3
  32. package/src/components/StatusControls.tsx +12 -12
  33. package/src/components/composer-attachments.ts +5 -5
  34. package/src/driver-labels.ts +3 -3
  35. package/src/execution-entry-status.ts +1 -1
  36. package/src/goal-view.ts +1 -1
  37. package/src/hooks/command-menu.ts +2 -2
  38. package/src/hooks/excluded-commands.ts +2 -2
  39. package/src/hooks/prompt-state.ts +4 -4
  40. package/src/hooks/server-message-handling.ts +11 -11
  41. package/src/hooks/session-client-types.ts +29 -29
  42. package/src/hooks/ui-intent-state.ts +6 -6
  43. package/src/hooks/use-agent-switcher-state.ts +2 -2
  44. package/src/hooks/use-media-query.ts +1 -1
  45. package/src/hooks/use-model-list.ts +1 -1
  46. package/src/hooks/use-project-panel.ts +1 -1
  47. package/src/hooks/use-schedules-state.ts +4 -4
  48. package/src/hooks/use-session-directory.ts +4 -4
  49. package/src/hooks/use-settings-state.ts +4 -4
  50. package/src/hooks/useSessionClient.ts +44 -44
  51. package/src/index.ts +5 -5
  52. package/src/schedule-view.ts +1 -1
  53. 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 (REMOTE-007/009). Under local == remote the paired
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 (#3280 §1). A click on a button always answers at once. An ask whose
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 (#3280 §3). The field's typed value
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 (#3280 §3 follow-up).
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 (§3289 §3): a prompt raised by its own turn shows no requester. */
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
- * #3282 §2 (part 2) — the one destructive option in the provider profile action menu (the ask built
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 (#3280 §3): the field is the prompt's primary control, so focus goes
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
- // (#3280 §3 follow-up), and removing a focused DOM node drops focus to <body> as an intrinsic side
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 (#3280 §1).
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
- // #3282 §2 (part 2): the provider-delete confirmation (`buildProviderDelete` in agent-command's
203
- // `provider-command-profile-lifecycle.ts`) is now built on the shared `ConfirmDialog` (#3331)
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
- // REMOTE-014 E5 (display-only): the prompt belongs to the driver whose turn raised it. Shown so the owner
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
- // #3282 §2: a destructive option (Delete) is excluded from the digit-answerable list entirely —
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
- // #3288: an Edit/Write request carries the same server-built diff `tool_end` would show.
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
- // #3282 §2: Delete (in the provider profile action menu) renders apart from the rest, destructive-
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
- {/* #3288 §2: an Edit/Write request with a server-built diff preview shows "Edit
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
- Issue #3288 §1: a background agent's own request ALWAYS names it — whether or
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
- {/* #3282 §2: Delete sits apart at the bottom, styled destructive, with no digit
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
- /** #3282 §2: a destructive ask option (Delete) — visually distinct from Switch/Edit/Test/Duplicate. */
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 (#3280 §3).
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
- * #3288: a Shell request shows just its command — and the working directory only when the server
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 (#3282 §2): `role="menu"`, its choices as `menuitemradio` with `aria-checked`
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
- * #3282 §4c — 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 (#3277): everything the host implements is usable from
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
- // #3289 §3 review: this renders in the same slot as `ConversationView` (never
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 (#3289 §1): its name, else its stable first-message title, else "New session".
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
- * #3189 — this workspace's sessions on the left of the conversation, as in Claude Code Desktop:
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
- * #3289 §1 — each row also carries a "More" menu (or right-click) with Rename (inline) and Delete
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
- // #3282 §2 (part 2): session switching is disabled while disconnected, like the status chips and
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
- // #3289 §3 review: the explicit `aria-label` below replaces name-from-content outright, so
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
- // #3289 §3: an explicit name + description — the row's own visible text has no
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
- // #3282 §2 (part 2): while disconnected, the tooltip says why instead of previewing.
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
- // #3282 §2 (part 2): starting a session is disabled while disconnected, matching the open sidebar.
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 (#3289 §3 review) — nothing
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
- * Issue #3282 §3 — shown in place of the conversation while `sessionStatus.setupRequired` holds: the
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 (#3289 §3 review) — nothing above this slot supplies one.
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 (issue
125
- * #3280 §5, the desktop app); absent in the browser, where the banner says how to reopen it instead.
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 (#3282 §4d). */
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 (#3282 §4d). */
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, #3282 §4c). Absent in the browser, where the action
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
- // #3288 §1: the entry open in the detail sheet, resolved fresh from the live snapshot each render
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
- // #3282 §4 part b-3: 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 (issue #3282
176
- // §4's Expected section), not only execution-workspace entries.
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
- // #3282 §3: no provider is configured — the session is running (a placeholder stands in for the
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
- // #3289 §2 — below `md` the open sidebar is a sheet over the conversation (the shared `Dialog`,
199
- // #3331) rather than a column beside it: a dimmed backdrop, Esc, an outside click or choosing a
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
- {/* #3282 §3: hidden while there is no provider to send to — the setup panel's own
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
- // #3282 §4b: "Manage providers…" now opens Settings at the Providers & Models
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 (#3289 §3 review): nothing above `CenteredChrome`'s
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
- /** #3282 §4c: every view the primary nav can show — 'project' is always offered (desktop and
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
- * #3289 §1 — the folder this session works in, wherever `document.title` is settable: a page (the
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 (#3289 §1); absent when the host has not said. */
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
- * #3282 §2 (part 2): mirrors `ConnectionBanner`'s own "give-up" flag (`useWsSession`'s
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
- // #3282 §2 (part 2): the same "has this surface ever connected" rule `ConnectionBanner` uses to
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
- // #3282 §4c principle 4 (#3277): everything the agent implements is usable from the GUI through
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 (issue #3280 §5): a banner directly above the
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 (#3289 §3) as one plain sentence naming
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` (#3282 §4a). Never internal ids:
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 (#3282 §4 part b-2): every configured server's name, where it is
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 (#3282 §4a).
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 (#3282 §4 part b-2): every installed plugin's name, description and enabled
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 (#3282 §4b). */
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 (#3282 §4b), each row a plain provider name, its model's label,
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 (#3282 §2).
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 (#3282 §4). Adding a later section (Advisor) is one entry here plus
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 (#3282 §4a) — the
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
- // #3282 §4b: the profile a pending "Delete…" confirms, or null when none is pending.
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,