@fungi.computer/parakeet 0.2.1 → 0.4.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.
- package/README.md +3 -0
- package/dist/account-commands.js +20 -7
- package/dist/components/ChatComposer.js +3 -1
- package/dist/components/InteractionPanel.js +18 -4
- package/dist/components/RunFailureNotice.js +11 -2
- package/dist/components/TimelineItem.js +1 -1
- package/dist/core/build-timeline.js +5 -2
- package/dist/core/client-actions.js +1 -20
- package/dist/core/picker-text.d.ts +7 -0
- package/dist/core/picker-text.js +24 -0
- package/dist/core/session-projection.js +4 -0
- package/dist/core/session-view-state.d.ts +1 -1
- package/dist/core/session-view-state.js +14 -6
- package/dist/headless.d.ts +1 -0
- package/dist/headless.js +1 -0
- package/dist/model-commands.js +14 -15
- package/dist/session-commands.js +6 -2
- package/dist/store/types.d.ts +2 -0
- package/dist/styles.css +12 -0
- package/package.json +9 -8
package/README.md
CHANGED
|
@@ -70,6 +70,9 @@ does not cancel accepted work or close a borrowed Agent. When replacing a
|
|
|
70
70
|
Session, provide its new handle rather than copying a snapshot into another
|
|
71
71
|
writable store.
|
|
72
72
|
|
|
73
|
+
On touch-primary devices, Enter inserts a newline and the Send button submits
|
|
74
|
+
the draft. On desktop, Enter submits and Shift+Enter inserts a newline.
|
|
75
|
+
|
|
73
76
|
The [standalone composer tests](test/standalone-composer.test.tsx) exercise the
|
|
74
77
|
provider, query cache, observations and prompt admission with typed fixtures.
|
|
75
78
|
The browser contract uses the real component with a fixture Session. A fake
|
package/dist/account-commands.js
CHANGED
|
@@ -40,7 +40,7 @@ function requireInteraction(input) {
|
|
|
40
40
|
throw new WhistleActionError("interaction_required", "Signing in needs an interactive client.");
|
|
41
41
|
return input.interaction;
|
|
42
42
|
}
|
|
43
|
-
async function chooseProvider(providers, requested, interaction,
|
|
43
|
+
async function chooseProvider(providers, requested, interaction, prompt, choices) {
|
|
44
44
|
if (requested !== undefined) {
|
|
45
45
|
const provider = providers.find((candidate) => candidate.id === requested);
|
|
46
46
|
if (provider === undefined)
|
|
@@ -51,7 +51,8 @@ async function chooseProvider(providers, requested, interaction, message, choice
|
|
|
51
51
|
throw new WhistleActionError("no_providers", "No model providers are available here.");
|
|
52
52
|
const id = await interaction.prompt({
|
|
53
53
|
type: "choice",
|
|
54
|
-
message,
|
|
54
|
+
message: prompt.message,
|
|
55
|
+
summary: prompt.summary,
|
|
55
56
|
collapsedGroup: "More providers",
|
|
56
57
|
options: choices,
|
|
57
58
|
});
|
|
@@ -73,6 +74,7 @@ async function chooseMethod(provider, requested, interaction) {
|
|
|
73
74
|
const kind = await interaction.prompt({
|
|
74
75
|
type: "choice",
|
|
75
76
|
message: `How do you want to sign in to ${provider.name}?`,
|
|
77
|
+
summary: "You can add the other kind later.",
|
|
76
78
|
options: provider.methods.map((method) => ({
|
|
77
79
|
value: method.kind,
|
|
78
80
|
label: method.label,
|
|
@@ -102,9 +104,11 @@ export async function signInProvider(owner, provider, interaction, request = {})
|
|
|
102
104
|
async function signOutProvider(owner, provider, interaction) {
|
|
103
105
|
const confirmed = await interaction.prompt({
|
|
104
106
|
type: "confirmation",
|
|
105
|
-
message: `Sign out of ${provider.name}
|
|
107
|
+
message: `Sign out of ${provider.name}?`,
|
|
108
|
+
summary: "Conversations on this device stop using this account.",
|
|
106
109
|
confirmLabel: "Sign out",
|
|
107
110
|
cancelLabel: "Keep signed in",
|
|
111
|
+
destructive: true,
|
|
108
112
|
});
|
|
109
113
|
if (!confirmed)
|
|
110
114
|
return;
|
|
@@ -162,15 +166,21 @@ export function accountCommandDefinitions(owner, options) {
|
|
|
162
166
|
const access = request.providerId === undefined
|
|
163
167
|
? await owner.access(interaction.signal)
|
|
164
168
|
: [];
|
|
165
|
-
|
|
169
|
+
// Several providers can be signed in at once, so none is "current";
|
|
170
|
+
// the Signed in group already comes first and takes the highlight.
|
|
171
|
+
const provider = await chooseProvider(registry, request.providerId, interaction, {
|
|
172
|
+
message: "Sign in to a model provider",
|
|
173
|
+
summary: "Use a subscription or an API key. You can add more later.",
|
|
174
|
+
}, providerChoices(registry, access, owner.popularProviderIds, "login"));
|
|
166
175
|
if (request.providerId === undefined &&
|
|
167
176
|
access.some((item) => item.providerId === provider.id)) {
|
|
168
177
|
const action = await interaction.prompt({
|
|
169
178
|
type: "choice",
|
|
170
|
-
message: provider.name
|
|
179
|
+
message: `Already signed in to ${provider.name}`,
|
|
180
|
+
summary: "Switch to another account, or sign out of this one.",
|
|
171
181
|
options: [
|
|
172
182
|
{ value: "switch", label: "Switch account" },
|
|
173
|
-
{ value: "logout", label: "Sign out" },
|
|
183
|
+
{ value: "logout", label: "Sign out…", destructive: true },
|
|
174
184
|
],
|
|
175
185
|
});
|
|
176
186
|
if (action === "logout") {
|
|
@@ -211,7 +221,10 @@ export function accountCommandDefinitions(owner, options) {
|
|
|
211
221
|
});
|
|
212
222
|
return;
|
|
213
223
|
}
|
|
214
|
-
const provider = await chooseProvider(signedIn, request.providerId, interaction,
|
|
224
|
+
const provider = await chooseProvider(signedIn, request.providerId, interaction, {
|
|
225
|
+
message: "Sign out of which provider?",
|
|
226
|
+
summary: "Other providers stay signed in.",
|
|
227
|
+
}, signedIn.map((provider) => ({
|
|
215
228
|
value: provider.id,
|
|
216
229
|
label: provider.name,
|
|
217
230
|
})));
|
|
@@ -140,7 +140,9 @@ function HostComposerTextarea({ placeholder, onChange, onSend, onAbort, slash, s
|
|
|
140
140
|
});
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
|
-
if (event.key === "Enter" &&
|
|
143
|
+
if (event.key === "Enter" &&
|
|
144
|
+
!event.shiftKey &&
|
|
145
|
+
!window.matchMedia("(pointer: coarse)").matches) {
|
|
144
146
|
event.preventDefault();
|
|
145
147
|
saveAndSend();
|
|
146
148
|
return;
|
|
@@ -1,27 +1,41 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Alert } from "@fungi.computer/caps/components/alert";
|
|
3
3
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
4
|
+
import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
|
|
5
|
+
import { Disclosure } from "@fungi.computer/caps/components/disclosure";
|
|
4
6
|
import { Field } from "@fungi.computer/caps/components/field";
|
|
5
7
|
import { Input } from "@fungi.computer/caps/components/input";
|
|
6
8
|
import { Listbox } from "@fungi.computer/caps/components/listbox";
|
|
7
9
|
import { Status } from "@fungi.computer/caps/components/status";
|
|
8
10
|
import { Display } from "@fungi.computer/caps/components/surface";
|
|
9
11
|
import { useState, useSyncExternalStore } from "react";
|
|
12
|
+
/** Summary, facts, long text and on-demand details: the shared modal anatomy. */
|
|
13
|
+
function PromptHeader({ request }) {
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const entries = (list) => list === undefined ? null : (_jsx(DataList, { density: "compact", children: list.map((entry) => (_jsxs(DataRow, { children: [_jsx("span", { className: "text-sm opacity-70", children: entry.label }), " ", _jsx("span", { className: "break-words", children: entry.value })] }, entry.label))) }));
|
|
16
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { className: "font-medium", children: request.message }), request.summary === undefined ? null : (_jsx("p", { className: "text-sm opacity-70", children: request.summary })), entries(request.facts), request.body === undefined ? null : (_jsxs("p", { className: "line-clamp-3 whitespace-pre-line break-words", children: [request.bodyLabel === undefined ? null : (_jsxs("span", { className: "text-sm opacity-70", children: [request.bodyLabel, " "] })), request.body] })), request.details === undefined && request.body === undefined ? null : (_jsxs(Disclosure, { open: open, onOpenChange: setOpen, summary: "Details", children: [request.body === undefined ? null : (_jsx("p", { className: "whitespace-pre-line break-words", children: request.body })), entries(request.details)] }))] }));
|
|
17
|
+
}
|
|
10
18
|
function PromptView({ host, pending, }) {
|
|
11
19
|
const [text, setText] = useState("");
|
|
12
20
|
const request = pending.request;
|
|
13
21
|
if (request.type === "choice")
|
|
14
|
-
return (_jsxs("div", { className: "flex flex-col gap-xs", children: [_jsx(
|
|
22
|
+
return (_jsxs("div", { className: "flex flex-col gap-xs", children: [_jsx(PromptHeader, { request: request }), _jsx(Listbox, { "aria-label": request.message, items: request.options, getKey: (option) => option.value, getLabel: (option) => option.label, onValueChange: (value) => {
|
|
15
23
|
if (value !== null)
|
|
16
24
|
host.answer(pending.sequence, value);
|
|
17
|
-
}, renderItem: (option) => (_jsxs("span", { className:
|
|
25
|
+
}, renderItem: (option) => (_jsxs("span", { className: `flex flex-col${option.destructive === true ? " text-error" : ""}`, children: [_jsxs("span", { children: [option.value === request.current ? "● " : "", option.label] }), option.description === undefined ? null : (_jsx("span", { className: "text-sm opacity-70", children: option.description }))] })) })] }));
|
|
18
26
|
if (request.type === "confirmation")
|
|
19
|
-
return (_jsxs("div", { className: "flex flex-col gap-sm", children: [_jsx(
|
|
27
|
+
return (_jsxs("div", { className: "flex flex-col gap-sm", children: [_jsx(PromptHeader, { request: request }), _jsxs("div", { className: "flex justify-end gap-xs", children: [_jsx(Button
|
|
28
|
+
// A destructive question focuses the safe answer, which comes first.
|
|
29
|
+
, {
|
|
30
|
+
// A destructive question focuses the safe answer, which comes first.
|
|
31
|
+
autoFocus: request.destructive === true, variant: "ghost", onClick: () => host.answer(pending.sequence, false), children: request.cancelLabel ?? "Cancel" }), _jsx(Button, { variant: request.destructive === true ? "destructive" : "primary", onClick: () => host.answer(pending.sequence, true), children: request.confirmLabel ?? "Confirm" })] })] }));
|
|
20
32
|
const submit = (event) => {
|
|
21
33
|
event.preventDefault();
|
|
22
34
|
host.answer(pending.sequence, text);
|
|
23
35
|
};
|
|
24
|
-
return (_jsxs("form", { className: "flex flex-col gap-sm", onSubmit: submit, children: [
|
|
36
|
+
return (_jsxs("form", { className: "flex flex-col gap-sm", onSubmit: submit, children: [request.summary === undefined &&
|
|
37
|
+
request.facts === undefined &&
|
|
38
|
+
request.body === undefined ? null : (_jsx(PromptHeader, { request: request })), _jsx(Field, { label: request.message, children: _jsx(Input, { autoComplete: "off", autoFocus: true, placeholder: request.placeholder, type: request.secret === true ? "password" : "text", value: text, onChange: (event) => {
|
|
25
39
|
setText(event.target.value);
|
|
26
40
|
} }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", children: "Continue" }) })] }));
|
|
27
41
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Alert } from "@fungi.computer/caps/components/alert";
|
|
3
3
|
import { Button } from "@fungi.computer/caps/components/button";
|
|
4
|
+
import { boundedDiagnostic } from "@fungi.computer/bird-dog/effect";
|
|
4
5
|
const messages = {
|
|
5
6
|
admission: "Your prompt didn’t make it through. Let’s give it another go.",
|
|
6
7
|
failed: "The run stopped. Check the message above before trying again.",
|
|
@@ -9,5 +10,13 @@ const messages = {
|
|
|
9
10
|
};
|
|
10
11
|
/** Session failures do not carry a billing receipt; never promise a refund. */
|
|
11
12
|
export function RunFailureNotice({ failure, cause, onRetry, }) {
|
|
12
|
-
|
|
13
|
+
const bounded = cause === undefined ? undefined : boundedDiagnostic(cause);
|
|
14
|
+
// Diagnostic metadata is useful to the owner, not a public explanation.
|
|
15
|
+
const publicCause = bounded !== undefined &&
|
|
16
|
+
!bounded.startsWith("reason_unavailable:") &&
|
|
17
|
+
!/\b(?:run|session)=/iu.test(bounded)
|
|
18
|
+
? bounded
|
|
19
|
+
: undefined;
|
|
20
|
+
const interruptedWithReason = failure === "interrupted" && publicCause !== undefined;
|
|
21
|
+
return (_jsx(Alert, { tone: "warning", title: failure === "cancelled" ? "Taking a breather" : "A little tangle", description: interruptedWithReason ? (_jsxs(_Fragment, { children: ["This turn stopped before it finished: ", publicCause, ". Your prompt is still here."] })) : (_jsxs(_Fragment, { children: [messages[failure], " If the model didn\u2019t start, there\u2019s no charge. Model work already done can still use credits.", publicCause ? _jsx("span", { className: "block", children: publicCause }) : null] })), action: failure === "cancelled" ? undefined : (_jsx(Button, { "aria-label": "Retry prompt", variant: "outline", onClick: onRetry, children: "Retry" })) }));
|
|
13
22
|
}
|
|
@@ -44,7 +44,7 @@ export const TimelineItem = memo(function TimelineItem({ item, onDismissFailed,
|
|
|
44
44
|
case "assistant_message":
|
|
45
45
|
if (!assistantMessageVisible(item))
|
|
46
46
|
return null;
|
|
47
|
-
return (_jsxs(_Fragment, { children: [item.author ? (_jsx("div", { className: "px-3 text-xs text-dim", "data-author": item.author.id, children: item.author.name })) : null, _jsx(ChatMessage, { isError: item.isError === true, pending: item.pending, speaker: "agent", ...(item.thinking ? { thinking: item.thinking } : {}), children: formatAssistantText?.(item.text) ?? item.text }), !item.pending && !item.isError
|
|
47
|
+
return (_jsxs(_Fragment, { children: [item.author ? (_jsx("div", { className: "px-3 text-xs text-dim", "data-author": item.author.id, children: item.author.name })) : null, _jsx(ChatMessage, { isError: item.isError === true, pending: item.pending, speaker: "agent", ...(item.thinking ? { thinking: item.thinking } : {}), children: formatAssistantText?.(item.text) ?? item.text }), item.isError && item.providerError !== undefined ? (_jsxs("div", { className: "px-3 text-error", "data-provider-error": item.id, children: [_jsx("p", { className: "whitespace-pre-wrap", children: item.providerError }), _jsxs("details", { children: [_jsx("summary", { children: "Provider details" }), _jsx("pre", { className: "whitespace-pre-wrap", children: item.providerError })] })] })) : null, !item.pending && !item.isError
|
|
48
48
|
? renderAssistantAttachment?.(item.text)
|
|
49
49
|
: null] }));
|
|
50
50
|
case "tool_group":
|
|
@@ -96,9 +96,12 @@ function assistantMessageItem(id, value, blocks) {
|
|
|
96
96
|
tokens: value.usage.totalTokens,
|
|
97
97
|
},
|
|
98
98
|
};
|
|
99
|
+
const withProviderError = value.stopReason === "error" && value.providerError !== undefined
|
|
100
|
+
? { ...item, providerError: value.providerError }
|
|
101
|
+
: item;
|
|
99
102
|
return blocks.thinking.length === 0
|
|
100
|
-
?
|
|
101
|
-
: { ...
|
|
103
|
+
? withProviderError
|
|
104
|
+
: { ...withProviderError, thinking: blocks.thinking.join("\n") };
|
|
102
105
|
}
|
|
103
106
|
function projectAssistant(id, value, author) {
|
|
104
107
|
const blocks = collectAssistantBlocks(value);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {} from "@fungi.computer/whistle";
|
|
2
2
|
import { reportFailure } from "../lib/failure.js";
|
|
3
3
|
function advertisedActions(entry) {
|
|
4
4
|
return [
|
|
@@ -68,23 +68,6 @@ export function createClientActions(options) {
|
|
|
68
68
|
if (intents.get(intent)?.pending)
|
|
69
69
|
return;
|
|
70
70
|
publish(intent, { pending: true });
|
|
71
|
-
const notificationOnly = {
|
|
72
|
-
// Waiting cancellation has no interactive retry surface.
|
|
73
|
-
run: (operation) => operation(),
|
|
74
|
-
signal: controller.signal,
|
|
75
|
-
prompt: () => Promise.reject(new WhistleActionError("client_waiting_prompt", "A waiting cancellation cannot request an answer")),
|
|
76
|
-
notify: (notice) => {
|
|
77
|
-
// A success only confirms the person's own action: no echo.
|
|
78
|
-
if (retired() || notice.type === "success")
|
|
79
|
-
return;
|
|
80
|
-
options.interactions.toast({
|
|
81
|
-
variant: "info",
|
|
82
|
-
message: notice.type === "handoff"
|
|
83
|
-
? (notice.message ?? notice.url)
|
|
84
|
-
: notice.message,
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
71
|
let outcome;
|
|
89
72
|
try {
|
|
90
73
|
let input = {
|
|
@@ -93,8 +76,6 @@ export function createClientActions(options) {
|
|
|
93
76
|
};
|
|
94
77
|
if (action.input !== undefined)
|
|
95
78
|
input = { ...input, arguments: action.input };
|
|
96
|
-
if (waiting)
|
|
97
|
-
input = { ...input, interaction: notificationOnly };
|
|
98
79
|
outcome = await options.commands.execute(`${entryId}:${action.id}`, input);
|
|
99
80
|
}
|
|
100
81
|
catch (cause) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When something happened, as a picker column: `14:02` for today, `Yesterday`,
|
|
3
|
+
* or `2026-10-24`. Shared by every picker that lists past work.
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatWhen(epochMillis: number, now: number): string;
|
|
6
|
+
/** `text` on one line, whitespace collapsed and cut at a word to `limit`. */
|
|
7
|
+
export declare function oneLine(text: string, limit: number): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
2
|
+
const dayStart = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
3
|
+
/**
|
|
4
|
+
* When something happened, as a picker column: `14:02` for today, `Yesterday`,
|
|
5
|
+
* or `2026-10-24`. Shared by every picker that lists past work.
|
|
6
|
+
*/
|
|
7
|
+
export function formatWhen(epochMillis, now) {
|
|
8
|
+
const at = new Date(epochMillis);
|
|
9
|
+
const days = Math.round((dayStart(new Date(now)) - dayStart(at)) / 86_400_000);
|
|
10
|
+
if (days <= 0)
|
|
11
|
+
return `${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
|
12
|
+
if (days === 1)
|
|
13
|
+
return "Yesterday";
|
|
14
|
+
return `${String(at.getFullYear())}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
|
|
15
|
+
}
|
|
16
|
+
/** `text` on one line, whitespace collapsed and cut at a word to `limit`. */
|
|
17
|
+
export function oneLine(text, limit) {
|
|
18
|
+
const line = text.replace(/\s+/gu, " ").trim();
|
|
19
|
+
if (line.length <= limit)
|
|
20
|
+
return line;
|
|
21
|
+
const cut = line.slice(0, Math.max(0, limit - 1));
|
|
22
|
+
const space = cut.lastIndexOf(" ");
|
|
23
|
+
return `${(space > limit / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
24
|
+
}
|
|
@@ -116,6 +116,10 @@ export function applySessionFrame(current, frame) {
|
|
|
116
116
|
compactions: mergeCompactions([], frame.baseline.compactions ?? []),
|
|
117
117
|
compactionNotices: mergeCompactionNotices([], frame.baseline.compactionNotices ?? []),
|
|
118
118
|
};
|
|
119
|
+
if (frame.kind === "activity")
|
|
120
|
+
return sameActivity(current.activity, frame.activity)
|
|
121
|
+
? current
|
|
122
|
+
: { ...current, activity: frame.activity };
|
|
119
123
|
if (frame.kind === "tool_output") {
|
|
120
124
|
const completed = new Set(current.messages.flatMap((message) => message.value.role === "toolResult" ? [message.value.toolCallId] : []));
|
|
121
125
|
const toolOutput = frame.toolOutput
|
|
@@ -46,7 +46,7 @@ export type SessionViewState = Readonly<{
|
|
|
46
46
|
}> | Readonly<{
|
|
47
47
|
/** The latest terminal failed or was interrupted. */
|
|
48
48
|
kind: "error";
|
|
49
|
-
/**
|
|
49
|
+
/** Redacted, bounded terminal diagnostic for presentation. */
|
|
50
50
|
cause: string;
|
|
51
51
|
}>;
|
|
52
52
|
/** The facts a view state is decided from. */
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { boundedDiagnostic } from "@fungi.computer/bird-dog/effect";
|
|
2
|
+
import stripAnsi from "strip-ansi";
|
|
1
3
|
const retryCauseLimit = 160;
|
|
2
|
-
const errorCauseLimit = 200;
|
|
3
4
|
function collapsed(text, limit) {
|
|
4
5
|
return text.replace(/\s+/gu, " ").trim().slice(0, limit);
|
|
5
6
|
}
|
|
7
|
+
function displayDiagnostic(text) {
|
|
8
|
+
// Recognize whole terminal programs before dropping individual controls:
|
|
9
|
+
// otherwise OSC payloads become visible text after their ESC is removed.
|
|
10
|
+
const safe = stripAnsi(text).replace(/[\p{Cc}\u200b\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069\u2060\ufeff]/gu, (character) => (/\s/u.test(character) ? " " : ""));
|
|
11
|
+
return boundedDiagnostic(safe);
|
|
12
|
+
}
|
|
6
13
|
function idleViewState({ activity, terminals, }) {
|
|
7
14
|
let terminal;
|
|
8
15
|
for (const candidate of terminals)
|
|
@@ -15,14 +22,15 @@ function idleViewState({ activity, terminals, }) {
|
|
|
15
22
|
if (activity.lastModelChoiceSequence !== null &&
|
|
16
23
|
activity.lastModelChoiceSequence > terminal.sequence)
|
|
17
24
|
return { kind: "waiting_for_you" };
|
|
18
|
-
if (terminal.outcome === "failed" || terminal.outcome === "interrupted")
|
|
25
|
+
if (terminal.outcome === "failed" || terminal.outcome === "interrupted") {
|
|
26
|
+
const fallback = terminal.outcome === "interrupted"
|
|
27
|
+
? "Run was interrupted"
|
|
28
|
+
: "Run failed without a diagnostic";
|
|
19
29
|
return {
|
|
20
30
|
kind: "error",
|
|
21
|
-
cause:
|
|
22
|
-
(terminal.outcome === "interrupted"
|
|
23
|
-
? "Run was interrupted"
|
|
24
|
-
: "Run failed without a diagnostic"), errorCauseLimit),
|
|
31
|
+
cause: displayDiagnostic(terminal.diagnostic ?? fallback) ?? fallback,
|
|
25
32
|
};
|
|
33
|
+
}
|
|
26
34
|
return { kind: "waiting_for_you" };
|
|
27
35
|
}
|
|
28
36
|
/** Decide a Session's view state from its activity, terminals and progress. */
|
package/dist/headless.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { formatModelIdentity, formatModelWithLevel, } from "./core/model-identit
|
|
|
2
2
|
export type { QueuedRecallState, QueuedRecallOutcome, } from "./core/queued-recall.js";
|
|
3
3
|
export type { ParakeetDraftIdentity, ParakeetDraftStorage, } from "./core/draft-storage.js";
|
|
4
4
|
export { formatContextUsage, formatTokenCount } from "./core/format-context.js";
|
|
5
|
+
export { formatWhen, oneLine } from "./core/picker-text.js";
|
|
5
6
|
export { providerChoices } from "./provider-choices.js";
|
|
6
7
|
export { ParakeetProvider, useComposer, useClientContributions, useParakeetInteractions, useExtensionSlots, useParakeetSession, useStatus, useTimeline, type ComposerController, type ParakeetDraftCheckpoint, type ParakeetModel, type ParakeetObservation, type ParakeetReconnectOutcome, type ParakeetStatus, type ParakeetSessionViewState, type ParakeetUsage, type StatusController, type TimelineController, } from "./conversation.js";
|
|
7
8
|
export { sessionViewState, queuedSessionLabel, sessionActivityStatus, type SessionActivityStatus, type SessionViewFacts, type SessionViewState as ParakeetOwnerViewState, } from "./core/session-view-state.js";
|
package/dist/headless.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { formatModelIdentity, formatModelWithLevel, } from "./core/model-identity.js";
|
|
2
2
|
export { formatContextUsage, formatTokenCount } from "./core/format-context.js";
|
|
3
|
+
export { formatWhen, oneLine } from "./core/picker-text.js";
|
|
3
4
|
export { providerChoices } from "./provider-choices.js";
|
|
4
5
|
export { ParakeetProvider, useComposer, useClientContributions, useParakeetInteractions, useExtensionSlots, useParakeetSession, useStatus, useTimeline, } from "./conversation.js";
|
|
5
6
|
export { sessionViewState, queuedSessionLabel, sessionActivityStatus, } from "./core/session-view-state.js";
|
package/dist/model-commands.js
CHANGED
|
@@ -8,9 +8,6 @@ const defaultChoice = "default";
|
|
|
8
8
|
const recentModels = new WeakMap();
|
|
9
9
|
/** Private memory ceiling, independent of how many models a provider offers. */
|
|
10
10
|
const maximumRememberedProviders = 256;
|
|
11
|
-
function modelLabel(model) {
|
|
12
|
-
return `${model.providerName} · ${model.name}`;
|
|
13
|
-
}
|
|
14
11
|
/** Each provider's current or last-used model first, then catalog order. */
|
|
15
12
|
function orderModels(models, current, recent) {
|
|
16
13
|
return [...models].sort((left, right) => {
|
|
@@ -33,19 +30,18 @@ async function chooseReasoning(interaction, model, current) {
|
|
|
33
30
|
const answer = await interaction.prompt({
|
|
34
31
|
type: "choice",
|
|
35
32
|
message: `Thinking level for ${model.name}`,
|
|
33
|
+
summary: "How long the model thinks before it answers.",
|
|
34
|
+
current: current ?? defaultChoice,
|
|
36
35
|
options: [
|
|
37
36
|
{ value: defaultChoice, label: "Model default" },
|
|
38
|
-
...model.reasoning.map((level) => ({
|
|
39
|
-
value: level,
|
|
40
|
-
label: level === current ? `${level} (current)` : level,
|
|
41
|
-
})),
|
|
37
|
+
...model.reasoning.map((level) => ({ value: level, label: level })),
|
|
42
38
|
],
|
|
43
39
|
});
|
|
44
40
|
return model.reasoning.find((level) => level === answer);
|
|
45
41
|
}
|
|
46
42
|
/** Leave one Whistle option for inheriting the Agent model. */
|
|
47
43
|
const maximumModelOptions = 255;
|
|
48
|
-
async function chooseProviderModels(interaction, models, account, refreshModels) {
|
|
44
|
+
async function chooseProviderModels(interaction, models, account, refreshModels, currentProviderId) {
|
|
49
45
|
const { owner } = account;
|
|
50
46
|
const [access, registry] = await Promise.all([
|
|
51
47
|
owner.access(interaction.signal),
|
|
@@ -76,7 +72,9 @@ async function chooseProviderModels(interaction, models, account, refreshModels)
|
|
|
76
72
|
}
|
|
77
73
|
const providerId = await interaction.prompt({
|
|
78
74
|
type: "choice",
|
|
79
|
-
message: "
|
|
75
|
+
message: "Choose a provider",
|
|
76
|
+
summary: "Or follow the Agent's own model.",
|
|
77
|
+
current: currentProviderId ?? defaultChoice,
|
|
80
78
|
collapsedGroup: "More providers",
|
|
81
79
|
options: [
|
|
82
80
|
{ value: defaultChoice, label: "Use agent model" },
|
|
@@ -156,21 +154,22 @@ export function sessionModelCommandDefinitions(session, options) {
|
|
|
156
154
|
return session.modelPreference.read();
|
|
157
155
|
const current = await session.modelPreference.read();
|
|
158
156
|
remember(current);
|
|
159
|
-
const providerModels = await chooseProviderModels(interaction, options.models, options.account, options.refreshModels);
|
|
157
|
+
const providerModels = await chooseProviderModels(interaction, options.models, options.account, options.refreshModels, current?.providerId);
|
|
160
158
|
if (providerModels === null) {
|
|
161
159
|
await interaction.run(() => session.modelPreference.set(null));
|
|
162
160
|
return interaction.run(() => session.modelPreference.read());
|
|
163
161
|
}
|
|
164
162
|
const ordered = orderModels(providerModels, current, recent);
|
|
163
|
+
const currentIndex = ordered.findIndex((model) => model.providerId === current?.providerId &&
|
|
164
|
+
model.id === current.modelId);
|
|
165
165
|
const answer = await interaction.prompt({
|
|
166
166
|
type: "choice",
|
|
167
|
-
message: "
|
|
167
|
+
message: "Choose a model",
|
|
168
|
+
summary: `Models from ${ordered[0]?.providerName ?? "this provider"}.`,
|
|
169
|
+
current: currentIndex < 0 ? undefined : String(currentIndex),
|
|
168
170
|
options: ordered.map((model, index) => ({
|
|
169
171
|
value: String(index),
|
|
170
|
-
label: model.
|
|
171
|
-
model.id === current.modelId
|
|
172
|
-
? `${modelLabel(model)} (current)`
|
|
173
|
-
: modelLabel(model),
|
|
172
|
+
label: model.name,
|
|
174
173
|
})),
|
|
175
174
|
});
|
|
176
175
|
const model = ordered[Number(answer)];
|
package/dist/session-commands.js
CHANGED
|
@@ -2,6 +2,7 @@ import { nanoid } from "@fungi.computer/cairn";
|
|
|
2
2
|
import { ShiitakeError } from "@fungi.computer/shiitake";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { buildDurableTimeline } from "./core/build-timeline.js";
|
|
5
|
+
import { formatWhen, oneLine } from "./core/picker-text.js";
|
|
5
6
|
import { sessionActivityStatus, sessionViewState, } from "./core/session-view-state.js";
|
|
6
7
|
/** A choice prompt holds at most 256 options; the default read already bounds this. */
|
|
7
8
|
const maxForkOptions = 200;
|
|
@@ -132,12 +133,15 @@ export function contributeSessionCommands(commands, session, options) {
|
|
|
132
133
|
if (interaction === undefined)
|
|
133
134
|
throw new Error("Forking a Session needs a person.");
|
|
134
135
|
const ordered = [...candidates].reverse().slice(0, maxForkOptions);
|
|
136
|
+
const now = Date.now();
|
|
135
137
|
const answer = await interaction.prompt({
|
|
136
138
|
type: "choice",
|
|
137
|
-
message: "Fork from message",
|
|
139
|
+
message: "Fork from which message?",
|
|
140
|
+
summary: "Start a new conversation from your message.",
|
|
138
141
|
options: ordered.map((item) => ({
|
|
139
142
|
value: item.id,
|
|
140
|
-
label: item.text
|
|
143
|
+
label: oneLine(item.text, 44) || "(empty message)",
|
|
144
|
+
description: item.at === undefined ? undefined : formatWhen(item.at, now),
|
|
141
145
|
})),
|
|
142
146
|
});
|
|
143
147
|
const forked = await interaction.run(async () => {
|
package/dist/store/types.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export type AssistantMessageItem = Readonly<{
|
|
|
71
71
|
at?: number;
|
|
72
72
|
/** Assistant-visible response text. */
|
|
73
73
|
text: string;
|
|
74
|
+
/** Pi's private failed-turn error text; never a terminal or delegation diagnostic. */
|
|
75
|
+
providerError?: string;
|
|
74
76
|
/** Assistant reasoning text, when supplied by the model. */
|
|
75
77
|
thinking?: string;
|
|
76
78
|
/** Usage carried by the exact assistant message. */
|
package/dist/styles.css
CHANGED
|
@@ -5932,6 +5932,12 @@
|
|
|
5932
5932
|
--mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e");
|
|
5933
5933
|
}
|
|
5934
5934
|
}
|
|
5935
|
+
.line-clamp-3 {
|
|
5936
|
+
overflow: hidden;
|
|
5937
|
+
display: -webkit-box;
|
|
5938
|
+
-webkit-box-orient: vertical;
|
|
5939
|
+
-webkit-line-clamp: 3;
|
|
5940
|
+
}
|
|
5935
5941
|
.prose {
|
|
5936
5942
|
:root & {
|
|
5937
5943
|
--tw-prose-body: var(--color-base-content);
|
|
@@ -6176,6 +6182,9 @@
|
|
|
6176
6182
|
.max-w-\[48rem\] {
|
|
6177
6183
|
max-width: 48rem;
|
|
6178
6184
|
}
|
|
6185
|
+
.max-w-\[50\%\] {
|
|
6186
|
+
max-width: 50%;
|
|
6187
|
+
}
|
|
6179
6188
|
.max-w-\[80ch\] {
|
|
6180
6189
|
max-width: 80ch;
|
|
6181
6190
|
}
|
|
@@ -6784,6 +6793,9 @@
|
|
|
6784
6793
|
.whitespace-pre {
|
|
6785
6794
|
white-space: pre;
|
|
6786
6795
|
}
|
|
6796
|
+
.whitespace-pre-line {
|
|
6797
|
+
white-space: pre-line;
|
|
6798
|
+
}
|
|
6787
6799
|
.whitespace-pre-wrap {
|
|
6788
6800
|
white-space: pre-wrap;
|
|
6789
6801
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/parakeet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Standalone React composer for a typed Shiitake Session",
|
|
@@ -47,15 +47,17 @@
|
|
|
47
47
|
"react-dom": "^18.2.0 || ^19.2.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@fungi.computer/agar": "0.
|
|
50
|
+
"@fungi.computer/agar": "0.4.0",
|
|
51
|
+
"@fungi.computer/bird-dog": "0.2.1",
|
|
51
52
|
"@fungi.computer/cairn": "0.1.0",
|
|
52
|
-
"@fungi.computer/caps": "0.
|
|
53
|
-
"@fungi.computer/
|
|
54
|
-
"@fungi.computer/
|
|
53
|
+
"@fungi.computer/caps": "0.3.1",
|
|
54
|
+
"@fungi.computer/gills": "0.2.0",
|
|
55
|
+
"@fungi.computer/knapsack": "0.3.1",
|
|
56
|
+
"@fungi.computer/shiitake": "0.5.0",
|
|
55
57
|
"@fungi.computer/sigils": "0.2.0",
|
|
56
58
|
"@fungi.computer/sprite": "0.2.0",
|
|
57
59
|
"@fungi.computer/sprite-zdog": "0.2.0",
|
|
58
|
-
"@fungi.computer/whistle": "0.
|
|
60
|
+
"@fungi.computer/whistle": "0.2.0",
|
|
59
61
|
"@tanstack/markdown": "0.0.12",
|
|
60
62
|
"@tanstack/react-virtual": "3.14.10",
|
|
61
63
|
"diff": "8.0.4",
|
|
@@ -64,8 +66,7 @@
|
|
|
64
66
|
"jotai-tanstack-query": "0.11.0",
|
|
65
67
|
"lucide-react": "0.460.0",
|
|
66
68
|
"strip-ansi": "7.2.0",
|
|
67
|
-
"zod": "4.4.3"
|
|
68
|
-
"@fungi.computer/gills": "0.2.0"
|
|
69
|
+
"zod": "4.4.3"
|
|
69
70
|
},
|
|
70
71
|
"dependenciesMeta": {
|
|
71
72
|
"@fungi.computer/caps": {
|