@fungi.computer/parakeet 0.3.0 → 0.5.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/dist/account-commands.js +20 -7
- 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 +150 -38
- package/dist/components/ClientRows.js +42 -26
- package/dist/components/CompletionMenu.d.ts +54 -0
- package/dist/components/CompletionMenu.js +52 -0
- package/dist/components/DecisionComposer.d.ts +30 -0
- package/dist/components/DecisionComposer.js +146 -0
- package/dist/components/InteractionPanel.d.ts +10 -1
- package/dist/components/InteractionPanel.js +24 -5
- package/dist/components/RunFailureNotice.js +11 -2
- 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 +3 -3
- package/dist/components/ToolGroup.d.ts +1 -0
- package/dist/components/ToolGroup.js +7 -7
- package/dist/conversation.d.ts +5 -1
- package/dist/conversation.js +72 -20
- 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 +56 -5
- package/dist/core/client-actions.d.ts +1 -0
- package/dist/core/client-actions.js +8 -21
- package/dist/core/draft-storage.d.ts +4 -0
- package/dist/core/draft-storage.js +12 -5
- package/dist/core/picker-text.d.ts +7 -0
- package/dist/core/picker-text.js +24 -0
- 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 +1 -1
- package/dist/core/session-view-state.js +14 -6
- package/dist/headless.d.ts +7 -2
- package/dist/headless.js +6 -1
- package/dist/model-commands.js +14 -15
- package/dist/parakeet.d.ts +6 -2
- package/dist/parakeet.js +15 -6
- package/dist/session-commands.js +6 -2
- package/dist/store/types.d.ts +5 -1
- package/dist/styles.css +407 -113
- 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 +822 -41
- package/docs/api/index.md +13 -4
- package/package.json +15 -8
- package/src/completion-menu.css +71 -0
- package/src/styles.css +174 -46
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@fungi.computer/caps/components/button";
|
|
3
|
+
import { Checkbox } from "@fungi.computer/caps/components/checkbox";
|
|
4
|
+
import { KeycapButton } from "@fungi.computer/caps/components/keycap";
|
|
5
|
+
import { Input } from "@fungi.computer/caps/components/input";
|
|
6
|
+
import { Listbox } from "@fungi.computer/caps/components/listbox";
|
|
7
|
+
import { isImeComposing } from "@fungi.computer/caps/lib/ime";
|
|
8
|
+
import { useEffect, useRef } from "react";
|
|
9
|
+
import { useDecision } from "../use-decision.js";
|
|
10
|
+
import { PromptHeader } from "./InteractionPanel.js";
|
|
11
|
+
const kindNames = { question: "Question", approval: "Approval" };
|
|
12
|
+
/**
|
|
13
|
+
* The composer while the Agent waits on the person: the decision's rows,
|
|
14
|
+
* an Other line, and Send / Later. It binds one waiting decision to its
|
|
15
|
+
* reducer and draws the result.
|
|
16
|
+
*/
|
|
17
|
+
export function DecisionComposer({ item, position, total, onLater, takeFocus, }) {
|
|
18
|
+
const decision = useDecision(item, { position, total, later: onLater });
|
|
19
|
+
if (decision.view === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
return (_jsx(DecisionPanel, { view: decision.view, dispatch: decision.dispatch, onLater: decision.later, takeFocus: takeFocus }));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Draw Parakeet's decision view with Caps parts and forward keys and clicks as
|
|
25
|
+
* intents; the rules live in the reducer, so every surface answers alike.
|
|
26
|
+
*/
|
|
27
|
+
export function DecisionPanel({ view, dispatch, onLater, takeFocus, }) {
|
|
28
|
+
const list = useRef(null);
|
|
29
|
+
const input = useRef(null);
|
|
30
|
+
const typing = view.phase === "other";
|
|
31
|
+
const wasTyping = useRef(false);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (takeFocus)
|
|
34
|
+
list.current?.focus();
|
|
35
|
+
}, [takeFocus]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (typing)
|
|
38
|
+
input.current?.focus();
|
|
39
|
+
else if (wasTyping.current && document.activeElement === input.current)
|
|
40
|
+
list.current?.focus();
|
|
41
|
+
wasTyping.current = typing;
|
|
42
|
+
}, [typing]);
|
|
43
|
+
const rows = view.rows;
|
|
44
|
+
const field = view.other;
|
|
45
|
+
const sending = view.phase === "sending";
|
|
46
|
+
const act = (event, handled, run) => {
|
|
47
|
+
if (!handled)
|
|
48
|
+
return false;
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
run();
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
const onListKey = (event) => {
|
|
54
|
+
// Keys typed in the Other row's field belong to the field.
|
|
55
|
+
if (event.target instanceof HTMLInputElement)
|
|
56
|
+
return;
|
|
57
|
+
if (isImeComposing(event))
|
|
58
|
+
return;
|
|
59
|
+
if (event.ctrlKey || event.metaKey || event.altKey)
|
|
60
|
+
return;
|
|
61
|
+
const { key } = event;
|
|
62
|
+
if (act(event, key === "ArrowDown", () => {
|
|
63
|
+
dispatch({ type: "move", delta: 1 });
|
|
64
|
+
}) ||
|
|
65
|
+
act(event, key === "ArrowUp", () => {
|
|
66
|
+
dispatch({ type: "move", delta: -1 });
|
|
67
|
+
}) ||
|
|
68
|
+
act(event, key === "Enter", () => {
|
|
69
|
+
dispatch({ type: "enter" });
|
|
70
|
+
}) ||
|
|
71
|
+
act(event, key === " ", () => {
|
|
72
|
+
dispatch({ type: "toggle" });
|
|
73
|
+
}) ||
|
|
74
|
+
act(event, key === "Escape", () => {
|
|
75
|
+
dispatch({ type: "escape" });
|
|
76
|
+
}) ||
|
|
77
|
+
act(event, /^[1-9]$/u.test(key), () => {
|
|
78
|
+
dispatch({ type: "pick", number: Number(key) });
|
|
79
|
+
}) ||
|
|
80
|
+
act(event, key.length === 1, () => {
|
|
81
|
+
dispatch({ type: "append", text: key });
|
|
82
|
+
}))
|
|
83
|
+
return;
|
|
84
|
+
};
|
|
85
|
+
const onOtherKey = (event) => {
|
|
86
|
+
if (isImeComposing(event))
|
|
87
|
+
return;
|
|
88
|
+
const { key } = event;
|
|
89
|
+
if (act(event, key === "ArrowDown", () => {
|
|
90
|
+
dispatch({ type: "move", delta: 1 });
|
|
91
|
+
}) ||
|
|
92
|
+
act(event, key === "ArrowUp", () => {
|
|
93
|
+
dispatch({ type: "move", delta: -1 });
|
|
94
|
+
}) ||
|
|
95
|
+
act(event, key === "Enter", () => {
|
|
96
|
+
dispatch({ type: "enter" });
|
|
97
|
+
}) ||
|
|
98
|
+
act(event, key === "Escape", () => {
|
|
99
|
+
dispatch({ type: "escape" });
|
|
100
|
+
}))
|
|
101
|
+
return;
|
|
102
|
+
};
|
|
103
|
+
// A multiple decision's marker is a Caps check, not `[x]` text. The row
|
|
104
|
+
// itself is the control: the option owns aria-selected and the click, so
|
|
105
|
+
// the check is drawn for sight only.
|
|
106
|
+
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 }) }));
|
|
107
|
+
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 }))] }));
|
|
108
|
+
const rowContent = (row) => view.multiple ? (_jsxs("span", { className: "flex items-start gap-xs", children: [check(row), words(row)] })) : (words(row));
|
|
109
|
+
const picked = rows.find((row) => row.highlighted);
|
|
110
|
+
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: {
|
|
111
|
+
message: view.title,
|
|
112
|
+
summary: view.summary,
|
|
113
|
+
facts: view.facts,
|
|
114
|
+
body: view.body,
|
|
115
|
+
bodyLabel: view.bodyLabel,
|
|
116
|
+
details: view.details.length === 0 ? undefined : view.details,
|
|
117
|
+
}, 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) => {
|
|
118
|
+
dispatch({ type: "type", text: event.target.value });
|
|
119
|
+
}, 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
|
|
120
|
+
? {
|
|
121
|
+
multiple: true,
|
|
122
|
+
value: rows
|
|
123
|
+
.filter((row) => row.toggled)
|
|
124
|
+
.map((row) => String(row.index)),
|
|
125
|
+
onValueChange: (_value, selected) => {
|
|
126
|
+
const clicked = rows.find((row) => selected.some((chosen) => chosen.index === row.index) !==
|
|
127
|
+
row.toggled);
|
|
128
|
+
if (clicked === undefined)
|
|
129
|
+
return;
|
|
130
|
+
dispatch({ type: "pick", number: clicked.index + 1 });
|
|
131
|
+
if (clicked.kind === "option")
|
|
132
|
+
dispatch({ type: "toggle" });
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
: {
|
|
136
|
+
value: picked === undefined ? null : String(picked.index),
|
|
137
|
+
onValueChange: (_value, row) => {
|
|
138
|
+
if (row !== null)
|
|
139
|
+
dispatch({ type: "pick", number: row.index + 1 });
|
|
140
|
+
},
|
|
141
|
+
}) }), view.status === undefined ? null : (_jsx("p", { className: view.status.tone === "error"
|
|
142
|
+
? "text-sm text-error"
|
|
143
|
+
: "text-sm opacity-70", role: view.status.tone === "error" ? "alert" : "status", children: view.status.text })), _jsxs("div", { className: "parakeet-decision-actions", children: [_jsx(Button, { variant: "ghost", onClick: onLater, children: "Later" }), _jsx(KeycapButton, { disabled: !view.canSend, label: "Send", onClick: () => {
|
|
144
|
+
dispatch({ type: "enter" });
|
|
145
|
+
}, tone: "primary" })] })] }));
|
|
146
|
+
}
|
|
@@ -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.
|
|
@@ -1,27 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Alert } from "@fungi.computer/caps/components/alert";
|
|
3
3
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
4
|
+
import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
|
|
5
|
+
import { Disclosure } from "@fungi.computer/caps/components/disclosure";
|
|
4
6
|
import { Field } from "@fungi.computer/caps/components/field";
|
|
5
7
|
import { Input } from "@fungi.computer/caps/components/input";
|
|
6
8
|
import { Listbox } from "@fungi.computer/caps/components/listbox";
|
|
7
9
|
import { Status } from "@fungi.computer/caps/components/status";
|
|
8
10
|
import { Display } from "@fungi.computer/caps/components/surface";
|
|
9
|
-
import { useState, useSyncExternalStore } from "react";
|
|
11
|
+
import { useState, useSyncExternalStore, } from "react";
|
|
12
|
+
/** Summary, facts, long text and on-demand details: the shared modal anatomy. */
|
|
13
|
+
export function PromptHeader({ request, trailing, }) {
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
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)] }))] }));
|
|
19
|
+
}
|
|
10
20
|
function PromptView({ host, pending, }) {
|
|
11
21
|
const [text, setText] = useState("");
|
|
12
22
|
const request = pending.request;
|
|
13
23
|
if (request.type === "choice")
|
|
14
|
-
return (_jsxs("div", { className: "flex flex-col gap-xs", children: [_jsx(
|
|
24
|
+
return (_jsxs("div", { className: "flex flex-col gap-xs", children: [_jsx(PromptHeader, { request: request }), _jsx(Listbox, { "aria-label": request.message, items: request.options, getKey: (option) => option.value, getLabel: (option) => option.label, onValueChange: (value) => {
|
|
15
25
|
if (value !== null)
|
|
16
26
|
host.answer(pending.sequence, value);
|
|
17
|
-
}, renderItem: (option) => (_jsxs("span", { className:
|
|
27
|
+
}, renderItem: (option) => (_jsxs("span", { className: `flex flex-col${option.destructive === true ? " text-error" : ""}`, children: [_jsxs("span", { children: [option.value === request.current ? "● " : "", option.label] }), option.description === undefined ? null : (_jsx("span", { className: "text-sm opacity-70", children: option.description }))] })) })] }));
|
|
18
28
|
if (request.type === "confirmation")
|
|
19
|
-
return (_jsxs("div", { className: "flex flex-col gap-sm", children: [_jsx(
|
|
29
|
+
return (_jsxs("div", { className: "flex flex-col gap-sm", children: [_jsx(PromptHeader, { request: request }), _jsxs("div", { className: "flex justify-end gap-xs", children: [_jsx(Button
|
|
30
|
+
// A destructive question focuses the safe answer, which comes first.
|
|
31
|
+
, {
|
|
32
|
+
// A destructive question focuses the safe answer, which comes first.
|
|
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;
|
|
20
37
|
const submit = (event) => {
|
|
21
38
|
event.preventDefault();
|
|
22
39
|
host.answer(pending.sequence, text);
|
|
23
40
|
};
|
|
24
|
-
return (_jsxs("form", { className: "flex flex-col gap-sm", onSubmit: submit, children: [
|
|
41
|
+
return (_jsxs("form", { className: "flex flex-col gap-sm", onSubmit: submit, children: [request.summary === undefined &&
|
|
42
|
+
request.facts === undefined &&
|
|
43
|
+
request.body === undefined ? null : (_jsx(PromptHeader, { request: request })), _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
44
|
setText(event.target.value);
|
|
26
45
|
} }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", children: "Continue" }) })] }));
|
|
27
46
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Alert } from "@fungi.computer/caps/components/alert";
|
|
3
3
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
4
|
+
import { boundedDiagnostic } from "@fungi.computer/bird-dog/effect";
|
|
4
5
|
const messages = {
|
|
5
6
|
admission: "Your prompt didn’t make it through. Let’s give it another go.",
|
|
6
7
|
failed: "The run stopped. Check the message above before trying again.",
|
|
@@ -9,5 +10,13 @@ const messages = {
|
|
|
9
10
|
};
|
|
10
11
|
/** Session failures do not carry a billing receipt; never promise a refund. */
|
|
11
12
|
export function RunFailureNotice({ failure, cause, onRetry, }) {
|
|
12
|
-
|
|
13
|
+
const bounded = cause === undefined ? undefined : boundedDiagnostic(cause);
|
|
14
|
+
// Diagnostic metadata is useful to the owner, not a public explanation.
|
|
15
|
+
const publicCause = bounded !== undefined &&
|
|
16
|
+
!bounded.startsWith("reason_unavailable:") &&
|
|
17
|
+
!/\b(?:run|session)=/iu.test(bounded)
|
|
18
|
+
? bounded
|
|
19
|
+
: undefined;
|
|
20
|
+
const interruptedWithReason = failure === "interrupted" && publicCause !== undefined;
|
|
21
|
+
return (_jsx(Alert, { tone: "warning", title: failure === "cancelled" ? "Taking a breather" : "A little tangle", description: interruptedWithReason ? (_jsxs(_Fragment, { children: ["This turn stopped before it finished: ", publicCause, ". Your prompt is still here."] })) : (_jsxs(_Fragment, { children: [messages[failure], " If the model didn\u2019t start, there\u2019s no charge. Model work already done can still use credits.", publicCause ? _jsx("span", { className: "block", children: publicCause }) : null] })), action: failure === "cancelled" ? undefined : (_jsx(Button, { "aria-label": "Retry prompt", variant: "outline", onClick: onRetry, children: "Retry" })) }));
|
|
13
22
|
}
|
|
@@ -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";
|
|
@@ -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
|
}>>;
|
|
@@ -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.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
|
});
|
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. */
|
package/dist/conversation.js
CHANGED
|
@@ -66,7 +66,7 @@ function composerStatus(snapshot, admissions, failure) {
|
|
|
66
66
|
return status;
|
|
67
67
|
if (failure !== undefined || view.kind === "error")
|
|
68
68
|
return "error";
|
|
69
|
-
if (admissions.some(({ status: state }) => state === "accepted"))
|
|
69
|
+
if (admissions.some(({ status: state }) => state === "accepted" || state === "queued-local"))
|
|
70
70
|
return "queued";
|
|
71
71
|
if (admissions.some(({ status: state }) => state === "admitting"))
|
|
72
72
|
return "sending";
|
|
@@ -113,13 +113,14 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
113
113
|
const saved = journal.read();
|
|
114
114
|
let mounted = false;
|
|
115
115
|
const timers = new Map();
|
|
116
|
-
const recoveryReasons = new Map(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
const recoveryReasons = new Map();
|
|
117
|
+
for (const pending of saved.pending)
|
|
118
|
+
if (pending.status === "admitting")
|
|
119
|
+
recoveryReasons.set(pending.requestId, "Closed before confirmation");
|
|
120
120
|
const recovering = new Set();
|
|
121
121
|
const queryAtom = atomWithQuery(() => sessionQueryOptions(session), () => queryClient);
|
|
122
122
|
const snapshotAtom = atom((get) => get(queryAtom).data?.snapshot);
|
|
123
|
+
const lastReplyAtAtom = atom((get) => get(queryAtom).data?.lastReplyAt);
|
|
123
124
|
const pinnedHistoryAtom = atom(undefined);
|
|
124
125
|
const historyBeforeAtom = atom((get) => (get(pinnedHistoryAtom) ?? get(snapshotAtom))?.oldestCursor ?? null);
|
|
125
126
|
const historyAtom = atomWithInfiniteQuery((get) => sessionHistoryQueryOptions(session, get(historyBeforeAtom)), () => queryClient);
|
|
@@ -279,10 +280,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
279
280
|
const recallStateAtom = atom({ kind: "idle" });
|
|
280
281
|
let recallOwner;
|
|
281
282
|
const draftRevisionAtom = atom(0);
|
|
282
|
-
const rawAdmissionsAtom = atom(saved.pending
|
|
283
|
-
...pending,
|
|
284
|
-
status: "admitting",
|
|
285
|
-
})));
|
|
283
|
+
const rawAdmissionsAtom = atom(saved.pending);
|
|
286
284
|
const persistAtom = atom(null, (get, _set, state) => {
|
|
287
285
|
// Unavailable storage is session-only, not a pretend durable journal.
|
|
288
286
|
// A supplied adapter's write failures still refuse durable operations.
|
|
@@ -299,16 +297,19 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
299
297
|
recall: state?.recall === null
|
|
300
298
|
? undefined
|
|
301
299
|
: (state?.recall ?? get(recallReservationAtom)),
|
|
302
|
-
pending: (state?.admissions ?? get(rawAdmissionsAtom))
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
300
|
+
pending: (state?.admissions ?? get(rawAdmissionsAtom)).flatMap(({ status, requestId, content, submittedDraft, delivery, images, restoreDraft, }) => status === "admitting" || status === "queued-local"
|
|
301
|
+
? [
|
|
302
|
+
{
|
|
303
|
+
status,
|
|
304
|
+
requestId,
|
|
305
|
+
content,
|
|
306
|
+
submittedDraft,
|
|
307
|
+
delivery,
|
|
308
|
+
images,
|
|
309
|
+
restoreDraft,
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
: []),
|
|
312
313
|
});
|
|
313
314
|
});
|
|
314
315
|
const recallAtom = atom(null, (get, set) => {
|
|
@@ -436,6 +437,24 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
436
437
|
set(draftAtom, value);
|
|
437
438
|
set(admissionErrorAtom, undefined);
|
|
438
439
|
});
|
|
440
|
+
const setImagesAtom = atom(null, (get, set, images) => {
|
|
441
|
+
const next = images === undefined || images.length === 0 ? undefined : images;
|
|
442
|
+
try {
|
|
443
|
+
set(persistAtom, {
|
|
444
|
+
draftPayload: { images: next, delivery: get(draftDeliveryAtom) },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch (cause) {
|
|
448
|
+
reportFailure("save draft images", cause);
|
|
449
|
+
interactions.toast({
|
|
450
|
+
variant: "error",
|
|
451
|
+
message: `Images not saved: ${errorMessage(cause)}`,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
set(draftImagesAtom, next);
|
|
455
|
+
set(draftRevisionAtom, (revision) => revision + 1);
|
|
456
|
+
set(admissionErrorAtom, undefined);
|
|
457
|
+
});
|
|
439
458
|
/** Consume only the exact draft a command submitted. */
|
|
440
459
|
const clearSubmittedDraftAtom = atom(null, (get, set, submitted) => {
|
|
441
460
|
if (get(draftRevisionAtom) !== submitted.revision ||
|
|
@@ -616,6 +635,10 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
616
635
|
set(draftDeliveryAtom, undefined);
|
|
617
636
|
set(setDraftAtom, "");
|
|
618
637
|
}
|
|
638
|
+
// The offline prompt is saved alongside the draft. Await a fresh owner
|
|
639
|
+
// baseline before attempting admission.
|
|
640
|
+
if (admission.status === "queued-local")
|
|
641
|
+
return;
|
|
619
642
|
const prompt = {
|
|
620
643
|
type: "prompt",
|
|
621
644
|
text: admission.content,
|
|
@@ -730,6 +753,20 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
730
753
|
const maintainRecoveryAtom = atom(null, (get, set) => {
|
|
731
754
|
const observation = get(observationAtom).status;
|
|
732
755
|
const admissions = get(rawAdmissionsAtom);
|
|
756
|
+
if (observation === "live") {
|
|
757
|
+
if (get(admissionErrorAtom) === "offline_queue_full")
|
|
758
|
+
set(admissionErrorAtom, undefined);
|
|
759
|
+
const queued = admissions.find(({ status }) => status === "queued-local");
|
|
760
|
+
if (queued !== undefined) {
|
|
761
|
+
const sending = {
|
|
762
|
+
...queued,
|
|
763
|
+
status: "admitting",
|
|
764
|
+
};
|
|
765
|
+
set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === queued.requestId ? sending : candidate));
|
|
766
|
+
// Preserve the idempotent request id when reconnect admits the prompt.
|
|
767
|
+
set(dispatchAdmissionAtom, { admission: sending, consumeDraft: false });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
733
770
|
for (const [id, timer] of timers) {
|
|
734
771
|
if (!admissions.some(({ requestId, status }) => requestId === id && status === "admitting")) {
|
|
735
772
|
clearTimeout(timer);
|
|
@@ -774,6 +811,12 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
774
811
|
return;
|
|
775
812
|
const admissions = get(admissionsAtom);
|
|
776
813
|
const unresolved = admissions.filter(({ status }) => status !== "failed");
|
|
814
|
+
const offline = get(observationAtom).status !== "live";
|
|
815
|
+
if (offline &&
|
|
816
|
+
unresolved.some(({ status }) => status === "queued-local")) {
|
|
817
|
+
set(admissionErrorAtom, "offline_queue_full");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
777
820
|
if (unresolved.length >= MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
|
|
778
821
|
set(admissionErrorAtom, "queue_full");
|
|
779
822
|
return;
|
|
@@ -785,7 +828,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
785
828
|
requestId: nanoid(),
|
|
786
829
|
submittedDraft: draft,
|
|
787
830
|
restoreDraft: true,
|
|
788
|
-
status: "admitting",
|
|
831
|
+
status: offline ? "queued-local" : "admitting",
|
|
789
832
|
};
|
|
790
833
|
set(rawAdmissionsAtom, [...admissions, admission]);
|
|
791
834
|
set(latestSubmissionIdAtom, admission.requestId);
|
|
@@ -991,6 +1034,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
991
1034
|
jumpToLatestAtom,
|
|
992
1035
|
loadOlderHistoryAtom,
|
|
993
1036
|
loadNewerHistoryAtom,
|
|
1037
|
+
lastReplyAtAtom,
|
|
994
1038
|
observationAtom,
|
|
995
1039
|
observedCommandIdsAtom,
|
|
996
1040
|
rawAdmissionsAtom,
|
|
@@ -1002,6 +1046,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
|
|
|
1002
1046
|
reconnectAtom,
|
|
1003
1047
|
retryAtom,
|
|
1004
1048
|
setDraftAtom,
|
|
1049
|
+
setImagesAtom,
|
|
1005
1050
|
sendAtom,
|
|
1006
1051
|
snapshotAtom,
|
|
1007
1052
|
statusAtom,
|
|
@@ -1180,6 +1225,7 @@ export function useComposer() {
|
|
|
1180
1225
|
const failure = useStoreAtom(atoms.failureAtom);
|
|
1181
1226
|
const admissionError = useStoreAtom(atoms.admissionErrorAtom);
|
|
1182
1227
|
const setDraft = useStoreSetter(atoms.setDraftAtom);
|
|
1228
|
+
const setImages = useStoreSetter(atoms.setImagesAtom);
|
|
1183
1229
|
const checkpointDraft = useStoreSetter(atoms.checkpointDraftAtom);
|
|
1184
1230
|
const send = useStoreSetter(atoms.sendAtom);
|
|
1185
1231
|
const submitCommand = useStoreSetter(atoms.submitCommandAtom);
|
|
@@ -1202,6 +1248,7 @@ export function useComposer() {
|
|
|
1202
1248
|
retry: retryLatest,
|
|
1203
1249
|
send,
|
|
1204
1250
|
setDraft,
|
|
1251
|
+
setImages,
|
|
1205
1252
|
submitCommand,
|
|
1206
1253
|
}), [
|
|
1207
1254
|
admissionError,
|
|
@@ -1216,6 +1263,7 @@ export function useComposer() {
|
|
|
1216
1263
|
retryLatest,
|
|
1217
1264
|
send,
|
|
1218
1265
|
setDraft,
|
|
1266
|
+
setImages,
|
|
1219
1267
|
submitCommand,
|
|
1220
1268
|
]);
|
|
1221
1269
|
}
|
|
@@ -1250,6 +1298,10 @@ export function useStatus() {
|
|
|
1250
1298
|
viewState,
|
|
1251
1299
|
}), [commandId, context, observation, reconnect, status, viewState]);
|
|
1252
1300
|
}
|
|
1301
|
+
/** Time of the last delivered watch snapshot or frame, not an SSE comment. */
|
|
1302
|
+
export function useLastSessionReplyAt() {
|
|
1303
|
+
return useStoreAtom(useConversationScope().atoms.lastReplyAtAtom);
|
|
1304
|
+
}
|
|
1253
1305
|
/** Presentation admitted once from the selected Session's already observed slots. */
|
|
1254
1306
|
export function useClientContributions() {
|
|
1255
1307
|
return useStoreAtom(useConversationScope().contributions);
|