@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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { formatTokens } from "../core/format-tokens.js";
|
|
3
|
+
import { formatModelIdentity } from "../core/model-identity.js";
|
|
4
|
+
import { formatContextUsage } from "../core/format-context.js";
|
|
5
|
+
import { Composer, } from "@fungi.computer/caps/blocks/composer";
|
|
6
|
+
import { isImeComposing } from "@fungi.computer/caps/lib/ime";
|
|
7
|
+
import { RunFailureNotice } from "./RunFailureNotice.js";
|
|
8
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
9
|
+
import { formatMoney } from "@fungi.computer/caps/lib/format-money";
|
|
10
|
+
import { Popover, PopoverAnchor, } from "@fungi.computer/caps/components/popover";
|
|
11
|
+
import { Status } from "@fungi.computer/caps/components/status";
|
|
12
|
+
import { memo, useCallback, useId, useLayoutEffect, useRef, useState, } from "react";
|
|
13
|
+
import { Textarea } from "@fungi.computer/caps/components/textarea";
|
|
14
|
+
import { formatHints } from "../commands/hints.js";
|
|
15
|
+
import { SlashMenu, slashOptionId } from "./SlashMenu.js";
|
|
16
|
+
const presentations = {
|
|
17
|
+
connecting: {
|
|
18
|
+
busy: false,
|
|
19
|
+
tone: "neutral",
|
|
20
|
+
},
|
|
21
|
+
cancelling: {
|
|
22
|
+
busy: true,
|
|
23
|
+
tone: "warning",
|
|
24
|
+
},
|
|
25
|
+
error: {
|
|
26
|
+
busy: false,
|
|
27
|
+
tone: "error",
|
|
28
|
+
},
|
|
29
|
+
idle: {
|
|
30
|
+
busy: false,
|
|
31
|
+
tone: "neutral",
|
|
32
|
+
},
|
|
33
|
+
queued: {
|
|
34
|
+
busy: true,
|
|
35
|
+
tone: "warning",
|
|
36
|
+
},
|
|
37
|
+
retrying: {
|
|
38
|
+
busy: true,
|
|
39
|
+
tone: "warning",
|
|
40
|
+
},
|
|
41
|
+
sending: {
|
|
42
|
+
busy: true,
|
|
43
|
+
tone: "success",
|
|
44
|
+
},
|
|
45
|
+
streaming: {
|
|
46
|
+
busy: true,
|
|
47
|
+
tone: "success",
|
|
48
|
+
},
|
|
49
|
+
thinking: {
|
|
50
|
+
busy: true,
|
|
51
|
+
tone: "warning",
|
|
52
|
+
},
|
|
53
|
+
disconnected: {
|
|
54
|
+
busy: false,
|
|
55
|
+
tone: "neutral",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, slashListId, textareaId, textareaRef, value, }) {
|
|
59
|
+
const latestValue = useRef(value);
|
|
60
|
+
const [history, setHistory] = useState([]);
|
|
61
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
62
|
+
latestValue.current = value;
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const textarea = textareaRef.current;
|
|
65
|
+
if (!textarea)
|
|
66
|
+
return;
|
|
67
|
+
textarea.style.height = "auto";
|
|
68
|
+
textarea.style.height = `${String(Math.min(textarea.scrollHeight, 320))}px`;
|
|
69
|
+
}, [textareaRef, value]);
|
|
70
|
+
const saveAndSend = useCallback(() => {
|
|
71
|
+
const draft = latestValue.current.trim();
|
|
72
|
+
if (!draft)
|
|
73
|
+
return;
|
|
74
|
+
onSend();
|
|
75
|
+
setHistory((previous) => previous.at(-1) === draft ? previous : [...previous, draft]);
|
|
76
|
+
setHistoryIndex(-1);
|
|
77
|
+
}, [onSend]);
|
|
78
|
+
const onKeyDown = useCallback((event) => {
|
|
79
|
+
if (isImeComposing(event))
|
|
80
|
+
return;
|
|
81
|
+
if (slash?.open === true) {
|
|
82
|
+
const handled = event.key === "ArrowDown"
|
|
83
|
+
? slash.move(1)
|
|
84
|
+
: event.key === "ArrowUp"
|
|
85
|
+
? slash.move(-1)
|
|
86
|
+
: event.key === "Tab"
|
|
87
|
+
? slash.complete()
|
|
88
|
+
: event.key === "Escape"
|
|
89
|
+
? slash.dismiss()
|
|
90
|
+
: false;
|
|
91
|
+
if (handled) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (event.key === "Escape") {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
onAbort();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
saveAndSend();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (event.key === "ArrowUp" &&
|
|
107
|
+
textareaRef.current?.selectionStart === 0 &&
|
|
108
|
+
history.length > 0 &&
|
|
109
|
+
(historyIndex === -1 || historyIndex > 0)) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
const next = historyIndex === -1 ? history.length - 1 : historyIndex - 1;
|
|
112
|
+
setHistoryIndex(next);
|
|
113
|
+
const entry = history[next];
|
|
114
|
+
if (entry !== undefined)
|
|
115
|
+
onChange(entry);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (event.key === "ArrowDown" && historyIndex !== -1) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
const next = historyIndex + 1;
|
|
121
|
+
if (next >= history.length) {
|
|
122
|
+
setHistoryIndex(-1);
|
|
123
|
+
onChange("");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
setHistoryIndex(next);
|
|
127
|
+
const entry = history[next];
|
|
128
|
+
if (entry !== undefined)
|
|
129
|
+
onChange(entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, [history, historyIndex, onAbort, onChange, saveAndSend, slash, textareaRef]);
|
|
133
|
+
const listOpen = slash?.open === true && slash.completion.kind !== "none"
|
|
134
|
+
? slash.completion.items.length > 0
|
|
135
|
+
: 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-autocomplete": "list", "aria-controls": listOpen ? slashListId : undefined, "aria-expanded": listOpen, "aria-activedescendant": listOpen && slash !== undefined
|
|
137
|
+
? slashOptionId(slashListId, slash.selected)
|
|
138
|
+
: 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
|
+
onChange(event.target.value);
|
|
140
|
+
setHistoryIndex(-1);
|
|
141
|
+
}, onKeyDown: onKeyDown, placeholder: placeholder ??
|
|
142
|
+
(slash === undefined ? "Ask agent..." : "Ask agent… (/ for commands)"), role: slash === undefined ? undefined : "combobox", ref: textareaRef, rows: 1, value: value })] }));
|
|
143
|
+
}
|
|
144
|
+
/** Shared visible and screen-reader readout of the observed Session state. */
|
|
145
|
+
export function sessionReadout(status, view) {
|
|
146
|
+
if (view.kind === "connecting")
|
|
147
|
+
return "connecting";
|
|
148
|
+
if (view.kind === "disconnected")
|
|
149
|
+
return "unavailable";
|
|
150
|
+
if (status === "sending")
|
|
151
|
+
return "sending prompt";
|
|
152
|
+
if (status === "queued" &&
|
|
153
|
+
(view.kind === "idle" ||
|
|
154
|
+
view.kind === "waiting_for_you" ||
|
|
155
|
+
view.kind === "error"))
|
|
156
|
+
return "prompt accepted";
|
|
157
|
+
switch (view.kind) {
|
|
158
|
+
case "waiting_for_you":
|
|
159
|
+
return "waiting for you";
|
|
160
|
+
case "responding":
|
|
161
|
+
return "writing";
|
|
162
|
+
case "running_tool":
|
|
163
|
+
return `running ${view.tool}`;
|
|
164
|
+
case "retrying":
|
|
165
|
+
return `retrying ${String(view.attempt)}/${String(view.maxAttempts)}`;
|
|
166
|
+
case "error":
|
|
167
|
+
return "failed";
|
|
168
|
+
case "queued":
|
|
169
|
+
return "queued";
|
|
170
|
+
case "cancelling":
|
|
171
|
+
return "cancelling";
|
|
172
|
+
case "compacting":
|
|
173
|
+
return "compacting";
|
|
174
|
+
case "thinking":
|
|
175
|
+
return "thinking";
|
|
176
|
+
case "idle":
|
|
177
|
+
return "idle";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function ComposerFooter({ statusChips, model, status, viewState, usage, context, }) {
|
|
181
|
+
const tone = viewState.kind === "error" ? "error" : presentations[status].tone;
|
|
182
|
+
const label = sessionReadout(status, viewState);
|
|
183
|
+
return (_jsxs("div", { className: "composer-footer", children: [_jsx(Status, { label: label, title: label, tone: tone }), 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
|
+
}
|
|
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, }) {
|
|
186
|
+
const textareaId = useId();
|
|
187
|
+
const slashListId = `${textareaId}-commands`;
|
|
188
|
+
const { busy } = presentations[status];
|
|
189
|
+
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: 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: draft.trim().length === 0, onClick: () => {
|
|
190
|
+
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: draft.trim().length === 0, onClick: () => {
|
|
192
|
+
onSend("steer");
|
|
193
|
+
}, title: "Send", size: "lg", variant: actionVariant, children: "Send" }));
|
|
194
|
+
const composerInput = (_jsx(HostComposerTextarea, { placeholder: placeholder, onChange: onChange, onAbort: onAbort, onSend: () => {
|
|
195
|
+
onSend(busy ? "followUp" : "steer");
|
|
196
|
+
}, slash: slash, slashListId: slashListId, textareaId: textareaId, textareaRef: textareaRef, value: draft }));
|
|
197
|
+
const composerProps = {
|
|
198
|
+
action: composerAction,
|
|
199
|
+
input: composerInput,
|
|
200
|
+
};
|
|
201
|
+
const footerProps = { status, viewState, statusChips };
|
|
202
|
+
if (model)
|
|
203
|
+
footerProps.model = model;
|
|
204
|
+
if (usage)
|
|
205
|
+
footerProps.usage = usage;
|
|
206
|
+
if (context)
|
|
207
|
+
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: "parakeet-key-hints", "aria-live": "polite", children: formatHints(slash.hints) }))] }));
|
|
209
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function ChatMessage({ children, isError, pending, speaker, thinking, markdownPolicy, }: Readonly<{
|
|
2
|
+
children: string;
|
|
3
|
+
isError?: boolean;
|
|
4
|
+
pending?: boolean;
|
|
5
|
+
speaker: "agent" | "user";
|
|
6
|
+
thinking?: string;
|
|
7
|
+
markdownPolicy?: "session" | "visitor";
|
|
8
|
+
}>): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
3
|
+
import { ThinkingBlock } from "./ThinkingBlock.js";
|
|
4
|
+
export function ChatMessage({ children, isError, pending, speaker, thinking, markdownPolicy, }) {
|
|
5
|
+
const content = (_jsx(MarkdownRenderer, { policy: markdownPolicy, children: children }));
|
|
6
|
+
if (speaker === "user") {
|
|
7
|
+
return (_jsx("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
|
+
background: "var(--stipe-surface)",
|
|
9
|
+
borderLeft: "3px solid var(--color-primary)",
|
|
10
|
+
}, children: _jsx("div", { className: "message-body text-base-content", children: content }) }));
|
|
11
|
+
}
|
|
12
|
+
const hasText = children.trim().length > 0;
|
|
13
|
+
if (!hasText && !thinking)
|
|
14
|
+
return null;
|
|
15
|
+
return (_jsxs("div", { "data-speaker": "agent", "data-pending": pending ? "true" : undefined, children: [thinking ? _jsx(ThinkingBlock, { thinking: thinking }) : null, hasText ? (_jsx("div", { className: `px-3 py-2 ${isError ? "text-error" : "text-base-content"}`, children: content })) : null] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClientContributionV1 } from "@fungi.computer/shiitake/client-entry";
|
|
2
|
+
import type { RowV1 } from "@fungi.computer/gills";
|
|
3
|
+
import type { useClientActions } from "../client-contributions.js";
|
|
4
|
+
export type ClientRowsProps = Readonly<{
|
|
5
|
+
entryId: string;
|
|
6
|
+
rows: ClientContributionV1["panel"];
|
|
7
|
+
actions: ReturnType<typeof useClientActions>;
|
|
8
|
+
onOpen?: ((target: NonNullable<RowV1["open"]>) => void) | undefined;
|
|
9
|
+
}>;
|
|
10
|
+
/** Render admitted data; commands and request state remain in Parakeet headless. */
|
|
11
|
+
export declare function ClientRows({ entryId, rows, actions, onOpen, }: ClientRowsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
3
|
+
import { DataList, DataRow, DataListHeading, } from "@fungi.computer/caps/components/data-list";
|
|
4
|
+
import { Status } from "@fungi.computer/caps/components/status";
|
|
5
|
+
import { useSyncExternalStore } from "react";
|
|
6
|
+
import { useParakeetInteractions } from "../conversation.js";
|
|
7
|
+
const rowColors = {
|
|
8
|
+
attention: "text-warning",
|
|
9
|
+
running: "text-info",
|
|
10
|
+
error: "text-error",
|
|
11
|
+
settled: "text-success",
|
|
12
|
+
muted: "text-dim",
|
|
13
|
+
};
|
|
14
|
+
/** Render admitted data; commands and request state remain in Parakeet headless. */
|
|
15
|
+
export function ClientRows({ entryId, rows, actions, onOpen, }) {
|
|
16
|
+
const store = useParakeetInteractions();
|
|
17
|
+
const { inspection } = useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
|
|
18
|
+
if (rows.length === 0)
|
|
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
|
|
21
|
+
? null
|
|
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
|
+
if (row.open !== undefined)
|
|
24
|
+
onOpen?.(row.open);
|
|
25
|
+
}, children: "Open" })), row.actions?.map((action) => {
|
|
26
|
+
const state = actions.state.get(action);
|
|
27
|
+
return (_jsx(Button, { size: "xs", variant: "outline", className: "shrink-0", disabled: !actions.enabled || state?.pending === true, loading: state?.pending === true, onClick: () => {
|
|
28
|
+
void actions.execute(entryId, action);
|
|
29
|
+
}, children: action.label }, action.id));
|
|
30
|
+
})] }), row.inspection !== undefined &&
|
|
31
|
+
inspection?.entryId === entryId &&
|
|
32
|
+
inspection.rowId === row.id ? (_jsxs("section", { "aria-label": row.title, "data-client-inspection": row.id, children: [_jsx(DataListHeading, { aside: _jsx(Button, { size: "xs", variant: "ghost", onClick: () => {
|
|
33
|
+
store.inspect(undefined);
|
|
34
|
+
}, children: "Close" }), children: row.title }), _jsx(DataList, { density: "compact", tabIndex: 0, className: "max-h-64 overflow-y-auto overscroll-contain", children: row.inspection.rows.map((child) => (_jsxs(DataRow, { children: [_jsx(Status, { glyph: "dot", size: "compact", tone: child.tone === "settled"
|
|
35
|
+
? "success"
|
|
36
|
+
: child.tone === "running"
|
|
37
|
+
? "info"
|
|
38
|
+
: child.tone === "error"
|
|
39
|
+
? "error"
|
|
40
|
+
: child.tone === "attention"
|
|
41
|
+
? "warning"
|
|
42
|
+
: "neutral", label: child.title }), child.detail === undefined ? null : (_jsx("span", { children: child.detail }))] }, child.id))) })] })) : null, row.actions?.map((action) => {
|
|
43
|
+
const error = actions.state.get(action)?.error;
|
|
44
|
+
return error === undefined ? null : (_jsx("p", { className: "text-error", role: "alert", children: error }, action.id));
|
|
45
|
+
})] }, row.id))) }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
export type CodeBlockProps = {
|
|
3
|
+
className?: string;
|
|
4
|
+
content: string;
|
|
5
|
+
header?: ReactNode;
|
|
6
|
+
hideCopy?: boolean;
|
|
7
|
+
language?: string;
|
|
8
|
+
maxHeight?: string;
|
|
9
|
+
style?: CSSProperties;
|
|
10
|
+
};
|
|
11
|
+
export declare function CodeBlock({ className, content, header, hideCopy, language, maxHeight, style, }: CodeBlockProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check, Copy } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { reportFailure } from "../lib/failure.js";
|
|
5
|
+
import { highlightToHtml } from "@fungi.computer/caps/lib/tanstack-highlight";
|
|
6
|
+
const HIGHLIGHT_THRESHOLD = 10_000;
|
|
7
|
+
export function CodeBlock({ className, content, header, hideCopy, language, maxHeight = "320px", style, }) {
|
|
8
|
+
const [copyState, setCopyState] = useState("idle");
|
|
9
|
+
const handleCopy = async () => {
|
|
10
|
+
try {
|
|
11
|
+
await navigator.clipboard.writeText(content);
|
|
12
|
+
setCopyState("copied");
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
setCopyState("idle");
|
|
15
|
+
}, 1500);
|
|
16
|
+
}
|
|
17
|
+
catch (cause) {
|
|
18
|
+
reportFailure("copy code to clipboard", cause);
|
|
19
|
+
setCopyState("failed");
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const shouldHighlight = Boolean(language) && content.length <= HIGHLIGHT_THRESHOLD;
|
|
23
|
+
const innerHtml = shouldHighlight ? highlightToHtml(content, language) : null;
|
|
24
|
+
const containerStyle = {
|
|
25
|
+
background: "var(--stipe-surface)",
|
|
26
|
+
border: "1px solid var(--color-base-300)",
|
|
27
|
+
color: "var(--color-base-content)",
|
|
28
|
+
...style,
|
|
29
|
+
};
|
|
30
|
+
return (_jsxs("div", { className: `relative not-prose ${className ?? ""}`, style: containerStyle, children: [header ? (_jsx("div", { className: "text-dim px-2 pt-1.5 pb-1 text-[9px] uppercase tracking-wider", children: header })) : null, !hideCopy ? (_jsx("button", { "aria-label": "Copy to clipboard", className: "btn btn-circle btn-ghost btn-xs text-dim absolute top-1 right-1 z-10 hover:text-base-content", onClick: () => {
|
|
31
|
+
void handleCopy();
|
|
32
|
+
}, type: "button", children: copyState === "copied" ? _jsx(Check, { size: 10 }) : _jsx(Copy, { size: 10 }) })) : null, !hideCopy ? (_jsx("span", { "aria-live": "polite", className: "sr-only", role: "status", children: copyState === "failed" ? "Copy to clipboard failed." : null })) : null, _jsx("div", { className: "overflow-y-auto text-[10px]", "data-scrollable": true, style: {
|
|
33
|
+
maxHeight: maxHeight === "0" ? undefined : maxHeight,
|
|
34
|
+
padding: "6px 4px",
|
|
35
|
+
}, children: innerHtml === null ? (_jsxs("pre", { className: "whitespace-pre-wrap break-words font-mono text-base-content", style: { margin: 0 }, children: [content, content.length > HIGHLIGHT_THRESHOLD ? (_jsx("span", { className: "text-dim ml-2 italic", children: "(syntax highlighting skipped for very long content)" })) : null] })) : (_jsx("div", {
|
|
36
|
+
// TanStack Highlight escapes source text; output is token spans.
|
|
37
|
+
dangerouslySetInnerHTML: { __html: innerHtml } })) })] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WhistleInteractionHost } from "@fungi.computer/whistle/interaction";
|
|
2
|
+
/** The interaction to draw and the page's close and retry callbacks. */
|
|
3
|
+
export type InteractionPanelProps = Readonly<{
|
|
4
|
+
/** The renderer-neutral interaction store. */
|
|
5
|
+
host: WhistleInteractionHost;
|
|
6
|
+
/** Close a settled interaction. */
|
|
7
|
+
onClose?: () => void;
|
|
8
|
+
/** Run the same command again after a failure. */
|
|
9
|
+
onRetry?: () => void;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Browser renderer for one Whistle interaction. It draws the same host
|
|
13
|
+
* snapshot the terminal draws; the command, not the page, defines the steps.
|
|
14
|
+
*/
|
|
15
|
+
export declare function InteractionPanel({ host, onClose, onRetry, }: InteractionPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Alert } from "@fungi.computer/caps/components/alert";
|
|
3
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
4
|
+
import { Field } from "@fungi.computer/caps/components/field";
|
|
5
|
+
import { Input } from "@fungi.computer/caps/components/input";
|
|
6
|
+
import { Listbox } from "@fungi.computer/caps/components/listbox";
|
|
7
|
+
import { Status } from "@fungi.computer/caps/components/status";
|
|
8
|
+
import { Display } from "@fungi.computer/caps/components/surface";
|
|
9
|
+
import { useState, useSyncExternalStore } from "react";
|
|
10
|
+
function PromptView({ host, pending, }) {
|
|
11
|
+
const [text, setText] = useState("");
|
|
12
|
+
const request = pending.request;
|
|
13
|
+
if (request.type === "choice")
|
|
14
|
+
return (_jsxs("div", { className: "flex flex-col gap-xs", children: [_jsx("p", { className: "font-medium", children: request.message }), _jsx(Listbox, { "aria-label": request.message, items: request.options, getKey: (option) => option.value, getLabel: (option) => option.label, onValueChange: (value) => {
|
|
15
|
+
if (value !== null)
|
|
16
|
+
host.answer(pending.sequence, value);
|
|
17
|
+
}, renderItem: (option) => (_jsxs("span", { className: "flex flex-col", children: [_jsx("span", { children: option.label }), option.description === undefined ? null : (_jsx("span", { className: "text-sm opacity-70", children: option.description }))] })) })] }));
|
|
18
|
+
if (request.type === "confirmation")
|
|
19
|
+
return (_jsxs("div", { className: "flex flex-col gap-sm", children: [_jsx("p", { children: request.message }), _jsxs("div", { className: "flex justify-end gap-xs", children: [_jsx(Button, { variant: "ghost", onClick: () => host.answer(pending.sequence, false), children: request.cancelLabel ?? "Cancel" }), _jsx(Button, { onClick: () => host.answer(pending.sequence, true), children: request.confirmLabel ?? "Confirm" })] })] }));
|
|
20
|
+
const submit = (event) => {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
host.answer(pending.sequence, text);
|
|
23
|
+
};
|
|
24
|
+
return (_jsxs("form", { className: "flex flex-col gap-sm", onSubmit: submit, children: [_jsx(Field, { label: request.message, children: _jsx(Input, { autoComplete: "off", autoFocus: true, placeholder: request.placeholder, type: request.secret === true ? "password" : "text", value: text, onChange: (event) => {
|
|
25
|
+
setText(event.target.value);
|
|
26
|
+
} }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", children: "Continue" }) })] }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Browser renderer for one Whistle interaction. It draws the same host
|
|
30
|
+
* snapshot the terminal draws; the command, not the page, defines the steps.
|
|
31
|
+
*/
|
|
32
|
+
export function InteractionPanel({ host, onClose, onRetry, }) {
|
|
33
|
+
const state = useSyncExternalStore(host.subscribe, host.snapshot);
|
|
34
|
+
const active = state.status === "working" || state.status === "prompting";
|
|
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 ??
|
|
36
|
+
(state.handoff.code === undefined
|
|
37
|
+
? "Continue in your browser"
|
|
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
|
+
host.cancel();
|
|
40
|
+
onClose?.();
|
|
41
|
+
}, children: "Close" }), onRetry === undefined ? null : (_jsx(Button, { onClick: onRetry, children: "Try again" }))] })) : null, state.status === "done" || state.status === "cancelled" ? (_jsx(Button, { onClick: onClose, children: "Done" })) : null] })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type MarkdownRendererProps = {
|
|
2
|
+
children: string;
|
|
3
|
+
policy?: "session" | "visitor" | undefined;
|
|
4
|
+
};
|
|
5
|
+
export declare const MarkdownPolicyContext: import("react").Context<"session" | "visitor">;
|
|
6
|
+
export declare function MarkdownRenderer({ children, policy }: MarkdownRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Markdown } from "@fungi.computer/caps/components/markdown";
|
|
3
|
+
import { cn } from "@fungi.computer/caps/lib/cn";
|
|
4
|
+
import { createContext, useContext } from "react";
|
|
5
|
+
export const MarkdownPolicyContext = createContext("session");
|
|
6
|
+
const components = {
|
|
7
|
+
blockquote({ children }) {
|
|
8
|
+
return (_jsx("blockquote", { className: "text-dim my-2 border-l-2 border-primary pl-3 italic", children: children }));
|
|
9
|
+
},
|
|
10
|
+
pre({ children, className, ...props }) {
|
|
11
|
+
return (_jsx("pre", { ...props, className: cn("whitespace-pre-wrap break-words", className), children: children }));
|
|
12
|
+
},
|
|
13
|
+
img({ alt, src }) {
|
|
14
|
+
const source = src ?? "";
|
|
15
|
+
if (source.startsWith("/")) {
|
|
16
|
+
return (_jsxs("div", { className: "my-2 px-3 py-2 font-mono text-[10px]", style: {
|
|
17
|
+
background: "var(--stipe-surface-selected)",
|
|
18
|
+
border: "1px solid var(--color-base-300)",
|
|
19
|
+
}, children: [_jsx("span", { className: "text-dim", children: alt ?? "Generated image" }), _jsx("span", { className: "text-dim mx-2", children: "\u00B7" }), _jsx("span", { style: { color: "var(--color-base-content)" }, children: source })] }));
|
|
20
|
+
}
|
|
21
|
+
return (_jsx("img", { alt: alt ?? "Image", className: "my-2 max-w-full", src: source, style: { maxHeight: 400 } }));
|
|
22
|
+
},
|
|
23
|
+
strong({ children }) {
|
|
24
|
+
return _jsx("strong", { className: "font-semibold text-primary", children: children });
|
|
25
|
+
},
|
|
26
|
+
table({ children }) {
|
|
27
|
+
return (_jsx("table", { className: "my-2 w-full border-collapse text-xs", children: children }));
|
|
28
|
+
},
|
|
29
|
+
thead({ children }) {
|
|
30
|
+
return (_jsx("thead", { style: { background: "var(--stipe-surface-selected)" }, children: children }));
|
|
31
|
+
},
|
|
32
|
+
th({ children }) {
|
|
33
|
+
return (_jsx("th", { className: "border px-3 py-2 text-left font-semibold", style: { borderColor: "var(--color-base-300)" }, children: children }));
|
|
34
|
+
},
|
|
35
|
+
td({ children }) {
|
|
36
|
+
return (_jsx("td", { className: "border px-3 py-2", style: { borderColor: "var(--color-base-300)" }, children: children }));
|
|
37
|
+
},
|
|
38
|
+
tr({ children }) {
|
|
39
|
+
return _jsx("tr", { children: children });
|
|
40
|
+
},
|
|
41
|
+
tbody({ children }) {
|
|
42
|
+
return _jsx("tbody", { children: children });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export function MarkdownRenderer({ children, policy }) {
|
|
46
|
+
const inheritedPolicy = useContext(MarkdownPolicyContext);
|
|
47
|
+
return (_jsx(Markdown, { className: "markdown-renderer", components: components, frontmatter: false, policy: (policy ?? inheritedPolicy) === "visitor" ? "visitor" : "default", typography: "inherit", children: children }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComposerController } from "../conversation.js";
|
|
2
|
+
/** Session failures do not carry a billing receipt; never promise a refund. */
|
|
3
|
+
export declare function RunFailureNotice({ failure, cause, onRetry, }: Readonly<{
|
|
4
|
+
failure: NonNullable<ComposerController["failure"]>;
|
|
5
|
+
cause?: string | undefined;
|
|
6
|
+
onRetry: () => void;
|
|
7
|
+
}>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Alert } from "@fungi.computer/caps/components/alert";
|
|
3
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
4
|
+
const messages = {
|
|
5
|
+
admission: "Your prompt didn’t make it through. Let’s give it another go.",
|
|
6
|
+
failed: "The run stopped. Check the message above before trying again.",
|
|
7
|
+
interrupted: "The worker stopped before this run finished. Your prompt is still here.",
|
|
8
|
+
cancelled: "The run was cancelled. Send a new prompt when you’re ready.",
|
|
9
|
+
};
|
|
10
|
+
/** Session failures do not carry a billing receipt; never promise a refund. */
|
|
11
|
+
export function RunFailureNotice({ failure, cause, onRetry, }) {
|
|
12
|
+
return (_jsx(Alert, { tone: "warning", title: failure === "cancelled" ? "Taking a breather" : "A little tangle", description: _jsxs(_Fragment, { children: [messages[failure], " If the model didn\u2019t start, there\u2019s no charge. Model work already done can still use credits.", cause ? _jsx("span", { className: "block", children: cause }) : null] }), action: failure === "cancelled" ? undefined : (_jsx(Button, { "aria-label": "Retry prompt", variant: "outline", onClick: onRetry, children: "Retry" })) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Avatar } from "@fungi.computer/sprite/avatar";
|
|
2
|
+
import { type AvatarPresentation, type SpriteService } from "@fungi.computer/sprite";
|
|
3
|
+
/** The current activity and expression supplied to a composer avatar. */
|
|
4
|
+
export type AvatarState = Readonly<Pick<AvatarPresentation, "activity" | "expression">>;
|
|
5
|
+
/**
|
|
6
|
+
* Shiitake's sprite head, drawn in the activity and expression it is given.
|
|
7
|
+
* With no `avatar`, this is the product mascot, exactly as before avatars
|
|
8
|
+
* existed: no seeded colour, the character's own defaults.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ShiitakeAvatar: import("react").NamedExoticComponent<{
|
|
11
|
+
/** The open conversation's avatar; omitted or `null` draws the mascot. */
|
|
12
|
+
avatar?: Avatar | null;
|
|
13
|
+
/** Replaces the default 3rem size, for a host that sizes the head itself. */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Replaces the shared Shiitake service; a faithful fake for tests. */
|
|
16
|
+
service?: SpriteService;
|
|
17
|
+
/** Activity and expression presented by the avatar. */
|
|
18
|
+
state: AvatarState;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "@fungi.computer/sprite";
|
|
3
|
+
import { Sprite } from "@fungi.computer/sprite/react";
|
|
4
|
+
import { memo, useMemo } from "react";
|
|
5
|
+
import { avatarPresentation } from "./avatar-presentation.js";
|
|
6
|
+
import { shiitakeSprite } from "./sprite-service.js";
|
|
7
|
+
/**
|
|
8
|
+
* Shiitake's sprite head, drawn in the activity and expression it is given.
|
|
9
|
+
* With no `avatar`, this is the product mascot, exactly as before avatars
|
|
10
|
+
* existed: no seeded colour, the character's own defaults.
|
|
11
|
+
*/
|
|
12
|
+
export const ShiitakeAvatar = memo(function ShiitakeAvatar({ avatar, className = "h-12 w-12 shrink-0", service = shiitakeSprite, state, }) {
|
|
13
|
+
const presentation = useMemo(() => (avatar ? avatarPresentation(avatar, "shiitake-mascot") : undefined), [avatar]);
|
|
14
|
+
return (_jsx(Sprite, { "aria-hidden": "true", className: className, service: service, src: presentation?.src ?? { local: "mushroom" }, ...(presentation
|
|
15
|
+
? {
|
|
16
|
+
appearance: presentation.appearance,
|
|
17
|
+
renderStyle: presentation.renderStyle,
|
|
18
|
+
}
|
|
19
|
+
: {}), ...state }));
|
|
20
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SlashCommandController } from "../commands/use-slash-commands.js";
|
|
2
|
+
/** Stable id of one slash row, for `aria-activedescendant`. */
|
|
3
|
+
export declare function slashOptionId(listId: string, index: number): string;
|
|
4
|
+
/** The browser slash menu: draws Parakeet's completion rows, nothing more. */
|
|
5
|
+
export declare function SlashMenu({ slash, listId, }: Readonly<{
|
|
6
|
+
slash: SlashCommandController;
|
|
7
|
+
listId: string;
|
|
8
|
+
}>): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PopoverOption } from "@fungi.computer/caps/components/popover";
|
|
3
|
+
/** Split a label into matched and unmatched runs (UTF-16 indices, like the matcher). */
|
|
4
|
+
function emphasize(label, matches) {
|
|
5
|
+
if (matches.length === 0)
|
|
6
|
+
return label;
|
|
7
|
+
const marked = new Set(matches);
|
|
8
|
+
const runs = [];
|
|
9
|
+
for (let index = 0; index < label.length; index += 1) {
|
|
10
|
+
const isMarked = marked.has(index);
|
|
11
|
+
const last = runs.at(-1);
|
|
12
|
+
if (last?.marked === isMarked)
|
|
13
|
+
last.text += label.charAt(index);
|
|
14
|
+
else
|
|
15
|
+
runs.push({ text: label.charAt(index), marked: isMarked, start: index });
|
|
16
|
+
}
|
|
17
|
+
return runs.map((run) => run.marked ? (_jsx("mark", { className: "parakeet-slash-match", children: run.text }, run.start)) : (_jsx("span", { children: run.text }, run.start)));
|
|
18
|
+
}
|
|
19
|
+
/** Stable id of one slash row, for `aria-activedescendant`. */
|
|
20
|
+
export function slashOptionId(listId, index) {
|
|
21
|
+
return `${listId}-${String(index)}`;
|
|
22
|
+
}
|
|
23
|
+
/** The browser slash menu: draws Parakeet's completion rows, nothing more. */
|
|
24
|
+
export function SlashMenu({ slash, listId, }) {
|
|
25
|
+
const { completion, selected } = slash;
|
|
26
|
+
if (completion.kind === "none")
|
|
27
|
+
return null;
|
|
28
|
+
const rows = completion.kind === "commands"
|
|
29
|
+
? completion.items.map((item) => ({
|
|
30
|
+
key: item.command.commandId + item.label,
|
|
31
|
+
label: emphasize(item.label, item.matches),
|
|
32
|
+
detail: item.command.grammar === undefined
|
|
33
|
+
? item.command.title
|
|
34
|
+
: `${item.command.title} · ${item.command.grammar.hint}`,
|
|
35
|
+
keys: item.command.keys.join(", "),
|
|
36
|
+
disabled: item.command.availability.status === "unavailable",
|
|
37
|
+
}))
|
|
38
|
+
: completion.items.map((item) => ({
|
|
39
|
+
key: item.label,
|
|
40
|
+
label: emphasize(item.label, item.matches),
|
|
41
|
+
detail: item.word.description ?? "",
|
|
42
|
+
keys: "",
|
|
43
|
+
disabled: false,
|
|
44
|
+
}));
|
|
45
|
+
return (_jsxs("div", { className: "parakeet-slash-menu", "data-kind": completion.kind, children: [completion.kind === "arguments" ? (_jsx("p", { className: "parakeet-slash-hint", children: completion.hint })) : null, rows.length === 0 ? null : (_jsx("div", { id: listId, role: "listbox", "aria-label": "Commands", children: rows.map((row, index) => (_jsxs(PopoverOption, { id: slashOptionId(listId, index), active: index === selected, "aria-disabled": row.disabled || undefined, "data-disabled": row.disabled, onMouseEnter: () => {
|
|
46
|
+
slash.highlight(index);
|
|
47
|
+
}, onClick: () => {
|
|
48
|
+
slash.choose(index);
|
|
49
|
+
}, children: [_jsx("span", { className: "parakeet-slash-label", children: row.label }), _jsx("span", { className: "parakeet-slash-detail", children: row.detail }), row.keys.length === 0 ? null : (_jsx("kbd", { className: "parakeet-slash-keys", children: row.keys }))] }, row.key))) }))] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
3
|
+
import { memo, useState } from "react";
|
|
4
|
+
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
5
|
+
export const ThinkingBlock = memo(function ThinkingBlock({ thinking, }) {
|
|
6
|
+
const [expanded, setExpanded] = useState(false);
|
|
7
|
+
return (_jsxs("div", { children: [_jsxs("button", { "aria-expanded": expanded, className: "flex h-8 w-full items-center gap-2 px-3 text-left text-[10px] uppercase tracking-wider transition-colors", onClick: () => {
|
|
8
|
+
setExpanded((previous) => !previous);
|
|
9
|
+
}, style: {
|
|
10
|
+
color: "var(--stipe-fg-muted)",
|
|
11
|
+
fontFamily: "var(--font-sans)",
|
|
12
|
+
fontStyle: "italic",
|
|
13
|
+
}, type: "button", children: ["Thoughts", _jsx("span", { "aria-hidden": "true", className: "btn btn-circle btn-ghost btn-xs ml-auto text-dim", style: { transform: expanded ? "rotate(90deg)" : "rotate(0deg)" }, children: _jsx(ChevronRight, { size: 12 }) })] }), expanded ? (_jsx("div", { className: "max-w-[80ch] break-words px-3 pb-2 text-[11px]", style: {
|
|
14
|
+
color: "var(--stipe-fg-muted)",
|
|
15
|
+
fontFamily: "var(--font-sans)",
|
|
16
|
+
fontStyle: "italic",
|
|
17
|
+
}, children: _jsx(MarkdownRenderer, { children: thinking }) })) : null] }));
|
|
18
|
+
});
|