@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,187 @@
1
- # Temporary Holding Version
1
+ # Parakeet (`@fungi.computer/parakeet`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Parakeet renders an Agent conversation from a typed Shiitake Session. Browser
4
+ hosts use its React view; terminal hosts use its headless hooks to render the
5
+ same history, composer and status through their own UI.
6
+
7
+ Website hosts mount the same `Parakeet` component inside an isolated iframe. A
8
+ visitor-session adapter supplies the visitor's scoped Session read, watch and
9
+ prompt commands; credentials and admission remain with the host.
10
+ `markdownPolicy="visitor"` renders Markdown links without loading images or
11
+ unsafe destinations. A prompt recorded for a human returns `status: "recorded"`,
12
+ not a fabricated Agent-work acceptance. Closing the view stops observation, not
13
+ the Agent's work. Pass `visitor={{ agentName: "Front Desk" }}` to hide model,
14
+ usage and command controls. Slash-prefixed text remains a normal visitor
15
+ message; the visitor mode does not register or execute slash commands.
16
+
17
+ ## Set up
18
+
19
+ Install `@fungi.computer/parakeet` from its package tarball. It requires a
20
+ compatible React and React DOM pair, TanStack Query and an authorized Session
21
+ supplied by the host. The manifest supports React 18.2 or 19.2 and later within
22
+ those major versions. Build the dependency closure from the repository root:
23
+
24
+ ```sh
25
+ pnpm turbo:run run build --filter=@fungi.computer/parakeet
26
+ ```
27
+
28
+ Browser hosts import `@fungi.computer/parakeet/styles.css` once. It includes the
29
+ Caps/Stipe foundation and conversation utilities. Do not also load
30
+ `@fungi.computer/caps/styles.css` in that document. Import Caps' `fonts.css`
31
+ separately when using its bundled fonts, with font URLs handled by your bundler.
32
+
33
+ A host compiling its own Tailwind utilities uses the `/tailwind.css` source
34
+ instead and supplies its own `@source`. The built stylesheet cannot scan your
35
+ host's additional classes.
36
+
37
+ ## Quick start in a browser
38
+
39
+ Illustrative: your host supplies the authorized `ParakeetSession` and a browser
40
+ bundler that handles CSS. This component owns its query cache and clears it on
41
+ unmount. It does not create an Agent, sign in or grant tools.
42
+
43
+ ```tsx
44
+ import { useEffect, useState } from "react";
45
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
46
+ import { Parakeet, type ParakeetSession } from "@fungi.computer/parakeet";
47
+ import "@fungi.computer/parakeet/styles.css";
48
+
49
+ export function Conversation({ session }: { session: ParakeetSession }) {
50
+ const [queryClient] = useState(() => new QueryClient());
51
+ useEffect(() => () => queryClient.clear(), [queryClient]);
52
+ return (
53
+ <QueryClientProvider client={queryClient}>
54
+ <Parakeet session={session} />
55
+ </QueryClientProvider>
56
+ );
57
+ }
58
+ ```
59
+
60
+ Parakeet never reads a message's host metadata itself. To show who wrote a line,
61
+ pass `authorOf`: your reading of that metadata into a `ParakeetAuthor`
62
+ (`{ id, name }`). A Fungi app passes `messageAuthor` from
63
+ `@fungi.computer/hub-client/message-meta`; without it, lines show no author.
64
+
65
+ The view presents the Session's observed history, live work, composer and
66
+ errors. Screen readers receive the same Session status as the composer footer
67
+ through a hidden polite live region; streamed transcript tokens are outside that
68
+ region. The Session transport stays owned by the caller. Releasing observation
69
+ does not cancel accepted work or close a borrowed Agent. When replacing a
70
+ Session, provide its new handle rather than copying a snapshot into another
71
+ writable store.
72
+
73
+ The [standalone composer tests](test/standalone-composer.test.tsx) exercise the
74
+ provider, query cache, observations and prompt admission with typed fixtures.
75
+ The browser contract uses the real component with a fixture Session. A fake
76
+ Session checks the component, not hosted authentication or a model provider.
77
+
78
+ ## Core concepts
79
+
80
+ | Surface | What it owns |
81
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------- |
82
+ | `Parakeet` | The browser conversation, including provider and view. |
83
+ | `ParakeetProvider` / `ParakeetView` | Separate composition when the host needs custom controls in the same conversation scope. |
84
+ | `useTimeline()` | Durable history and the replaceable live tail, bounded history paging and model/usage presentation. |
85
+ | `useComposer()` | Draft, submission, pending work, cancellation and retry controls. |
86
+ | `useStatus()` | Observed work and connection state, including goal controls. |
87
+ | `useMarkdown(content)` | Parsed chat Markdown for custom and terminal renderers. |
88
+
89
+ Both composition forms require the host's `QueryClientProvider`. `Parakeet`
90
+ takes the `session` directly. With separate composition, pass `session` to
91
+ `ParakeetProvider` and put `ParakeetView` and custom hook consumers beneath it.
92
+ Pass `commands` and presentation props to the view, not the provider.
93
+
94
+ `Parakeet` and `ParakeetView` share these optional presentation props:
95
+
96
+ | Prop | Contract |
97
+ | --------------------------- | ----------------------------------------------------------------------- |
98
+ | `composerActionVariant` | `"primary"` or `"outline"`. |
99
+ | `renderToolActions` | Renders host actions for a completed `ToolItem`. |
100
+ | `renderAssistantAttachment` | Receives the original settled reply text. |
101
+ | `formatAssistantText` | Changes displayed reply text without changing the attachment input. |
102
+ | `onLink` | Receives a transcript URL; return `true` to prevent browser navigation. |
103
+ | `empty` | Replaces the empty conversation content. |
104
+
105
+ Import renderer-neutral hooks from `/headless` in the terminal. That entry
106
+ avoids loading the DOM presentation, Caps and Sprite closure. The root exports
107
+ those same hooks for browser callers. They share the Session's TanStack Query
108
+ cache. Derived atoms select observed values rather than duplicating Session
109
+ truth. History pages share the host's cache by Session address and survive
110
+ provider remounts until that cache expires. Paging retains four 100-message
111
+ pages, with bounded return cursors for the Session's before-only read API.
112
+ Jumping to latest cancels the outstanding history fetch and clears that shared
113
+ history window; the live observation remains separate.
114
+
115
+ Tool calls are grouped in the shared projection. Browser and terminal renderers
116
+ receive the same typed summaries, error-first sections and edit diffs.
117
+ Disclosure state belongs to the view. A goal row displays the committed goal;
118
+ pausing it leaves current accepted work running. Idle conversation state does
119
+ not mean the goal is complete.
120
+
121
+ ## Commands and errors
122
+
123
+ A host may supply its Whistle runtime through `commands`. Parakeet builds the
124
+ slash registry from that host, its built-ins and advertised extension catalogs.
125
+ It owns parsing, completion, stable request identities and interaction state.
126
+ The browser and terminal render the same questions, choices and notices.
127
+ `/model` and `/thinking` use that interaction path.
128
+
129
+ Account commands are provider-neutral. The host's account owner and policy
130
+ decide which login methods are available. The presentation layer does not gain a
131
+ credential store or make an advertised command authorized.
132
+
133
+ The composer reconciles optimistic prompts with owner facts by request identity.
134
+ Retrying a failed admission preserves its request identity. Retrying failed or
135
+ interrupted durable work submits a new request identity. Failed observation
136
+ shows connection failure; reconnecting does not prove pending work failed or
137
+ resubmit it automatically. Typed command errors remain visible and retryable.
138
+ The host remains responsible for the Session transport, current permissions and
139
+ operator diagnostics.
140
+
141
+ ## API and examples
142
+
143
+ Use the [generated API reference](docs/api/README.md) and
144
+ [exports](package.json). The root owns browser components, `/headless` owns
145
+ shared hooks, `/slash` owns command grammar, and `/agent-head` owns Agent
146
+ appearance contracts.
147
+
148
+ [Transcript stories](workbench/transcript.stories.tsx) show settled, streaming
149
+ and failed replies. [Tool group stories](workbench/tool-groups.stories.tsx) show
150
+ shared projections. [Slash parity tests](test/slash-parity.test.tsx) check the
151
+ common interaction contract.
152
+ [Headless surface tests](test/headless-surface.test.ts) check that non-browser
153
+ imports stay independent of the DOM view.
154
+
155
+ ## Contribute and license
156
+
157
+ Add a fixture case to the nearest conversation, command or rendering test. From
158
+ the repository root:
159
+
160
+ ```sh
161
+ pnpm --dir packages/parakeet run test
162
+ pnpm --dir packages/parakeet run browser:contract
163
+ pnpm --dir packages/parakeet run docs
164
+ ```
165
+
166
+ The browser contract needs the repository's configured browser tooling. Unit
167
+ checks do not replace that proof. Parakeet is MIT licensed; see
168
+ [LICENSE](LICENSE).
169
+
170
+ ## TypeScript consumer dependencies
171
+
172
+ Install `@modelcontextprotocol/sdk@1.25.2` alongside this package when compiling
173
+ its complete public declarations. The shared provider SDK imports those types
174
+ through an optional peer; the packed consumer supplies it explicitly. This does
175
+ not require a provider account or make a provider call.
176
+
177
+ ## Packed consumer readiness
178
+
179
+ Run `pnpm run build`, `pnpm run pack` and `pnpm run stranger` in this package.
180
+ The maintained stranger proof installs only package tarballs into a fresh npm
181
+ consumer, checks the exact shipped files, runs the public runtime fixture and
182
+ compiles every JavaScript entrypoint with strict NodeNext and no library-check
183
+ skip. This qualification does not publish npm releases or push a repository
184
+ split.
185
+
186
+ See [LICENSE](LICENSE) for the license and the package manifest for its public
187
+ repository destination.
@@ -0,0 +1,102 @@
1
+ import { type WhistleContribution, type WhistleContributionLease, type WhistleInteraction, type WhistleRuntime } from "@fungi.computer/whistle";
2
+ /** How a person proves they may use a provider: an API key or their subscription sign-in. */
3
+ export type LoginMethodKind = "api_key" | "subscription";
4
+ /** One sign-in method a provider offers, labelled by its owner. */
5
+ export type LoginMethod = Readonly<{
6
+ /** API key or subscription sign-in. */
7
+ kind: LoginMethodKind;
8
+ /** Provider-supplied label, e.g. "Anthropic API key". */
9
+ label: string;
10
+ }>;
11
+ /** One model provider in the registry. Names come from data, never code. */
12
+ export type LoginProvider = Readonly<{
13
+ /** Stable provider identity, e.g. a Pi provider id. */
14
+ id: string;
15
+ /** Provider-supplied display name. */
16
+ name: string;
17
+ /** Sign-in methods this provider offers. */
18
+ methods: readonly LoginMethod[];
19
+ }>;
20
+ /** Where the credential will be used: the person's machine or Fungi's servers. */
21
+ export type LoginHost = "local" | "hosted";
22
+ /** Whose credential it is: one person, or a Team. */
23
+ export type LoginScope = "user" | "team";
24
+ /**
25
+ * The credential policy owner's decision, the same shape as
26
+ * `@fungi.computer/veil/model-credentials` `loginAllowed`. Login offers only
27
+ * what it permits.
28
+ */
29
+ export type LoginPolicy = Readonly<{
30
+ /** Whether this method may be offered for this provider, host and scope. */
31
+ loginAllowed: (request: Readonly<{
32
+ providerId: string;
33
+ method: LoginMethodKind;
34
+ host: LoginHost;
35
+ scope: LoginScope;
36
+ }>) => boolean;
37
+ }>;
38
+ /** The providers and methods one surface may offer, in display order. */
39
+ export declare function loginRegistry(providers: readonly LoginProvider[], options: Readonly<{
40
+ policy: LoginPolicy;
41
+ host: LoginHost;
42
+ scope: LoginScope;
43
+ }>): readonly LoginProvider[];
44
+ /** A configured provider reported by the credential owner, without secrets. */
45
+ export type ProviderAccess = Readonly<{
46
+ /** Stable registry identity. */
47
+ providerId: string;
48
+ /** Provider-owned display label. */
49
+ providerName: string;
50
+ /** The configured credential kind. */
51
+ type: "oauth" | "api_key";
52
+ }>;
53
+ /** The credential owner behind the account commands. */
54
+ export type AccountOwner = Readonly<{
55
+ /** Host-owned popular order, shared by account and model choices. */
56
+ popularProviderIds: readonly string[];
57
+ /** Configured provider access from the credential owner's parsed status. */
58
+ access: (signal: AbortSignal) => Promise<readonly ProviderAccess[]>;
59
+ /** Every provider the owner can sign in to, before policy. */
60
+ providers: (signal: AbortSignal) => Promise<readonly LoginProvider[]>;
61
+ /** Run the provider's own sign-in; it may prompt and notify. */
62
+ login: (request: Readonly<{
63
+ providerId: string;
64
+ method: LoginMethodKind;
65
+ /** Provider-specific choice, e.g. a device-code flow. */
66
+ option?: string;
67
+ }>, interaction: WhistleInteraction) => Promise<void>;
68
+ /** Remove the provider's stored credential. */
69
+ logout: (providerId: string, interaction: WhistleInteraction) => Promise<void>;
70
+ /** A short human summary of configured providers. */
71
+ status: (signal: AbortSignal) => Promise<string>;
72
+ }>;
73
+ /** Contribution identity and the policy context for account commands. */
74
+ export type AccountCommandOptions = Readonly<{
75
+ /** Host source that owns the contribution lease. */
76
+ sourceId: string;
77
+ /** Namespace for the account commands. */
78
+ namespace: string;
79
+ /** Where credentials signed in here will be used. */
80
+ host: LoginHost;
81
+ /** Whose credentials they are. */
82
+ scope: LoginScope;
83
+ /** The credential policy owner's decision. */
84
+ policy: LoginPolicy;
85
+ }>;
86
+ /** One account owner and its credential policy context for an interactive model picker. */
87
+ export type ModelAccountContext = Pick<AccountCommandOptions, "policy" | "host" | "scope"> & Readonly<{
88
+ /** Existing credential owner, also used by the account commands. */
89
+ owner: AccountOwner;
90
+ }>;
91
+ /** Run the existing provider-owned sign-in after the caller applies credential policy. */
92
+ export declare function signInProvider(owner: AccountOwner, provider: LoginProvider, interaction: WhistleInteraction, request?: Readonly<{
93
+ method?: LoginMethodKind | undefined;
94
+ option?: string | undefined;
95
+ }>): Promise<void>;
96
+ /**
97
+ * Provider-agnostic account commands. Every question and status update is a
98
+ * Whistle interaction, so any client that renders interactions renders login.
99
+ */
100
+ export declare function accountCommandDefinitions(owner: AccountOwner, options: AccountCommandOptions): WhistleContribution["commands"];
101
+ /** Contribute the account commands as one lease. */
102
+ export declare function contributeAccountCommands(runtime: Pick<WhistleRuntime, "contribute">, owner: AccountOwner, options: AccountCommandOptions): WhistleContributionLease;
@@ -0,0 +1,239 @@
1
+ import { WhistleActionError, } from "@fungi.computer/whistle";
2
+ import { z } from "zod";
3
+ import { providerChoices } from "./provider-choices.js";
4
+ /** The providers and methods one surface may offer, in display order. */
5
+ export function loginRegistry(providers, options) {
6
+ return providers
7
+ .map((provider) => ({
8
+ ...provider,
9
+ methods: provider.methods.filter((method) => options.policy.loginAllowed({
10
+ providerId: provider.id,
11
+ method: method.kind,
12
+ host: options.host,
13
+ scope: options.scope,
14
+ })),
15
+ }))
16
+ .filter((provider) => provider.methods.length > 0)
17
+ .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
18
+ }
19
+ const loginArguments = z.strictObject({
20
+ providerId: z.string().min(1).max(200).optional(),
21
+ method: z.enum(["api_key", "subscription"]).optional(),
22
+ option: z.string().min(1).max(200).optional(),
23
+ });
24
+ const loginArgumentsSchema = z
25
+ .record(z.string(), z.json())
26
+ .parse(structuredClone(z.toJSONSchema(loginArguments)));
27
+ const logoutArguments = z.strictObject({
28
+ providerId: z.string().min(1).max(200).optional(),
29
+ });
30
+ const logoutArgumentsSchema = z
31
+ .record(z.string(), z.json())
32
+ .parse(structuredClone(z.toJSONSchema(logoutArguments)));
33
+ function methodDescription(method, provider) {
34
+ return method.kind === "api_key"
35
+ ? "Paste an API key you created"
36
+ : `Sign in with your ${provider.name} account`;
37
+ }
38
+ function requireInteraction(input) {
39
+ if (input.interaction === undefined)
40
+ throw new WhistleActionError("interaction_required", "Signing in needs an interactive client.");
41
+ return input.interaction;
42
+ }
43
+ async function chooseProvider(providers, requested, interaction, message, choices) {
44
+ if (requested !== undefined) {
45
+ const provider = providers.find((candidate) => candidate.id === requested);
46
+ if (provider === undefined)
47
+ throw new WhistleActionError("unknown_provider", `${requested} is not available here.`);
48
+ return provider;
49
+ }
50
+ if (providers.length === 0)
51
+ throw new WhistleActionError("no_providers", "No model providers are available here.");
52
+ const id = await interaction.prompt({
53
+ type: "choice",
54
+ message,
55
+ collapsedGroup: "More providers",
56
+ options: choices,
57
+ });
58
+ const provider = providers.find((candidate) => candidate.id === id);
59
+ if (provider === undefined)
60
+ throw new WhistleActionError("unknown_provider", `${id} is unavailable.`);
61
+ return provider;
62
+ }
63
+ async function chooseMethod(provider, requested, interaction) {
64
+ if (requested !== undefined) {
65
+ const method = provider.methods.find((one) => one.kind === requested);
66
+ if (method === undefined)
67
+ throw new WhistleActionError("method_unavailable", `${provider.name} does not offer that sign-in method here.`);
68
+ return method;
69
+ }
70
+ const [only, ...rest] = provider.methods;
71
+ if (only !== undefined && rest.length === 0)
72
+ return only;
73
+ const kind = await interaction.prompt({
74
+ type: "choice",
75
+ message: `How do you want to sign in to ${provider.name}?`,
76
+ options: provider.methods.map((method) => ({
77
+ value: method.kind,
78
+ label: method.label,
79
+ description: methodDescription(method, provider),
80
+ })),
81
+ });
82
+ const method = provider.methods.find((one) => one.kind === kind);
83
+ if (method === undefined)
84
+ throw new WhistleActionError("method_unavailable", "Choose a listed method.");
85
+ return method;
86
+ }
87
+ /** Run the existing provider-owned sign-in after the caller applies credential policy. */
88
+ export async function signInProvider(owner, provider, interaction, request = {}) {
89
+ const method = await chooseMethod(provider, request.method, interaction);
90
+ interaction.notify({
91
+ type: "progress",
92
+ message: `Signing in to ${provider.name}…`,
93
+ });
94
+ await owner.login(request.option === undefined
95
+ ? { providerId: provider.id, method: method.kind }
96
+ : {
97
+ providerId: provider.id,
98
+ method: method.kind,
99
+ option: request.option,
100
+ }, interaction);
101
+ interaction.notify({
102
+ type: "success",
103
+ message: `Signed in to ${provider.name}.`,
104
+ });
105
+ }
106
+ async function signOutProvider(owner, provider, interaction) {
107
+ const confirmed = await interaction.prompt({
108
+ type: "confirmation",
109
+ message: `Sign out of ${provider.name}? Local Sessions stop using this account.`,
110
+ confirmLabel: "Sign out",
111
+ cancelLabel: "Keep signed in",
112
+ });
113
+ if (!confirmed)
114
+ return;
115
+ await owner.logout(provider.id, interaction);
116
+ interaction.notify({
117
+ type: "success",
118
+ message: `Signed out of ${provider.name}.`,
119
+ });
120
+ }
121
+ /** Cancelling a sign-in is an ordinary ending, not a failure. */
122
+ async function unlessCancelled(interaction, work) {
123
+ try {
124
+ await work();
125
+ return true;
126
+ }
127
+ catch (cause) {
128
+ if (interaction.signal.aborted)
129
+ return false;
130
+ throw cause;
131
+ }
132
+ }
133
+ /**
134
+ * Provider-agnostic account commands. Every question and status update is a
135
+ * Whistle interaction, so any client that renders interactions renders login.
136
+ */
137
+ export function accountCommandDefinitions(owner, options) {
138
+ const { policy } = options;
139
+ return [
140
+ {
141
+ id: "account",
142
+ title: "Model accounts",
143
+ category: "Settings",
144
+ projections: {
145
+ help: { order: 10 },
146
+ slash: { aliases: ["account"], order: 10 },
147
+ },
148
+ handler: async (input) => {
149
+ const summary = await owner.status(input.signal ?? new AbortController().signal);
150
+ input.interaction?.notify({ type: "info", message: summary });
151
+ return summary;
152
+ },
153
+ },
154
+ {
155
+ id: "login",
156
+ title: "Sign in to a model provider",
157
+ category: "Settings",
158
+ projections: {
159
+ help: { order: 11 },
160
+ menu: { order: 11 },
161
+ palette: { order: 11 },
162
+ slash: { aliases: ["login"], order: 11 },
163
+ },
164
+ action: { inputSchema: loginArgumentsSchema },
165
+ handler: async (input) => {
166
+ const interaction = requireInteraction(input);
167
+ const request = loginArguments.parse(input.arguments ?? {});
168
+ await unlessCancelled(interaction, async () => {
169
+ const registry = loginRegistry(await owner.providers(interaction.signal), { policy, host: options.host, scope: options.scope });
170
+ const access = request.providerId === undefined
171
+ ? await owner.access(interaction.signal)
172
+ : [];
173
+ const provider = await chooseProvider(registry, request.providerId, interaction, "Choose a model provider", providerChoices(registry, access, owner.popularProviderIds, "login"));
174
+ if (request.providerId === undefined &&
175
+ access.some((item) => item.providerId === provider.id)) {
176
+ const action = await interaction.prompt({
177
+ type: "choice",
178
+ message: provider.name,
179
+ options: [
180
+ { value: "switch", label: "Switch account" },
181
+ { value: "logout", label: "Sign out" },
182
+ ],
183
+ });
184
+ if (action === "logout") {
185
+ await signOutProvider(owner, provider, interaction);
186
+ return;
187
+ }
188
+ }
189
+ await signInProvider(owner, provider, interaction, request);
190
+ });
191
+ },
192
+ },
193
+ {
194
+ id: "logout",
195
+ title: "Sign out of a model provider",
196
+ category: "Settings",
197
+ projections: {
198
+ help: { order: 12 },
199
+ palette: { order: 12 },
200
+ slash: { aliases: ["logout"], order: 12 },
201
+ },
202
+ action: { inputSchema: logoutArgumentsSchema },
203
+ handler: async (input) => {
204
+ const interaction = requireInteraction(input);
205
+ const request = logoutArguments.parse(input.arguments ?? {});
206
+ await unlessCancelled(interaction, async () => {
207
+ // Only providers the credential owner reports as signed in can be signed out of.
208
+ const signedIn = (await owner.access(interaction.signal))
209
+ .map((access) => ({
210
+ id: access.providerId,
211
+ name: access.providerName,
212
+ methods: [],
213
+ }))
214
+ .sort((left, right) => left.name.localeCompare(right.name));
215
+ if (signedIn.length === 0) {
216
+ interaction.notify({
217
+ type: "info",
218
+ message: await owner.status(interaction.signal),
219
+ });
220
+ return;
221
+ }
222
+ const provider = await chooseProvider(signedIn, request.providerId, interaction, "Sign out of which provider?", signedIn.map((provider) => ({
223
+ value: provider.id,
224
+ label: provider.name,
225
+ })));
226
+ await signOutProvider(owner, provider, interaction);
227
+ });
228
+ },
229
+ },
230
+ ];
231
+ }
232
+ /** Contribute the account commands as one lease. */
233
+ export function contributeAccountCommands(runtime, owner, options) {
234
+ return runtime.contribute({
235
+ sourceId: options.sourceId,
236
+ namespace: options.namespace,
237
+ commands: accountCommandDefinitions(owner, options),
238
+ });
239
+ }
@@ -0,0 +1,4 @@
1
+ export { AgentHead } from "./components/AgentHead.js";
2
+ export type { AgentHeadActivity } from "./components/AgentHead.js";
3
+ export { ShiitakeAvatar } from "./components/ShiitakeAvatar.js";
4
+ export type { AvatarState } from "./components/ShiitakeAvatar.js";
@@ -0,0 +1,2 @@
1
+ export { AgentHead } from "./components/AgentHead.js";
2
+ export { ShiitakeAvatar } from "./components/ShiitakeAvatar.js";
@@ -0,0 +1,20 @@
1
+ import type { ClientContributionV1 } from "@fungi.computer/shiitake/client-entry";
2
+ import type { ParakeetCommandRuntime } from "./commands/use-slash-commands.js";
3
+ /** Ordinary advertised actions over the current admitted rows and observation. */
4
+ export declare function useClientActions(commands: ParakeetCommandRuntime): {
5
+ /** Current Session-owned state of admitted action intents. */
6
+ state: ReadonlyMap<{
7
+ readonly label: string;
8
+ readonly id: string;
9
+ readonly input?: {
10
+ readonly [x: string]: import("effect/Schema").Json;
11
+ } | undefined;
12
+ }, Readonly<{
13
+ pending: boolean;
14
+ error?: string;
15
+ }>>;
16
+ /** Observation is live and this Session's action owner is attached. */
17
+ enabled: boolean;
18
+ /** Execute an admitted intent through its owning advertised command. */
19
+ execute: (entryId: string, action: NonNullable<ClientContributionV1["waiting"]>["cancel"], waiting?: boolean) => Promise<void>;
20
+ };
@@ -0,0 +1,41 @@
1
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
2
+ import { useClientContributions, useParakeetInteractions, useParakeetSession, useStatus, } from "./conversation.js";
3
+ import { createClientActions, } from "./core/client-actions.js";
4
+ const emptyActions = new Map();
5
+ const noActions = () => emptyActions;
6
+ const noSubscription = () => () => undefined;
7
+ /** Ordinary advertised actions over the current admitted rows and observation. */
8
+ export function useClientActions(commands) {
9
+ const contributions = useClientContributions();
10
+ const { observation } = useStatus();
11
+ const interactions = useParakeetInteractions();
12
+ const session = useParakeetSession();
13
+ const [owner, setOwner] = useState();
14
+ // This mutable input carries committed slots, never an optimistic projection.
15
+ const input = useRef({ contributions, observation });
16
+ input.current = { contributions, observation };
17
+ useEffect(() => {
18
+ const selected = createClientActions({
19
+ commands,
20
+ contributions: () => input.current.contributions,
21
+ online: () => input.current.observation.status === "live",
22
+ interactions,
23
+ });
24
+ setOwner(selected);
25
+ return () => {
26
+ selected.dispose();
27
+ };
28
+ }, [commands, interactions, session]);
29
+ useEffect(() => {
30
+ owner?.refresh();
31
+ }, [owner, contributions]);
32
+ const state = useSyncExternalStore(owner?.subscribe ?? noSubscription, owner?.snapshot ?? noActions, owner?.snapshot ?? noActions);
33
+ return {
34
+ /** Current Session-owned state of admitted action intents. */
35
+ state,
36
+ /** Observation is live and this Session's action owner is attached. */
37
+ enabled: owner !== undefined && observation.status === "live",
38
+ /** Execute an admitted intent through its owning advertised command. */
39
+ execute: (entryId, action, waiting = false) => owner?.execute(entryId, action, waiting) ?? Promise.resolve(),
40
+ };
41
+ }
@@ -0,0 +1,23 @@
1
+ import type { SessionExtensionSlots } from "@fungi.computer/shiitake/client";
2
+ import type { WhistleRuntime } from "@fungi.computer/whistle";
3
+ import type { ParakeetSession } from "../session.js";
4
+ /** Why one advertised catalog could not become commands. */
5
+ export type AdvertisedCatalogError = Readonly<{
6
+ /** The extension that advertised it. */
7
+ extensionId: string;
8
+ /** What was wrong. */
9
+ message: string;
10
+ }>;
11
+ /** Live commands for every catalog a Session's extensions advertise. */
12
+ export type AdvertisedCommands = Readonly<{
13
+ /** Apply the latest extension slots; changed catalogs replace their commands. */
14
+ update: (slots: SessionExtensionSlots) => readonly AdvertisedCatalogError[];
15
+ /** Withdraw every advertised command. */
16
+ dispose: () => void;
17
+ }>;
18
+ /**
19
+ * Turn each running extension's advertised `whistle.catalog/v1` catalog into
20
+ * Whistle commands that call that extension's own door. The client names no
21
+ * command: a new extension's catalog appears as new commands.
22
+ */
23
+ export declare function advertisedCommands(runtime: Pick<WhistleRuntime, "contribute">, session: Pick<ParakeetSession, "extension">, onInspect?: (entryId: string, commandId: string) => void): AdvertisedCommands;