@fungi.computer/parakeet 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +186 -2
- package/dist/account-commands.d.ts +102 -0
- package/dist/account-commands.js +239 -0
- package/dist/agent-head.d.ts +4 -0
- package/dist/agent-head.js +2 -0
- package/dist/client-contributions.d.ts +20 -0
- package/dist/client-contributions.js +41 -0
- package/dist/commands/advertised.d.ts +23 -0
- package/dist/commands/advertised.js +72 -0
- package/dist/commands/builtins.d.ts +14 -0
- package/dist/commands/builtins.js +91 -0
- package/dist/commands/hints.d.ts +36 -0
- package/dist/commands/hints.js +64 -0
- package/dist/commands/interactions.d.ts +55 -0
- package/dist/commands/interactions.js +101 -0
- package/dist/commands/run.d.ts +11 -0
- package/dist/commands/run.js +61 -0
- package/dist/commands/slash.d.ts +124 -0
- package/dist/commands/slash.js +182 -0
- package/dist/commands/use-slash-commands.d.ts +57 -0
- package/dist/commands/use-slash-commands.js +207 -0
- package/dist/components/AgentHead.d.ts +27 -0
- package/dist/components/AgentHead.js +53 -0
- package/dist/components/ChatComposer.d.ts +43 -0
- package/dist/components/ChatComposer.js +209 -0
- package/dist/components/ChatMessage.d.ts +8 -0
- package/dist/components/ChatMessage.js +16 -0
- package/dist/components/ClientRows.d.ts +11 -0
- package/dist/components/ClientRows.js +46 -0
- package/dist/components/CodeBlock.d.ts +11 -0
- package/dist/components/CodeBlock.js +38 -0
- package/dist/components/InteractionPanel.d.ts +15 -0
- package/dist/components/InteractionPanel.js +42 -0
- package/dist/components/MarkdownRenderer.d.ts +6 -0
- package/dist/components/MarkdownRenderer.js +48 -0
- package/dist/components/RunFailureNotice.d.ts +7 -0
- package/dist/components/RunFailureNotice.js +13 -0
- package/dist/components/ShiitakeAvatar.d.ts +19 -0
- package/dist/components/ShiitakeAvatar.js +20 -0
- package/dist/components/SlashMenu.d.ts +8 -0
- package/dist/components/SlashMenu.js +50 -0
- package/dist/components/ThinkingBlock.d.ts +3 -0
- package/dist/components/ThinkingBlock.js +18 -0
- package/dist/components/TimelineItem.d.ts +15 -0
- package/dist/components/TimelineItem.js +35 -0
- package/dist/components/ToolGroup.d.ts +12 -0
- package/dist/components/ToolGroup.js +90 -0
- package/dist/components/WaitingNotice.d.ts +7 -0
- package/dist/components/WaitingNotice.js +8 -0
- package/dist/components/avatar-presentation.d.ts +23 -0
- package/dist/components/avatar-presentation.js +16 -0
- package/dist/components/sprite-service.d.ts +2 -0
- package/dist/components/sprite-service.js +9 -0
- package/dist/components/still-frame-cache.d.ts +16 -0
- package/dist/components/still-frame-cache.js +63 -0
- package/dist/conversation.d.ts +164 -0
- package/dist/conversation.js +760 -0
- package/dist/core/assistant-visibility.d.ts +3 -0
- package/dist/core/assistant-visibility.js +4 -0
- package/dist/core/author-labels.d.ts +16 -0
- package/dist/core/author-labels.js +16 -0
- package/dist/core/build-timeline.d.ts +36 -0
- package/dist/core/build-timeline.js +328 -0
- package/dist/core/client-actions.d.ts +35 -0
- package/dist/core/client-actions.js +128 -0
- package/dist/core/client-contributions.d.ts +24 -0
- package/dist/core/client-contributions.js +115 -0
- package/dist/core/client-timeline.d.ts +33 -0
- package/dist/core/client-timeline.js +72 -0
- package/dist/core/format-context.d.ts +11 -0
- package/dist/core/format-context.js +20 -0
- package/dist/core/format-tokens.d.ts +2 -0
- package/dist/core/format-tokens.js +6 -0
- package/dist/core/model-identity.d.ts +3 -0
- package/dist/core/model-identity.js +7 -0
- package/dist/core/session-projection.d.ts +11 -0
- package/dist/core/session-projection.js +191 -0
- package/dist/core/session-query.d.ts +118 -0
- package/dist/core/session-query.js +187 -0
- package/dist/core/session-view-state.d.ts +66 -0
- package/dist/core/session-view-state.js +114 -0
- package/dist/core/tool-contract.d.ts +15 -0
- package/dist/core/tool-contract.js +3 -0
- package/dist/core/tool-groups.d.ts +36 -0
- package/dist/core/tool-groups.js +151 -0
- package/dist/core/tool-view.d.ts +182 -0
- package/dist/core/tool-view.js +618 -0
- package/dist/headless.d.ts +28 -0
- package/dist/headless.js +24 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -0
- package/dist/lib/failure.d.ts +2 -0
- package/dist/lib/failure.js +9 -0
- package/dist/markdown.d.ts +3 -0
- package/dist/markdown.js +6 -0
- package/dist/model-commands.d.ts +44 -0
- package/dist/model-commands.js +232 -0
- package/dist/parakeet.d.ts +77 -0
- package/dist/parakeet.js +276 -0
- package/dist/provider-choices.d.ts +8 -0
- package/dist/provider-choices.js +43 -0
- package/dist/reload-command.d.ts +43 -0
- package/dist/reload-command.js +72 -0
- package/dist/session-commands.d.ts +53 -0
- package/dist/session-commands.js +259 -0
- package/dist/session.d.ts +24 -0
- package/dist/session.js +1 -0
- package/dist/store/types.d.ts +163 -0
- package/dist/store/types.js +1 -0
- package/dist/styles.css +19156 -0
- package/docs/api/README.md +15 -0
- package/docs/api/commands/slash.md +321 -0
- package/docs/api/headless.md +2510 -0
- package/docs/api/index.md +1253 -0
- package/package.json +79 -3
- package/src/styles.css +268 -0
package/dist/parakeet.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
3
|
+
import { Dialog, DialogContent } from "@fungi.computer/caps/components/dialog";
|
|
4
|
+
import { ScrollToBottomBlob } from "@fungi.computer/caps/components/scroll-to-bottom-blob";
|
|
5
|
+
import { createWhistle } from "@fungi.computer/whistle";
|
|
6
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
8
|
+
import { ParakeetProvider, useComposer, useClientContributions, useDismissFailedPrompt, useStatus, useTimeline, } from "./conversation.js";
|
|
9
|
+
import { initialToolDisclosure, toggleDisclosure } from "./core/tool-groups.js";
|
|
10
|
+
import { authorDisplayNames, } from "./core/author-labels.js";
|
|
11
|
+
import { ChatComposer, sessionReadout, } from "./components/ChatComposer.js";
|
|
12
|
+
import { ClientRows } from "./components/ClientRows.js";
|
|
13
|
+
import { useClientActions } from "./client-contributions.js";
|
|
14
|
+
import { placeClientTimeline } from "./core/client-timeline.js";
|
|
15
|
+
import { InteractionPanel } from "./components/InteractionPanel.js";
|
|
16
|
+
import { WaitingNotice } from "./components/WaitingNotice.js";
|
|
17
|
+
import { useSlashCommands, } from "./commands/use-slash-commands.js";
|
|
18
|
+
import { TimelineItem } from "./components/TimelineItem.js";
|
|
19
|
+
import { MarkdownPolicyContext } from "./components/MarkdownRenderer.js";
|
|
20
|
+
/** Render the retained conversation surface within one Parakeet provider. */
|
|
21
|
+
export function ParakeetView({ disambiguateAuthors, visitor, composerActionVariant, empty, commands, renderToolActions, renderAssistantAttachment, formatAssistantText, onLink, onOpen, }) {
|
|
22
|
+
const textareaRef = useRef(null);
|
|
23
|
+
const [ownCommands] = useState(() => createWhistle());
|
|
24
|
+
const slash = useSlashCommands({
|
|
25
|
+
enabled: visitor === undefined,
|
|
26
|
+
commands: commands ?? ownCommands,
|
|
27
|
+
origin: "browser",
|
|
28
|
+
keys: { newline: "Shift+Enter" },
|
|
29
|
+
busy: useBusy(),
|
|
30
|
+
});
|
|
31
|
+
const clientActions = useClientActions(commands ?? ownCommands);
|
|
32
|
+
const contributions = useClientContributions();
|
|
33
|
+
const timelineController = useTimeline();
|
|
34
|
+
const placed = useMemo(() => placeClientTimeline(contributions, timelineController.commandAnchors), [contributions, timelineController.commandAnchors]);
|
|
35
|
+
return (_jsxs("div", { className: "parakeet flex h-full flex-col items-stretch", "data-parakeet": "standalone", "data-testid": "parakeet-view", onClickCapture: (event) => {
|
|
36
|
+
const anchor = event.target instanceof Element
|
|
37
|
+
? event.target.closest("a[href]")
|
|
38
|
+
: null;
|
|
39
|
+
if (anchor instanceof HTMLAnchorElement && onLink?.(anchor.href))
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
}, children: [_jsxs("div", { className: "flex h-full w-full flex-col border-r border-base-300", children: [_jsx(ConversationTimeline, { disambiguateAuthors: visitor === undefined && disambiguateAuthors === true, clientActions: clientActions, onOpen: onOpen, focusAfterJump: textareaRef, empty: empty, renderToolActions: renderToolActions, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText }), contributions.some(({ value }) => value.panel.length > 0) ? (_jsx("section", { "aria-label": "Session extension controls", className: "w-full shrink-0 border-t border-base-300 bg-base-100", children: contributions.map(({ entryId, value }) => {
|
|
42
|
+
// An extension's retained-event notice and null-anchor rows
|
|
43
|
+
// join its own footer entry; one with no panel row here keeps
|
|
44
|
+
// none of this, rather than growing a section just to show it.
|
|
45
|
+
const omittedHere = value.panel.length === 0
|
|
46
|
+
? undefined
|
|
47
|
+
: placed.omitted.find((entry) => entry.entryId === entryId);
|
|
48
|
+
const unanchoredHere = value.panel.length === 0
|
|
49
|
+
? []
|
|
50
|
+
: placed.unanchored
|
|
51
|
+
.filter((entry) => entry.entryId === entryId)
|
|
52
|
+
.flatMap(({ cell }) => cell.rows.map((row) => ({
|
|
53
|
+
...row,
|
|
54
|
+
id: `unanchored:${cell.id}:${row.id}`,
|
|
55
|
+
})));
|
|
56
|
+
return (_jsx(ClientRows, { entryId: entryId, rows: [
|
|
57
|
+
...value.panel,
|
|
58
|
+
...(omittedHere === undefined
|
|
59
|
+
? []
|
|
60
|
+
: [
|
|
61
|
+
{
|
|
62
|
+
id: `omitted:${entryId}`,
|
|
63
|
+
title: `${String(omittedHere.count)} earlier change${omittedHere.count === 1 ? "" : "s"} omitted`,
|
|
64
|
+
tone: "muted",
|
|
65
|
+
},
|
|
66
|
+
]),
|
|
67
|
+
...unanchoredHere,
|
|
68
|
+
], actions: clientActions, onOpen: onOpen }, entryId));
|
|
69
|
+
}) })) : null, _jsx(ConversationWaiting, { clientActions: clientActions }), _jsx(ConversationComposer, { visitor: visitor, textareaRef: textareaRef, composerActionVariant: composerActionVariant, slash: slash })] }), visitor === undefined ? _jsx(ConversationInteraction, { slash: slash }) : null] }));
|
|
70
|
+
}
|
|
71
|
+
const busyStatuses = new Set([
|
|
72
|
+
"cancelling",
|
|
73
|
+
"queued",
|
|
74
|
+
"retrying",
|
|
75
|
+
"sending",
|
|
76
|
+
"streaming",
|
|
77
|
+
"thinking",
|
|
78
|
+
]);
|
|
79
|
+
function useBusy() {
|
|
80
|
+
return busyStatuses.has(useStatus().status);
|
|
81
|
+
}
|
|
82
|
+
/** The browser renderer of Parakeet's open command conversation. */
|
|
83
|
+
function ConversationInteraction({ slash, }) {
|
|
84
|
+
const open = slash.interactions.open;
|
|
85
|
+
return (_jsx(Dialog, { open: open !== undefined, onOpenChange: (next) => {
|
|
86
|
+
if (!next)
|
|
87
|
+
slash.store.close();
|
|
88
|
+
}, children: _jsx(DialogContent, { className: "w-[min(32rem,calc(100vw-2rem))] rounded-box bg-base-100 p-md shadow-lg", keepMounted: false, children: open === undefined ? null : (_jsx(InteractionPanel, { host: open.host, onClose: slash.store.close, onRetry: () => {
|
|
89
|
+
slash.store.close();
|
|
90
|
+
open.retry();
|
|
91
|
+
} }, open.key)) }) }));
|
|
92
|
+
}
|
|
93
|
+
function ConversationTimeline({ disambiguateAuthors, clientActions, onOpen, focusAfterJump, empty, renderToolActions, renderAssistantAttachment, formatAssistantText, }) {
|
|
94
|
+
const dismissFailed = useDismissFailedPrompt();
|
|
95
|
+
const timelineController = useTimeline();
|
|
96
|
+
const authorNames = useMemo(() => authorDisplayNames(disambiguateAuthors
|
|
97
|
+
? [...timelineController.settled, ...timelineController.tail].flatMap((item) => ("author" in item && item.author ? [item.author] : []))
|
|
98
|
+
: []), [disambiguateAuthors, timelineController.settled, timelineController.tail]);
|
|
99
|
+
const contributions = useClientContributions();
|
|
100
|
+
const placed = placeClientTimeline(contributions, timelineController.commandAnchors);
|
|
101
|
+
// View-only: kept above the virtualizer so off-screen groups remember it.
|
|
102
|
+
const [toolDisclosure, setToolDisclosure] = useState(initialToolDisclosure);
|
|
103
|
+
const toggleTool = useCallback((id, defaultOpen) => {
|
|
104
|
+
setToolDisclosure((current) => toggleDisclosure(current, id, defaultOpen));
|
|
105
|
+
}, []);
|
|
106
|
+
const sessionStatus = useStatus();
|
|
107
|
+
const scrollerRef = useRef(null);
|
|
108
|
+
const previousScrollTop = useRef(undefined);
|
|
109
|
+
const count = timelineController.settled.length + timelineController.tail.length;
|
|
110
|
+
const itemAt = useCallback((index) => index < timelineController.settled.length
|
|
111
|
+
? timelineController.settled[index]
|
|
112
|
+
: timelineController.tail[index - timelineController.settled.length], [timelineController.settled, timelineController.tail]);
|
|
113
|
+
const getItemKey = useCallback((index) => {
|
|
114
|
+
const item = itemAt(index);
|
|
115
|
+
if (item === undefined)
|
|
116
|
+
return index;
|
|
117
|
+
// One submitted prompt keeps one row from submit to durable: no remount.
|
|
118
|
+
return item.kind === "user_message" && item.requestId !== undefined
|
|
119
|
+
? item.requestId
|
|
120
|
+
: item.id;
|
|
121
|
+
}, [itemAt]);
|
|
122
|
+
const timeline = useVirtualizer({
|
|
123
|
+
anchorTo: "end",
|
|
124
|
+
count,
|
|
125
|
+
estimateSize: () => 72,
|
|
126
|
+
followOnAppend: "auto",
|
|
127
|
+
getItemKey,
|
|
128
|
+
getScrollElement: () => scrollerRef.current,
|
|
129
|
+
initialRect: { height: 800, width: 800 },
|
|
130
|
+
overscan: 6,
|
|
131
|
+
useFlushSync: false,
|
|
132
|
+
});
|
|
133
|
+
const virtualItems = timeline.getVirtualItems();
|
|
134
|
+
const atBottom = timeline.isAtEnd();
|
|
135
|
+
const [unread, setUnread] = useState(0);
|
|
136
|
+
const lastItem = count > 0 ? itemAt(count - 1)?.id : undefined;
|
|
137
|
+
const observedTail = useRef({ count, lastItem });
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const previous = observedTail.current;
|
|
140
|
+
observedTail.current = { count, lastItem };
|
|
141
|
+
if (atBottom)
|
|
142
|
+
setUnread(0);
|
|
143
|
+
else if (previous.lastItem !== undefined && lastItem !== previous.lastItem)
|
|
144
|
+
setUnread((value) => value + Math.max(1, count - previous.count));
|
|
145
|
+
}, [atBottom, count, lastItem]);
|
|
146
|
+
const nearTop = (virtualItems[0]?.index ?? Number.POSITIVE_INFINITY) < 3;
|
|
147
|
+
const { hasMoreHistory, hasNewerHistory, loadingOlderHistory, loadOlderHistory, loadNewerHistory, } = timelineController;
|
|
148
|
+
const requestedAtTop = useRef(false);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!nearTop) {
|
|
151
|
+
requestedAtTop.current = false;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (hasMoreHistory && !loadingOlderHistory && !requestedAtTop.current) {
|
|
155
|
+
requestedAtTop.current = true;
|
|
156
|
+
loadOlderHistory();
|
|
157
|
+
}
|
|
158
|
+
}, [hasMoreHistory, loadOlderHistory, loadingOlderHistory, nearTop]);
|
|
159
|
+
const jumpToBottom = useCallback(() => {
|
|
160
|
+
setUnread(0);
|
|
161
|
+
previousScrollTop.current = undefined;
|
|
162
|
+
timelineController.jumpToLatest();
|
|
163
|
+
timeline.scrollToEnd({ behavior: "smooth" });
|
|
164
|
+
}, [timeline, timelineController]);
|
|
165
|
+
return (_jsxs("div", { className: "prose relative min-h-0 flex-1", children: [_jsx("div", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: sessionReadout(sessionStatus.status, sessionStatus.viewState) }), sessionStatus.observation.status === "unavailable" ? (_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-1 text-xs text-dim", role: "status", children: [_jsx("span", { children: sessionStatus.observation.reason }), _jsx("button", { className: "underline", onClick: () => void sessionStatus.reconnect(), type: "button", children: "Reconnect" })] })) : null, _jsx("div", { className: "h-full min-h-0 overflow-y-auto", "data-chat-scroller": true, ref: scrollerRef, onScroll: (event) => {
|
|
166
|
+
const top = event.currentTarget.scrollTop;
|
|
167
|
+
if (previousScrollTop.current !== undefined &&
|
|
168
|
+
top < previousScrollTop.current)
|
|
169
|
+
timelineController.pinVisibleHistory();
|
|
170
|
+
previousScrollTop.current = top;
|
|
171
|
+
}, onWheel: (event) => {
|
|
172
|
+
const scroller = scrollerRef.current;
|
|
173
|
+
if (scroller === null || loadingOlderHistory)
|
|
174
|
+
return;
|
|
175
|
+
if (event.deltaY < 0)
|
|
176
|
+
timelineController.pinVisibleHistory();
|
|
177
|
+
if (event.deltaY < 0 && scroller.scrollTop <= 1 && hasMoreHistory)
|
|
178
|
+
loadOlderHistory();
|
|
179
|
+
if (event.deltaY > 0 &&
|
|
180
|
+
scroller.scrollTop + scroller.clientHeight >=
|
|
181
|
+
scroller.scrollHeight - 1 &&
|
|
182
|
+
hasNewerHistory)
|
|
183
|
+
loadNewerHistory();
|
|
184
|
+
}, style: { overscrollBehavior: "contain" }, children: count > 0 ? (_jsx("div", { className: "relative w-full", style: { height: `${String(timeline.getTotalSize())}px` }, children: virtualItems.map((virtualItem) => {
|
|
185
|
+
const item = itemAt(virtualItem.index);
|
|
186
|
+
if (item === undefined)
|
|
187
|
+
return null;
|
|
188
|
+
return (_jsxs("div", { "data-index": virtualItem.index, "data-parakeet-entry": item.kind === "user_message" ? "user" : item.kind, "data-parakeet-id": item.id, "data-status": item.kind === "assistant_message"
|
|
189
|
+
? item.isError
|
|
190
|
+
? "error"
|
|
191
|
+
: undefined
|
|
192
|
+
: item.kind === "compaction_divider" ||
|
|
193
|
+
item.kind === "compaction_notice"
|
|
194
|
+
? undefined
|
|
195
|
+
: item.status, "data-tone": item.kind === "assistant_message" && item.isError
|
|
196
|
+
? "error"
|
|
197
|
+
: undefined, ref: timeline.measureElement, style: {
|
|
198
|
+
left: 0,
|
|
199
|
+
position: "absolute",
|
|
200
|
+
top: 0,
|
|
201
|
+
transform: `translateY(${String(virtualItem.start)}px)`,
|
|
202
|
+
width: "100%",
|
|
203
|
+
}, children: [_jsx(TimelineItem, { item: "author" in item &&
|
|
204
|
+
item.author &&
|
|
205
|
+
authorNames.has(item.author.id)
|
|
206
|
+
? {
|
|
207
|
+
...item,
|
|
208
|
+
author: {
|
|
209
|
+
...item.author,
|
|
210
|
+
name: authorNames.get(item.author.id) ??
|
|
211
|
+
item.author.name,
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
: item, onDismissFailed: dismissFailed, onToggleTool: toggleTool, renderToolActions: renderToolActions, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText, toolDisclosure: toolDisclosure }), placed.after.get(item.id)?.map(({ entryId, cell }) => (_jsx(ClientRows, { entryId: entryId, rows: cell.rows, actions: clientActions, onOpen: onOpen }, `${entryId}:${cell.id}`)))] }, virtualItem.key));
|
|
215
|
+
}) })) : ((empty ?? (_jsx("div", { className: "flex h-full items-center justify-center text-center", children: _jsxs("div", { children: [_jsx("p", { className: "mb-1 text-xs text-base-content", children: "Ready" }), _jsx("p", { className: "text-[10px] text-dim", children: "What would you like to build?" })] }) })))) }), count > 0 ? (_jsx(ScrollToBottomBlob, { direction: "forward", unread: unread, focusAfterJump: focusAfterJump, onJump: jumpToBottom, scrollerRef: scrollerRef })) : null] }));
|
|
216
|
+
}
|
|
217
|
+
/** The owner's queued state and independently contributed waiting controls. */
|
|
218
|
+
function ConversationWaiting({ clientActions, }) {
|
|
219
|
+
const { viewState } = useStatus();
|
|
220
|
+
const contributions = useClientContributions();
|
|
221
|
+
const extensionWaiting = viewState.kind === "idle"
|
|
222
|
+
? contributions.find(({ value }) => value.waiting !== null)
|
|
223
|
+
: undefined;
|
|
224
|
+
const waiting = viewState.kind === "queued" ? viewState : null;
|
|
225
|
+
return (_jsxs(_Fragment, { children: [extensionWaiting?.value.waiting === undefined ||
|
|
226
|
+
extensionWaiting.value.waiting === null ? null : (_jsxs("div", { className: "flex items-center gap-2 px-3 text-xs", children: [_jsx("span", { children: extensionWaiting.value.waiting.summary }), _jsx(Button, { size: "xs", variant: "outline", disabled: !clientActions.enabled ||
|
|
227
|
+
clientActions.state.get(extensionWaiting.value.waiting.cancel)
|
|
228
|
+
?.pending === true, onClick: () => {
|
|
229
|
+
if (extensionWaiting.value.waiting !== null)
|
|
230
|
+
void clientActions.execute(extensionWaiting.entryId, extensionWaiting.value.waiting.cancel, true);
|
|
231
|
+
}, children: extensionWaiting.value.waiting.cancel.label })] })), _jsx(WaitingNotice, { waiting: waiting })] }));
|
|
232
|
+
}
|
|
233
|
+
function ConversationComposer({ visitor, textareaRef, composerActionVariant, slash, }) {
|
|
234
|
+
const composer = useComposer();
|
|
235
|
+
const sessionStatus = useStatus();
|
|
236
|
+
const contributions = useClientContributions();
|
|
237
|
+
const timeline = useTimeline();
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (composer.failure !== undefined)
|
|
240
|
+
textareaRef.current?.focus();
|
|
241
|
+
}, [composer.failure, textareaRef]);
|
|
242
|
+
const abort = useCallback(() => {
|
|
243
|
+
void composer.cancel();
|
|
244
|
+
}, [composer.cancel]);
|
|
245
|
+
const composerProps = {
|
|
246
|
+
statusChips: contributions
|
|
247
|
+
.flatMap(({ value }) => value.chips)
|
|
248
|
+
.sort((left, right) => left.order - right.order)
|
|
249
|
+
.map(({ row }) => row.title),
|
|
250
|
+
actionVariant: composerActionVariant,
|
|
251
|
+
draft: composer.draft,
|
|
252
|
+
failure: composer.failure,
|
|
253
|
+
failureCause: sessionStatus.viewState.kind === "error"
|
|
254
|
+
? sessionStatus.viewState.cause
|
|
255
|
+
: undefined,
|
|
256
|
+
admissionError: composer.admissionError,
|
|
257
|
+
onAbort: abort,
|
|
258
|
+
onChange: composer.setDraft,
|
|
259
|
+
onRetry: composer.retry,
|
|
260
|
+
onSend: slash.submit,
|
|
261
|
+
slash: visitor === undefined ? slash : undefined,
|
|
262
|
+
placeholder: visitor === undefined ? undefined : `Ask ${visitor.agentName}…`,
|
|
263
|
+
showFooter: visitor === undefined,
|
|
264
|
+
status: sessionStatus.status,
|
|
265
|
+
viewState: sessionStatus.viewState,
|
|
266
|
+
textareaRef,
|
|
267
|
+
model: timeline.model,
|
|
268
|
+
usage: timeline.usage,
|
|
269
|
+
context: sessionStatus.context,
|
|
270
|
+
};
|
|
271
|
+
return _jsx(ChatComposer, { ...composerProps });
|
|
272
|
+
}
|
|
273
|
+
/** Standalone typed composer and exact-message timeline. */
|
|
274
|
+
export function Parakeet({ authorOf, disambiguateAuthors, visitor, markdownPolicy = "session", composerActionVariant, empty, session, clients, renderToolActions, renderAssistantAttachment, formatAssistantText, commands, onLink, onOpen, }) {
|
|
275
|
+
return (_jsx(MarkdownPolicyContext.Provider, { value: markdownPolicy, children: _jsx(ParakeetProvider, { session: session, clients: clients, authorOf: authorOf, children: _jsx(ParakeetView, { disambiguateAuthors: disambiguateAuthors, visitor: visitor, composerActionVariant: composerActionVariant, empty: empty, renderToolActions: renderToolActions, renderAssistantAttachment: renderAssistantAttachment, formatAssistantText: formatAssistantText, commands: commands, onLink: onLink, onOpen: onOpen }) }) }));
|
|
276
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { WhistlePrompt } from "@fungi.computer/whistle";
|
|
2
|
+
import type { LoginProvider, ProviderAccess } from "./account-commands.js";
|
|
3
|
+
/** Whistle-owned choice options returned by the provider grouping helper. */
|
|
4
|
+
export type ChoiceOptions = Extract<WhistlePrompt, {
|
|
5
|
+
type: "choice";
|
|
6
|
+
}>["options"];
|
|
7
|
+
/** Group the existing registry or model providers without owning another catalog. */
|
|
8
|
+
export declare function providerChoices(providers: readonly LoginProvider[], access: readonly ProviderAccess[], popularProviderIds: readonly string[], screen: "login" | "model"): ChoiceOptions;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Group the existing registry or model providers without owning another catalog. */
|
|
2
|
+
export function providerChoices(providers, access, popularProviderIds, screen) {
|
|
3
|
+
const signedIn = new Map(access.map((provider) => [provider.providerId, provider]));
|
|
4
|
+
const popular = new Map(popularProviderIds.map((id, index) => [id, index]));
|
|
5
|
+
const group = (id) => signedIn.has(id) ? 0 : screen === "login" && popular.has(id) ? 1 : 2;
|
|
6
|
+
const isDefault = (id) => screen === "model" && id === "fungi";
|
|
7
|
+
return [
|
|
8
|
+
...new Map(providers.map((provider) => [provider.id, provider])).values(),
|
|
9
|
+
]
|
|
10
|
+
.sort((left, right) => Number(isDefault(right.id)) - Number(isDefault(left.id)) ||
|
|
11
|
+
group(left.id) - group(right.id) ||
|
|
12
|
+
(group(left.id) < 2
|
|
13
|
+
? (popular.get(left.id) ?? popularProviderIds.length) -
|
|
14
|
+
(popular.get(right.id) ?? popularProviderIds.length)
|
|
15
|
+
: 0) ||
|
|
16
|
+
left.name.localeCompare(right.name) ||
|
|
17
|
+
left.id.localeCompare(right.id))
|
|
18
|
+
.map((provider) => {
|
|
19
|
+
const methods = provider.methods.map((method) => method.kind === "api_key" ? "API key" : "Subscription");
|
|
20
|
+
const credential = signedIn.get(provider.id);
|
|
21
|
+
const description = methods.length > 0
|
|
22
|
+
? methods.join(" or ")
|
|
23
|
+
: credential?.type === "oauth"
|
|
24
|
+
? "Subscription"
|
|
25
|
+
: credential?.type === "api_key"
|
|
26
|
+
? "API key"
|
|
27
|
+
: undefined;
|
|
28
|
+
const choice = {
|
|
29
|
+
value: provider.id,
|
|
30
|
+
label: provider.name,
|
|
31
|
+
group: isDefault(provider.id) && !signedIn.has(provider.id)
|
|
32
|
+
? "Default"
|
|
33
|
+
: group(provider.id) === 0
|
|
34
|
+
? "Signed in"
|
|
35
|
+
: group(provider.id) === 1
|
|
36
|
+
? "Popular"
|
|
37
|
+
: "More providers",
|
|
38
|
+
};
|
|
39
|
+
if (description === undefined)
|
|
40
|
+
return choice;
|
|
41
|
+
return { ...choice, description };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WhistleContribution, WhistleRuntime } from "@fungi.computer/whistle";
|
|
2
|
+
/**
|
|
3
|
+
* One category a host can account for in `/reload`, described by the owner
|
|
4
|
+
* that already holds it. Nothing here caches, restarts or replaces anything:
|
|
5
|
+
* `now` re-reads through the owner's own refresh, `next_turn` names a source
|
|
6
|
+
* the owner already re-reads at the start of every turn, and `unsupported`
|
|
7
|
+
* names a category with no safe re-read or replace lifecycle today.
|
|
8
|
+
*/
|
|
9
|
+
export type ReloadSource = Readonly<{
|
|
10
|
+
/** The category the report names, such as "model catalog". */
|
|
11
|
+
name: string;
|
|
12
|
+
} & ({
|
|
13
|
+
/** Re-read now, while the command runs. */
|
|
14
|
+
effect: "now";
|
|
15
|
+
/** Re-read this category; a rejection is reported, never thrown. */
|
|
16
|
+
reload: (signal: AbortSignal) => Promise<void>;
|
|
17
|
+
} | {
|
|
18
|
+
/** The owner already re-reads it at the start of every turn. */
|
|
19
|
+
effect: "next_turn";
|
|
20
|
+
} | {
|
|
21
|
+
/** No safe re-read or replace lifecycle exists for it today. */
|
|
22
|
+
effect: "unsupported";
|
|
23
|
+
/** Why it cannot be reloaded, shown in the report. */
|
|
24
|
+
reason: string;
|
|
25
|
+
})>;
|
|
26
|
+
/** Where one `/reload` contribution comes from and what it can report. */
|
|
27
|
+
export type ReloadCommandOptions = Readonly<{
|
|
28
|
+
/** Host source that owns the contribution lease. */
|
|
29
|
+
sourceId: string;
|
|
30
|
+
/** Namespace for the reload command. */
|
|
31
|
+
namespace: string;
|
|
32
|
+
/** Read at execution so the report names the categories mounted right now. */
|
|
33
|
+
sources: () => readonly ReloadSource[];
|
|
34
|
+
}>;
|
|
35
|
+
/** Refresh what supports it and report every category honestly. */
|
|
36
|
+
export declare function reloadSources(sources: readonly ReloadSource[], signal: AbortSignal): Promise<string>;
|
|
37
|
+
/** The shared `/reload` definition over host-described reload sources. */
|
|
38
|
+
export declare function reloadCommandDefinitions(options: Pick<ReloadCommandOptions, "sources">): WhistleContribution["commands"];
|
|
39
|
+
/** Contribute `/reload` to one Whistle host. */
|
|
40
|
+
export declare function contributeReloadCommand(commands: Pick<WhistleRuntime, "contribute">, options: ReloadCommandOptions): Readonly<{
|
|
41
|
+
update: (commands: WhistleContribution["commands"]) => void;
|
|
42
|
+
dispose: () => void;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function failure(cause) {
|
|
2
|
+
return cause instanceof Error && cause.message.length > 0
|
|
3
|
+
? cause.message.slice(0, 200)
|
|
4
|
+
: "refresh failed";
|
|
5
|
+
}
|
|
6
|
+
/** Refresh what supports it and report every category honestly. */
|
|
7
|
+
export async function reloadSources(sources, signal) {
|
|
8
|
+
const refreshed = [];
|
|
9
|
+
const failed = [];
|
|
10
|
+
const nextTurn = [];
|
|
11
|
+
const unsupported = [];
|
|
12
|
+
const reloadable = [];
|
|
13
|
+
for (const source of sources) {
|
|
14
|
+
if (source.effect === "next_turn")
|
|
15
|
+
nextTurn.push(source.name);
|
|
16
|
+
else if (source.effect === "unsupported")
|
|
17
|
+
unsupported.push(`${source.name} (${source.reason})`);
|
|
18
|
+
else
|
|
19
|
+
reloadable.push(source);
|
|
20
|
+
}
|
|
21
|
+
// Each refresh settles on its own: one failure is reported, never thrown,
|
|
22
|
+
// and never stops the others. A synchronous throw settles as a rejection.
|
|
23
|
+
const settled = await Promise.allSettled(reloadable.map((source) => Promise.resolve().then(() => source.reload(signal))));
|
|
24
|
+
reloadable.forEach((source, index) => {
|
|
25
|
+
const result = settled[index];
|
|
26
|
+
if (result?.status === "fulfilled")
|
|
27
|
+
refreshed.push(source.name);
|
|
28
|
+
else
|
|
29
|
+
failed.push(`${source.name} (${failure(result?.reason)})`);
|
|
30
|
+
});
|
|
31
|
+
const order = (names) => names.sort((left, right) => left.localeCompare(right)).join(", ");
|
|
32
|
+
const lines = [
|
|
33
|
+
`Refreshed now: ${refreshed.length === 0 ? "nothing" : order(refreshed)}.`,
|
|
34
|
+
];
|
|
35
|
+
if (failed.length > 0)
|
|
36
|
+
lines.push(`Failed: ${order(failed)}.`);
|
|
37
|
+
if (nextTurn.length > 0)
|
|
38
|
+
lines.push(`Re-read at the next turn: ${order(nextTurn)}.`);
|
|
39
|
+
if (unsupported.length > 0)
|
|
40
|
+
lines.push(`Not reloadable: ${order(unsupported)}.`);
|
|
41
|
+
lines.push("Active work and its captured context were not changed.");
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** The shared `/reload` definition over host-described reload sources. */
|
|
45
|
+
export function reloadCommandDefinitions(options) {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
id: "reload",
|
|
49
|
+
title: "Reload",
|
|
50
|
+
category: "Settings",
|
|
51
|
+
description: "Re-read settings that support it",
|
|
52
|
+
projections: {
|
|
53
|
+
help: {},
|
|
54
|
+
palette: {},
|
|
55
|
+
slash: { aliases: ["reload"] },
|
|
56
|
+
},
|
|
57
|
+
handler: async ({ interaction }) => {
|
|
58
|
+
const report = await reloadSources(options.sources(), interaction?.signal ?? new AbortController().signal);
|
|
59
|
+
interaction?.notify({ type: "info", message: report });
|
|
60
|
+
return report;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
/** Contribute `/reload` to one Whistle host. */
|
|
66
|
+
export function contributeReloadCommand(commands, options) {
|
|
67
|
+
return commands.contribute({
|
|
68
|
+
sourceId: options.sourceId,
|
|
69
|
+
namespace: options.namespace,
|
|
70
|
+
commands: reloadCommandDefinitions(options),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CancelResult } from "@fungi.computer/shiitake/client";
|
|
2
|
+
import type { ParakeetSessionCommand } from "./session.js";
|
|
3
|
+
import type { WhistleRuntime } from "@fungi.computer/whistle";
|
|
4
|
+
import { buildDurableTimeline } from "./core/build-timeline.js";
|
|
5
|
+
import { type SessionViewFacts } from "./core/session-view-state.js";
|
|
6
|
+
export type SessionCommandRuntime = Pick<WhistleRuntime, "contribute">;
|
|
7
|
+
export type SessionCommandSession = Readonly<{
|
|
8
|
+
read: () => Promise<Readonly<{
|
|
9
|
+
sessionId: string;
|
|
10
|
+
messages?: Parameters<typeof buildDurableTimeline>[0];
|
|
11
|
+
compactions?: Parameters<typeof buildDurableTimeline>[1];
|
|
12
|
+
pending: Readonly<{
|
|
13
|
+
hasMore?: boolean;
|
|
14
|
+
prompts: readonly Readonly<{
|
|
15
|
+
commandId: string;
|
|
16
|
+
text: string;
|
|
17
|
+
delivery: "steer" | "followUp";
|
|
18
|
+
}>[];
|
|
19
|
+
}>;
|
|
20
|
+
}> & SessionViewFacts>;
|
|
21
|
+
cancel?: ((input: Readonly<{
|
|
22
|
+
commandId: string;
|
|
23
|
+
onlyIfPending?: boolean;
|
|
24
|
+
}>) => Promise<CancelResult>) | undefined;
|
|
25
|
+
/** Execute a type-discriminated Session command, including `fork`. */
|
|
26
|
+
command: ParakeetSessionCommand;
|
|
27
|
+
}>;
|
|
28
|
+
/** Shared Session actions with optional human presentation callbacks. */
|
|
29
|
+
export type ParakeetSessionCommandOptions = Readonly<{
|
|
30
|
+
/** Host source that owns the contribution lease. */
|
|
31
|
+
sourceId: string;
|
|
32
|
+
/** Namespace for the Session commands. */
|
|
33
|
+
namespace: string;
|
|
34
|
+
/** Open the host's saved-Session chooser. */
|
|
35
|
+
sessionResume?: (() => void | Promise<void>) | undefined;
|
|
36
|
+
/** Whether this host currently supports saved-Session selection. */
|
|
37
|
+
sessionResumeAvailable: boolean;
|
|
38
|
+
/** Restore an owner-withdrawn queued prompt into the host composer. */
|
|
39
|
+
onEditQueued?: (text: string) => void;
|
|
40
|
+
/** Refuse withdrawal while the host has a draft that must be preserved. */
|
|
41
|
+
canEditQueued?: () => boolean;
|
|
42
|
+
/** Navigate to the Session a fork just created. */
|
|
43
|
+
onForked?: ((sessionId: string) => void | Promise<void>) | undefined;
|
|
44
|
+
/** Whether this host currently supports navigating to a forked Session. */
|
|
45
|
+
forkAvailable: boolean;
|
|
46
|
+
/** Show a title the owner just saved; null means the derived title is back. */
|
|
47
|
+
onTitled?: ((title: string | null) => void | Promise<void>) | undefined;
|
|
48
|
+
}>;
|
|
49
|
+
/** Contribute the shared Session command vocabulary to one Whistle host. */
|
|
50
|
+
export declare function contributeSessionCommands(commands: Pick<SessionCommandRuntime, "contribute">, session: SessionCommandSession, options: ParakeetSessionCommandOptions): Readonly<{
|
|
51
|
+
update: (commands: import("@fungi.computer/whistle").WhistleContribution["commands"]) => void;
|
|
52
|
+
dispose: () => void;
|
|
53
|
+
}>;
|