@robota-sdk/agent-ui-web 0.0.0-stage → 3.0.0-beta.101
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 +557 -0
- package/LICENSE +661 -0
- package/README.md +19 -2
- package/dist/browser/index.d.ts +736 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +8525 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/node/index.cjs +8575 -0
- package/dist/node/index.d.cts +736 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +736 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +8525 -0
- package/dist/node/index.js.map +1 -0
- package/docs/README.md +62 -0
- package/docs/SPEC.md +130 -0
- package/package.json +99 -3
- package/src/app-view/bridge.ts +217 -0
- package/src/app-view/call-tool-result.ts +58 -0
- package/src/app-view/host-context.ts +37 -0
- package/src/app-view/types.ts +12 -0
- 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/AppView.tsx +147 -0
- package/src/components/Brand.tsx +33 -0
- package/src/components/Composer.tsx +685 -0
- package/src/components/ConversationView.tsx +789 -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 +762 -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 +482 -0
- package/src/components/SessionSurfaceChrome.tsx +319 -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 +95 -0
- package/src/hooks/session-client-types.ts +344 -0
- package/src/hooks/ui-intent-state.ts +74 -0
- package/src/hooks/use-agent-switcher-state.ts +125 -0
- package/src/hooks/use-app-view-requests.ts +178 -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 +1051 -0
- package/src/index.ts +72 -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,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Check, X } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Dialog } from './Dialog.js';
|
|
7
|
+
|
|
8
|
+
import type { IWsSessionState } from '../hooks/useSessionClient.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* #3282 §4 part b-3 — the agent switcher: `show-agent-switcher` (`/agent` with no args) opens this
|
|
12
|
+
* sheet instead of the surface's old "not available" line. It lists every agent this session can
|
|
13
|
+
* run — name, one-line description, and where each is defined in plain words — with the current one
|
|
14
|
+
* checked. Choosing a row runs the SAME path as typing `/agent <name>` (a plain `command`, wired in
|
|
15
|
+
* `useAgentSwitcherState`); the reply is this sheet's own confirmation line, never a conversation
|
|
16
|
+
* card (issue #3282 §4's decided design).
|
|
17
|
+
*/
|
|
18
|
+
export function AgentSwitcherSheet({ state }: { state: IWsSessionState }): React.ReactElement | null {
|
|
19
|
+
if (!state.agentSwitcherOpen) return null;
|
|
20
|
+
|
|
21
|
+
const loading = state.agentSwitcherStatus === 'loading' && state.agentDefinitions.length === 0;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Dialog
|
|
25
|
+
open
|
|
26
|
+
onClose={() => state.closeAgentSwitcher()}
|
|
27
|
+
title="Switch agent"
|
|
28
|
+
panelClassName="flex max-h-[min(560px,85vh)] w-full max-w-md flex-col overflow-hidden rounded-2xl bg-card text-card-foreground shadow-[0_8px_30px_-12px_rgb(0_0_0/0.45)] focus:outline-none"
|
|
29
|
+
>
|
|
30
|
+
<div className="flex h-12 flex-shrink-0 items-center gap-1 border-b border-border px-3">
|
|
31
|
+
<h2 className="px-1.5 text-[15px] font-semibold text-foreground">Switch agent</h2>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
aria-label="Close"
|
|
35
|
+
onClick={() => state.closeAgentSwitcher()}
|
|
36
|
+
className="ml-auto rounded-md p-1.5 text-muted-foreground hover:bg-hover hover:text-foreground"
|
|
37
|
+
>
|
|
38
|
+
<X size={17} strokeWidth={1.75} />
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{state.agentSwitchMessage ? (
|
|
43
|
+
<p
|
|
44
|
+
role="status"
|
|
45
|
+
className="mx-4 mt-3 rounded-lg bg-raised px-3 py-2 text-[13px] text-foreground"
|
|
46
|
+
>
|
|
47
|
+
{state.agentSwitchMessage}
|
|
48
|
+
</p>
|
|
49
|
+
) : null}
|
|
50
|
+
|
|
51
|
+
<ul className="min-h-0 flex-1 overflow-y-auto p-2">
|
|
52
|
+
{loading ? (
|
|
53
|
+
<li className="px-3 py-4 text-[13.5px] text-muted-foreground">Loading agents…</li>
|
|
54
|
+
) : state.agentDefinitions.length === 0 ? (
|
|
55
|
+
<li className="px-3 py-4 text-[13.5px] text-muted-foreground">
|
|
56
|
+
No agents are configured on this host.
|
|
57
|
+
</li>
|
|
58
|
+
) : (
|
|
59
|
+
state.agentDefinitions.map((agent) => {
|
|
60
|
+
const isCurrent = agent.name === state.currentAgentType;
|
|
61
|
+
return (
|
|
62
|
+
<li key={agent.name}>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onClick={() => state.selectAgent(agent.name)}
|
|
66
|
+
aria-current={isCurrent ? 'true' : undefined}
|
|
67
|
+
className="flex w-full items-start gap-3 rounded-xl px-3 py-2.5 text-left hover:bg-hover"
|
|
68
|
+
>
|
|
69
|
+
<span className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center">
|
|
70
|
+
{isCurrent ? (
|
|
71
|
+
<Check size={16} strokeWidth={2} className="text-accent" />
|
|
72
|
+
) : null}
|
|
73
|
+
</span>
|
|
74
|
+
<span className="min-w-0 flex-1">
|
|
75
|
+
<span className="block truncate text-[14px] font-medium text-foreground">
|
|
76
|
+
{agent.name}
|
|
77
|
+
</span>
|
|
78
|
+
<span className="block truncate text-[13px] text-muted-foreground">
|
|
79
|
+
{agent.description}
|
|
80
|
+
</span>
|
|
81
|
+
<span className="block truncate text-[12px] text-subtle">
|
|
82
|
+
{agent.definedIn}
|
|
83
|
+
</span>
|
|
84
|
+
</span>
|
|
85
|
+
</button>
|
|
86
|
+
</li>
|
|
87
|
+
);
|
|
88
|
+
})
|
|
89
|
+
)}
|
|
90
|
+
</ul>
|
|
91
|
+
</Dialog>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { createAppViewBridge } from '../app-view/bridge.js';
|
|
6
|
+
import { toCallToolResult } from '../app-view/call-tool-result.js';
|
|
7
|
+
import { AppViewHostContext, readAppViewHostContext } from '../app-view/host-context.js';
|
|
8
|
+
import { appViewKey } from '../hooks/use-app-view-requests.js';
|
|
9
|
+
|
|
10
|
+
import type { IAppViewHost } from '../app-view/host-context.js';
|
|
11
|
+
import type { TAppView } from '../app-view/types.js';
|
|
12
|
+
import type { IActiveTool } from '../hooks/useSessionClient.js';
|
|
13
|
+
|
|
14
|
+
const INITIAL_HEIGHT_PX = 160;
|
|
15
|
+
|
|
16
|
+
export const APP_VIEW_UNAVAILABLE_TEXT =
|
|
17
|
+
'This tool returned an app view, which this window cannot show.';
|
|
18
|
+
|
|
19
|
+
type TLoad =
|
|
20
|
+
| { status: 'loading' }
|
|
21
|
+
| { status: 'ready'; view: TAppView; host: IAppViewHost; sandboxUrl: string; signal: AbortSignal }
|
|
22
|
+
| { status: 'failed'; reason: string };
|
|
23
|
+
|
|
24
|
+
export function AppView({ tool }: { tool: IActiveTool }): React.ReactElement | null {
|
|
25
|
+
const host = useContext(AppViewHostContext);
|
|
26
|
+
if (tool.appView === undefined) return null;
|
|
27
|
+
const viewKey = appViewKey(tool);
|
|
28
|
+
|
|
29
|
+
if (host === null || host.sandboxUrl === undefined || viewKey === undefined) {
|
|
30
|
+
return <p className="text-[13px] text-subtle">{APP_VIEW_UNAVAILABLE_TEXT}</p>;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<AppViewFrame
|
|
34
|
+
key={viewKey}
|
|
35
|
+
tool={tool}
|
|
36
|
+
host={host}
|
|
37
|
+
sandboxUrl={host.sandboxUrl}
|
|
38
|
+
viewKey={viewKey}
|
|
39
|
+
sourceId={tool.appView.sourceId}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function AppViewFrame({
|
|
45
|
+
tool,
|
|
46
|
+
host,
|
|
47
|
+
sandboxUrl,
|
|
48
|
+
viewKey,
|
|
49
|
+
sourceId,
|
|
50
|
+
}: {
|
|
51
|
+
tool: IActiveTool;
|
|
52
|
+
host: IAppViewHost;
|
|
53
|
+
sandboxUrl: string;
|
|
54
|
+
|
|
55
|
+
viewKey: string;
|
|
56
|
+
sourceId: string;
|
|
57
|
+
}): React.ReactElement {
|
|
58
|
+
const [load, setLoad] = useState<TLoad>({ status: 'loading' });
|
|
59
|
+
const [height, setHeight] = useState(INITIAL_HEIGHT_PX);
|
|
60
|
+
const frameRef = useRef<HTMLIFrameElement>(null);
|
|
61
|
+
const toolRef = useRef(tool);
|
|
62
|
+
toolRef.current = tool;
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
setLoad({ status: 'loading' });
|
|
67
|
+
host.readAppView(viewKey, controller.signal).then(
|
|
68
|
+
(view) => {
|
|
69
|
+
if (!controller.signal.aborted)
|
|
70
|
+
setLoad({ status: 'ready', view, host, sandboxUrl, signal: controller.signal });
|
|
71
|
+
},
|
|
72
|
+
(error: unknown) => {
|
|
73
|
+
if (!controller.signal.aborted) {
|
|
74
|
+
setLoad({
|
|
75
|
+
status: 'failed',
|
|
76
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
return () => {
|
|
82
|
+
controller.abort();
|
|
83
|
+
};
|
|
84
|
+
}, [viewKey, host.readAppView, host.callAppTool, sandboxUrl]);
|
|
85
|
+
|
|
86
|
+
const ready =
|
|
87
|
+
load.status === 'ready' &&
|
|
88
|
+
load.host.readAppView === host.readAppView &&
|
|
89
|
+
load.host.callAppTool === host.callAppTool &&
|
|
90
|
+
load.sandboxUrl === sandboxUrl
|
|
91
|
+
? load
|
|
92
|
+
: null;
|
|
93
|
+
// A layout effect, so the teardown it posts on unmount still finds the frame in the document.
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
const frame = frameRef.current;
|
|
96
|
+
if (ready === null || frame === null) return undefined;
|
|
97
|
+
const current = ready.host;
|
|
98
|
+
const view = ready.view;
|
|
99
|
+
const bridge = createAppViewBridge({
|
|
100
|
+
view,
|
|
101
|
+
toolResult: toCallToolResult(toolRef.current, view),
|
|
102
|
+
hostInfo: current.hostInfo,
|
|
103
|
+
hostContext: readAppViewHostContext(current.platform),
|
|
104
|
+
// The frame's origin is opaque, so no target origin can name it; the bridge accepts replies
|
|
105
|
+
|
|
106
|
+
post: (message) => frame.contentWindow?.postMessage(message, '*'),
|
|
107
|
+
source: () => frame.contentWindow,
|
|
108
|
+
|
|
109
|
+
callAppTool: (name, args) => current.callAppTool(viewKey, name, args, ready.signal),
|
|
110
|
+
onHeight: setHeight,
|
|
111
|
+
});
|
|
112
|
+
const onMessage = (event: MessageEvent): void => bridge.handleMessage(event);
|
|
113
|
+
window.addEventListener('message', onMessage);
|
|
114
|
+
return () => {
|
|
115
|
+
window.removeEventListener('message', onMessage);
|
|
116
|
+
bridge.dispose();
|
|
117
|
+
};
|
|
118
|
+
}, [ready, viewKey]);
|
|
119
|
+
|
|
120
|
+
if (load.status === 'failed') {
|
|
121
|
+
return (
|
|
122
|
+
<p className="text-[13px] text-subtle">
|
|
123
|
+
The app from {sourceId} could not be shown: {load.reason}
|
|
124
|
+
</p>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (ready === null) {
|
|
128
|
+
return <p className="text-[13px] text-subtle">Loading the app from {sourceId}…</p>;
|
|
129
|
+
}
|
|
130
|
+
return (
|
|
131
|
+
<div className="my-1 flex flex-col gap-1">
|
|
132
|
+
<p className="text-[12px] text-subtle">App from {sourceId}</p>
|
|
133
|
+
<iframe
|
|
134
|
+
ref={frameRef}
|
|
135
|
+
|
|
136
|
+
// `allow-same-origin` the frame has an opaque origin, so its document cannot read this page.
|
|
137
|
+
sandbox="allow-scripts"
|
|
138
|
+
src={sandboxUrl}
|
|
139
|
+
title={`App from ${sourceId}`}
|
|
140
|
+
referrerPolicy="no-referrer"
|
|
141
|
+
data-testid="app-view-frame"
|
|
142
|
+
style={{ width: '100%', height }}
|
|
143
|
+
className="block rounded-lg border border-border bg-card"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useProductIdentity } from '../product-identity.js';
|
|
2
|
+
/** Shared mark: a rounded face with two eyes, in the accent colour. */
|
|
3
|
+
export function ProductMark({ size = 20 }: { size?: number }): React.ReactElement {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={size}
|
|
7
|
+
height={size}
|
|
8
|
+
viewBox="0 0 20 20"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
className="flex-shrink-0"
|
|
11
|
+
>
|
|
12
|
+
<rect x="1" y="2.5" width="18" height="15" rx="5.5" className="fill-accent" />
|
|
13
|
+
<circle cx="7.2" cy="10" r="1.7" className="fill-accent-foreground" />
|
|
14
|
+
<circle cx="12.8" cy="10" r="1.7" className="fill-accent-foreground" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The mark and the wordmark, as the app's name in its chrome. */
|
|
20
|
+
export function ProductWordmark({ surface }: { surface?: string }): React.ReactElement {
|
|
21
|
+
const { identity } = useProductIdentity();
|
|
22
|
+
return (
|
|
23
|
+
<span className="flex items-center gap-2">
|
|
24
|
+
<ProductMark />
|
|
25
|
+
<span className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">{identity.displayName}</span>
|
|
26
|
+
{surface ? (
|
|
27
|
+
<span className="rounded-md bg-raised px-1.5 py-px text-[11px] font-medium text-muted-foreground">
|
|
28
|
+
{surface}
|
|
29
|
+
</span>
|
|
30
|
+
) : null}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|