@opengeni/react 0.29.0 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approvals.d.ts +14 -0
- package/dist/{chunk-M7X4JZOD.js → chunk-4QOQ2DSR.js} +8 -1
- package/dist/chunk-4QOQ2DSR.js.map +1 -0
- package/dist/{chunk-HQFPO7VX.js → chunk-7CJOAW3V.js} +24 -3
- package/dist/{chunk-HQFPO7VX.js.map → chunk-7CJOAW3V.js.map} +1 -1
- package/dist/{chunk-CSJBF3QJ.js → chunk-A2WITZTI.js} +377 -113
- package/dist/chunk-A2WITZTI.js.map +1 -0
- package/dist/{chunk-BC6RKIHS.js → chunk-GZ4KR67U.js} +11 -11
- package/dist/{chunk-AVLMIWYO.js → chunk-KZRUYCL4.js} +12 -6
- package/dist/chunk-KZRUYCL4.js.map +1 -0
- package/dist/{chunk-OKXCECLG.js → chunk-TCCE5ZU5.js} +171 -22
- package/dist/chunk-TCCE5ZU5.js.map +1 -0
- package/dist/chunk-U255M5EZ.js +178 -0
- package/dist/chunk-U255M5EZ.js.map +1 -0
- package/dist/{chunk-IPVOVOW6.js → chunk-WY2QN2CX.js} +2539 -2174
- package/dist/chunk-WY2QN2CX.js.map +1 -0
- package/dist/chunk-ZNNE2VES.js +6413 -0
- package/dist/chunk-ZNNE2VES.js.map +1 -0
- package/dist/client.d.ts +33 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/registry.d.ts +41 -0
- package/dist/commands/types.d.ts +85 -0
- package/dist/components/approval-surface.d.ts +28 -0
- package/dist/components/chat-composer.d.ts +51 -0
- package/dist/components/code-editor.d.ts +43 -0
- package/dist/components/command-palette.d.ts +25 -0
- package/dist/components/composer-transcription-control.d.ts +36 -0
- package/dist/components/composer.d.ts +277 -0
- package/dist/components/copy-button.d.ts +26 -0
- package/dist/components/desktop-viewer.d.ts +85 -0
- package/dist/components/diff-view.d.ts +38 -0
- package/dist/components/enrollment-consent.d.ts +33 -0
- package/dist/components/enrollment-device-flow.d.ts +27 -0
- package/dist/components/file-browser.d.ts +47 -0
- package/dist/components/fleet-tile.d.ts +18 -0
- package/dist/components/human-input-form.d.ts +47 -0
- package/dist/components/machine-card.d.ts +21 -0
- package/dist/components/machine-dock-bar.d.ts +31 -0
- package/dist/components/machine-health-pill.d.ts +9 -0
- package/dist/components/machine-metrics.d.ts +15 -0
- package/dist/components/machine-status-pill.d.ts +54 -0
- package/dist/components/machines/health.d.ts +25 -0
- package/dist/components/machines/machine-detail.d.ts +14 -0
- package/dist/components/machines/metric-history-chart.d.ts +24 -0
- package/dist/components/machines/metric-sparkline.d.ts +9 -0
- package/dist/components/machines/series.d.ts +24 -0
- package/dist/components/machines-dashboard.d.ts +30 -0
- package/dist/components/markdown.d.ts +31 -0
- package/dist/components/message-timeline.d.ts +98 -0
- package/dist/components/model-picker.d.ts +19 -0
- package/dist/components/pierre-diff.d.ts +34 -0
- package/dist/components/pierre-file.d.ts +27 -0
- package/dist/components/queue-draft-policy.d.ts +7 -0
- package/dist/components/queue-surface-implementation.d.ts +20 -0
- package/dist/components/queue-surface-state.d.ts +12 -0
- package/dist/components/queue-surface.d.ts +12 -0
- package/dist/components/sandbox-files.d.ts +49 -0
- package/dist/components/sandbox-terminal.d.ts +123 -0
- package/dist/components/sandbox-workspace.d.ts +128 -0
- package/dist/components/session-chrome.d.ts +70 -0
- package/dist/components/session-status.d.ts +25 -0
- package/dist/components/soften-streaming-markdown.d.ts +13 -0
- package/dist/components/stream-reveal.d.ts +37 -0
- package/dist/components/tip-follow.d.ts +139 -0
- package/dist/components/tooltip.d.ts +16 -0
- package/dist/components/workbench-changes.d.ts +34 -0
- package/dist/components/workspace-dock.d.ts +35 -0
- package/dist/composer.d.ts +11 -7
- package/dist/composer.js +9 -6
- package/dist/hooks/internal.d.ts +46 -0
- package/dist/hooks/use-available-models.d.ts +37 -0
- package/dist/hooks/use-billing-usage.d.ts +26 -0
- package/dist/hooks/use-codex-accounts.d.ts +69 -0
- package/dist/{use-composer-ClHy1rkL.d.ts → hooks/use-composer.d.ts} +16 -24
- package/dist/hooks/use-desktop-stream.d.ts +43 -0
- package/dist/hooks/use-environments.d.ts +35 -0
- package/dist/{use-file-attachments-BmT8WtqO.d.ts → hooks/use-file-attachments.d.ts} +6 -9
- package/dist/hooks/use-goal.d.ts +46 -0
- package/dist/hooks/use-human-input.d.ts +26 -0
- package/dist/hooks/use-last-started-turn-policy.d.ts +27 -0
- package/dist/hooks/use-machine-chip.d.ts +54 -0
- package/dist/hooks/use-machines.d.ts +81 -0
- package/dist/hooks/use-packs.d.ts +27 -0
- package/dist/hooks/use-relay-frame-stream.d.ts +53 -0
- package/dist/hooks/use-rigs.d.ts +87 -0
- package/dist/hooks/use-sandbox-files.d.ts +108 -0
- package/dist/hooks/use-sandbox-git.d.ts +58 -0
- package/dist/hooks/use-sandbox-terminal.d.ts +62 -0
- package/dist/hooks/use-scheduled-tasks.d.ts +15 -0
- package/dist/hooks/use-session-capabilities.d.ts +83 -0
- package/dist/hooks/use-session-control.d.ts +22 -0
- package/dist/hooks/use-session-events.d.ts +92 -0
- package/dist/hooks/use-session-lineage.d.ts +17 -0
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +23 -0
- package/dist/hooks/use-session.d.ts +23 -0
- package/dist/hooks/use-slash-commands.d.ts +66 -0
- package/dist/hooks/use-terminal-stream.d.ts +46 -0
- package/dist/hooks/use-transcription.d.ts +73 -0
- package/dist/hooks/use-turn-queue.d.ts +44 -0
- package/dist/hooks/use-voice-input.d.ts +25 -0
- package/dist/hooks/use-windowed-sections.d.ts +42 -0
- package/dist/hooks/use-workspace-capture.d.ts +49 -0
- package/dist/hooks/use-workspace-edit.d.ts +73 -0
- package/dist/hooks/use-workspace-sessions.d.ts +30 -0
- package/dist/hooks/use-workspaces.d.ts +22 -0
- package/dist/human-input.d.ts +18 -0
- package/dist/index.d.ts +144 -2119
- package/dist/index.js +421 -324
- package/dist/index.js.map +1 -1
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/format.d.ts +48 -0
- package/dist/lib/git-patch.d.ts +8 -0
- package/dist/lib/motion-inspect.d.ts +5 -0
- package/dist/lib/motion.d.ts +14 -0
- package/dist/lib/noto-sans-arabic-loader.d.ts +1 -0
- package/dist/lib/noto-sans-jp-loader.generated.d.ts +1 -0
- package/dist/lib/relay-wire.d.ts +30 -0
- package/dist/lib/use-portal-token-style.d.ts +9 -0
- package/dist/lib/use-theme-type.d.ts +5 -0
- package/dist/lib/use-unicode-fonts.d.ts +12 -0
- package/dist/lib/xterm-renderer.d.ts +19 -0
- package/dist/lib/xterm-theme.d.ts +75 -0
- package/dist/machines.d.ts +30 -413
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +32 -0
- package/dist/model-policy.js +33 -0
- package/dist/model-policy.js.map +1 -0
- package/dist/provider.d.ts +16 -0
- package/dist/{queue-surface-implementation-UQ535OKQ.js → queue-surface-implementation-THPAKGMD.js} +2 -2
- package/dist/{queue-surface-implementation-UQ535OKQ.js.map → queue-surface-implementation-THPAKGMD.js.map} +1 -1
- package/dist/session-context.d.ts +82 -0
- package/dist/session-ui.d.ts +10 -7
- package/dist/session-ui.js +9 -5
- package/dist/session.d.ts +28 -6
- package/dist/session.js +11 -11
- package/dist/timeline/activity-rail.d.ts +19 -0
- package/dist/timeline/disclosure-context.d.ts +14 -0
- package/dist/timeline/entrance.d.ts +14 -0
- package/dist/timeline/fleet-decision-projection.d.ts +9 -0
- package/dist/timeline/fleet-decision-row.d.ts +11 -0
- package/dist/timeline/fold-memory.d.ts +15 -0
- package/dist/timeline/index.d.ts +18 -0
- package/dist/timeline/parsers.d.ts +96 -0
- package/dist/timeline/projection.d.ts +38 -0
- package/dist/timeline/registry.d.ts +40 -0
- package/dist/timeline/screenshot-lightbox.d.ts +26 -0
- package/dist/timeline/seen-activity-ids.d.ts +2 -0
- package/dist/timeline/shared.d.ts +125 -0
- package/dist/timeline/tool-diff.d.ts +8 -0
- package/dist/timeline/tool-display-name.d.ts +10 -0
- package/dist/timeline/tool-renderers.d.ts +5 -0
- package/dist/timeline/turn-summary.d.ts +116 -0
- package/dist/{use-turn-queue-D_1a8W_l.d.ts → timeline/types.d.ts} +41 -69
- package/dist/types/machines.d.ts +11 -0
- package/package.json +8 -4
- package/src/client.ts +1 -0
- package/src/components/composer-transcription-control.tsx +278 -130
- package/src/components/composer.tsx +20 -15
- package/src/components/copy-button.tsx +127 -0
- package/src/components/markdown.tsx +231 -32
- package/src/components/message-timeline.tsx +1304 -378
- package/src/components/model-picker.tsx +83 -43
- package/src/components/queue-surface-implementation.tsx +1 -1
- package/src/components/session-chrome.tsx +942 -0
- package/src/components/soften-streaming-markdown.ts +205 -0
- package/src/components/stream-reveal.ts +195 -0
- package/src/components/tip-follow.ts +459 -0
- package/src/components/tooltip.tsx +103 -0
- package/src/composer.ts +6 -0
- package/src/hooks/internal.ts +30 -4
- package/src/hooks/use-available-models.ts +51 -4
- package/src/hooks/use-composer.ts +23 -3
- package/src/hooks/use-last-started-turn-policy.ts +95 -0
- package/src/hooks/use-session-events.ts +359 -5
- package/src/hooks/use-voice-input.ts +232 -0
- package/src/index.ts +47 -1
- package/src/lib/clipboard.ts +58 -0
- package/src/lib/format.ts +18 -0
- package/src/lib/motion-inspect.ts +5 -0
- package/src/lib/motion.ts +80 -0
- package/src/model-policy.ts +210 -0
- package/src/session-ui.ts +7 -0
- package/src/timeline/activity-rail.tsx +46 -7
- package/src/timeline/entrance.tsx +10 -0
- package/src/timeline/fold-memory.ts +52 -0
- package/src/timeline/index.ts +9 -1
- package/src/timeline/projection.ts +226 -41
- package/src/timeline/seen-activity-ids.ts +19 -0
- package/src/timeline/shared.tsx +1 -1
- package/src/timeline/tool-renderers.tsx +75 -6
- package/src/timeline/turn-summary.tsx +387 -97
- package/src/timeline/types.ts +21 -0
- package/styles/index.css +99 -7
- package/styles/tokens.css +44 -3
- package/dist/chunk-AVLMIWYO.js.map +0 -1
- package/dist/chunk-CSJBF3QJ.js.map +0 -1
- package/dist/chunk-IPVOVOW6.js.map +0 -1
- package/dist/chunk-M7X4JZOD.js.map +0 -1
- package/dist/chunk-OKXCECLG.js.map +0 -1
- package/dist/chunk-ZMNPFZWL.js +0 -3918
- package/dist/chunk-ZMNPFZWL.js.map +0 -1
- package/dist/composer-BMzimGTO.d.ts +0 -528
- package/dist/session-DaONt-dY.d.ts +0 -264
- package/dist/session-context-CAjlaeZb.d.ts +0 -84
- package/dist/session-ui-B5_roA95.d.ts +0 -259
- /package/dist/{chunk-BC6RKIHS.js.map → chunk-GZ4KR67U.js.map} +0 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionChrome — compact merged session signals above the composer.
|
|
3
|
+
*
|
|
4
|
+
* Built-in session chrome for production and embeds. Token-themed
|
|
5
|
+
* (`--og-session-chrome-*`); hosts override on `.og-session-chrome` or an ancestor.
|
|
6
|
+
*
|
|
7
|
+
* ## Host tokens
|
|
8
|
+
* Defaults live in `tokens.css`.
|
|
9
|
+
*
|
|
10
|
+
* | Token | Role |
|
|
11
|
+
* | --- | --- |
|
|
12
|
+
* | `--og-session-chrome-surface` / `-open` | Dock fill (collapsed / expanded) |
|
|
13
|
+
* | `--og-session-chrome-border` / `-open` | Dock edge |
|
|
14
|
+
* | `--og-session-chrome-highlight` | Sliding chip selection pill |
|
|
15
|
+
* | `--og-session-chrome-shadow` / `-open` | Elevation |
|
|
16
|
+
* | `--og-session-chrome-radius` | Dock corner radius |
|
|
17
|
+
* | `--og-session-chrome-chip-min-height` | Signal chip height |
|
|
18
|
+
* | `--og-session-chrome-chip-pad-x` / `--og-session-chrome-chip-gap` | Chip padding / rail gap |
|
|
19
|
+
* | `--og-session-chrome-rail-pad` | Outer rail inset |
|
|
20
|
+
* | `--og-session-chrome-panel-pad-x` / `-y` | Expanded panel padding |
|
|
21
|
+
* | `--og-session-chrome-duration` / `--og-session-chrome-ease` | Expand + pill motion |
|
|
22
|
+
* | `--og-session-chrome-crossfade-duration` | Segment content opacity crossfade |
|
|
23
|
+
* | `--og-session-chrome-row-hover` | Inbox / queue row hover wash |
|
|
24
|
+
*
|
|
25
|
+
* Inbox and queue stay separate segments. Queue hover actions wire to
|
|
26
|
+
* `UseTurnQueueResult` (`editTurn` / `steerTurn` / `moveTurn` / `removeTurn`).
|
|
27
|
+
* Inbox has no product dismiss API; pass `onDismissIncoming` when the host
|
|
28
|
+
* wants a visible action (dev harness may use a local dummy).
|
|
29
|
+
*
|
|
30
|
+
* Segment switches keep the panel shell mounted and crossfade content while
|
|
31
|
+
* animating measured height — no `mode="wait"` unmount flash.
|
|
32
|
+
*/
|
|
33
|
+
import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
|
|
34
|
+
import {
|
|
35
|
+
ArrowDownIcon,
|
|
36
|
+
ArrowUpIcon,
|
|
37
|
+
BotIcon,
|
|
38
|
+
InboxIcon,
|
|
39
|
+
ListOrderedIcon,
|
|
40
|
+
Loader2Icon,
|
|
41
|
+
PauseIcon,
|
|
42
|
+
PencilIcon,
|
|
43
|
+
PlayIcon,
|
|
44
|
+
Trash2Icon,
|
|
45
|
+
TriangleAlertIcon,
|
|
46
|
+
XIcon,
|
|
47
|
+
ZapIcon,
|
|
48
|
+
} from "lucide-react";
|
|
49
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
50
|
+
import {
|
|
51
|
+
useEffect,
|
|
52
|
+
useId,
|
|
53
|
+
useLayoutEffect,
|
|
54
|
+
useMemo,
|
|
55
|
+
useRef,
|
|
56
|
+
useState,
|
|
57
|
+
type ReactNode,
|
|
58
|
+
} from "react";
|
|
59
|
+
|
|
60
|
+
import type { ComposerState } from "../hooks/use-composer";
|
|
61
|
+
import type { UseGoalResult } from "../hooks/use-goal";
|
|
62
|
+
import type { UseTurnQueueResult } from "../hooks/use-turn-queue";
|
|
63
|
+
import { cn } from "../lib/cn";
|
|
64
|
+
import { requestQueueDraftEdit } from "./queue-draft-policy";
|
|
65
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
66
|
+
|
|
67
|
+
export type SessionChromeSignalId = "incoming" | "queue" | "goal" | "agents";
|
|
68
|
+
|
|
69
|
+
export type SessionChromeSignalTone = "neutral" | "accent" | "waiting" | "running";
|
|
70
|
+
|
|
71
|
+
export type SessionChromeAgentsSignal = {
|
|
72
|
+
count: number;
|
|
73
|
+
detail?: string | undefined;
|
|
74
|
+
tone?: SessionChromeSignalTone | undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type SessionChromeProps = {
|
|
78
|
+
queue: UseTurnQueueResult;
|
|
79
|
+
/** Needed for queue edit → composer checkout. Omit with `readOnly`. */
|
|
80
|
+
composer?: ComposerState | undefined;
|
|
81
|
+
goal?: UseGoalResult | null | undefined;
|
|
82
|
+
/** Expanded agents body (host supplies tree / list). */
|
|
83
|
+
agentsPanel?: ReactNode;
|
|
84
|
+
/** Chip summary; when `count > 0` the agents segment appears. */
|
|
85
|
+
agentsSignal?: SessionChromeAgentsSignal | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Optional inbox dismiss. Product pending-inputs have no remove API — hosts
|
|
88
|
+
* (and the gallery) may still pass a handler so the action is visible.
|
|
89
|
+
*/
|
|
90
|
+
onDismissIncoming?: ((inputId: string) => void) | undefined;
|
|
91
|
+
readOnly?: boolean | undefined;
|
|
92
|
+
className?: string | undefined;
|
|
93
|
+
/** Controlled active segment; omit for uncontrolled. */
|
|
94
|
+
active?: SessionChromeSignalId | null | undefined;
|
|
95
|
+
defaultActive?: SessionChromeSignalId | null | undefined;
|
|
96
|
+
onActiveChange?: ((next: SessionChromeSignalId | null) => void) | undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type GoalPillState =
|
|
100
|
+
| "pursuing"
|
|
101
|
+
| "scheduled"
|
|
102
|
+
| "blocked"
|
|
103
|
+
| "held"
|
|
104
|
+
| "paused"
|
|
105
|
+
| "invariant_broken"
|
|
106
|
+
| "completed";
|
|
107
|
+
|
|
108
|
+
const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
109
|
+
pursuing: "Pursuing",
|
|
110
|
+
scheduled: "Scheduled",
|
|
111
|
+
blocked: "Blocked",
|
|
112
|
+
held: "Held",
|
|
113
|
+
paused: "Paused",
|
|
114
|
+
invariant_broken: "Needs attention",
|
|
115
|
+
completed: "Done",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Select pill state from the goal's authoritative continuation projection. */
|
|
119
|
+
export function sessionChromeGoalPillState(
|
|
120
|
+
goalStatus: "active" | "paused" | "completed",
|
|
121
|
+
continuation: SessionGoal["continuation"] | null | undefined,
|
|
122
|
+
): GoalPillState {
|
|
123
|
+
if (goalStatus === "completed") return "completed";
|
|
124
|
+
if (goalStatus === "paused") return "paused";
|
|
125
|
+
if (!continuation) return "invariant_broken";
|
|
126
|
+
if (continuation.state === "running") {
|
|
127
|
+
return continuation.reason === "goal_turn_running"
|
|
128
|
+
? "pursuing"
|
|
129
|
+
: continuation.reason === "human_turn_running"
|
|
130
|
+
? "blocked"
|
|
131
|
+
: "invariant_broken";
|
|
132
|
+
}
|
|
133
|
+
if (continuation.state === "scheduled") return "scheduled";
|
|
134
|
+
if (continuation.state === "blocked") {
|
|
135
|
+
return continuation.reason === "workstream_paused" ? "held" : "blocked";
|
|
136
|
+
}
|
|
137
|
+
return "invariant_broken";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatCoarseElapsed(ms: number): string {
|
|
141
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
142
|
+
const days = Math.floor(totalSeconds / 86_400);
|
|
143
|
+
const hours = Math.floor((totalSeconds % 86_400) / 3_600);
|
|
144
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
145
|
+
const seconds = totalSeconds % 60;
|
|
146
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
147
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
148
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
149
|
+
return `${seconds}s`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function useLiveElapsed(
|
|
153
|
+
startIso: string | null | undefined,
|
|
154
|
+
live: boolean,
|
|
155
|
+
endIso?: string | null,
|
|
156
|
+
) {
|
|
157
|
+
const start = startIso ? Date.parse(startIso) : Number.NaN;
|
|
158
|
+
const [now, setNow] = useState(() => Date.now());
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!live) return;
|
|
161
|
+
const id = setInterval(() => setNow(Date.now()), 1_000);
|
|
162
|
+
return () => clearInterval(id);
|
|
163
|
+
}, [live]);
|
|
164
|
+
if (!Number.isFinite(start)) return null;
|
|
165
|
+
const end = live ? now : endIso ? Date.parse(endIso) : now;
|
|
166
|
+
return formatCoarseElapsed((Number.isFinite(end) ? end : now) - start);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pendingKindLabel(kind: SessionPendingInputPreview["kind"]): string {
|
|
170
|
+
switch (kind) {
|
|
171
|
+
case "child_terminal_result":
|
|
172
|
+
return "Child result";
|
|
173
|
+
case "agent_steer_instruction":
|
|
174
|
+
return "Steer";
|
|
175
|
+
case "scheduled_occurrence":
|
|
176
|
+
return "Schedule";
|
|
177
|
+
case "goal_continuation":
|
|
178
|
+
return "Goal wake";
|
|
179
|
+
case "agent_message":
|
|
180
|
+
return "Update";
|
|
181
|
+
default:
|
|
182
|
+
return "Incoming";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toneClass(tone: SessionChromeSignalTone, selected: boolean): string {
|
|
187
|
+
switch (tone) {
|
|
188
|
+
case "accent":
|
|
189
|
+
return "text-og-accent";
|
|
190
|
+
case "waiting":
|
|
191
|
+
return "text-og-status-waiting";
|
|
192
|
+
case "running":
|
|
193
|
+
return "text-og-status-running";
|
|
194
|
+
default:
|
|
195
|
+
return selected ? "text-og-fg" : "text-og-fg-subtle";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function SessionChrome({
|
|
200
|
+
queue,
|
|
201
|
+
composer,
|
|
202
|
+
goal,
|
|
203
|
+
agentsPanel,
|
|
204
|
+
agentsSignal,
|
|
205
|
+
onDismissIncoming,
|
|
206
|
+
readOnly = false,
|
|
207
|
+
className,
|
|
208
|
+
active: activeControlled,
|
|
209
|
+
defaultActive = null,
|
|
210
|
+
onActiveChange,
|
|
211
|
+
}: SessionChromeProps) {
|
|
212
|
+
const reactId = useId();
|
|
213
|
+
const panelId = `og-session-chrome-panel-${reactId}`;
|
|
214
|
+
const reduceMotion = useReducedMotion();
|
|
215
|
+
const record = goal?.goal ?? null;
|
|
216
|
+
const incoming = queue.pendingInputs;
|
|
217
|
+
const turns = queue.queue;
|
|
218
|
+
const canMutateQueue = !readOnly && composer !== undefined;
|
|
219
|
+
|
|
220
|
+
const liveGoal =
|
|
221
|
+
record?.status === "active" &&
|
|
222
|
+
record.continuation?.state === "running" &&
|
|
223
|
+
record.continuation.reason === "goal_turn_running";
|
|
224
|
+
const elapsed = useLiveElapsed(
|
|
225
|
+
record?.createdAt,
|
|
226
|
+
Boolean(liveGoal),
|
|
227
|
+
!liveGoal ? record?.updatedAt : null,
|
|
228
|
+
);
|
|
229
|
+
const goalState = record ? sessionChromeGoalPillState(record.status, record.continuation) : null;
|
|
230
|
+
|
|
231
|
+
const signals = useMemo(() => {
|
|
232
|
+
const rows: Array<{
|
|
233
|
+
id: SessionChromeSignalId;
|
|
234
|
+
label: string;
|
|
235
|
+
detail?: string | undefined;
|
|
236
|
+
tone: SessionChromeSignalTone;
|
|
237
|
+
icon: ReactNode;
|
|
238
|
+
}> = [];
|
|
239
|
+
if (incoming.length > 0) {
|
|
240
|
+
const detail = incoming[0]?.summary;
|
|
241
|
+
rows.push({
|
|
242
|
+
id: "incoming",
|
|
243
|
+
label: `${incoming.length} in`,
|
|
244
|
+
...(detail ? { detail } : {}),
|
|
245
|
+
tone: incoming.some(
|
|
246
|
+
(item) => item.classification === "action_required" || item.classification === "failure",
|
|
247
|
+
)
|
|
248
|
+
? "waiting"
|
|
249
|
+
: "neutral",
|
|
250
|
+
icon: <InboxIcon className="size-3" />,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (turns.length > 0) {
|
|
254
|
+
const detail = turns[0]?.prompt;
|
|
255
|
+
rows.push({
|
|
256
|
+
id: "queue",
|
|
257
|
+
label: `${turns.length} queued prompt${turns.length === 1 ? "" : "s"}`,
|
|
258
|
+
...(detail ? { detail } : {}),
|
|
259
|
+
tone: "neutral",
|
|
260
|
+
icon: <ListOrderedIcon className="size-3" />,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (record && goalState) {
|
|
264
|
+
const waiting = goalState === "blocked" || goalState === "held" || goalState === "paused";
|
|
265
|
+
rows.push({
|
|
266
|
+
id: "goal",
|
|
267
|
+
label: GOAL_LABEL[goalState],
|
|
268
|
+
detail: elapsed ? `${elapsed} · ${record.text}` : record.text,
|
|
269
|
+
tone: waiting
|
|
270
|
+
? "waiting"
|
|
271
|
+
: goalState === "pursuing" || goalState === "scheduled"
|
|
272
|
+
? "accent"
|
|
273
|
+
: "neutral",
|
|
274
|
+
icon:
|
|
275
|
+
goalState === "blocked" || goalState === "invariant_broken" ? (
|
|
276
|
+
<TriangleAlertIcon className="size-3" />
|
|
277
|
+
) : goalState === "paused" || goalState === "held" ? (
|
|
278
|
+
<PauseIcon className="size-3" />
|
|
279
|
+
) : (
|
|
280
|
+
<ZapIcon className="size-3" />
|
|
281
|
+
),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
if (agentsSignal && agentsSignal.count > 0) {
|
|
285
|
+
const detail = agentsSignal.detail;
|
|
286
|
+
rows.push({
|
|
287
|
+
id: "agents",
|
|
288
|
+
label: `${agentsSignal.count} agent${agentsSignal.count === 1 ? "" : "s"}`,
|
|
289
|
+
...(detail ? { detail } : {}),
|
|
290
|
+
tone: agentsSignal.tone ?? "neutral",
|
|
291
|
+
icon: <BotIcon className="size-3" />,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return rows;
|
|
295
|
+
}, [agentsSignal, elapsed, goalState, incoming, record, turns]);
|
|
296
|
+
|
|
297
|
+
const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(
|
|
298
|
+
defaultActive,
|
|
299
|
+
);
|
|
300
|
+
const active = activeControlled !== undefined ? activeControlled : activeUncontrolled;
|
|
301
|
+
const setActive = (next: SessionChromeSignalId | null) => {
|
|
302
|
+
if (activeControlled === undefined) setActiveUncontrolled(next);
|
|
303
|
+
onActiveChange?.(next);
|
|
304
|
+
};
|
|
305
|
+
const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
|
|
306
|
+
|
|
307
|
+
const chipRefs = useRef<Partial<Record<SessionChromeSignalId, HTMLButtonElement | null>>>({});
|
|
308
|
+
const railRef = useRef<HTMLDivElement | null>(null);
|
|
309
|
+
const panelBodyRef = useRef<HTMLDivElement | null>(null);
|
|
310
|
+
const [pill, setPill] = useState({ left: 0, width: 0, opacity: 0 });
|
|
311
|
+
const [panelHeight, setPanelHeight] = useState(0);
|
|
312
|
+
|
|
313
|
+
const signalIds = signals.map((signal) => signal.id).join(",");
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (active && !signalIds.split(",").includes(active)) {
|
|
316
|
+
if (activeControlled === undefined) setActiveUncontrolled(null);
|
|
317
|
+
onActiveChange?.(null);
|
|
318
|
+
}
|
|
319
|
+
}, [active, activeControlled, onActiveChange, signalIds]);
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (active !== "queue" || !replaceDraftFor) return;
|
|
323
|
+
if (!turns.some((turn) => turn.id === replaceDraftFor)) {
|
|
324
|
+
setReplaceDraftFor(null);
|
|
325
|
+
}
|
|
326
|
+
}, [active, replaceDraftFor, turns]);
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const rail = railRef.current;
|
|
330
|
+
const measure = () => {
|
|
331
|
+
if (!rail || !active) {
|
|
332
|
+
setPill((prev) => (prev.opacity === 0 ? prev : { ...prev, opacity: 0 }));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const chip = chipRefs.current[active];
|
|
336
|
+
if (!chip) return;
|
|
337
|
+
const railBox = rail.getBoundingClientRect();
|
|
338
|
+
const chipBox = chip.getBoundingClientRect();
|
|
339
|
+
setPill({
|
|
340
|
+
left: chipBox.left - railBox.left,
|
|
341
|
+
width: chipBox.width,
|
|
342
|
+
opacity: 1,
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
measure();
|
|
346
|
+
if (!rail) return;
|
|
347
|
+
const observer = new ResizeObserver(measure);
|
|
348
|
+
observer.observe(rail);
|
|
349
|
+
window.addEventListener("resize", measure);
|
|
350
|
+
return () => {
|
|
351
|
+
observer.disconnect();
|
|
352
|
+
window.removeEventListener("resize", measure);
|
|
353
|
+
};
|
|
354
|
+
}, [active, signals]);
|
|
355
|
+
|
|
356
|
+
const open = active !== null;
|
|
357
|
+
|
|
358
|
+
useLayoutEffect(() => {
|
|
359
|
+
if (!open) {
|
|
360
|
+
setPanelHeight(0);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const node = panelBodyRef.current;
|
|
364
|
+
if (!node) return;
|
|
365
|
+
setPanelHeight(node.offsetHeight);
|
|
366
|
+
}, [open, active, incoming, turns, record, goalState, agentsPanel, agentsSignal]);
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
if (!open) return;
|
|
370
|
+
const node = panelBodyRef.current;
|
|
371
|
+
if (!node || typeof ResizeObserver === "undefined") return;
|
|
372
|
+
const observer = new ResizeObserver(() => {
|
|
373
|
+
setPanelHeight(node.offsetHeight);
|
|
374
|
+
});
|
|
375
|
+
observer.observe(node);
|
|
376
|
+
return () => observer.disconnect();
|
|
377
|
+
}, [open, active]);
|
|
378
|
+
|
|
379
|
+
if (signals.length === 0) return null;
|
|
380
|
+
|
|
381
|
+
const shellDuration = reduceMotion ? 0 : 0.22;
|
|
382
|
+
const crossfadeDuration = reduceMotion ? 0 : 0.18;
|
|
383
|
+
const ease = [0.22, 1, 0.36, 1] as const;
|
|
384
|
+
|
|
385
|
+
const panelBody =
|
|
386
|
+
active === "incoming" ? (
|
|
387
|
+
<IncomingPanel inputs={incoming} onDismiss={onDismissIncoming} />
|
|
388
|
+
) : active === "queue" ? (
|
|
389
|
+
<QueuePanel
|
|
390
|
+
turns={turns}
|
|
391
|
+
readOnly={!canMutateQueue}
|
|
392
|
+
mutationFor={queue.mutationFor}
|
|
393
|
+
replaceDraftFor={replaceDraftFor}
|
|
394
|
+
onCancelReplace={() => setReplaceDraftFor(null)}
|
|
395
|
+
onConfirmReplace={
|
|
396
|
+
canMutateQueue && composer && replaceDraftFor
|
|
397
|
+
? () => {
|
|
398
|
+
const turnId = replaceDraftFor;
|
|
399
|
+
setReplaceDraftFor(null);
|
|
400
|
+
void queue.editTurn(turnId, {
|
|
401
|
+
expectedDraftRevision: composer.draftRevision,
|
|
402
|
+
replaceDraft: true,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
: undefined
|
|
406
|
+
}
|
|
407
|
+
onEdit={
|
|
408
|
+
canMutateQueue && composer
|
|
409
|
+
? (turn) => {
|
|
410
|
+
requestQueueDraftEdit(
|
|
411
|
+
composer,
|
|
412
|
+
() => setReplaceDraftFor(turn.id),
|
|
413
|
+
() => {
|
|
414
|
+
void queue.editTurn(turn.id, {
|
|
415
|
+
expectedDraftRevision: composer.draftRevision,
|
|
416
|
+
replaceDraft: false,
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
: undefined
|
|
422
|
+
}
|
|
423
|
+
onSteer={
|
|
424
|
+
canMutateQueue
|
|
425
|
+
? (turnId) => {
|
|
426
|
+
void queue.steerTurn(turnId);
|
|
427
|
+
}
|
|
428
|
+
: undefined
|
|
429
|
+
}
|
|
430
|
+
onRemove={
|
|
431
|
+
canMutateQueue
|
|
432
|
+
? (turnId) => {
|
|
433
|
+
void queue.removeTurn(turnId);
|
|
434
|
+
}
|
|
435
|
+
: undefined
|
|
436
|
+
}
|
|
437
|
+
onMove={
|
|
438
|
+
canMutateQueue
|
|
439
|
+
? (turnId, beforeTurnId) => {
|
|
440
|
+
void queue.moveTurn(turnId, beforeTurnId);
|
|
441
|
+
}
|
|
442
|
+
: undefined
|
|
443
|
+
}
|
|
444
|
+
/>
|
|
445
|
+
) : active === "goal" && record && goalState && goal ? (
|
|
446
|
+
<GoalPanel goal={goal} state={goalState} elapsed={elapsed} readOnly={readOnly} />
|
|
447
|
+
) : active === "agents" ? (
|
|
448
|
+
<div data-og-session-chrome-panel="agents">
|
|
449
|
+
{agentsPanel ?? <p className="text-og-xs text-og-fg-muted">No agent details.</p>}
|
|
450
|
+
</div>
|
|
451
|
+
) : null;
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<TooltipProvider delayDuration={300}>
|
|
455
|
+
<div
|
|
456
|
+
className={cn("og-session-chrome og-root w-full", className)}
|
|
457
|
+
data-testid="session-chrome"
|
|
458
|
+
data-og-session-chrome=""
|
|
459
|
+
data-og-session-chrome-open={open ? "true" : "false"}
|
|
460
|
+
>
|
|
461
|
+
<div
|
|
462
|
+
className={cn(
|
|
463
|
+
"relative overflow-hidden border",
|
|
464
|
+
"transition-[background-color,border-color,box-shadow] motion-reduce:transition-none",
|
|
465
|
+
)}
|
|
466
|
+
style={{
|
|
467
|
+
borderRadius: "var(--og-session-chrome-radius)",
|
|
468
|
+
background: open
|
|
469
|
+
? "var(--og-session-chrome-surface-open)"
|
|
470
|
+
: "var(--og-session-chrome-surface)",
|
|
471
|
+
borderColor: open
|
|
472
|
+
? "var(--og-session-chrome-border-open)"
|
|
473
|
+
: "var(--og-session-chrome-border)",
|
|
474
|
+
boxShadow: open
|
|
475
|
+
? "var(--og-session-chrome-shadow-open)"
|
|
476
|
+
: "var(--og-session-chrome-shadow)",
|
|
477
|
+
transitionDuration: "var(--og-session-chrome-duration)",
|
|
478
|
+
transitionTimingFunction: "var(--og-session-chrome-ease)",
|
|
479
|
+
}}
|
|
480
|
+
>
|
|
481
|
+
<div
|
|
482
|
+
ref={railRef}
|
|
483
|
+
className="relative flex flex-wrap items-stretch"
|
|
484
|
+
style={{
|
|
485
|
+
gap: "var(--og-session-chrome-chip-gap)",
|
|
486
|
+
padding: "var(--og-session-chrome-rail-pad)",
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
<motion.div
|
|
490
|
+
aria-hidden
|
|
491
|
+
className="pointer-events-none absolute rounded-og-md ring-1 ring-og-border/50"
|
|
492
|
+
style={{
|
|
493
|
+
top: "var(--og-session-chrome-rail-pad)",
|
|
494
|
+
bottom: "var(--og-session-chrome-rail-pad)",
|
|
495
|
+
background: "var(--og-session-chrome-highlight)",
|
|
496
|
+
}}
|
|
497
|
+
initial={false}
|
|
498
|
+
animate={{
|
|
499
|
+
x: pill.left,
|
|
500
|
+
width: pill.width,
|
|
501
|
+
opacity: pill.opacity,
|
|
502
|
+
}}
|
|
503
|
+
transition={{ duration: shellDuration, ease }}
|
|
504
|
+
/>
|
|
505
|
+
{signals.map((signal) => {
|
|
506
|
+
const selected = active === signal.id;
|
|
507
|
+
return (
|
|
508
|
+
<button
|
|
509
|
+
key={signal.id}
|
|
510
|
+
type="button"
|
|
511
|
+
ref={(node) => {
|
|
512
|
+
chipRefs.current[signal.id] = node;
|
|
513
|
+
}}
|
|
514
|
+
aria-expanded={selected}
|
|
515
|
+
aria-controls={panelId}
|
|
516
|
+
data-testid={`session-chrome-${signal.id}`}
|
|
517
|
+
data-og-session-chrome-signal={signal.id}
|
|
518
|
+
onClick={() => setActive(selected ? null : signal.id)}
|
|
519
|
+
className={cn(
|
|
520
|
+
"group relative z-[1] inline-flex min-h-[var(--og-session-chrome-chip-min-height)] max-w-full items-center gap-1 rounded-og-md text-left text-og-xs outline-none",
|
|
521
|
+
"transition-colors duration-150 motion-reduce:transition-none",
|
|
522
|
+
"hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
|
|
523
|
+
"pointer-coarse:min-h-11",
|
|
524
|
+
selected ? "text-og-fg" : "text-og-fg-muted",
|
|
525
|
+
)}
|
|
526
|
+
style={{
|
|
527
|
+
paddingInline: "var(--og-session-chrome-chip-pad-x)",
|
|
528
|
+
}}
|
|
529
|
+
>
|
|
530
|
+
<span className={cn("shrink-0", toneClass(signal.tone, selected))}>
|
|
531
|
+
{signal.icon}
|
|
532
|
+
</span>
|
|
533
|
+
<span className="shrink-0 font-medium text-og-fg">{signal.label}</span>
|
|
534
|
+
{signal.detail ? (
|
|
535
|
+
<>
|
|
536
|
+
<span aria-hidden className="shrink-0 text-og-fg-subtle/60">
|
|
537
|
+
·
|
|
538
|
+
</span>
|
|
539
|
+
<span className="min-w-0 max-w-[8.5rem] truncate text-og-fg sm:max-w-[12rem]">
|
|
540
|
+
{signal.detail}
|
|
541
|
+
</span>
|
|
542
|
+
</>
|
|
543
|
+
) : null}
|
|
544
|
+
</button>
|
|
545
|
+
);
|
|
546
|
+
})}
|
|
547
|
+
<AnimatePresence initial={false}>
|
|
548
|
+
{open ? (
|
|
549
|
+
<motion.button
|
|
550
|
+
key="close"
|
|
551
|
+
type="button"
|
|
552
|
+
aria-label="Close session chrome panel"
|
|
553
|
+
onClick={() => setActive(null)}
|
|
554
|
+
initial={reduceMotion ? false : { opacity: 0 }}
|
|
555
|
+
animate={{ opacity: 1 }}
|
|
556
|
+
exit={reduceMotion ? { opacity: 1 } : { opacity: 0 }}
|
|
557
|
+
transition={{ duration: crossfadeDuration, ease }}
|
|
558
|
+
className="relative z-[1] ml-auto inline-flex size-7 shrink-0 items-center justify-center rounded-og-md text-og-fg-subtle outline-none transition-colors hover:bg-og-surface-3/60 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 pointer-coarse:size-11"
|
|
559
|
+
>
|
|
560
|
+
<XIcon className="size-3" />
|
|
561
|
+
</motion.button>
|
|
562
|
+
) : null}
|
|
563
|
+
</AnimatePresence>
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
<motion.div
|
|
567
|
+
id={panelId}
|
|
568
|
+
data-og-session-chrome-panel-shell=""
|
|
569
|
+
initial={false}
|
|
570
|
+
animate={{
|
|
571
|
+
height: open ? panelHeight : 0,
|
|
572
|
+
opacity: open ? 1 : 0,
|
|
573
|
+
}}
|
|
574
|
+
transition={{ duration: shellDuration, ease }}
|
|
575
|
+
className="overflow-hidden"
|
|
576
|
+
>
|
|
577
|
+
<div
|
|
578
|
+
ref={panelBodyRef}
|
|
579
|
+
className="relative border-t border-og-border/50"
|
|
580
|
+
style={{
|
|
581
|
+
paddingInline: "var(--og-session-chrome-panel-pad-x)",
|
|
582
|
+
paddingBlock: "var(--og-session-chrome-panel-pad-y)",
|
|
583
|
+
}}
|
|
584
|
+
>
|
|
585
|
+
<AnimatePresence initial={false}>
|
|
586
|
+
{active && panelBody ? (
|
|
587
|
+
<motion.div
|
|
588
|
+
key={active}
|
|
589
|
+
data-og-session-chrome-panel-frame={active}
|
|
590
|
+
initial={reduceMotion ? false : { opacity: 0 }}
|
|
591
|
+
animate={{ opacity: 1, position: "relative" }}
|
|
592
|
+
exit={
|
|
593
|
+
reduceMotion
|
|
594
|
+
? { opacity: 1, position: "relative" }
|
|
595
|
+
: {
|
|
596
|
+
opacity: 0,
|
|
597
|
+
position: "absolute",
|
|
598
|
+
top: 0,
|
|
599
|
+
left: 0,
|
|
600
|
+
right: 0,
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
transition={{ duration: crossfadeDuration, ease }}
|
|
604
|
+
>
|
|
605
|
+
{panelBody}
|
|
606
|
+
</motion.div>
|
|
607
|
+
) : null}
|
|
608
|
+
</AnimatePresence>
|
|
609
|
+
</div>
|
|
610
|
+
</motion.div>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
</TooltipProvider>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function IncomingPanel({
|
|
618
|
+
inputs,
|
|
619
|
+
onDismiss,
|
|
620
|
+
}: {
|
|
621
|
+
inputs: SessionPendingInputPreview[];
|
|
622
|
+
onDismiss?: ((inputId: string) => void) | undefined;
|
|
623
|
+
}) {
|
|
624
|
+
return (
|
|
625
|
+
<ul
|
|
626
|
+
className="flex flex-col gap-0.5"
|
|
627
|
+
aria-label="Incoming updates"
|
|
628
|
+
data-og-session-chrome-panel="incoming"
|
|
629
|
+
>
|
|
630
|
+
{inputs.map((input) => (
|
|
631
|
+
<li
|
|
632
|
+
key={input.id}
|
|
633
|
+
className="group flex items-start gap-1.5 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--og-session-chrome-row-hover)]"
|
|
634
|
+
>
|
|
635
|
+
<span
|
|
636
|
+
className={cn(
|
|
637
|
+
"mt-px shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4",
|
|
638
|
+
input.classification === "action_required" || input.classification === "failure"
|
|
639
|
+
? "bg-og-status-waiting/12 text-og-status-waiting"
|
|
640
|
+
: "bg-og-surface-3/80 text-og-fg-muted",
|
|
641
|
+
)}
|
|
642
|
+
>
|
|
643
|
+
{pendingKindLabel(input.kind)}
|
|
644
|
+
</span>
|
|
645
|
+
<p className="min-w-0 flex-1 text-og-xs leading-4 text-og-fg">{input.summary}</p>
|
|
646
|
+
{onDismiss ? (
|
|
647
|
+
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
|
|
648
|
+
<IconAction
|
|
649
|
+
label={`Dismiss incoming ${pendingKindLabel(input.kind)}`}
|
|
650
|
+
tip="Dismiss"
|
|
651
|
+
onClick={() => onDismiss(input.id)}
|
|
652
|
+
danger
|
|
653
|
+
>
|
|
654
|
+
<Trash2Icon className="size-3" />
|
|
655
|
+
</IconAction>
|
|
656
|
+
</div>
|
|
657
|
+
) : null}
|
|
658
|
+
</li>
|
|
659
|
+
))}
|
|
660
|
+
</ul>
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function QueuePanel({
|
|
665
|
+
turns,
|
|
666
|
+
readOnly,
|
|
667
|
+
mutationFor,
|
|
668
|
+
replaceDraftFor,
|
|
669
|
+
onCancelReplace,
|
|
670
|
+
onConfirmReplace,
|
|
671
|
+
onEdit,
|
|
672
|
+
onSteer,
|
|
673
|
+
onRemove,
|
|
674
|
+
onMove,
|
|
675
|
+
}: {
|
|
676
|
+
turns: SessionTurn[];
|
|
677
|
+
readOnly: boolean;
|
|
678
|
+
mutationFor: UseTurnQueueResult["mutationFor"];
|
|
679
|
+
replaceDraftFor?: string | null | undefined;
|
|
680
|
+
onCancelReplace?: (() => void) | undefined;
|
|
681
|
+
onConfirmReplace?: (() => void) | undefined;
|
|
682
|
+
onEdit?: ((turn: SessionTurn) => void) | undefined;
|
|
683
|
+
onSteer?: ((turnId: string) => void) | undefined;
|
|
684
|
+
onRemove?: ((turnId: string) => void) | undefined;
|
|
685
|
+
onMove?: ((turnId: string, beforeTurnId: string | null) => void) | undefined;
|
|
686
|
+
}) {
|
|
687
|
+
return (
|
|
688
|
+
<ol
|
|
689
|
+
className="flex flex-col gap-0.5"
|
|
690
|
+
aria-label="Queued prompts"
|
|
691
|
+
data-og-session-chrome-panel="queue"
|
|
692
|
+
>
|
|
693
|
+
{turns.map((turn, index) => {
|
|
694
|
+
const pending = mutationFor(turn.id);
|
|
695
|
+
const beforeUp = index > 0 ? (turns[index - 1]?.id ?? null) : null;
|
|
696
|
+
const beforeDown = index < turns.length - 1 ? (turns[index + 2]?.id ?? null) : null;
|
|
697
|
+
const showActions = !readOnly && (onEdit || onSteer || onRemove || onMove);
|
|
698
|
+
const confirmingReplace = replaceDraftFor === turn.id;
|
|
699
|
+
return (
|
|
700
|
+
<li
|
|
701
|
+
key={turn.id}
|
|
702
|
+
data-queue-turn-id={turn.id}
|
|
703
|
+
className="group flex flex-col gap-1 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--og-session-chrome-row-hover)]"
|
|
704
|
+
>
|
|
705
|
+
<div className="flex items-start gap-1.5">
|
|
706
|
+
<span className="mt-px shrink-0 font-og-mono text-[10px] leading-4 text-og-fg-subtle">
|
|
707
|
+
{index + 1}
|
|
708
|
+
</span>
|
|
709
|
+
<p className="min-w-0 flex-1 truncate text-og-xs leading-4 text-og-fg">
|
|
710
|
+
{turn.prompt}
|
|
711
|
+
</p>
|
|
712
|
+
{showActions ? (
|
|
713
|
+
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
|
|
714
|
+
{onMove && turns.length > 1 ? (
|
|
715
|
+
<>
|
|
716
|
+
<IconAction
|
|
717
|
+
label={`Move queued prompt ${index + 1} up`}
|
|
718
|
+
tip="Move up"
|
|
719
|
+
disabled={pending !== null || index === 0}
|
|
720
|
+
onClick={() => onMove(turn.id, beforeUp)}
|
|
721
|
+
>
|
|
722
|
+
<ArrowUpIcon className="size-3" />
|
|
723
|
+
</IconAction>
|
|
724
|
+
<IconAction
|
|
725
|
+
label={`Move queued prompt ${index + 1} down`}
|
|
726
|
+
tip="Move down"
|
|
727
|
+
disabled={pending !== null || index >= turns.length - 1}
|
|
728
|
+
onClick={() => onMove(turn.id, beforeDown)}
|
|
729
|
+
>
|
|
730
|
+
<ArrowDownIcon className="size-3" />
|
|
731
|
+
</IconAction>
|
|
732
|
+
</>
|
|
733
|
+
) : null}
|
|
734
|
+
{onSteer ? (
|
|
735
|
+
<IconAction
|
|
736
|
+
label={`Steer queued prompt ${index + 1}`}
|
|
737
|
+
tip={QUEUE_STEER_TIP}
|
|
738
|
+
disabled={pending !== null}
|
|
739
|
+
onClick={() => onSteer(turn.id)}
|
|
740
|
+
>
|
|
741
|
+
{pending === "steer" ? (
|
|
742
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
743
|
+
) : (
|
|
744
|
+
<ZapIcon className="size-3" />
|
|
745
|
+
)}
|
|
746
|
+
</IconAction>
|
|
747
|
+
) : null}
|
|
748
|
+
{onEdit ? (
|
|
749
|
+
<IconAction
|
|
750
|
+
label={`Edit queued prompt ${index + 1}`}
|
|
751
|
+
tip={QUEUE_EDIT_TIP}
|
|
752
|
+
disabled={pending !== null}
|
|
753
|
+
onClick={() => onEdit(turn)}
|
|
754
|
+
>
|
|
755
|
+
{pending === "edit" ? (
|
|
756
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
757
|
+
) : (
|
|
758
|
+
<PencilIcon className="size-3" />
|
|
759
|
+
)}
|
|
760
|
+
</IconAction>
|
|
761
|
+
) : null}
|
|
762
|
+
{onRemove ? (
|
|
763
|
+
<IconAction
|
|
764
|
+
label={`Remove queued prompt ${index + 1}`}
|
|
765
|
+
tip={QUEUE_DELETE_TIP}
|
|
766
|
+
disabled={pending !== null}
|
|
767
|
+
onClick={() => onRemove(turn.id)}
|
|
768
|
+
danger
|
|
769
|
+
>
|
|
770
|
+
{pending === "delete" ? (
|
|
771
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
772
|
+
) : (
|
|
773
|
+
<Trash2Icon className="size-3" />
|
|
774
|
+
)}
|
|
775
|
+
</IconAction>
|
|
776
|
+
) : null}
|
|
777
|
+
</div>
|
|
778
|
+
) : null}
|
|
779
|
+
</div>
|
|
780
|
+
{confirmingReplace ? (
|
|
781
|
+
<div className="rounded-og-sm border border-og-status-waiting/30 bg-og-status-waiting/10 p-2 text-og-xs text-og-fg">
|
|
782
|
+
<p>Your composer already has a draft. Replace it with this queued prompt?</p>
|
|
783
|
+
<p className="mt-0.5 text-og-fg-muted">
|
|
784
|
+
The current draft will be permanently discarded; this queued prompt is preserved
|
|
785
|
+
until you confirm.
|
|
786
|
+
</p>
|
|
787
|
+
<div className="mt-2 flex justify-end gap-1.5">
|
|
788
|
+
<button
|
|
789
|
+
type="button"
|
|
790
|
+
className="rounded-og-sm px-2 py-1 font-medium hover:bg-og-surface-3/70 focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
791
|
+
onClick={onCancelReplace}
|
|
792
|
+
>
|
|
793
|
+
Keep current draft
|
|
794
|
+
</button>
|
|
795
|
+
<button
|
|
796
|
+
type="button"
|
|
797
|
+
className="rounded-og-sm bg-og-accent px-2 py-1 font-medium text-og-accent-fg hover:opacity-90 focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
798
|
+
onClick={onConfirmReplace}
|
|
799
|
+
>
|
|
800
|
+
Replace and edit
|
|
801
|
+
</button>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
) : null}
|
|
805
|
+
</li>
|
|
806
|
+
);
|
|
807
|
+
})}
|
|
808
|
+
</ol>
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function GoalPanel({
|
|
813
|
+
goal,
|
|
814
|
+
state,
|
|
815
|
+
elapsed,
|
|
816
|
+
readOnly,
|
|
817
|
+
}: {
|
|
818
|
+
goal: UseGoalResult;
|
|
819
|
+
state: GoalPillState;
|
|
820
|
+
elapsed: string | null;
|
|
821
|
+
readOnly: boolean;
|
|
822
|
+
}) {
|
|
823
|
+
const record = goal.goal;
|
|
824
|
+
if (!record) return null;
|
|
825
|
+
const canToggle = !readOnly && (record.status === "active" || record.status === "paused");
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<div className="space-y-1.5" data-og-session-chrome-panel="goal">
|
|
829
|
+
<div className="flex flex-wrap items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-og-fg-subtle">
|
|
830
|
+
<span>{GOAL_LABEL[state]}</span>
|
|
831
|
+
{elapsed ? (
|
|
832
|
+
<span className="tabular-nums normal-case tracking-normal text-og-fg-muted">
|
|
833
|
+
· {elapsed}
|
|
834
|
+
</span>
|
|
835
|
+
) : null}
|
|
836
|
+
<span className="normal-case tracking-normal text-og-fg-muted">· v{record.version}</span>
|
|
837
|
+
</div>
|
|
838
|
+
<p className="text-og-sm leading-5 text-og-fg">{record.text}</p>
|
|
839
|
+
{record.successCriteria ? (
|
|
840
|
+
<p className="text-og-xs leading-4 text-og-fg-muted">
|
|
841
|
+
<span className="font-medium text-og-fg">Done when</span> {record.successCriteria}
|
|
842
|
+
</p>
|
|
843
|
+
) : null}
|
|
844
|
+
{record.continuation?.lastError ? (
|
|
845
|
+
<p className="rounded-og-sm bg-og-status-waiting/10 px-1.5 py-1 text-og-xs leading-4 text-og-status-waiting">
|
|
846
|
+
{record.continuation.lastError}
|
|
847
|
+
</p>
|
|
848
|
+
) : null}
|
|
849
|
+
<div className="flex flex-wrap items-center justify-between gap-1.5 pt-0.5">
|
|
850
|
+
<div className="flex flex-wrap gap-1 text-[10px] text-og-fg-muted">
|
|
851
|
+
<span className="rounded bg-og-surface-3/70 px-1 py-px">
|
|
852
|
+
{record.autoContinuations} auto-continues
|
|
853
|
+
</span>
|
|
854
|
+
<span className="rounded bg-og-surface-3/70 px-1 py-px">
|
|
855
|
+
{record.noProgressStreak} stalled
|
|
856
|
+
</span>
|
|
857
|
+
</div>
|
|
858
|
+
{!readOnly ? (
|
|
859
|
+
<div className="flex items-center gap-0.5">
|
|
860
|
+
{canToggle ? (
|
|
861
|
+
<button
|
|
862
|
+
type="button"
|
|
863
|
+
disabled={goal.updating}
|
|
864
|
+
onClick={() =>
|
|
865
|
+
void (record.status === "paused"
|
|
866
|
+
? goal.resume()
|
|
867
|
+
: goal.pause("Paused from session chrome"))
|
|
868
|
+
}
|
|
869
|
+
className="inline-flex h-6 items-center gap-1 rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-muted outline-none transition-colors hover:bg-og-surface-3/70 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:opacity-50"
|
|
870
|
+
>
|
|
871
|
+
{goal.updating ? (
|
|
872
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
873
|
+
) : record.status === "paused" ? (
|
|
874
|
+
<PlayIcon className="size-3" />
|
|
875
|
+
) : (
|
|
876
|
+
<PauseIcon className="size-3" />
|
|
877
|
+
)}
|
|
878
|
+
{record.status === "paused" ? "Resume" : "Pause"}
|
|
879
|
+
</button>
|
|
880
|
+
) : null}
|
|
881
|
+
<button
|
|
882
|
+
type="button"
|
|
883
|
+
disabled={goal.updating}
|
|
884
|
+
onClick={() => void goal.deleteGoal()}
|
|
885
|
+
className="inline-flex h-6 items-center gap-1 rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-subtle outline-none transition-colors hover:bg-og-surface-3/70 hover:text-og-danger focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:opacity-50"
|
|
886
|
+
>
|
|
887
|
+
<Trash2Icon className="size-3" />
|
|
888
|
+
Clear
|
|
889
|
+
</button>
|
|
890
|
+
</div>
|
|
891
|
+
) : null}
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const QUEUE_STEER_TIP = (
|
|
898
|
+
<span className="flex flex-col gap-0.5 text-left">
|
|
899
|
+
<span className="font-medium">Steer</span>
|
|
900
|
+
<span className="opacity-80">Interrupt the current turn and send this message now</span>
|
|
901
|
+
</span>
|
|
902
|
+
);
|
|
903
|
+
const QUEUE_EDIT_TIP = "Edit in composer";
|
|
904
|
+
const QUEUE_DELETE_TIP = "Delete this queued prompt";
|
|
905
|
+
|
|
906
|
+
function IconAction({
|
|
907
|
+
label,
|
|
908
|
+
tip,
|
|
909
|
+
onClick,
|
|
910
|
+
disabled,
|
|
911
|
+
danger,
|
|
912
|
+
children,
|
|
913
|
+
}: {
|
|
914
|
+
label: string;
|
|
915
|
+
tip: ReactNode;
|
|
916
|
+
onClick: () => void;
|
|
917
|
+
disabled?: boolean;
|
|
918
|
+
danger?: boolean;
|
|
919
|
+
children: ReactNode;
|
|
920
|
+
}) {
|
|
921
|
+
return (
|
|
922
|
+
<Tooltip>
|
|
923
|
+
<TooltipTrigger asChild>
|
|
924
|
+
<button
|
|
925
|
+
type="button"
|
|
926
|
+
aria-label={label}
|
|
927
|
+
disabled={disabled}
|
|
928
|
+
onClick={onClick}
|
|
929
|
+
className={cn(
|
|
930
|
+
"inline-flex size-6 items-center justify-center rounded-og-sm text-og-fg-subtle outline-none transition-colors",
|
|
931
|
+
"hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40",
|
|
932
|
+
"disabled:pointer-events-none disabled:opacity-40 pointer-coarse:size-9",
|
|
933
|
+
danger && "hover:text-og-danger",
|
|
934
|
+
)}
|
|
935
|
+
>
|
|
936
|
+
{children}
|
|
937
|
+
</button>
|
|
938
|
+
</TooltipTrigger>
|
|
939
|
+
<TooltipContent side="top">{tip}</TooltipContent>
|
|
940
|
+
</Tooltip>
|
|
941
|
+
);
|
|
942
|
+
}
|