@robota-sdk/agent-ui-web 3.2.1 → 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 +29 -0
- package/dist/index.cjs +161 -118
- package/dist/index.d.cts +65 -63
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +65 -63
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +161 -118
- package/dist/index.js.map +1 -1
- package/docs/SPEC.md +6 -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 +46 -39
- package/src/components/AgentSwitcherSheet.tsx +2 -2
- package/src/components/Composer.tsx +40 -37
- 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 +83 -56
- 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 +13 -11
- package/src/hooks/session-client-types.ts +36 -30
- package/src/hooks/ui-intent-state.ts +6 -6
- package/src/hooks/use-agent-switcher-state.ts +2 -2
- package/src/hooks/use-app-view-requests.ts +8 -1
- 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 +11 -6
- package/src/hooks/use-settings-state.ts +4 -4
- package/src/hooks/useSessionClient.ts +81 -50
- package/src/index.ts +5 -5
- package/src/schedule-view.ts +1 -1
- package/src/styles/surface.css +1 -1
|
@@ -16,7 +16,11 @@ import type {
|
|
|
16
16
|
IExecutionDetailRecord,
|
|
17
17
|
IExecutionWorkspaceSnapshot,
|
|
18
18
|
} from '@robota-sdk/agent-interface-execution';
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
IWireAgentDefinitionSummary,
|
|
21
|
+
TServerMessage,
|
|
22
|
+
TSessionSurfaceRole,
|
|
23
|
+
} from '@robota-sdk/agent-transport';
|
|
20
24
|
|
|
21
25
|
export type { TAppToolArguments, TAppToolOutcome, TAppView } from '../app-view/types.js';
|
|
22
26
|
|
|
@@ -34,7 +38,7 @@ export type TCommandCatalog = Omit<Extract<TServerMessage, { type: 'commands' }>
|
|
|
34
38
|
export type TSessionStatus = Extract<TServerMessage, { type: 'session_status' }>['status'];
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
|
-
*
|
|
41
|
+
* The models a person may switch to, grouped by configured provider profile —
|
|
38
42
|
* what the model control's pop-up menu is built from. `null` until requested (`list-models`).
|
|
39
43
|
*/
|
|
40
44
|
export type TModelListSnapshot = Omit<
|
|
@@ -55,7 +59,7 @@ export type TCurrentSessionUsageReport = Extract<
|
|
|
55
59
|
{ type: 'usage_report' }
|
|
56
60
|
>['report'];
|
|
57
61
|
|
|
58
|
-
/**
|
|
62
|
+
/** The Project panel's reads. Each result crosses the wire unchanged from the session
|
|
59
63
|
* method's own return value (see `wire-messages.ts`'s doc comment on `project_status`). */
|
|
60
64
|
export type TProjectStatusRead = Extract<TServerMessage, { type: 'project_status' }>['result'];
|
|
61
65
|
export type TProjectDiffRead = Extract<TServerMessage, { type: 'project_diff' }>['result'];
|
|
@@ -66,7 +70,7 @@ export interface IConversationMessage {
|
|
|
66
70
|
role: 'user' | 'assistant';
|
|
67
71
|
content: string;
|
|
68
72
|
isStreaming?: boolean;
|
|
69
|
-
/**
|
|
73
|
+
/** The co-driving author of a user turn. */
|
|
70
74
|
author?: string;
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -97,7 +101,7 @@ export interface IChangedFileSummary {
|
|
|
97
101
|
diffLines: readonly IDiffLine[];
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
/**
|
|
104
|
+
/** A turn that changed files ends with this compact summary row. */
|
|
101
105
|
export interface IChangedFilesEntry {
|
|
102
106
|
id: string;
|
|
103
107
|
role: 'changed-files';
|
|
@@ -113,28 +117,28 @@ export interface IActiveTool {
|
|
|
113
117
|
status: 'running' | 'done' | 'error';
|
|
114
118
|
input?: string;
|
|
115
119
|
result?: IToolState['result'];
|
|
116
|
-
/**
|
|
120
|
+
/** Correlates this call across `tool_start`/`tool_end` — how a finished call is matched. */
|
|
117
121
|
executionId?: string;
|
|
118
|
-
/**
|
|
122
|
+
/** A unified diff (Edit/Write), built server-side — the same one an approval prompt showed. */
|
|
119
123
|
diffLines?: readonly IDiffLine[];
|
|
120
|
-
/**
|
|
124
|
+
/** The file `diffLines` concerns, workspace-relative when the server could make it so. */
|
|
121
125
|
diffFile?: string;
|
|
122
|
-
/**
|
|
126
|
+
/** The tool's raw result payload (Shell output, Read content, …), already capped server-side. */
|
|
123
127
|
toolResultData?: string;
|
|
124
128
|
/** Admitted observations; opaque resource URIs do not authorize a client-side fetch. */
|
|
125
129
|
toolResultParts?: IToolState['toolResultParts'];
|
|
126
|
-
/**
|
|
130
|
+
/** A workspace-relative display form of a path argument, additive to `input`/`firstArg`. */
|
|
127
131
|
displayPath?: string;
|
|
128
|
-
/**
|
|
132
|
+
/** Set when this call is a projected `/command` tool — render "Ran /<commandName>". */
|
|
129
133
|
commandName?: string;
|
|
130
|
-
/**
|
|
134
|
+
/** True for an internal signal tool (e.g. goal-status) — never rendered as a call. */
|
|
131
135
|
internal?: boolean;
|
|
132
136
|
|
|
133
137
|
appView?: IToolAppViewRef;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
/**
|
|
137
|
-
*
|
|
141
|
+
* A prompt the host queued behind the running turn — the server has already taken it, so it
|
|
138
142
|
* is not lost, but it will not run until the turn ends. `count` is the TOTAL number waiting (the shown
|
|
139
143
|
* `text` plus any behind it), matching the wire's `pendingCount`.
|
|
140
144
|
*/
|
|
@@ -148,7 +152,7 @@ export interface ISessionNotice {
|
|
|
148
152
|
/**
|
|
149
153
|
* `session-change-refused`: the host refused a new or switch this surface asked for.
|
|
150
154
|
* `background-task-control-failed`: a Stop in the Agents panel did not take effect.
|
|
151
|
-
* `command-failed
|
|
155
|
+
* `command-failed`: a change made through a status control (model/mode/effort)
|
|
152
156
|
* failed — a plain notice rather than a conversation card, since the control's own label is where
|
|
153
157
|
* a successful change confirms itself and the label stays unchanged when it fails.
|
|
154
158
|
*/
|
|
@@ -161,7 +165,7 @@ export interface ISessionNotice {
|
|
|
161
165
|
| 'context-compacted';
|
|
162
166
|
message: string;
|
|
163
167
|
/**
|
|
164
|
-
* A `session-error` classified at the wire boundary
|
|
168
|
+
* A `session-error` classified at the wire boundary — present only when the host could
|
|
165
169
|
* tell what kind of failure this was. `SessionNotices` maps it to one plain sentence and keeps
|
|
166
170
|
* `message` behind a "Details" disclosure; absent, it shows `message` as before.
|
|
167
171
|
*/
|
|
@@ -191,7 +195,7 @@ export type TMakeSessionClient<TStatus extends string = TConnectionStatus> = (ca
|
|
|
191
195
|
export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
192
196
|
status: TStatus;
|
|
193
197
|
/**
|
|
194
|
-
* Set by `useWsSession` once its reconnect retries give up
|
|
198
|
+
* Set by `useWsSession` once its reconnect retries give up: the runtime is not
|
|
195
199
|
* coming back by itself. Cleared again once `status` reaches `connected`. Undefined for a reducer
|
|
196
200
|
* with no such concept (e.g. the WebRTC surface, which has its own `failed`/`refused` statuses).
|
|
197
201
|
*/
|
|
@@ -204,12 +208,14 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
204
208
|
sessionName: string | null;
|
|
205
209
|
/** This connection's own driver id, learned from the server's first frame; null until then. */
|
|
206
210
|
ownDriverId: TDriverId | null;
|
|
211
|
+
/** The role the host gave this connection, learned when it connects; null until the host answers. */
|
|
212
|
+
role: TSessionSurfaceRole | null;
|
|
207
213
|
/** Null until the session has answered; refreshed after every command. */
|
|
208
214
|
commandCatalog: TCommandCatalog | null;
|
|
209
215
|
/** Null until the session has answered; refreshed after every command and turn. */
|
|
210
216
|
sessionStatus: TSessionStatus | null;
|
|
211
217
|
/**
|
|
212
|
-
*
|
|
218
|
+
* The models the model control's pop-up menu offers. Null until requested — the
|
|
213
219
|
* menu asks for it when it opens (or refreshes it) rather than every surface fetching it eagerly.
|
|
214
220
|
*/
|
|
215
221
|
modelList: TModelListSnapshot | null;
|
|
@@ -233,13 +239,13 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
233
239
|
setSessionSidebarOpen: (open: boolean) => void;
|
|
234
240
|
send: (msg: TClientMessage) => void;
|
|
235
241
|
/**
|
|
236
|
-
*
|
|
242
|
+
* Run a command the SAME way `send({type:'command', ...})` does, but mark its
|
|
237
243
|
* outcome as controls-triggered — `useSessionClient` skips the conversation card for a change made
|
|
238
244
|
* this way (the control's own label confirms it instead), while a typed command keeps its card.
|
|
239
245
|
*/
|
|
240
246
|
sendCommandSilently: (name: string, args?: string) => void;
|
|
241
247
|
pendingPrompts: readonly TPendingPrompt[];
|
|
242
|
-
/**
|
|
248
|
+
/** The prompt queued behind a running turn, or null when none is queued. */
|
|
243
249
|
queuedPrompt: IQueuedPrompt | null;
|
|
244
250
|
answerPermission: (id: string, result: TPermissionResultValue) => void;
|
|
245
251
|
answerAsk: (id: string, response: TActionResponse) => void;
|
|
@@ -257,29 +263,29 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
257
263
|
requestCurrentSessionUsage: () => void;
|
|
258
264
|
sessionNotices: readonly ISessionNotice[];
|
|
259
265
|
dismissSessionNotice: (id: string) => void;
|
|
260
|
-
/**
|
|
266
|
+
/** Whether the Settings modal is shown. `/settings` and the gear button both open it. */
|
|
261
267
|
settingsOpen: boolean;
|
|
262
268
|
settingsStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
263
269
|
/** Null until the first `get-settings` reply arrives. */
|
|
264
270
|
settingsSnapshot: ISettingsSnapshot | null;
|
|
265
271
|
/** The plain message from the most recent failed read or write, if any. */
|
|
266
272
|
settingsError: string | null;
|
|
267
|
-
/**
|
|
273
|
+
/** Which section the screen should land on for the open in progress. */
|
|
268
274
|
settingsInitialSectionId: string | null;
|
|
269
275
|
/**
|
|
270
276
|
* Opens the screen and (re-)fetches its snapshot — the "reopen and see the new value" path.
|
|
271
277
|
* `sectionId` jumps straight to that section (e.g. `/plugin` → `'plugins'`, "Manage providers…"
|
|
272
|
-
*
|
|
278
|
+
* → `'providers'`); omitted opens on the screen's own default (General).
|
|
273
279
|
*/
|
|
274
280
|
openSettings: (sectionId?: string) => void;
|
|
275
281
|
closeSettings: () => void;
|
|
276
282
|
/** A failed write leaves `settingsSnapshot` at its previous value; `settingsError` names why. */
|
|
277
283
|
updateSettings: (patch: TSettingsPatch) => void;
|
|
278
|
-
/**
|
|
284
|
+
/** Whether the Help sheet is shown. `/help` opens it instead of reaching the session. */
|
|
279
285
|
helpOpen: boolean;
|
|
280
286
|
openHelp: () => void;
|
|
281
287
|
closeHelp: () => void;
|
|
282
|
-
/**
|
|
288
|
+
/** The id of the execution-workspace entry open in the detail sheet, or null when closed. */
|
|
283
289
|
openEntryId: string | null;
|
|
284
290
|
executionDetailStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
285
291
|
executionDetailRecords: readonly IExecutionDetailRecord[];
|
|
@@ -294,21 +300,21 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
294
300
|
openExecutionDetail: (entryId: string) => void;
|
|
295
301
|
loadMoreExecutionDetail: () => void;
|
|
296
302
|
closeExecutionDetail: () => void;
|
|
297
|
-
/**
|
|
303
|
+
/** The Project panel's "Changes" section (git status). */
|
|
298
304
|
projectStatusState: 'idle' | 'loading' | 'ready' | 'error';
|
|
299
305
|
projectStatus: TProjectStatusRead | null;
|
|
300
306
|
requestProjectStatus: () => void;
|
|
301
|
-
/**
|
|
307
|
+
/** The Project panel's "File diff" section, for the file `projectDiffPath` names. */
|
|
302
308
|
projectDiffState: 'idle' | 'loading' | 'ready' | 'error';
|
|
303
309
|
projectDiffPath: string | null;
|
|
304
310
|
projectDiff: TProjectDiffRead | null;
|
|
305
311
|
requestProjectDiff: (path: string) => void;
|
|
306
|
-
/**
|
|
312
|
+
/** The Project panel's "Memory" section. */
|
|
307
313
|
projectMemoryState: 'idle' | 'loading' | 'ready' | 'error';
|
|
308
314
|
projectMemory: TProjectMemoryRead | null;
|
|
309
315
|
requestProjectMemory: () => void;
|
|
310
316
|
/**
|
|
311
|
-
*
|
|
317
|
+
* Whether the agent switcher sheet is shown. `/agent` (bare) and this surface's
|
|
312
318
|
* own control both open it.
|
|
313
319
|
*/
|
|
314
320
|
agentSwitcherOpen: boolean;
|
|
@@ -325,7 +331,7 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
325
331
|
/** Runs the same path as `/agent <name>`. */
|
|
326
332
|
selectAgent: (agentType: string) => void;
|
|
327
333
|
/**
|
|
328
|
-
*
|
|
334
|
+
* The schedules shown in the Agents panel's "Scheduled" group — every
|
|
329
335
|
* `IBackgroundTaskState` of `kind: 'scheduled'`, refreshed on connect and after a write.
|
|
330
336
|
*/
|
|
331
337
|
scheduledTasks: readonly IBackgroundTaskState<'scheduled'>[];
|
|
@@ -334,7 +340,7 @@ export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
|
334
340
|
resumeSchedule: (taskId: string) => void;
|
|
335
341
|
/**
|
|
336
342
|
* Permanently stops a schedule — the same `cancel-background-task` write Stop already uses. A
|
|
337
|
-
* failure surfaces the same way a failed Stop does (
|
|
343
|
+
* failure surfaces the same way a failed Stop does (the `background-task-control-failed`
|
|
338
344
|
* session notice) — no separate error state here.
|
|
339
345
|
*/
|
|
340
346
|
deleteSchedule: (taskId: string) => void;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* How the GUI surface answers a `ui_intent`.
|
|
3
3
|
*
|
|
4
4
|
* The server requester-routes `ui_intent`, so everything that arrives here was asked for by THIS
|
|
5
5
|
* surface and must be answered visibly. An intent with a GUI screen opens it (the session picker is
|
|
6
|
-
* the session sidebar
|
|
7
|
-
*
|
|
6
|
+
* the session sidebar; settings and the plugin manager both open the Settings modal; the agent
|
|
7
|
+
* switcher is a sheet). An intent whose
|
|
8
8
|
* command is on the GUI's exclusion list never reaches here at all (`excluded-commands.ts` catches it
|
|
9
9
|
* before the session is asked) — this module only answers a screen this HOST cannot back even though
|
|
10
10
|
* the GUI knows how to render it (the session picker, when the host cannot list sessions), or a kind
|
|
@@ -40,8 +40,8 @@ export function guiScreenForUiIntent(
|
|
|
40
40
|
): 'session-sidebar' | 'settings' | 'settings-plugins' | 'agent-switcher' | null {
|
|
41
41
|
if (intent.type === 'show-session-picker') return 'session-sidebar';
|
|
42
42
|
if (intent.type === 'show-settings') return 'settings';
|
|
43
|
-
//
|
|
44
|
-
//
|
|
43
|
+
// `/plugin` opens the Settings screen's Plugins section, the way `show-settings`
|
|
44
|
+
// does.
|
|
45
45
|
if (intent.type === 'show-plugin-manager') return 'settings-plugins';
|
|
46
46
|
if (intent.type === 'show-agent-switcher') return 'agent-switcher';
|
|
47
47
|
return null;
|
|
@@ -51,7 +51,7 @@ export function guiScreenForUiIntent(
|
|
|
51
51
|
* Describe one intent for this surface: a plain, honest sentence for a screen this HOST cannot back
|
|
52
52
|
* right now (a session picker with no session list), the `/theme` exclusion this intent defensively
|
|
53
53
|
* reuses, or an unrecognized future kind. Never called for `show-settings`, `show-plugin-manager` or
|
|
54
|
-
* `show-agent-switcher` — all three always have a screen now
|
|
54
|
+
* `show-agent-switcher` — all three always have a screen now —
|
|
55
55
|
* but their cases are left out of this switch rather than kept as dead branches that would claim
|
|
56
56
|
* otherwise; an unknown wire-level kind (a newer host) still yields an explicit notice naming the raw
|
|
57
57
|
* kind, so the "never a silent no-op" floor holds for future intents too.
|
|
@@ -24,10 +24,10 @@ function nextRequestId(prefix: string): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* The agent switcher sheet's own correlated request/response state, the same
|
|
28
28
|
* shape `useSettingsState` already uses for `get-settings`/`update-settings` — independent from the
|
|
29
29
|
* chat conversation, because choosing an agent is answered by the sheet's own confirmation line
|
|
30
|
-
* ("Default agent: Explore"), never a conversation card
|
|
30
|
+
* ("Default agent: Explore"), never a conversation card.
|
|
31
31
|
*
|
|
32
32
|
* `openAgentSwitcher` re-fetches every time (`/agent` bare, or reopening after a close), so a change
|
|
33
33
|
* made elsewhere reaches an open-then-reopened sheet, matching `useSettingsState`'s own reasoning.
|
|
@@ -20,6 +20,7 @@ function nextRequestId(prefix: string): string {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const CONNECTION_CLOSED = 'The connection to the session closed.';
|
|
23
|
+
const READ_ONLY = 'This session is read-only.';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* The key a finished call's app view is read and called by, taken from the tool row's `appView`
|
|
@@ -53,6 +54,7 @@ function callAppToolMessage(
|
|
|
53
54
|
export function useAppViewRequests(
|
|
54
55
|
send: (msg: TClientMessage) => void,
|
|
55
56
|
isConnected: () => boolean,
|
|
57
|
+
isObserving: () => boolean,
|
|
56
58
|
): {
|
|
57
59
|
readAppView: (viewKey: string, signal?: AbortSignal) => Promise<TAppView>;
|
|
58
60
|
callAppTool: (
|
|
@@ -92,6 +94,11 @@ export function useAppViewRequests(
|
|
|
92
94
|
reject(new Error(signal?.aborted ? 'The app view closed.' : CONNECTION_CLOSED));
|
|
93
95
|
return;
|
|
94
96
|
}
|
|
97
|
+
// An observer's request is never sent, so no reply would come.
|
|
98
|
+
if (isObserving()) {
|
|
99
|
+
reject(new Error(READ_ONLY));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
95
102
|
const requestId = nextRequestId(`${requestOwner}_${prefix}`);
|
|
96
103
|
const dispose = (): void => signal?.removeEventListener('abort', abort);
|
|
97
104
|
const abort = (): void => {
|
|
@@ -109,7 +116,7 @@ export function useAppViewRequests(
|
|
|
109
116
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
110
117
|
}
|
|
111
118
|
}),
|
|
112
|
-
[isConnected, requestOwner, send],
|
|
119
|
+
[isConnected, isObserving, requestOwner, send],
|
|
113
120
|
);
|
|
114
121
|
|
|
115
122
|
const readAppView = useCallback(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Tracks whether `query` currently matches, reactively (
|
|
4
|
+
* Tracks whether `query` currently matches, reactively (the narrow-window session sheet
|
|
5
5
|
* below `md`). A host without `window.matchMedia` (an older embedding, or a non-browser test) reports
|
|
6
6
|
* `false` and never subscribes, rather than throwing.
|
|
7
7
|
*/
|
|
@@ -13,7 +13,7 @@ function nextRequestId(): string {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* The model list the model control's pop-up menu is built from, requested on
|
|
17
17
|
* demand (`list-models`) and correlated by `requestId` (mirrors `usePersonalUsageState`'s pattern) so
|
|
18
18
|
* a reply to an older request — one already superseded by a newer one, e.g. across a reconnect — is
|
|
19
19
|
* never applied over the list a still-open menu is currently showing.
|
|
@@ -25,7 +25,7 @@ function nextRequestId(prefix: string): string {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* The Project panel's correlated request/response state (status, one file's diff, project
|
|
29
29
|
* memory), independent from chat state. Mirrors `usePersonalUsageState`'s shape: a status per read, a
|
|
30
30
|
* `requestId` ref that drops a stale reply (a diff request superseded by clicking another file before
|
|
31
31
|
* the first answered), and a request function per read.
|
|
@@ -10,15 +10,15 @@ type TSchedulesState = Pick<
|
|
|
10
10
|
>;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* The Agents panel's "Scheduled" group. No `requestId` correlates
|
|
14
14
|
* `get-background-tasks`/`background_tasks` (the message carries none — see `wire-messages.ts`), so
|
|
15
15
|
* this owns that whole message type for the GUI: every reply is a full refresh of the roster,
|
|
16
16
|
* filtered server-side to `kind: 'scheduled'` (the same tasks `/schedule list` shows).
|
|
17
17
|
*
|
|
18
18
|
* Pause/resume send the SAME `command` text `/schedule pause <id>` / `/schedule resume <id>` runs.
|
|
19
19
|
* Delete sends the existing `cancel-background-task` write — the same one Stop already uses for any
|
|
20
|
-
* other background-task kind
|
|
21
|
-
* one is permanent (distinct from the reversible pause)
|
|
20
|
+
* other background-task kind — since a schedule IS a background task, and cancelling
|
|
21
|
+
* one is permanent (distinct from the reversible pause).
|
|
22
22
|
*/
|
|
23
23
|
export function useSchedulesState(send: (msg: TClientMessage) => void): TSchedulesState & {
|
|
24
24
|
/** Handles `background_tasks` and a `cancel` `background_task_control_result` — both refresh. */
|
|
@@ -46,7 +46,7 @@ export function useSchedulesState(send: (msg: TClientMessage) => void): TSchedul
|
|
|
46
46
|
if (msg.type === 'background_task_control_result' && msg.action === 'cancel') {
|
|
47
47
|
// A schedule's Delete — refresh so a successful cancel drops it from the group at once; a
|
|
48
48
|
// refused one leaves the row exactly as it was (the central reducer's own case already
|
|
49
|
-
// turns a failure into a plain-message session notice —
|
|
49
|
+
// turns a failure into a plain-message session notice — so this never duplicates it).
|
|
50
50
|
if (msg.success) requestSchedules();
|
|
51
51
|
return false; // not exclusively ours — other future consumers may still want this event
|
|
52
52
|
}
|
|
@@ -30,7 +30,7 @@ function keepsSessionsLive(listing: TSessionListing): boolean {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* A desktop Reconnect reloads the page, which loses every bit of JS state — so the
|
|
34
34
|
* session to go back to cannot live in a ref like `lastSwitchedIdRef` below. `sessionStorage` survives
|
|
35
35
|
* the reload (and only that tab), so it carries the id across it. Namespaced so a page hosting other
|
|
36
36
|
* state under the same origin does not collide.
|
|
@@ -74,14 +74,17 @@ function initialSidebarOpen(): boolean {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* The host's session list beside the conversation: requests correlated by id (the latest
|
|
78
78
|
* wins), the start/switch commands, and whether the sidebar is shown.
|
|
79
79
|
*
|
|
80
80
|
* A host that keeps sessions live binds each connection to its own session and puts a new connection
|
|
81
81
|
* on its primary one. After a reconnect to such a host this surface goes back to the session it was
|
|
82
82
|
* on; an older host has one session for everyone, so there is nothing to go back to.
|
|
83
83
|
*/
|
|
84
|
-
export function useSessionDirectoryState(
|
|
84
|
+
export function useSessionDirectoryState(
|
|
85
|
+
send: (msg: TClientMessage) => void,
|
|
86
|
+
isObserving: () => boolean,
|
|
87
|
+
): TSessionDirectoryState & {
|
|
85
88
|
handleSessionsMessage: (msg: TServerMessage) => boolean;
|
|
86
89
|
/** Whether the host can list sessions, as far as its last answer says. */
|
|
87
90
|
canListSessions: () => boolean;
|
|
@@ -117,7 +120,7 @@ export function useSessionDirectoryState(send: (msg: TClientMessage) => void): T
|
|
|
117
120
|
(): void => send({ type: 'new-session', requestId: nextRequestId('session_change') }),
|
|
118
121
|
[send, browserNamespace],
|
|
119
122
|
);
|
|
120
|
-
//
|
|
123
|
+
// Rename or delete a row in the list — current or not. Both answer on their own
|
|
121
124
|
// correlated reply (`session_renamed_in_list`/`session_deleted`, or a `_failed` counterpart); the
|
|
122
125
|
// reducer in `useSessionClient` refreshes the listing on success and raises a notice on failure.
|
|
123
126
|
const renameSessionInList = useCallback(
|
|
@@ -135,7 +138,7 @@ export function useSessionDirectoryState(send: (msg: TClientMessage) => void): T
|
|
|
135
138
|
const lastSwitchedIdRef = useRef<string | null>(null);
|
|
136
139
|
const restoreTargetRef = useRef<string | null>(null);
|
|
137
140
|
const armRestore = useCallback((): void => {
|
|
138
|
-
// A remembered id (
|
|
141
|
+
// A remembered id (a desktop Reconnect's reload) wins over the same-page target:
|
|
139
142
|
// on a fresh page, `lastSwitchedIdRef` is always null anyway, so this only ever adds a source.
|
|
140
143
|
restoreTargetRef.current = readStoredRestoreId(browserNamespace) ?? lastSwitchedIdRef.current;
|
|
141
144
|
}, [browserNamespace]);
|
|
@@ -148,12 +151,14 @@ export function useSessionDirectoryState(send: (msg: TClientMessage) => void): T
|
|
|
148
151
|
// launch with no remembered id never reaches here (target stays null), so nothing switches.
|
|
149
152
|
clearStoredRestoreId(browserNamespace);
|
|
150
153
|
if (target === listing.currentSessionId) return;
|
|
154
|
+
// An observer never moves the session, so there is nothing to go back to.
|
|
155
|
+
if (isObserving()) return;
|
|
151
156
|
if (!keepsSessionsLive(listing)) return;
|
|
152
157
|
// A session the host no longer lists (an empty one it never saved) is nothing to go back to.
|
|
153
158
|
if (!listing.sessions.some((row) => row.id === target)) return;
|
|
154
159
|
send({ type: 'switch-session', sessionId: target, requestId: nextRequestId('session_change') });
|
|
155
160
|
},
|
|
156
|
-
[send, browserNamespace],
|
|
161
|
+
[send, browserNamespace, isObserving],
|
|
157
162
|
);
|
|
158
163
|
|
|
159
164
|
const handleSessionsMessage = useCallback(
|
|
@@ -24,12 +24,12 @@ function nextRequestId(prefix: string): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* The Settings screen's own correlated request/response state, independent from chat
|
|
28
28
|
* state — the same shape `usePersonalUsageState` already uses for `get-personal-usage-report`.
|
|
29
29
|
*
|
|
30
30
|
* `openSettings` re-fetches every time (opening from the gear, `/settings`, or reopening after a
|
|
31
31
|
* close): the simplest correct way for a change made elsewhere — another typed command, another
|
|
32
|
-
* client — to reach an open-then-reopened screen
|
|
32
|
+
* client — to reach an open-then-reopened screen.
|
|
33
33
|
*
|
|
34
34
|
* A failed UPDATE never touches `settingsSnapshot`, so a control bound to it shows the value it had
|
|
35
35
|
* before the write — "a failed write leaves the control at its previous value" — while
|
|
@@ -39,7 +39,7 @@ function nextRequestId(prefix: string): string {
|
|
|
39
39
|
export function useSettingsState(send: (msg: TClientMessage) => void): TSettingsState & {
|
|
40
40
|
handleSettingsMessage: (msg: TServerMessage) => boolean;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Re-fetch without touching `settingsOpen`/`settingsInitialSectionId` — for a change
|
|
43
43
|
* made elsewhere while the screen is already open (e.g. "Add provider…"'s ask flow finishing).
|
|
44
44
|
* Unlike `openSettings`, this never resets which section is showing.
|
|
45
45
|
*/
|
|
@@ -49,7 +49,7 @@ export function useSettingsState(send: (msg: TClientMessage) => void): TSettings
|
|
|
49
49
|
const [settingsStatus, setSettingsStatus] = useState<IWsSessionState['settingsStatus']>('idle');
|
|
50
50
|
const [settingsSnapshot, setSettingsSnapshot] = useState<ISettingsSnapshot | null>(null);
|
|
51
51
|
const [settingsError, setSettingsError] = useState<string | null>(null);
|
|
52
|
-
//
|
|
52
|
+
// Which section to land on THIS open — `/plugin` opens straight to
|
|
53
53
|
// Plugins, "Manage providers…" straight to Providers & Models; everything else (the gear,
|
|
54
54
|
// `/settings`) opens on the screen's own default (General).
|
|
55
55
|
const [settingsInitialSectionId, setSettingsInitialSectionId] = useState<string | null>(null);
|