@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +557 -0
  2. package/LICENSE +661 -0
  3. package/README.md +19 -2
  4. package/dist/browser/index.d.ts +736 -0
  5. package/dist/browser/index.d.ts.map +1 -0
  6. package/dist/browser/index.js +8525 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/node/index.cjs +8575 -0
  9. package/dist/node/index.d.cts +736 -0
  10. package/dist/node/index.d.cts.map +1 -0
  11. package/dist/node/index.d.ts +736 -0
  12. package/dist/node/index.d.ts.map +1 -0
  13. package/dist/node/index.js +8525 -0
  14. package/dist/node/index.js.map +1 -0
  15. package/docs/README.md +62 -0
  16. package/docs/SPEC.md +130 -0
  17. package/package.json +99 -3
  18. package/src/app-view/bridge.ts +217 -0
  19. package/src/app-view/call-tool-result.ts +58 -0
  20. package/src/app-view/host-context.ts +37 -0
  21. package/src/app-view/types.ts +12 -0
  22. package/src/client/runtime-host.ts +84 -0
  23. package/src/client/ws-session-client.ts +131 -0
  24. package/src/clipboard.ts +81 -0
  25. package/src/components/AgentActivityPanel.tsx +371 -0
  26. package/src/components/AgentSwitcherSheet.tsx +93 -0
  27. package/src/components/AppView.tsx +147 -0
  28. package/src/components/Brand.tsx +33 -0
  29. package/src/components/Composer.tsx +685 -0
  30. package/src/components/ConversationView.tsx +789 -0
  31. package/src/components/Dialog.tsx +230 -0
  32. package/src/components/DiffLines.tsx +81 -0
  33. package/src/components/ExecutionDetailSheet.tsx +142 -0
  34. package/src/components/HelpSheet.tsx +104 -0
  35. package/src/components/PermissionPrompt.tsx +762 -0
  36. package/src/components/PersonalUsageDashboard.tsx +39 -0
  37. package/src/components/PersonalUsageDashboardDetails.tsx +368 -0
  38. package/src/components/PersonalUsageDashboardSections.tsx +250 -0
  39. package/src/components/PopupMenu.tsx +166 -0
  40. package/src/components/ProjectPanel.tsx +268 -0
  41. package/src/components/SessionMonitor.tsx +180 -0
  42. package/src/components/SessionSidebar.tsx +429 -0
  43. package/src/components/SessionSurface.tsx +482 -0
  44. package/src/components/SessionSurfaceChrome.tsx +319 -0
  45. package/src/components/SettingsFields.tsx +159 -0
  46. package/src/components/SettingsGeneralSection.tsx +49 -0
  47. package/src/components/SettingsMcpSection.tsx +157 -0
  48. package/src/components/SettingsPermissionsSection.tsx +137 -0
  49. package/src/components/SettingsPluginsSection.tsx +165 -0
  50. package/src/components/SettingsProvidersSection.tsx +211 -0
  51. package/src/components/SettingsScreen.tsx +340 -0
  52. package/src/components/StatusControls.tsx +294 -0
  53. package/src/components/composer-attachments.ts +246 -0
  54. package/src/components/personal-usage-dashboard-types.ts +26 -0
  55. package/src/driver-labels.ts +88 -0
  56. package/src/execution-entry-status.ts +55 -0
  57. package/src/goal-view.ts +19 -0
  58. package/src/hooks/command-menu.ts +65 -0
  59. package/src/hooks/excluded-commands.ts +52 -0
  60. package/src/hooks/prompt-state.ts +83 -0
  61. package/src/hooks/server-message-handling.ts +95 -0
  62. package/src/hooks/session-client-types.ts +344 -0
  63. package/src/hooks/ui-intent-state.ts +74 -0
  64. package/src/hooks/use-agent-switcher-state.ts +125 -0
  65. package/src/hooks/use-app-view-requests.ts +178 -0
  66. package/src/hooks/use-execution-detail.ts +118 -0
  67. package/src/hooks/use-media-query.ts +27 -0
  68. package/src/hooks/use-model-list.ts +45 -0
  69. package/src/hooks/use-personal-usage.ts +138 -0
  70. package/src/hooks/use-project-panel.ts +117 -0
  71. package/src/hooks/use-schedules-state.ts +92 -0
  72. package/src/hooks/use-session-directory.ts +202 -0
  73. package/src/hooks/use-settings-state.ts +115 -0
  74. package/src/hooks/useSessionClient.ts +1051 -0
  75. package/src/index.ts +72 -0
  76. package/src/product-identity.tsx +26 -0
  77. package/src/schedule-view.ts +92 -0
  78. package/src/styles/surface.css +231 -0
  79. package/src/styles/surface.css.d.mts +1 -0
  80. package/src/styles/theme.css +38 -0
  81. 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
+ }