@fungi.computer/parakeet 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) 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 +11 -0
  18. package/dist/commands/run.js +61 -0
  19. package/dist/commands/slash.d.ts +124 -0
  20. package/dist/commands/slash.js +182 -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 +27 -0
  24. package/dist/components/AgentHead.js +53 -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 +19 -0
  40. package/dist/components/ShiitakeAvatar.js +20 -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 +23 -0
  52. package/dist/components/avatar-presentation.js +16 -0
  53. package/dist/components/sprite-service.d.ts +2 -0
  54. package/dist/components/sprite-service.js +9 -0
  55. package/dist/components/still-frame-cache.d.ts +16 -0
  56. package/dist/components/still-frame-cache.js +63 -0
  57. package/dist/conversation.d.ts +164 -0
  58. package/dist/conversation.js +760 -0
  59. package/dist/core/assistant-visibility.d.ts +3 -0
  60. package/dist/core/assistant-visibility.js +4 -0
  61. package/dist/core/author-labels.d.ts +16 -0
  62. package/dist/core/author-labels.js +16 -0
  63. package/dist/core/build-timeline.d.ts +36 -0
  64. package/dist/core/build-timeline.js +328 -0
  65. package/dist/core/client-actions.d.ts +35 -0
  66. package/dist/core/client-actions.js +128 -0
  67. package/dist/core/client-contributions.d.ts +24 -0
  68. package/dist/core/client-contributions.js +115 -0
  69. package/dist/core/client-timeline.d.ts +33 -0
  70. package/dist/core/client-timeline.js +72 -0
  71. package/dist/core/format-context.d.ts +11 -0
  72. package/dist/core/format-context.js +20 -0
  73. package/dist/core/format-tokens.d.ts +2 -0
  74. package/dist/core/format-tokens.js +6 -0
  75. package/dist/core/model-identity.d.ts +3 -0
  76. package/dist/core/model-identity.js +7 -0
  77. package/dist/core/session-projection.d.ts +11 -0
  78. package/dist/core/session-projection.js +191 -0
  79. package/dist/core/session-query.d.ts +118 -0
  80. package/dist/core/session-query.js +187 -0
  81. package/dist/core/session-view-state.d.ts +66 -0
  82. package/dist/core/session-view-state.js +114 -0
  83. package/dist/core/tool-contract.d.ts +15 -0
  84. package/dist/core/tool-contract.js +3 -0
  85. package/dist/core/tool-groups.d.ts +36 -0
  86. package/dist/core/tool-groups.js +151 -0
  87. package/dist/core/tool-view.d.ts +182 -0
  88. package/dist/core/tool-view.js +618 -0
  89. package/dist/headless.d.ts +28 -0
  90. package/dist/headless.js +24 -0
  91. package/dist/index.d.ts +9 -0
  92. package/dist/index.js +4 -0
  93. package/dist/lib/failure.d.ts +2 -0
  94. package/dist/lib/failure.js +9 -0
  95. package/dist/markdown.d.ts +3 -0
  96. package/dist/markdown.js +6 -0
  97. package/dist/model-commands.d.ts +44 -0
  98. package/dist/model-commands.js +232 -0
  99. package/dist/parakeet.d.ts +77 -0
  100. package/dist/parakeet.js +276 -0
  101. package/dist/provider-choices.d.ts +8 -0
  102. package/dist/provider-choices.js +43 -0
  103. package/dist/reload-command.d.ts +43 -0
  104. package/dist/reload-command.js +72 -0
  105. package/dist/session-commands.d.ts +53 -0
  106. package/dist/session-commands.js +259 -0
  107. package/dist/session.d.ts +24 -0
  108. package/dist/session.js +1 -0
  109. package/dist/store/types.d.ts +163 -0
  110. package/dist/store/types.js +1 -0
  111. package/dist/styles.css +19156 -0
  112. package/docs/api/README.md +15 -0
  113. package/docs/api/commands/slash.md +321 -0
  114. package/docs/api/headless.md +2510 -0
  115. package/docs/api/index.md +1253 -0
  116. package/package.json +79 -3
  117. package/src/styles.css +268 -0
