@fungi.computer/parakeet 0.0.1 → 0.2.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 +45 -6
- package/dist/account-commands.js +3 -11
- package/dist/commands/advertised.d.ts +3 -1
- package/dist/commands/advertised.js +89 -4
- package/dist/commands/builtins.js +1 -1
- package/dist/commands/interactions.d.ts +22 -7
- package/dist/commands/interactions.js +41 -17
- package/dist/commands/run.d.ts +3 -2
- package/dist/commands/run.js +5 -2
- package/dist/commands/slash.d.ts +2 -0
- package/dist/commands/slash.js +13 -7
- package/dist/commands/use-slash-commands.js +4 -4
- package/dist/components/AgentHead.d.ts +9 -12
- package/dist/components/AgentHead.js +12 -44
- package/dist/components/ChatComposer.d.ts +3 -1
- package/dist/components/ChatComposer.js +79 -15
- package/dist/components/ChatMessage.d.ts +4 -0
- package/dist/components/ChatMessage.js +6 -2
- package/dist/components/ClientRows.js +2 -2
- package/dist/components/InteractionPanel.d.ts +2 -4
- package/dist/components/InteractionPanel.js +2 -2
- package/dist/components/ShiitakeAvatar.d.ts +7 -5
- package/dist/components/ShiitakeAvatar.js +6 -4
- package/dist/components/TimelineItem.d.ts +2 -0
- package/dist/components/TimelineItem.js +31 -7
- package/dist/components/avatar-presentation.d.ts +7 -14
- package/dist/components/avatar-presentation.js +6 -13
- package/dist/conversation-opening.d.ts +47 -0
- package/dist/conversation-opening.js +173 -0
- package/dist/conversation.d.ts +50 -5
- package/dist/conversation.js +587 -87
- package/dist/core/author-labels.d.ts +2 -0
- package/dist/core/build-timeline.d.ts +4 -2
- package/dist/core/build-timeline.js +46 -7
- package/dist/core/client-actions.js +6 -3
- package/dist/core/client-timeline.d.ts +9 -2
- package/dist/core/client-timeline.js +15 -4
- package/dist/core/draft-storage.d.ts +63 -0
- package/dist/core/draft-storage.js +50 -0
- package/dist/core/model-identity.d.ts +2 -0
- package/dist/core/model-identity.js +6 -0
- package/dist/core/queued-recall.d.ts +35 -0
- package/dist/core/queued-recall.js +138 -0
- package/dist/core/session-projection.js +17 -0
- package/dist/core/session-query.d.ts +54 -11
- package/dist/core/session-query.js +38 -12
- package/dist/core/tool-groups.d.ts +1 -1
- package/dist/core/tool-groups.js +3 -1
- package/dist/core/tool-view.d.ts +1 -1
- package/dist/core/tool-view.js +3 -2
- package/dist/core/use-timeline-item-key.d.ts +8 -0
- package/dist/core/use-timeline-item-key.js +29 -0
- package/dist/headless.d.ts +6 -4
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/model-commands.js +65 -40
- package/dist/parakeet.js +17 -18
- package/dist/provider-choices.js +10 -7
- package/dist/session-commands.d.ts +0 -9
- package/dist/session-commands.js +39 -68
- package/dist/session.d.ts +1 -1
- package/dist/store/types.d.ts +10 -4
- package/dist/styles.css +105 -117
- package/docs/api/commands/slash.md +24 -21
- package/docs/api/headless.md +375 -232
- package/docs/api/index.md +168 -60
- package/package.json +16 -10
- package/src/styles.css +4 -9
- package/dist/components/still-frame-cache.d.ts +0 -16
- package/dist/components/still-frame-cache.js +0 -63
package/README.md
CHANGED
|
@@ -126,17 +126,56 @@ It owns parsing, completion, stable request identities and interaction state.
|
|
|
126
126
|
The browser and terminal render the same questions, choices and notices.
|
|
127
127
|
`/model` and `/thinking` use that interaction path.
|
|
128
128
|
|
|
129
|
+
`ParakeetProvider` accepts `onToast` for transient command information. The host
|
|
130
|
+
renders it through its existing toast owner: the terminal's shared toast
|
|
131
|
+
viewport or Grove's `ui.showToast`. `ParakeetInteractions.toast` and
|
|
132
|
+
`subscribeToasts` expose the same renderer-neutral seam. Ordinary first-success
|
|
133
|
+
echoes are silent, including after a dialog completes. Progress, questions and
|
|
134
|
+
handoffs remain in their interaction while active; failures retain their retry
|
|
135
|
+
or attention UI rather than expiring as information.
|
|
136
|
+
|
|
137
|
+
After Try again, a final information or success notice stays in that
|
|
138
|
+
interaction's existing result row until Done/close, without an extra toast. A
|
|
139
|
+
producer that supplies no completion notice does not gain a fabricated result.
|
|
140
|
+
|
|
129
141
|
Account commands are provider-neutral. The host's account owner and policy
|
|
130
142
|
decide which login methods are available. The presentation layer does not gain a
|
|
131
143
|
credential store or make an advertised command authorized.
|
|
132
144
|
|
|
145
|
+
Queued timeline rows offer Cancel through
|
|
146
|
+
`useTimeline().cancelQueued(commandId)`. The row disappears immediately while
|
|
147
|
+
the existing Session owner decides the pending-only cancellation. This never
|
|
148
|
+
stops the running turn or restores text to the composer. TooLate or an
|
|
149
|
+
unconfirmed response produces a warning and restores any row still reported as
|
|
150
|
+
queued; an accepted response stays hidden through stale queued snapshots until
|
|
151
|
+
the owner projection catches up. Only one queued gesture waits for its owner at
|
|
152
|
+
a time, and switching conversations discards its presentation state. Recall
|
|
153
|
+
remains a separate restore-to-composer gesture.
|
|
154
|
+
|
|
133
155
|
The composer reconciles optimistic prompts with owner facts by request identity.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
Submitting clears the draft immediately and adds a faded pending message.
|
|
157
|
+
Acceptance restores normal emphasis; owner-queued messages remain faded. If
|
|
158
|
+
admission fails, the exact submitted text returns to the composer and the host
|
|
159
|
+
receives an error toast. Any newer draft stays first, followed by a blank line
|
|
160
|
+
and the restored text; acceptance never clears newer typing. An uncertain
|
|
161
|
+
admission retains its request identity while the owner resolves it. An accepted
|
|
162
|
+
resolution confirms the admitted command or recorded entry; only confirmed
|
|
163
|
+
withdrawal restores the draft and permits retry with a new request identity,
|
|
164
|
+
because the old identity is durably fenced. Retrying failed or interrupted
|
|
165
|
+
durable work also submits a new request identity, preserving the failed prompt's
|
|
166
|
+
text and image cargo, including image-only prompts after live-page eviction. It
|
|
167
|
+
does not consume a newer unsent draft. Failed observation shows connection
|
|
168
|
+
failure; reconnecting does not prove pending work failed or resubmit it
|
|
169
|
+
automatically. Typed command errors remain visible and retryable. The host
|
|
170
|
+
remains responsible for the Session transport, current permissions and operator
|
|
171
|
+
diagnostics.
|
|
172
|
+
|
|
173
|
+
An owner's accepted withdrawal receipt can name a run-free recorded entry:
|
|
174
|
+
`receipt.commandId === message.id` even though `message.commandId === null`.
|
|
175
|
+
That receipt confirms durable recording, not cancellation or model admission. A
|
|
176
|
+
translating host retains the original composer identity in its durable metadata
|
|
177
|
+
and projects it as `messages[].requestId` or `pending.prompts[].requestId`; its
|
|
178
|
+
internal admission key is not that identity.
|
|
140
179
|
|
|
141
180
|
## API and examples
|
|
142
181
|
|
package/dist/account-commands.js
CHANGED
|
@@ -91,17 +91,13 @@ export async function signInProvider(owner, provider, interaction, request = {})
|
|
|
91
91
|
type: "progress",
|
|
92
92
|
message: `Signing in to ${provider.name}…`,
|
|
93
93
|
});
|
|
94
|
-
await owner.login(request.option === undefined
|
|
94
|
+
await interaction.run(() => owner.login(request.option === undefined
|
|
95
95
|
? { providerId: provider.id, method: method.kind }
|
|
96
96
|
: {
|
|
97
97
|
providerId: provider.id,
|
|
98
98
|
method: method.kind,
|
|
99
99
|
option: request.option,
|
|
100
|
-
}, interaction);
|
|
101
|
-
interaction.notify({
|
|
102
|
-
type: "success",
|
|
103
|
-
message: `Signed in to ${provider.name}.`,
|
|
104
|
-
});
|
|
100
|
+
}, interaction));
|
|
105
101
|
}
|
|
106
102
|
async function signOutProvider(owner, provider, interaction) {
|
|
107
103
|
const confirmed = await interaction.prompt({
|
|
@@ -112,11 +108,7 @@ async function signOutProvider(owner, provider, interaction) {
|
|
|
112
108
|
});
|
|
113
109
|
if (!confirmed)
|
|
114
110
|
return;
|
|
115
|
-
await owner.logout(provider.id, interaction);
|
|
116
|
-
interaction.notify({
|
|
117
|
-
type: "success",
|
|
118
|
-
message: `Signed out of ${provider.name}.`,
|
|
119
|
-
});
|
|
111
|
+
await interaction.run(() => owner.logout(provider.id, interaction));
|
|
120
112
|
}
|
|
121
113
|
/** Cancelling a sign-in is an ordinary ending, not a failure. */
|
|
122
114
|
async function unlessCancelled(interaction, work) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SessionExtensionSlots } from "@fungi.computer/shiitake/client";
|
|
2
2
|
import type { WhistleRuntime } from "@fungi.computer/whistle";
|
|
3
3
|
import type { ParakeetSession } from "../session.js";
|
|
4
|
+
import type { ClientContribution } from "../core/client-contributions.js";
|
|
4
5
|
/** Why one advertised catalog could not become commands. */
|
|
5
6
|
export type AdvertisedCatalogError = Readonly<{
|
|
6
7
|
/** The extension that advertised it. */
|
|
@@ -19,5 +20,6 @@ export type AdvertisedCommands = Readonly<{
|
|
|
19
20
|
* Turn each running extension's advertised `whistle.catalog/v1` catalog into
|
|
20
21
|
* Whistle commands that call that extension's own door. The client names no
|
|
21
22
|
* command: a new extension's catalog appears as new commands.
|
|
23
|
+
* @param contributions - Current admitted panels that own unanswered prompts.
|
|
22
24
|
*/
|
|
23
|
-
export declare function advertisedCommands(runtime: Pick<WhistleRuntime, "contribute">, session: Pick<ParakeetSession, "extension">, onInspect?: (entryId: string, commandId: string) => void): AdvertisedCommands;
|
|
25
|
+
export declare function advertisedCommands(runtime: Pick<WhistleRuntime, "contribute">, session: Pick<ParakeetSession, "extension">, contributions: () => readonly ClientContribution[], onInspect?: (entryId: string, commandId: string) => void): AdvertisedCommands;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
-
import { catalogCommands, parseCatalog } from "@fungi.computer/whistle/catalog";
|
|
2
|
+
import { catalogCommands, parseCatalog, parseCatalogStep, } from "@fungi.computer/whistle/catalog";
|
|
3
3
|
const cancelled = Schema.is(Schema.Struct({ cancelled: Schema.Literal(true) }));
|
|
4
4
|
/** A bare invocation views; one carrying action arguments (`/plan clear`) acts. */
|
|
5
5
|
const bare = Schema.is(Schema.UndefinedOr(Schema.Record(Schema.String, Schema.Never)));
|
|
@@ -7,9 +7,28 @@ const bare = Schema.is(Schema.UndefinedOr(Schema.Record(Schema.String, Schema.Ne
|
|
|
7
7
|
* Turn each running extension's advertised `whistle.catalog/v1` catalog into
|
|
8
8
|
* Whistle commands that call that extension's own door. The client names no
|
|
9
9
|
* command: a new extension's catalog appears as new commands.
|
|
10
|
+
* @param contributions - Current admitted panels that own unanswered prompts.
|
|
10
11
|
*/
|
|
11
|
-
export function advertisedCommands(runtime, session, onInspect) {
|
|
12
|
+
export function advertisedCommands(runtime, session, contributions, onInspect) {
|
|
12
13
|
const live = new Map();
|
|
14
|
+
const panelActions = (id, command) => contributions()
|
|
15
|
+
.find((entry) => entry.entryId === id)
|
|
16
|
+
?.value.panel.flatMap((row) => row.actions ?? [])
|
|
17
|
+
.filter((action) => action.id === command) ?? [];
|
|
18
|
+
const intent = (action) => JSON.stringify([action.id, action.input ?? {}]);
|
|
19
|
+
// A prompt borrows its panel intent, not the entire backend operation.
|
|
20
|
+
// Once answered, its pending response must retain its ordinary lifetime.
|
|
21
|
+
const prompts = new Set();
|
|
22
|
+
const refreshPrompts = () => {
|
|
23
|
+
for (const prompt of prompts) {
|
|
24
|
+
const visible = new Set(panelActions(prompt.entryId, prompt.commandId).map(intent));
|
|
25
|
+
if (live.has(prompt.entryId) &&
|
|
26
|
+
prompt.intents.every((id) => visible.has(id)))
|
|
27
|
+
continue;
|
|
28
|
+
prompt.execution.withdrawn = true;
|
|
29
|
+
prompt.controller.abort();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
13
32
|
const withdraw = (id) => {
|
|
14
33
|
live.get(id)?.lease.dispose();
|
|
15
34
|
live.delete(id);
|
|
@@ -31,7 +50,8 @@ export function advertisedCommands(runtime, session, onInspect) {
|
|
|
31
50
|
withdraw(id);
|
|
32
51
|
try {
|
|
33
52
|
const address = session.extension(id);
|
|
34
|
-
const
|
|
53
|
+
const catalog = parseCatalog(slot.catalog);
|
|
54
|
+
const commands = catalogCommands(catalog, (command, args) => address.call(command, args));
|
|
35
55
|
live.set(id, {
|
|
36
56
|
catalog: encoded,
|
|
37
57
|
lease: runtime.contribute({
|
|
@@ -40,7 +60,70 @@ export function advertisedCommands(runtime, session, onInspect) {
|
|
|
40
60
|
commands: commands.map((command) => ({
|
|
41
61
|
...command,
|
|
42
62
|
handler: async (input) => {
|
|
43
|
-
const
|
|
63
|
+
const actions = panelActions(id, command.id);
|
|
64
|
+
const execution = {
|
|
65
|
+
intents: actions.map(intent),
|
|
66
|
+
withdrawn: false,
|
|
67
|
+
};
|
|
68
|
+
// Invocation-local closures retain admitted identities only.
|
|
69
|
+
// A caller may share its signal and pass secret arguments.
|
|
70
|
+
const scopedCommand = catalogCommands(catalog, async (name, args) => {
|
|
71
|
+
const reply = await address.call(name, args);
|
|
72
|
+
const step = parseCatalogStep(reply);
|
|
73
|
+
if (step.type === "prompt") {
|
|
74
|
+
const selected = actions.filter((action) => Object.entries(action.input ?? {}).every(([key, value]) => JSON.stringify(Object.hasOwn(step.next.arguments, key)
|
|
75
|
+
? step.next.arguments[key]
|
|
76
|
+
: args[key]) === JSON.stringify(value)));
|
|
77
|
+
// Before selection, a chooser borrows all its candidates.
|
|
78
|
+
execution.intents = (selected.length > 0 ? selected : actions).map(intent);
|
|
79
|
+
}
|
|
80
|
+
return reply;
|
|
81
|
+
}).find((candidate) => candidate.id === command.id);
|
|
82
|
+
if (scopedCommand === undefined)
|
|
83
|
+
throw new Error("The advertised command is unavailable");
|
|
84
|
+
let scoped = input;
|
|
85
|
+
const interaction = input.interaction;
|
|
86
|
+
if (interaction !== undefined) {
|
|
87
|
+
const bound = {
|
|
88
|
+
...interaction,
|
|
89
|
+
prompt: (request, options) => {
|
|
90
|
+
const prompt = {
|
|
91
|
+
entryId: id,
|
|
92
|
+
commandId: command.id,
|
|
93
|
+
intents: execution.intents,
|
|
94
|
+
execution,
|
|
95
|
+
controller: new AbortController(),
|
|
96
|
+
};
|
|
97
|
+
prompts.add(prompt);
|
|
98
|
+
refreshPrompts();
|
|
99
|
+
return interaction
|
|
100
|
+
.prompt(request, {
|
|
101
|
+
signal: options?.signal === undefined
|
|
102
|
+
? prompt.controller.signal
|
|
103
|
+
: AbortSignal.any([
|
|
104
|
+
options.signal,
|
|
105
|
+
prompt.controller.signal,
|
|
106
|
+
]),
|
|
107
|
+
})
|
|
108
|
+
.finally(() => prompts.delete(prompt));
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
scoped = { ...scoped, interaction: bound };
|
|
112
|
+
}
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = await scopedCommand.handler(scoped);
|
|
116
|
+
}
|
|
117
|
+
catch (cause) {
|
|
118
|
+
if (execution.withdrawn)
|
|
119
|
+
return { cancelled: true };
|
|
120
|
+
throw cause;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
for (const prompt of prompts)
|
|
124
|
+
if (prompt.execution === execution)
|
|
125
|
+
prompts.delete(prompt);
|
|
126
|
+
}
|
|
44
127
|
if (input.origin !== "agent" &&
|
|
45
128
|
bare(input.arguments) &&
|
|
46
129
|
!cancelled(result) &&
|
|
@@ -62,11 +145,13 @@ export function advertisedCommands(runtime, session, onInspect) {
|
|
|
62
145
|
});
|
|
63
146
|
}
|
|
64
147
|
}
|
|
148
|
+
refreshPrompts();
|
|
65
149
|
return errors;
|
|
66
150
|
},
|
|
67
151
|
dispose: () => {
|
|
68
152
|
for (const id of live.keys())
|
|
69
153
|
withdraw(id);
|
|
154
|
+
refreshPrompts();
|
|
70
155
|
},
|
|
71
156
|
};
|
|
72
157
|
}
|
|
@@ -49,7 +49,7 @@ export function parakeetBuiltinCommands(runtime, options) {
|
|
|
49
49
|
category: "Advanced",
|
|
50
50
|
projections: {
|
|
51
51
|
help: { order: -2 },
|
|
52
|
-
slash: { aliases: ["keys"], order: -2 },
|
|
52
|
+
slash: { aliases: ["keys"], order: -2, consumesDraft: true },
|
|
53
53
|
},
|
|
54
54
|
handler: async (input) => {
|
|
55
55
|
const keys = keyBindings(runtime.snapshot());
|
|
@@ -6,13 +6,24 @@ export type ParakeetOpenInteraction = Readonly<{
|
|
|
6
6
|
key: number;
|
|
7
7
|
/** The renderer-neutral interaction store to draw. */
|
|
8
8
|
host: WhistleInteractionHost;
|
|
9
|
-
/** Run the same command again with the same input. */
|
|
10
|
-
retry: () => void;
|
|
11
9
|
}>;
|
|
12
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* An attention item shown beside the composer until it is resolved: something
|
|
12
|
+
* failed or waits on the person. Everything else is a toast.
|
|
13
|
+
*/
|
|
13
14
|
export type ParakeetNotice = Readonly<{
|
|
14
15
|
/** How to color it. */
|
|
15
|
-
tone: "info" | "
|
|
16
|
+
tone: "info" | "error";
|
|
17
|
+
/** What happened. */
|
|
18
|
+
message: string;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* A transient message a renderer shows as a toast. A command's confirmation of
|
|
22
|
+
* what the person just did is never one: it is an echo and is not announced.
|
|
23
|
+
*/
|
|
24
|
+
export type ParakeetToast = Readonly<{
|
|
25
|
+
/** How to color it. */
|
|
26
|
+
variant: "info" | "warning" | "error";
|
|
16
27
|
/** What happened. */
|
|
17
28
|
message: string;
|
|
18
29
|
}>;
|
|
@@ -27,7 +38,7 @@ export type ParakeetInteractionState = Readonly<{
|
|
|
27
38
|
/** Stable identity of the panel row being inspected. */
|
|
28
39
|
rowId: string;
|
|
29
40
|
}> | undefined;
|
|
30
|
-
/** The latest
|
|
41
|
+
/** The latest attention item: it stays until the person resolves it. */
|
|
31
42
|
notice?: ParakeetNotice | undefined;
|
|
32
43
|
}>;
|
|
33
44
|
/** Parakeet's command conversation store, shared by every renderer. */
|
|
@@ -40,8 +51,12 @@ export type ParakeetInteractions = Readonly<{
|
|
|
40
51
|
close: () => void;
|
|
41
52
|
/** Open or dismiss an admitted panel inspection without starting a command. */
|
|
42
53
|
inspect: (inspection: ParakeetInteractionState["inspection"]) => void;
|
|
43
|
-
/** Replace or clear the
|
|
54
|
+
/** Replace or clear the attention notice, starting a new command turn. */
|
|
44
55
|
report: (notice: ParakeetNotice | undefined) => void;
|
|
56
|
+
/** Announce a transient message to every toast subscriber. */
|
|
57
|
+
toast: (toast: ParakeetToast) => void;
|
|
58
|
+
/** Receive transient messages; the renderer decides how to show them. */
|
|
59
|
+
subscribeToasts: (listener: (toast: ParakeetToast) => void) => () => void;
|
|
45
60
|
/** Become the runtime's presenter until the returned disposer runs. */
|
|
46
61
|
attach: (present: WhistleRuntime["present"]) => () => void;
|
|
47
62
|
/** How the most recent interaction ended; a dismissal is not a failure to report. */
|
|
@@ -50,6 +65,6 @@ export type ParakeetInteractions = Readonly<{
|
|
|
50
65
|
/**
|
|
51
66
|
* Own how commands talk to the human. An interaction opens only when it asks
|
|
52
67
|
* a question, hands off to a URL or shows progress; a command that just
|
|
53
|
-
* reports
|
|
68
|
+
* reports information emits a toast; ordinary success echoes are silent.
|
|
54
69
|
*/
|
|
55
70
|
export declare function createParakeetInteractions(): ParakeetInteractions;
|
|
@@ -2,13 +2,23 @@ import { createWhistleInteractionHost, } from "@fungi.computer/whistle/interacti
|
|
|
2
2
|
/**
|
|
3
3
|
* Own how commands talk to the human. An interaction opens only when it asks
|
|
4
4
|
* a question, hands off to a URL or shows progress; a command that just
|
|
5
|
-
* reports
|
|
5
|
+
* reports information emits a toast; ordinary success echoes are silent.
|
|
6
6
|
*/
|
|
7
7
|
export function createParakeetInteractions() {
|
|
8
8
|
let state = {};
|
|
9
9
|
let sequence = 0;
|
|
10
10
|
let lastEnd;
|
|
11
11
|
const listeners = new Set();
|
|
12
|
+
const toastListeners = new Set();
|
|
13
|
+
const toast = (next) => {
|
|
14
|
+
for (const listener of toastListeners)
|
|
15
|
+
listener(next);
|
|
16
|
+
};
|
|
17
|
+
/** Information is a toast; a success only echoes what the person did. */
|
|
18
|
+
const announce = (message) => {
|
|
19
|
+
if (message.type === "info")
|
|
20
|
+
toast({ variant: "info", message: message.message });
|
|
21
|
+
};
|
|
12
22
|
const publish = (next) => {
|
|
13
23
|
state = next;
|
|
14
24
|
for (const listener of listeners)
|
|
@@ -18,9 +28,10 @@ export function createParakeetInteractions() {
|
|
|
18
28
|
if (state.open?.key === key)
|
|
19
29
|
publish({ ...state, open: undefined });
|
|
20
30
|
};
|
|
21
|
-
const presenter = ({ title
|
|
31
|
+
const presenter = ({ title }) => {
|
|
22
32
|
const key = ++sequence;
|
|
23
33
|
let shown = false;
|
|
34
|
+
let hadRetryableFailure = false;
|
|
24
35
|
const host = createWhistleInteractionHost({
|
|
25
36
|
title,
|
|
26
37
|
onEnd: (end) => {
|
|
@@ -32,35 +43,41 @@ export function createParakeetInteractions() {
|
|
|
32
43
|
...state,
|
|
33
44
|
notice: { tone: "error", message: end.message },
|
|
34
45
|
});
|
|
35
|
-
else if (message !== undefined
|
|
36
|
-
|
|
37
|
-
...state,
|
|
38
|
-
notice: {
|
|
39
|
-
tone: message.type === "success" ? "success" : "info",
|
|
40
|
-
message: message.message,
|
|
41
|
-
},
|
|
42
|
-
});
|
|
46
|
+
else if (message !== undefined)
|
|
47
|
+
announce(message);
|
|
43
48
|
return;
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
// A retried result stays in the same notice row until Done. A
|
|
51
|
+
// progress-only ending is not a completion notice to manufacture.
|
|
52
|
+
if (end.status === "handled" &&
|
|
53
|
+
hadRetryableFailure &&
|
|
54
|
+
message !== undefined &&
|
|
55
|
+
message.type !== "progress")
|
|
56
|
+
return;
|
|
57
|
+
const closing = host.snapshot().closing;
|
|
58
|
+
if (end.status === "cancelled" && closing !== undefined)
|
|
59
|
+
announce(closing);
|
|
60
|
+
if (end.status === "handled" && message !== undefined)
|
|
61
|
+
announce(message);
|
|
62
|
+
// Failed interactions retain their failure UI; ordinary results never
|
|
63
|
+
// leave a dialog behind just to confirm the completed action.
|
|
64
|
+
if (end.status !== "failed")
|
|
50
65
|
closeKey(key);
|
|
51
66
|
},
|
|
52
67
|
});
|
|
53
68
|
const unsubscribe = host.subscribe(() => {
|
|
69
|
+
const current = host.snapshot();
|
|
70
|
+
if (current.retryable)
|
|
71
|
+
hadRetryableFailure = true;
|
|
54
72
|
if (shown)
|
|
55
73
|
return;
|
|
56
|
-
const current = host.snapshot();
|
|
57
74
|
if (current.prompt === undefined &&
|
|
58
75
|
current.handoff === undefined &&
|
|
59
76
|
current.message?.type !== "progress")
|
|
60
77
|
return;
|
|
61
78
|
shown = true;
|
|
62
79
|
const previous = state.open;
|
|
63
|
-
publish({ ...state, open: { key, host
|
|
80
|
+
publish({ ...state, open: { key, host }, notice: undefined });
|
|
64
81
|
previous?.host.cancel();
|
|
65
82
|
});
|
|
66
83
|
const session = host.begin();
|
|
@@ -95,6 +112,13 @@ export function createParakeetInteractions() {
|
|
|
95
112
|
lastEnd = undefined;
|
|
96
113
|
publish({ ...state, notice });
|
|
97
114
|
},
|
|
115
|
+
toast,
|
|
116
|
+
subscribeToasts: (listener) => {
|
|
117
|
+
toastListeners.add(listener);
|
|
118
|
+
return () => {
|
|
119
|
+
toastListeners.delete(listener);
|
|
120
|
+
};
|
|
121
|
+
},
|
|
98
122
|
attach: (present) => present(presenter),
|
|
99
123
|
lastEnd: () => lastEnd,
|
|
100
124
|
};
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare function isCancelledOutcome(outcome: WhistleExecutionOutcome): bo
|
|
|
6
6
|
/**
|
|
7
7
|
* Run one composer draft: a prompt goes to `send`, a `/command` runs through
|
|
8
8
|
* Whistle. A grammar that asks for a request id keeps the same id across
|
|
9
|
-
* retries of the same draft until the command is handled.
|
|
9
|
+
* retries of the same draft until the command is handled. A slash projection
|
|
10
|
+
* can request draft consumption before dispatch, including while a dialog is open.
|
|
10
11
|
*/
|
|
11
|
-
export declare function runSlashDraft(draft: string, commands: SlashRuntime, send: () => void, origin: WhistleExecutionOrigin): Promise<WhistleExecutionOutcome | undefined>;
|
|
12
|
+
export declare function runSlashDraft(draft: string, commands: SlashRuntime, send: () => void, origin: WhistleExecutionOrigin, consumeDraft?: () => void): Promise<WhistleExecutionOutcome | undefined>;
|
package/dist/commands/run.js
CHANGED
|
@@ -12,9 +12,10 @@ export function isCancelledOutcome(outcome) {
|
|
|
12
12
|
/**
|
|
13
13
|
* Run one composer draft: a prompt goes to `send`, a `/command` runs through
|
|
14
14
|
* Whistle. A grammar that asks for a request id keeps the same id across
|
|
15
|
-
* retries of the same draft until the command is handled.
|
|
15
|
+
* retries of the same draft until the command is handled. A slash projection
|
|
16
|
+
* can request draft consumption before dispatch, including while a dialog is open.
|
|
16
17
|
*/
|
|
17
|
-
export async function runSlashDraft(draft, commands, send, origin) {
|
|
18
|
+
export async function runSlashDraft(draft, commands, send, origin, consumeDraft) {
|
|
18
19
|
const parsed = parseSlashDraft(slashRegistry(commands.snapshot()), draft);
|
|
19
20
|
if (parsed.kind === "prompt") {
|
|
20
21
|
send();
|
|
@@ -26,6 +27,8 @@ export async function runSlashDraft(draft, commands, send, origin) {
|
|
|
26
27
|
error: { code: "invalid_arguments", message: parsed.message },
|
|
27
28
|
};
|
|
28
29
|
const { command, requestIdField } = parsed;
|
|
30
|
+
if (command.consumesDraft && command.availability.status === "available")
|
|
31
|
+
consumeDraft?.();
|
|
29
32
|
if (parsed.arguments === undefined)
|
|
30
33
|
return commands.execute(command.commandId, { origin });
|
|
31
34
|
if (requestIdField === undefined)
|
package/dist/commands/slash.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type SlashCommand = Readonly<{
|
|
|
5
5
|
alias: string;
|
|
6
6
|
/** Canonical Whistle command identity. */
|
|
7
7
|
commandId: string;
|
|
8
|
+
/** Consume this draft on dispatch, including for a dialog or refusal. */
|
|
9
|
+
consumesDraft?: boolean | undefined;
|
|
8
10
|
/** Human-facing title. */
|
|
9
11
|
title: string;
|
|
10
12
|
/** Optional longer explanation. */
|
package/dist/commands/slash.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/** Human key labels from Whistle bindings, with the platform's modifier name. */
|
|
2
2
|
export function keyLabel(binding, mac = /Mac|iPhone|iPad/u.test(globalThis.navigator.userAgent)) {
|
|
3
|
+
if (binding.startsWith("<leader>"))
|
|
4
|
+
return `Ctrl K ${binding.slice("<leader>".length)}`;
|
|
3
5
|
return binding
|
|
4
6
|
.split(" ")
|
|
5
|
-
.map((chord
|
|
6
|
-
if (chord === "<leader>")
|
|
7
|
-
return "Ctrl K";
|
|
8
|
-
if (index > 0 && binding.startsWith("<leader> "))
|
|
9
|
-
return chord;
|
|
7
|
+
.map((chord) => {
|
|
10
8
|
const parts = chord
|
|
11
9
|
.split("+")
|
|
12
10
|
.map((part) => part === "mod"
|
|
@@ -38,6 +36,7 @@ export function slashRegistry(snapshot) {
|
|
|
38
36
|
category: row.category,
|
|
39
37
|
order: row.order,
|
|
40
38
|
grammar: row.arguments,
|
|
39
|
+
consumesDraft: row.consumesDraft,
|
|
41
40
|
keys: keys.get(row.commandId) ?? [],
|
|
42
41
|
availability: row.availability,
|
|
43
42
|
}));
|
|
@@ -135,6 +134,7 @@ function rank(entries, text, query, order) {
|
|
|
135
134
|
text(left.entry).localeCompare(text(right.entry)))
|
|
136
135
|
.map(({ entry, match }) => ({ entry, indices: match.indices }));
|
|
137
136
|
}
|
|
137
|
+
const commonAliases = ["help", "login", "model"];
|
|
138
138
|
/** Rank completions for one draft; renderers draw exactly these rows. */
|
|
139
139
|
export function completeSlash(registry, draft, options = {}) {
|
|
140
140
|
if (!draft.startsWith("/") || draft.includes("\n"))
|
|
@@ -144,11 +144,17 @@ export function completeSlash(registry, draft, options = {}) {
|
|
|
144
144
|
const query = draft.slice(1);
|
|
145
145
|
const items = rank(registry.filter((command) => command.availability.status === "available" &&
|
|
146
146
|
(query.length === 0
|
|
147
|
-
? command.category === (options.idleCategory ?? "Conversation")
|
|
147
|
+
? command.category === (options.idleCategory ?? "Conversation") ||
|
|
148
|
+
commonAliases.includes(command.alias)
|
|
148
149
|
: command.category !== "Advanced" ||
|
|
149
150
|
command.alias
|
|
150
151
|
.toLocaleLowerCase()
|
|
151
|
-
.startsWith(query.toLocaleLowerCase()))), (command) => command.alias, query, (command) =>
|
|
152
|
+
.startsWith(query.toLocaleLowerCase()))), (command) => command.alias, query, (command) => {
|
|
153
|
+
const common = commonAliases.indexOf(command.alias);
|
|
154
|
+
return query.length === 0 && common >= 0
|
|
155
|
+
? common - commonAliases.length
|
|
156
|
+
: command.order;
|
|
157
|
+
}).map(({ entry, indices }) => ({
|
|
152
158
|
command: entry,
|
|
153
159
|
label: `/${entry.alias}`,
|
|
154
160
|
matches: indices.map((index) => index + 1),
|
|
@@ -70,7 +70,7 @@ export function useSlashCommands(options) {
|
|
|
70
70
|
lease.dispose();
|
|
71
71
|
};
|
|
72
72
|
}, [commands, enabled, store, hasCommandPalette]);
|
|
73
|
-
const advertised = useMemo(() => advertisedCommands(commands, session, (entryId, commandId) => {
|
|
73
|
+
const advertised = useMemo(() => advertisedCommands(commands, session, () => contributionsRef.current, (entryId, commandId) => {
|
|
74
74
|
const row = contributionsRef.current
|
|
75
75
|
.find((entry) => entry.entryId === entryId)
|
|
76
76
|
?.value.panel.find((row) => row.inspection?.commandId === commandId);
|
|
@@ -91,7 +91,7 @@ export function useSlashCommands(options) {
|
|
|
91
91
|
tone: "error",
|
|
92
92
|
message: `${error.extensionId} commands are unavailable: ${error.message}`,
|
|
93
93
|
});
|
|
94
|
-
}, [advertised, enabled, slots, store]);
|
|
94
|
+
}, [advertised, enabled, slots, contributions, store]);
|
|
95
95
|
const registry = useMemo(() => slashRegistry(snapshot), [snapshot]);
|
|
96
96
|
const [dismissed, setDismissed] = useState();
|
|
97
97
|
const [highlighted, setHighlighted] = useState({ draft: "", index: 0 });
|
|
@@ -123,9 +123,9 @@ export function useSlashCommands(options) {
|
|
|
123
123
|
store.report(undefined);
|
|
124
124
|
setDismissed(submitted);
|
|
125
125
|
void current
|
|
126
|
-
.submitCommand((captured) => runSlashDraft(captured, commands, () => {
|
|
126
|
+
.submitCommand((captured, consumeDraft) => runSlashDraft(captured, commands, () => {
|
|
127
127
|
current.send(delivery);
|
|
128
|
-
}, origin))
|
|
128
|
+
}, origin, consumeDraft))
|
|
129
129
|
.then((outcome) => {
|
|
130
130
|
const message = failureMessage(outcome);
|
|
131
131
|
if (message !== undefined &&
|
|
@@ -2,26 +2,23 @@ import type { Avatar } from "@fungi.computer/sprite/avatar";
|
|
|
2
2
|
/** What the head is doing: at rest, working something out, or talking. */
|
|
3
3
|
export type AgentHeadActivity = "idle" | "thinking" | "speaking";
|
|
4
4
|
/**
|
|
5
|
-
* An Agent's head at avatar size
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* live canvas, so a long rail is not a wall of animation. A render style
|
|
11
|
-
* other than "plain" is a CSS filter over the live canvas, not part of the
|
|
12
|
-
* captured pixels, so a styled head always stays live.
|
|
5
|
+
* An Agent's head at avatar size, drawn from the Agent's own avatar (a
|
|
6
|
+
* property of the Agent record, never of a conversation). The letter shows
|
|
7
|
+
* until a frame is ready. The live
|
|
8
|
+
* canvas stays mounted and is paused unless `thinking` or `speaking`, so an
|
|
9
|
+
* idle head shows its real look without animating.
|
|
13
10
|
*/
|
|
14
11
|
export declare const AgentHead: import("react").NamedExoticComponent<{
|
|
12
|
+
/** External animation clock in milliseconds; omitted uses live motion. */
|
|
13
|
+
timeMs?: number;
|
|
15
14
|
/** Motion presented by the head; defaults to idle. */
|
|
16
15
|
activity?: AgentHeadActivity;
|
|
17
|
-
/** The Agent's
|
|
18
|
-
avatar
|
|
16
|
+
/** The Agent's avatar, from its Agent record. */
|
|
17
|
+
avatar: Avatar;
|
|
19
18
|
/** Host classes applied to the head container. */
|
|
20
19
|
className?: string;
|
|
21
20
|
/** The name whose first letter stands in until a frame is ready. */
|
|
22
21
|
name: string;
|
|
23
|
-
/** Stable identity that picks the fallback cap colour. */
|
|
24
|
-
seed: string;
|
|
25
22
|
/** Width and height in pixels. */
|
|
26
23
|
size?: number;
|
|
27
24
|
}>;
|