@fungi.computer/parakeet 0.3.0 → 0.5.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/dist/account-commands.js +20 -7
- package/dist/client-contributions.d.ts +1 -0
- package/dist/commands/awaiting-driver.d.ts +20 -0
- package/dist/commands/awaiting-driver.js +121 -0
- package/dist/commands/builtins.js +6 -4
- package/dist/commands/interactions.d.ts +25 -0
- package/dist/commands/interactions.js +76 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/slash.d.ts +7 -3
- package/dist/commands/slash.js +2 -0
- package/dist/commands/use-slash-commands.d.ts +13 -0
- package/dist/commands/use-slash-commands.js +58 -1
- package/dist/components/ChatComposer.d.ts +2 -0
- package/dist/components/ChatComposer.js +150 -38
- package/dist/components/ClientRows.js +42 -26
- package/dist/components/CompletionMenu.d.ts +54 -0
- package/dist/components/CompletionMenu.js +52 -0
- package/dist/components/DecisionComposer.d.ts +30 -0
- package/dist/components/DecisionComposer.js +146 -0
- package/dist/components/InteractionPanel.d.ts +10 -1
- package/dist/components/InteractionPanel.js +24 -5
- package/dist/components/RunFailureNotice.js +11 -2
- package/dist/components/SlashMenu.d.ts +1 -3
- package/dist/components/SlashMenu.js +5 -13
- package/dist/components/TimelineItem.d.ts +1 -0
- package/dist/components/TimelineItem.js +3 -3
- package/dist/components/ToolGroup.d.ts +1 -0
- package/dist/components/ToolGroup.js +7 -7
- package/dist/conversation.d.ts +5 -1
- package/dist/conversation.js +72 -20
- package/dist/core/approval-decision.d.ts +198 -0
- package/dist/core/approval-decision.js +291 -0
- package/dist/core/awaiting-answer.d.ts +229 -0
- package/dist/core/awaiting-answer.js +303 -0
- package/dist/core/build-timeline.js +56 -5
- package/dist/core/client-actions.d.ts +1 -0
- package/dist/core/client-actions.js +8 -21
- package/dist/core/draft-storage.d.ts +4 -0
- package/dist/core/draft-storage.js +12 -5
- package/dist/core/picker-text.d.ts +7 -0
- package/dist/core/picker-text.js +24 -0
- package/dist/core/session-query.d.ts +8 -0
- package/dist/core/session-query.js +13 -11
- package/dist/core/session-view-state.d.ts +1 -1
- package/dist/core/session-view-state.js +14 -6
- package/dist/headless.d.ts +7 -2
- package/dist/headless.js +6 -1
- package/dist/model-commands.js +14 -15
- package/dist/parakeet.d.ts +6 -2
- package/dist/parakeet.js +15 -6
- package/dist/session-commands.js +6 -2
- package/dist/store/types.d.ts +5 -1
- package/dist/styles.css +407 -113
- package/dist/use-approval-awaiting.d.ts +22 -0
- package/dist/use-approval-awaiting.js +81 -0
- package/dist/use-decision.d.ts +20 -0
- package/dist/use-decision.js +79 -0
- package/docs/api/commands/slash.md +30 -26
- package/docs/api/headless.md +822 -41
- package/docs/api/index.md +13 -4
- package/package.json +15 -8
- package/src/completion-menu.css +71 -0
- package/src/styles.css +174 -46
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
|
})));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ClientContribution } from "../core/client-contributions.js";
|
|
2
|
+
import type { ParakeetInteractions } from "./interactions.js";
|
|
3
|
+
import type { ParakeetCommandRuntime } from "./use-slash-commands.js";
|
|
4
|
+
/**
|
|
5
|
+
* Open the conversation of every decision the Agent is waiting on, so its
|
|
6
|
+
* first prompt can take over the composer without the person asking. Each row
|
|
7
|
+
* runs its own advertised command once; the row leaving cancels it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useAwaitingDriver(options: Readonly<{
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
commands: ParakeetCommandRuntime;
|
|
12
|
+
store: ParakeetInteractions;
|
|
13
|
+
contributions: readonly ClientContribution[];
|
|
14
|
+
/** Session observation is live; nothing opens while it is not. */
|
|
15
|
+
online: boolean;
|
|
16
|
+
/** Counts requests to answer, which restart a conversation that failed. */
|
|
17
|
+
requests: number;
|
|
18
|
+
/** Changes whenever the runtime's commands do, so a late command is found. */
|
|
19
|
+
commandsVersion: unknown;
|
|
20
|
+
}>): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createWhistleInteractionHost } from "@fungi.computer/whistle/interaction";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
/** Footer rows whose action asks the person to decide in the composer. */
|
|
4
|
+
function awaitingRows(contributions) {
|
|
5
|
+
return contributions.flatMap(({ entryId, value }) => value.panel.flatMap((row) => (row.actions ?? [])
|
|
6
|
+
.filter((action) => action.awaiting !== undefined)
|
|
7
|
+
.map((action) => ({
|
|
8
|
+
key: JSON.stringify([entryId, row.id]),
|
|
9
|
+
command: { namespace: entryId, id: action.id },
|
|
10
|
+
title: row.title,
|
|
11
|
+
action,
|
|
12
|
+
}))));
|
|
13
|
+
}
|
|
14
|
+
function failure(outcome) {
|
|
15
|
+
if (outcome.status === "handled")
|
|
16
|
+
return undefined;
|
|
17
|
+
if (outcome.status === "failed")
|
|
18
|
+
return outcome.error.message;
|
|
19
|
+
if (outcome.status === "unavailable")
|
|
20
|
+
return outcome.reason;
|
|
21
|
+
return `The question could not be opened (${outcome.status}).`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Open the conversation of every decision the Agent is waiting on, so its
|
|
25
|
+
* first prompt can take over the composer without the person asking. Each row
|
|
26
|
+
* runs its own advertised command once; the row leaving cancels it.
|
|
27
|
+
*/
|
|
28
|
+
export function useAwaitingDriver(options) {
|
|
29
|
+
const { enabled, commands, store, contributions, online, requests, commandsVersion, } = options;
|
|
30
|
+
const running = useRef(new Map());
|
|
31
|
+
const handled = useRef(requests);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const wanted = new Map((enabled && online ? awaitingRows(contributions) : []).map((row) => [
|
|
34
|
+
row.key,
|
|
35
|
+
row,
|
|
36
|
+
]));
|
|
37
|
+
const retry = handled.current !== requests;
|
|
38
|
+
handled.current = requests;
|
|
39
|
+
for (const [key, entry] of running.current)
|
|
40
|
+
if (!wanted.has(key) || (retry && entry.failed())) {
|
|
41
|
+
entry.stop();
|
|
42
|
+
running.current.delete(key);
|
|
43
|
+
}
|
|
44
|
+
for (const row of wanted.values()) {
|
|
45
|
+
if (running.current.has(row.key))
|
|
46
|
+
continue;
|
|
47
|
+
const host = createWhistleInteractionHost({ title: row.title });
|
|
48
|
+
const session = host.begin();
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
let leave;
|
|
51
|
+
let failed = false;
|
|
52
|
+
const unsubscribe = host.subscribe(() => {
|
|
53
|
+
if (leave === undefined && host.snapshot().prompt !== undefined)
|
|
54
|
+
leave = store.awaiting.add({
|
|
55
|
+
key: row.key,
|
|
56
|
+
kind: "question",
|
|
57
|
+
arrivedAt: Date.now(),
|
|
58
|
+
host,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
const base = {
|
|
62
|
+
origin: "menu",
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
interaction: session.interaction,
|
|
65
|
+
};
|
|
66
|
+
void commands
|
|
67
|
+
.execute(row.command, row.action.input === undefined
|
|
68
|
+
? base
|
|
69
|
+
: { ...base, arguments: row.action.input })
|
|
70
|
+
.then((outcome) => {
|
|
71
|
+
unsubscribe();
|
|
72
|
+
leave?.();
|
|
73
|
+
if (outcome.status === "missing" ||
|
|
74
|
+
outcome.status === "unavailable") {
|
|
75
|
+
// Its command is not advertised yet; start over when it is.
|
|
76
|
+
session.settle({ status: "cancelled" });
|
|
77
|
+
if (running.current.get(row.key)?.failed() === false)
|
|
78
|
+
running.current.delete(row.key);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const message = failure(outcome);
|
|
82
|
+
if (message === undefined) {
|
|
83
|
+
session.settle({ status: "handled" });
|
|
84
|
+
const notice = host.snapshot().message;
|
|
85
|
+
if (notice?.type === "info")
|
|
86
|
+
store.toast({ variant: "info", message: notice.message });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (controller.signal.aborted) {
|
|
90
|
+
session.settle({ status: "cancelled" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
failed = true;
|
|
94
|
+
session.settle({ status: "failed", message });
|
|
95
|
+
store.report({ tone: "error", message });
|
|
96
|
+
});
|
|
97
|
+
running.current.set(row.key, {
|
|
98
|
+
failed: () => failed,
|
|
99
|
+
stop: () => {
|
|
100
|
+
unsubscribe();
|
|
101
|
+
leave?.();
|
|
102
|
+
controller.abort();
|
|
103
|
+
host.cancel();
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}, [
|
|
108
|
+
enabled,
|
|
109
|
+
online,
|
|
110
|
+
contributions,
|
|
111
|
+
commands,
|
|
112
|
+
store,
|
|
113
|
+
requests,
|
|
114
|
+
commandsVersion,
|
|
115
|
+
]);
|
|
116
|
+
useEffect(() => () => {
|
|
117
|
+
for (const entry of running.current.values())
|
|
118
|
+
entry.stop();
|
|
119
|
+
running.current.clear();
|
|
120
|
+
}, [commands, store, enabled]);
|
|
121
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { keyBindings, slashRegistry } from "./slash.js";
|
|
2
2
|
/** Run a chosen command after the choosing command has settled. */
|
|
3
|
-
function runAfter(runtime,
|
|
3
|
+
function runAfter(runtime, command, origin) {
|
|
4
|
+
if (command === undefined)
|
|
5
|
+
return;
|
|
4
6
|
setTimeout(() => {
|
|
5
|
-
void runtime.execute(
|
|
7
|
+
void runtime.execute(command, { origin });
|
|
6
8
|
}, 0);
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
@@ -39,7 +41,7 @@ export function parakeetBuiltinCommands(runtime, options) {
|
|
|
39
41
|
: `${command.title} · ${command.keys.join(", ")}`,
|
|
40
42
|
})),
|
|
41
43
|
});
|
|
42
|
-
runAfter(runtime, chosen, input.origin);
|
|
44
|
+
runAfter(runtime, commands.find((command) => command.commandId === chosen)?.command, input.origin);
|
|
43
45
|
return undefined;
|
|
44
46
|
},
|
|
45
47
|
},
|
|
@@ -71,7 +73,7 @@ export function parakeetBuiltinCommands(runtime, options) {
|
|
|
71
73
|
description: row.keys,
|
|
72
74
|
})),
|
|
73
75
|
});
|
|
74
|
-
runAfter(runtime, chosen, input.origin);
|
|
76
|
+
runAfter(runtime, keys.find((row) => row.commandId === chosen)?.command, input.origin);
|
|
75
77
|
return undefined;
|
|
76
78
|
},
|
|
77
79
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { WhistleInteractionEnd, WhistleRuntime } from "@fungi.computer/whistle";
|
|
2
2
|
import { type WhistleInteractionHost } from "@fungi.computer/whistle/interaction";
|
|
3
|
+
import type { AwaitingItem } from "../core/awaiting-answer.js";
|
|
3
4
|
/** One interaction a renderer should draw as a dialog or panel. */
|
|
4
5
|
export type ParakeetOpenInteraction = Readonly<{
|
|
5
6
|
/** Changes whenever a different interaction opens. */
|
|
@@ -27,8 +28,21 @@ export type ParakeetToast = Readonly<{
|
|
|
27
28
|
/** What happened. */
|
|
28
29
|
message: string;
|
|
29
30
|
}>;
|
|
31
|
+
/** The decisions waiting on the person, which the composer takes over for. */
|
|
32
|
+
export type ParakeetAwaiting = Readonly<{
|
|
33
|
+
/** Every waiting decision, in the order it joined. */
|
|
34
|
+
items: readonly AwaitingItem[];
|
|
35
|
+
/** Keys the person postponed with Later; they wait in the footer row. */
|
|
36
|
+
later: ReadonlySet<string>;
|
|
37
|
+
/** The person asked for the composer even though a draft is in it. */
|
|
38
|
+
forced: boolean;
|
|
39
|
+
/** Counts each request to answer, so a stalled item can start over. */
|
|
40
|
+
requests: number;
|
|
41
|
+
}>;
|
|
30
42
|
/** Everything a renderer draws for command conversations. */
|
|
31
43
|
export type ParakeetInteractionState = Readonly<{
|
|
44
|
+
/** Decisions that take over the composer instead of opening a dialog. */
|
|
45
|
+
awaiting: ParakeetAwaiting;
|
|
32
46
|
/** The interaction that needs the human, when one does. */
|
|
33
47
|
open?: ParakeetOpenInteraction | undefined;
|
|
34
48
|
/** A manually opened, live panel row from an admitted client entry. */
|
|
@@ -61,6 +75,17 @@ export type ParakeetInteractions = Readonly<{
|
|
|
61
75
|
attach: (present: WhistleRuntime["present"]) => () => void;
|
|
62
76
|
/** How the most recent interaction ended; a dismissal is not a failure to report. */
|
|
63
77
|
lastEnd: () => WhistleInteractionEnd["status"] | undefined;
|
|
78
|
+
/** The queue of decisions the composer answers. */
|
|
79
|
+
awaiting: Readonly<{
|
|
80
|
+
/** Join the queue; the returned function leaves it. */
|
|
81
|
+
add: (item: AwaitingItem) => () => void;
|
|
82
|
+
/** Leave the queue. */
|
|
83
|
+
remove: (key: string) => void;
|
|
84
|
+
/** Postpone one decision: it leaves the composer but stays waiting. */
|
|
85
|
+
later: (key: string) => void;
|
|
86
|
+
/** Bring back the earliest postponed decision, even over a draft. */
|
|
87
|
+
focus: () => void;
|
|
88
|
+
}>;
|
|
64
89
|
}>;
|
|
65
90
|
/**
|
|
66
91
|
* Own how commands talk to the human. An interaction opens only when it asks
|
|
@@ -5,7 +5,14 @@ import { createWhistleInteractionHost, } from "@fungi.computer/whistle/interacti
|
|
|
5
5
|
* reports information emits a toast; ordinary success echoes are silent.
|
|
6
6
|
*/
|
|
7
7
|
export function createParakeetInteractions() {
|
|
8
|
-
let state = {
|
|
8
|
+
let state = {
|
|
9
|
+
awaiting: {
|
|
10
|
+
items: [],
|
|
11
|
+
later: new Set(),
|
|
12
|
+
forced: false,
|
|
13
|
+
requests: 0,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
9
16
|
let sequence = 0;
|
|
10
17
|
let lastEnd;
|
|
11
18
|
const listeners = new Set();
|
|
@@ -24,6 +31,30 @@ export function createParakeetInteractions() {
|
|
|
24
31
|
for (const listener of listeners)
|
|
25
32
|
listener();
|
|
26
33
|
};
|
|
34
|
+
const setAwaiting = (next) => {
|
|
35
|
+
publish({ ...state, awaiting: { ...state.awaiting, ...next } });
|
|
36
|
+
};
|
|
37
|
+
const removeAwaiting = (key) => {
|
|
38
|
+
const { items, later } = state.awaiting;
|
|
39
|
+
if (!items.some((item) => item.key === key))
|
|
40
|
+
return;
|
|
41
|
+
setAwaiting({
|
|
42
|
+
items: items.filter((item) => item.key !== key),
|
|
43
|
+
later: new Set([...later].filter((postponed) => postponed !== key)),
|
|
44
|
+
forced: false,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const addAwaiting = (item) => {
|
|
48
|
+
setAwaiting({
|
|
49
|
+
items: [
|
|
50
|
+
...state.awaiting.items.filter(({ key }) => key !== item.key),
|
|
51
|
+
item,
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
removeAwaiting(item.key);
|
|
56
|
+
};
|
|
57
|
+
};
|
|
27
58
|
const closeKey = (key) => {
|
|
28
59
|
if (state.open?.key === key)
|
|
29
60
|
publish({ ...state, open: undefined });
|
|
@@ -36,6 +67,7 @@ export function createParakeetInteractions() {
|
|
|
36
67
|
title,
|
|
37
68
|
onEnd: (end) => {
|
|
38
69
|
lastEnd = end.status;
|
|
70
|
+
leaveQueue?.();
|
|
39
71
|
const message = host.snapshot().message;
|
|
40
72
|
if (!shown) {
|
|
41
73
|
if (end.status === "failed")
|
|
@@ -65,11 +97,28 @@ export function createParakeetInteractions() {
|
|
|
65
97
|
closeKey(key);
|
|
66
98
|
},
|
|
67
99
|
});
|
|
100
|
+
let leaveQueue;
|
|
68
101
|
const unsubscribe = host.subscribe(() => {
|
|
69
102
|
const current = host.snapshot();
|
|
70
103
|
if (current.retryable)
|
|
71
104
|
hadRetryableFailure = true;
|
|
72
|
-
|
|
105
|
+
// A decision is the composer's, never a dialog's, even when the same
|
|
106
|
+
// conversation reached it through a dialog (a row action chooser).
|
|
107
|
+
if (leaveQueue === undefined &&
|
|
108
|
+
current.prompt?.request.type === "decision") {
|
|
109
|
+
if (shown) {
|
|
110
|
+
shown = false;
|
|
111
|
+
closeKey(key);
|
|
112
|
+
}
|
|
113
|
+
leaveQueue = addAwaiting({
|
|
114
|
+
key: `interaction:${String(key)}`,
|
|
115
|
+
kind: "question",
|
|
116
|
+
arrivedAt: Date.now(),
|
|
117
|
+
host,
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (leaveQueue !== undefined || shown)
|
|
73
122
|
return;
|
|
74
123
|
if (current.prompt === undefined &&
|
|
75
124
|
current.handoff === undefined &&
|
|
@@ -121,5 +170,30 @@ export function createParakeetInteractions() {
|
|
|
121
170
|
},
|
|
122
171
|
attach: (present) => present(presenter),
|
|
123
172
|
lastEnd: () => lastEnd,
|
|
173
|
+
awaiting: {
|
|
174
|
+
add: addAwaiting,
|
|
175
|
+
remove: removeAwaiting,
|
|
176
|
+
later: (key) => {
|
|
177
|
+
if (!state.awaiting.items.some((item) => item.key === key))
|
|
178
|
+
return;
|
|
179
|
+
setAwaiting({
|
|
180
|
+
later: new Set([...state.awaiting.later, key]),
|
|
181
|
+
forced: false,
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
focus: () => {
|
|
185
|
+
const { items, later, requests } = state.awaiting;
|
|
186
|
+
const earliest = [...items]
|
|
187
|
+
.sort((left, right) => left.arrivedAt - right.arrivedAt)
|
|
188
|
+
.find((item) => later.has(item.key));
|
|
189
|
+
setAwaiting({
|
|
190
|
+
later: earliest === undefined
|
|
191
|
+
? later
|
|
192
|
+
: new Set([...later].filter((key) => key !== earliest.key)),
|
|
193
|
+
forced: items.length > 0,
|
|
194
|
+
requests: requests + 1,
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
},
|
|
124
198
|
};
|
|
125
199
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -30,9 +30,9 @@ export async function runSlashDraft(draft, commands, send, origin, consumeDraft)
|
|
|
30
30
|
if (command.consumesDraft && command.availability.status === "available")
|
|
31
31
|
consumeDraft?.();
|
|
32
32
|
if (parsed.arguments === undefined)
|
|
33
|
-
return commands.execute(command.
|
|
33
|
+
return commands.execute(command.command, { origin });
|
|
34
34
|
if (requestIdField === undefined)
|
|
35
|
-
return commands.execute(command.
|
|
35
|
+
return commands.execute(command.command, {
|
|
36
36
|
origin,
|
|
37
37
|
arguments: parsed.arguments,
|
|
38
38
|
});
|
|
@@ -47,7 +47,7 @@ export async function runSlashDraft(draft, commands, send, origin, consumeDraft)
|
|
|
47
47
|
};
|
|
48
48
|
states.set(command.commandId, state);
|
|
49
49
|
const running = commands
|
|
50
|
-
.execute(command.
|
|
50
|
+
.execute(command.command, {
|
|
51
51
|
origin,
|
|
52
52
|
arguments: { ...parsed.arguments, [requestIdField]: state.requestId },
|
|
53
53
|
})
|
package/dist/commands/slash.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { WhistleAvailability, WhistleJsonObject, WhistleSlashArguments, WhistleSlashWord, WhistleSnapshot } from "@fungi.computer/whistle";
|
|
1
|
+
import type { WhistleAvailability, WhistleCommandKey, WhistleJsonObject, WhistleSlashArguments, WhistleSlashWord, WhistleSnapshot } from "@fungi.computer/whistle";
|
|
2
2
|
/** One slash command as every Parakeet renderer lists it. */
|
|
3
3
|
export type SlashCommand = Readonly<{
|
|
4
4
|
/** The alias typed after `/`. */
|
|
5
5
|
alias: string;
|
|
6
|
-
/**
|
|
6
|
+
/** Opaque Whistle render key. */
|
|
7
7
|
commandId: string;
|
|
8
|
+
/** The command's typed Whistle identity; execute it by this. */
|
|
9
|
+
command: WhistleCommandKey;
|
|
8
10
|
/** Consume this draft on dispatch, including for a dialog or refusal. */
|
|
9
11
|
consumesDraft?: boolean | undefined;
|
|
10
12
|
/** Human-facing title. */
|
|
@@ -24,8 +26,10 @@ export type SlashCommand = Readonly<{
|
|
|
24
26
|
}>;
|
|
25
27
|
/** One key binding as help and hint surfaces list it. */
|
|
26
28
|
export type KeyBindingRow = Readonly<{
|
|
27
|
-
/**
|
|
29
|
+
/** Opaque Whistle render key. */
|
|
28
30
|
commandId: string;
|
|
31
|
+
/** The command's typed Whistle identity; execute it by this. */
|
|
32
|
+
command: WhistleCommandKey;
|
|
29
33
|
/** Human key label, e.g. `Ctrl+O`. */
|
|
30
34
|
keys: string;
|
|
31
35
|
/** What the key does. */
|
package/dist/commands/slash.js
CHANGED
|
@@ -31,6 +31,7 @@ export function slashRegistry(snapshot) {
|
|
|
31
31
|
return snapshot.slash.map((row) => ({
|
|
32
32
|
alias: row.alias,
|
|
33
33
|
commandId: row.commandId,
|
|
34
|
+
command: row.command,
|
|
34
35
|
title: row.title,
|
|
35
36
|
description: row.description,
|
|
36
37
|
category: row.category,
|
|
@@ -48,6 +49,7 @@ export function keyBindings(snapshot) {
|
|
|
48
49
|
const current = rows.get(row.commandId);
|
|
49
50
|
rows.set(row.commandId, {
|
|
50
51
|
commandId: row.commandId,
|
|
52
|
+
command: row.command,
|
|
51
53
|
keys: current === undefined
|
|
52
54
|
? keyLabel(row.binding)
|
|
53
55
|
: `${current.keys}, ${keyLabel(row.binding)}`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PromptDelivery } from "@fungi.computer/shiitake/client";
|
|
2
2
|
import type { WhistleExecutionOrigin, WhistleRuntime } from "@fungi.computer/whistle";
|
|
3
|
+
import { type AwaitingQueue } from "../core/awaiting-answer.js";
|
|
3
4
|
import { type ComposerKeys, type KeyHint } from "./hints.js";
|
|
4
5
|
import { type ParakeetInteractions, type ParakeetInteractionState } from "./interactions.js";
|
|
5
6
|
import { type SlashCompletion } from "./slash.js";
|
|
@@ -36,6 +37,14 @@ export type SlashCommandController = Readonly<{
|
|
|
36
37
|
interactions: ParakeetInteractionState;
|
|
37
38
|
/** The conversation store, for closing and retrying dialogs. */
|
|
38
39
|
store: ParakeetInteractions;
|
|
40
|
+
/**
|
|
41
|
+
* The decision the composer is answering, if any. It takes over an empty
|
|
42
|
+
* composer, or any composer once the person asked for it (`forced`).
|
|
43
|
+
*/
|
|
44
|
+
awaiting: AwaitingQueue & Readonly<{
|
|
45
|
+
/** The person asked for the composer even though a draft is in it. */
|
|
46
|
+
forced: boolean;
|
|
47
|
+
}>;
|
|
39
48
|
/** Move the highlight; false when no list is open. */
|
|
40
49
|
move: (delta: -1 | 1) => boolean;
|
|
41
50
|
/** Highlight one row (pointer hover). */
|
|
@@ -48,6 +57,10 @@ export type SlashCommandController = Readonly<{
|
|
|
48
57
|
submit: (delivery?: PromptDelivery) => void;
|
|
49
58
|
/** Run one completion row (pointer choice). */
|
|
50
59
|
choose: (index: number) => void;
|
|
60
|
+
/** Whether a registered command answers this slash alias and can run now. */
|
|
61
|
+
available: (alias: string) => boolean;
|
|
62
|
+
/** Run the command behind a slash alias, as typing it would, leaving the draft alone. */
|
|
63
|
+
runCommand: (alias: string) => void;
|
|
51
64
|
}>;
|
|
52
65
|
/**
|
|
53
66
|
* The one slash-command controller behind every Parakeet composer. It owns
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
2
|
-
import { useComposer, useClientContributions, useExtensionSlots, useParakeetSession, useParakeetInteractions, } from "../conversation.js";
|
|
2
|
+
import { useComposer, useClientContributions, useExtensionSlots, useParakeetSession, useParakeetInteractions, useStatus, } from "../conversation.js";
|
|
3
|
+
import { awaitingQueue } from "../core/awaiting-answer.js";
|
|
4
|
+
import { useAwaitingDriver } from "./awaiting-driver.js";
|
|
3
5
|
import { advertisedCommands } from "./advertised.js";
|
|
4
6
|
import { parakeetBuiltinCommands } from "./builtins.js";
|
|
5
7
|
import { composerHints } from "./hints.js";
|
|
@@ -39,6 +41,10 @@ export function useSlashCommands(options) {
|
|
|
39
41
|
entry.value.panel.some((row) => row.id === inspection.rowId && row.inspection !== undefined)))
|
|
40
42
|
store.inspect(undefined);
|
|
41
43
|
}, [contributions, interactions.inspection, store]);
|
|
44
|
+
const awaiting = {
|
|
45
|
+
...awaitingQueue(interactions.awaiting.items, interactions.awaiting.later),
|
|
46
|
+
forced: interactions.awaiting.forced,
|
|
47
|
+
};
|
|
42
48
|
const snapshot = useSyncExternalStore(commands.subscribe, commands.snapshot, commands.snapshot);
|
|
43
49
|
const composerRef = useRef(composer);
|
|
44
50
|
composerRef.current = composer;
|
|
@@ -92,6 +98,44 @@ export function useSlashCommands(options) {
|
|
|
92
98
|
message: `${error.extensionId} commands are unavailable: ${error.message}`,
|
|
93
99
|
});
|
|
94
100
|
}, [advertised, enabled, slots, contributions, store]);
|
|
101
|
+
const { observation } = useStatus();
|
|
102
|
+
useAwaitingDriver({
|
|
103
|
+
enabled,
|
|
104
|
+
commands,
|
|
105
|
+
store,
|
|
106
|
+
contributions,
|
|
107
|
+
online: observation.status === "live",
|
|
108
|
+
requests: interactions.awaiting.requests,
|
|
109
|
+
commandsVersion: snapshot,
|
|
110
|
+
});
|
|
111
|
+
const waiting = interactions.awaiting.items.length > 0;
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!enabled || !waiting)
|
|
114
|
+
return;
|
|
115
|
+
const lease = commands.contribute({
|
|
116
|
+
sourceId: "parakeet-awaiting",
|
|
117
|
+
namespace: "parakeet-awaiting",
|
|
118
|
+
commands: [
|
|
119
|
+
{
|
|
120
|
+
id: "answer",
|
|
121
|
+
title: "Answer a question",
|
|
122
|
+
category: "Conversation",
|
|
123
|
+
description: "Answer what the Agent is waiting on",
|
|
124
|
+
projections: {
|
|
125
|
+
help: { order: 5 },
|
|
126
|
+
palette: { order: 5 },
|
|
127
|
+
slash: { aliases: ["answer"], order: 5, consumesDraft: true },
|
|
128
|
+
},
|
|
129
|
+
handler: () => {
|
|
130
|
+
store.awaiting.focus();
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
return () => {
|
|
136
|
+
lease.dispose();
|
|
137
|
+
};
|
|
138
|
+
}, [commands, enabled, store, waiting]);
|
|
95
139
|
const registry = useMemo(() => slashRegistry(snapshot), [snapshot]);
|
|
96
140
|
const [dismissed, setDismissed] = useState();
|
|
97
141
|
const [highlighted, setHighlighted] = useState({ draft: "", index: 0 });
|
|
@@ -157,6 +201,7 @@ export function useSlashCommands(options) {
|
|
|
157
201
|
hints,
|
|
158
202
|
interactions,
|
|
159
203
|
store,
|
|
204
|
+
awaiting,
|
|
160
205
|
move: (delta) => {
|
|
161
206
|
if (rowCount === 0)
|
|
162
207
|
return false;
|
|
@@ -203,5 +248,17 @@ export function useSlashCommands(options) {
|
|
|
203
248
|
run(delivery, insert?.trimEnd());
|
|
204
249
|
},
|
|
205
250
|
choose,
|
|
251
|
+
available: (alias) => registry.some((command) => command.alias === alias &&
|
|
252
|
+
command.availability.status === "available"),
|
|
253
|
+
runCommand: (alias) => {
|
|
254
|
+
store.report(undefined);
|
|
255
|
+
void runSlashDraft(`/${alias}`, commands, () => undefined, origin).then((outcome) => {
|
|
256
|
+
const message = failureMessage(outcome);
|
|
257
|
+
if (message !== undefined &&
|
|
258
|
+
store.snapshot().open === undefined &&
|
|
259
|
+
store.lastEnd() !== "cancelled")
|
|
260
|
+
store.report({ tone: "error", message: `/${alias}: ${message}` });
|
|
261
|
+
});
|
|
262
|
+
},
|
|
206
263
|
};
|
|
207
264
|
}
|
|
@@ -39,6 +39,8 @@ export type ChatComposerProps = {
|
|
|
39
39
|
context?: SessionContext | undefined;
|
|
40
40
|
/** Parakeet's slash controller; the composer only draws and forwards keys. */
|
|
41
41
|
slash?: SlashCommandController | undefined;
|
|
42
|
+
/** Replace the images riding with the draft; omitted, the composer offers no Attach key. */
|
|
43
|
+
onImagesChange?: ((images: ComposerController["images"]) => void) | undefined;
|
|
42
44
|
};
|
|
43
45
|
/** Full Session readout for announcements; the footer only draws attention states. */
|
|
44
46
|
export declare function sessionReadout(status: ParakeetStatus, view: ParakeetSessionViewState): string;
|