@opengeni/react 0.12.0 → 0.15.0
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/README.md +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
package/src/provider.tsx
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
OpenGeniApiContractMismatchError,
|
|
3
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
4
|
+
type StreamConnectionState,
|
|
5
|
+
type WorkspaceControlEvent,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
useCallback,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
} from "react";
|
|
2
17
|
import type { SessionClientLike } from "./client";
|
|
3
18
|
|
|
4
19
|
export type OpenGeniContextValue = {
|
|
5
20
|
client: SessionClientLike;
|
|
6
21
|
workspaceId: string;
|
|
22
|
+
workspaceControlEvent: WorkspaceControlEvent | null;
|
|
23
|
+
workspaceControlConnectionState: StreamConnectionState | "idle" | "error";
|
|
24
|
+
registerSessionReconciler: (
|
|
25
|
+
sessionId: string,
|
|
26
|
+
key: string,
|
|
27
|
+
reconcile: () => Promise<void>,
|
|
28
|
+
) => () => void;
|
|
29
|
+
reconcileSession: (sessionId: string) => Promise<void>;
|
|
7
30
|
};
|
|
8
31
|
|
|
9
32
|
const OpenGeniContext = createContext<OpenGeniContextValue | null>(null);
|
|
33
|
+
const NOOP_REGISTER_RECONCILER: OpenGeniContextValue["registerSessionReconciler"] = () => () =>
|
|
34
|
+
undefined;
|
|
35
|
+
const NOOP_RECONCILE_SESSION: OpenGeniContextValue["reconcileSession"] = async () => undefined;
|
|
36
|
+
const CONTRACT_RELOAD_STORAGE_PREFIX = "opengeni.reloadForApiContract:";
|
|
10
37
|
|
|
11
38
|
export type OpenGeniProviderProps = {
|
|
12
39
|
client: SessionClientLike;
|
|
13
40
|
workspaceId: string;
|
|
41
|
+
onWorkspaceControlEvent?: ((event: WorkspaceControlEvent) => void) | undefined;
|
|
14
42
|
children?: ReactNode;
|
|
15
43
|
};
|
|
16
44
|
|
|
@@ -18,9 +46,161 @@ export type OpenGeniProviderProps = {
|
|
|
18
46
|
* Supplies the OpenGeni client + workspace to all hooks below it. Hooks also
|
|
19
47
|
* accept `{ client, workspaceId }` overrides per call for multi-workspace UIs.
|
|
20
48
|
*/
|
|
21
|
-
export function OpenGeniProvider({
|
|
22
|
-
|
|
23
|
-
|
|
49
|
+
export function OpenGeniProvider({
|
|
50
|
+
client,
|
|
51
|
+
workspaceId,
|
|
52
|
+
onWorkspaceControlEvent,
|
|
53
|
+
children,
|
|
54
|
+
}: OpenGeniProviderProps) {
|
|
55
|
+
const [workspaceControlEvent, setWorkspaceControlEvent] = useState<WorkspaceControlEvent | null>(
|
|
56
|
+
null,
|
|
57
|
+
);
|
|
58
|
+
const [workspaceControlConnectionState, setWorkspaceControlConnectionState] = useState<
|
|
59
|
+
StreamConnectionState | "idle" | "error"
|
|
60
|
+
>("idle");
|
|
61
|
+
const [contractMismatch, setContractMismatch] = useState<OpenGeniApiContractMismatchError | null>(
|
|
62
|
+
null,
|
|
63
|
+
);
|
|
64
|
+
const callbackRef = useRef(onWorkspaceControlEvent);
|
|
65
|
+
const reconcilersRef = useRef(new Map<string, Map<string, () => Promise<void>>>());
|
|
66
|
+
callbackRef.current = onWorkspaceControlEvent;
|
|
67
|
+
|
|
68
|
+
const verifyApiContract = useCallback(async (): Promise<void> => {
|
|
69
|
+
try {
|
|
70
|
+
const config = await client.getClientConfig();
|
|
71
|
+
if (config.apiContractRevision !== OPENGENI_API_CONTRACT_REVISION) {
|
|
72
|
+
throw new OpenGeniApiContractMismatchError(
|
|
73
|
+
OPENGENI_API_CONTRACT_REVISION,
|
|
74
|
+
String(config.apiContractRevision || "(missing)"),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof OpenGeniApiContractMismatchError) {
|
|
79
|
+
setContractMismatch(error);
|
|
80
|
+
reloadForContractMismatchOnce(error);
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}, [client]);
|
|
85
|
+
|
|
86
|
+
const registerSessionReconciler = useMemo(
|
|
87
|
+
() =>
|
|
88
|
+
(sessionId: string, key: string, reconcile: () => Promise<void>): (() => void) => {
|
|
89
|
+
const sessionReconcilers = reconcilersRef.current.get(sessionId) ?? new Map();
|
|
90
|
+
sessionReconcilers.set(key, reconcile);
|
|
91
|
+
reconcilersRef.current.set(sessionId, sessionReconcilers);
|
|
92
|
+
return () => {
|
|
93
|
+
const current = reconcilersRef.current.get(sessionId);
|
|
94
|
+
current?.delete(key);
|
|
95
|
+
if (current?.size === 0) reconcilersRef.current.delete(sessionId);
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
[],
|
|
99
|
+
);
|
|
100
|
+
const reconcileSession = useMemo(
|
|
101
|
+
() =>
|
|
102
|
+
async (sessionId: string): Promise<void> => {
|
|
103
|
+
// This read also crosses the exact API-contract handshake before stale
|
|
104
|
+
// state can be presented as live after a deployment.
|
|
105
|
+
await verifyApiContract();
|
|
106
|
+
const callbacks = [...(reconcilersRef.current.get(sessionId)?.values() ?? [])];
|
|
107
|
+
await Promise.all(callbacks.map((reconcile) => reconcile()));
|
|
108
|
+
},
|
|
109
|
+
[verifyApiContract],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
setWorkspaceControlEvent(null);
|
|
115
|
+
setWorkspaceControlConnectionState("connecting");
|
|
116
|
+
void (async () => {
|
|
117
|
+
try {
|
|
118
|
+
await verifyApiContract();
|
|
119
|
+
const workspace = await client.getWorkspace(workspaceId);
|
|
120
|
+
const stream = client.streamWorkspaceControlEvents(workspaceId, {
|
|
121
|
+
after: workspace.inferenceControl.revision,
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
onStateChange: setWorkspaceControlConnectionState,
|
|
124
|
+
});
|
|
125
|
+
for await (const event of stream) {
|
|
126
|
+
if (controller.signal.aborted) return;
|
|
127
|
+
setWorkspaceControlEvent((current) =>
|
|
128
|
+
!current || event.sequence > current.sequence ? event : current,
|
|
129
|
+
);
|
|
130
|
+
callbackRef.current?.(event);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof OpenGeniApiContractMismatchError) {
|
|
134
|
+
setContractMismatch(error);
|
|
135
|
+
reloadForContractMismatchOnce(error);
|
|
136
|
+
}
|
|
137
|
+
if (!controller.signal.aborted) setWorkspaceControlConnectionState("error");
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
return () => controller.abort();
|
|
141
|
+
}, [client, verifyApiContract, workspaceId]);
|
|
142
|
+
|
|
143
|
+
const value = useMemo(
|
|
144
|
+
() => ({
|
|
145
|
+
client,
|
|
146
|
+
workspaceId,
|
|
147
|
+
workspaceControlEvent,
|
|
148
|
+
workspaceControlConnectionState,
|
|
149
|
+
registerSessionReconciler,
|
|
150
|
+
reconcileSession,
|
|
151
|
+
}),
|
|
152
|
+
[
|
|
153
|
+
client,
|
|
154
|
+
workspaceId,
|
|
155
|
+
workspaceControlEvent,
|
|
156
|
+
workspaceControlConnectionState,
|
|
157
|
+
registerSessionReconciler,
|
|
158
|
+
reconcileSession,
|
|
159
|
+
],
|
|
160
|
+
);
|
|
161
|
+
return (
|
|
162
|
+
<OpenGeniContext.Provider value={value}>
|
|
163
|
+
{children}
|
|
164
|
+
{contractMismatch ? <ApiContractMismatchScreen mismatch={contractMismatch} /> : null}
|
|
165
|
+
</OpenGeniContext.Provider>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ApiContractMismatchScreen({ mismatch }: { mismatch: OpenGeniApiContractMismatchError }) {
|
|
170
|
+
return (
|
|
171
|
+
<div
|
|
172
|
+
className="og-root fixed inset-0 z-[2147483647] grid place-items-center bg-og-bg/95 p-6 backdrop-blur-sm"
|
|
173
|
+
role="alert"
|
|
174
|
+
aria-live="assertive"
|
|
175
|
+
data-opengeni-api-contract-mismatch
|
|
176
|
+
>
|
|
177
|
+
<div className="w-full max-w-md rounded-xl border border-og-border bg-og-surface p-6 shadow-2xl">
|
|
178
|
+
<p className="text-sm font-semibold text-og-fg">OpenGeni updated</p>
|
|
179
|
+
<p className="mt-2 text-sm leading-6 text-og-muted">
|
|
180
|
+
This tab cannot safely continue with the new server version. Reload it before sending or
|
|
181
|
+
controlling work.
|
|
182
|
+
</p>
|
|
183
|
+
<p className="mt-3 font-mono text-xs text-og-subtle">
|
|
184
|
+
Client {mismatch.expected} · API {mismatch.actual}
|
|
185
|
+
</p>
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
className="mt-5 inline-flex h-9 items-center rounded-md bg-og-fg px-3 text-sm font-medium text-og-bg"
|
|
189
|
+
onClick={() => window.location.reload()}
|
|
190
|
+
>
|
|
191
|
+
Reload now
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function reloadForContractMismatchOnce(mismatch: OpenGeniApiContractMismatchError): void {
|
|
199
|
+
if (typeof window === "undefined" || typeof sessionStorage === "undefined") return;
|
|
200
|
+
const key = `${CONTRACT_RELOAD_STORAGE_PREFIX}${mismatch.actual}`;
|
|
201
|
+
if (sessionStorage.getItem(key) === OPENGENI_API_CONTRACT_REVISION) return;
|
|
202
|
+
sessionStorage.setItem(key, OPENGENI_API_CONTRACT_REVISION);
|
|
203
|
+
window.setTimeout(() => window.location.reload(), 150);
|
|
24
204
|
}
|
|
25
205
|
|
|
26
206
|
export type ClientOverride = {
|
|
@@ -38,14 +218,23 @@ export function useOpenGeni(override: ClientOverride = {}): OpenGeniContextValue
|
|
|
38
218
|
"@opengeni/react: no OpenGeni client/workspace available. Wrap the tree in <OpenGeniProvider> or pass { client, workspaceId } to the hook.",
|
|
39
219
|
);
|
|
40
220
|
}
|
|
41
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
client,
|
|
223
|
+
workspaceId,
|
|
224
|
+
workspaceControlEvent: context?.workspaceControlEvent ?? null,
|
|
225
|
+
workspaceControlConnectionState: context?.workspaceControlConnectionState ?? "idle",
|
|
226
|
+
registerSessionReconciler: context?.registerSessionReconciler ?? NOOP_REGISTER_RECONCILER,
|
|
227
|
+
reconcileSession: context?.reconcileSession ?? NOOP_RECONCILE_SESSION,
|
|
228
|
+
};
|
|
42
229
|
}
|
|
43
230
|
|
|
44
231
|
/**
|
|
45
232
|
* Resolve the client only — for hooks that are not workspace-scoped
|
|
46
233
|
* (`useWorkspaces`, `useBillingUsage`).
|
|
47
234
|
*/
|
|
48
|
-
export function useOpenGeniClient(
|
|
235
|
+
export function useOpenGeniClient(
|
|
236
|
+
override: Pick<ClientOverride, "client"> = {},
|
|
237
|
+
): SessionClientLike {
|
|
49
238
|
const context = useContext(OpenGeniContext);
|
|
50
239
|
const client = override.client ?? context?.client;
|
|
51
240
|
if (!client) {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ArrowRightIcon,
|
|
3
|
+
BotIcon,
|
|
4
|
+
BrainCircuitIcon,
|
|
5
|
+
BrainIcon,
|
|
6
|
+
SquareTerminalIcon,
|
|
7
|
+
} from "lucide-react";
|
|
2
8
|
import { cn } from "../lib/cn";
|
|
3
9
|
import { truncate } from "../lib/format";
|
|
4
10
|
import { defaultToolRegistry } from "./tool-renderers";
|
|
5
11
|
import { useEntranceAnimation } from "./entrance";
|
|
6
12
|
import type { ToolRegistry } from "./registry";
|
|
7
|
-
import { PayloadBlock, ActivityDisclosure } from "./shared";
|
|
13
|
+
import { BodyNote, PayloadBlock, ActivityDisclosure } from "./shared";
|
|
8
14
|
import { toolDisplayName } from "./projection";
|
|
9
|
-
import type { ActivityItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
|
|
15
|
+
import type { ActivityItem, MemoryItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
|
|
10
16
|
|
|
11
17
|
/* ----------------------------------------------------------------------------
|
|
12
18
|
Activity rail
|
|
@@ -25,6 +31,12 @@ export type ActivityRailProps = {
|
|
|
25
31
|
toolRegistry?: ToolRegistry | undefined;
|
|
26
32
|
/** Drill into a spawned worker session. */
|
|
27
33
|
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Deep-link a memory row to its record in the host's memory pane. Opt-in: the
|
|
36
|
+
* library draws no "View in memory" affordance without a handler (the memory
|
|
37
|
+
* row is then non-interactive rich content). See {@link MessageTimelineProps}.
|
|
38
|
+
*/
|
|
39
|
+
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
28
40
|
/** Drop the left rule + indent (used inside a folded turn summary). */
|
|
29
41
|
bare?: boolean | undefined;
|
|
30
42
|
className?: string | undefined;
|
|
@@ -42,7 +54,14 @@ function familyOf(item: ActivityItem): string {
|
|
|
42
54
|
return item.kind;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
export function ActivityRail({
|
|
57
|
+
export function ActivityRail({
|
|
58
|
+
items,
|
|
59
|
+
toolRegistry = defaultToolRegistry,
|
|
60
|
+
onOpenSession,
|
|
61
|
+
onMemoryClick,
|
|
62
|
+
bare,
|
|
63
|
+
className,
|
|
64
|
+
}: ActivityRailProps) {
|
|
46
65
|
const enter = useEntranceAnimation();
|
|
47
66
|
return (
|
|
48
67
|
<div
|
|
@@ -58,7 +77,7 @@ export function ActivityRail({ items, toolRegistry = defaultToolRegistry, onOpen
|
|
|
58
77
|
>
|
|
59
78
|
{items.map((item, index) => {
|
|
60
79
|
const newFamily = index > 0 && familyOf(item) !== familyOf(items[index - 1]!);
|
|
61
|
-
const row = renderActivity(item, toolRegistry, onOpenSession);
|
|
80
|
+
const row = renderActivity(item, toolRegistry, onOpenSession, onMemoryClick);
|
|
62
81
|
return (
|
|
63
82
|
<div key={item.id} className={cn(newFamily && "mt-3")}>
|
|
64
83
|
{row}
|
|
@@ -78,6 +97,7 @@ function renderActivity(
|
|
|
78
97
|
item: ActivityItem,
|
|
79
98
|
toolRegistry: ToolRegistry,
|
|
80
99
|
onOpenSession: ((sessionId: string) => void) | undefined,
|
|
100
|
+
onMemoryClick: ((memoryId: string) => void) | undefined,
|
|
81
101
|
) {
|
|
82
102
|
switch (item.kind) {
|
|
83
103
|
case "reasoning":
|
|
@@ -90,6 +110,8 @@ function renderActivity(
|
|
|
90
110
|
return <WorkerRow item={item} onOpenSession={onOpenSession} />;
|
|
91
111
|
case "sandbox":
|
|
92
112
|
return <SandboxRow item={item} />;
|
|
113
|
+
case "memory":
|
|
114
|
+
return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
|
|
93
115
|
default:
|
|
94
116
|
return assertNever(item);
|
|
95
117
|
}
|
|
@@ -117,12 +139,131 @@ function ReasoningRow({ item }: { item: ReasoningItem }) {
|
|
|
117
139
|
);
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Human labels for the memory kinds, translated at the SDK boundary so a raw
|
|
144
|
+
* enum slug never renders as UI. Kept local to the library (the app has its own
|
|
145
|
+
* `KIND_LABEL`); an unknown kind simply omits the chip rather than showing a slug.
|
|
146
|
+
*/
|
|
147
|
+
const MEMORY_KIND_LABEL: Record<string, string> = {
|
|
148
|
+
preference: "Preference",
|
|
149
|
+
semantic: "Fact",
|
|
150
|
+
procedural: "Procedure",
|
|
151
|
+
decision: "Decision",
|
|
152
|
+
episodic: "History",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A memory write the agent made mid-turn. A calm, NEUTRAL step (a successful save
|
|
157
|
+
* is ordinary progress, never an exceptional state, so no accent/color): a brain-
|
|
158
|
+
* circuit glyph, "Saved to memory" / "Updated memory", a human kind chip, and the
|
|
159
|
+
* memory text. Expanding reveals the full text; a supersede shows the old text
|
|
160
|
+
* struck through above the new one. When the host opts in with `onMemoryClick`,
|
|
161
|
+
* a quiet "View in memory" affordance deep-links to the LIVE record.
|
|
162
|
+
*/
|
|
163
|
+
function MemoryRow({
|
|
164
|
+
item,
|
|
165
|
+
onMemoryClick,
|
|
166
|
+
}: {
|
|
167
|
+
item: MemoryItem;
|
|
168
|
+
onMemoryClick?: ((memoryId: string) => void) | undefined;
|
|
169
|
+
}) {
|
|
170
|
+
const corrected = item.variant === "corrected";
|
|
171
|
+
const kindLabel = MEMORY_KIND_LABEL[item.memoryKind];
|
|
172
|
+
// A supersede carries both the old text (`preview`) and the new (`replacementPreview`);
|
|
173
|
+
// an in-place update / archive carries only `preview`.
|
|
174
|
+
const superseded = corrected && Boolean(item.replacementPreview);
|
|
175
|
+
// Link to the LIVE record: a supersede's replacement when present, else the memory itself.
|
|
176
|
+
const targetId = corrected ? (item.replacementMemoryId ?? item.memoryId) : item.memoryId;
|
|
177
|
+
const deepLink = Boolean(onMemoryClick);
|
|
178
|
+
return (
|
|
179
|
+
<ActivityDisclosure
|
|
180
|
+
icon={<BrainCircuitIcon className="size-3.5" />}
|
|
181
|
+
iconTone="muted"
|
|
182
|
+
title={
|
|
183
|
+
<span className="inline-flex min-w-0 items-center gap-2">
|
|
184
|
+
<span className="shrink-0">{corrected ? "Updated memory" : "Saved to memory"}</span>
|
|
185
|
+
{kindLabel ? (
|
|
186
|
+
<span className="shrink-0 rounded-og-xs bg-og-surface-2 px-1.5 py-px text-og-xs font-normal leading-tight text-og-fg-subtle">
|
|
187
|
+
{kindLabel}
|
|
188
|
+
</span>
|
|
189
|
+
) : null}
|
|
190
|
+
</span>
|
|
191
|
+
}
|
|
192
|
+
preview={superseded ? item.replacementPreview : item.preview}
|
|
193
|
+
>
|
|
194
|
+
{superseded ? (
|
|
195
|
+
// The correction as a before → after: the old memory struck through and
|
|
196
|
+
// dimmed, the new text in the ordinary body weight below it.
|
|
197
|
+
<div className="flex flex-col gap-1.5">
|
|
198
|
+
<p className="whitespace-pre-wrap text-og-sm leading-6 text-og-fg-subtle line-through">
|
|
199
|
+
{item.preview}
|
|
200
|
+
</p>
|
|
201
|
+
<p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">
|
|
202
|
+
{item.replacementPreview}
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
) : corrected && item.action === "updated" ? (
|
|
206
|
+
// Edited in place, no replacement record: the memory is still live, so
|
|
207
|
+
// show its current text — NOT the archived treatment.
|
|
208
|
+
<>
|
|
209
|
+
<p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">
|
|
210
|
+
{item.preview}
|
|
211
|
+
</p>
|
|
212
|
+
<BodyNote tone="muted">Updated in place.</BodyNote>
|
|
213
|
+
</>
|
|
214
|
+
) : corrected ? (
|
|
215
|
+
// A correction with no replacement (and not an in-place update) archived the record.
|
|
216
|
+
<BodyNote tone="muted">Archived.</BodyNote>
|
|
217
|
+
) : (
|
|
218
|
+
<p className="whitespace-pre-wrap text-og-base leading-6 text-og-fg-muted">
|
|
219
|
+
{item.preview}
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
222
|
+
{item.deduped ? <BodyNote tone="muted">Merged into an existing memory.</BodyNote> : null}
|
|
223
|
+
{deepLink ? (
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={() => onMemoryClick?.(targetId)}
|
|
227
|
+
className={cn(
|
|
228
|
+
"group/memlink -mx-1 inline-flex w-fit items-center gap-1 rounded-og-sm px-1 py-0.5 text-left text-og-sm text-og-fg-subtle",
|
|
229
|
+
"outline-none transition-colors duration-150 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
View in memory
|
|
233
|
+
<ArrowRightIcon className="size-3.5 transition-transform duration-150 group-hover/memlink:translate-x-0.5" />
|
|
234
|
+
</button>
|
|
235
|
+
) : null}
|
|
236
|
+
</ActivityDisclosure>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A human title for a sandbox operation row. Named platform operations (the lazy
|
|
242
|
+
* `sandbox.provision` first-establish that now runs mid-turn) read as calm,
|
|
243
|
+
* status-aware English instead of a raw op id — the box coming up should look
|
|
244
|
+
* like "Starting sandbox", never an unexplained long-running command. Unnamed /
|
|
245
|
+
* unknown ops fall back to the generic id-to-words {@link toolDisplayName}.
|
|
246
|
+
*/
|
|
247
|
+
function sandboxRowTitle(item: SandboxItem): string {
|
|
248
|
+
if (item.name === "sandbox.provision") {
|
|
249
|
+
if (item.status === "failed") return "Sandbox didn’t start";
|
|
250
|
+
if (item.status === "running") return "Starting sandbox";
|
|
251
|
+
if (item.origin === "resumed") return "Sandbox reattached";
|
|
252
|
+
if (item.origin === "restored") return "Sandbox restored";
|
|
253
|
+
if (item.origin === "created") return "Sandbox created";
|
|
254
|
+
return "Sandbox ready";
|
|
255
|
+
}
|
|
256
|
+
return toolDisplayName(item.name);
|
|
257
|
+
}
|
|
258
|
+
|
|
120
259
|
function SandboxRow({ item }: { item: SandboxItem }) {
|
|
121
260
|
return (
|
|
122
261
|
<ActivityDisclosure
|
|
123
262
|
icon={<SquareTerminalIcon className="size-3.5" />}
|
|
124
|
-
iconTone={
|
|
125
|
-
|
|
263
|
+
iconTone={
|
|
264
|
+
item.status === "failed" ? "failed" : item.status === "running" ? "running" : "muted"
|
|
265
|
+
}
|
|
266
|
+
title={sandboxRowTitle(item)}
|
|
126
267
|
running={item.status === "running"}
|
|
127
268
|
failed={item.status === "failed"}
|
|
128
269
|
cancelled={item.status === "cancelled"}
|
|
@@ -135,7 +276,13 @@ function SandboxRow({ item }: { item: SandboxItem }) {
|
|
|
135
276
|
}
|
|
136
277
|
|
|
137
278
|
/** Spawned/messaged worker sessions get a first-class card, not a tool row. */
|
|
138
|
-
function WorkerRow({
|
|
279
|
+
function WorkerRow({
|
|
280
|
+
item,
|
|
281
|
+
onOpenSession,
|
|
282
|
+
}: {
|
|
283
|
+
item: WorkerItem;
|
|
284
|
+
onOpenSession?: ((sessionId: string) => void) | undefined;
|
|
285
|
+
}) {
|
|
139
286
|
const running = item.status === "running";
|
|
140
287
|
const failed = item.status === "failed";
|
|
141
288
|
const cancelled = item.status === "cancelled";
|
|
@@ -148,47 +295,49 @@ function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?:
|
|
|
148
295
|
: cancelled
|
|
149
296
|
? "Worker interrupted"
|
|
150
297
|
: "Worker spawned"
|
|
151
|
-
:
|
|
152
|
-
?
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
>
|
|
177
|
-
<BotIcon className="size-4" />
|
|
298
|
+
: running
|
|
299
|
+
? "Messaging worker"
|
|
300
|
+
: failed
|
|
301
|
+
? "Worker message failed"
|
|
302
|
+
: cancelled
|
|
303
|
+
? "Worker interrupted"
|
|
304
|
+
: "Worker messaged";
|
|
305
|
+
// A worker is a first-class actor but still a STEP on the rail — a borderless
|
|
306
|
+
// row (no card), aligned to its sibling tool rows: the chevron column is an
|
|
307
|
+
// empty spacer (a worker doesn't expand), then the bot glyph, then the title
|
|
308
|
+
// with a quiet prompt beneath, and one right-gutter affordance.
|
|
309
|
+
//
|
|
310
|
+
// Once the worker's session id is known, the WHOLE row is a clean, clickable
|
|
311
|
+
// deep-link into that child session (a trailing arrow brightens on hover) —
|
|
312
|
+
// the spawn moment itself is the affordance, not a small side button. A
|
|
313
|
+
// still-in-flight spawn (no id yet) or a failed/cancelled worker is inert, so
|
|
314
|
+
// there is never a dead click target. The gutter mirrors the worker-completion
|
|
315
|
+
// card's calm language: red chip on failure, "interrupted" on cancel.
|
|
316
|
+
const sessionId = item.workerSessionId;
|
|
317
|
+
const deepLink = Boolean(sessionId) && Boolean(onOpenSession) && !failed && !cancelled;
|
|
318
|
+
const inner = (
|
|
319
|
+
<>
|
|
320
|
+
<span className="size-3.5 shrink-0" aria-hidden />
|
|
321
|
+
<span className={cn("mt-px shrink-0", failed ? "text-og-status-failed" : "text-og-accent")}>
|
|
322
|
+
<BotIcon className="size-3.5" />
|
|
178
323
|
</span>
|
|
179
324
|
<div className="min-w-0 flex-1">
|
|
180
325
|
{/* In-flight state is carried ONLY by the shimmering title (no detached
|
|
181
326
|
pulse badge), matching every other running row in the rail. */}
|
|
182
|
-
<span
|
|
327
|
+
<span
|
|
328
|
+
className={cn(
|
|
329
|
+
"text-og-base font-medium",
|
|
330
|
+
running ? "og-shimmer-text" : failed ? "text-og-status-failed" : "text-og-fg",
|
|
331
|
+
)}
|
|
332
|
+
>
|
|
183
333
|
{title}
|
|
184
334
|
</span>
|
|
185
|
-
{item.prompt ?
|
|
186
|
-
|
|
187
|
-
|
|
335
|
+
{item.prompt ? (
|
|
336
|
+
<p className="mt-0.5 truncate text-og-sm text-og-fg-muted">
|
|
337
|
+
{truncate(item.prompt, 140)}
|
|
338
|
+
</p>
|
|
188
339
|
) : null}
|
|
189
340
|
</div>
|
|
190
|
-
{/* Right gutter: failed gets a red chip; cancelled gets a calm "interrupted"
|
|
191
|
-
chip (no dot, no red); a live complete worker shows an "Open session" button. */}
|
|
192
341
|
{failed ? (
|
|
193
342
|
<span className="inline-flex shrink-0 self-center items-center gap-1.5 font-og-mono text-og-xs leading-none text-og-status-failed">
|
|
194
343
|
<span className="size-1.5 rounded-full bg-og-status-failed" />
|
|
@@ -198,19 +347,29 @@ function WorkerRow({ item, onOpenSession }: { item: WorkerItem; onOpenSession?:
|
|
|
198
347
|
<span className="og-cancelled-chip shrink-0 self-center font-og-mono text-og-xs leading-none text-og-fg-subtle">
|
|
199
348
|
interrupted
|
|
200
349
|
</span>
|
|
201
|
-
) :
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"shrink-0 self-center rounded-og-sm border border-og-border px-2.5 py-1 text-og-sm font-medium text-og-fg-muted pointer-coarse:py-2",
|
|
207
|
-
"outline-none transition-colors duration-150 hover:border-og-border-strong hover:text-og-fg",
|
|
208
|
-
"focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
209
|
-
)}
|
|
210
|
-
>
|
|
211
|
-
Open session
|
|
212
|
-
</button>
|
|
350
|
+
) : deepLink ? (
|
|
351
|
+
<ArrowRightIcon
|
|
352
|
+
aria-hidden
|
|
353
|
+
className="mt-0.5 size-3.5 shrink-0 text-og-fg-subtle transition-[transform,color] duration-150 group-hover/worker:translate-x-0.5 group-hover/worker:text-og-fg"
|
|
354
|
+
/>
|
|
213
355
|
) : null}
|
|
214
|
-
|
|
356
|
+
</>
|
|
215
357
|
);
|
|
358
|
+
|
|
359
|
+
if (deepLink && sessionId && onOpenSession) {
|
|
360
|
+
return (
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
onClick={() => onOpenSession(sessionId)}
|
|
364
|
+
className={cn(
|
|
365
|
+
"group/worker -mx-1.5 flex w-full items-start gap-2 rounded-og-sm px-1.5 py-1.5 text-left",
|
|
366
|
+
"outline-none transition-colors duration-150 hover:bg-og-surface-1 focus-visible:ring-2 focus-visible:ring-og-accent",
|
|
367
|
+
"pointer-coarse:py-2.5",
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
{inner}
|
|
371
|
+
</button>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
return <div className="flex items-start gap-2 px-1.5 py-1.5">{inner}</div>;
|
|
216
375
|
}
|
|
@@ -21,8 +21,18 @@ const DisclosureDefaultsContext = createContext<boolean | undefined>(undefined);
|
|
|
21
21
|
* Seed the initial open state of every timeline collapsible below this node.
|
|
22
22
|
* Intended for screenshot/test instrumentation only; absent by default.
|
|
23
23
|
*/
|
|
24
|
-
export function DisclosureDefaultsProvider({
|
|
25
|
-
|
|
24
|
+
export function DisclosureDefaultsProvider({
|
|
25
|
+
defaultOpen,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
defaultOpen: boolean;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<DisclosureDefaultsContext.Provider value={defaultOpen}>
|
|
33
|
+
{children}
|
|
34
|
+
</DisclosureDefaultsContext.Provider>
|
|
35
|
+
);
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
/**
|
package/src/timeline/index.ts
CHANGED
|
@@ -16,13 +16,22 @@
|
|
|
16
16
|
-------------------------------------------------------------------------- */
|
|
17
17
|
|
|
18
18
|
// projection
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
buildTimeline,
|
|
21
|
+
creditExhaustedFromEvents,
|
|
22
|
+
extractSessionRef,
|
|
23
|
+
groupTimeline,
|
|
24
|
+
sessionStatusFromEvents,
|
|
25
|
+
toolDisplayName,
|
|
26
|
+
} from "./projection";
|
|
20
27
|
|
|
21
28
|
// item types
|
|
22
29
|
export type {
|
|
23
30
|
ActivityItem,
|
|
24
31
|
AgentMessageItem,
|
|
32
|
+
AuthNeededItem,
|
|
25
33
|
GoalItem,
|
|
34
|
+
MemoryItem,
|
|
26
35
|
NoticeItem,
|
|
27
36
|
ReasoningItem,
|
|
28
37
|
SandboxItem,
|
|
@@ -32,6 +41,7 @@ export type {
|
|
|
32
41
|
TurnEndItem,
|
|
33
42
|
ToolCallItem,
|
|
34
43
|
UserMessageItem,
|
|
44
|
+
WorkerCompletionItem,
|
|
35
45
|
WorkerItem,
|
|
36
46
|
} from "./types";
|
|
37
47
|
|
|
@@ -53,7 +63,16 @@ export { ActivityRail } from "./activity-rail";
|
|
|
53
63
|
export type { ActivityRailProps } from "./activity-rail";
|
|
54
64
|
|
|
55
65
|
// shared primitives (extension authors compose these)
|
|
56
|
-
export {
|
|
66
|
+
export {
|
|
67
|
+
ActivityDisclosure,
|
|
68
|
+
BodyNote,
|
|
69
|
+
MediaEmpty,
|
|
70
|
+
MediaSkeleton,
|
|
71
|
+
PayloadBlock,
|
|
72
|
+
ScreenshotFigure,
|
|
73
|
+
TermBlock,
|
|
74
|
+
Thumbnail,
|
|
75
|
+
} from "./shared";
|
|
57
76
|
export type { ActivityDisclosureProps, DisclosureChip } from "./shared";
|
|
58
77
|
|
|
59
78
|
// screenshot lightbox
|