@robota-sdk/agent-ui-web 0.0.0-stage → 3.0.0-beta.94
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 +409 -0
- package/LICENSE +661 -0
- package/README.md +19 -2
- package/dist/browser/index.d.ts +715 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +7923 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/node/index.cjs +7973 -0
- package/dist/node/index.d.cts +715 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +715 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +7923 -0
- package/dist/node/index.js.map +1 -0
- package/docs/README.md +62 -0
- package/docs/SPEC.md +124 -0
- package/package.json +99 -3
- package/src/client/runtime-host.ts +84 -0
- package/src/client/ws-session-client.ts +131 -0
- package/src/clipboard.ts +81 -0
- package/src/components/AgentActivityPanel.tsx +371 -0
- package/src/components/AgentSwitcherSheet.tsx +93 -0
- package/src/components/Brand.tsx +33 -0
- package/src/components/Composer.tsx +685 -0
- package/src/components/ConversationView.tsx +772 -0
- package/src/components/Dialog.tsx +230 -0
- package/src/components/DiffLines.tsx +81 -0
- package/src/components/ExecutionDetailSheet.tsx +142 -0
- package/src/components/HelpSheet.tsx +104 -0
- package/src/components/PermissionPrompt.tsx +661 -0
- package/src/components/PersonalUsageDashboard.tsx +39 -0
- package/src/components/PersonalUsageDashboardDetails.tsx +368 -0
- package/src/components/PersonalUsageDashboardSections.tsx +250 -0
- package/src/components/PopupMenu.tsx +166 -0
- package/src/components/ProjectPanel.tsx +268 -0
- package/src/components/SessionMonitor.tsx +180 -0
- package/src/components/SessionSidebar.tsx +429 -0
- package/src/components/SessionSurface.tsx +455 -0
- package/src/components/SessionSurfaceChrome.tsx +316 -0
- package/src/components/SettingsFields.tsx +159 -0
- package/src/components/SettingsGeneralSection.tsx +49 -0
- package/src/components/SettingsMcpSection.tsx +157 -0
- package/src/components/SettingsPermissionsSection.tsx +137 -0
- package/src/components/SettingsPluginsSection.tsx +165 -0
- package/src/components/SettingsProvidersSection.tsx +211 -0
- package/src/components/SettingsScreen.tsx +340 -0
- package/src/components/StatusControls.tsx +294 -0
- package/src/components/composer-attachments.ts +246 -0
- package/src/components/personal-usage-dashboard-types.ts +26 -0
- package/src/driver-labels.ts +88 -0
- package/src/execution-entry-status.ts +55 -0
- package/src/goal-view.ts +19 -0
- package/src/hooks/command-menu.ts +65 -0
- package/src/hooks/excluded-commands.ts +52 -0
- package/src/hooks/prompt-state.ts +83 -0
- package/src/hooks/server-message-handling.ts +92 -0
- package/src/hooks/session-client-types.ts +329 -0
- package/src/hooks/ui-intent-state.ts +74 -0
- package/src/hooks/use-agent-switcher-state.ts +125 -0
- package/src/hooks/use-execution-detail.ts +118 -0
- package/src/hooks/use-media-query.ts +27 -0
- package/src/hooks/use-model-list.ts +45 -0
- package/src/hooks/use-personal-usage.ts +138 -0
- package/src/hooks/use-project-panel.ts +117 -0
- package/src/hooks/use-schedules-state.ts +92 -0
- package/src/hooks/use-session-directory.ts +202 -0
- package/src/hooks/use-settings-state.ts +115 -0
- package/src/hooks/useSessionClient.ts +991 -0
- package/src/index.ts +66 -0
- package/src/product-identity.tsx +26 -0
- package/src/schedule-view.ts +92 -0
- package/src/styles/surface.css +231 -0
- package/src/styles/surface.css.d.mts +1 -0
- package/src/styles/theme.css +38 -0
- package/src/styles/theme.css.d.mts +1 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { TServerMessage } from '@robota-sdk/agent-transport';
|
|
2
|
+
|
|
3
|
+
export type TServerMessageHandling =
|
|
4
|
+
'reducer-state' | 'visible-notice' | 'intentionally-not-rendered' | 'transport-control';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ARCH-2164: exhaustive ownership for every decoded server variant on the shared GUI surface.
|
|
8
|
+
* Adding a wire variant without deciding its GUI disposition is a compile error instead of a silent
|
|
9
|
+
* reducer omission. `intentionally-not-rendered` is an explicit disposition for server-side query
|
|
10
|
+
* snapshots and domain events that this session/usage surface does not currently visualize.
|
|
11
|
+
*/
|
|
12
|
+
export const SERVER_MESSAGE_HANDLING = {
|
|
13
|
+
text_delta: 'reducer-state',
|
|
14
|
+
user_message: 'reducer-state',
|
|
15
|
+
tool_start: 'reducer-state',
|
|
16
|
+
tool_end: 'reducer-state',
|
|
17
|
+
thinking: 'reducer-state',
|
|
18
|
+
complete: 'reducer-state',
|
|
19
|
+
interrupted: 'reducer-state',
|
|
20
|
+
error: 'visible-notice',
|
|
21
|
+
command_result: 'reducer-state',
|
|
22
|
+
messages: 'reducer-state',
|
|
23
|
+
// #3189: the full history and its change signal serve a client that renders the whole session
|
|
24
|
+
// (the TUI); this surface builds its transcript from `messages` and the streamed turn.
|
|
25
|
+
history: 'intentionally-not-rendered',
|
|
26
|
+
history_changed: 'intentionally-not-rendered',
|
|
27
|
+
context: 'intentionally-not-rendered',
|
|
28
|
+
turn_source: 'intentionally-not-rendered',
|
|
29
|
+
commands: 'reducer-state',
|
|
30
|
+
session_status: 'reducer-state',
|
|
31
|
+
// #3282 §2 part 2: the reply to `list-models`, fed into `modelList` state by the model control's
|
|
32
|
+
// pop-up menu (`useModelListState`).
|
|
33
|
+
model_list: 'reducer-state',
|
|
34
|
+
// #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state (`useAgentSwitcherState`).
|
|
35
|
+
agent_definitions: 'reducer-state',
|
|
36
|
+
sessions: 'reducer-state',
|
|
37
|
+
sessions_error: 'reducer-state',
|
|
38
|
+
session_switched: 'reducer-state',
|
|
39
|
+
session_change_failed: 'visible-notice',
|
|
40
|
+
session_renamed_in_list: 'reducer-state',
|
|
41
|
+
session_rename_failed: 'visible-notice',
|
|
42
|
+
session_deleted: 'reducer-state',
|
|
43
|
+
session_delete_failed: 'visible-notice',
|
|
44
|
+
usage_report: 'reducer-state',
|
|
45
|
+
personal_usage_report: 'reducer-state',
|
|
46
|
+
personal_usage_report_error: 'visible-notice',
|
|
47
|
+
stored_session_usage_report: 'reducer-state',
|
|
48
|
+
stored_session_usage_report_error: 'visible-notice',
|
|
49
|
+
executing: 'intentionally-not-rendered',
|
|
50
|
+
// #3280 §2: the prompt queued behind a running turn — shown above the composer with Edit/Remove.
|
|
51
|
+
pending: 'reducer-state',
|
|
52
|
+
execution_workspace_event: 'reducer-state',
|
|
53
|
+
// #3288 §1: the Agents panel's detail sheet reads a page at a time.
|
|
54
|
+
execution_detail: 'reducer-state',
|
|
55
|
+
execution_detail_error: 'reducer-state',
|
|
56
|
+
// Esc's loop stop is a TUI-only shortcut; this surface's loops stop through the Agents panel
|
|
57
|
+
// instead (`/loop stop <id>`, or `stop-waiting-loop` is simply never sent here).
|
|
58
|
+
waiting_loop_stop: 'intentionally-not-rendered',
|
|
59
|
+
// The Agents panel already reflects a task's cancel/close/send through `execution_workspace_event`;
|
|
60
|
+
// nothing here renders this narrower, task-only event on its own.
|
|
61
|
+
// #3282 §4c: the Project panel's reads — reduced into state by `useProjectPanelState`.
|
|
62
|
+
project_status: 'reducer-state',
|
|
63
|
+
project_diff: 'reducer-state',
|
|
64
|
+
project_memory: 'reducer-state',
|
|
65
|
+
background_task_event: 'intentionally-not-rendered',
|
|
66
|
+
background_job_group_event: 'intentionally-not-rendered',
|
|
67
|
+
plan_event: 'intentionally-not-rendered',
|
|
68
|
+
context_file_refreshed: 'intentionally-not-rendered',
|
|
69
|
+
branch_event: 'intentionally-not-rendered',
|
|
70
|
+
// #3282 §4 part b-3: the Agents panel's "Scheduled" group's own roster (`useSchedulesState`).
|
|
71
|
+
background_tasks: 'reducer-state',
|
|
72
|
+
background_task: 'intentionally-not-rendered',
|
|
73
|
+
background_job_groups: 'intentionally-not-rendered',
|
|
74
|
+
background_job_group: 'intentionally-not-rendered',
|
|
75
|
+
background_task_log: 'intentionally-not-rendered',
|
|
76
|
+
permission_request: 'reducer-state',
|
|
77
|
+
ask_request: 'reducer-state',
|
|
78
|
+
prompt_resolved: 'reducer-state',
|
|
79
|
+
ui_intent: 'reducer-state',
|
|
80
|
+
session_renamed: 'reducer-state',
|
|
81
|
+
history_cleared: 'reducer-state',
|
|
82
|
+
// #3288 §1: a failed Stop (e.g. a task that finished a moment before it arrived) is worth telling
|
|
83
|
+
// the operator; a success shows itself through the workspace snapshot's own update. #3282 §4 part
|
|
84
|
+
// b-3: a schedule's Delete uses this same `cancel-background-task` write — `useSchedulesState`
|
|
85
|
+
// refreshes the roster on a successful cancel, and a failure gets the same visible notice as Stop.
|
|
86
|
+
background_task_control_result: 'visible-notice',
|
|
87
|
+
protocol_error: 'visible-notice',
|
|
88
|
+
resume_gap: 'transport-control',
|
|
89
|
+
// #3282 §4a: the Settings screen's own correlated request/response state (`useSettingsState`).
|
|
90
|
+
settings: 'reducer-state',
|
|
91
|
+
settings_error: 'reducer-state',
|
|
92
|
+
} as const satisfies Readonly<Record<TServerMessage['type'], TServerMessageHandling>>;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { TConnectionStatus, TClientMessage } from '../client/ws-session-client.js';
|
|
2
|
+
import type { TPendingPrompt } from './prompt-state.js';
|
|
3
|
+
import type { TActionResponse } from '@robota-sdk/agent-interface-transport';
|
|
4
|
+
import type {
|
|
5
|
+
IDiffLine,
|
|
6
|
+
IToolState,
|
|
7
|
+
ISettingsSnapshot,
|
|
8
|
+
TDriverId,
|
|
9
|
+
TPermissionResultValue,
|
|
10
|
+
TSettingsPatch,
|
|
11
|
+
} from '@robota-sdk/agent-interface-session';
|
|
12
|
+
import type {
|
|
13
|
+
IBackgroundTaskState,
|
|
14
|
+
IExecutionDetailRecord,
|
|
15
|
+
IExecutionWorkspaceSnapshot,
|
|
16
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
17
|
+
import type { IWireAgentDefinitionSummary, TServerMessage } from '@robota-sdk/agent-transport';
|
|
18
|
+
|
|
19
|
+
export type TPersonalUsageReport = Extract<
|
|
20
|
+
TServerMessage,
|
|
21
|
+
{ type: 'personal_usage_report' }
|
|
22
|
+
>['report'];
|
|
23
|
+
export type TStoredSessionUsageReport = Extract<
|
|
24
|
+
TServerMessage,
|
|
25
|
+
{ type: 'stored_session_usage_report' }
|
|
26
|
+
>['report'];
|
|
27
|
+
/** The commands and skills the session offers — what a `/` menu lists. */
|
|
28
|
+
export type TCommandCatalog = Omit<Extract<TServerMessage, { type: 'commands' }>, 'type'>;
|
|
29
|
+
/** The session's status beside the conversation: model, permission mode, effort, context. */
|
|
30
|
+
export type TSessionStatus = Extract<TServerMessage, { type: 'session_status' }>['status'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* #3282 §2 (part 2): the models a person may switch to, grouped by configured provider profile —
|
|
34
|
+
* what the model control's pop-up menu is built from. `null` until requested (`list-models`).
|
|
35
|
+
*/
|
|
36
|
+
export type TModelListSnapshot = Omit<
|
|
37
|
+
Extract<TServerMessage, { type: 'model_list' }>,
|
|
38
|
+
'type' | 'requestId'
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
/** The host's sessions in this workspace, which one is current, and the records it could not read. */
|
|
42
|
+
export type TSessionListing = Extract<TServerMessage, { type: 'sessions' }>['listing'];
|
|
43
|
+
/** Why the host did not list its sessions: it cannot (`not_available`), or listing failed. */
|
|
44
|
+
export type TSessionsError = Omit<
|
|
45
|
+
Extract<TServerMessage, { type: 'sessions_error' }>,
|
|
46
|
+
'type' | 'requestId'
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
export type TCurrentSessionUsageReport = Extract<
|
|
50
|
+
TServerMessage,
|
|
51
|
+
{ type: 'usage_report' }
|
|
52
|
+
>['report'];
|
|
53
|
+
|
|
54
|
+
/** #3282 §4c — the Project panel's reads. Each result crosses the wire unchanged from the session
|
|
55
|
+
* method's own return value (see `wire-messages.ts`'s doc comment on `project_status`). */
|
|
56
|
+
export type TProjectStatusRead = Extract<TServerMessage, { type: 'project_status' }>['result'];
|
|
57
|
+
export type TProjectDiffRead = Extract<TServerMessage, { type: 'project_diff' }>['result'];
|
|
58
|
+
export type TProjectMemoryRead = Extract<TServerMessage, { type: 'project_memory' }>['result'];
|
|
59
|
+
|
|
60
|
+
export interface IConversationMessage {
|
|
61
|
+
id: string;
|
|
62
|
+
role: 'user' | 'assistant';
|
|
63
|
+
content: string;
|
|
64
|
+
isStreaming?: boolean;
|
|
65
|
+
/** REMOTE-014 E5: the co-driving author of a user turn. */
|
|
66
|
+
author?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The outcome of a slash command, shown in the conversation where it was typed — the TUI adds the
|
|
71
|
+
* same outcome to its transcript. `info` is a command this surface cannot carry out.
|
|
72
|
+
*/
|
|
73
|
+
export interface ICommandOutputEntry {
|
|
74
|
+
id: string;
|
|
75
|
+
role: 'command';
|
|
76
|
+
name: string;
|
|
77
|
+
content: string;
|
|
78
|
+
tone: 'success' | 'error' | 'info';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The tool calls of one finished turn, kept in the conversation ahead of the reply they led to. */
|
|
82
|
+
export interface IToolGroupEntry {
|
|
83
|
+
id: string;
|
|
84
|
+
role: 'tools';
|
|
85
|
+
tools: readonly IActiveTool[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One file an Edit/Write call touched during a turn, and the (latest) diff a click can open. */
|
|
89
|
+
export interface IChangedFileSummary {
|
|
90
|
+
path: string;
|
|
91
|
+
added: number;
|
|
92
|
+
removed: number;
|
|
93
|
+
diffLines: readonly IDiffLine[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** #3288: a turn that changed files ends with this compact summary row. */
|
|
97
|
+
export interface IChangedFilesEntry {
|
|
98
|
+
id: string;
|
|
99
|
+
role: 'changed-files';
|
|
100
|
+
files: readonly IChangedFileSummary[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type TConversationEntry =
|
|
104
|
+
IConversationMessage | ICommandOutputEntry | IToolGroupEntry | IChangedFilesEntry;
|
|
105
|
+
|
|
106
|
+
export interface IActiveTool {
|
|
107
|
+
id: string;
|
|
108
|
+
name: string;
|
|
109
|
+
status: 'running' | 'done' | 'error';
|
|
110
|
+
input?: string;
|
|
111
|
+
result?: IToolState['result'];
|
|
112
|
+
/** #3288: correlates this call across `tool_start`/`tool_end` — how a finished call is matched. */
|
|
113
|
+
executionId?: string;
|
|
114
|
+
/** #3288: a unified diff (Edit/Write), built server-side — the same one an approval prompt showed. */
|
|
115
|
+
diffLines?: readonly IDiffLine[];
|
|
116
|
+
/** #3288: the file `diffLines` concerns, workspace-relative when the server could make it so. */
|
|
117
|
+
diffFile?: string;
|
|
118
|
+
/** #3288: the tool's raw result payload (Shell output, Read content, …), already capped server-side. */
|
|
119
|
+
toolResultData?: string;
|
|
120
|
+
/** Admitted observations; opaque resource URIs do not authorize a client-side fetch. */
|
|
121
|
+
toolResultParts?: IToolState['toolResultParts'];
|
|
122
|
+
/** #3288: a workspace-relative display form of a path argument, additive to `input`/`firstArg`. */
|
|
123
|
+
displayPath?: string;
|
|
124
|
+
/** #3288: set when this call is a projected `/command` tool — render "Ran /<commandName>". */
|
|
125
|
+
commandName?: string;
|
|
126
|
+
/** #3288: true for an internal signal tool (e.g. goal-status) — never rendered as a call. */
|
|
127
|
+
internal?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* #3280 §2: a prompt the host queued behind the running turn — the server has already taken it, so it
|
|
132
|
+
* is not lost, but it will not run until the turn ends. `count` is the TOTAL number waiting (the shown
|
|
133
|
+
* `text` plus any behind it), matching the wire's `pendingCount`.
|
|
134
|
+
*/
|
|
135
|
+
export interface IQueuedPrompt {
|
|
136
|
+
text: string;
|
|
137
|
+
count: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface ISessionNotice {
|
|
141
|
+
id: string;
|
|
142
|
+
/**
|
|
143
|
+
* `session-change-refused`: the host refused a new or switch this surface asked for.
|
|
144
|
+
* `background-task-control-failed`: a Stop in the Agents panel did not take effect.
|
|
145
|
+
* `command-failed` (#3282 §2 part 2): a change made through a status control (model/mode/effort)
|
|
146
|
+
* failed — a plain notice rather than a conversation card, since the control's own label is where
|
|
147
|
+
* a successful change confirms itself and the label stays unchanged when it fails.
|
|
148
|
+
*/
|
|
149
|
+
kind:
|
|
150
|
+
| 'session-error'
|
|
151
|
+
| 'protocol-error'
|
|
152
|
+
| 'session-change-refused'
|
|
153
|
+
| 'background-task-control-failed'
|
|
154
|
+
| 'command-failed';
|
|
155
|
+
message: string;
|
|
156
|
+
/**
|
|
157
|
+
* A `session-error` classified at the wire boundary (#3289 §3) — present only when the host could
|
|
158
|
+
* tell what kind of failure this was. `SessionNotices` maps it to one plain sentence and keeps
|
|
159
|
+
* `message` behind a "Details" disclosure; absent, it shows `message` as before.
|
|
160
|
+
*/
|
|
161
|
+
code?: 'auth' | 'rate_limit' | 'model_unavailable' | 'network' | 'provider';
|
|
162
|
+
provider?: string;
|
|
163
|
+
retryAfterSeconds?: number;
|
|
164
|
+
/**
|
|
165
|
+
* For `code: 'model_unavailable'`, the model the failed request tried — captured when the notice
|
|
166
|
+
* was created (from the wire frame's own `model`, or `sessionStatus` at that moment), never read
|
|
167
|
+
* live off the CURRENT session status. The person may switch models before dismissing the notice,
|
|
168
|
+
* and a live read would then blame the new model for the old failure.
|
|
169
|
+
*/
|
|
170
|
+
model?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ISessionClientHandle {
|
|
174
|
+
connect: () => void;
|
|
175
|
+
disconnect: () => void;
|
|
176
|
+
send: (msg: TClientMessage) => void;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type TMakeSessionClient<TStatus extends string = TConnectionStatus> = (callbacks: {
|
|
180
|
+
onMessage: (msg: TServerMessage) => void;
|
|
181
|
+
onStatusChange: (status: TStatus) => void;
|
|
182
|
+
}) => ISessionClientHandle;
|
|
183
|
+
|
|
184
|
+
export interface IWsSessionState<TStatus extends string = TConnectionStatus> {
|
|
185
|
+
status: TStatus;
|
|
186
|
+
/**
|
|
187
|
+
* Set by `useWsSession` once its reconnect retries give up (issue #3280 §5): the runtime is not
|
|
188
|
+
* coming back by itself. Cleared again once `status` reaches `connected`. Undefined for a reducer
|
|
189
|
+
* with no such concept (e.g. the WebRTC surface, which has its own `failed`/`refused` statuses).
|
|
190
|
+
*/
|
|
191
|
+
connectionLost?: boolean;
|
|
192
|
+
messages: TConversationEntry[];
|
|
193
|
+
activeTools: IActiveTool[];
|
|
194
|
+
streamingText: string;
|
|
195
|
+
isThinking: boolean;
|
|
196
|
+
executionWorkspace: IExecutionWorkspaceSnapshot | null;
|
|
197
|
+
sessionName: string | null;
|
|
198
|
+
/** This connection's own driver id, learned from the server's first frame; null until then. */
|
|
199
|
+
ownDriverId: TDriverId | null;
|
|
200
|
+
/** Null until the session has answered; refreshed after every command. */
|
|
201
|
+
commandCatalog: TCommandCatalog | null;
|
|
202
|
+
/** Null until the session has answered; refreshed after every command and turn. */
|
|
203
|
+
sessionStatus: TSessionStatus | null;
|
|
204
|
+
/**
|
|
205
|
+
* #3282 §2 (part 2): the models the model control's pop-up menu offers. Null until requested — the
|
|
206
|
+
* menu asks for it when it opens (or refreshes it) rather than every surface fetching it eagerly.
|
|
207
|
+
*/
|
|
208
|
+
modelList: TModelListSnapshot | null;
|
|
209
|
+
/** Ask the session for the current model list (a fresh `list-models` round trip). */
|
|
210
|
+
requestModelList: () => void;
|
|
211
|
+
/** Null until the host has answered; refreshed on connect, after a switch, a rename and a turn. */
|
|
212
|
+
sessionListing: TSessionListing | null;
|
|
213
|
+
/** Set when the host answered the latest listing request with an error instead. */
|
|
214
|
+
sessionsError: TSessionsError | null;
|
|
215
|
+
requestSessions: () => void;
|
|
216
|
+
/** Make another stored session current. A refusal arrives as a notice saying why. */
|
|
217
|
+
switchSession: (sessionId: string) => void;
|
|
218
|
+
/** Start a fresh session and make it current. */
|
|
219
|
+
newSession: () => void;
|
|
220
|
+
/** Rename a row in the list — current or not. A refusal arrives as a notice saying why. */
|
|
221
|
+
renameSessionInList: (sessionId: string, name: string) => void;
|
|
222
|
+
/** Delete a stored session for good. A refusal arrives as a notice saying why. */
|
|
223
|
+
deleteSession: (sessionId: string) => void;
|
|
224
|
+
/** Whether the session sidebar is shown; `/resume` opens it. */
|
|
225
|
+
sessionSidebarOpen: boolean;
|
|
226
|
+
setSessionSidebarOpen: (open: boolean) => void;
|
|
227
|
+
send: (msg: TClientMessage) => void;
|
|
228
|
+
/**
|
|
229
|
+
* #3282 §2 (part 2): run a command the SAME way `send({type:'command', ...})` does, but mark its
|
|
230
|
+
* outcome as controls-triggered — `useSessionClient` skips the conversation card for a change made
|
|
231
|
+
* this way (the control's own label confirms it instead), while a typed command keeps its card.
|
|
232
|
+
*/
|
|
233
|
+
sendCommandSilently: (name: string, args?: string) => void;
|
|
234
|
+
pendingPrompts: readonly TPendingPrompt[];
|
|
235
|
+
/** #3280 §2: the prompt queued behind a running turn, or null when none is queued. */
|
|
236
|
+
queuedPrompt: IQueuedPrompt | null;
|
|
237
|
+
answerPermission: (id: string, result: TPermissionResultValue) => void;
|
|
238
|
+
answerAsk: (id: string, response: TActionResponse) => void;
|
|
239
|
+
personalUsageStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
240
|
+
personalUsageReport: TPersonalUsageReport | null;
|
|
241
|
+
personalUsageError: string | null;
|
|
242
|
+
requestPersonalUsage: (period: '7d' | '30d') => void;
|
|
243
|
+
storedSessionUsageStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
244
|
+
storedSessionUsageReport: TStoredSessionUsageReport | null;
|
|
245
|
+
storedSessionUsageSessionId: string | null;
|
|
246
|
+
storedSessionUsageError: string | null;
|
|
247
|
+
requestStoredSessionUsage: (sessionId: string) => void;
|
|
248
|
+
currentSessionUsageStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
249
|
+
currentSessionUsageReport: TCurrentSessionUsageReport | null;
|
|
250
|
+
requestCurrentSessionUsage: () => void;
|
|
251
|
+
sessionNotices: readonly ISessionNotice[];
|
|
252
|
+
dismissSessionNotice: (id: string) => void;
|
|
253
|
+
/** #3282 §4a: whether the Settings modal is shown. `/settings` and the gear button both open it. */
|
|
254
|
+
settingsOpen: boolean;
|
|
255
|
+
settingsStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
256
|
+
/** Null until the first `get-settings` reply arrives. */
|
|
257
|
+
settingsSnapshot: ISettingsSnapshot | null;
|
|
258
|
+
/** The plain message from the most recent failed read or write, if any. */
|
|
259
|
+
settingsError: string | null;
|
|
260
|
+
/** #3282 §4 part b-2: which section the screen should land on for the open in progress. */
|
|
261
|
+
settingsInitialSectionId: string | null;
|
|
262
|
+
/**
|
|
263
|
+
* Opens the screen and (re-)fetches its snapshot — the "reopen and see the new value" path.
|
|
264
|
+
* `sectionId` jumps straight to that section (e.g. `/plugin` → `'plugins'`, "Manage providers…"
|
|
265
|
+
* (#3282 §2/§4b) → `'providers'`); omitted opens on the screen's own default (General).
|
|
266
|
+
*/
|
|
267
|
+
openSettings: (sectionId?: string) => void;
|
|
268
|
+
closeSettings: () => void;
|
|
269
|
+
/** A failed write leaves `settingsSnapshot` at its previous value; `settingsError` names why. */
|
|
270
|
+
updateSettings: (patch: TSettingsPatch) => void;
|
|
271
|
+
/** #3282 §4e: whether the Help sheet is shown. `/help` opens it instead of reaching the session. */
|
|
272
|
+
helpOpen: boolean;
|
|
273
|
+
openHelp: () => void;
|
|
274
|
+
closeHelp: () => void;
|
|
275
|
+
/** #3288 §1: the id of the execution-workspace entry open in the detail sheet, or null when closed. */
|
|
276
|
+
openEntryId: string | null;
|
|
277
|
+
executionDetailStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
278
|
+
executionDetailRecords: readonly IExecutionDetailRecord[];
|
|
279
|
+
executionDetailError: string | null;
|
|
280
|
+
/** True once the last page read had no further cursor — nothing more to load. */
|
|
281
|
+
executionDetailComplete: boolean;
|
|
282
|
+
openExecutionDetail: (entryId: string) => void;
|
|
283
|
+
loadMoreExecutionDetail: () => void;
|
|
284
|
+
closeExecutionDetail: () => void;
|
|
285
|
+
/** #3282 §4c — the Project panel's "Changes" section (git status). */
|
|
286
|
+
projectStatusState: 'idle' | 'loading' | 'ready' | 'error';
|
|
287
|
+
projectStatus: TProjectStatusRead | null;
|
|
288
|
+
requestProjectStatus: () => void;
|
|
289
|
+
/** #3282 §4c — the Project panel's "File diff" section, for the file `projectDiffPath` names. */
|
|
290
|
+
projectDiffState: 'idle' | 'loading' | 'ready' | 'error';
|
|
291
|
+
projectDiffPath: string | null;
|
|
292
|
+
projectDiff: TProjectDiffRead | null;
|
|
293
|
+
requestProjectDiff: (path: string) => void;
|
|
294
|
+
/** #3282 §4c — the Project panel's "Memory" section. */
|
|
295
|
+
projectMemoryState: 'idle' | 'loading' | 'ready' | 'error';
|
|
296
|
+
projectMemory: TProjectMemoryRead | null;
|
|
297
|
+
requestProjectMemory: () => void;
|
|
298
|
+
/**
|
|
299
|
+
* #3282 §4 part b-3: whether the agent switcher sheet is shown. `/agent` (bare) and this surface's
|
|
300
|
+
* own control both open it.
|
|
301
|
+
*/
|
|
302
|
+
agentSwitcherOpen: boolean;
|
|
303
|
+
agentSwitcherStatus: 'idle' | 'loading' | 'ready' | 'error';
|
|
304
|
+
/** The available agents: name, one-line description, and where each is defined in plain words. */
|
|
305
|
+
agentDefinitions: readonly IWireAgentDefinitionSummary[];
|
|
306
|
+
/** The agent type `/agent <name>` (no prompt) currently selects — the checked row. */
|
|
307
|
+
currentAgentType: string | null;
|
|
308
|
+
/** The plain confirmation of the last selection's result, shown in the sheet — never a conversation card. */
|
|
309
|
+
agentSwitchMessage: string | null;
|
|
310
|
+
/** Opens the sheet and (re-)fetches its roster. */
|
|
311
|
+
openAgentSwitcher: () => void;
|
|
312
|
+
closeAgentSwitcher: () => void;
|
|
313
|
+
/** Runs the same path as `/agent <name>`. */
|
|
314
|
+
selectAgent: (agentType: string) => void;
|
|
315
|
+
/**
|
|
316
|
+
* #3282 §4 part b-3: the schedules shown in the Agents panel's "Scheduled" group — every
|
|
317
|
+
* `IBackgroundTaskState` of `kind: 'scheduled'`, refreshed on connect and after a write.
|
|
318
|
+
*/
|
|
319
|
+
scheduledTasks: readonly IBackgroundTaskState<'scheduled'>[];
|
|
320
|
+
/** Runs the same path as `/schedule pause <id>` / `/schedule resume <id>`. */
|
|
321
|
+
pauseSchedule: (taskId: string) => void;
|
|
322
|
+
resumeSchedule: (taskId: string) => void;
|
|
323
|
+
/**
|
|
324
|
+
* Permanently stops a schedule — the same `cancel-background-task` write Stop already uses. A
|
|
325
|
+
* failure surfaces the same way a failed Stop does (#3288 §1's `background-task-control-failed`
|
|
326
|
+
* session notice) — no separate error state here.
|
|
327
|
+
*/
|
|
328
|
+
deleteSchedule: (taskId: string) => void;
|
|
329
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Stage D — how the GUI surface answers a `ui_intent`.
|
|
3
|
+
*
|
|
4
|
+
* The server requester-routes `ui_intent`, so everything that arrives here was asked for by THIS
|
|
5
|
+
* surface and must be answered visibly. An intent with a GUI screen opens it (the session picker is
|
|
6
|
+
* the session sidebar, #3189; settings and the plugin manager both open the Settings modal, #3282
|
|
7
|
+
* §4a/§4 part b-2; the agent switcher is a sheet, #3282 §4 part b-3). #3282 §4e: an intent whose
|
|
8
|
+
* command is on the GUI's exclusion list never reaches here at all (`excluded-commands.ts` catches it
|
|
9
|
+
* before the session is asked) — this module only answers a screen this HOST cannot back even though
|
|
10
|
+
* the GUI knows how to render it (the session picker, when the host cannot list sessions), or a kind
|
|
11
|
+
* this build does not know at all — and it never says "not available on this surface": each gets its
|
|
12
|
+
* own plain, honest sentence instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { excludedCommandMessage } from './excluded-commands.js';
|
|
16
|
+
|
|
17
|
+
import type { TCommandUiIntent } from '@robota-sdk/agent-interface-command';
|
|
18
|
+
|
|
19
|
+
/** The command a user runs for the screen an intent asks for — how the info line is labelled. */
|
|
20
|
+
export function uiIntentCommandName(intent: TCommandUiIntent): string {
|
|
21
|
+
switch (intent.type) {
|
|
22
|
+
case 'show-plugin-manager':
|
|
23
|
+
return 'plugin';
|
|
24
|
+
case 'show-settings':
|
|
25
|
+
return 'settings';
|
|
26
|
+
case 'show-session-picker':
|
|
27
|
+
return 'resume';
|
|
28
|
+
case 'show-agent-switcher':
|
|
29
|
+
return 'agent';
|
|
30
|
+
case 'show-theme-picker':
|
|
31
|
+
return 'theme';
|
|
32
|
+
default:
|
|
33
|
+
return (intent as { type: string }).type;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The GUI screen an intent opens, or null when this surface has none for it. */
|
|
38
|
+
export function guiScreenForUiIntent(
|
|
39
|
+
intent: TCommandUiIntent,
|
|
40
|
+
): 'session-sidebar' | 'settings' | 'settings-plugins' | 'agent-switcher' | null {
|
|
41
|
+
if (intent.type === 'show-session-picker') return 'session-sidebar';
|
|
42
|
+
if (intent.type === 'show-settings') return 'settings';
|
|
43
|
+
// #3282 §4 part b-2: `/plugin` opens the Settings screen's Plugins section, the way #3331 mapped
|
|
44
|
+
// `show-settings` — replacing the "not available on this surface" line this intent used to get.
|
|
45
|
+
if (intent.type === 'show-plugin-manager') return 'settings-plugins';
|
|
46
|
+
if (intent.type === 'show-agent-switcher') return 'agent-switcher';
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Describe one intent for this surface: a plain, honest sentence for a screen this HOST cannot back
|
|
52
|
+
* right now (a session picker with no session list), the `/theme` exclusion this intent defensively
|
|
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 (#3282 §4a, §4 part b-2, §4 part b-3) —
|
|
55
|
+
* but their cases are left out of this switch rather than kept as dead branches that would claim
|
|
56
|
+
* otherwise; an unknown wire-level kind (a newer host) still yields an explicit notice naming the raw
|
|
57
|
+
* kind, so the "never a silent no-op" floor holds for future intents too.
|
|
58
|
+
*/
|
|
59
|
+
export function describeUiIntentForGui(intent: TCommandUiIntent): string {
|
|
60
|
+
switch (intent.type) {
|
|
61
|
+
case 'show-session-picker':
|
|
62
|
+
// The GUI knows how to render this screen (`guiScreenForUiIntent` says so); it is only reached
|
|
63
|
+
// here when the HOST itself cannot list sessions, which is this host's limitation, not a
|
|
64
|
+
// missing GUI feature.
|
|
65
|
+
return 'This host cannot list sessions, so there is nothing to resume.';
|
|
66
|
+
case 'show-theme-picker':
|
|
67
|
+
// Defensive only: `/theme` is on the exclusion list and never reaches the session from the GUI.
|
|
68
|
+
return excludedCommandMessage('theme');
|
|
69
|
+
default:
|
|
70
|
+
// Spec-mandated unsupported signal for an intent kind this build does not know (not a
|
|
71
|
+
// fallback: the explicit notice IS the defined behavior for an unrenderable intent).
|
|
72
|
+
return `This client does not have a screen for '${(intent as { type: string }).type}' yet.`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { IWsSessionState } from './session-client-types.js';
|
|
4
|
+
import type { TClientMessage } from '../client/ws-session-client.js';
|
|
5
|
+
import type { IWireAgentDefinitionSummary } from '@robota-sdk/agent-transport';
|
|
6
|
+
import type { TServerMessage } from '@robota-sdk/agent-transport';
|
|
7
|
+
|
|
8
|
+
type TAgentSwitcherState = Pick<
|
|
9
|
+
IWsSessionState,
|
|
10
|
+
| 'agentSwitcherOpen'
|
|
11
|
+
| 'agentSwitcherStatus'
|
|
12
|
+
| 'agentDefinitions'
|
|
13
|
+
| 'currentAgentType'
|
|
14
|
+
| 'agentSwitchMessage'
|
|
15
|
+
| 'openAgentSwitcher'
|
|
16
|
+
| 'closeAgentSwitcher'
|
|
17
|
+
| 'selectAgent'
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
let requestCounter = 0;
|
|
21
|
+
function nextRequestId(prefix: string): string {
|
|
22
|
+
requestCounter += 1;
|
|
23
|
+
return `${prefix}_${requestCounter}_${Date.now()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* #3282 §4 part b-3: the agent switcher sheet's own correlated request/response state, the same
|
|
28
|
+
* shape `useSettingsState` already uses for `get-settings`/`update-settings` — independent from the
|
|
29
|
+
* chat conversation, because choosing an agent is answered by the sheet's own confirmation line
|
|
30
|
+
* ("Default agent: Explore"), never a conversation card (issue #3282 §4's decided design).
|
|
31
|
+
*
|
|
32
|
+
* `openAgentSwitcher` re-fetches every time (`/agent` bare, or reopening after a close), so a change
|
|
33
|
+
* made elsewhere reaches an open-then-reopened sheet, matching `useSettingsState`'s own reasoning.
|
|
34
|
+
*
|
|
35
|
+
* Choosing an agent runs the SAME path as typing `/agent <name>` — a plain `command` wire message —
|
|
36
|
+
* so the write goes through `executeCommand` exactly like every other command; this hook only
|
|
37
|
+
* correlates its own request so the reply lands in the sheet, not the conversation. The central
|
|
38
|
+
* `command_result` case in `useSessionClient` calls `resolveSwitchResult` to hand it the reply
|
|
39
|
+
* (and, critically, still performs the shared `commandsInFlightRef` bookkeeping itself — this hook
|
|
40
|
+
* never short-circuits `command_result` the way `handleAgentDefinitionsMessage` short-circuits reads).
|
|
41
|
+
*/
|
|
42
|
+
export function useAgentSwitcherState(send: (msg: TClientMessage) => void): TAgentSwitcherState & {
|
|
43
|
+
/** Handles `agent_definitions` only — never `command_result` (see the note above). */
|
|
44
|
+
handleAgentDefinitionsMessage: (msg: TServerMessage) => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Called from the central `command_result` case. Returns true when this requestId was the
|
|
47
|
+
* sheet's own pending switch, meaning the central handler must not also add a conversation card.
|
|
48
|
+
*/
|
|
49
|
+
resolveSwitchResult: (
|
|
50
|
+
requestId: string | undefined,
|
|
51
|
+
result: { success: boolean; message: string },
|
|
52
|
+
) => boolean;
|
|
53
|
+
} {
|
|
54
|
+
const [agentSwitcherOpen, setAgentSwitcherOpen] = useState(false);
|
|
55
|
+
const [agentSwitcherStatus, setAgentSwitcherStatus] =
|
|
56
|
+
useState<IWsSessionState['agentSwitcherStatus']>('idle');
|
|
57
|
+
const [agentDefinitions, setAgentDefinitions] = useState<
|
|
58
|
+
readonly IWireAgentDefinitionSummary[]
|
|
59
|
+
>([]);
|
|
60
|
+
const [currentAgentType, setCurrentAgentType] = useState<string | null>(null);
|
|
61
|
+
const [agentSwitchMessage, setAgentSwitchMessage] = useState<string | null>(null);
|
|
62
|
+
const listRequestRef = useRef<string | null>(null);
|
|
63
|
+
const switchRequestRef = useRef<string | null>(null);
|
|
64
|
+
|
|
65
|
+
const handleAgentDefinitionsMessage = useCallback((msg: TServerMessage): boolean => {
|
|
66
|
+
if (msg.type !== 'agent_definitions') return false;
|
|
67
|
+
if (listRequestRef.current !== msg.requestId) return true; // superseded by a later request
|
|
68
|
+
setAgentDefinitions(msg.agents);
|
|
69
|
+
setCurrentAgentType(msg.current);
|
|
70
|
+
setAgentSwitcherStatus('ready');
|
|
71
|
+
return true;
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const requestAgentDefinitions = useCallback((): void => {
|
|
75
|
+
const requestId = nextRequestId('get_agent_definitions');
|
|
76
|
+
listRequestRef.current = requestId;
|
|
77
|
+
send({ type: 'get-agent-definitions', requestId });
|
|
78
|
+
}, [send]);
|
|
79
|
+
|
|
80
|
+
const openAgentSwitcher = useCallback((): void => {
|
|
81
|
+
setAgentSwitcherOpen(true);
|
|
82
|
+
setAgentSwitcherStatus((current) => (current === 'ready' ? 'ready' : 'loading'));
|
|
83
|
+
setAgentSwitchMessage(null);
|
|
84
|
+
requestAgentDefinitions();
|
|
85
|
+
}, [requestAgentDefinitions]);
|
|
86
|
+
|
|
87
|
+
const closeAgentSwitcher = useCallback((): void => {
|
|
88
|
+
setAgentSwitcherOpen(false);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const selectAgent = useCallback(
|
|
92
|
+
(agentType: string): void => {
|
|
93
|
+
const requestId = nextRequestId('agent_switch');
|
|
94
|
+
switchRequestRef.current = requestId;
|
|
95
|
+
setAgentSwitchMessage(null);
|
|
96
|
+
// The SAME path `/agent <name>` runs: a plain command, not a dedicated write message.
|
|
97
|
+
send({ type: 'command', name: 'agent', args: agentType, requestId });
|
|
98
|
+
},
|
|
99
|
+
[send],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const resolveSwitchResult = useCallback(
|
|
103
|
+
(requestId: string | undefined, result: { success: boolean; message: string }): boolean => {
|
|
104
|
+
if (requestId === undefined || switchRequestRef.current !== requestId) return false;
|
|
105
|
+
switchRequestRef.current = null;
|
|
106
|
+
setAgentSwitchMessage(result.message);
|
|
107
|
+
if (result.success) requestAgentDefinitions();
|
|
108
|
+
return true;
|
|
109
|
+
},
|
|
110
|
+
[requestAgentDefinitions],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
agentSwitcherOpen,
|
|
115
|
+
agentSwitcherStatus,
|
|
116
|
+
agentDefinitions,
|
|
117
|
+
currentAgentType,
|
|
118
|
+
agentSwitchMessage,
|
|
119
|
+
openAgentSwitcher,
|
|
120
|
+
closeAgentSwitcher,
|
|
121
|
+
selectAgent,
|
|
122
|
+
handleAgentDefinitionsMessage,
|
|
123
|
+
resolveSwitchResult,
|
|
124
|
+
};
|
|
125
|
+
}
|