@fungi.computer/parakeet 0.0.1 → 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.
@@ -49,7 +49,7 @@ export function parakeetBuiltinCommands(runtime, options) {
49
49
  category: "Advanced",
50
50
  projections: {
51
51
  help: { order: -2 },
52
- slash: { aliases: ["keys"], order: -2 },
52
+ slash: { aliases: ["keys"], order: -2, consumesDraft: true },
53
53
  },
54
54
  handler: async (input) => {
55
55
  const keys = keyBindings(runtime.snapshot());
@@ -6,6 +6,7 @@ export declare function isCancelledOutcome(outcome: WhistleExecutionOutcome): bo
6
6
  /**
7
7
  * Run one composer draft: a prompt goes to `send`, a `/command` runs through
8
8
  * Whistle. A grammar that asks for a request id keeps the same id across
9
- * retries of the same draft until the command is handled.
9
+ * retries of the same draft until the command is handled. A slash projection
10
+ * can request draft consumption before dispatch, including while a dialog is open.
10
11
  */
11
- export declare function runSlashDraft(draft: string, commands: SlashRuntime, send: () => void, origin: WhistleExecutionOrigin): Promise<WhistleExecutionOutcome | undefined>;
12
+ export declare function runSlashDraft(draft: string, commands: SlashRuntime, send: () => void, origin: WhistleExecutionOrigin, consumeDraft?: () => void): Promise<WhistleExecutionOutcome | undefined>;
@@ -12,9 +12,10 @@ export function isCancelledOutcome(outcome) {
12
12
  /**
13
13
  * Run one composer draft: a prompt goes to `send`, a `/command` runs through
14
14
  * Whistle. A grammar that asks for a request id keeps the same id across
15
- * retries of the same draft until the command is handled.
15
+ * retries of the same draft until the command is handled. A slash projection
16
+ * can request draft consumption before dispatch, including while a dialog is open.
16
17
  */
17
- export async function runSlashDraft(draft, commands, send, origin) {
18
+ export async function runSlashDraft(draft, commands, send, origin, consumeDraft) {
18
19
  const parsed = parseSlashDraft(slashRegistry(commands.snapshot()), draft);
19
20
  if (parsed.kind === "prompt") {
20
21
  send();
@@ -26,6 +27,8 @@ export async function runSlashDraft(draft, commands, send, origin) {
26
27
  error: { code: "invalid_arguments", message: parsed.message },
27
28
  };
28
29
  const { command, requestIdField } = parsed;
30
+ if (command.consumesDraft && command.availability.status === "available")
31
+ consumeDraft?.();
29
32
  if (parsed.arguments === undefined)
30
33
  return commands.execute(command.commandId, { origin });
31
34
  if (requestIdField === undefined)
@@ -5,6 +5,8 @@ export type SlashCommand = Readonly<{
5
5
  alias: string;
6
6
  /** Canonical Whistle command identity. */
7
7
  commandId: string;
8
+ /** Consume this draft on dispatch, including for a dialog or refusal. */
9
+ consumesDraft?: boolean | undefined;
8
10
  /** Human-facing title. */
9
11
  title: string;
10
12
  /** Optional longer explanation. */
@@ -38,6 +38,7 @@ export function slashRegistry(snapshot) {
38
38
  category: row.category,
39
39
  order: row.order,
40
40
  grammar: row.arguments,
41
+ consumesDraft: row.consumesDraft,
41
42
  keys: keys.get(row.commandId) ?? [],
42
43
  availability: row.availability,
43
44
  }));
@@ -135,6 +136,7 @@ function rank(entries, text, query, order) {
135
136
  text(left.entry).localeCompare(text(right.entry)))
136
137
  .map(({ entry, match }) => ({ entry, indices: match.indices }));
137
138
  }
139
+ const commonAliases = ["help", "login", "model"];
138
140
  /** Rank completions for one draft; renderers draw exactly these rows. */
139
141
  export function completeSlash(registry, draft, options = {}) {
140
142
  if (!draft.startsWith("/") || draft.includes("\n"))
@@ -144,11 +146,17 @@ export function completeSlash(registry, draft, options = {}) {
144
146
  const query = draft.slice(1);
145
147
  const items = rank(registry.filter((command) => command.availability.status === "available" &&
146
148
  (query.length === 0
147
- ? command.category === (options.idleCategory ?? "Conversation")
149
+ ? command.category === (options.idleCategory ?? "Conversation") ||
150
+ commonAliases.includes(command.alias)
148
151
  : command.category !== "Advanced" ||
149
152
  command.alias
150
153
  .toLocaleLowerCase()
151
- .startsWith(query.toLocaleLowerCase()))), (command) => command.alias, query, (command) => command.order).map(({ entry, indices }) => ({
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 }) => ({
152
160
  command: entry,
153
161
  label: `/${entry.alias}`,
154
162
  matches: indices.map((index) => index + 1),
@@ -123,9 +123,9 @@ export function useSlashCommands(options) {
123
123
  store.report(undefined);
124
124
  setDismissed(submitted);
125
125
  void current
126
- .submitCommand((captured) => runSlashDraft(captured, commands, () => {
126
+ .submitCommand((captured, consumeDraft) => runSlashDraft(captured, commands, () => {
127
127
  current.send(delivery);
128
- }, origin))
128
+ }, origin, consumeDraft))
129
129
  .then((outcome) => {
130
130
  const message = failureMessage(outcome);
131
131
  if (message !== undefined &&
@@ -2,26 +2,21 @@ import type { Avatar } from "@fungi.computer/sprite/avatar";
2
2
  /** What the head is doing: at rest, working something out, or talking. */
3
3
  export type AgentHeadActivity = "idle" | "thinking" | "speaking";
4
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.
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.
13
10
  */
14
11
  export declare const AgentHead: import("react").NamedExoticComponent<{
15
12
  /** Motion presented by the head; defaults to idle. */
16
13
  activity?: AgentHeadActivity;
17
- /** The Agent's stored avatar; omitted or `null` falls back to the seed. */
18
- avatar?: Avatar | null;
14
+ /** The Agent's avatar, from its Agent record. */
15
+ avatar: Avatar;
19
16
  /** Host classes applied to the head container. */
20
17
  className?: string;
21
18
  /** The name whose first letter stands in until a frame is ready. */
22
19
  name: string;
23
- /** Stable identity that picks the fallback cap colour. */
24
- seed: string;
25
20
  /** Width and height in pixels. */
26
21
  size?: number;
27
22
  }>;
@@ -1,53 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Monogram } from "@fungi.computer/caps/components/monogram";
3
3
  import { Sprite } from "@fungi.computer/sprite/react";
4
- import { memo, useEffect, useMemo, useRef, useState } from "react";
4
+ import { memo, useMemo, useState } from "react";
5
5
  import { avatarPresentation } from "./avatar-presentation.js";
6
6
  import { shiitakeSprite } from "./sprite-service.js";
7
- import { captureStillFrame, getStillFrame, setStillFrame, stillFrameKey, } from "./still-frame-cache.js";
8
7
  /**
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.
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.
17
13
  */
18
- export const AgentHead = memo(function AgentHead({ activity = "idle", avatar, className, name, seed, size = 24, }) {
14
+ export const AgentHead = memo(function AgentHead({ activity = "idle", avatar, className, name, size = 24, }) {
19
15
  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) => {
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) => {
42
18
  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] }));
19
+ }, paused: activity === "idle", renderStyle: presentation.renderStyle, service: shiitakeSprite, src: presentation.src })] }));
53
20
  });
@@ -1,14 +1,16 @@
1
1
  import type { Avatar } from "@fungi.computer/sprite/avatar";
2
2
  import { type AvatarPresentation, type SpriteService } from "@fungi.computer/sprite";
3
- /** The current activity and expression supplied to a composer avatar. */
3
+ /** The current activity and expression supplied to a live avatar. */
4
4
  export type AvatarState = Readonly<Pick<AvatarPresentation, "activity" | "expression">>;
5
5
  /**
6
- * Shiitake's sprite head, drawn in the activity and expression it is given.
7
- * With no `avatar`, this is the product mascot, exactly as before avatars
8
- * existed: no seeded colour, the character's own defaults.
6
+ * A sprite head drawn in the live activity and expression it is given. With
7
+ * an `avatar` it draws that look (a member's own, in their settings); with
8
+ * none it is Shiitake, the product mascot, in the character's own defaults.
9
+ * Conversations have no avatar: an Agent's head is `AgentHead`, drawn from
10
+ * the Agent record.
9
11
  */
10
12
  export declare const ShiitakeAvatar: import("react").NamedExoticComponent<{
11
- /** The open conversation's avatar; omitted or `null` draws the mascot. */
13
+ /** The look to draw; omitted or `null` draws the mascot. */
12
14
  avatar?: Avatar | null;
13
15
  /** Replaces the default 3rem size, for a host that sizes the head itself. */
14
16
  className?: string;
@@ -5,12 +5,14 @@ import { memo, useMemo } from "react";
5
5
  import { avatarPresentation } from "./avatar-presentation.js";
6
6
  import { shiitakeSprite } from "./sprite-service.js";
7
7
  /**
8
- * Shiitake's sprite head, drawn in the activity and expression it is given.
9
- * With no `avatar`, this is the product mascot, exactly as before avatars
10
- * existed: no seeded colour, the character's own defaults.
8
+ * A sprite head drawn in the live activity and expression it is given. With
9
+ * an `avatar` it draws that look (a member's own, in their settings); with
10
+ * none it is Shiitake, the product mascot, in the character's own defaults.
11
+ * Conversations have no avatar: an Agent's head is `AgentHead`, drawn from
12
+ * the Agent record.
11
13
  */
12
14
  export const ShiitakeAvatar = memo(function ShiitakeAvatar({ avatar, className = "h-12 w-12 shrink-0", service = shiitakeSprite, state, }) {
13
- const presentation = useMemo(() => (avatar ? avatarPresentation(avatar, "shiitake-mascot") : undefined), [avatar]);
15
+ const presentation = useMemo(() => (avatar ? avatarPresentation(avatar) : undefined), [avatar]);
14
16
  return (_jsx(Sprite, { "aria-hidden": "true", className: className, service: service, src: presentation?.src ?? { local: "mushroom" }, ...(presentation
15
17
  ? {
16
18
  appearance: presentation.appearance,
@@ -1,12 +1,10 @@
1
- import { type Avatar } from "@fungi.computer/sprite/avatar";
1
+ import type { Avatar } from "@fungi.computer/sprite/avatar";
2
2
  import type { AvatarPresentation as SpriteAvatarPresentation } from "@fungi.computer/sprite";
3
3
  /**
4
- * Sprite props for one avatar, independent of the live `activity` a composer
5
- * drives moment to moment. Every avatar-aware Parakeet component builds its
6
- * `<Sprite>` props through this one mapper, so a stored avatar and the
7
- * pre-avatar seeded look reach the canvas the same way. Unlike the Sprite
8
- * service's own `AvatarPresentation`, these are never optional: the mapper
9
- * always resolves a complete avatar, stored or legacy.
4
+ * Sprite props for one Agent's avatar, independent of the live `activity` a
5
+ * host drives moment to moment. Unlike the Sprite service's own
6
+ * `AvatarPresentation`, these are never optional: every Agent record carries
7
+ * a complete avatar.
10
8
  */
11
9
  export type AvatarSpriteProps = {
12
10
  readonly src: SpriteAvatarPresentation["src"];
@@ -14,10 +12,5 @@ export type AvatarSpriteProps = {
14
12
  readonly expression: NonNullable<SpriteAvatarPresentation["expression"]>;
15
13
  readonly renderStyle: NonNullable<SpriteAvatarPresentation["renderStyle"]>;
16
14
  };
17
- /**
18
- * Map a stored avatar (or `null`/`undefined`, meaning none is saved yet) to
19
- * Sprite props. The fallback is `legacyAvatar(seed)`: the mushroom, in the
20
- * same seeded cap colour Agents have always worn, so an existing Agent
21
- * renders byte-identically to before avatars existed.
22
- */
23
- export declare function avatarPresentation(avatar: Avatar | null | undefined, seed: string): AvatarSpriteProps;
15
+ /** Map an Agent's avatar to Sprite props. */
16
+ export declare function avatarPresentation(avatar: Avatar): AvatarSpriteProps;
@@ -1,16 +1,9 @@
1
- import { legacyAvatar } from "@fungi.computer/sprite/avatar";
2
- /**
3
- * Map a stored avatar (or `null`/`undefined`, meaning none is saved yet) to
4
- * Sprite props. The fallback is `legacyAvatar(seed)`: the mushroom, in the
5
- * same seeded cap colour Agents have always worn, so an existing Agent
6
- * renders byte-identically to before avatars existed.
7
- */
8
- export function avatarPresentation(avatar, seed) {
9
- const resolved = avatar ?? legacyAvatar(seed);
1
+ /** Map an Agent's avatar to Sprite props. */
2
+ export function avatarPresentation(avatar) {
10
3
  return {
11
- src: { local: resolved.character },
12
- appearance: resolved.appearance,
13
- expression: resolved.expression,
14
- renderStyle: resolved.renderStyle,
4
+ src: { local: avatar.character },
5
+ appearance: avatar.appearance,
6
+ expression: avatar.expression,
7
+ renderStyle: avatar.renderStyle,
15
8
  };
16
9
  }
@@ -98,9 +98,10 @@ export type ComposerController = Readonly<{
98
98
  * Run a host command, such as Whistle slash dispatch, for the current draft.
99
99
  * Parakeet clears the draft only when the command handled it without
100
100
  * declining (a refused replacement keeps the text) and the draft was
101
- * not edited while the command ran.
101
+ * not edited while the command ran. The callback can consume that exact
102
+ * draft on dispatch when the command definition requests it.
102
103
  */
103
- submitCommand: (run: (draft: string) => Promise<WhistleExecutionOutcome | undefined>) => Promise<WhistleExecutionOutcome | undefined>;
104
+ submitCommand: (run: (draft: string, consumeDraft: () => void) => Promise<WhistleExecutionOutcome | undefined>) => Promise<WhistleExecutionOutcome | undefined>;
104
105
  /** Replace the local input. */
105
106
  setDraft: (value: string) => void;
106
107
  }>;
@@ -423,7 +423,9 @@ function createConversationAtoms(session, queryClient, authorOf) {
423
423
  draft: get(draftAtom),
424
424
  revision: get(draftRevisionAtom),
425
425
  };
426
- const outcome = await run(submitted.draft);
426
+ const outcome = await run(submitted.draft, () => {
427
+ set(clearSubmittedDraftAtom, submitted);
428
+ });
427
429
  if (outcome?.status === "handled" && !isCancelledOutcome(outcome))
428
430
  set(clearSubmittedDraftAtom, submitted);
429
431
  return outcome;
@@ -19,7 +19,7 @@ export declare const toolKindSigils: {
19
19
  /** Artwork for file reads. */
20
20
  readonly read: "read";
21
21
  /** Artwork for search results. */
22
- readonly search: "list";
22
+ readonly search: "search";
23
23
  };
24
24
  /** One changed or context line of a diff hunk. */
25
25
  export type ToolDiffLine = Readonly<{
@@ -25,7 +25,7 @@ export const toolKindSigils = {
25
25
  /** Artwork for file reads. */
26
26
  read: "read",
27
27
  /** Artwork for search results. */
28
- search: "list",
28
+ search: "search",
29
29
  };
30
30
  /** Items shown before "more" for excerpts, matches, and output. */
31
31
  export const toolPreviewLineLimit = 12;
@@ -198,7 +198,7 @@ export function sessionModelCommandDefinitions(session, options) {
198
198
  projections: {
199
199
  palette: { order: 1 },
200
200
  help: {},
201
- slash: { aliases: ["thinking"] },
201
+ slash: { aliases: ["thinking"], consumesDraft: true },
202
202
  },
203
203
  availability: options.availability ?? (() => ({ status: "available" })),
204
204
  handler: async (input) => {
@@ -18,13 +18,16 @@ export function providerChoices(providers, access, popularProviderIds, screen) {
18
18
  .map((provider) => {
19
19
  const methods = provider.methods.map((method) => method.kind === "api_key" ? "API key" : "Subscription");
20
20
  const credential = signedIn.get(provider.id);
21
- const description = methods.length > 0
22
- ? methods.join(" or ")
23
- : credential?.type === "oauth"
24
- ? "Subscription"
25
- : credential?.type === "api_key"
26
- ? "API key"
27
- : undefined;
21
+ // Fungi's sign-in is the Fungi account; its Team pays, not a subscription.
22
+ const description = provider.id === "fungi"
23
+ ? "Fungi account"
24
+ : methods.length > 0
25
+ ? methods.join(" or ")
26
+ : credential?.type === "oauth"
27
+ ? "Subscription"
28
+ : credential?.type === "api_key"
29
+ ? "API key"
30
+ : undefined;
28
31
  const choice = {
29
32
  value: provider.id,
30
33
  label: provider.name,
package/dist/styles.css CHANGED
@@ -5743,9 +5743,6 @@
5743
5743
  .mb-1 {
5744
5744
  margin-bottom: calc(var(--spacing) * 1);
5745
5745
  }
5746
- .mb-xs {
5747
- margin-bottom: var(--spacing-xs);
5748
- }
5749
5746
  .ml-2 {
5750
5747
  margin-left: calc(var(--spacing) * 2);
5751
5748
  }
@@ -6081,9 +6078,6 @@
6081
6078
  .h-0 {
6082
6079
  height: calc(var(--spacing) * 0);
6083
6080
  }
6084
- .h-4 {
6085
- height: calc(var(--spacing) * 4);
6086
- }
6087
6081
  .h-8 {
6088
6082
  height: calc(var(--spacing) * 8);
6089
6083
  }
@@ -6152,12 +6146,6 @@
6152
6146
  .w-12 {
6153
6147
  width: calc(var(--spacing) * 12);
6154
6148
  }
6155
- .w-20 {
6156
- width: calc(var(--spacing) * 20);
6157
- }
6158
- .w-\[calc\(100\%-1rem\)\] {
6159
- width: calc(100% - 1rem);
6160
- }
6161
6149
  .w-\[min\(32rem\,calc\(100vw-2rem\)\)\] {
6162
6150
  width: min(32rem, calc(100vw - 2rem));
6163
6151
  }
@@ -6203,12 +6191,12 @@
6203
6191
  .flex-shrink-0 {
6204
6192
  flex-shrink: 0;
6205
6193
  }
6194
+ .shrink {
6195
+ flex-shrink: 1;
6196
+ }
6206
6197
  .shrink-0 {
6207
6198
  flex-shrink: 0;
6208
6199
  }
6209
- .grow {
6210
- flex-grow: 1;
6211
- }
6212
6200
  .border-collapse {
6213
6201
  border-collapse: collapse;
6214
6202
  }
@@ -6360,9 +6348,6 @@
6360
6348
  .self-stretch {
6361
6349
  align-self: stretch;
6362
6350
  }
6363
- .justify-self-end {
6364
- justify-self: flex-end;
6365
- }
6366
6351
  .truncate {
6367
6352
  overflow: hidden;
6368
6353
  text-overflow: ellipsis;
@@ -6578,9 +6563,6 @@
6578
6563
  .p-lg {
6579
6564
  padding: var(--spacing-lg);
6580
6565
  }
6581
- .p-lg\! {
6582
- padding: var(--spacing-lg) !important;
6583
- }
6584
6566
  .p-md {
6585
6567
  padding: var(--spacing-md);
6586
6568
  }
@@ -6590,9 +6572,6 @@
6590
6572
  .p-xs {
6591
6573
  padding: var(--spacing-xs);
6592
6574
  }
6593
- .p-xs\! {
6594
- padding: var(--spacing-xs) !important;
6595
- }
6596
6575
  .caps-stat-compact {
6597
6576
  & > .stat {
6598
6577
  padding-block: var(--spacing-xs);
@@ -6608,9 +6587,6 @@
6608
6587
  .px-3 {
6609
6588
  padding-inline: calc(var(--spacing) * 3);
6610
6589
  }
6611
- .px-lg {
6612
- padding-inline: var(--spacing-lg);
6613
- }
6614
6590
  .px-md {
6615
6591
  padding-inline: var(--spacing-md);
6616
6592
  }
@@ -6644,15 +6620,6 @@
6644
6620
  .py-xs {
6645
6621
  padding-block: var(--spacing-xs);
6646
6622
  }
6647
- .ps-md {
6648
- padding-inline-start: var(--spacing-md);
6649
- }
6650
- .ps-xs {
6651
- padding-inline-start: var(--spacing-xs);
6652
- }
6653
- .pe-sm {
6654
- padding-inline-end: var(--spacing-sm);
6655
- }
6656
6623
  .pt-1\.5 {
6657
6624
  padding-top: calc(var(--spacing) * 1.5);
6658
6625
  }
@@ -7056,9 +7023,6 @@
7056
7023
  transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
7057
7024
  transition-duration: var(--tw-duration, var(--default-transition-duration));
7058
7025
  }
7059
- .transition-none {
7060
- transition-property: none;
7061
- }
7062
7026
  .ease-in {
7063
7027
  --tw-ease: var(--ease-in);
7064
7028
  transition-timing-function: var(--ease-in);
@@ -7324,20 +7288,6 @@
7324
7288
  }
7325
7289
  }
7326
7290
  }
7327
- .hover\:gap-md {
7328
- &:hover {
7329
- @media (hover: hover) {
7330
- gap: var(--spacing-md);
7331
- }
7332
- }
7333
- }
7334
- .hover\:gap-sm {
7335
- &:hover {
7336
- @media (hover: hover) {
7337
- gap: var(--spacing-sm);
7338
- }
7339
- }
7340
- }
7341
7291
  .hover\:bg-base-200 {
7342
7292
  &:hover {
7343
7293
  @media (hover: hover) {
@@ -7373,11 +7323,6 @@
7373
7323
  }
7374
7324
  }
7375
7325
  }
7376
- .focus\:gap-xs {
7377
- &:focus {
7378
- gap: var(--spacing-xs);
7379
- }
7380
- }
7381
7326
  .focus\:\!outline-none {
7382
7327
  &:focus {
7383
7328
  --tw-outline-style: none !important;
@@ -7538,16 +7483,6 @@
7538
7483
  }
7539
7484
  }
7540
7485
  }
7541
- .data-\[open\]\:bg-base-200 {
7542
- &[data-open] {
7543
- background-color: var(--color-base-200);
7544
- }
7545
- }
7546
- .data-\[open\]\:bg-base-300 {
7547
- &[data-open] {
7548
- background-color: var(--color-base-300);
7549
- }
7550
- }
7551
7486
  .motion-reduce\:transition-none {
7552
7487
  @media (prefers-reduced-motion: reduce) {
7553
7488
  transition-property: none;
@@ -7568,16 +7503,6 @@
7568
7503
  grid-template-columns: repeat(3, minmax(0, 1fr));
7569
7504
  }
7570
7505
  }
7571
- .md\:px-lg {
7572
- @media (width >= 48rem) {
7573
- padding-inline: var(--spacing-lg);
7574
- }
7575
- }
7576
- .md\:px-xs {
7577
- @media (width >= 48rem) {
7578
- padding-inline: var(--spacing-xs);
7579
- }
7580
- }
7581
7506
  .md\:btn-xs {
7582
7507
  @media (width >= 48rem) {
7583
7508
  @layer daisyui.l1.l2 {
@@ -9262,37 +9187,6 @@
9262
9187
  font-family: var(--font-mono);
9263
9188
  line-height: 1;
9264
9189
  }
9265
- .caps-led {
9266
- display: inline-flex;
9267
- align-items: center;
9268
- gap: var(--spacing-xs);
9269
- color: var(--marginalia-ink);
9270
- font-size: 0.75rem;
9271
- }
9272
- .caps-led-bead {
9273
- width: 0.625rem;
9274
- height: 0.625rem;
9275
- flex: none;
9276
- border: var(--card-rule-width) solid var(--card-rule);
9277
- border-radius: var(--radius-selector);
9278
- background: var(--caps-led-color);
9279
- box-shadow: 0 0 0 var(--card-rule-width) var(--caps-led-color);
9280
- @supports (color: color-mix(in lab, red, red)) {
9281
- box-shadow: 0 0 0 var(--card-rule-width) color-mix(in srgb, var(--caps-led-color) 36%, transparent);
9282
- }
9283
- }
9284
- .caps-led[data-tone="spore"] {
9285
- --caps-led-color: var(--growth-spore);
9286
- }
9287
- .caps-led[data-tone="mycelium"] {
9288
- --caps-led-color: var(--growth-mycelium);
9289
- }
9290
- .caps-led[data-tone="fruiting"] {
9291
- --caps-led-color: var(--growth-fruiting);
9292
- }
9293
- .caps-led[data-active="false"] {
9294
- --caps-led-color: var(--marginalia-ink);
9295
- }
9296
9190
  .caps-cartridge-label {
9297
9191
  display: grid;
9298
9192
  justify-items: center;
@@ -14185,6 +14079,88 @@
14185
14079
  min-height: 44px;
14186
14080
  }
14187
14081
  }
14082
+ .caps-window-header:has(> .caps-window-crumbs) {
14083
+ min-height: 40px;
14084
+ }
14085
+ .caps-window-crumbs {
14086
+ display: flex;
14087
+ align-items: center;
14088
+ min-width: 0;
14089
+ flex-shrink: 1;
14090
+ font-family: var(--caps-label-font, inherit);
14091
+ font-size: var(--text-sm);
14092
+ font-weight: 500;
14093
+ }
14094
+ .caps-window-crumb-item[data-caps-crumb-position="current"] {
14095
+ font-weight: 600;
14096
+ }
14097
+ .caps-window-crumb-item {
14098
+ display: flex;
14099
+ align-items: center;
14100
+ min-width: 0;
14101
+ }
14102
+ .caps-window-crumb-separator {
14103
+ padding-inline: var(--spacing-2xs);
14104
+ color: currentColor;
14105
+ @supports (color: color-mix(in lab, red, red)) {
14106
+ color: color-mix(in srgb, currentColor 50%, transparent);
14107
+ }
14108
+ }
14109
+ .caps-window-crumb {
14110
+ min-width: 0;
14111
+ font-size: inherit;
14112
+ font-weight: inherit;
14113
+ gap: var(--spacing-2xs);
14114
+ }
14115
+ .caps-window-crumb-label {
14116
+ overflow: hidden;
14117
+ text-overflow: ellipsis;
14118
+ white-space: nowrap;
14119
+ }
14120
+ .caps-window-crumb-item[data-caps-crumb-position="current"] .caps-window-crumb {
14121
+ padding-inline: var(--spacing-xs);
14122
+ }
14123
+ .caps-window-crumbs:has(+ .caps-window-title) {
14124
+ max-width: 70%;
14125
+ }
14126
+ .caps-window-statuses {
14127
+ display: flex;
14128
+ align-items: center;
14129
+ gap: var(--spacing-sm);
14130
+ flex-shrink: 0;
14131
+ padding-inline-end: var(--spacing-sm);
14132
+ font-size: var(--text-sm);
14133
+ color: currentColor;
14134
+ @supports (color: color-mix(in lab, red, red)) {
14135
+ color: color-mix(in srgb, currentColor 65%, transparent);
14136
+ }
14137
+ }
14138
+ .caps-window-commands {
14139
+ display: flex;
14140
+ align-items: center;
14141
+ gap: var(--spacing-2xs);
14142
+ flex-shrink: 0;
14143
+ }
14144
+ @media (max-width: 640px) {
14145
+ .caps-window-statuses {
14146
+ gap: var(--spacing-2xs);
14147
+ padding-inline-end: var(--spacing-2xs);
14148
+ }
14149
+ .caps-window-status > span {
14150
+ position: absolute;
14151
+ width: 1px;
14152
+ height: 1px;
14153
+ overflow: hidden;
14154
+ clip-path: inset(50%);
14155
+ white-space: nowrap;
14156
+ }
14157
+ .caps-window-crumb-item[data-caps-crumb-position="ancestor"] {
14158
+ display: none;
14159
+ }
14160
+ .caps-window-crumb-item[data-caps-crumb-position="parent"] .caps-window-crumb-separator {
14161
+ display: none;
14162
+ }
14163
+ }
14188
14164
  .caps-window-body {
14189
14165
  flex: 1;
14190
14166
  min-height: 0;
@@ -15222,9 +15198,6 @@
15222
15198
  font-size: var(--text-xs);
15223
15199
  white-space: nowrap;
15224
15200
  }
15225
- .caps-status-compact .caps-status-bead {
15226
- transform: scale(0.6);
15227
- }
15228
15201
  .caps-action-row-trailing {
15229
15202
  display: flex;
15230
15203
  width: 6rem;
package/docs/api/index.md CHANGED
@@ -42,7 +42,7 @@ type AvatarState = Readonly<
42
42
  >;
43
43
  ```
44
44
 
45
- The current activity and expression supplied to a composer avatar.
45
+ The current activity and expression supplied to a live avatar.
46
46
 
47
47
  ---
48
48
 
@@ -802,7 +802,7 @@ Stable row grammar rendered by Parakeet's timeline.
802
802
  ```ts
803
803
  const AgentHead: NamedExoticComponent<{
804
804
  activity?: AgentHeadActivity;
805
- avatar?: {
805
+ avatar: {
806
806
  v: 1;
807
807
  character: "mushroom" | "cat" | "burger";
808
808
  appearance: z.ZodRecord<
@@ -819,22 +819,17 @@ const AgentHead: NamedExoticComponent<{
819
819
  | "blueprint"
820
820
  | "crt"
821
821
  | "watercolor";
822
- } | null;
822
+ };
823
823
  className?: string;
824
824
  name: string;
825
- seed: string;
826
825
  size?: number;
827
826
  }>;
828
827
  ```
829
828
 
830
- An Agent's head at avatar size. Renders the Agent's own avatar when given one;
831
- otherwise the pre-avatar seeded mushroom cap, byte-identical to before avatars
832
- existed. The letter shows until a frame is ready. It holds still unless
833
- `thinking` or `speaking`: an idle head whose render style is "plain" and already
834
- has a cached still frame for its look never mounts a live canvas, so a long rail
835
- is not a wall of animation. A render style other than "plain" is a CSS filter
836
- over the live canvas, not part of the captured pixels, so a styled head always
837
- stays live.
829
+ An Agent's head at avatar size, drawn from the Agent's own avatar (a property of
830
+ the Agent record, never of a conversation). The letter shows until a frame is
831
+ ready. The live canvas stays mounted and is paused unless `thinking` or
832
+ `speaking`, so an idle head shows its real look without animating.
838
833
 
839
834
  ---
840
835
 
@@ -866,9 +861,10 @@ const ShiitakeAvatar: NamedExoticComponent<{
866
861
  }>;
867
862
  ```
868
863
 
869
- Shiitake's sprite head, drawn in the activity and expression it is given. With
870
- no `avatar`, this is the product mascot, exactly as before avatars existed: no
871
- seeded colour, the character's own defaults.
864
+ A sprite head drawn in the live activity and expression it is given. With an
865
+ `avatar` it draws that look (a member's own, in their settings); with none it is
866
+ Shiitake, the product mascot, in the character's own defaults. Conversations
867
+ have no avatar: an Agent's head is `AgentHead`, drawn from the Agent record.
872
868
 
873
869
  ---
874
870
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/parakeet",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "private": false,
6
6
  "description": "Standalone React composer for a typed Shiitake Session",
@@ -44,13 +44,13 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@fungi.computer/cairn": "0.0.1",
47
- "@fungi.computer/caps": "0.0.2",
48
- "@fungi.computer/knapsack": "0.0.2",
49
- "@fungi.computer/shiitake": "0.1.0",
50
- "@fungi.computer/sigils": "0.0.1",
51
- "@fungi.computer/sprite": "0.0.1",
52
- "@fungi.computer/sprite-zdog": "0.0.1",
53
- "@fungi.computer/whistle": "0.0.1",
47
+ "@fungi.computer/caps": "0.1.0",
48
+ "@fungi.computer/knapsack": "0.1.0",
49
+ "@fungi.computer/shiitake": "0.2.0",
50
+ "@fungi.computer/sigils": "0.1.0",
51
+ "@fungi.computer/sprite": "0.1.0",
52
+ "@fungi.computer/sprite-zdog": "0.1.0",
53
+ "@fungi.computer/whistle": "0.0.2",
54
54
  "@tanstack/markdown": "0.0.12",
55
55
  "@tanstack/react-virtual": "3.14.10",
56
56
  "diff": "8.0.4",
@@ -59,7 +59,7 @@
59
59
  "lucide-react": "0.460.0",
60
60
  "strip-ansi": "7.2.0",
61
61
  "zod": "4.4.3",
62
- "@fungi.computer/gills": "0.0.1"
62
+ "@fungi.computer/gills": "0.1.0"
63
63
  },
64
64
  "dependenciesMeta": {
65
65
  "@fungi.computer/caps": {
package/src/styles.css CHANGED
@@ -5,7 +5,11 @@
5
5
  */
6
6
  @import "@fungi.computer/caps/tailwind.css";
7
7
 
8
+ /* In the workspace Tailwind scans the component source; published, this
9
+ * entry ships without it, and the compiled components in dist carry the same
10
+ * class names. */
8
11
  @source "./";
12
+ @source "../dist/**/*.js";
9
13
 
10
14
  @plugin "@tailwindcss/typography";
11
15
 
@@ -1,16 +0,0 @@
1
- import type { AvatarSpriteProps } from "./avatar-presentation.js";
2
- /** The still-frame cache key for one presentation at one pixel size. */
3
- export declare function stillFrameKey(presentation: AvatarSpriteProps, size: number, devicePixelRatio: number): string;
4
- /** The cached still frame for `key`, refreshing its recency, or `undefined`. */
5
- export declare function getStillFrame(key: string): string | undefined;
6
- /** Cache `value` (a data URL) for `key`, evicting the oldest entry past 128. */
7
- export declare function setStillFrame(key: string, value: string): void;
8
- /**
9
- * Capture the idle frame of a just-rendered Sprite host as a still image, or
10
- * `undefined` when the host has no live canvas yet (not ready) or the
11
- * canvas export is denied because the canvas is tainted. Other export failures
12
- * propagate with their cause.
13
- */
14
- export declare function captureStillFrame(host: HTMLElement | null): string | undefined;
15
- /** Test-only: clears every cached still frame so tests do not leak between runs. */
16
- export declare function clearStillFrameCache(): void;
@@ -1,63 +0,0 @@
1
- const MAX_ENTRIES = 128;
2
- /**
3
- * Idle heads shown over and over (an Agent rail) do not need their own live
4
- * canvas: once one instance of a descriptor has rendered, every other idle
5
- * instance can show the same still frame. Keyed by the resolved presentation
6
- * plus the pixel size it was drawn at, so a resize or a different size
7
- * elsewhere never serves the wrong frame. An LRU of about 128 entries; the
8
- * oldest is evicted once that is exceeded.
9
- */
10
- const cache = new Map();
11
- /** The still-frame cache key for one presentation at one pixel size. */
12
- export function stillFrameKey(presentation, size, devicePixelRatio) {
13
- return JSON.stringify([
14
- presentation.src,
15
- presentation.appearance,
16
- presentation.expression,
17
- presentation.renderStyle,
18
- size,
19
- devicePixelRatio,
20
- ]);
21
- }
22
- /** The cached still frame for `key`, refreshing its recency, or `undefined`. */
23
- export function getStillFrame(key) {
24
- const value = cache.get(key);
25
- if (value === undefined)
26
- return undefined;
27
- cache.delete(key);
28
- cache.set(key, value);
29
- return value;
30
- }
31
- /** Cache `value` (a data URL) for `key`, evicting the oldest entry past 128. */
32
- export function setStillFrame(key, value) {
33
- cache.delete(key);
34
- cache.set(key, value);
35
- if (cache.size <= MAX_ENTRIES)
36
- return;
37
- const oldestKey = cache.keys().next().value;
38
- if (oldestKey !== undefined)
39
- cache.delete(oldestKey);
40
- }
41
- /**
42
- * Capture the idle frame of a just-rendered Sprite host as a still image, or
43
- * `undefined` when the host has no live canvas yet (not ready) or the
44
- * canvas export is denied because the canvas is tainted. Other export failures
45
- * propagate with their cause.
46
- */
47
- export function captureStillFrame(host) {
48
- const canvas = host?.querySelector("canvas");
49
- if (!canvas)
50
- return undefined;
51
- try {
52
- return canvas.toDataURL("image/png");
53
- }
54
- catch (cause) {
55
- if (cause instanceof DOMException && cause.name === "SecurityError")
56
- return undefined;
57
- throw new Error("Could not capture an avatar still frame", { cause });
58
- }
59
- }
60
- /** Test-only: clears every cached still frame so tests do not leak between runs. */
61
- export function clearStillFrameCache() {
62
- cache.clear();
63
- }