@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.
- package/LICENSE +21 -0
- package/README.md +186 -2
- package/dist/account-commands.d.ts +102 -0
- package/dist/account-commands.js +239 -0
- package/dist/agent-head.d.ts +4 -0
- package/dist/agent-head.js +2 -0
- package/dist/client-contributions.d.ts +20 -0
- package/dist/client-contributions.js +41 -0
- package/dist/commands/advertised.d.ts +23 -0
- package/dist/commands/advertised.js +72 -0
- package/dist/commands/builtins.d.ts +14 -0
- package/dist/commands/builtins.js +91 -0
- package/dist/commands/hints.d.ts +36 -0
- package/dist/commands/hints.js +64 -0
- package/dist/commands/interactions.d.ts +55 -0
- package/dist/commands/interactions.js +101 -0
- package/dist/commands/run.d.ts +11 -0
- package/dist/commands/run.js +61 -0
- package/dist/commands/slash.d.ts +124 -0
- package/dist/commands/slash.js +182 -0
- package/dist/commands/use-slash-commands.d.ts +57 -0
- package/dist/commands/use-slash-commands.js +207 -0
- package/dist/components/AgentHead.d.ts +27 -0
- package/dist/components/AgentHead.js +53 -0
- package/dist/components/ChatComposer.d.ts +43 -0
- package/dist/components/ChatComposer.js +209 -0
- package/dist/components/ChatMessage.d.ts +8 -0
- package/dist/components/ChatMessage.js +16 -0
- package/dist/components/ClientRows.d.ts +11 -0
- package/dist/components/ClientRows.js +46 -0
- package/dist/components/CodeBlock.d.ts +11 -0
- package/dist/components/CodeBlock.js +38 -0
- package/dist/components/InteractionPanel.d.ts +15 -0
- package/dist/components/InteractionPanel.js +42 -0
- package/dist/components/MarkdownRenderer.d.ts +6 -0
- package/dist/components/MarkdownRenderer.js +48 -0
- package/dist/components/RunFailureNotice.d.ts +7 -0
- package/dist/components/RunFailureNotice.js +13 -0
- package/dist/components/ShiitakeAvatar.d.ts +19 -0
- package/dist/components/ShiitakeAvatar.js +20 -0
- package/dist/components/SlashMenu.d.ts +8 -0
- package/dist/components/SlashMenu.js +50 -0
- package/dist/components/ThinkingBlock.d.ts +3 -0
- package/dist/components/ThinkingBlock.js +18 -0
- package/dist/components/TimelineItem.d.ts +15 -0
- package/dist/components/TimelineItem.js +35 -0
- package/dist/components/ToolGroup.d.ts +12 -0
- package/dist/components/ToolGroup.js +90 -0
- package/dist/components/WaitingNotice.d.ts +7 -0
- package/dist/components/WaitingNotice.js +8 -0
- package/dist/components/avatar-presentation.d.ts +23 -0
- package/dist/components/avatar-presentation.js +16 -0
- package/dist/components/sprite-service.d.ts +2 -0
- package/dist/components/sprite-service.js +9 -0
- package/dist/components/still-frame-cache.d.ts +16 -0
- package/dist/components/still-frame-cache.js +63 -0
- package/dist/conversation.d.ts +164 -0
- package/dist/conversation.js +760 -0
- package/dist/core/assistant-visibility.d.ts +3 -0
- package/dist/core/assistant-visibility.js +4 -0
- package/dist/core/author-labels.d.ts +16 -0
- package/dist/core/author-labels.js +16 -0
- package/dist/core/build-timeline.d.ts +36 -0
- package/dist/core/build-timeline.js +328 -0
- package/dist/core/client-actions.d.ts +35 -0
- package/dist/core/client-actions.js +128 -0
- package/dist/core/client-contributions.d.ts +24 -0
- package/dist/core/client-contributions.js +115 -0
- package/dist/core/client-timeline.d.ts +33 -0
- package/dist/core/client-timeline.js +72 -0
- package/dist/core/format-context.d.ts +11 -0
- package/dist/core/format-context.js +20 -0
- package/dist/core/format-tokens.d.ts +2 -0
- package/dist/core/format-tokens.js +6 -0
- package/dist/core/model-identity.d.ts +3 -0
- package/dist/core/model-identity.js +7 -0
- package/dist/core/session-projection.d.ts +11 -0
- package/dist/core/session-projection.js +191 -0
- package/dist/core/session-query.d.ts +118 -0
- package/dist/core/session-query.js +187 -0
- package/dist/core/session-view-state.d.ts +66 -0
- package/dist/core/session-view-state.js +114 -0
- package/dist/core/tool-contract.d.ts +15 -0
- package/dist/core/tool-contract.js +3 -0
- package/dist/core/tool-groups.d.ts +36 -0
- package/dist/core/tool-groups.js +151 -0
- package/dist/core/tool-view.d.ts +182 -0
- package/dist/core/tool-view.js +618 -0
- package/dist/headless.d.ts +28 -0
- package/dist/headless.js +24 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -0
- package/dist/lib/failure.d.ts +2 -0
- package/dist/lib/failure.js +9 -0
- package/dist/markdown.d.ts +3 -0
- package/dist/markdown.js +6 -0
- package/dist/model-commands.d.ts +44 -0
- package/dist/model-commands.js +232 -0
- package/dist/parakeet.d.ts +77 -0
- package/dist/parakeet.js +276 -0
- package/dist/provider-choices.d.ts +8 -0
- package/dist/provider-choices.js +43 -0
- package/dist/reload-command.d.ts +43 -0
- package/dist/reload-command.js +72 -0
- package/dist/session-commands.d.ts +53 -0
- package/dist/session-commands.js +259 -0
- package/dist/session.d.ts +24 -0
- package/dist/session.js +1 -0
- package/dist/store/types.d.ts +163 -0
- package/dist/store/types.js +1 -0
- package/dist/styles.css +19156 -0
- package/docs/api/README.md +15 -0
- package/docs/api/commands/slash.md +321 -0
- package/docs/api/headless.md +2510 -0
- package/docs/api/index.md +1253 -0
- package/package.json +79 -3
- package/src/styles.css +268 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionMessage } from "@fungi.computer/shiitake/client";
|
|
2
|
+
/** Who wrote a timeline line, as the host chose to show them. */
|
|
3
|
+
export type ParakeetAuthor = Readonly<{
|
|
4
|
+
/** A stable identity that tells two same-named authors apart. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The name shown beside the line. */
|
|
7
|
+
name: string;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* The host's reading of a message's opaque metadata: the author to show, or
|
|
11
|
+
* none. Parakeet never interprets metadata itself.
|
|
12
|
+
*/
|
|
13
|
+
export type ParakeetAuthorOf = (meta: SessionMessage["meta"]) => ParakeetAuthor | undefined;
|
|
14
|
+
/** The default reading: metadata names no author Parakeet can show. */
|
|
15
|
+
export declare const noAuthor: ParakeetAuthorOf;
|
|
16
|
+
export declare function authorDisplayNames(authors: readonly ParakeetAuthor[]): ReadonlyMap<string, string>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The default reading: metadata names no author Parakeet can show. */
|
|
2
|
+
export const noAuthor = () => undefined;
|
|
3
|
+
export function authorDisplayNames(authors) {
|
|
4
|
+
const identities = new Map();
|
|
5
|
+
for (const { id, name } of authors) {
|
|
6
|
+
const known = identities.get(name) ?? new Set();
|
|
7
|
+
known.add(id);
|
|
8
|
+
identities.set(name, known);
|
|
9
|
+
}
|
|
10
|
+
return new Map(authors.map(({ id, name }) => [
|
|
11
|
+
id,
|
|
12
|
+
(identities.get(name)?.size ?? 0) > 1
|
|
13
|
+
? `${name} · ${id.slice(-6)}`
|
|
14
|
+
: name,
|
|
15
|
+
]));
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ParakeetAuthorOf } from "./author-labels.js";
|
|
2
|
+
import type { SessionActivity, SessionCompactionMarker, SessionCompactionNotice, SessionMessage, SessionPendingPrompt, SessionProgress } from "@fungi.computer/shiitake/client";
|
|
3
|
+
import type { LocalPromptAdmission, TimelineItem } from "../store/types.js";
|
|
4
|
+
export type TimelineProjection = Readonly<{
|
|
5
|
+
items: readonly TimelineItem[];
|
|
6
|
+
model?: Readonly<{
|
|
7
|
+
model: string;
|
|
8
|
+
provider: string;
|
|
9
|
+
}>;
|
|
10
|
+
usage?: Readonly<{
|
|
11
|
+
cost: number;
|
|
12
|
+
tokens: number;
|
|
13
|
+
}>;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* The client request identity of a prompt this composer submitted, by its
|
|
17
|
+
* owner-issued command, so one row keeps its identity from submit to durable.
|
|
18
|
+
*/
|
|
19
|
+
export type PromptRequestIdOf = (commandId: string) => string | undefined;
|
|
20
|
+
export type DurableTimelineProjection = TimelineProjection & Readonly<{
|
|
21
|
+
completedMessageIds: ReadonlySet<string>;
|
|
22
|
+
commandIds: ReadonlySet<string>;
|
|
23
|
+
commandAnchors: ReadonlyMap<string, string>;
|
|
24
|
+
}>;
|
|
25
|
+
/**
|
|
26
|
+
* Project one durable Session revision into timeline rows, folding consecutive
|
|
27
|
+
* tool calls into groups. `compactions` places one divider row immediately
|
|
28
|
+
* before the message it marks; `notices` places a short row at a compaction
|
|
29
|
+
* that changed nothing. The model's own context is unaffected by this purely
|
|
30
|
+
* client-side projection.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildDurableTimeline(messages: readonly SessionMessage[], compactions?: readonly SessionCompactionMarker[], activity?: SessionActivity, authorOf?: ParakeetAuthorOf, requestIdOf?: PromptRequestIdOf, notices?: readonly SessionCompactionNotice[]): DurableTimelineProjection;
|
|
33
|
+
/** Project replaceable live work without copying settled history. */
|
|
34
|
+
export declare function buildTimelineTail(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[], authorOf?: ParakeetAuthorOf, requestIdOf?: PromptRequestIdOf): readonly TimelineItem[];
|
|
35
|
+
/** Overlay live progress and an optimistic prompt over stable durable rows. */
|
|
36
|
+
export declare function buildTimeline(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[]): TimelineProjection;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { noAuthor, } from "./author-labels.js";
|
|
2
|
+
import { completedMessageIds } from "./session-projection.js";
|
|
3
|
+
import { ToolArgumentsSchema } from "./tool-contract.js";
|
|
4
|
+
import { assistantMessageVisible } from "./assistant-visibility.js";
|
|
5
|
+
import { groupToolCalls } from "./tool-groups.js";
|
|
6
|
+
function orderedSources(messages, compactions, notices) {
|
|
7
|
+
const sources = [
|
|
8
|
+
...messages.map((message) => ({
|
|
9
|
+
sequence: message.sequence,
|
|
10
|
+
tie: 1,
|
|
11
|
+
kind: "message",
|
|
12
|
+
message,
|
|
13
|
+
})),
|
|
14
|
+
...compactions.map((marker) => ({
|
|
15
|
+
sequence: marker.sequence,
|
|
16
|
+
tie: 0,
|
|
17
|
+
kind: "marker",
|
|
18
|
+
marker,
|
|
19
|
+
})),
|
|
20
|
+
...notices.map((notice) => ({
|
|
21
|
+
sequence: notice.sequence,
|
|
22
|
+
tie: 2,
|
|
23
|
+
kind: "notice",
|
|
24
|
+
notice,
|
|
25
|
+
})),
|
|
26
|
+
];
|
|
27
|
+
return sources.sort((left, right) => left.sequence - right.sequence || left.tie - right.tie);
|
|
28
|
+
}
|
|
29
|
+
const noRequestId = () => undefined;
|
|
30
|
+
/** Carry a local prompt's request identity onto its owner-reported row. */
|
|
31
|
+
function withRequestId(item, requestId) {
|
|
32
|
+
return requestId === undefined ? item : { ...item, requestId };
|
|
33
|
+
}
|
|
34
|
+
function userText(content) {
|
|
35
|
+
return Array.isArray(content)
|
|
36
|
+
? content
|
|
37
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
38
|
+
.join("\n")
|
|
39
|
+
: content;
|
|
40
|
+
}
|
|
41
|
+
function toolItem(call, startedAt) {
|
|
42
|
+
return {
|
|
43
|
+
args: ToolArgumentsSchema.parse(call.arguments),
|
|
44
|
+
id: call.id,
|
|
45
|
+
kind: "tool",
|
|
46
|
+
name: call.name,
|
|
47
|
+
startedAt,
|
|
48
|
+
status: "unknown",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function progressItem(progress) {
|
|
52
|
+
const base = {
|
|
53
|
+
id: progress.messageId,
|
|
54
|
+
kind: "assistant_message",
|
|
55
|
+
pending: true,
|
|
56
|
+
text: progress.text,
|
|
57
|
+
};
|
|
58
|
+
return progress.thinking === ""
|
|
59
|
+
? base
|
|
60
|
+
: { ...base, thinking: progress.thinking };
|
|
61
|
+
}
|
|
62
|
+
function collectAssistantBlocks(value) {
|
|
63
|
+
const text = [];
|
|
64
|
+
const thinking = [];
|
|
65
|
+
const tools = [];
|
|
66
|
+
for (const block of value.content) {
|
|
67
|
+
if (block.type === "text")
|
|
68
|
+
text.push(block.text);
|
|
69
|
+
if (block.type === "thinking")
|
|
70
|
+
thinking.push(block.thinking);
|
|
71
|
+
if (block.type === "toolCall")
|
|
72
|
+
tools.push(toolItem(block, value.timestamp));
|
|
73
|
+
}
|
|
74
|
+
return { text, thinking, tools };
|
|
75
|
+
}
|
|
76
|
+
function assistantMessageItem(id, value, blocks) {
|
|
77
|
+
if (blocks.text.length === 0 &&
|
|
78
|
+
blocks.thinking.length === 0 &&
|
|
79
|
+
blocks.tools.length > 0)
|
|
80
|
+
return undefined;
|
|
81
|
+
const item = {
|
|
82
|
+
id,
|
|
83
|
+
isError: value.stopReason === "error" || value.stopReason === "aborted",
|
|
84
|
+
kind: "assistant_message",
|
|
85
|
+
pending: false,
|
|
86
|
+
at: value.timestamp,
|
|
87
|
+
text: blocks.text.length === 0
|
|
88
|
+
? value.stopReason === "error"
|
|
89
|
+
? "This run couldn’t finish."
|
|
90
|
+
: value.stopReason === "aborted"
|
|
91
|
+
? "The run was cancelled."
|
|
92
|
+
: ""
|
|
93
|
+
: blocks.text.join(""),
|
|
94
|
+
usage: {
|
|
95
|
+
cost: value.usage.cost.total,
|
|
96
|
+
tokens: value.usage.totalTokens,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
return blocks.thinking.length === 0
|
|
100
|
+
? item
|
|
101
|
+
: { ...item, thinking: blocks.thinking.join("\n") };
|
|
102
|
+
}
|
|
103
|
+
function projectAssistant(id, value, author) {
|
|
104
|
+
const blocks = collectAssistantBlocks(value);
|
|
105
|
+
const message = assistantMessageItem(id, value, blocks);
|
|
106
|
+
return {
|
|
107
|
+
items: message === undefined
|
|
108
|
+
? blocks.tools
|
|
109
|
+
: [{ ...message, author }, ...blocks.tools],
|
|
110
|
+
model: { model: value.model, provider: value.provider },
|
|
111
|
+
usage: { cost: value.usage.cost.total, tokens: value.usage.totalTokens },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function appendAssistant(items, toolIndexes, assistant) {
|
|
115
|
+
for (const item of assistant.items) {
|
|
116
|
+
if (item.kind === "tool")
|
|
117
|
+
toolIndexes.set(item.id, items.length);
|
|
118
|
+
items.push(item);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function completeTool(items, toolIndexes, value) {
|
|
122
|
+
const toolIndex = toolIndexes.get(value.toolCallId);
|
|
123
|
+
if (toolIndex === undefined)
|
|
124
|
+
return;
|
|
125
|
+
const tool = items[toolIndex];
|
|
126
|
+
if (tool?.kind !== "tool")
|
|
127
|
+
return;
|
|
128
|
+
items[toolIndex] = {
|
|
129
|
+
...tool,
|
|
130
|
+
result: value,
|
|
131
|
+
status: value.isError ? "error" : "completed",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Project one durable Session revision into timeline rows, folding consecutive
|
|
136
|
+
* tool calls into groups. `compactions` places one divider row immediately
|
|
137
|
+
* before the message it marks; `notices` places a short row at a compaction
|
|
138
|
+
* that changed nothing. The model's own context is unaffected by this purely
|
|
139
|
+
* client-side projection.
|
|
140
|
+
*/
|
|
141
|
+
export function buildDurableTimeline(messages, compactions = [], activity, authorOf = noAuthor, requestIdOf = noRequestId, notices = []) {
|
|
142
|
+
const items = [];
|
|
143
|
+
const toolIndexes = new Map();
|
|
144
|
+
const commandIds = new Set();
|
|
145
|
+
const itemCommands = new Map();
|
|
146
|
+
let model;
|
|
147
|
+
let tokens = 0;
|
|
148
|
+
let cost = 0;
|
|
149
|
+
for (const source of orderedSources(messages, compactions, notices)) {
|
|
150
|
+
if (source.kind === "marker") {
|
|
151
|
+
items.push({
|
|
152
|
+
id: source.marker.id,
|
|
153
|
+
kind: "compaction_divider",
|
|
154
|
+
at: source.marker.at,
|
|
155
|
+
tokensBefore: source.marker.tokensBefore,
|
|
156
|
+
tokensAfter: source.marker.tokensAfter,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (source.kind === "notice") {
|
|
161
|
+
items.push({
|
|
162
|
+
id: source.notice.id,
|
|
163
|
+
kind: "compaction_notice",
|
|
164
|
+
at: source.notice.at,
|
|
165
|
+
trigger: source.notice.trigger,
|
|
166
|
+
result: source.notice.result,
|
|
167
|
+
diagnostic: source.notice.diagnostic,
|
|
168
|
+
});
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const message = source.message;
|
|
172
|
+
const value = message.value;
|
|
173
|
+
if (message.commandId !== null)
|
|
174
|
+
commandIds.add(message.commandId);
|
|
175
|
+
const firstItem = items.length;
|
|
176
|
+
switch (value.role) {
|
|
177
|
+
case "user": {
|
|
178
|
+
const commandId = message.commandId ?? message.id;
|
|
179
|
+
const confirmed = {
|
|
180
|
+
id: message.id,
|
|
181
|
+
kind: "user_message",
|
|
182
|
+
commandId,
|
|
183
|
+
status: "confirmed",
|
|
184
|
+
at: value.timestamp,
|
|
185
|
+
text: userText(value.content),
|
|
186
|
+
author: authorOf(message.meta),
|
|
187
|
+
origin: message.origin,
|
|
188
|
+
};
|
|
189
|
+
items.push(withRequestId(confirmed, requestIdOf(commandId)));
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "assistant": {
|
|
193
|
+
const assistant = projectAssistant(message.id, value, authorOf(message.meta));
|
|
194
|
+
model = assistant.model;
|
|
195
|
+
tokens += assistant.usage.tokens;
|
|
196
|
+
cost += assistant.usage.cost;
|
|
197
|
+
appendAssistant(items, toolIndexes, assistant);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "toolResult":
|
|
201
|
+
completeTool(items, toolIndexes, value);
|
|
202
|
+
if (message.commandId !== null) {
|
|
203
|
+
const commands = itemCommands.get(value.toolCallId) ?? new Set();
|
|
204
|
+
commands.add(message.commandId);
|
|
205
|
+
itemCommands.set(value.toolCallId, commands);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (message.commandId !== null) {
|
|
210
|
+
for (const item of items.slice(firstItem))
|
|
211
|
+
itemCommands.set(item.id, new Set([message.commandId]));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// A durable call without a result is not evidence that it still runs.
|
|
215
|
+
// Only the current command's last matching call can borrow live tool activity.
|
|
216
|
+
const queue = activity?.queue;
|
|
217
|
+
if (queue?.state === "running" && activity?.activeTool !== null) {
|
|
218
|
+
let currentCommand = false;
|
|
219
|
+
let toolIndex = -1;
|
|
220
|
+
for (let index = 0; index < items.length; index++) {
|
|
221
|
+
const item = items[index];
|
|
222
|
+
if (item?.kind === "user_message") {
|
|
223
|
+
currentCommand = item.commandId === queue.commandId;
|
|
224
|
+
if (currentCommand)
|
|
225
|
+
toolIndex = -1;
|
|
226
|
+
}
|
|
227
|
+
if (currentCommand &&
|
|
228
|
+
item?.kind === "tool" &&
|
|
229
|
+
item.status === "unknown" &&
|
|
230
|
+
item.name === activity?.activeTool)
|
|
231
|
+
toolIndex = index;
|
|
232
|
+
}
|
|
233
|
+
const active = items[toolIndex];
|
|
234
|
+
if (active?.kind === "tool")
|
|
235
|
+
items[toolIndex] = { ...active, status: "running" };
|
|
236
|
+
}
|
|
237
|
+
const grouped = groupToolCalls(items);
|
|
238
|
+
const commandAnchors = new Map();
|
|
239
|
+
for (const row of grouped) {
|
|
240
|
+
if (row.kind === "assistant_message" && !assistantMessageVisible(row))
|
|
241
|
+
continue;
|
|
242
|
+
const members = row.kind === "tool_group" ? row.tools : [row];
|
|
243
|
+
for (const member of members)
|
|
244
|
+
for (const command of itemCommands.get(member.id) ?? [])
|
|
245
|
+
commandAnchors.set(command, row.id);
|
|
246
|
+
}
|
|
247
|
+
const base = {
|
|
248
|
+
commandAnchors,
|
|
249
|
+
completedMessageIds: completedMessageIds(messages),
|
|
250
|
+
items: grouped,
|
|
251
|
+
commandIds,
|
|
252
|
+
};
|
|
253
|
+
return model === undefined
|
|
254
|
+
? base
|
|
255
|
+
: { ...base, model, usage: { cost, tokens } };
|
|
256
|
+
}
|
|
257
|
+
/** Project replaceable live work without copying settled history. */
|
|
258
|
+
export function buildTimelineTail(durable, progress, queued, admissions = [], authorOf = noAuthor, requestIdOf = noRequestId) {
|
|
259
|
+
const visibleProgress = progress === null ||
|
|
260
|
+
durable.completedMessageIds.has(progress.messageId) ||
|
|
261
|
+
(progress.text === "" && progress.thinking === "")
|
|
262
|
+
? undefined
|
|
263
|
+
: progress;
|
|
264
|
+
if (admissions.length === 0 &&
|
|
265
|
+
visibleProgress === undefined &&
|
|
266
|
+
queued.length === 0)
|
|
267
|
+
return [];
|
|
268
|
+
const items = [];
|
|
269
|
+
const queuedCommandIds = new Set(queued.map(({ commandId }) => commandId));
|
|
270
|
+
for (const prompt of queued) {
|
|
271
|
+
if (durable.commandIds.has(prompt.commandId))
|
|
272
|
+
continue;
|
|
273
|
+
const waiting = {
|
|
274
|
+
id: prompt.commandId,
|
|
275
|
+
kind: "user_message",
|
|
276
|
+
commandId: prompt.commandId,
|
|
277
|
+
status: "queued",
|
|
278
|
+
delivery: prompt.delivery,
|
|
279
|
+
text: prompt.text,
|
|
280
|
+
origin: prompt.origin,
|
|
281
|
+
author: authorOf(prompt.meta),
|
|
282
|
+
};
|
|
283
|
+
items.push(withRequestId(waiting, requestIdOf(prompt.commandId)));
|
|
284
|
+
}
|
|
285
|
+
for (const admission of admissions) {
|
|
286
|
+
if (admission.commandId !== undefined &&
|
|
287
|
+
(queuedCommandIds.has(admission.commandId) ||
|
|
288
|
+
durable.commandIds.has(admission.commandId)))
|
|
289
|
+
continue;
|
|
290
|
+
const baseItem = {
|
|
291
|
+
id: admission.requestId,
|
|
292
|
+
kind: "user_message",
|
|
293
|
+
requestId: admission.requestId,
|
|
294
|
+
status: admission.status === "failed"
|
|
295
|
+
? "failed"
|
|
296
|
+
: admission.status === "accepted"
|
|
297
|
+
? "accepted"
|
|
298
|
+
: "pending",
|
|
299
|
+
delivery: admission.delivery,
|
|
300
|
+
text: admission.content,
|
|
301
|
+
};
|
|
302
|
+
const commandItem = admission.commandId === undefined
|
|
303
|
+
? baseItem
|
|
304
|
+
: { ...baseItem, commandId: admission.commandId };
|
|
305
|
+
const item = admission.error === undefined
|
|
306
|
+
? commandItem
|
|
307
|
+
: { ...commandItem, error: admission.error };
|
|
308
|
+
items.push(item);
|
|
309
|
+
}
|
|
310
|
+
// Group only unresolved input; settled history and live progress keep their
|
|
311
|
+
// places. Filtering preserves the owner's order within each delivery group.
|
|
312
|
+
return [
|
|
313
|
+
...(visibleProgress === undefined ? [] : [progressItem(visibleProgress)]),
|
|
314
|
+
...items.filter((item) => item.status !== "failed" && item.delivery === "steer"),
|
|
315
|
+
...items.filter((item) => item.status !== "failed" && item.delivery === "followUp"),
|
|
316
|
+
...items.filter((item) => item.status === "failed"),
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
/** Overlay live progress and an optimistic prompt over stable durable rows. */
|
|
320
|
+
export function buildTimeline(durable, progress, queued, admissions = []) {
|
|
321
|
+
const tail = buildTimelineTail(durable, progress, queued, admissions);
|
|
322
|
+
if (tail.length === 0)
|
|
323
|
+
return durable;
|
|
324
|
+
const items = [...durable.items, ...tail];
|
|
325
|
+
if (durable.model === undefined || durable.usage === undefined)
|
|
326
|
+
return { items };
|
|
327
|
+
return { items, model: durable.model, usage: durable.usage };
|
|
328
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ClientContributionV1 } from "@fungi.computer/shiitake/client-entry";
|
|
2
|
+
import { type WhistleRuntime } from "@fungi.computer/whistle";
|
|
3
|
+
import type { ParakeetInteractions } from "../commands/interactions.js";
|
|
4
|
+
import type { ClientContribution } from "./client-contributions.js";
|
|
5
|
+
/** Action state belongs to the selected Session, never the rendered row. */
|
|
6
|
+
export type ClientActionState = Readonly<{
|
|
7
|
+
/** The same advertised intent is already in flight. */
|
|
8
|
+
pending: boolean;
|
|
9
|
+
/** Visible execution failure; cleared when this intent is retried. */
|
|
10
|
+
error?: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** Run admitted row intents through the ordinary advertised Whistle commands. */
|
|
13
|
+
export declare function createClientActions(options: Readonly<{
|
|
14
|
+
contributions: () => readonly ClientContribution[];
|
|
15
|
+
online: () => boolean;
|
|
16
|
+
commands: Pick<WhistleRuntime, "execute">;
|
|
17
|
+
interactions: ParakeetInteractions;
|
|
18
|
+
}>): {
|
|
19
|
+
snapshot: () => ReadonlyMap<{
|
|
20
|
+
readonly label: string;
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly input?: {
|
|
23
|
+
readonly [x: string]: import("effect/Schema").Json;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}, Readonly<{
|
|
26
|
+
/** The same advertised intent is already in flight. */
|
|
27
|
+
pending: boolean;
|
|
28
|
+
/** Visible execution failure; cleared when this intent is retried. */
|
|
29
|
+
error?: string;
|
|
30
|
+
}>>;
|
|
31
|
+
refresh: () => void;
|
|
32
|
+
subscribe: (listener: () => void) => () => void;
|
|
33
|
+
execute: (entryId: string, action: NonNullable<ClientContributionV1["waiting"]>["cancel"], waiting?: boolean) => Promise<void>;
|
|
34
|
+
dispose: () => void;
|
|
35
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { WhistleActionError, } from "@fungi.computer/whistle";
|
|
2
|
+
import { reportFailure } from "../lib/failure.js";
|
|
3
|
+
function advertisedActions(entry) {
|
|
4
|
+
return [
|
|
5
|
+
...entry.value.panel.flatMap((row) => row.actions ?? []),
|
|
6
|
+
...entry.value.timeline.cells.flatMap((cell) => cell.rows.flatMap((row) => row.actions ?? [])),
|
|
7
|
+
...(entry.value.waiting === null ? [] : [entry.value.waiting.cancel]),
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
/** Run admitted row intents through the ordinary advertised Whistle commands. */
|
|
11
|
+
export function createClientActions(options) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
// Abort may change while the advertised command is awaiting its owner.
|
|
14
|
+
const retired = () => controller.signal.aborted;
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
let state = new Map();
|
|
17
|
+
// Admission decodes fresh row objects on a committed slot change. Pending
|
|
18
|
+
// work belongs to the advertised command intent, not that object's address.
|
|
19
|
+
const intents = new Map();
|
|
20
|
+
const key = (entryId, action) => JSON.stringify([entryId, action.id, action.input ?? {}]);
|
|
21
|
+
const refresh = () => {
|
|
22
|
+
if (retired())
|
|
23
|
+
return;
|
|
24
|
+
const current = new Map();
|
|
25
|
+
const visible = new Set();
|
|
26
|
+
for (const entry of options.contributions()) {
|
|
27
|
+
for (const action of advertisedActions(entry)) {
|
|
28
|
+
const id = key(entry.entryId, action);
|
|
29
|
+
visible.add(id);
|
|
30
|
+
const value = intents.get(id);
|
|
31
|
+
if (value !== undefined)
|
|
32
|
+
current.set(action, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const [id, value] of intents)
|
|
36
|
+
if (!visible.has(id) && !value.pending)
|
|
37
|
+
intents.delete(id);
|
|
38
|
+
state = current;
|
|
39
|
+
for (const listener of listeners)
|
|
40
|
+
listener();
|
|
41
|
+
};
|
|
42
|
+
const publish = (id, next) => {
|
|
43
|
+
if (retired())
|
|
44
|
+
return;
|
|
45
|
+
intents.set(id, next);
|
|
46
|
+
refresh();
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
snapshot: () => state,
|
|
50
|
+
refresh,
|
|
51
|
+
subscribe: (listener) => {
|
|
52
|
+
listeners.add(listener);
|
|
53
|
+
return () => {
|
|
54
|
+
listeners.delete(listener);
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
execute: async (entryId, action, waiting = false) => {
|
|
58
|
+
const entry = options
|
|
59
|
+
.contributions()
|
|
60
|
+
.find((value) => value.entryId === entryId);
|
|
61
|
+
if (retired() ||
|
|
62
|
+
!options.online() ||
|
|
63
|
+
entry === undefined ||
|
|
64
|
+
!advertisedActions(entry).includes(action) ||
|
|
65
|
+
(waiting && entry.value.waiting?.cancel !== action))
|
|
66
|
+
return;
|
|
67
|
+
const intent = key(entryId, action);
|
|
68
|
+
if (intents.get(intent)?.pending)
|
|
69
|
+
return;
|
|
70
|
+
publish(intent, { pending: true });
|
|
71
|
+
const notificationOnly = {
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
prompt: () => Promise.reject(new WhistleActionError("client_waiting_prompt", "A waiting cancellation cannot request an answer")),
|
|
74
|
+
notify: (notice) => {
|
|
75
|
+
if (retired())
|
|
76
|
+
return;
|
|
77
|
+
options.interactions.report({
|
|
78
|
+
tone: notice.type === "success" ? "success" : "info",
|
|
79
|
+
message: notice.type === "handoff"
|
|
80
|
+
? (notice.message ?? notice.url)
|
|
81
|
+
: notice.message,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
let outcome;
|
|
86
|
+
try {
|
|
87
|
+
let input = {
|
|
88
|
+
origin: "menu",
|
|
89
|
+
signal: controller.signal,
|
|
90
|
+
};
|
|
91
|
+
if (action.input !== undefined)
|
|
92
|
+
input = { ...input, arguments: action.input };
|
|
93
|
+
if (waiting)
|
|
94
|
+
input = { ...input, interaction: notificationOnly };
|
|
95
|
+
outcome = await options.commands.execute(`${entryId}:${action.id}`, input);
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
reportFailure("client row command", cause);
|
|
99
|
+
if (retired())
|
|
100
|
+
return;
|
|
101
|
+
const error = cause instanceof Error
|
|
102
|
+
? cause
|
|
103
|
+
: new Error("Client row command failed", { cause });
|
|
104
|
+
publish(intent, { pending: false, error: error.message });
|
|
105
|
+
options.interactions.report({ tone: "error", message: error.message });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (retired())
|
|
109
|
+
return;
|
|
110
|
+
const error = outcome.status === "handled"
|
|
111
|
+
? undefined
|
|
112
|
+
: outcome.status === "failed"
|
|
113
|
+
? outcome.error.message
|
|
114
|
+
: outcome.status === "unavailable"
|
|
115
|
+
? outcome.reason
|
|
116
|
+
: `Client row command ${outcome.status}`;
|
|
117
|
+
publish(intent, error === undefined ? { pending: false } : { pending: false, error });
|
|
118
|
+
if (error !== undefined)
|
|
119
|
+
options.interactions.report({ tone: "error", message: error });
|
|
120
|
+
},
|
|
121
|
+
dispose: () => {
|
|
122
|
+
controller.abort();
|
|
123
|
+
state = new Map();
|
|
124
|
+
intents.clear();
|
|
125
|
+
listeners.clear();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ClientContributionV1, type ClientEntryV1 } from "@fungi.computer/shiitake/client-entry";
|
|
2
|
+
import type { SessionExtensionSlots } from "@fungi.computer/shiitake/client";
|
|
3
|
+
/** Decoded client data, still owned and ordered by its composition root. */
|
|
4
|
+
export type ClientContribution = Readonly<{
|
|
5
|
+
/** Composition-root identity of the producing entry. */
|
|
6
|
+
entryId: string;
|
|
7
|
+
/** Strictly admitted presentation data for the current observed slot. */
|
|
8
|
+
value: ClientContributionV1;
|
|
9
|
+
}>;
|
|
10
|
+
/**
|
|
11
|
+
* The selected-Session evaluation owner. Feed existing observed slots here;
|
|
12
|
+
* renderers subscribe to admitted data and never evaluate an entry in render.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createClientContributions(installed: readonly ClientEntryV1[], onError: (entryId: string, error: Error) => void): {
|
|
15
|
+
snapshot: () => readonly Readonly<{
|
|
16
|
+
/** Composition-root identity of the producing entry. */
|
|
17
|
+
entryId: string;
|
|
18
|
+
/** Strictly admitted presentation data for the current observed slot. */
|
|
19
|
+
value: ClientContributionV1;
|
|
20
|
+
}>[];
|
|
21
|
+
subscribe: (listener: () => void) => () => void;
|
|
22
|
+
update: (slots: SessionExtensionSlots) => void;
|
|
23
|
+
dispose: () => void;
|
|
24
|
+
};
|