@fungi.computer/parakeet 0.0.0-stage → 0.1.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -2
  3. package/dist/account-commands.d.ts +102 -0
  4. package/dist/account-commands.js +239 -0
  5. package/dist/agent-head.d.ts +4 -0
  6. package/dist/agent-head.js +2 -0
  7. package/dist/client-contributions.d.ts +20 -0
  8. package/dist/client-contributions.js +41 -0
  9. package/dist/commands/advertised.d.ts +23 -0
  10. package/dist/commands/advertised.js +72 -0
  11. package/dist/commands/builtins.d.ts +14 -0
  12. package/dist/commands/builtins.js +91 -0
  13. package/dist/commands/hints.d.ts +36 -0
  14. package/dist/commands/hints.js +64 -0
  15. package/dist/commands/interactions.d.ts +55 -0
  16. package/dist/commands/interactions.js +101 -0
  17. package/dist/commands/run.d.ts +12 -0
  18. package/dist/commands/run.js +64 -0
  19. package/dist/commands/slash.d.ts +126 -0
  20. package/dist/commands/slash.js +190 -0
  21. package/dist/commands/use-slash-commands.d.ts +57 -0
  22. package/dist/commands/use-slash-commands.js +207 -0
  23. package/dist/components/AgentHead.d.ts +22 -0
  24. package/dist/components/AgentHead.js +20 -0
  25. package/dist/components/ChatComposer.d.ts +43 -0
  26. package/dist/components/ChatComposer.js +209 -0
  27. package/dist/components/ChatMessage.d.ts +8 -0
  28. package/dist/components/ChatMessage.js +16 -0
  29. package/dist/components/ClientRows.d.ts +11 -0
  30. package/dist/components/ClientRows.js +46 -0
  31. package/dist/components/CodeBlock.d.ts +11 -0
  32. package/dist/components/CodeBlock.js +38 -0
  33. package/dist/components/InteractionPanel.d.ts +15 -0
  34. package/dist/components/InteractionPanel.js +42 -0
  35. package/dist/components/MarkdownRenderer.d.ts +6 -0
  36. package/dist/components/MarkdownRenderer.js +48 -0
  37. package/dist/components/RunFailureNotice.d.ts +7 -0
  38. package/dist/components/RunFailureNotice.js +13 -0
  39. package/dist/components/ShiitakeAvatar.d.ts +21 -0
  40. package/dist/components/ShiitakeAvatar.js +22 -0
  41. package/dist/components/SlashMenu.d.ts +8 -0
  42. package/dist/components/SlashMenu.js +50 -0
  43. package/dist/components/ThinkingBlock.d.ts +3 -0
  44. package/dist/components/ThinkingBlock.js +18 -0
  45. package/dist/components/TimelineItem.d.ts +15 -0
  46. package/dist/components/TimelineItem.js +35 -0
  47. package/dist/components/ToolGroup.d.ts +12 -0
  48. package/dist/components/ToolGroup.js +90 -0
  49. package/dist/components/WaitingNotice.d.ts +7 -0
  50. package/dist/components/WaitingNotice.js +8 -0
  51. package/dist/components/avatar-presentation.d.ts +16 -0
  52. package/dist/components/avatar-presentation.js +9 -0
  53. package/dist/components/sprite-service.d.ts +2 -0
  54. package/dist/components/sprite-service.js +9 -0
  55. package/dist/conversation.d.ts +165 -0
  56. package/dist/conversation.js +762 -0
  57. package/dist/core/assistant-visibility.d.ts +3 -0
  58. package/dist/core/assistant-visibility.js +4 -0
  59. package/dist/core/author-labels.d.ts +16 -0
  60. package/dist/core/author-labels.js +16 -0
  61. package/dist/core/build-timeline.d.ts +36 -0
  62. package/dist/core/build-timeline.js +328 -0
  63. package/dist/core/client-actions.d.ts +35 -0
  64. package/dist/core/client-actions.js +128 -0
  65. package/dist/core/client-contributions.d.ts +24 -0
  66. package/dist/core/client-contributions.js +115 -0
  67. package/dist/core/client-timeline.d.ts +33 -0
  68. package/dist/core/client-timeline.js +72 -0
  69. package/dist/core/format-context.d.ts +11 -0
  70. package/dist/core/format-context.js +20 -0
  71. package/dist/core/format-tokens.d.ts +2 -0
  72. package/dist/core/format-tokens.js +6 -0
  73. package/dist/core/model-identity.d.ts +3 -0
  74. package/dist/core/model-identity.js +7 -0
  75. package/dist/core/session-projection.d.ts +11 -0
  76. package/dist/core/session-projection.js +191 -0
  77. package/dist/core/session-query.d.ts +118 -0
  78. package/dist/core/session-query.js +187 -0
  79. package/dist/core/session-view-state.d.ts +66 -0
  80. package/dist/core/session-view-state.js +114 -0
  81. package/dist/core/tool-contract.d.ts +15 -0
  82. package/dist/core/tool-contract.js +3 -0
  83. package/dist/core/tool-groups.d.ts +36 -0
  84. package/dist/core/tool-groups.js +151 -0
  85. package/dist/core/tool-view.d.ts +182 -0
  86. package/dist/core/tool-view.js +618 -0
  87. package/dist/headless.d.ts +28 -0
  88. package/dist/headless.js +24 -0
  89. package/dist/index.d.ts +9 -0
  90. package/dist/index.js +4 -0
  91. package/dist/lib/failure.d.ts +2 -0
  92. package/dist/lib/failure.js +9 -0
  93. package/dist/markdown.d.ts +3 -0
  94. package/dist/markdown.js +6 -0
  95. package/dist/model-commands.d.ts +44 -0
  96. package/dist/model-commands.js +232 -0
  97. package/dist/parakeet.d.ts +77 -0
  98. package/dist/parakeet.js +276 -0
  99. package/dist/provider-choices.d.ts +8 -0
  100. package/dist/provider-choices.js +46 -0
  101. package/dist/reload-command.d.ts +43 -0
  102. package/dist/reload-command.js +72 -0
  103. package/dist/session-commands.d.ts +53 -0
  104. package/dist/session-commands.js +259 -0
  105. package/dist/session.d.ts +24 -0
  106. package/dist/session.js +1 -0
  107. package/dist/store/types.d.ts +163 -0
  108. package/dist/store/types.js +1 -0
  109. package/dist/styles.css +19129 -0
  110. package/docs/api/README.md +15 -0
  111. package/docs/api/commands/slash.md +321 -0
  112. package/docs/api/headless.md +2510 -0
  113. package/docs/api/index.md +1249 -0
  114. package/package.json +79 -3
  115. package/src/styles.css +272 -0
