@fungi.computer/parakeet 0.1.0 → 0.2.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/README.md +45 -6
- package/dist/account-commands.js +3 -11
- package/dist/commands/advertised.d.ts +3 -1
- package/dist/commands/advertised.js +89 -4
- package/dist/commands/interactions.d.ts +22 -7
- package/dist/commands/interactions.js +41 -17
- package/dist/commands/slash.js +3 -5
- package/dist/commands/use-slash-commands.js +2 -2
- package/dist/components/AgentHead.d.ts +2 -0
- package/dist/components/AgentHead.js +5 -4
- package/dist/components/ChatComposer.d.ts +3 -1
- package/dist/components/ChatComposer.js +79 -15
- package/dist/components/ChatMessage.d.ts +4 -0
- package/dist/components/ChatMessage.js +6 -2
- package/dist/components/ClientRows.js +2 -2
- package/dist/components/InteractionPanel.d.ts +2 -4
- package/dist/components/InteractionPanel.js +2 -2
- package/dist/components/TimelineItem.d.ts +2 -0
- package/dist/components/TimelineItem.js +31 -7
- package/dist/conversation-opening.d.ts +47 -0
- package/dist/conversation-opening.js +173 -0
- package/dist/conversation.d.ts +47 -3
- package/dist/conversation.js +584 -86
- package/dist/core/author-labels.d.ts +2 -0
- package/dist/core/build-timeline.d.ts +4 -2
- package/dist/core/build-timeline.js +46 -7
- package/dist/core/client-actions.js +6 -3
- package/dist/core/client-timeline.d.ts +9 -2
- package/dist/core/client-timeline.js +15 -4
- package/dist/core/draft-storage.d.ts +63 -0
- package/dist/core/draft-storage.js +50 -0
- package/dist/core/model-identity.d.ts +2 -0
- package/dist/core/model-identity.js +6 -0
- package/dist/core/queued-recall.d.ts +35 -0
- package/dist/core/queued-recall.js +138 -0
- package/dist/core/session-projection.js +17 -0
- package/dist/core/session-query.d.ts +54 -11
- package/dist/core/session-query.js +38 -12
- package/dist/core/tool-groups.d.ts +1 -1
- package/dist/core/tool-groups.js +3 -1
- package/dist/core/tool-view.js +2 -1
- package/dist/core/use-timeline-item-key.d.ts +8 -0
- package/dist/core/use-timeline-item-key.js +29 -0
- package/dist/headless.d.ts +6 -4
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/model-commands.js +64 -39
- package/dist/parakeet.js +17 -18
- package/dist/session-commands.d.ts +0 -9
- package/dist/session-commands.js +39 -68
- package/dist/session.d.ts +1 -1
- package/dist/store/types.d.ts +10 -4
- package/dist/styles.css +27 -12
- package/docs/api/commands/slash.md +24 -21
- package/docs/api/headless.md +375 -232
- package/docs/api/index.md +157 -45
- package/package.json +16 -10
- package/src/styles.css +0 -9
|
@@ -55,11 +55,21 @@ const presentations = {
|
|
|
55
55
|
tone: "neutral",
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
|
-
function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, slashListId, textareaId, textareaRef, value, }) {
|
|
58
|
+
function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, slashListId, textareaId, textareaRef, value, queuedRecall, }) {
|
|
59
59
|
const latestValue = useRef(value);
|
|
60
60
|
const [history, setHistory] = useState([]);
|
|
61
61
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
62
62
|
latestValue.current = value;
|
|
63
|
+
const recallState = queuedRecall?.recallState;
|
|
64
|
+
const recalling = recallState?.kind === "recalling";
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
if (recallState?.kind !== "restored")
|
|
67
|
+
return;
|
|
68
|
+
setHistoryIndex(-1);
|
|
69
|
+
const textarea = textareaRef.current;
|
|
70
|
+
if (textarea !== null)
|
|
71
|
+
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
72
|
+
}, [recallState, textareaRef]);
|
|
63
73
|
useLayoutEffect(() => {
|
|
64
74
|
const textarea = textareaRef.current;
|
|
65
75
|
if (!textarea)
|
|
@@ -69,15 +79,18 @@ function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, s
|
|
|
69
79
|
}, [textareaRef, value]);
|
|
70
80
|
const saveAndSend = useCallback(() => {
|
|
71
81
|
const draft = latestValue.current.trim();
|
|
72
|
-
if (!draft)
|
|
82
|
+
if (!draft && (queuedRecall?.images?.length ?? 0) === 0)
|
|
73
83
|
return;
|
|
74
84
|
onSend();
|
|
75
|
-
|
|
85
|
+
if (draft)
|
|
86
|
+
setHistory((previous) => previous.at(-1) === draft ? previous : [...previous, draft]);
|
|
76
87
|
setHistoryIndex(-1);
|
|
77
|
-
}, [onSend]);
|
|
88
|
+
}, [onSend, queuedRecall?.images?.length]);
|
|
78
89
|
const onKeyDown = useCallback((event) => {
|
|
79
90
|
if (isImeComposing(event))
|
|
80
91
|
return;
|
|
92
|
+
if (recalling)
|
|
93
|
+
return;
|
|
81
94
|
if (slash?.open === true) {
|
|
82
95
|
const handled = event.key === "ArrowDown"
|
|
83
96
|
? slash.move(1)
|
|
@@ -98,6 +111,35 @@ function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, s
|
|
|
98
111
|
onAbort();
|
|
99
112
|
return;
|
|
100
113
|
}
|
|
114
|
+
if (queuedRecall !== undefined &&
|
|
115
|
+
event.key === "ArrowUp" &&
|
|
116
|
+
!event.repeat &&
|
|
117
|
+
!event.altKey &&
|
|
118
|
+
!event.ctrlKey &&
|
|
119
|
+
!event.metaKey &&
|
|
120
|
+
!event.shiftKey &&
|
|
121
|
+
slash?.open !== true &&
|
|
122
|
+
latestValue.current === "" &&
|
|
123
|
+
(queuedRecall.images?.length ?? 0) === 0 &&
|
|
124
|
+
document.activeElement === event.currentTarget) {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
const input = event.currentTarget;
|
|
127
|
+
void queuedRecall.recall().then((outcome) => {
|
|
128
|
+
if (outcome !== "empty" ||
|
|
129
|
+
!input.isConnected ||
|
|
130
|
+
input.value !== "" ||
|
|
131
|
+
textareaRef.current !== input ||
|
|
132
|
+
history.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const next = historyIndex === -1 ? history.length - 1 : historyIndex - 1;
|
|
135
|
+
const entry = history[next];
|
|
136
|
+
if (entry !== undefined) {
|
|
137
|
+
setHistoryIndex(next);
|
|
138
|
+
onChange(entry);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
101
143
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
102
144
|
event.preventDefault();
|
|
103
145
|
saveAndSend();
|
|
@@ -129,19 +171,36 @@ function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, s
|
|
|
129
171
|
onChange(entry);
|
|
130
172
|
}
|
|
131
173
|
}
|
|
132
|
-
}, [
|
|
174
|
+
}, [
|
|
175
|
+
history,
|
|
176
|
+
historyIndex,
|
|
177
|
+
onAbort,
|
|
178
|
+
onChange,
|
|
179
|
+
saveAndSend,
|
|
180
|
+
slash,
|
|
181
|
+
textareaRef,
|
|
182
|
+
queuedRecall,
|
|
183
|
+
recalling,
|
|
184
|
+
]);
|
|
133
185
|
const listOpen = slash?.open === true && slash.completion.kind !== "none"
|
|
134
186
|
? slash.completion.items.length > 0
|
|
135
187
|
: false;
|
|
136
|
-
return (_jsxs("div", { className: "relative min-w-0 flex-1", children: [_jsx("label", { className: "sr-only", htmlFor: textareaId, children: "Message" }), _jsx(Textarea, { "aria-
|
|
188
|
+
return (_jsxs("div", { className: "relative min-w-0 flex-1", children: [_jsx("label", { className: "sr-only", htmlFor: textareaId, children: "Message" }), _jsx(Textarea, { "aria-describedby": [
|
|
189
|
+
slash === undefined ? undefined : `${textareaId}-guidance`,
|
|
190
|
+
queuedRecall === undefined
|
|
191
|
+
? undefined
|
|
192
|
+
: `${textareaId}-recall-guidance`,
|
|
193
|
+
]
|
|
194
|
+
.filter((id) => id !== undefined)
|
|
195
|
+
.join(" ") || undefined, "aria-busy": recalling, readOnly: recalling, "aria-autocomplete": "list", "aria-controls": listOpen ? slashListId : undefined, "aria-expanded": listOpen, "aria-activedescendant": listOpen && slash !== undefined
|
|
137
196
|
? slashOptionId(slashListId, slash.selected)
|
|
138
197
|
: undefined, className: "join-item min-h-[48px] max-h-[320px] w-full resize-none overflow-x-hidden overflow-y-auto whitespace-pre-wrap break-words px-3 py-[calc((3rem-1.25rem)/2)] font-sans text-xs text-base-content !border-0 !bg-transparent !shadow-none focus:!outline-none", id: textareaId, onChange: (event) => {
|
|
139
198
|
onChange(event.target.value);
|
|
140
199
|
setHistoryIndex(-1);
|
|
141
200
|
}, onKeyDown: onKeyDown, placeholder: placeholder ??
|
|
142
|
-
(slash === undefined ? "Ask agent..." : "Ask agent… (/ for commands)"), role: slash === undefined ? undefined : "combobox", ref: textareaRef, rows: 1, value: value })] }));
|
|
201
|
+
(slash === undefined ? "Ask agent..." : "Ask agent… (/ for commands)"), role: slash === undefined ? undefined : "combobox", ref: textareaRef, rows: 1, value: value }), queuedRecall === undefined ? null : (_jsx("span", { id: `${textareaId}-recall-guidance`, className: "sr-only", children: "Press Up on an empty composer to recall your newest queued message." })), (queuedRecall?.images?.length ?? 0) === 0 ? null : (_jsxs("p", { className: "text-xs", children: [queuedRecall?.images?.length, " attached image(s)"] }))] }));
|
|
143
202
|
}
|
|
144
|
-
/**
|
|
203
|
+
/** Full Session readout for announcements; the footer only draws attention states. */
|
|
145
204
|
export function sessionReadout(status, view) {
|
|
146
205
|
if (view.kind === "connecting")
|
|
147
206
|
return "connecting";
|
|
@@ -179,19 +238,24 @@ export function sessionReadout(status, view) {
|
|
|
179
238
|
}
|
|
180
239
|
function ComposerFooter({ statusChips, model, status, viewState, usage, context, }) {
|
|
181
240
|
const tone = viewState.kind === "error" ? "error" : presentations[status].tone;
|
|
182
|
-
|
|
183
|
-
|
|
241
|
+
// Admission must not replace an outstanding owner attention readout.
|
|
242
|
+
const label = sessionReadout("idle", viewState);
|
|
243
|
+
const needsAttention = viewState.kind === "waiting_for_you" || viewState.kind === "error";
|
|
244
|
+
return (_jsxs("div", { className: "composer-footer", children: [needsAttention ? (_jsx(Status, { label: label, title: label, tone: tone })) : null, statusChips?.map((chip, index) => (_jsx("span", { className: "min-w-0 truncate", title: chip, children: chip }, index))), model ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "footer-divider" }), _jsx("span", { className: "font-mono font-semibold", children: formatModelIdentity(model) })] })) : null, context ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "footer-divider" }), _jsx("span", { className: "font-mono", title: "Context used / model context", children: formatContextUsage(context) })] })) : null, usage ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "footer-divider" }), _jsx("span", { className: "font-mono", children: formatTokens(usage.tokens) }), _jsx("span", { className: "footer-divider" }), _jsx("span", { className: "font-mono", children: formatMoney(usage.cost) })] })) : null] }));
|
|
184
245
|
}
|
|
185
|
-
export const ChatComposer = memo(function ChatComposer({ placeholder, showFooter = true, statusChips, actionVariant = "primary", draft, failure, failureCause, admissionError, model, onAbort, onChange, onRetry, onSend, status, viewState, textareaRef, usage, context, slash, }) {
|
|
246
|
+
export const ChatComposer = memo(function ChatComposer({ queuedRecall, placeholder, showFooter = true, statusChips, actionVariant = "primary", draft, failure, failureCause, admissionError, model, onAbort, onChange, onRetry, onSend, status, viewState, textareaRef, usage, context, slash, }) {
|
|
186
247
|
const textareaId = useId();
|
|
187
248
|
const slashListId = `${textareaId}-commands`;
|
|
188
249
|
const { busy } = presentations[status];
|
|
189
|
-
const
|
|
250
|
+
const recallBusy = queuedRecall?.recallState.kind === "recalling";
|
|
251
|
+
const cannotSend = recallBusy ||
|
|
252
|
+
(draft.trim().length === 0 && (queuedRecall?.images?.length ?? 0) === 0);
|
|
253
|
+
const composerAction = busy ? (_jsxs("div", { className: "flex items-stretch", children: [_jsx(Button, { "aria-label": "Stop current turn", className: "join-item h-auto min-h-0 self-stretch", "data-testid": "composer-stop", disabled: recallBusy || status === "queued" || status === "cancelling", onClick: onAbort, title: "Stop the current turn. Queued messages keep running in order.", size: "lg", variant: "ghost", children: "Stop" }), _jsx(Button, { "aria-label": "Queue follow-up", className: "join-item h-auto min-h-0 self-stretch", disabled: cannotSend, onClick: () => {
|
|
190
254
|
onSend("followUp");
|
|
191
|
-
}, size: "lg", variant: actionVariant, children: "Queue" })] })) : (_jsx(Button, { "aria-label": "Send message", className: "join-item h-auto min-h-0 self-stretch", disabled:
|
|
255
|
+
}, size: "lg", variant: actionVariant, children: "Queue" })] })) : (_jsx(Button, { "aria-label": "Send message", className: "join-item h-auto min-h-0 self-stretch", disabled: cannotSend, onClick: () => {
|
|
192
256
|
onSend("steer");
|
|
193
257
|
}, title: "Send", size: "lg", variant: actionVariant, children: "Send" }));
|
|
194
|
-
const composerInput = (_jsx(HostComposerTextarea, { placeholder: placeholder, onChange: onChange, onAbort: onAbort, onSend: () => {
|
|
258
|
+
const composerInput = (_jsx(HostComposerTextarea, { queuedRecall: queuedRecall, placeholder: placeholder, onChange: onChange, onAbort: onAbort, onSend: () => {
|
|
195
259
|
onSend(busy ? "followUp" : "steer");
|
|
196
260
|
}, slash: slash, slashListId: slashListId, textareaId: textareaId, textareaRef: textareaRef, value: draft }));
|
|
197
261
|
const composerProps = {
|
|
@@ -205,5 +269,5 @@ export const ChatComposer = memo(function ChatComposer({ placeholder, showFooter
|
|
|
205
269
|
footerProps.usage = usage;
|
|
206
270
|
if (context)
|
|
207
271
|
footerProps.context = context;
|
|
208
|
-
return (_jsxs("div", { className: "chat-composer not-prose flex-shrink-0 border-t border-base-300", children: [_jsxs("div", { className: "relative", children: [admissionError === "queue_full" ? (_jsx("div", { className: "px-3 py-1.5 text-xs text-error", role: "alert", children: "Too many prompts are still awaiting admission. Try again shortly." })) : null, failure !== undefined ? (_jsx(RunFailureNotice, { failure: failure, cause: failureCause, onRetry: onRetry })) : null, slash?.interactions.notice === undefined ? null : (_jsx("div", { className: "parakeet-command-notice px-3 py-1.5 text-xs", "data-tone": slash.interactions.notice.tone, role: slash.interactions.notice.tone === "error" ? "alert" : "status", children: slash.interactions.notice.message })), _jsxs(PopoverAnchor, { className: "composer-body", children: [slash === undefined ? null : (_jsx(Popover, { open: slash.open, side: "top", "data-parakeet-slash": "", children: _jsx(SlashMenu, { slash: slash, listId: slashListId }) })), _jsx(Composer, { ...composerProps })] })] }), showFooter ? _jsx(ComposerFooter, { ...footerProps }) : null, slash === undefined ? null : (_jsx("p", { className: "
|
|
272
|
+
return (_jsxs("div", { className: "chat-composer not-prose flex-shrink-0 border-t border-base-300", children: [_jsxs("div", { className: "relative", children: [admissionError === "queue_full" ? (_jsx("div", { className: "px-3 py-1.5 text-xs text-error", role: "alert", children: "Too many prompts are still awaiting admission. Try again shortly." })) : null, failure !== undefined ? (_jsx(RunFailureNotice, { failure: failure, cause: failureCause, onRetry: onRetry })) : null, slash?.interactions.notice === undefined ? null : (_jsx("div", { className: "parakeet-command-notice px-3 py-1.5 text-xs", "data-tone": slash.interactions.notice.tone, role: slash.interactions.notice.tone === "error" ? "alert" : "status", children: slash.interactions.notice.message })), _jsxs(PopoverAnchor, { className: "composer-body", children: [slash === undefined ? null : (_jsx(Popover, { open: slash.open, side: "top", "data-parakeet-slash": "", children: _jsx(SlashMenu, { slash: slash, listId: slashListId }) })), _jsx(Composer, { ...composerProps })] })] }), showFooter ? _jsx(ComposerFooter, { ...footerProps }) : null, slash === undefined ? null : (_jsx("p", { className: "sr-only", id: `${textareaId}-guidance`, children: formatHints(slash.hints) }))] }));
|
|
209
273
|
});
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** User-message admission state, shared by full and compact transcript rows. */
|
|
2
|
+
export declare function PendingMessageStatus({ pending, }: Readonly<{
|
|
3
|
+
pending: boolean | undefined;
|
|
4
|
+
}>): import("react/jsx-runtime").JSX.Element | null;
|
|
1
5
|
export declare function ChatMessage({ children, isError, pending, speaker, thinking, markdownPolicy, }: Readonly<{
|
|
2
6
|
children: string;
|
|
3
7
|
isError?: boolean;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
3
3
|
import { ThinkingBlock } from "./ThinkingBlock.js";
|
|
4
|
+
/** User-message admission state, shared by full and compact transcript rows. */
|
|
5
|
+
export function PendingMessageStatus({ pending, }) {
|
|
6
|
+
return pending ? (_jsx("span", { className: "sr-only", role: "status", children: "Sending" })) : null;
|
|
7
|
+
}
|
|
4
8
|
export function ChatMessage({ children, isError, pending, speaker, thinking, markdownPolicy, }) {
|
|
5
9
|
const content = (_jsx(MarkdownRenderer, { policy: markdownPolicy, children: children }));
|
|
6
10
|
if (speaker === "user") {
|
|
7
|
-
return (
|
|
11
|
+
return (_jsxs("div", { className: `px-3 py-2 transition-opacity motion-reduce:transition-none ${pending ? "opacity-50" : ""}`, "data-pending": pending ? "true" : undefined, "data-speaker": "user", style: {
|
|
8
12
|
background: "var(--stipe-surface)",
|
|
9
13
|
borderLeft: "3px solid var(--color-primary)",
|
|
10
|
-
}, children: _jsx("div", { className: "message-body text-base-content", children: content }) }));
|
|
14
|
+
}, children: [_jsx(PendingMessageStatus, { pending: pending }), _jsx("div", { className: "message-body text-base-content", children: content })] }));
|
|
11
15
|
}
|
|
12
16
|
const hasText = children.trim().length > 0;
|
|
13
17
|
if (!hasText && !thinking)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
3
3
|
import { DataList, DataRow, DataListHeading, } from "@fungi.computer/caps/components/data-list";
|
|
4
4
|
import { Status } from "@fungi.computer/caps/components/status";
|
|
@@ -17,7 +17,7 @@ export function ClientRows({ entryId, rows, actions, onOpen, }) {
|
|
|
17
17
|
const { inspection } = useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
|
|
18
18
|
if (rows.length === 0)
|
|
19
19
|
return null;
|
|
20
|
-
return (_jsx("div", { className: "px-3 text-xs text-base-content", "data-client-entry": entryId, children: rows.map((row) => (_jsxs("div", { "data-client-row": row.id, className: "py-1", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsxs("details", { className: "min-w-0 flex-1", children: [_jsxs("summary", { className: `cursor-pointer break-words leading-4 ${row.tone === undefined ? "" : rowColors[row.tone]}`, children: [row.title, row.inspection === undefined || row.detail === undefined
|
|
20
|
+
return (_jsx("div", { className: "px-3 text-xs text-base-content", "data-client-entry": entryId, children: rows.map((row) => (_jsxs("div", { "data-client-row": row.id, className: "py-1", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("span", { "aria-hidden": "true", "data-client-row-dot": "", className: `w-3 shrink-0 text-center leading-4 ${row.tone === undefined ? "" : rowColors[row.tone]}`, children: row.tone === undefined ? "" : "●" }), _jsxs("details", { className: "min-w-0 flex-1", children: [_jsxs("summary", { className: `cursor-pointer break-words leading-4 ${row.tone === undefined ? "" : rowColors[row.tone]}`, children: [row.title, row.inspection === undefined || row.detail === undefined
|
|
21
21
|
? null
|
|
22
22
|
: ` · ${row.detail}`] }), row.detail === undefined ? null : (_jsx("p", { className: "max-h-32 overflow-y-auto overscroll-contain whitespace-pre-wrap break-words pb-2", children: row.detail }))] }), row.open === undefined ? null : (_jsx(Button, { size: "xs", variant: "outline", disabled: onOpen === undefined, onClick: () => {
|
|
23
23
|
if (row.open !== undefined)
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type { WhistleInteractionHost } from "@fungi.computer/whistle/interaction";
|
|
2
|
-
/** The interaction to draw and the page's close
|
|
2
|
+
/** The interaction to draw and the page's close callback. */
|
|
3
3
|
export type InteractionPanelProps = Readonly<{
|
|
4
4
|
/** The renderer-neutral interaction store. */
|
|
5
5
|
host: WhistleInteractionHost;
|
|
6
6
|
/** Close a settled interaction. */
|
|
7
7
|
onClose?: () => void;
|
|
8
|
-
/** Run the same command again after a failure. */
|
|
9
|
-
onRetry?: () => void;
|
|
10
8
|
}>;
|
|
11
9
|
/**
|
|
12
10
|
* Browser renderer for one Whistle interaction. It draws the same host
|
|
13
11
|
* snapshot the terminal draws; the command, not the page, defines the steps.
|
|
14
12
|
*/
|
|
15
|
-
export declare function InteractionPanel({ host, onClose
|
|
13
|
+
export declare function InteractionPanel({ host, onClose }: InteractionPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -29,7 +29,7 @@ function PromptView({ host, pending, }) {
|
|
|
29
29
|
* Browser renderer for one Whistle interaction. It draws the same host
|
|
30
30
|
* snapshot the terminal draws; the command, not the page, defines the steps.
|
|
31
31
|
*/
|
|
32
|
-
export function InteractionPanel({ host, onClose
|
|
32
|
+
export function InteractionPanel({ host, onClose }) {
|
|
33
33
|
const state = useSyncExternalStore(host.subscribe, host.snapshot);
|
|
34
34
|
const active = state.status === "working" || state.status === "prompting";
|
|
35
35
|
return (_jsxs("section", { "aria-label": state.title, className: "flex flex-col gap-sm", "data-status": state.status, children: [_jsx("h2", { className: "text-lg font-semibold", children: state.title }), state.handoff === undefined ? null : (_jsx(Display, { density: "compact", children: _jsxs("div", { className: "flex flex-col gap-xs p-sm", children: [_jsx("span", { className: "text-sm", children: state.handoff.message ??
|
|
@@ -38,5 +38,5 @@ export function InteractionPanel({ host, onClose, onRetry, }) {
|
|
|
38
38
|
: "Open this page and enter the code") }), _jsx("a", { className: "link break-all font-mono", href: state.handoff.url, rel: "noreferrer", target: "_blank", children: state.handoff.url }), state.handoff.code === undefined ? null : (_jsx("span", { className: "font-mono text-2xl tracking-widest", children: state.handoff.code }))] }) })), state.message === undefined || state.status === "failed" ? null : (_jsxs("p", { className: "flex items-center gap-xs", children: [_jsx(Status, { tone: state.message.type === "success" ? "success" : "info", motion: state.message.type === "progress" && active ? "pulse" : "none" }), state.message.message] })), state.links.map((link) => (_jsx("a", { className: "link break-all", href: link.url, rel: "noreferrer", target: "_blank", children: link.label ?? link.url }, link.url))), state.prompt === undefined ? null : (_jsx(PromptView, { host: host, pending: state.prompt }, state.prompt.sequence)), state.status === "failed" ? (_jsx(Alert, { tone: "error", title: "That did not work", description: state.failure })) : null, _jsxs("div", { className: "flex justify-end gap-xs", children: [active ? (_jsx(Button, { variant: "ghost", onClick: host.cancel, children: "Cancel" })) : null, state.status === "failed" ? (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", onClick: () => {
|
|
39
39
|
host.cancel();
|
|
40
40
|
onClose?.();
|
|
41
|
-
}, children: "Close" }),
|
|
41
|
+
}, children: "Close" }), !state.retryable ? null : (_jsx(Button, { onClick: host.retry, children: "Try again" }))] })) : null, state.status === "done" || state.status === "cancelled" ? (_jsx(Button, { onClick: onClose, children: "Done" })) : null] })] }));
|
|
42
42
|
}
|
|
@@ -5,6 +5,8 @@ import { type ToggleToolDisclosure } from "./ToolGroup.js";
|
|
|
5
5
|
export declare const TimelineItem: import("react").NamedExoticComponent<Readonly<{
|
|
6
6
|
item: TimelineRow;
|
|
7
7
|
onDismissFailed?: ((requestId: string) => void) | undefined;
|
|
8
|
+
onCancelQueued?: ((commandId: string) => void) | undefined;
|
|
9
|
+
cancellingQueued?: boolean | undefined;
|
|
8
10
|
renderToolActions?: ((item: ToolItem) => ReactNode) | undefined;
|
|
9
11
|
renderAssistantAttachment?: ((text: string) => ReactNode) | undefined;
|
|
10
12
|
formatAssistantText?: ((text: string) => string) | undefined;
|
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Sigil } from "@fungi.computer/sigils";
|
|
3
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
2
4
|
import { memo } from "react";
|
|
3
|
-
import { initialToolDisclosure, } from "../core/tool-groups.js";
|
|
5
|
+
import { disclosureOpen, initialToolDisclosure, } from "../core/tool-groups.js";
|
|
4
6
|
import { assistantMessageVisible } from "../core/assistant-visibility.js";
|
|
5
7
|
import { formatTokenCount } from "../core/format-context.js";
|
|
6
|
-
import { ChatMessage } from "./ChatMessage.js";
|
|
8
|
+
import { ChatMessage, PendingMessageStatus } from "./ChatMessage.js";
|
|
7
9
|
import { ToolGroup } from "./ToolGroup.js";
|
|
8
10
|
const ignoreToggle = () => undefined;
|
|
9
|
-
|
|
11
|
+
const compactionNoticeLabel = {
|
|
12
|
+
nothing_to_compact: "nothing to compact",
|
|
13
|
+
failed: "compaction failed",
|
|
14
|
+
stale: "compaction skipped: the context moved on",
|
|
15
|
+
insufficient: "compaction skipped: the summary would not fit",
|
|
16
|
+
};
|
|
17
|
+
export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed, onCancelQueued, cancellingQueued = false, renderToolActions, renderAssistantAttachment, formatAssistantText, toolDisclosure = initialToolDisclosure, onToggleTool = ignoreToggle, }) {
|
|
10
18
|
switch (item.kind) {
|
|
11
19
|
case "user_message": {
|
|
20
|
+
const pending = item.status === "pending" || item.status === "queued";
|
|
12
21
|
const requestId = item.requestId;
|
|
22
|
+
const commandId = item.commandId;
|
|
23
|
+
const cancelAction = item.status === "queued" &&
|
|
24
|
+
commandId !== undefined &&
|
|
25
|
+
onCancelQueued !== undefined ? (_jsx("div", { className: "flex justify-end px-3", children: _jsx(Button, { "aria-label": "Cancel queued message", disabled: cancellingQueued, size: "sm", variant: "ghost", onClick: () => {
|
|
26
|
+
onCancelQueued(commandId);
|
|
27
|
+
}, children: "Cancel" }) })) : null;
|
|
13
28
|
const dismissAction = requestId === undefined || onDismissFailed === undefined ? null : (_jsx("button", { "aria-label": "Dismiss failed prompt", className: "btn btn-ghost btn-xs", onClick: () => {
|
|
14
29
|
onDismissFailed(requestId);
|
|
15
30
|
}, type: "button", children: "Dismiss" }));
|
|
16
|
-
|
|
31
|
+
const failed = item.status === "failed" ? (_jsxs("div", { className: "flex items-center gap-2 px-3 text-xs text-error", role: "alert", children: [_jsx("span", { children: item.author?.kind === "agent"
|
|
32
|
+
? "That Agent message didn’t make it through."
|
|
33
|
+
: "Your prompt didn’t make it through." }), dismissAction] })) : null;
|
|
34
|
+
if (item.author?.kind === "agent") {
|
|
35
|
+
const open = disclosureOpen(toolDisclosure, item.id, false);
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: item.status === "confirmed"
|
|
37
|
+
? "px-3 text-xs"
|
|
38
|
+
: "px-3 text-xs opacity-60", "data-agent-message": item.author.id, "data-author": item.author.id, children: [_jsx(PendingMessageStatus, { pending: pending }), _jsxs("button", { "aria-expanded": open, className: "text-dim flex w-full min-w-0 items-center gap-sm text-left", onClick: () => {
|
|
39
|
+
onToggleTool(item.id, false);
|
|
40
|
+
}, type: "button", children: [_jsx("span", { className: "flex shrink-0", children: _jsx(Sigil, { name: "agent", size: 16 }) }), _jsx("span", { className: "shrink-0", children: item.author.name }), open ? null : (_jsxs("span", { className: "min-w-0 truncate", children: ["\u00B7 ", item.text] }))] }), open ? (_jsx("div", { className: "whitespace-pre-wrap break-words pl-3", children: item.text })) : null] }), failed, cancelAction] }));
|
|
41
|
+
}
|
|
42
|
+
return (_jsxs(_Fragment, { children: [item.author ? (_jsx("div", { className: "px-3 text-xs text-dim", "data-author": item.author.id, children: item.author.name })) : null, _jsx(ChatMessage, { pending: pending, speaker: "user", children: item.text }), failed, cancelAction] }));
|
|
17
43
|
}
|
|
18
44
|
case "assistant_message":
|
|
19
45
|
if (!assistantMessageVisible(item))
|
|
@@ -28,8 +54,6 @@ export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed,
|
|
|
28
54
|
? "compacted · the agent’s memory starts below"
|
|
29
55
|
: `compacted ${formatTokenCount(item.tokensBefore)} → ${formatTokenCount(item.tokensAfter)}` }));
|
|
30
56
|
case "compaction_notice":
|
|
31
|
-
return (_jsxs("div", { className: `px-3 text-xs ${item.result === "failed" ? "text-error" : "text-dim"}`, role: "status", children: ["\u00B7
|
|
32
|
-
? "compaction failed"
|
|
33
|
-
: "nothing to compact", item.diagnostic === null ? "" : `: ${item.diagnostic}`] }));
|
|
57
|
+
return (_jsxs("div", { className: `px-3 text-xs ${item.result === "failed" ? "text-error" : "text-dim"}`, role: "status", children: ["\u00B7 ", compactionNoticeLabel[item.result], item.diagnostic === null ? "" : `: ${item.diagnostic}`] }));
|
|
34
58
|
}
|
|
35
59
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AppRoot } from "@fungi.computer/agar";
|
|
2
|
+
import { openAppAgent } from "@fungi.computer/agar/agents";
|
|
3
|
+
import { type ParakeetSession } from "./headless.js";
|
|
4
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { createShiitakeClient, type ShiitakeAgent, type ShiitakeEventSourceConstructor, type ShiitakeSession } from "@fungi.computer/shiitake/client";
|
|
6
|
+
export type OpenConversation<Session extends ParakeetSession = ShiitakeSession> = Readonly<{
|
|
7
|
+
agentId: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
session: Session;
|
|
10
|
+
}>;
|
|
11
|
+
export type PendingSelection = Readonly<{
|
|
12
|
+
agentId: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
}>;
|
|
15
|
+
export type InboxClient = Pick<ShiitakeAgent, "createSession" | "deleteSession" | "session" | "close">;
|
|
16
|
+
/** What the opening owner needs of a client: one addressable Session, closable. */
|
|
17
|
+
export type OpeningClient<Session extends ParakeetSession = ShiitakeSession> = Readonly<{
|
|
18
|
+
session: (sessionId: string) => Session;
|
|
19
|
+
close: () => Promise<void>;
|
|
20
|
+
}>;
|
|
21
|
+
/** The owner's own view of its services; a full {@link Services} satisfies it. */
|
|
22
|
+
export type OpeningServices<Session extends ParakeetSession = ShiitakeSession> = Readonly<{
|
|
23
|
+
openAgent: typeof openAppAgent;
|
|
24
|
+
createClient: (options: Parameters<typeof createShiitakeClient>[0]) => OpeningClient<Session>;
|
|
25
|
+
}>;
|
|
26
|
+
export type Services = Readonly<{
|
|
27
|
+
openAgent: typeof openAppAgent;
|
|
28
|
+
createClient: (options: Parameters<typeof createShiitakeClient>[0]) => InboxClient;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function eventSourceFor(fetcher: typeof globalThis.fetch): ShiitakeEventSourceConstructor;
|
|
31
|
+
/** Owns the selected conversation's Agent, client and Parakeet query lifetimes. */
|
|
32
|
+
export declare function createConversationOpeningOwner<Session extends ParakeetSession = ShiitakeSession>(options: Readonly<{
|
|
33
|
+
connection: Pick<AppRoot, "capabilities"> & {
|
|
34
|
+
ui: Pick<AppRoot["ui"], "reportAgentSessionSelection">;
|
|
35
|
+
};
|
|
36
|
+
queryClient: QueryClient;
|
|
37
|
+
services: OpeningServices<Session>;
|
|
38
|
+
onPending: (selection: PendingSelection | null) => void;
|
|
39
|
+
onConversation: (conversation: OpenConversation<Session> | null) => void;
|
|
40
|
+
onError: (message: string | null) => void;
|
|
41
|
+
/** Only the primary conversation is the selection the host shell sees. */
|
|
42
|
+
reportsSelection?: boolean;
|
|
43
|
+
}>): {
|
|
44
|
+
select: (agentId: string, sessionId: string, fromOpenRequest?: boolean, waitForBaseline?: boolean) => void;
|
|
45
|
+
clear: () => void;
|
|
46
|
+
close: () => void;
|
|
47
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { openAppAgent } from "@fungi.computer/agar/agents";
|
|
2
|
+
import { seedSessionQuery } from "./headless.js";
|
|
3
|
+
import { createShiitakeClient, } from "@fungi.computer/shiitake/client";
|
|
4
|
+
import { ShiitakeError } from "@fungi.computer/shiitake";
|
|
5
|
+
import { EventSource as EventSourceImplementation, } from "eventsource";
|
|
6
|
+
/** Seed Parakeet's shared query with the bounded read before mounting its watch. */
|
|
7
|
+
function prepareConversation(queryClient, session) {
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
const readBaseline = async () => {
|
|
10
|
+
try {
|
|
11
|
+
return await session.read();
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
if (!(cause instanceof ShiitakeError) ||
|
|
15
|
+
cause.boundary !== "runtime" ||
|
|
16
|
+
cause.cause !== "unavailable")
|
|
17
|
+
throw cause;
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
19
|
+
if (cancelled)
|
|
20
|
+
throw new Error("Conversation opening cancelled", { cause });
|
|
21
|
+
return session.read();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let cancel;
|
|
25
|
+
const cancelledRead = new Promise((_, reject) => {
|
|
26
|
+
cancel = () => {
|
|
27
|
+
reject(new Error("Conversation opening cancelled"));
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
const ready = Promise.race([readBaseline(), cancelledRead]).then((snapshot) => {
|
|
31
|
+
if (cancelled)
|
|
32
|
+
throw new Error("Conversation opening cancelled");
|
|
33
|
+
seedSessionQuery(queryClient, session, snapshot);
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
ready,
|
|
37
|
+
close: () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
cancel();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function eventSourceFor(fetcher) {
|
|
44
|
+
return class HostEventSource {
|
|
45
|
+
#source;
|
|
46
|
+
constructor(url) {
|
|
47
|
+
this.#source = new EventSourceImplementation(url, {
|
|
48
|
+
fetch: (input, init) => fetcher(input, init),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
get readyState() {
|
|
52
|
+
return this.#source.readyState;
|
|
53
|
+
}
|
|
54
|
+
addEventListener(type, listener, options) {
|
|
55
|
+
this.#source.addEventListener(type, listener, options);
|
|
56
|
+
}
|
|
57
|
+
close() {
|
|
58
|
+
this.#source.close();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Owns the selected conversation's Agent, client and Parakeet query lifetimes. */
|
|
63
|
+
export function createConversationOpeningOwner(options) {
|
|
64
|
+
const { connection, queryClient, services } = options;
|
|
65
|
+
let pending = null;
|
|
66
|
+
let current = null;
|
|
67
|
+
let currentClose = null;
|
|
68
|
+
let disposed = false;
|
|
69
|
+
const same = (left, right) => left?.agentId === right.agentId && left.sessionId === right.sessionId;
|
|
70
|
+
const isCurrent = (opening) => !disposed && pending === opening;
|
|
71
|
+
const select = (agentId, sessionId, fromOpenRequest = false, waitForBaseline = true) => {
|
|
72
|
+
if (disposed)
|
|
73
|
+
return;
|
|
74
|
+
const selection = { agentId, sessionId };
|
|
75
|
+
if (same(pending, selection))
|
|
76
|
+
return;
|
|
77
|
+
pending?.close();
|
|
78
|
+
pending = null;
|
|
79
|
+
options.onPending(null);
|
|
80
|
+
options.onError(null);
|
|
81
|
+
if (same(current, selection))
|
|
82
|
+
return;
|
|
83
|
+
let openedClose = null;
|
|
84
|
+
let closed = false;
|
|
85
|
+
const opening = {
|
|
86
|
+
...selection,
|
|
87
|
+
close: () => {
|
|
88
|
+
if (closed)
|
|
89
|
+
return;
|
|
90
|
+
closed = true;
|
|
91
|
+
openedClose?.();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
pending = opening;
|
|
95
|
+
options.onPending(selection);
|
|
96
|
+
void (async () => {
|
|
97
|
+
try {
|
|
98
|
+
const transport = await services.openAgent(connection.capabilities, {
|
|
99
|
+
agentId,
|
|
100
|
+
});
|
|
101
|
+
if (!isCurrent(opening)) {
|
|
102
|
+
await transport.close();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const client = services.createClient({
|
|
106
|
+
baseUrl: transport.baseUrl,
|
|
107
|
+
eventSource: eventSourceFor(transport.fetch),
|
|
108
|
+
fetch: transport.fetch,
|
|
109
|
+
});
|
|
110
|
+
const session = client.session(sessionId);
|
|
111
|
+
const observation = prepareConversation(queryClient, session);
|
|
112
|
+
openedClose = () => {
|
|
113
|
+
observation.close();
|
|
114
|
+
void Promise.allSettled([client.close(), transport.close()]);
|
|
115
|
+
};
|
|
116
|
+
await observation.ready;
|
|
117
|
+
if (waitForBaseline && options.reportsSelection !== false) {
|
|
118
|
+
await connection.ui.reportAgentSessionSelection({
|
|
119
|
+
agentId,
|
|
120
|
+
sessionId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (!isCurrent(opening))
|
|
124
|
+
return;
|
|
125
|
+
const conversation = { agentId, sessionId, session };
|
|
126
|
+
const previousClose = currentClose;
|
|
127
|
+
pending = null;
|
|
128
|
+
current = conversation;
|
|
129
|
+
currentClose = opening.close;
|
|
130
|
+
options.onConversation(conversation);
|
|
131
|
+
options.onPending(null);
|
|
132
|
+
previousClose?.();
|
|
133
|
+
}
|
|
134
|
+
catch (cause) {
|
|
135
|
+
console.error("Chat Session opening failed", cause);
|
|
136
|
+
opening.close();
|
|
137
|
+
if (isCurrent(opening)) {
|
|
138
|
+
pending = null;
|
|
139
|
+
options.onPending(null);
|
|
140
|
+
options.onError(fromOpenRequest
|
|
141
|
+
? `That Session could not be opened. ${current ? "The current conversation remains open." : "Choose another Session from the list."}`
|
|
142
|
+
: `Could not open this Session${cause instanceof Error ? `: ${cause.message}` : ""}. ${current ? "The previous conversation remains open." : "Try opening it again."}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
};
|
|
147
|
+
const clear = () => {
|
|
148
|
+
if (disposed)
|
|
149
|
+
return;
|
|
150
|
+
const hadSelection = pending !== null || current !== null;
|
|
151
|
+
pending?.close();
|
|
152
|
+
pending = null;
|
|
153
|
+
currentClose?.();
|
|
154
|
+
currentClose = null;
|
|
155
|
+
current = null;
|
|
156
|
+
options.onPending(null);
|
|
157
|
+
options.onConversation(null);
|
|
158
|
+
options.onError(null);
|
|
159
|
+
if (!hadSelection || options.reportsSelection === false)
|
|
160
|
+
return;
|
|
161
|
+
void connection.ui
|
|
162
|
+
.reportAgentSessionSelection(null)
|
|
163
|
+
.catch((cause) => {
|
|
164
|
+
console.error("Chat Session selection could not be reported", cause);
|
|
165
|
+
return undefined;
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const close = () => {
|
|
169
|
+
clear();
|
|
170
|
+
disposed = true;
|
|
171
|
+
};
|
|
172
|
+
return { select, clear, close };
|
|
173
|
+
}
|