@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.
Files changed (82) hide show
  1. package/README.md +17 -1
  2. package/dist/client-contributions.d.ts +1 -0
  3. package/dist/commands/awaiting-driver.d.ts +20 -0
  4. package/dist/commands/awaiting-driver.js +121 -0
  5. package/dist/commands/builtins.js +6 -4
  6. package/dist/commands/interactions.d.ts +25 -0
  7. package/dist/commands/interactions.js +76 -2
  8. package/dist/commands/run.js +3 -3
  9. package/dist/commands/slash.d.ts +7 -3
  10. package/dist/commands/slash.js +2 -0
  11. package/dist/commands/use-slash-commands.d.ts +13 -0
  12. package/dist/commands/use-slash-commands.js +58 -1
  13. package/dist/components/ChatComposer.d.ts +2 -0
  14. package/dist/components/ChatComposer.js +158 -40
  15. package/dist/components/ClientRows.js +42 -26
  16. package/dist/components/CodeBlock.js +3 -2
  17. package/dist/components/CompletionMenu.d.ts +54 -0
  18. package/dist/components/CompletionMenu.js +52 -0
  19. package/dist/components/ComposerModelKey.d.ts +11 -0
  20. package/dist/components/ComposerModelKey.js +9 -0
  21. package/dist/components/ComposerShell.d.ts +47 -0
  22. package/dist/components/ComposerShell.js +29 -0
  23. package/dist/components/DecisionComposer.d.ts +30 -0
  24. package/dist/components/DecisionComposer.js +145 -0
  25. package/dist/components/InteractionPanel.d.ts +10 -1
  26. package/dist/components/InteractionPanel.js +9 -4
  27. package/dist/components/SlashMenu.d.ts +1 -3
  28. package/dist/components/SlashMenu.js +5 -13
  29. package/dist/components/TimelineItem.d.ts +1 -0
  30. package/dist/components/TimelineItem.js +4 -4
  31. package/dist/components/ToolGroup.d.ts +1 -0
  32. package/dist/components/ToolGroup.js +8 -8
  33. package/dist/conversation-opening.d.ts +1 -1
  34. package/dist/conversation-opening.js +2 -2
  35. package/dist/conversation.d.ts +5 -1
  36. package/dist/conversation.js +84 -29
  37. package/dist/core/approval-decision.d.ts +198 -0
  38. package/dist/core/approval-decision.js +291 -0
  39. package/dist/core/awaiting-answer.d.ts +229 -0
  40. package/dist/core/awaiting-answer.js +303 -0
  41. package/dist/core/build-timeline.js +61 -7
  42. package/dist/core/client-actions.d.ts +1 -0
  43. package/dist/core/client-actions.js +7 -1
  44. package/dist/core/composer-model-name.d.ts +3 -0
  45. package/dist/core/composer-model-name.js +21 -0
  46. package/dist/core/display-text.d.ts +2 -0
  47. package/dist/core/display-text.js +7 -0
  48. package/dist/core/draft-storage.d.ts +6 -0
  49. package/dist/core/draft-storage.js +25 -5
  50. package/dist/core/session-projection.d.ts +1 -0
  51. package/dist/core/session-projection.js +3 -2
  52. package/dist/core/session-query.d.ts +8 -0
  53. package/dist/core/session-query.js +13 -11
  54. package/dist/core/session-view-state.d.ts +2 -4
  55. package/dist/core/session-view-state.js +27 -9
  56. package/dist/core/tool-groups.d.ts +3 -0
  57. package/dist/core/tool-groups.js +37 -1
  58. package/dist/core/tool-view.d.ts +6 -0
  59. package/dist/core/tool-view.js +33 -0
  60. package/dist/core/use-timeline-item-key.d.ts +3 -4
  61. package/dist/core/use-timeline-item-key.js +8 -14
  62. package/dist/headless.d.ts +10 -5
  63. package/dist/headless.js +9 -4
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.js +3 -0
  66. package/dist/parakeet.d.ts +6 -2
  67. package/dist/parakeet.js +24 -17
  68. package/dist/store/types.d.ts +14 -1
  69. package/dist/styles.css +911 -162
  70. package/dist/use-approval-awaiting.d.ts +22 -0
  71. package/dist/use-approval-awaiting.js +81 -0
  72. package/dist/use-decision.d.ts +20 -0
  73. package/dist/use-decision.js +79 -0
  74. package/docs/api/commands/slash.md +30 -26
  75. package/docs/api/headless.md +917 -54
  76. package/docs/api/index.md +166 -5
  77. package/package.json +17 -8
  78. package/src/completion-menu.css +72 -0
  79. package/src/composer-shell.css +125 -0
  80. package/src/styles.css +99 -53
  81. package/dist/components/WaitingNotice.d.ts +0 -7
  82. 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) => (_jsxs(DataRow, { children: [_jsx("span", { className: "text-sm opacity-70", children: entry.label }), " ", _jsx("span", { className: "break-words", children: entry.value })] }, entry.label))) }));
16
- return (_jsxs(_Fragment, { children: [_jsx("p", { className: "font-medium", children: request.message }), 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)] }))] }));
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
- /** 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. */
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, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PopoverOption } from "@fungi.computer/caps/components/popover";
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
- /** 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. */
19
+ /** Parakeet's slash adapter: maps its completion rows onto the shared menu. */
24
20
  export function SlashMenu({ slash, listId, }) {
25
- const { completion, selected } = slash;
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 (_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))) }))] }));
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("button", { "aria-label": "Dismiss failed prompt", className: "btn btn-ghost btn-xs", onClick: () => {
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.providerError !== undefined ? (_jsxs("div", { className: "px-3 text-error", "data-provider-error": item.id, children: [_jsx("p", { className: "whitespace-pre-wrap", children: item.providerError }), _jsxs("details", { children: [_jsx("summary", { children: "Provider details" }), _jsx("pre", { className: "whitespace-pre-wrap", children: item.providerError })] })] })) : null, !item.pending && !item.isError
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, waitForBaseline?: boolean) => void;
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, waitForBaseline = true) => {
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 (waitForBaseline && options.reportsSelection !== false) {
117
+ if (options.reportsSelection !== false) {
118
118
  await connection.ui.reportAgentSessionSelection({
119
119
  agentId,
120
120
  sessionId,
@@ -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. */