@volter-ai-dev/supercode-ui 0.1.70 → 0.1.72
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 +61 -1
- package/activity.mjs +5 -1235
- package/chunks/chunk-2FG7VKIW.mjs +201 -0
- package/chunks/chunk-4XUMCWPC.mjs +29 -0
- package/chunks/chunk-E65I2I2L.mjs +120 -0
- package/chunks/chunk-IL5T2AST.mjs +842 -0
- package/chunks/chunk-LOP6XOON.mjs +671 -0
- package/chunks/chunk-LS6JBXNT.mjs +1237 -0
- package/chunks/chunk-SJ3YTA6Z.mjs +316 -0
- package/chunks/chunk-TKTOVDXB.mjs +139 -0
- package/chunks/chunk-V3UM7H7W.mjs +328 -0
- package/chunks/chunk-WRK5TBX3.mjs +224 -0
- package/chunks/chunk-ZLYHUYE2.mjs +62 -0
- package/components.d.ts +3 -0
- package/components.mjs +59 -3766
- package/composer.mjs +8 -580
- package/conversation.mjs +16 -1373
- package/embed.mjs +15 -3681
- package/icon.mjs +3 -58
- package/index.d.ts +25 -0
- package/logo.mjs +5 -87
- package/messenger.d.ts +3 -0
- package/messenger.mjs +21 -3677
- package/package.json +22 -3
- package/react/activity.mjs +5 -1235
- package/react/chunks/chunk-7ES5FSLG.mjs +316 -0
- package/react/chunks/chunk-LS6JBXNT.mjs +1237 -0
- package/react/chunks/chunk-LYRIQE5M.mjs +29 -0
- package/react/chunks/chunk-NJC6S2AW.mjs +201 -0
- package/react/chunks/chunk-OINC7LV7.mjs +62 -0
- package/react/chunks/chunk-QL6QEYPK.mjs +842 -0
- package/react/chunks/chunk-UGL6AB2K.mjs +120 -0
- package/react/chunks/chunk-UKHDCPME.mjs +328 -0
- package/react/chunks/chunk-UKMTRNJN.mjs +671 -0
- package/react/chunks/chunk-VFOVJ4S3.mjs +139 -0
- package/react/chunks/chunk-YK7SJ6EH.mjs +224 -0
- package/react/components.mjs +59 -3766
- package/react/composer.mjs +8 -580
- package/react/conversation.mjs +16 -1373
- package/react/icon.mjs +3 -58
- package/react/index.d.ts +25 -0
- package/react/logo.mjs +5 -87
- package/react/messenger.d.ts +3 -0
- package/react/messenger.mjs +21 -3677
- package/react/sessions.mjs +9 -519
- package/react/settings.mjs +9 -425
- package/react/subagents.mjs +8 -1546
- package/react/supervision-components.mjs +1354 -0
- package/react/supervision.d.ts +36 -0
- package/sessions.mjs +9 -519
- package/settings.mjs +9 -425
- package/source-inventory.d.ts +240 -0
- package/source-inventory.mjs +367 -0
- package/styles.css +104 -0
- package/subagents.mjs +8 -1546
- package/supervision-components.d.ts +36 -0
- package/supervision-components.mjs +1354 -0
- package/supervision.d.ts +242 -0
- package/supervision.mjs +349 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HarnessAdvisory,
|
|
3
|
+
HarnessPicker,
|
|
4
|
+
HarnessReadiness,
|
|
5
|
+
HarnessSettingsPanel
|
|
6
|
+
} from "./chunk-YK7SJ6EH.mjs";
|
|
7
|
+
import {
|
|
8
|
+
SessionList
|
|
9
|
+
} from "./chunk-NJC6S2AW.mjs";
|
|
10
|
+
import {
|
|
11
|
+
SubagentInspector
|
|
12
|
+
} from "./chunk-UGL6AB2K.mjs";
|
|
13
|
+
import {
|
|
14
|
+
HarnessLogo
|
|
15
|
+
} from "./chunk-VFOVJ4S3.mjs";
|
|
16
|
+
import {
|
|
17
|
+
Conversation
|
|
18
|
+
} from "./chunk-UKMTRNJN.mjs";
|
|
19
|
+
import {
|
|
20
|
+
Composer,
|
|
21
|
+
ContinuationBar,
|
|
22
|
+
ExecutionModeSelect,
|
|
23
|
+
dispatchConfirmedIntent,
|
|
24
|
+
useAutosizeTextarea
|
|
25
|
+
} from "./chunk-7ES5FSLG.mjs";
|
|
26
|
+
import {
|
|
27
|
+
ContextCandidates,
|
|
28
|
+
ContextTray,
|
|
29
|
+
ImageTray,
|
|
30
|
+
MAX_CONTEXT_ITEMS,
|
|
31
|
+
MAX_IMAGE_ITEMS,
|
|
32
|
+
boundedSet,
|
|
33
|
+
imageAttachmentsFromFiles,
|
|
34
|
+
mergeContext,
|
|
35
|
+
mergeImages,
|
|
36
|
+
normalizeAttachmentCandidates,
|
|
37
|
+
partitionAttachments,
|
|
38
|
+
uiMemory
|
|
39
|
+
} from "./chunk-UKHDCPME.mjs";
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_LABELS,
|
|
42
|
+
harnessDisplayName,
|
|
43
|
+
isSendKey,
|
|
44
|
+
normalizeUiState,
|
|
45
|
+
operationLabel,
|
|
46
|
+
sessionDisplayName,
|
|
47
|
+
terminalCommand
|
|
48
|
+
} from "./chunk-LS6JBXNT.mjs";
|
|
49
|
+
import {
|
|
50
|
+
UiIcon
|
|
51
|
+
} from "./chunk-OINC7LV7.mjs";
|
|
52
|
+
|
|
53
|
+
// src/messenger.jsx
|
|
54
|
+
import { createContext } from "react";
|
|
55
|
+
import { useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
56
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
57
|
+
var pendingMessageMemory = uiMemory();
|
|
58
|
+
var messengerViewMemory = uiMemory();
|
|
59
|
+
var newChatMemory = uiMemory();
|
|
60
|
+
var TRACKED_ACTIONS = /* @__PURE__ */ new Set(["resume", "join", "detach", "branch", "reduce", "terminal", "export", "steer", "interrupt", "respond", "configureHarness", "refresh", "loadEarlier", "loadSessions"]);
|
|
61
|
+
function UniversalNouns({ state, adapter }) {
|
|
62
|
+
const banners = [];
|
|
63
|
+
if (state.participant && state.participant.kind !== "local-user") {
|
|
64
|
+
banners.push(
|
|
65
|
+
/* @__PURE__ */ jsx("div", { className: "scui-notice scui-noun-banner", "data-noun": "participant", "data-kind": state.participant.kind, children: state.participant.kind === "foreign" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
66
|
+
"This is ",
|
|
67
|
+
/* @__PURE__ */ jsx("strong", { children: state.participant.label ?? "someone else's" }),
|
|
68
|
+
" conversation",
|
|
69
|
+
state.participant.origin ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
70
|
+
" via ",
|
|
71
|
+
state.participant.origin
|
|
72
|
+
] }) : null,
|
|
73
|
+
"."
|
|
74
|
+
] }) : /* @__PURE__ */ jsx(Fragment, { children: "Unknown participant \u2014 this conversation was not started here." }) }, "participant")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (state.mirror) {
|
|
78
|
+
banners.push(
|
|
79
|
+
/* @__PURE__ */ jsxs("div", { className: "scui-notice scui-noun-banner", "data-noun": "mirror", "data-truncated": state.mirror.truncated, children: [
|
|
80
|
+
state.mirror.bounded ? "Bounded mirror" : "Mirror",
|
|
81
|
+
" of a ",
|
|
82
|
+
/* @__PURE__ */ jsx("strong", { children: harnessDisplayName(state.mirror.canonicalHarness) }),
|
|
83
|
+
" session \u2014 the canonical record lives in its own store",
|
|
84
|
+
state.mirror.truncated ? " (truncated here)" : "",
|
|
85
|
+
".",
|
|
86
|
+
state.mirror.canonicalKey ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => adapter.onIntent({ action: "open_session", key: state.mirror.canonicalKey }), children: "Open canonical session" }) : null
|
|
87
|
+
] }, "mirror")
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (state.holder && state.holder.surface !== "supercode") {
|
|
91
|
+
banners.push(
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "scui-notice scui-noun-banner", "data-noun": "holder", "data-surface": state.holder.surface ?? "unheld", children: [
|
|
93
|
+
state.holder.surface ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
94
|
+
"Held by ",
|
|
95
|
+
/* @__PURE__ */ jsx("strong", { children: state.holder.surface }),
|
|
96
|
+
" right now."
|
|
97
|
+
] }) : /* @__PURE__ */ jsx(Fragment, { children: "No surface holds this session right now." }),
|
|
98
|
+
state.holder.canTakeOver ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => dispatchConfirmedIntent(adapter, { action: "take_over" }), children: "Take over" }) : null
|
|
99
|
+
] }, "holder")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (!banners.length) return null;
|
|
103
|
+
return /* @__PURE__ */ jsx("div", { className: "scui-noun-banners", children: banners });
|
|
104
|
+
}
|
|
105
|
+
function Receipt({ state, adapter }) {
|
|
106
|
+
const receipt = state.reductionReceipt;
|
|
107
|
+
if (receipt) return /* @__PURE__ */ jsx("div", { className: "scui-receipt", children: /* @__PURE__ */ jsxs("span", { children: [
|
|
108
|
+
/* @__PURE__ */ jsx("strong", { children: "Reduced and verified" }),
|
|
109
|
+
/* @__PURE__ */ jsxs("small", { children: [
|
|
110
|
+
receipt.sourceTokens.toLocaleString(),
|
|
111
|
+
" \u2192 ",
|
|
112
|
+
receipt.reducedTokens.toLocaleString(),
|
|
113
|
+
" tokens \xB7 ",
|
|
114
|
+
receipt.ratio.toFixed(1),
|
|
115
|
+
"\xD7 \xB7 reversible"
|
|
116
|
+
] })
|
|
117
|
+
] }) });
|
|
118
|
+
if (state.exportReceipt) return /* @__PURE__ */ jsxs("div", { className: "scui-receipt", children: [
|
|
119
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
120
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
121
|
+
"Lossless export ready \xB7 ",
|
|
122
|
+
harnessDisplayName(state.exportReceipt.targetHarness)
|
|
123
|
+
] }),
|
|
124
|
+
/* @__PURE__ */ jsxs("small", { children: [
|
|
125
|
+
state.exportReceipt.path,
|
|
126
|
+
" \xB7 ",
|
|
127
|
+
state.exportReceipt.files,
|
|
128
|
+
" files"
|
|
129
|
+
] })
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => adapter.copyText?.(state.exportReceipt.path), children: "Copy path" })
|
|
132
|
+
] });
|
|
133
|
+
if (state.terminalHandoff) return /* @__PURE__ */ jsxs("div", { className: "scui-receipt", children: [
|
|
134
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
135
|
+
/* @__PURE__ */ jsx("strong", { children: "Terminal handoff ready" }),
|
|
136
|
+
/* @__PURE__ */ jsx("small", { children: state.terminalHandoff.cwd })
|
|
137
|
+
] }),
|
|
138
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => adapter.copyText?.(terminalCommand(state.terminalHandoff)), children: "Copy command" })
|
|
139
|
+
] });
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function ConversationActions({ state, adapter, actionPending, onNew, onSettings }) {
|
|
143
|
+
const [open, setOpen] = useState(false);
|
|
144
|
+
const root = useRef(null);
|
|
145
|
+
const panel = useRef(null);
|
|
146
|
+
const trigger = useRef(null);
|
|
147
|
+
const menuId = useId();
|
|
148
|
+
const targets = state.harnesses.filter((item) => item.startable);
|
|
149
|
+
const groups = [
|
|
150
|
+
{
|
|
151
|
+
label: "Session",
|
|
152
|
+
items: [
|
|
153
|
+
{ key: "new", label: "New chat", onSelect: onNew },
|
|
154
|
+
state.canDetach ? { key: "detach", label: "Detach to read-only", intent: { action: "detach" } } : null,
|
|
155
|
+
state.canOpenTerminal ? { key: "terminal", label: "Prepare terminal handoff", intent: { action: "terminal" } } : null,
|
|
156
|
+
state.canExport && state.exportBackTarget ? { key: "export", label: `Export back to ${harnessDisplayName(state.exportBackTarget)}`, intent: { action: "export", targetHarness: state.exportBackTarget } } : null,
|
|
157
|
+
state.interopSettings || state.interopSettingsError ? { key: "settings", label: "Interoperability settings", onSelect: onSettings } : null,
|
|
158
|
+
adapter.openKeyboardShortcuts ? { key: "shortcuts", label: "Keyboard shortcuts", onSelect: adapter.openKeyboardShortcuts } : null
|
|
159
|
+
].filter(Boolean)
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
label: "Lower-cost continuation",
|
|
163
|
+
items: state.canReduce ? targets.map((target) => ({ key: `reduce:${target.id}`, label: target.id === state.harness ? `Reduce and continue in ${target.label}` : `Reduce and switch to ${target.label}`, intent: { action: "reduce", targetHarness: target.id } })) : []
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
label: "Independent continuation",
|
|
167
|
+
items: state.canBranch ? targets.map((target) => ({ key: `branch:${target.id}`, label: target.id === state.harness ? `Fork in ${target.label}` : `Continue with ${target.label}`, intent: { action: "branch", targetHarness: target.id } })) : []
|
|
168
|
+
}
|
|
169
|
+
].filter((group) => group.items.length);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!open) return;
|
|
172
|
+
panel.current?.querySelector("button:not(:disabled)")?.focus({ preventScroll: true });
|
|
173
|
+
const dismiss = (event) => {
|
|
174
|
+
if (event.type === "keydown" && event.key === "Escape") {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
setOpen(false);
|
|
177
|
+
trigger.current?.focus({ preventScroll: true });
|
|
178
|
+
} else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
|
|
179
|
+
setOpen(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
document.addEventListener("keydown", dismiss);
|
|
183
|
+
document.addEventListener("pointerdown", dismiss, true);
|
|
184
|
+
return () => {
|
|
185
|
+
document.removeEventListener("keydown", dismiss);
|
|
186
|
+
document.removeEventListener("pointerdown", dismiss, true);
|
|
187
|
+
};
|
|
188
|
+
}, [open]);
|
|
189
|
+
const dispatch = (item) => {
|
|
190
|
+
setOpen(false);
|
|
191
|
+
if (item.onSelect) item.onSelect();
|
|
192
|
+
else dispatchConfirmedIntent(adapter, item.intent);
|
|
193
|
+
};
|
|
194
|
+
const navigate = (event) => {
|
|
195
|
+
if (event.key === "Escape") {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
event.stopPropagation();
|
|
198
|
+
setOpen(false);
|
|
199
|
+
trigger.current?.focus({ preventScroll: true });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
|
|
203
|
+
const items = [...panel.current.querySelectorAll("button:not(:disabled)")];
|
|
204
|
+
if (!items.length) return;
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
const current = items.indexOf(document.activeElement);
|
|
207
|
+
const index = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : event.key === "ArrowDown" ? (current + 1) % items.length : (current <= 0 ? items.length : current) - 1;
|
|
208
|
+
items[index].focus({ preventScroll: true });
|
|
209
|
+
};
|
|
210
|
+
return /* @__PURE__ */ jsxs("div", { className: "scui-menu", ref: root, onBlur: (event) => {
|
|
211
|
+
if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
212
|
+
}, children: [
|
|
213
|
+
/* @__PURE__ */ jsx("button", { ref: trigger, className: "scui-menu-trigger", type: "button", "aria-label": "Conversation actions", "aria-haspopup": "menu", "aria-expanded": open, "aria-controls": open ? menuId : void 0, onClick: () => setOpen((value) => !value), children: /* @__PURE__ */ jsx(UiIcon, { name: "menu", size: 18 }) }),
|
|
214
|
+
open ? /* @__PURE__ */ jsx("div", { ref: panel, id: menuId, className: "scui-menu-panel", role: "menu", "aria-label": "Conversation actions", onKeyDown: navigate, children: groups.map((group) => /* @__PURE__ */ jsxs("section", { role: "group", "aria-label": group.label, children: [
|
|
215
|
+
/* @__PURE__ */ jsx("strong", { children: group.label }),
|
|
216
|
+
group.items.map((item) => /* @__PURE__ */ jsx("button", { role: "menuitem", type: "button", disabled: actionPending, onClick: () => dispatch(item), children: item.label }, item.key))
|
|
217
|
+
] }, group.label)) }) : null
|
|
218
|
+
] });
|
|
219
|
+
}
|
|
220
|
+
function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings, headerActions: HeaderActions }) {
|
|
221
|
+
const back = useRef(null);
|
|
222
|
+
const harness = state.attached?.harness ?? state.harness;
|
|
223
|
+
const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
|
|
224
|
+
const status = state.needsInput ? "Needs input" : pendingStatus === "failed" ? "Send failed" : state.busy ? "Working" : pendingStatus === "sending" ? "Sending" : pendingStatus === "editing" ? "Editing message" : state.messaging === "live_peer" ? "Live" : state.mode === "mirror" ? "Read-only" : "Ready";
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (state.mode === "mirror" && !state.canSend) back.current?.focus({ preventScroll: true });
|
|
227
|
+
}, []);
|
|
228
|
+
return /* @__PURE__ */ jsxs("header", { className: "scui-head scui-chat-head", children: [
|
|
229
|
+
onBack ? /* @__PURE__ */ jsx("button", { ref: back, type: "button", "aria-label": "Back to chats", onClick: onBack, children: /* @__PURE__ */ jsx(UiIcon, { name: "back", size: 17 }) }) : null,
|
|
230
|
+
/* @__PURE__ */ jsx(HarnessLogo, { id: harness, size: 24 }),
|
|
231
|
+
/* @__PURE__ */ jsxs("span", { className: "scui-head-copy", children: [
|
|
232
|
+
/* @__PURE__ */ jsx("strong", { children: title }),
|
|
233
|
+
/* @__PURE__ */ jsxs("small", { children: [
|
|
234
|
+
harnessDisplayName(harness),
|
|
235
|
+
" \xB7 ",
|
|
236
|
+
status
|
|
237
|
+
] })
|
|
238
|
+
] }),
|
|
239
|
+
HeaderActions ? /* @__PURE__ */ jsx(HeaderActions, { state, adapter, value: "chat" }) : null,
|
|
240
|
+
/* @__PURE__ */ jsx(ConversationActions, { state, adapter, actionPending, onNew, onSettings }),
|
|
241
|
+
onClose ? /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx(UiIcon, { name: "close", size: 18 }) }) : null
|
|
242
|
+
] });
|
|
243
|
+
}
|
|
244
|
+
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
|
|
245
|
+
const Header = slots.header;
|
|
246
|
+
const HeaderActions = slots.headerActions;
|
|
247
|
+
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
248
|
+
const [pending, setPendingState] = useState(() => pendingMessageMemory.get(memoryKey) ?? null);
|
|
249
|
+
const [pendingAction, setPendingAction] = useState(null);
|
|
250
|
+
const [restoreDraft, setRestoreDraft] = useState(null);
|
|
251
|
+
const [settings, setSettings] = useState(null);
|
|
252
|
+
const restoreSequence = useRef(0);
|
|
253
|
+
const actionSequence = useRef(0);
|
|
254
|
+
const lastNavigation = useRef(null);
|
|
255
|
+
const acknowledged = useRef(/* @__PURE__ */ new Set());
|
|
256
|
+
const setPending = (update) => setPendingState((current) => {
|
|
257
|
+
const next = typeof update === "function" ? update(current) : update;
|
|
258
|
+
if (next) boundedSet(pendingMessageMemory, memoryKey, next);
|
|
259
|
+
else pendingMessageMemory.delete(memoryKey);
|
|
260
|
+
return next;
|
|
261
|
+
});
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
setPendingState(pendingMessageMemory.get(memoryKey) ?? null);
|
|
264
|
+
setPendingAction(null);
|
|
265
|
+
setRestoreDraft(null);
|
|
266
|
+
}, [memoryKey]);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (!navigation || navigation.id === lastNavigation.current) return;
|
|
269
|
+
lastNavigation.current = navigation.id;
|
|
270
|
+
if (navigation.draft === void 0 && !navigation.context?.length && !navigation.images?.length) return;
|
|
271
|
+
setRestoreDraft({
|
|
272
|
+
id: `host:${String(navigation.id)}`,
|
|
273
|
+
text: navigation.draft ?? "",
|
|
274
|
+
context: navigation.context,
|
|
275
|
+
images: navigation.images
|
|
276
|
+
});
|
|
277
|
+
}, [navigation]);
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!pending) return;
|
|
280
|
+
if (state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.text.trim() && !pending.baselineIds.includes(entry.id))) {
|
|
281
|
+
setPending(null);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
285
|
+
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
286
|
+
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
287
|
+
const beginPending = (text, context = [], images = []) => setPending({
|
|
288
|
+
text,
|
|
289
|
+
context,
|
|
290
|
+
images,
|
|
291
|
+
status: "sending",
|
|
292
|
+
initialError: state.error,
|
|
293
|
+
seenBusy: state.busy,
|
|
294
|
+
baselineIds: state.transcript.filter((entry) => entry.role === "user" && entry.text.trim() === text.trim()).map((entry) => entry.id)
|
|
295
|
+
});
|
|
296
|
+
const retryPending = () => {
|
|
297
|
+
if (!pending) return;
|
|
298
|
+
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
|
|
299
|
+
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
300
|
+
};
|
|
301
|
+
const editPending = () => {
|
|
302
|
+
if (!pending) return;
|
|
303
|
+
restoreSequence.current += 1;
|
|
304
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
|
|
305
|
+
setPending({ ...pending, status: "editing" });
|
|
306
|
+
};
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
const key = state.attached?.key;
|
|
309
|
+
if (!key || !state.attention.some((item) => item.key === key)) {
|
|
310
|
+
if (key) acknowledged.current.delete(key);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (!acknowledged.current.has(key)) {
|
|
314
|
+
acknowledged.current.add(key);
|
|
315
|
+
adapter.onIntent({ action: "ack", key });
|
|
316
|
+
}
|
|
317
|
+
}, [adapter, state.attached?.key, state.attention]);
|
|
318
|
+
const trackedAdapter = useMemo(() => ({
|
|
319
|
+
...adapter,
|
|
320
|
+
onIntent(intent) {
|
|
321
|
+
if (!TRACKED_ACTIONS.has(intent.action)) return adapter.onIntent(intent);
|
|
322
|
+
const id = actionSequence.current + 1;
|
|
323
|
+
actionSequence.current = id;
|
|
324
|
+
const action2 = intent.action;
|
|
325
|
+
setPendingAction({ id, action: action2, initialError: state.error, seenOperation: false });
|
|
326
|
+
let result;
|
|
327
|
+
try {
|
|
328
|
+
result = adapter.onIntent(intent);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
setPendingAction((current) => current?.id === id ? null : current);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
if (result && typeof result.then === "function") {
|
|
334
|
+
Promise.resolve(result).then(
|
|
335
|
+
() => setPendingAction((current) => current?.id === id ? null : current),
|
|
336
|
+
() => setPendingAction((current) => current?.id === id ? null : current)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
}), [adapter, state.error]);
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (!pendingAction) return;
|
|
344
|
+
if (state.operation && !pendingAction.seenOperation) {
|
|
345
|
+
setPendingAction({ ...pendingAction, seenOperation: true });
|
|
346
|
+
} else if (!state.operation && (pendingAction.seenOperation || state.error !== pendingAction.initialError)) {
|
|
347
|
+
setPendingAction(null);
|
|
348
|
+
}
|
|
349
|
+
}, [pendingAction, state.error, state.operation]);
|
|
350
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
351
|
+
const action = state.operation || pendingAction?.action || null;
|
|
352
|
+
const actionLabel = operationLabel(action);
|
|
353
|
+
const actionState = action ? { ...state, operation: action, canSteer: false, canInterrupt: false, canRespond: false } : state;
|
|
354
|
+
const unreadAfterMessages = state.attention.find((item) => item.key === state.attached?.key)?.afterMessages ?? null;
|
|
355
|
+
const Advisory = components.HarnessAdvisory ?? HarnessAdvisory;
|
|
356
|
+
const SettingsPanel = components.HarnessSettingsPanel ?? HarnessSettingsPanel;
|
|
357
|
+
return /* @__PURE__ */ jsxs("section", { className: "scui-chat", children: [
|
|
358
|
+
Header ? /* @__PURE__ */ jsx(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }), headerActions: HeaderActions }),
|
|
359
|
+
actionLabel ? /* @__PURE__ */ jsxs("div", { className: "scui-operation", role: "status", children: [
|
|
360
|
+
/* @__PURE__ */ jsx("i", {}),
|
|
361
|
+
actionLabel
|
|
362
|
+
] }) : null,
|
|
363
|
+
state.error ? /* @__PURE__ */ jsxs("div", { className: "scui-error", role: "alert", children: [
|
|
364
|
+
/* @__PURE__ */ jsx("span", { children: state.error }),
|
|
365
|
+
state.recoverable ? /* @__PURE__ */ jsx("button", { type: "button", disabled: Boolean(action), onClick: () => trackedAdapter.onIntent({ action: "refresh" }), children: "Retry" }) : null
|
|
366
|
+
] }) : null,
|
|
367
|
+
/* @__PURE__ */ jsx(Receipt, { state, adapter }),
|
|
368
|
+
/* @__PURE__ */ jsx(UniversalNouns, { state: actionState, adapter: trackedAdapter }),
|
|
369
|
+
/* @__PURE__ */ jsx(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
|
|
370
|
+
/* @__PURE__ */ jsx(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
|
|
371
|
+
/* @__PURE__ */ jsx(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
|
|
372
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, `composer:${memoryKey}`) : null,
|
|
373
|
+
settings ? /* @__PURE__ */ jsx(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
|
|
374
|
+
] });
|
|
375
|
+
}
|
|
376
|
+
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand, variant = "default", environmentLabel, suggestions = [], initialDraft = "", onDraftChange }) {
|
|
377
|
+
const startable = state.harnesses.filter((item) => item.startable);
|
|
378
|
+
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
379
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: initialDraft, context: [], images: [], modes: {} };
|
|
380
|
+
const [harness, setHarness] = useState(remembered.harness);
|
|
381
|
+
const [draft, setDraft] = useState(remembered.draft);
|
|
382
|
+
const [context, setContext] = useState(remembered.context);
|
|
383
|
+
const [images, setImages] = useState(remembered.images ?? []);
|
|
384
|
+
const [modes, setModes] = useState(remembered.modes ?? {});
|
|
385
|
+
const [starting, setStarting] = useState(null);
|
|
386
|
+
const [picking, setPicking] = useState(false);
|
|
387
|
+
const [dragging, setDragging] = useState(false);
|
|
388
|
+
const [pickerError, setPickerError] = useState(null);
|
|
389
|
+
const startSequence = useRef(0);
|
|
390
|
+
const lastComposerCommand = useRef(null);
|
|
391
|
+
const textarea = useRef(null);
|
|
392
|
+
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
393
|
+
const Picker = components.HarnessPicker ?? HarnessPicker;
|
|
394
|
+
const Readiness = components.HarnessReadiness ?? HarnessReadiness;
|
|
395
|
+
const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
|
|
396
|
+
const rememberedMode = modes[harness];
|
|
397
|
+
const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
|
|
398
|
+
const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
|
|
399
|
+
const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
|
|
400
|
+
const remember = (overrides = {}) => {
|
|
401
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
|
|
402
|
+
if (Object.hasOwn(overrides, "draft")) onDraftChange?.(overrides.draft);
|
|
403
|
+
};
|
|
404
|
+
useAutosizeTextarea(textarea, draft);
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
if (startable.some((item) => item.id === harness)) return;
|
|
407
|
+
const next = startable[0]?.id ?? "";
|
|
408
|
+
setHarness(next);
|
|
409
|
+
remember({ harness: next });
|
|
410
|
+
}, [harness, startableKey]);
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (!starting || starting.mode === "terminal" || starting.waitForResult || state.error) return;
|
|
413
|
+
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
414
|
+
if (!sessionChanged && !state.busy) return;
|
|
415
|
+
newChatMemory.delete(memoryKey);
|
|
416
|
+
onDraftChange?.("");
|
|
417
|
+
onStarted("headless");
|
|
418
|
+
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (starting && state.error && (!starting.waitForResult || state.error !== starting.initialError) && !state.busy && !state.operation) setStarting(null);
|
|
421
|
+
}, [starting, state.busy, state.error, state.operation]);
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
textarea.current?.focus({ preventScroll: true });
|
|
424
|
+
}, []);
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (!navigation) return;
|
|
427
|
+
const nextHarness = startable.some((item) => item.id === navigation.harness) ? navigation.harness : harness;
|
|
428
|
+
const nextDraft = typeof navigation.draft === "string" ? navigation.draft : draft;
|
|
429
|
+
const nextContext = mergeContext([], navigation.context ?? []);
|
|
430
|
+
const nextImages = mergeImages([], navigation.images ?? []);
|
|
431
|
+
setHarness(nextHarness);
|
|
432
|
+
setDraft(nextDraft);
|
|
433
|
+
setContext(nextContext);
|
|
434
|
+
setImages(nextImages);
|
|
435
|
+
remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
|
|
436
|
+
textarea.current?.focus({ preventScroll: true });
|
|
437
|
+
}, [navigation?.id]);
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
|
|
440
|
+
lastComposerCommand.current = composerCommand.id;
|
|
441
|
+
if (composerCommand.action === "attach") {
|
|
442
|
+
if (mode === "terminal" && !launchModes.includes("headless")) {
|
|
443
|
+
setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
|
|
444
|
+
} else {
|
|
445
|
+
try {
|
|
446
|
+
const attachments = partitionAttachments(composerCommand.attachments);
|
|
447
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
448
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
449
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
450
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
451
|
+
setContext(nextContext);
|
|
452
|
+
setImages(nextImages);
|
|
453
|
+
setPickerError(null);
|
|
454
|
+
remember({ context: nextContext, images: nextImages });
|
|
455
|
+
} catch (error) {
|
|
456
|
+
setPickerError(error instanceof Error ? error.message : "Could not attach context.");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
textarea.current?.focus({ preventScroll: true });
|
|
461
|
+
}, [composerCommand?.id]);
|
|
462
|
+
const pickContext = () => {
|
|
463
|
+
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
464
|
+
setPicking(true);
|
|
465
|
+
setPickerError(null);
|
|
466
|
+
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
467
|
+
const attachments = partitionAttachments(picked);
|
|
468
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
469
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
470
|
+
setContext((current) => {
|
|
471
|
+
const next = mergeContext(current, attachments.context);
|
|
472
|
+
remember({ context: next });
|
|
473
|
+
return next;
|
|
474
|
+
});
|
|
475
|
+
setImages((current) => {
|
|
476
|
+
const next = mergeImages(current, attachments.images);
|
|
477
|
+
remember({ images: next });
|
|
478
|
+
return next;
|
|
479
|
+
});
|
|
480
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
481
|
+
};
|
|
482
|
+
const attachCandidate = (picked) => {
|
|
483
|
+
if (mode === "terminal") return;
|
|
484
|
+
const attachments = partitionAttachments(picked);
|
|
485
|
+
const nextContext = mergeContext(context, attachments.context);
|
|
486
|
+
const nextImages = mergeImages(images, attachments.images);
|
|
487
|
+
setContext(nextContext);
|
|
488
|
+
setImages(nextImages);
|
|
489
|
+
remember({ context: nextContext, images: nextImages });
|
|
490
|
+
};
|
|
491
|
+
const addImageFiles = (value, source) => {
|
|
492
|
+
const allFiles = Array.from(value ?? []);
|
|
493
|
+
if (!allFiles.length) return false;
|
|
494
|
+
if (mode === "terminal") {
|
|
495
|
+
setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
499
|
+
if (files.length !== allFiles.length) {
|
|
500
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
504
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
setPicking(true);
|
|
508
|
+
setPickerError(null);
|
|
509
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
510
|
+
const next = mergeImages(current, picked);
|
|
511
|
+
remember({ images: next });
|
|
512
|
+
return next;
|
|
513
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
514
|
+
return true;
|
|
515
|
+
};
|
|
516
|
+
const pasteImages = (event) => {
|
|
517
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
518
|
+
};
|
|
519
|
+
const dropImages = (event) => {
|
|
520
|
+
setDragging(false);
|
|
521
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
522
|
+
};
|
|
523
|
+
const send = () => {
|
|
524
|
+
const text = draft.trim();
|
|
525
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
526
|
+
const id = startSequence.current + 1;
|
|
527
|
+
startSequence.current = id;
|
|
528
|
+
setStarting({ id, mode, waitForResult: true, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
529
|
+
setPickerError(null);
|
|
530
|
+
let result;
|
|
531
|
+
const failed = (error) => {
|
|
532
|
+
setStarting((current) => current?.id === id ? null : current);
|
|
533
|
+
setPickerError(error instanceof Error ? error.message : "Could not start the conversation. Your draft is kept.");
|
|
534
|
+
};
|
|
535
|
+
try {
|
|
536
|
+
result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
537
|
+
} catch (error) {
|
|
538
|
+
failed(error);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (mode === "terminal") {
|
|
542
|
+
Promise.resolve(result).then(() => {
|
|
543
|
+
newChatMemory.delete(memoryKey);
|
|
544
|
+
onDraftChange?.("");
|
|
545
|
+
onStarted("terminal");
|
|
546
|
+
}, failed);
|
|
547
|
+
} else if (result && typeof result.then === "function") {
|
|
548
|
+
Promise.resolve(result).then(() => setStarting((current) => current?.id === id ? { ...current, waitForResult: false } : current), failed);
|
|
549
|
+
} else {
|
|
550
|
+
setStarting((current) => current?.id === id ? { ...current, waitForResult: false } : current);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
return /* @__PURE__ */ jsxs("section", { className: `scui-chat${variant === "starter" ? " scui-starter" : ""}`, children: [
|
|
554
|
+
state.error ? /* @__PURE__ */ jsx("div", { className: "scui-error", role: "alert", children: state.error }) : null,
|
|
555
|
+
variant !== "starter" ? /* @__PURE__ */ jsxs("header", { className: "scui-head", children: [
|
|
556
|
+
/* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Back", onClick: onBack, children: /* @__PURE__ */ jsx(UiIcon, { name: "back", size: 18 }) }),
|
|
557
|
+
/* @__PURE__ */ jsxs("span", { className: "scui-head-copy", children: [
|
|
558
|
+
/* @__PURE__ */ jsx("strong", { children: labels.newChat }),
|
|
559
|
+
/* @__PURE__ */ jsx("small", { children: "No session is created until you send" })
|
|
560
|
+
] }),
|
|
561
|
+
HeaderActions ? /* @__PURE__ */ jsx(HeaderActions, { state, adapter, value: "new" }) : null,
|
|
562
|
+
onClose ? /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx(UiIcon, { name: "close", size: 18 }) }) : null
|
|
563
|
+
] }) : null,
|
|
564
|
+
starting ? /* @__PURE__ */ jsxs("div", { className: "scui-operation", role: "status", children: [
|
|
565
|
+
/* @__PURE__ */ jsx("i", {}),
|
|
566
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
567
|
+
] }) : null,
|
|
568
|
+
/* @__PURE__ */ jsxs("div", { className: `scui-new${variant === "starter" ? " scui-new-starter" : ""}`, children: [
|
|
569
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
570
|
+
/* @__PURE__ */ jsx("strong", { children: "What should the agent build or fix?" }),
|
|
571
|
+
/* @__PURE__ */ jsx("small", { children: "Choose a coding harness and send the first message." })
|
|
572
|
+
] }),
|
|
573
|
+
/* @__PURE__ */ jsxs("div", { className: "scui-compose", children: [
|
|
574
|
+
environmentLabel ? /* @__PURE__ */ jsx("span", { className: "scui-environment", children: environmentLabel }) : null,
|
|
575
|
+
/* @__PURE__ */ jsx(Readiness, { harnesses: state.harnesses, state, adapter }),
|
|
576
|
+
mode !== "terminal" ? /* @__PURE__ */ jsx(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
|
|
577
|
+
/* @__PURE__ */ jsx(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
578
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
579
|
+
remember({ images: next });
|
|
580
|
+
return next;
|
|
581
|
+
}) }),
|
|
582
|
+
/* @__PURE__ */ jsx(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
583
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
584
|
+
remember({ context: next });
|
|
585
|
+
return next;
|
|
586
|
+
}) }),
|
|
587
|
+
terminalAttachments ? /* @__PURE__ */ jsx("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Headless." }) : pickerError ? /* @__PURE__ */ jsx("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
588
|
+
/* @__PURE__ */ jsxs("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
589
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
590
|
+
event.preventDefault();
|
|
591
|
+
setDragging(true);
|
|
592
|
+
}
|
|
593
|
+
}, onDragOver: (event) => {
|
|
594
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
595
|
+
}, onDragLeave: (event) => {
|
|
596
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
597
|
+
}, onDrop: dropImages, children: [
|
|
598
|
+
/* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
599
|
+
const value = event.currentTarget.value;
|
|
600
|
+
setDraft(value);
|
|
601
|
+
remember({ draft: value });
|
|
602
|
+
}, onKeyDown: (event) => {
|
|
603
|
+
if (isSendKey(event)) {
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
send();
|
|
606
|
+
}
|
|
607
|
+
} }),
|
|
608
|
+
/* @__PURE__ */ jsxs("footer", { className: "scui-new-envelope-controls", children: [
|
|
609
|
+
adapter.pickContext ? /* @__PURE__ */ jsx("button", { className: "scui-attach", type: "button", "aria-label": labels.attachContext ?? DEFAULT_LABELS.attachContext, disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
610
|
+
/* @__PURE__ */ jsx(Picker, { harnesses: state.harnesses, state, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
|
|
611
|
+
setHarness(value);
|
|
612
|
+
remember({ harness: value });
|
|
613
|
+
} }),
|
|
614
|
+
/* @__PURE__ */ jsx(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
|
|
615
|
+
const next = { ...modes, [harness]: item };
|
|
616
|
+
setModes(next);
|
|
617
|
+
setPickerError(null);
|
|
618
|
+
remember({ modes: next });
|
|
619
|
+
} }),
|
|
620
|
+
/* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start headless session", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx(UiIcon, { name: "send", size: 17 }) }) })
|
|
621
|
+
] })
|
|
622
|
+
] }),
|
|
623
|
+
suggestions.length ? /* @__PURE__ */ jsx("div", { className: "scui-starter-suggestions", children: suggestions.map((text) => /* @__PURE__ */ jsx("button", { type: "button", disabled: Boolean(starting), onClick: () => {
|
|
624
|
+
setDraft(text);
|
|
625
|
+
remember({ draft: text });
|
|
626
|
+
textarea.current?.focus();
|
|
627
|
+
}, children: text }, text)) }) : null
|
|
628
|
+
] })
|
|
629
|
+
] });
|
|
630
|
+
}
|
|
631
|
+
function useMessengerModel({ state: stateInput, adapter, initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
|
|
632
|
+
const state = useMemo(() => normalizeUiState(stateInput), [stateInput]);
|
|
633
|
+
const contextCandidates = useMemo(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
|
|
634
|
+
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
635
|
+
const memoryKey = state.workspace || "@default";
|
|
636
|
+
const [view, setViewState] = useState(initialView ?? (state.attention.length ? "list" : messengerViewMemory.get(memoryKey) ?? (state.attached || state.transcript.length ? "chat" : "list")));
|
|
637
|
+
const [opening, setOpening] = useState(null);
|
|
638
|
+
const [newNavigation, setNewNavigation] = useState(null);
|
|
639
|
+
const [chatNavigation, setChatNavigation] = useState(null);
|
|
640
|
+
const [listFocus, setListFocus] = useState(null);
|
|
641
|
+
const [inspecting, setInspecting] = useState(null);
|
|
642
|
+
const lastNavigation = useRef(null);
|
|
643
|
+
const setView = (next) => {
|
|
644
|
+
boundedSet(messengerViewMemory, memoryKey, next);
|
|
645
|
+
setViewState(next);
|
|
646
|
+
onViewChange?.(next);
|
|
647
|
+
};
|
|
648
|
+
useEffect(() => {
|
|
649
|
+
if (!navigation || navigation.id === lastNavigation.current) return;
|
|
650
|
+
lastNavigation.current = navigation.id;
|
|
651
|
+
if (navigation.view === "list") {
|
|
652
|
+
setOpening(null);
|
|
653
|
+
setChatNavigation(null);
|
|
654
|
+
setView("list");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (navigation.view === "new") {
|
|
658
|
+
setListFocus("@new");
|
|
659
|
+
setChatNavigation(null);
|
|
660
|
+
setNewNavigation(navigation);
|
|
661
|
+
setView("new");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (navigation.view === "chat" && typeof navigation.sessionKey === "string" && navigation.sessionKey) {
|
|
665
|
+
setListFocus(navigation.sessionKey);
|
|
666
|
+
setNewNavigation(null);
|
|
667
|
+
setChatNavigation(navigation);
|
|
668
|
+
if (state.attached?.key === navigation.sessionKey) {
|
|
669
|
+
setOpening(null);
|
|
670
|
+
setView("chat");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const row = state.sessions.find((item) => item.key === navigation.sessionKey);
|
|
674
|
+
setOpening(row ?? { key: navigation.sessionKey, harness: "supercode", name: "Chat", title: "Chat" });
|
|
675
|
+
adapter.onIntent({ action: "attach", key: navigation.sessionKey });
|
|
676
|
+
}
|
|
677
|
+
}, [adapter, navigation, state.attached?.key, state.sessions]);
|
|
678
|
+
useEffect(() => {
|
|
679
|
+
if (!opening) return;
|
|
680
|
+
if (state.attached?.key === opening.key) {
|
|
681
|
+
setOpening(null);
|
|
682
|
+
setView("chat");
|
|
683
|
+
} else if (state.attachError?.key === opening.key || state.error && !state.operation) {
|
|
684
|
+
setOpening(null);
|
|
685
|
+
}
|
|
686
|
+
}, [opening, state.attachError?.key, state.attached?.key, state.error, state.operation]);
|
|
687
|
+
const dismissInspector = () => {
|
|
688
|
+
if (inspecting || state.subagentInspector) {
|
|
689
|
+
setInspecting(null);
|
|
690
|
+
adapter.onIntent({ action: "closeSubagents" });
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
const open = (row) => {
|
|
694
|
+
dismissInspector();
|
|
695
|
+
setListFocus(row.key);
|
|
696
|
+
setChatNavigation(null);
|
|
697
|
+
setOpening(row);
|
|
698
|
+
adapter.onIntent({ action: "attach", key: row.key });
|
|
699
|
+
};
|
|
700
|
+
const openSubagents = (row) => {
|
|
701
|
+
setListFocus(row.key);
|
|
702
|
+
setInspecting(row);
|
|
703
|
+
adapter.onIntent({ action: "openSubagents", key: row.key });
|
|
704
|
+
};
|
|
705
|
+
const close = () => adapter.onClose?.();
|
|
706
|
+
const inspectorParent = inspecting ?? (state.subagentInspector ? state.sessions.find((row) => row.key === state.subagentInspector.parentKey) ?? { key: state.subagentInspector.parentKey, title: state.subagentInspector.parentTitle } : null);
|
|
707
|
+
const inspectorState = inspectorParent ? {
|
|
708
|
+
...state,
|
|
709
|
+
subagentInspector: state.subagentInspector?.parentKey === inspectorParent.key ? state.subagentInspector : { parentKey: inspectorParent.key, parentTitle: sessionDisplayName(inspectorParent), status: state.error ? "error" : "loading", items: [], selectedKey: null, transcript: [], error: state.error }
|
|
710
|
+
} : null;
|
|
711
|
+
const newConversation = () => {
|
|
712
|
+
dismissInspector();
|
|
713
|
+
setListFocus("@new");
|
|
714
|
+
setChatNavigation(null);
|
|
715
|
+
setNewNavigation(null);
|
|
716
|
+
setView("new");
|
|
717
|
+
};
|
|
718
|
+
const back = () => {
|
|
719
|
+
setListFocus(state.attached?.key ?? listFocus);
|
|
720
|
+
setView("list");
|
|
721
|
+
};
|
|
722
|
+
return {
|
|
723
|
+
state,
|
|
724
|
+
adapter,
|
|
725
|
+
view,
|
|
726
|
+
setView,
|
|
727
|
+
open,
|
|
728
|
+
openSubagents,
|
|
729
|
+
newConversation,
|
|
730
|
+
back,
|
|
731
|
+
close,
|
|
732
|
+
opening,
|
|
733
|
+
listFocus,
|
|
734
|
+
memoryKey,
|
|
735
|
+
inspectorState,
|
|
736
|
+
closeInspector: () => {
|
|
737
|
+
setInspecting(null);
|
|
738
|
+
adapter.onIntent({ action: "closeSubagents" });
|
|
739
|
+
},
|
|
740
|
+
newNavigation,
|
|
741
|
+
chatNavigation,
|
|
742
|
+
composerCommand,
|
|
743
|
+
contextCandidates,
|
|
744
|
+
labels: copy,
|
|
745
|
+
components,
|
|
746
|
+
slots
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
var MessengerContext = createContext(null);
|
|
750
|
+
function MessengerProvider({ children, ...props }) {
|
|
751
|
+
const model = useMessengerModel(props);
|
|
752
|
+
return /* @__PURE__ */ jsx(MessengerContext.Provider, { value: model, children });
|
|
753
|
+
}
|
|
754
|
+
function useMessenger() {
|
|
755
|
+
const model = useContext(MessengerContext);
|
|
756
|
+
if (!model) throw new Error("Messenger components require a MessengerProvider.");
|
|
757
|
+
return model;
|
|
758
|
+
}
|
|
759
|
+
function MessengerSessions() {
|
|
760
|
+
const m = useMessenger();
|
|
761
|
+
return /* @__PURE__ */ jsx(
|
|
762
|
+
SessionList,
|
|
763
|
+
{
|
|
764
|
+
state: m.state,
|
|
765
|
+
adapter: m.adapter,
|
|
766
|
+
focusKey: m.listFocus,
|
|
767
|
+
onOpen: m.open,
|
|
768
|
+
onOpenSubagents: m.openSubagents,
|
|
769
|
+
onNew: m.newConversation,
|
|
770
|
+
onClose: m.adapter.onClose ? m.close : void 0,
|
|
771
|
+
components: m.components,
|
|
772
|
+
labels: m.labels,
|
|
773
|
+
memoryKey: m.memoryKey,
|
|
774
|
+
slots: m.slots,
|
|
775
|
+
headerActions: m.slots.headerActions
|
|
776
|
+
}
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function MessengerContent({ starter = {}, showBack = true } = {}) {
|
|
780
|
+
const m = useMessenger();
|
|
781
|
+
const common = {
|
|
782
|
+
state: m.state,
|
|
783
|
+
adapter: m.adapter,
|
|
784
|
+
components: m.components,
|
|
785
|
+
labels: m.labels,
|
|
786
|
+
contextCandidates: m.contextCandidates,
|
|
787
|
+
composerCommand: m.composerCommand,
|
|
788
|
+
onClose: m.adapter.onClose ? m.close : void 0,
|
|
789
|
+
onBack: showBack ? m.back : void 0
|
|
790
|
+
};
|
|
791
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
792
|
+
m.inspectorState ? /* @__PURE__ */ jsx(SubagentInspector, { state: m.inspectorState, adapter: m.adapter, onClose: m.closeInspector }) : m.view === "new" || m.view === "list" && !m.state.attached && !m.state.transcript.length ? /* @__PURE__ */ jsx(
|
|
793
|
+
NewChat,
|
|
794
|
+
{
|
|
795
|
+
...common,
|
|
796
|
+
...starter,
|
|
797
|
+
memoryKey: m.memoryKey,
|
|
798
|
+
headerActions: m.slots.headerActions,
|
|
799
|
+
navigation: m.newNavigation,
|
|
800
|
+
onStarted: (mode) => m.setView(mode === "terminal" ? "list" : "chat")
|
|
801
|
+
}
|
|
802
|
+
) : /* @__PURE__ */ jsx(
|
|
803
|
+
Chat,
|
|
804
|
+
{
|
|
805
|
+
...common,
|
|
806
|
+
slots: m.slots,
|
|
807
|
+
onNew: m.newConversation,
|
|
808
|
+
navigation: m.chatNavigation?.sessionKey === m.state.attached?.key ? m.chatNavigation : null
|
|
809
|
+
}
|
|
810
|
+
),
|
|
811
|
+
m.opening ? /* @__PURE__ */ jsxs("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
812
|
+
/* @__PURE__ */ jsx(HarnessLogo, { id: m.opening.harness, size: 34 }),
|
|
813
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
814
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
815
|
+
"Opening ",
|
|
816
|
+
sessionDisplayName(m.opening)
|
|
817
|
+
] }),
|
|
818
|
+
/* @__PURE__ */ jsx("small", { children: "Loading the latest transcript window\u2026" })
|
|
819
|
+
] }),
|
|
820
|
+
/* @__PURE__ */ jsx("i", {})
|
|
821
|
+
] }) : null
|
|
822
|
+
] });
|
|
823
|
+
}
|
|
824
|
+
function MessengerSurface({ class: className = "", starter }) {
|
|
825
|
+
const m = useMessenger();
|
|
826
|
+
const Footer = m.slots.footer;
|
|
827
|
+
return /* @__PURE__ */ jsxs("main", { className: `scui-root ${className}`, "data-view": m.inspectorState ? "agents" : m.view, "data-mode": m.state.mode, "aria-label": "Supercode messenger", children: [
|
|
828
|
+
m.view === "list" && !m.inspectorState ? /* @__PURE__ */ jsx(MessengerSessions, {}) : /* @__PURE__ */ jsx(MessengerContent, { starter }),
|
|
829
|
+
Footer ? /* @__PURE__ */ jsx(Footer, { state: m.state, adapter: m.adapter, value: m.labels }) : null
|
|
830
|
+
] });
|
|
831
|
+
}
|
|
832
|
+
function SupercodeMessenger(props) {
|
|
833
|
+
return /* @__PURE__ */ jsx(MessengerProvider, { ...props, children: /* @__PURE__ */ jsx(MessengerSurface, { class: props.class, starter: props.starter }) });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export {
|
|
837
|
+
MessengerProvider,
|
|
838
|
+
useMessenger,
|
|
839
|
+
MessengerSessions,
|
|
840
|
+
MessengerContent,
|
|
841
|
+
SupercodeMessenger
|
|
842
|
+
};
|