@fungi.computer/parakeet 0.4.0 → 0.6.0
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 +17 -1
- package/dist/client-contributions.d.ts +1 -0
- package/dist/commands/awaiting-driver.d.ts +20 -0
- package/dist/commands/awaiting-driver.js +121 -0
- package/dist/commands/builtins.js +6 -4
- package/dist/commands/interactions.d.ts +25 -0
- package/dist/commands/interactions.js +76 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/slash.d.ts +7 -3
- package/dist/commands/slash.js +2 -0
- package/dist/commands/use-slash-commands.d.ts +13 -0
- package/dist/commands/use-slash-commands.js +58 -1
- package/dist/components/ChatComposer.d.ts +2 -0
- package/dist/components/ChatComposer.js +158 -40
- package/dist/components/ClientRows.js +42 -26
- package/dist/components/CodeBlock.js +3 -2
- package/dist/components/CompletionMenu.d.ts +54 -0
- package/dist/components/CompletionMenu.js +52 -0
- package/dist/components/ComposerModelKey.d.ts +11 -0
- package/dist/components/ComposerModelKey.js +9 -0
- package/dist/components/ComposerShell.d.ts +47 -0
- package/dist/components/ComposerShell.js +29 -0
- package/dist/components/DecisionComposer.d.ts +30 -0
- package/dist/components/DecisionComposer.js +145 -0
- package/dist/components/InteractionPanel.d.ts +10 -1
- package/dist/components/InteractionPanel.js +9 -4
- package/dist/components/SlashMenu.d.ts +1 -3
- package/dist/components/SlashMenu.js +5 -13
- package/dist/components/TimelineItem.d.ts +1 -0
- package/dist/components/TimelineItem.js +4 -4
- package/dist/components/ToolGroup.d.ts +1 -0
- package/dist/components/ToolGroup.js +8 -8
- package/dist/conversation-opening.d.ts +1 -1
- package/dist/conversation-opening.js +2 -2
- package/dist/conversation.d.ts +5 -1
- package/dist/conversation.js +84 -29
- package/dist/core/approval-decision.d.ts +198 -0
- package/dist/core/approval-decision.js +291 -0
- package/dist/core/awaiting-answer.d.ts +229 -0
- package/dist/core/awaiting-answer.js +303 -0
- package/dist/core/build-timeline.js +61 -7
- package/dist/core/client-actions.d.ts +1 -0
- package/dist/core/client-actions.js +7 -1
- package/dist/core/composer-model-name.d.ts +3 -0
- package/dist/core/composer-model-name.js +21 -0
- package/dist/core/display-text.d.ts +2 -0
- package/dist/core/display-text.js +7 -0
- package/dist/core/draft-storage.d.ts +6 -0
- package/dist/core/draft-storage.js +25 -5
- package/dist/core/session-projection.d.ts +1 -0
- package/dist/core/session-projection.js +3 -2
- package/dist/core/session-query.d.ts +8 -0
- package/dist/core/session-query.js +13 -11
- package/dist/core/session-view-state.d.ts +2 -4
- package/dist/core/session-view-state.js +27 -9
- package/dist/core/tool-groups.d.ts +3 -0
- package/dist/core/tool-groups.js +37 -1
- package/dist/core/tool-view.d.ts +6 -0
- package/dist/core/tool-view.js +33 -0
- package/dist/core/use-timeline-item-key.d.ts +3 -4
- package/dist/core/use-timeline-item-key.js +8 -14
- package/dist/headless.d.ts +10 -5
- package/dist/headless.js +9 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/parakeet.d.ts +6 -2
- package/dist/parakeet.js +24 -17
- package/dist/store/types.d.ts +14 -1
- package/dist/styles.css +911 -162
- package/dist/use-approval-awaiting.d.ts +22 -0
- package/dist/use-approval-awaiting.js +81 -0
- package/dist/use-decision.d.ts +20 -0
- package/dist/use-decision.js +79 -0
- package/docs/api/commands/slash.md +30 -26
- package/docs/api/headless.md +917 -54
- package/docs/api/index.md +166 -5
- package/package.json +17 -8
- package/src/completion-menu.css +72 -0
- package/src/composer-shell.css +125 -0
- package/src/styles.css +99 -53
- package/dist/components/WaitingNotice.d.ts +0 -7
- package/dist/components/WaitingNotice.js +0 -8
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AwaitingItem, DecisionIntent, DecisionView } from "../core/awaiting-answer.js";
|
|
2
|
+
/** Props for the decision that has taken over the composer. */
|
|
3
|
+
export type DecisionComposerProps = Readonly<{
|
|
4
|
+
/** The waiting decision to answer. */
|
|
5
|
+
item: AwaitingItem;
|
|
6
|
+
/** One-based place among every waiting decision. */
|
|
7
|
+
position: number;
|
|
8
|
+
/** Every waiting decision. */
|
|
9
|
+
total: number;
|
|
10
|
+
/** Postpone the decision; it waits in the footer row. */
|
|
11
|
+
onLater: () => void;
|
|
12
|
+
/** The person was typing in the composer, so focus follows into the list. */
|
|
13
|
+
takeFocus: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* The composer while the Agent waits on the person: the decision's rows,
|
|
17
|
+
* an Other line, and Send / Later. It binds one waiting decision to its
|
|
18
|
+
* reducer and draws the result.
|
|
19
|
+
*/
|
|
20
|
+
export declare function DecisionComposer({ item, position, total, onLater, takeFocus, }: DecisionComposerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
21
|
+
/**
|
|
22
|
+
* Draw Parakeet's decision view with Caps parts and forward keys and clicks as
|
|
23
|
+
* intents; the rules live in the reducer, so every surface answers alike.
|
|
24
|
+
*/
|
|
25
|
+
export declare function DecisionPanel({ view, dispatch, onLater, takeFocus, }: Readonly<{
|
|
26
|
+
view: DecisionView;
|
|
27
|
+
dispatch: (intent: DecisionIntent) => void;
|
|
28
|
+
onLater: () => void;
|
|
29
|
+
takeFocus: boolean;
|
|
30
|
+
}>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Checkbox } from "@fungi.computer/caps/components/checkbox";
|
|
3
|
+
import { KeycapButton } from "@fungi.computer/caps/components/keycap";
|
|
4
|
+
import { Input } from "@fungi.computer/caps/components/input";
|
|
5
|
+
import { Listbox } from "@fungi.computer/caps/components/listbox";
|
|
6
|
+
import { isImeComposing } from "@fungi.computer/caps/lib/ime";
|
|
7
|
+
import { useEffect, useRef } from "react";
|
|
8
|
+
import { useDecision } from "../use-decision.js";
|
|
9
|
+
import { PromptHeader } from "./InteractionPanel.js";
|
|
10
|
+
const kindNames = { question: "Question", approval: "Approval" };
|
|
11
|
+
/**
|
|
12
|
+
* The composer while the Agent waits on the person: the decision's rows,
|
|
13
|
+
* an Other line, and Send / Later. It binds one waiting decision to its
|
|
14
|
+
* reducer and draws the result.
|
|
15
|
+
*/
|
|
16
|
+
export function DecisionComposer({ item, position, total, onLater, takeFocus, }) {
|
|
17
|
+
const decision = useDecision(item, { position, total, later: onLater });
|
|
18
|
+
if (decision.view === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
return (_jsx(DecisionPanel, { view: decision.view, dispatch: decision.dispatch, onLater: decision.later, takeFocus: takeFocus }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Draw Parakeet's decision view with Caps parts and forward keys and clicks as
|
|
24
|
+
* intents; the rules live in the reducer, so every surface answers alike.
|
|
25
|
+
*/
|
|
26
|
+
export function DecisionPanel({ view, dispatch, onLater, takeFocus, }) {
|
|
27
|
+
const list = useRef(null);
|
|
28
|
+
const input = useRef(null);
|
|
29
|
+
const typing = view.phase === "other";
|
|
30
|
+
const wasTyping = useRef(false);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (takeFocus)
|
|
33
|
+
list.current?.focus();
|
|
34
|
+
}, [takeFocus]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (typing)
|
|
37
|
+
input.current?.focus();
|
|
38
|
+
else if (wasTyping.current && document.activeElement === input.current)
|
|
39
|
+
list.current?.focus();
|
|
40
|
+
wasTyping.current = typing;
|
|
41
|
+
}, [typing]);
|
|
42
|
+
const rows = view.rows;
|
|
43
|
+
const field = view.other;
|
|
44
|
+
const sending = view.phase === "sending";
|
|
45
|
+
const act = (event, handled, run) => {
|
|
46
|
+
if (!handled)
|
|
47
|
+
return false;
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
run();
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
const onListKey = (event) => {
|
|
53
|
+
// Keys typed in the Other row's field belong to the field.
|
|
54
|
+
if (event.target instanceof HTMLInputElement)
|
|
55
|
+
return;
|
|
56
|
+
if (isImeComposing(event))
|
|
57
|
+
return;
|
|
58
|
+
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
59
|
+
return;
|
|
60
|
+
const { key } = event;
|
|
61
|
+
if (act(event, key === "ArrowDown", () => {
|
|
62
|
+
dispatch({ type: "move", delta: 1 });
|
|
63
|
+
}) ||
|
|
64
|
+
act(event, key === "ArrowUp", () => {
|
|
65
|
+
dispatch({ type: "move", delta: -1 });
|
|
66
|
+
}) ||
|
|
67
|
+
act(event, key === "Enter", () => {
|
|
68
|
+
dispatch({ type: "enter" });
|
|
69
|
+
}) ||
|
|
70
|
+
act(event, key === " ", () => {
|
|
71
|
+
dispatch({ type: "toggle" });
|
|
72
|
+
}) ||
|
|
73
|
+
act(event, key === "Escape", () => {
|
|
74
|
+
dispatch({ type: "escape" });
|
|
75
|
+
}) ||
|
|
76
|
+
act(event, /^[1-9]$/u.test(key), () => {
|
|
77
|
+
dispatch({ type: "pick", number: Number(key) });
|
|
78
|
+
}) ||
|
|
79
|
+
act(event, key.length === 1, () => {
|
|
80
|
+
dispatch({ type: "append", text: key });
|
|
81
|
+
}))
|
|
82
|
+
return;
|
|
83
|
+
};
|
|
84
|
+
const onOtherKey = (event) => {
|
|
85
|
+
if (isImeComposing(event))
|
|
86
|
+
return;
|
|
87
|
+
const { key } = event;
|
|
88
|
+
if (act(event, key === "ArrowDown", () => {
|
|
89
|
+
dispatch({ type: "move", delta: 1 });
|
|
90
|
+
}) ||
|
|
91
|
+
act(event, key === "ArrowUp", () => {
|
|
92
|
+
dispatch({ type: "move", delta: -1 });
|
|
93
|
+
}) ||
|
|
94
|
+
act(event, key === "Enter", () => {
|
|
95
|
+
dispatch({ type: "enter" });
|
|
96
|
+
}) ||
|
|
97
|
+
act(event, key === "Escape", () => {
|
|
98
|
+
dispatch({ type: "escape" });
|
|
99
|
+
}))
|
|
100
|
+
return;
|
|
101
|
+
};
|
|
102
|
+
// A multiple decision's marker is a Caps check, not `[x]` text. The row
|
|
103
|
+
// itself is the control: the option owns aria-selected and the click, so
|
|
104
|
+
// the check is drawn for sight only.
|
|
105
|
+
const check = (row) => (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none flex shrink-0", children: _jsx(Checkbox, { checked: row.toggled, readOnly: true, tabIndex: -1 }) }));
|
|
106
|
+
const words = (row) => (_jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: row.destructive ? "text-error" : undefined, children: view.multiple ? row.caption : row.text }), row.description === undefined ? null : (_jsx("span", { className: "text-sm opacity-70", children: row.description }))] }));
|
|
107
|
+
const rowContent = (row) => view.multiple ? (_jsxs("span", { className: "flex items-start gap-xs", children: [check(row), words(row)] })) : (words(row));
|
|
108
|
+
const picked = rows.find((row) => row.highlighted);
|
|
109
|
+
return (_jsxs("div", { "aria-label": view.label, className: "parakeet-decision flex flex-col gap-xs p-sm", "data-kind": view.kind, "data-phase": view.phase, role: "group", children: [_jsx(PromptHeader, { request: {
|
|
110
|
+
message: view.title,
|
|
111
|
+
summary: view.summary,
|
|
112
|
+
facts: view.facts,
|
|
113
|
+
body: view.body,
|
|
114
|
+
bodyLabel: view.bodyLabel,
|
|
115
|
+
details: view.details.length === 0 ? undefined : view.details,
|
|
116
|
+
}, trailing: _jsx("span", { className: "shrink-0 text-sm opacity-70", children: view.counter ?? kindNames[view.kind] }) }), _jsx(Listbox, { "aria-label": view.title, className: "parakeet-decision-list", getKey: (row) => String(row.index), getLabel: (row) => row.label, isDisabled: () => sending, items: rows, rootRef: list, renderItem: (row) => row.kind === "other" && field !== undefined ? (_jsxs("span", { className: "flex items-center gap-xs", "data-highlight": row.highlighted || undefined, "data-row": "other", children: [view.multiple ? check(row) : null, _jsx("span", { className: "shrink-0", children: view.multiple ? row.caption : row.text }), row.highlighted || field.text !== "" ? (_jsx(Input, { "aria-label": field.label, autoComplete: "off", disabled: sending, maxLength: field.maxLength, onChange: (event) => {
|
|
117
|
+
dispatch({ type: "type", text: event.target.value });
|
|
118
|
+
}, onKeyDown: onOtherKey, placeholder: field.placeholder, ref: input, value: field.text })) : null] })) : (_jsx("span", { "data-highlight": row.highlighted || undefined, children: rowContent(row) })), onKeyDown: onListKey, ...(view.multiple
|
|
119
|
+
? {
|
|
120
|
+
multiple: true,
|
|
121
|
+
value: rows
|
|
122
|
+
.filter((row) => row.toggled)
|
|
123
|
+
.map((row) => String(row.index)),
|
|
124
|
+
onValueChange: (_value, selected) => {
|
|
125
|
+
const clicked = rows.find((row) => selected.some((chosen) => chosen.index === row.index) !==
|
|
126
|
+
row.toggled);
|
|
127
|
+
if (clicked === undefined)
|
|
128
|
+
return;
|
|
129
|
+
dispatch({ type: "pick", number: clicked.index + 1 });
|
|
130
|
+
if (clicked.kind === "option")
|
|
131
|
+
dispatch({ type: "toggle" });
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
: {
|
|
135
|
+
value: picked === undefined ? null : String(picked.index),
|
|
136
|
+
onValueChange: (_value, row) => {
|
|
137
|
+
if (row !== null)
|
|
138
|
+
dispatch({ type: "pick", number: row.index + 1 });
|
|
139
|
+
},
|
|
140
|
+
}) }), view.status === undefined ? null : (_jsx("p", { className: view.status.tone === "error"
|
|
141
|
+
? "text-sm text-error"
|
|
142
|
+
: "text-sm opacity-70", role: view.status.tone === "error" ? "alert" : "status", children: view.status.text })), _jsxs("div", { className: "parakeet-decision-actions", children: [_jsx(KeycapButton, { label: "Later", tone: "neutral", onClick: onLater }), _jsx(KeycapButton, { disabled: !view.canSend, label: "Send", onClick: () => {
|
|
143
|
+
dispatch({ type: "enter" });
|
|
144
|
+
}, tone: "primary" })] })] }));
|
|
145
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { WhistleInteractionHost } from "@fungi.computer/whistle/interaction";
|
|
1
|
+
import type { WhistleInteractionHost, WhistlePendingPrompt } from "@fungi.computer/whistle/interaction";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
2
3
|
/** The interaction to draw and the page's close callback. */
|
|
3
4
|
export type InteractionPanelProps = Readonly<{
|
|
4
5
|
/** The renderer-neutral interaction store. */
|
|
@@ -6,6 +7,14 @@ export type InteractionPanelProps = Readonly<{
|
|
|
6
7
|
/** Close a settled interaction. */
|
|
7
8
|
onClose?: () => void;
|
|
8
9
|
}>;
|
|
10
|
+
/** The text every prompt carries above its choices. */
|
|
11
|
+
export type PromptContent = Pick<WhistlePendingPrompt["request"], "message" | "summary" | "facts" | "body" | "bodyLabel" | "details">;
|
|
12
|
+
/** Summary, facts, long text and on-demand details: the shared modal anatomy. */
|
|
13
|
+
export declare function PromptHeader({ request, trailing, }: Readonly<{
|
|
14
|
+
request: PromptContent;
|
|
15
|
+
/** Drawn at the end of the title line, e.g. a `1 of 3` marker. */
|
|
16
|
+
trailing?: ReactNode;
|
|
17
|
+
}>): import("react/jsx-runtime").JSX.Element;
|
|
9
18
|
/**
|
|
10
19
|
* Browser renderer for one Whistle interaction. It draws the same host
|
|
11
20
|
* snapshot the terminal draws; the command, not the page, defines the steps.
|
|
@@ -8,12 +8,14 @@ import { Input } from "@fungi.computer/caps/components/input";
|
|
|
8
8
|
import { Listbox } from "@fungi.computer/caps/components/listbox";
|
|
9
9
|
import { Status } from "@fungi.computer/caps/components/status";
|
|
10
10
|
import { Display } from "@fungi.computer/caps/components/surface";
|
|
11
|
-
import { useState, useSyncExternalStore } from "react";
|
|
11
|
+
import { useState, useSyncExternalStore, } from "react";
|
|
12
12
|
/** Summary, facts, long text and on-demand details: the shared modal anatomy. */
|
|
13
|
-
function PromptHeader({ request }) {
|
|
13
|
+
export function PromptHeader({ request, trailing, }) {
|
|
14
14
|
const [open, setOpen] = useState(false);
|
|
15
|
-
const entries = (list) => list === undefined ? null : (_jsx(DataList, { density: "compact", children: list.map((entry) => (
|
|
16
|
-
|
|
15
|
+
const entries = (list) => list === undefined ? null : (_jsx(DataList, { density: "compact", children: list.map((entry) => (
|
|
16
|
+
// Facts read as one block of label/value lines, with no rules between.
|
|
17
|
+
_jsxs(DataRow, { className: "after:hidden", children: [_jsx("span", { className: "text-sm opacity-70", children: entry.label }), " ", _jsx("span", { className: "break-words", children: entry.value })] }, entry.label))) }));
|
|
18
|
+
return (_jsxs(_Fragment, { children: [trailing === undefined ? (_jsx("p", { className: "font-medium", children: request.message })) : (_jsxs("div", { className: "flex items-baseline justify-between gap-sm", children: [_jsx("p", { className: "font-medium", children: request.message }), trailing] })), request.summary === undefined ? null : (_jsx("p", { className: "text-sm opacity-70", children: request.summary })), entries(request.facts), request.body === undefined ? null : (_jsxs("p", { className: "line-clamp-3 whitespace-pre-line break-words", children: [request.bodyLabel === undefined ? null : (_jsxs("span", { className: "text-sm opacity-70", children: [request.bodyLabel, " "] })), request.body] })), request.details === undefined && request.body === undefined ? null : (_jsxs(Disclosure, { open: open, onOpenChange: setOpen, summary: "Details", children: [request.body === undefined ? null : (_jsx("p", { className: "whitespace-pre-line break-words", children: request.body })), entries(request.details)] }))] }));
|
|
17
19
|
}
|
|
18
20
|
function PromptView({ host, pending, }) {
|
|
19
21
|
const [text, setText] = useState("");
|
|
@@ -29,6 +31,9 @@ function PromptView({ host, pending, }) {
|
|
|
29
31
|
, {
|
|
30
32
|
// A destructive question focuses the safe answer, which comes first.
|
|
31
33
|
autoFocus: request.destructive === true, variant: "ghost", onClick: () => host.answer(pending.sequence, false), children: request.cancelLabel ?? "Cancel" }), _jsx(Button, { variant: request.destructive === true ? "destructive" : "primary", onClick: () => host.answer(pending.sequence, true), children: request.confirmLabel ?? "Confirm" })] })] }));
|
|
34
|
+
// A decision is answered in the composer; no dialog ever receives one.
|
|
35
|
+
if (request.type === "decision")
|
|
36
|
+
return null;
|
|
32
37
|
const submit = (event) => {
|
|
33
38
|
event.preventDefault();
|
|
34
39
|
host.answer(pending.sequence, text);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { SlashCommandController } from "../commands/use-slash-commands.js";
|
|
2
|
-
/**
|
|
3
|
-
export declare function slashOptionId(listId: string, index: number): string;
|
|
4
|
-
/** The browser slash menu: draws Parakeet's completion rows, nothing more. */
|
|
2
|
+
/** Parakeet's slash adapter: maps its completion rows onto the shared menu. */
|
|
5
3
|
export declare function SlashMenu({ slash, listId, }: Readonly<{
|
|
6
4
|
slash: SlashCommandController;
|
|
7
5
|
listId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CompletionMenu } from "./CompletionMenu.js";
|
|
3
3
|
/** Split a label into matched and unmatched runs (UTF-16 indices, like the matcher). */
|
|
4
4
|
function emphasize(label, matches) {
|
|
5
5
|
if (matches.length === 0)
|
|
@@ -16,13 +16,9 @@ function emphasize(label, matches) {
|
|
|
16
16
|
}
|
|
17
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
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
export function slashOptionId(listId, index) {
|
|
21
|
-
return `${listId}-${String(index)}`;
|
|
22
|
-
}
|
|
23
|
-
/** The browser slash menu: draws Parakeet's completion rows, nothing more. */
|
|
19
|
+
/** Parakeet's slash adapter: maps its completion rows onto the shared menu. */
|
|
24
20
|
export function SlashMenu({ slash, listId, }) {
|
|
25
|
-
const { completion
|
|
21
|
+
const { completion } = slash;
|
|
26
22
|
if (completion.kind === "none")
|
|
27
23
|
return null;
|
|
28
24
|
const rows = completion.kind === "commands"
|
|
@@ -42,9 +38,5 @@ export function SlashMenu({ slash, listId, }) {
|
|
|
42
38
|
keys: "",
|
|
43
39
|
disabled: false,
|
|
44
40
|
}));
|
|
45
|
-
return (
|
|
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))) }))] }));
|
|
41
|
+
return (_jsx(CompletionMenu, { open: slash.open, rows: rows, selected: slash.selected, listId: listId, label: "Commands", kind: completion.kind, hint: completion.kind === "arguments" ? completion.hint : undefined, onHighlight: slash.highlight, onChoose: slash.choose }));
|
|
50
42
|
}
|
|
@@ -8,6 +8,7 @@ export declare const TimelineItem: import("react").NamedExoticComponent<Readonly
|
|
|
8
8
|
onCancelQueued?: ((commandId: string) => void) | undefined;
|
|
9
9
|
cancellingQueued?: boolean | undefined;
|
|
10
10
|
renderToolActions?: ((item: ToolItem) => ReactNode) | undefined;
|
|
11
|
+
claimsToolMedia?: ((item: ToolItem) => boolean) | undefined;
|
|
11
12
|
renderAssistantAttachment?: ((text: string) => ReactNode) | undefined;
|
|
12
13
|
formatAssistantText?: ((text: string) => string) | undefined;
|
|
13
14
|
/** View-only open state for tool groups and calls. */
|
|
@@ -14,7 +14,7 @@ const compactionNoticeLabel = {
|
|
|
14
14
|
stale: "compaction skipped: the context moved on",
|
|
15
15
|
insufficient: "compaction skipped: the summary would not fit",
|
|
16
16
|
};
|
|
17
|
-
export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed, onCancelQueued, cancellingQueued = false, renderToolActions, renderAssistantAttachment, formatAssistantText, toolDisclosure = initialToolDisclosure, onToggleTool = ignoreToggle, }) {
|
|
17
|
+
export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed, onCancelQueued, cancellingQueued = false, renderToolActions, claimsToolMedia, renderAssistantAttachment, formatAssistantText, toolDisclosure = initialToolDisclosure, onToggleTool = ignoreToggle, }) {
|
|
18
18
|
switch (item.kind) {
|
|
19
19
|
case "user_message": {
|
|
20
20
|
const pending = item.status === "pending" || item.status === "queued";
|
|
@@ -25,7 +25,7 @@ export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed,
|
|
|
25
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
26
|
onCancelQueued(commandId);
|
|
27
27
|
}, children: "Cancel" }) })) : null;
|
|
28
|
-
const dismissAction = requestId === undefined || onDismissFailed === undefined ? null : (_jsx(
|
|
28
|
+
const dismissAction = requestId === undefined || onDismissFailed === undefined ? null : (_jsx(Button, { "aria-label": "Dismiss failed prompt", variant: "outline", size: "xs", onClick: () => {
|
|
29
29
|
onDismissFailed(requestId);
|
|
30
30
|
}, type: "button", children: "Dismiss" }));
|
|
31
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"
|
|
@@ -44,11 +44,11 @@ export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed,
|
|
|
44
44
|
case "assistant_message":
|
|
45
45
|
if (!assistantMessageVisible(item))
|
|
46
46
|
return null;
|
|
47
|
-
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, { isError: item.isError === true, pending: item.pending, speaker: "agent", ...(item.thinking ? { thinking: item.thinking } : {}), children: formatAssistantText?.(item.text) ?? item.text }), item.isError && item.
|
|
47
|
+
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, { isError: item.isError === true, pending: item.pending, speaker: "agent", ...(item.thinking ? { thinking: item.thinking } : {}), children: formatAssistantText?.(item.text) ?? item.text }), item.isError && item.repeatCount !== undefined ? (_jsxs("span", { className: "px-3 text-error", children: ["x", item.repeatCount] })) : null, item.isError && item.errorDetail ? (_jsx("p", { className: "px-3 text-dim", children: item.errorDetail })) : null, item.isError && item.providerError !== undefined ? (_jsx("div", { className: "px-3 text-error", "data-provider-error": item.id, children: _jsxs("details", { children: [_jsx("summary", { children: "Provider details (latest response)" }), _jsx("pre", { className: "whitespace-pre-wrap", children: item.providerError })] }) })) : null, !item.pending && !item.isError
|
|
48
48
|
? renderAssistantAttachment?.(item.text)
|
|
49
49
|
: null] }));
|
|
50
50
|
case "tool_group":
|
|
51
|
-
return (_jsx(ToolGroup, { disclosure: toolDisclosure, group: item, onToggle: onToggleTool, renderToolActions: renderToolActions }));
|
|
51
|
+
return (_jsx(ToolGroup, { disclosure: toolDisclosure, group: item, onToggle: onToggleTool, renderToolActions: renderToolActions, claimsToolMedia: claimsToolMedia }));
|
|
52
52
|
case "compaction_divider":
|
|
53
53
|
return (_jsx("div", { className: "divider text-dim my-2 text-xs", role: "separator", children: item.tokensAfter === null
|
|
54
54
|
? "compacted · the agent’s memory starts below"
|
|
@@ -9,4 +9,5 @@ export declare const ToolGroup: import("react").NamedExoticComponent<Readonly<{
|
|
|
9
9
|
disclosure: ToolDisclosureState;
|
|
10
10
|
onToggle: ToggleToolDisclosure;
|
|
11
11
|
renderToolActions?: ((item: ToolItem) => ReactNode) | undefined;
|
|
12
|
+
claimsToolMedia?: ((item: ToolItem) => boolean) | undefined;
|
|
12
13
|
}>>;
|
|
@@ -6,7 +6,7 @@ import { Status } from "@fungi.computer/caps/components/status";
|
|
|
6
6
|
import { Sigil } from "@fungi.computer/sigils";
|
|
7
7
|
import { memo } from "react";
|
|
8
8
|
import { disclosureOpen, toolCallDefaultOpen, toolGroupDefaultOpen, visibleToolCalls, } from "../core/tool-groups.js";
|
|
9
|
-
import { formatToolDuration, toolStatusTone as statusTone, toolKindSigils, } from "../core/tool-view.js";
|
|
9
|
+
import { formatToolDuration, toolStatusTone as statusTone, toolKindSigils, toolLiveLines, } from "../core/tool-view.js";
|
|
10
10
|
import { CodeBlock } from "./CodeBlock.js";
|
|
11
11
|
/** Items beyond a section's preview, unless the reader asked for all. */
|
|
12
12
|
function visible(items, preview, full) {
|
|
@@ -53,38 +53,38 @@ function SectionView({ section, full, onShowAll, }) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
function CallBody({ call, tool, disclosure, onToggle, }) {
|
|
56
|
+
function CallBody({ call, tool, disclosure, onToggle, mediaClaimed, }) {
|
|
57
57
|
const fullId = `${call.id}:full`;
|
|
58
58
|
const full = disclosureOpen(disclosure, fullId, false);
|
|
59
59
|
const showAll = () => {
|
|
60
60
|
onToggle(fullId, false);
|
|
61
61
|
};
|
|
62
|
-
return (_jsxs("div", { className: "space-y-1.5 py-1.5 pr-2", "data-parakeet-call-body": true, children: [call.error === undefined ? null : (_jsx("p", { className: "not-prose m-0 font-mono text-[11px] text-error", role: "alert", children: call.error })), call.sections.map((section, index) => (_jsx(SectionView, { full: full, onShowAll: showAll, section: section }, `${section.kind}:${String(index)}`))), tool?.result?.content
|
|
62
|
+
return (_jsxs("div", { className: "space-y-1.5 py-1.5 pr-2", "data-parakeet-call-body": true, children: [call.error === undefined ? null : (_jsx("p", { className: "not-prose m-0 font-mono text-[11px] text-error", role: "alert", children: call.error })), call.sections.map((section, index) => (_jsx(SectionView, { full: full, onShowAll: showAll, section: section }, `${section.kind}:${String(index)}`))), (mediaClaimed ? [] : (tool?.result?.content ?? []))
|
|
63
63
|
.filter((part) => part.type === "image")
|
|
64
64
|
.map((part, index) => (_jsx("img", { alt: "Tool result", className: "max-w-full", src: `data:${part.mimeType};base64,${part.data}`, style: { maxHeight: 400 } }, `${call.id}-image-${String(index)}`)))] }));
|
|
65
65
|
}
|
|
66
|
-
function CallRow({ call, tool, disclosureId, size, disclosure, onToggle, renderToolActions, }) {
|
|
66
|
+
function CallRow({ call, tool, disclosureId, size, disclosure, onToggle, renderToolActions, claimsToolMedia, }) {
|
|
67
67
|
const defaultOpen = toolCallDefaultOpen(call);
|
|
68
68
|
const open = disclosureOpen(disclosure, disclosureId, defaultOpen);
|
|
69
69
|
const running = call.status === "running";
|
|
70
70
|
return (_jsxs("div", { "data-parakeet-call": call.kind, "data-status": call.status, children: [_jsx(Disclosure, { leading: _jsx(Sigil, { name: toolKindSigils[call.kind], size: 16 }), onOpenChange: () => {
|
|
71
71
|
onToggle(disclosureId, defaultOpen);
|
|
72
|
-
}, open: open, size: size, summary: _jsxs(_Fragment, { children: [_jsx("span", { className: "text-base-content", children: running ? call.activeVerb : call.verb }), " ", _jsx("span", { className: "font-mono", children: call.label })] }), tone: call.status === "error" ? "error" : "neutral", trailing: _jsxs(_Fragment, { children: [call.exitCode === undefined ? null : (_jsxs(Badge, { size: "sm", tone: "error", children: ["exit ", String(call.exitCode)] })), call.durationMs === undefined ? null : (_jsx("span", { children: formatToolDuration(call.durationMs) })), _jsx(Status, { motion: running ? "bounce" : "none", title: call.status === "unknown" ? "result unavailable" : call.status, tone: statusTone(call.status) })] }), children: _jsx(CallBody, { call: call, disclosure: disclosure, onToggle: onToggle, tool: tool }) }), call.status === "completed" && tool?.result !== undefined
|
|
72
|
+
}, open: open, size: size, summary: _jsxs(_Fragment, { children: [_jsx("span", { className: "text-base-content", children: running ? call.activeVerb : call.verb }), " ", _jsx("span", { className: "font-mono", children: call.label })] }), tone: call.status === "error" ? "error" : "neutral", trailing: _jsxs(_Fragment, { children: [call.exitCode === undefined ? null : (_jsxs(Badge, { size: "sm", tone: "error", children: ["exit ", String(call.exitCode)] })), call.durationMs === undefined ? null : (_jsx("span", { children: formatToolDuration(call.durationMs) })), _jsx(Status, { motion: running ? "bounce" : "none", title: call.status === "unknown" ? "result unavailable" : call.status, tone: statusTone(call.status) })] }), children: _jsx(CallBody, { call: call, disclosure: disclosure, onToggle: onToggle, mediaClaimed: tool !== undefined && claimsToolMedia?.(tool) === true, tool: tool }) }), call.live === undefined ? null : (_jsx("pre", { className: monoBlock, "aria-label": "Live tool output", children: toolLiveLines(call, open).map((line, index) => (_jsx("span", { className: "block whitespace-pre", children: line || "\u00a0" }, String(index)))) })), call.status === "completed" && tool?.result !== undefined
|
|
73
73
|
? renderToolActions?.(tool)
|
|
74
74
|
: null] }));
|
|
75
75
|
}
|
|
76
76
|
/** Recent finished calls, or a live group, with reader-owned disclosure. */
|
|
77
|
-
export const ToolGroup = memo(function ToolGroup({ group, disclosure, onToggle, renderToolActions, }) {
|
|
77
|
+
export const ToolGroup = memo(function ToolGroup({ group, disclosure, onToggle, renderToolActions, claimsToolMedia, }) {
|
|
78
78
|
const defaultOpen = toolGroupDefaultOpen(group);
|
|
79
79
|
const open = disclosureOpen(disclosure, group.id, defaultOpen);
|
|
80
80
|
const running = group.status === "running";
|
|
81
81
|
const active = group.active;
|
|
82
82
|
const single = group.calls.length === 1 ? group.calls[0] : undefined;
|
|
83
83
|
if (single !== undefined)
|
|
84
|
-
return (_jsx("div", { className: "px-1", children: _jsx(CallRow, { call: single, disclosure: disclosure, disclosureId: single.id, onToggle: onToggle, renderToolActions: renderToolActions, size: "row", tool: group.tools[0] }) }));
|
|
84
|
+
return (_jsx("div", { className: "px-1", children: _jsx(CallRow, { call: single, disclosure: disclosure, disclosureId: single.id, onToggle: onToggle, renderToolActions: renderToolActions, claimsToolMedia: claimsToolMedia, size: "row", tool: group.tools[0] }) }));
|
|
85
85
|
return (_jsxs("div", { className: "px-1", children: [_jsx(Disclosure, { className: "[&>.caps-disclosure-panel]:hidden", leading: _jsx(Sigil, { name: "list", size: 16 }), onOpenChange: () => {
|
|
86
86
|
onToggle(group.id, defaultOpen);
|
|
87
|
-
}, open: open, summary: running && active !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-base-content", children: active.activeVerb }), " ", _jsx("span", { className: "font-mono", children: active.label }), " \u00B7", " ", String(group.calls.length), " ", group.calls.length === 1 ? "call" : "calls"] })) : (_jsxs("span", { className: "text-base-content", children: [group.summary, " \u00B7 ", String(group.calls.length), " ", group.calls.length === 1 ? "call" : "calls"] })), tone: group.status === "error" ? "error" : "neutral", trailing: _jsxs(_Fragment, { children: [group.failedCount > 0 ? (_jsxs("span", { className: "text-error", children: [String(group.failedCount), " failed"] })) : null, group.durationMs === undefined ? null : (_jsx("span", { children: formatToolDuration(group.durationMs) })), _jsx(Status, { motion: running ? "bounce" : "none", title: group.status === "unknown" ? "result unavailable" : group.status, tone: statusTone(group.status) })] }) }), visibleToolCalls(group, open).map((call) => (_jsx(CallRow, { call: call, disclosure: disclosure, disclosureId: call.id, onToggle: onToggle, renderToolActions: renderToolActions, size: "row", tool: group.tools.find((tool) => tool.id === call.id) }, call.id))), group.status === "completed" && group.calls.length > 3 ? (_jsx("button", { "aria-expanded": open, className: "btn btn-ghost btn-xs text-dim", onClick: () => {
|
|
87
|
+
}, open: open, summary: running && active !== undefined ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-base-content", children: active.activeVerb }), " ", _jsx("span", { className: "font-mono", children: active.label }), " \u00B7", " ", String(group.calls.length), " ", group.calls.length === 1 ? "call" : "calls"] })) : (_jsxs("span", { className: "text-base-content", children: [group.summary, " \u00B7 ", String(group.calls.length), " ", group.calls.length === 1 ? "call" : "calls"] })), tone: group.status === "error" ? "error" : "neutral", trailing: _jsxs(_Fragment, { children: [group.failedCount > 0 ? (_jsxs("span", { className: "text-error", children: [String(group.failedCount), " failed"] })) : null, group.durationMs === undefined ? null : (_jsx("span", { children: formatToolDuration(group.durationMs) })), _jsx(Status, { motion: running ? "bounce" : "none", title: group.status === "unknown" ? "result unavailable" : group.status, tone: statusTone(group.status) })] }) }), visibleToolCalls(group, open).map((call) => (_jsx(CallRow, { call: call, disclosure: disclosure, disclosureId: call.id, onToggle: onToggle, renderToolActions: renderToolActions, claimsToolMedia: claimsToolMedia, size: "row", tool: group.tools.find((tool) => tool.id === call.id) }, call.id))), group.status === "completed" && group.calls.length > 3 ? (_jsx("button", { "aria-expanded": open, className: "btn btn-ghost btn-xs text-dim", onClick: () => {
|
|
88
88
|
onToggle(group.id, defaultOpen);
|
|
89
89
|
}, type: "button", children: open ? "Show less" : `Show all ${String(group.calls.length)} calls` })) : null] }));
|
|
90
90
|
});
|
|
@@ -41,7 +41,7 @@ export declare function createConversationOpeningOwner<Session extends ParakeetS
|
|
|
41
41
|
/** Only the primary conversation is the selection the host shell sees. */
|
|
42
42
|
reportsSelection?: boolean;
|
|
43
43
|
}>): {
|
|
44
|
-
select: (agentId: string, sessionId: string, fromOpenRequest?: boolean
|
|
44
|
+
select: (agentId: string, sessionId: string, fromOpenRequest?: boolean) => void;
|
|
45
45
|
clear: () => void;
|
|
46
46
|
close: () => void;
|
|
47
47
|
};
|
|
@@ -68,7 +68,7 @@ export function createConversationOpeningOwner(options) {
|
|
|
68
68
|
let disposed = false;
|
|
69
69
|
const same = (left, right) => left?.agentId === right.agentId && left.sessionId === right.sessionId;
|
|
70
70
|
const isCurrent = (opening) => !disposed && pending === opening;
|
|
71
|
-
const select = (agentId, sessionId, fromOpenRequest = false
|
|
71
|
+
const select = (agentId, sessionId, fromOpenRequest = false) => {
|
|
72
72
|
if (disposed)
|
|
73
73
|
return;
|
|
74
74
|
const selection = { agentId, sessionId };
|
|
@@ -114,7 +114,7 @@ export function createConversationOpeningOwner(options) {
|
|
|
114
114
|
void Promise.allSettled([client.close(), transport.close()]);
|
|
115
115
|
};
|
|
116
116
|
await observation.ready;
|
|
117
|
-
if (
|
|
117
|
+
if (options.reportsSelection !== false) {
|
|
118
118
|
await connection.ui.reportAgentSessionSelection({
|
|
119
119
|
agentId,
|
|
120
120
|
sessionId,
|
package/dist/conversation.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export type ComposerController = Readonly<{
|
|
|
118
118
|
/** Typed reason the latest prompt did not complete. */
|
|
119
119
|
failure: "admission" | "failed" | "cancelled" | "interrupted" | undefined;
|
|
120
120
|
/** Current send could not enter the bounded local admission queue. */
|
|
121
|
-
admissionError: "queue_full" | undefined;
|
|
121
|
+
admissionError: "offline_queue_full" | "queue_full" | undefined;
|
|
122
122
|
/** Retry the exact most recent failed prompt. */
|
|
123
123
|
retry: () => void;
|
|
124
124
|
/** Remove one failed local admission from this conversation. */
|
|
@@ -135,6 +135,8 @@ export type ComposerController = Readonly<{
|
|
|
135
135
|
submitCommand: (run: (draft: string, consumeDraft: () => void) => Promise<WhistleExecutionOutcome | undefined>) => Promise<WhistleExecutionOutcome | undefined>;
|
|
136
136
|
/** Replace the local input. */
|
|
137
137
|
setDraft: (value: string) => void;
|
|
138
|
+
/** Replace the images riding with the draft; the Session owner admits them on send. */
|
|
139
|
+
setImages: (images: PromptCommand["images"]) => void;
|
|
138
140
|
}>;
|
|
139
141
|
/** Session activity, observation, and Agent-lifetime controls. */
|
|
140
142
|
export type StatusController = Readonly<{
|
|
@@ -203,6 +205,8 @@ export declare function useParakeetSession(): ParakeetSession;
|
|
|
203
205
|
export declare function useExtensionSlots(): SessionExtensionSlots;
|
|
204
206
|
/** Read Session activity and operate its observation. */
|
|
205
207
|
export declare function useStatus(): StatusController;
|
|
208
|
+
/** Time of the last delivered watch snapshot or frame, not an SSE comment. */
|
|
209
|
+
export declare function useLastSessionReplyAt(): number | undefined;
|
|
206
210
|
/** Presentation admitted once from the selected Session's already observed slots. */
|
|
207
211
|
export declare function useClientContributions(): readonly ClientContribution[];
|
|
208
212
|
/** The selected Session's ordinary command interaction owner. */
|