@@ -0,0 +1,182 @@
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
+ keys: keys.get(row.commandId) ?? [],
42
+ availability: row.availability,
43
+ }));
44
+ }
45
+ /** Every bound key in the live graph, grouped by command. */
46
+ export function keyBindings(snapshot) {
47
+ const rows = new Map();
48
+ for (const row of snapshot.bindings) {
49
+ const current = rows.get(row.commandId);
50
+ rows.set(row.commandId, {
51
+ commandId: row.commandId,
52
+ keys: current === undefined
53
+ ? keyLabel(row.binding)
54
+ : `${current.keys}, ${keyLabel(row.binding)}`,
55
+ title: row.title,
56
+ order: current?.order ?? row.order,
57
+ });
58
+ }
59
+ return [...rows.values()];
60
+ }
61
+ function usage(command) {
62
+ const words = command.grammar?.words?.map(({ word }) => word) ?? [];
63
+ if (command.grammar === undefined)
64
+ return `/${command.alias} takes no arguments.`;
65
+ if (command.grammar.text !== undefined)
66
+ return `Use /${command.alias} ${command.grammar.hint}.`;
67
+ return `Use /${command.alias} ${words.join(" or ")}.`;
68
+ }
69
+ /** Parse one draft against the registry; only a leading `/` names a command. */
70
+ export function parseSlashDraft(registry, draft) {
71
+ if (!draft.startsWith("/"))
72
+ return { kind: "prompt" };
73
+ const match = /^\/(\S*)(?:\s+([\s\S]*))?$/u.exec(draft.trimEnd());
74
+ const alias = match?.[1] ?? "";
75
+ const rest = (match?.[2] ?? "").trim();
76
+ const command = registry.find((candidate) => candidate.alias === alias);
77
+ if (command === undefined)
78
+ return {
79
+ kind: "invalid",
80
+ message: `Unsupported command: /${alias}. Use /help for available commands.`,
81
+ };
82
+ if (rest.length === 0)
83
+ return { kind: "command", command };
84
+ const grammar = command.grammar;
85
+ const word = grammar?.words?.find((candidate) => candidate.word === rest);
86
+ if (word !== undefined)
87
+ return { kind: "command", command, arguments: word.arguments };
88
+ if (grammar?.text === undefined)
89
+ return { kind: "invalid", message: usage(command) };
90
+ return {
91
+ kind: "command",
92
+ command,
93
+ arguments: { ...grammar.text.arguments, [grammar.text.field]: rest },
94
+ requestIdField: grammar.text.requestIdField,
95
+ };
96
+ }
97
+ /**
98
+ * Fuzzy match: a prefix beats a word start beats a substring beats an
99
+ * in-order subsequence; tighter subsequences rank first.
100
+ */
101
+ export function fuzzyMatch(candidate, query) {
102
+ const text = candidate.toLocaleLowerCase();
103
+ const needle = query.toLocaleLowerCase();
104
+ if (needle.length === 0)
105
+ return { tier: 0, spread: 0, indices: [] };
106
+ const range = (start) => Array.from({ length: needle.length }, (_, offset) => start + offset);
107
+ if (text.startsWith(needle))
108
+ return { tier: 0, spread: text.length - needle.length, indices: range(0) };
109
+ const wordStart = text.search(new RegExp(`[-_\\s]${needle.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&")}`, "u"));
110
+ if (wordStart >= 0)
111
+ return { tier: 1, spread: wordStart, indices: range(wordStart + 1) };
112
+ const inside = text.indexOf(needle);
113
+ if (inside >= 0)
114
+ return { tier: 2, spread: inside, indices: range(inside) };
115
+ const indices = [];
116
+ let from = 0;
117
+ for (const character of needle) {
118
+ const found = text.indexOf(character, from);
119
+ if (found < 0)
120
+ return undefined;
121
+ indices.push(found);
122
+ from = found + 1;
123
+ }
124
+ const first = indices[0] ?? 0;
125
+ const last = indices.at(-1) ?? 0;
126
+ return { tier: 3, spread: last - first - needle.length + 1, indices };
127
+ }
128
+ function rank(entries, text, query, order) {
129
+ return entries
130
+ .map((entry) => ({ entry, match: fuzzyMatch(text(entry), query) }))
131
+ .filter((scored) => scored.match !== undefined)
132
+ .sort((left, right) => left.match.tier - right.match.tier ||
133
+ left.match.spread - right.match.spread ||
134
+ order(left.entry) - order(right.entry) ||
135
+ text(left.entry).localeCompare(text(right.entry)))
136
+ .map(({ entry, match }) => ({ entry, indices: match.indices }));
137
+ }
138
+ /** Rank completions for one draft; renderers draw exactly these rows. */
139
+ export function completeSlash(registry, draft, options = {}) {
140
+ if (!draft.startsWith("/") || draft.includes("\n"))
141
+ return { kind: "none" };
142
+ const space = draft.search(/\s/u);
143
+ if (space < 0) {
144
+ const query = draft.slice(1);
145
+ const items = rank(registry.filter((command) => command.availability.status === "available" &&
146
+ (query.length === 0
147
+ ? command.category === (options.idleCategory ?? "Conversation")
148
+ : command.category !== "Advanced" ||
149
+ command.alias
150
+ .toLocaleLowerCase()
151
+ .startsWith(query.toLocaleLowerCase()))), (command) => command.alias, query, (command) => command.order).map(({ entry, indices }) => ({
152
+ command: entry,
153
+ label: `/${entry.alias}`,
154
+ matches: indices.map((index) => index + 1),
155
+ insert: entry.grammar === undefined ? `/${entry.alias}` : `/${entry.alias} `,
156
+ }));
157
+ return { kind: "commands", query, items };
158
+ }
159
+ const alias = draft.slice(1, space);
160
+ const command = registry.find((candidate) => candidate.alias === alias);
161
+ if (command?.grammar === undefined ||
162
+ command.availability.status !== "available")
163
+ return { kind: "none" };
164
+ const query = draft.slice(space).trimStart();
165
+ // Words match by prefix only, so free text is never mistaken for a word.
166
+ const words = query.includes(" ") ? [] : (command.grammar.words ?? []);
167
+ const items = words
168
+ .filter((word) => word.word.toLocaleLowerCase().startsWith(query.toLocaleLowerCase()))
169
+ .map((word) => ({
170
+ word,
171
+ label: word.word,
172
+ matches: Array.from({ length: query.length }, (_, index) => index),
173
+ insert: `/${alias} ${word.word}`,
174
+ }));
175
+ return {
176
+ kind: "arguments",
177
+ command,
178
+ query,
179
+ hint: `/${alias} ${command.grammar.hint}`,
180
+ items,
181
+ };
182
+ }
@@ -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) => runSlashDraft(captured, commands, () => {
127
+ current.send(delivery);
128
+ }, origin))
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,27 @@
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. Renders the Agent's own avatar when given
6
+ * one; otherwise the pre-avatar seeded mushroom cap, byte-identical to
7
+ * before avatars existed. The letter shows until a frame is ready. It holds
8
+ * still unless `thinking` or `speaking`: an idle head whose render style is
9
+ * "plain" and already has a cached still frame for its look never mounts a
10
+ * live canvas, so a long rail is not a wall of animation. A render style
11
+ * other than "plain" is a CSS filter over the live canvas, not part of the
12
+ * captured pixels, so a styled head always stays live.
13
+ */
14
+ export declare const AgentHead: import("react").NamedExoticComponent<{
15
+ /** Motion presented by the head; defaults to idle. */
16
+ activity?: AgentHeadActivity;
17
+ /** The Agent's stored avatar; omitted or `null` falls back to the seed. */
18
+ avatar?: Avatar | null;
19
+ /** Host classes applied to the head container. */
20
+ className?: string;
21
+ /** The name whose first letter stands in until a frame is ready. */
22
+ name: string;
23
+ /** Stable identity that picks the fallback cap colour. */
24
+ seed: string;
25
+ /** Width and height in pixels. */
26
+ size?: number;
27
+ }>;
@@ -0,0 +1,53 @@
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, useEffect, useMemo, useRef, useState } from "react";
5
+ import { avatarPresentation } from "./avatar-presentation.js";
6
+ import { shiitakeSprite } from "./sprite-service.js";
7
+ import { captureStillFrame, getStillFrame, setStillFrame, stillFrameKey, } from "./still-frame-cache.js";
8
+ /**
9
+ * An Agent's head at avatar size. Renders the Agent's own avatar when given
10
+ * one; otherwise the pre-avatar seeded mushroom cap, byte-identical to
11
+ * before avatars existed. The letter shows until a frame is ready. It holds
12
+ * still unless `thinking` or `speaking`: an idle head whose render style is
13
+ * "plain" and already has a cached still frame for its look never mounts a
14
+ * live canvas, so a long rail is not a wall of animation. A render style
15
+ * other than "plain" is a CSS filter over the live canvas, not part of the
16
+ * captured pixels, so a styled head always stays live.
17
+ */
18
+ export const AgentHead = memo(function AgentHead({ activity = "idle", avatar, className, name, seed, size = 24, }) {
19
+ const [ready, setReady] = useState(false);
20
+ const host = useRef(null);
21
+ const presentation = useMemo(() => avatarPresentation(avatar, seed), [avatar, seed]);
22
+ const dpr = window.devicePixelRatio;
23
+ const cacheKey = useMemo(() => stillFrameKey(presentation, size, dpr), [presentation, size, dpr]);
24
+ const [frame, setFrame] = useState(() => getStillFrame(cacheKey));
25
+ useEffect(() => {
26
+ setFrame(getStillFrame(cacheKey));
27
+ }, [cacheKey]);
28
+ // A render style is a CSS filter on the live stage (render-style.ts), not
29
+ // part of the captured canvas pixels, so a styled head must stay live;
30
+ // only a "plain" idle head can be replaced by its cached still frame.
31
+ const live = activity !== "idle" ||
32
+ frame === undefined ||
33
+ presentation.renderStyle !== "plain";
34
+ useEffect(() => {
35
+ // A cached idle head has no player; its next player must become ready anew.
36
+ if (!live)
37
+ setReady(false);
38
+ }, [live]);
39
+ return (_jsxs("span", { "aria-hidden": "true", className: `relative inline-block shrink-0 ${className ?? ""}`, "data-agent-head": frame !== undefined || ready ? "ready" : "loading", style: { height: size, width: size }, children: [frame === undefined && !ready ? (_jsx(Monogram, { className: "absolute inset-0", name: name, seed: seed })) : null, frame !== undefined &&
40
+ presentation.renderStyle === "plain" &&
41
+ (!live || !ready) ? (_jsx("img", { alt: "", className: "absolute inset-0 h-full w-full", src: frame })) : null, live ? (_jsx(Sprite, { activity: activity, appearance: presentation.appearance, className: "absolute inset-0", expression: presentation.expression, onState: (state) => {
42
+ setReady(state.status === "ready");
43
+ if (state.status === "ready" &&
44
+ activity === "idle" &&
45
+ presentation.renderStyle === "plain") {
46
+ const captured = captureStillFrame(host.current);
47
+ if (captured !== undefined) {
48
+ setStillFrame(cacheKey, captured);
49
+ setFrame(captured);
50
+ }
51
+ }
52
+ }, paused: activity === "idle", ref: host, renderStyle: presentation.renderStyle, service: shiitakeSprite, src: presentation.src })) : null] }));
53
+ });
@@ -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>;