@fungi.computer/parakeet 0.4.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/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 +9 -4
- 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 +7 -1
- package/dist/core/draft-storage.d.ts +4 -0
- package/dist/core/draft-storage.js +12 -5
- package/dist/core/session-query.d.ts +8 -0
- package/dist/core/session-query.js +13 -11
- package/dist/headless.d.ts +6 -2
- package/dist/headless.js +5 -1
- package/dist/parakeet.d.ts +6 -2
- package/dist/parakeet.js +15 -6
- package/dist/store/types.d.ts +5 -1
- package/dist/styles.css +395 -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 +13 -7
- package/src/completion-menu.css +71 -0
- package/src/styles.css +174 -46
|
@@ -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;
|