@@ -0,0 +1,190 @@
1
+ /** Human key labels from Whistle bindings, with the platform's modifier name. */
2
+ export function keyLabel(binding, mac = /Mac|iPhone|iPad/u.test(globalThis.navigator.userAgent)) {
3
+ return binding
4
+ .split(" ")
5
+ .map((chord, index) => {
6
+ if (chord === "<leader>")
7
+ return "Ctrl K";
8
+ if (index > 0 && binding.startsWith("<leader> "))
9
+ return chord;
10
+ const parts = chord
11
+ .split("+")
12
+ .map((part) => part === "mod"
13
+ ? mac
14
+ ? "⌘"
15
+ : "Ctrl"
16
+ : part === "escape"
17
+ ? "Esc"
18
+ : part.length === 1
19
+ ? part.toUpperCase()
20
+ : part.charAt(0).toUpperCase() + part.slice(1));
21
+ return parts.join(chord === "ctrl+k" || chord === "mod+k" ? " " : "+");
22
+ })
23
+ .join(" ");
24
+ }
25
+ /** Every slash alias in the live graph, with the keys bound to its command. */
26
+ export function slashRegistry(snapshot) {
27
+ const keys = new Map();
28
+ for (const row of snapshot.bindings)
29
+ keys.set(row.commandId, [
30
+ ...(keys.get(row.commandId) ?? []),
31
+ keyLabel(row.binding),
32
+ ]);
33
+ return snapshot.slash.map((row) => ({
34
+ alias: row.alias,
35
+ commandId: row.commandId,
36
+ title: row.title,
37
+ description: row.description,
38
+ category: row.category,
39
+ order: row.order,
40
+ grammar: row.arguments,
41
+ consumesDraft: row.consumesDraft,
42
+ keys: keys.get(row.commandId) ?? [],
43
+ availability: row.availability,
44
+ }));
45
+ }
46
+ /** Every bound key in the live graph, grouped by command. */
47
+ export function keyBindings(snapshot) {
48
+ const rows = new Map();
49
+ for (const row of snapshot.bindings) {
50
+ const current = rows.get(row.commandId);
51
+ rows.set(row.commandId, {
52
+ commandId: row.commandId,
53
+ keys: current === undefined
54
+ ? keyLabel(row.binding)
55
+ : `${current.keys}, ${keyLabel(row.binding)}`,
56
+ title: row.title,
57
+ order: current?.order ?? row.order,
58
+ });
59
+ }
60
+ return [...rows.values()];
61
+ }
62
+ function usage(command) {
63
+ const words = command.grammar?.words?.map(({ word }) => word) ?? [];
64
+ if (command.grammar === undefined)
65
+ return `/${command.alias} takes no arguments.`;
66
+ if (command.grammar.text !== undefined)
67
+ return `Use /${command.alias} ${command.grammar.hint}.`;
68
+ return `Use /${command.alias} ${words.join(" or ")}.`;
69
+ }
70
+ /** Parse one draft against the registry; only a leading `/` names a command. */
71
+ export function parseSlashDraft(registry, draft) {
72
+ if (!draft.startsWith("/"))
73
+ return { kind: "prompt" };
74
+ const match = /^\/(\S*)(?:\s+([\s\S]*))?$/u.exec(draft.trimEnd());
75
+ const alias = match?.[1] ?? "";
76
+ const rest = (match?.[2] ?? "").trim();
77
+ const command = registry.find((candidate) => candidate.alias === alias);
78
+ if (command === undefined)
79
+ return {
80
+ kind: "invalid",
81
+ message: `Unsupported command: /${alias}. Use /help for available commands.`,
82
+ };
83
+ if (rest.length === 0)
84
+ return { kind: "command", command };
85
+ const grammar = command.grammar;
86
+ const word = grammar?.words?.find((candidate) => candidate.word === rest);
87
+ if (word !== undefined)
88
+ return { kind: "command", command, arguments: word.arguments };
89
+ if (grammar?.text === undefined)
90
+ return { kind: "invalid", message: usage(command) };
91
+ return {
92
+ kind: "command",
93
+ command,
94
+ arguments: { ...grammar.text.arguments, [grammar.text.field]: rest },
95
+ requestIdField: grammar.text.requestIdField,
96
+ };
97
+ }
98
+ /**
99
+ * Fuzzy match: a prefix beats a word start beats a substring beats an
100
+ * in-order subsequence; tighter subsequences rank first.
101
+ */
102
+ export function fuzzyMatch(candidate, query) {
103
+ const text = candidate.toLocaleLowerCase();
104
+ const needle = query.toLocaleLowerCase();
105
+ if (needle.length === 0)
106
+ return { tier: 0, spread: 0, indices: [] };
107
+ const range = (start) => Array.from({ length: needle.length }, (_, offset) => start + offset);
108
+ if (text.startsWith(needle))
109
+ return { tier: 0, spread: text.length - needle.length, indices: range(0) };
110
+ const wordStart = text.search(new RegExp(`[-_\\s]${needle.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&")}`, "u"));
111
+ if (wordStart >= 0)
112
+ return { tier: 1, spread: wordStart, indices: range(wordStart + 1) };
113
+ const inside = text.indexOf(needle);
114
+ if (inside >= 0)
115
+ return { tier: 2, spread: inside, indices: range(inside) };
116
+ const indices = [];
117
+ let from = 0;
118
+ for (const character of needle) {
119
+ const found = text.indexOf(character, from);
120
+ if (found < 0)
121
+ return undefined;
122
+ indices.push(found);
123
+ from = found + 1;
124
+ }
125
+ const first = indices[0] ?? 0;
126
+ const last = indices.at(-1) ?? 0;
127
+ return { tier: 3, spread: last - first - needle.length + 1, indices };
128
+ }
129
+ function rank(entries, text, query, order) {
130
+ return entries
131
+ .map((entry) => ({ entry, match: fuzzyMatch(text(entry), query) }))
132
+ .filter((scored) => scored.match !== undefined)
133
+ .sort((left, right) => left.match.tier - right.match.tier ||
134
+ left.match.spread - right.match.spread ||
135
+ order(left.entry) - order(right.entry) ||
136
+ text(left.entry).localeCompare(text(right.entry)))
137
+ .map(({ entry, match }) => ({ entry, indices: match.indices }));
138
+ }
139
+ const commonAliases = ["help", "login", "model"];
140
+ /** Rank completions for one draft; renderers draw exactly these rows. */
141
+ export function completeSlash(registry, draft, options = {}) {
142
+ if (!draft.startsWith("/") || draft.includes("\n"))
143
+ return { kind: "none" };
144
+ const space = draft.search(/\s/u);
145
+ if (space < 0) {
146
+ const query = draft.slice(1);
147
+ const items = rank(registry.filter((command) => command.availability.status === "available" &&
148
+ (query.length === 0
149
+ ? command.category === (options.idleCategory ?? "Conversation") ||
150
+ commonAliases.includes(command.alias)
151
+ : command.category !== "Advanced" ||
152
+ command.alias
153
+ .toLocaleLowerCase()
154
+ .startsWith(query.toLocaleLowerCase()))), (command) => command.alias, query, (command) => {
155
+ const common = commonAliases.indexOf(command.alias);
156
+ return query.length === 0 && common >= 0
157
+ ? common - commonAliases.length
158
+ : command.order;
159
+ }).map(({ entry, indices }) => ({
160
+ command: entry,
161
+ label: `/${entry.alias}`,
162
+ matches: indices.map((index) => index + 1),
163
+ insert: entry.grammar === undefined ? `/${entry.alias}` : `/${entry.alias} `,
164
+ }));
165
+ return { kind: "commands", query, items };
166
+ }
167
+ const alias = draft.slice(1, space);
168
+ const command = registry.find((candidate) => candidate.alias === alias);
169
+ if (command?.grammar === undefined ||
170
+ command.availability.status !== "available")
171
+ return { kind: "none" };
172
+ const query = draft.slice(space).trimStart();
173
+ // Words match by prefix only, so free text is never mistaken for a word.
174
+ const words = query.includes(" ") ? [] : (command.grammar.words ?? []);
175
+ const items = words
176
+ .filter((word) => word.word.toLocaleLowerCase().startsWith(query.toLocaleLowerCase()))
177
+ .map((word) => ({
178
+ word,
179
+ label: word.word,
180
+ matches: Array.from({ length: query.length }, (_, index) => index),
181
+ insert: `/${alias} ${word.word}`,
182
+ }));
183
+ return {
184
+ kind: "arguments",
185
+ command,
186
+ query,
187
+ hint: `/${alias} ${command.grammar.hint}`,
188
+ items,
189
+ };
190
+ }
@@ -0,0 +1,57 @@
1
+ import type { PromptDelivery } from "@fungi.computer/shiitake/client";
2
+ import type { WhistleExecutionOrigin, WhistleRuntime } from "@fungi.computer/whistle";
3
+ import { type ComposerKeys, type KeyHint } from "./hints.js";
4
+ import { type ParakeetInteractions, type ParakeetInteractionState } from "./interactions.js";
5
+ import { type SlashCompletion } from "./slash.js";
6
+ /** The Whistle runtime a Parakeet composer runs commands through. */
7
+ export type ParakeetCommandRuntime = Pick<WhistleRuntime, "contribute" | "snapshot" | "subscribe" | "execute" | "present">;
8
+ /** Inputs for one composer's command controller. */
9
+ export type SlashCommandOptions = Readonly<{
10
+ /** Disable command registration, completion and execution for visitor conversations. */
11
+ enabled?: boolean;
12
+ /** The runtime holding host, built-in and advertised commands. */
13
+ commands: ParakeetCommandRuntime;
14
+ /** Execution origin for commands typed in this composer. */
15
+ origin: WhistleExecutionOrigin;
16
+ /** The renderer's composer keys, for hints. */
17
+ keys: ComposerKeys;
18
+ /** Whether a run is active. */
19
+ busy: boolean;
20
+ /** Host-owned interaction store shared with client activity observers. */
21
+ interactionStore?: ParakeetInteractions | undefined;
22
+ /** Use the host palette for /help instead of a separate slash-command list. */
23
+ showCommands?: (() => void | Promise<void>) | undefined;
24
+ }>;
25
+ /** Everything a renderer needs to draw and drive slash commands. */
26
+ export type SlashCommandController = Readonly<{
27
+ /** What to show for the current draft. */
28
+ completion: SlashCompletion;
29
+ /** Whether the completion popover is visible. */
30
+ open: boolean;
31
+ /** Index of the highlighted completion row. */
32
+ selected: number;
33
+ /** Key hints for the current state, most useful first when fitted. */
34
+ hints: readonly KeyHint[];
35
+ /** The command conversation state. */
36
+ interactions: ParakeetInteractionState;
37
+ /** The conversation store, for closing and retrying dialogs. */
38
+ store: ParakeetInteractions;
39
+ /** Move the highlight; false when no list is open. */
40
+ move: (delta: -1 | 1) => boolean;
41
+ /** Highlight one row (pointer hover). */
42
+ highlight: (index: number) => void;
43
+ /** Complete the highlighted row into the draft; false when nothing to complete. */
44
+ complete: () => boolean;
45
+ /** Dismiss the open interaction, then the completion, then the notice. */
46
+ dismiss: () => boolean;
47
+ /** Run the highlighted row, a command draft, or send a prompt. */
48
+ submit: (delivery?: PromptDelivery) => void;
49
+ /** Run one completion row (pointer choice). */
50
+ choose: (index: number) => void;
51
+ }>;
52
+ /**
53
+ * The one slash-command controller behind every Parakeet composer. It owns
54
+ * the registry, completion, hints, execution and conversations; renderers
55
+ * only draw its state and forward keys.
56
+ */
57
+ export declare function useSlashCommands(options: SlashCommandOptions): SlashCommandController;
@@ -0,0 +1,207 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
2
+ import { useComposer, useClientContributions, useExtensionSlots, useParakeetSession, useParakeetInteractions, } from "../conversation.js";
3
+ import { advertisedCommands } from "./advertised.js";
4
+ import { parakeetBuiltinCommands } from "./builtins.js";
5
+ import { composerHints } from "./hints.js";
6
+ import {} from "./interactions.js";
7
+ import { runSlashDraft } from "./run.js";
8
+ import { completeSlash, keyBindings, slashRegistry, } from "./slash.js";
9
+ function failureMessage(outcome) {
10
+ if (outcome === undefined || outcome.status === "handled")
11
+ return undefined;
12
+ if (outcome.status === "failed")
13
+ return outcome.error.message;
14
+ if (outcome.status === "unavailable")
15
+ return outcome.reason;
16
+ return `Command ${outcome.status}`;
17
+ }
18
+ /**
19
+ * The one slash-command controller behind every Parakeet composer. It owns
20
+ * the registry, completion, hints, execution and conversations; renderers
21
+ * only draw its state and forward keys.
22
+ */
23
+ export function useSlashCommands(options) {
24
+ const { commands, origin } = options;
25
+ const enabled = options.enabled ?? true;
26
+ const session = useParakeetSession();
27
+ const composer = useComposer();
28
+ const slots = useExtensionSlots();
29
+ const contributions = useClientContributions();
30
+ const contributionsRef = useRef(contributions);
31
+ contributionsRef.current = contributions;
32
+ const scopedInteractions = useParakeetInteractions();
33
+ const store = useMemo(() => options.interactionStore ?? scopedInteractions, [options.interactionStore, scopedInteractions]);
34
+ const interactions = useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
35
+ useEffect(() => {
36
+ const inspection = interactions.inspection;
37
+ if (inspection !== undefined &&
38
+ !contributions.some((entry) => entry.entryId === inspection.entryId &&
39
+ entry.value.panel.some((row) => row.id === inspection.rowId && row.inspection !== undefined)))
40
+ store.inspect(undefined);
41
+ }, [contributions, interactions.inspection, store]);
42
+ const snapshot = useSyncExternalStore(commands.subscribe, commands.snapshot, commands.snapshot);
43
+ const composerRef = useRef(composer);
44
+ composerRef.current = composer;
45
+ const showCommandsRef = useRef(options.showCommands);
46
+ showCommandsRef.current = options.showCommands;
47
+ const hasCommandPalette = options.showCommands !== undefined;
48
+ useEffect(() => {
49
+ if (!enabled)
50
+ return undefined;
51
+ return store.attach(commands.present);
52
+ }, [commands, enabled, store]);
53
+ useEffect(() => {
54
+ if (!enabled)
55
+ return;
56
+ const lease = commands.contribute({
57
+ sourceId: "parakeet",
58
+ namespace: "parakeet",
59
+ commands: parakeetBuiltinCommands(commands, {
60
+ showCommands: hasCommandPalette
61
+ ? () => showCommandsRef.current?.()
62
+ : undefined,
63
+ clear: () => {
64
+ composerRef.current.setDraft("");
65
+ store.report(undefined);
66
+ },
67
+ }),
68
+ });
69
+ return () => {
70
+ lease.dispose();
71
+ };
72
+ }, [commands, enabled, store, hasCommandPalette]);
73
+ const advertised = useMemo(() => advertisedCommands(commands, session, (entryId, commandId) => {
74
+ const row = contributionsRef.current
75
+ .find((entry) => entry.entryId === entryId)
76
+ ?.value.panel.find((row) => row.inspection?.commandId === commandId);
77
+ if (row !== undefined)
78
+ store.inspect({ entryId, rowId: row.id });
79
+ }), [commands, session, store]);
80
+ useEffect(() => () => {
81
+ advertised.dispose();
82
+ }, [advertised]);
83
+ useEffect(() => {
84
+ if (!enabled) {
85
+ advertised.dispose();
86
+ return;
87
+ }
88
+ const [error] = advertised.update(slots);
89
+ if (error !== undefined)
90
+ store.report({
91
+ tone: "error",
92
+ message: `${error.extensionId} commands are unavailable: ${error.message}`,
93
+ });
94
+ }, [advertised, enabled, slots, store]);
95
+ const registry = useMemo(() => slashRegistry(snapshot), [snapshot]);
96
+ const [dismissed, setDismissed] = useState();
97
+ const [highlighted, setHighlighted] = useState({ draft: "", index: 0 });
98
+ const draft = composer.draft;
99
+ const completion = !enabled || dismissed === draft || interactions.open !== undefined
100
+ ? { kind: "none" }
101
+ : completeSlash(registry, draft);
102
+ const rowCount = completion.kind === "none" ? 0 : completion.items.length;
103
+ const open = completion.kind === "arguments" ||
104
+ (completion.kind === "commands" && rowCount > 0);
105
+ const selected = highlighted.draft === draft
106
+ ? Math.min(highlighted.index, Math.max(0, rowCount - 1))
107
+ : 0;
108
+ const hints = composerHints({
109
+ completion,
110
+ busy: options.busy,
111
+ bindings: keyBindings(snapshot),
112
+ keys: options.keys,
113
+ });
114
+ const run = useCallback((delivery, replacement) => {
115
+ const current = composerRef.current;
116
+ if (!enabled) {
117
+ current.send(delivery);
118
+ return;
119
+ }
120
+ if (replacement !== undefined)
121
+ current.setDraft(replacement);
122
+ const submitted = replacement ?? current.draft;
123
+ store.report(undefined);
124
+ setDismissed(submitted);
125
+ void current
126
+ .submitCommand((captured, consumeDraft) => runSlashDraft(captured, commands, () => {
127
+ current.send(delivery);
128
+ }, origin, consumeDraft))
129
+ .then((outcome) => {
130
+ const message = failureMessage(outcome);
131
+ if (message !== undefined &&
132
+ store.snapshot().open === undefined &&
133
+ store.lastEnd() !== "cancelled")
134
+ store.report({
135
+ tone: "error",
136
+ message: `${submitted}: ${message}`,
137
+ });
138
+ });
139
+ }, [commands, enabled, origin, store]);
140
+ const insertAt = (index) => {
141
+ if (completion.kind === "commands")
142
+ return completion.items[index]?.insert;
143
+ if (completion.kind === "arguments")
144
+ return completion.items[index]?.insert;
145
+ return undefined;
146
+ };
147
+ const choose = (index) => {
148
+ const insert = insertAt(index);
149
+ if (insert === undefined)
150
+ return;
151
+ run(options.busy ? "followUp" : "steer", insert.trimEnd());
152
+ };
153
+ return {
154
+ completion,
155
+ open,
156
+ selected,
157
+ hints,
158
+ interactions,
159
+ store,
160
+ move: (delta) => {
161
+ if (rowCount === 0)
162
+ return false;
163
+ setHighlighted({
164
+ draft,
165
+ index: (selected + delta + rowCount) % rowCount,
166
+ });
167
+ return true;
168
+ },
169
+ highlight: (index) => {
170
+ setHighlighted({ draft, index });
171
+ },
172
+ complete: () => {
173
+ const insert = insertAt(selected);
174
+ if (insert === undefined)
175
+ return false;
176
+ composer.setDraft(insert);
177
+ return true;
178
+ },
179
+ dismiss: () => {
180
+ if (interactions.open !== undefined) {
181
+ store.close();
182
+ return true;
183
+ }
184
+ if (interactions.inspection !== undefined) {
185
+ store.inspect(undefined);
186
+ return true;
187
+ }
188
+ if (open) {
189
+ setDismissed(draft);
190
+ return true;
191
+ }
192
+ if (interactions.notice !== undefined) {
193
+ store.report(undefined);
194
+ return true;
195
+ }
196
+ return false;
197
+ },
198
+ submit: (delivery = options.busy ? "followUp" : "steer") => {
199
+ // A bare `/alias ` runs the alias; a typed word prefix runs its word.
200
+ const accepts = completion.kind === "commands" ||
201
+ (completion.kind === "arguments" && completion.query.length > 0);
202
+ const insert = accepts && rowCount > 0 ? insertAt(selected) : undefined;
203
+ run(delivery, insert?.trimEnd());
204
+ },
205
+ choose,
206
+ };
207
+ }
@@ -0,0 +1,22 @@
1
+ import type { Avatar } from "@fungi.computer/sprite/avatar";
2
+ /** What the head is doing: at rest, working something out, or talking. */
3
+ export type AgentHeadActivity = "idle" | "thinking" | "speaking";
4
+ /**
5
+ * An Agent's head at avatar size, drawn from the Agent's own avatar (a
6
+ * property of the Agent record, never of a conversation). The letter shows
7
+ * until a frame is ready. The live
8
+ * canvas stays mounted and is paused unless `thinking` or `speaking`, so an
9
+ * idle head shows its real look without animating.
10
+ */
11
+ export declare const AgentHead: import("react").NamedExoticComponent<{
12
+ /** Motion presented by the head; defaults to idle. */
13
+ activity?: AgentHeadActivity;
14
+ /** The Agent's avatar, from its Agent record. */
15
+ avatar: Avatar;
16
+ /** Host classes applied to the head container. */
17
+ className?: string;
18
+ /** The name whose first letter stands in until a frame is ready. */
19
+ name: string;
20
+ /** Width and height in pixels. */
21
+ size?: number;
22
+ }>;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Monogram } from "@fungi.computer/caps/components/monogram";
3
+ import { Sprite } from "@fungi.computer/sprite/react";
4
+ import { memo, useMemo, useState } from "react";
5
+ import { avatarPresentation } from "./avatar-presentation.js";
6
+ import { shiitakeSprite } from "./sprite-service.js";
7
+ /**
8
+ * An Agent's head at avatar size, drawn from the Agent's own avatar (a
9
+ * property of the Agent record, never of a conversation). The letter shows
10
+ * until a frame is ready. The live
11
+ * canvas stays mounted and is paused unless `thinking` or `speaking`, so an
12
+ * idle head shows its real look without animating.
13
+ */
14
+ export const AgentHead = memo(function AgentHead({ activity = "idle", avatar, className, name, size = 24, }) {
15
+ const [ready, setReady] = useState(false);
16
+ const presentation = useMemo(() => avatarPresentation(avatar), [avatar]);
17
+ return (_jsxs("span", { "aria-hidden": "true", className: `relative inline-block shrink-0 ${className ?? ""}`, "data-agent-head": ready ? "ready" : "loading", style: { height: size, width: size }, children: [ready ? null : (_jsx(Monogram, { className: "absolute inset-0", name: name, seed: name })), _jsx(Sprite, { activity: activity, appearance: presentation.appearance, className: "absolute inset-0", expression: presentation.expression, onState: (state) => {
18
+ setReady(state.status === "ready");
19
+ }, paused: activity === "idle", renderStyle: presentation.renderStyle, service: shiitakeSprite, src: presentation.src })] }));
20
+ });
@@ -0,0 +1,43 @@
1
+ import { type RefObject } from "react";
2
+ import type { PromptDelivery, SessionContext } from "@fungi.computer/shiitake/client";
3
+ import type { SlashCommandController } from "../commands/use-slash-commands.js";
4
+ import type { ComposerController, ParakeetStatus, ParakeetSessionViewState } from "../conversation.js";
5
+ export type ChatComposerModel = Readonly<{
6
+ model: string;
7
+ provider: string;
8
+ }>;
9
+ export type ChatComposerUsage = Readonly<{
10
+ cost: number;
11
+ tokens: number;
12
+ }>;
13
+ export type ChatComposerProps = {
14
+ /** Caller-owned prompt hint; omitted for the ordinary Session composer. */
15
+ placeholder?: string | undefined;
16
+ /** Hide internal model, usage and status readouts for visitors. */
17
+ showFooter?: boolean;
18
+ /** Display-only status text supplied by the conversation's admitted entries. */
19
+ statusChips?: readonly string[] | undefined;
20
+ actionVariant?: "primary" | "outline" | undefined;
21
+ draft: string;
22
+ failure: ComposerController["failure"];
23
+ failureCause?: string | undefined;
24
+ model?: ChatComposerModel | undefined;
25
+ onAbort: () => void;
26
+ onChange: (value: string) => void;
27
+ onRetry: () => void;
28
+ /** Submit the draft; `followUp` while a run is active, otherwise `steer`. */
29
+ onSend: (delivery: PromptDelivery) => void;
30
+ admissionError?: ComposerController["admissionError"];
31
+ status: ParakeetStatus;
32
+ /** Session owner readout, separate from local prompt admission. */
33
+ viewState: ParakeetSessionViewState;
34
+ textareaRef: RefObject<HTMLTextAreaElement>;
35
+ usage?: ChatComposerUsage | undefined;
36
+ /** Context use the Session's compaction measures, once known. */
37
+ context?: SessionContext | undefined;
38
+ /** Parakeet's slash controller; the composer only draws and forwards keys. */
39
+ slash?: SlashCommandController | undefined;
40
+ };
41
+ /** Shared visible and screen-reader readout of the observed Session state. */
42
+ export declare function sessionReadout(status: ParakeetStatus, view: ParakeetSessionViewState): string;
43
+ export declare const ChatComposer: import("react").NamedExoticComponent<ChatComposerProps>;