talon-agent 3.33.4 → 3.34.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/package.json +5 -2
- package/src/app.ts +16 -8
- package/src/backend/claude-sdk/handler.ts +428 -415
- package/src/backend/claude-sdk/stream.ts +76 -53
- package/src/backend/codex/handler/message.ts +275 -366
- package/src/backend/codex/handler/rollout-accounting.ts +137 -0
- package/src/backend/openai-agents/handler/message.ts +282 -356
- package/src/backend/remote-server/chat-turn.ts +66 -122
- package/src/backend/remote-server/index.ts +4 -0
- package/src/backend/remote-server/mcp.ts +73 -9
- package/src/backend/remote-server/model-catalog/presentation.ts +269 -228
- package/src/backend/remote-server/sessions.ts +2 -2
- package/src/backend/remote-server/turn.ts +58 -55
- package/src/backend/shared/cache-telemetry.ts +17 -1
- package/src/backend/shared/handler-to-events.ts +11 -16
- package/src/backend/shared/index.ts +14 -15
- package/src/backend/shared/result-events.ts +30 -0
- package/src/backend/shared/turn-phases.ts +277 -0
- package/src/bootstrap.ts +120 -79
- package/src/cli/setup.ts +375 -349
- package/src/core/background/cron-spec.ts +273 -0
- package/src/core/background/heartbeat/agent.ts +167 -104
- package/src/core/background/triggers/exit.ts +19 -2
- package/src/core/background/triggers/resume.ts +14 -7
- package/src/core/background/triggers/state.ts +9 -0
- package/src/core/engine/backend-controller/pool.ts +2 -0
- package/src/core/engine/backend-controller/state.ts +25 -12
- package/src/core/engine/gateway-actions/cron.ts +28 -292
- package/src/core/engine/gateway-routes.ts +239 -0
- package/src/core/engine/gateway.ts +66 -238
- package/src/core/mcp-hub/child-transport.ts +215 -0
- package/src/core/mcp-hub/children.ts +72 -13
- package/src/core/mcp-hub/index.ts +32 -13
- package/src/core/models/active-model.ts +29 -6
- package/src/core/vfs/mounts/files.ts +128 -115
- package/src/core/weaver/shuttle.ts +33 -2
- package/src/core/weaver/weaver.ts +37 -6
- package/src/frontend/discord/callbacks/components/agent-buttons.ts +82 -0
- package/src/frontend/discord/callbacks/components/backend-select.ts +149 -0
- package/src/frontend/discord/callbacks/components/effort.ts +72 -0
- package/src/frontend/discord/callbacks/components/index.ts +120 -0
- package/src/frontend/discord/callbacks/components/metrics.ts +24 -0
- package/src/frontend/discord/callbacks/components/model-nav.ts +93 -0
- package/src/frontend/discord/callbacks/components/model-select.ts +118 -0
- package/src/frontend/discord/callbacks/components/model.ts +33 -0
- package/src/frontend/discord/callbacks/components/pulse.ts +93 -0
- package/src/frontend/discord/callbacks/components/settings.ts +243 -0
- package/src/frontend/discord/callbacks/components/types.ts +34 -0
- package/src/frontend/discord/callbacks/index.ts +4 -4
- package/src/frontend/discord/connection.ts +36 -0
- package/src/frontend/discord/diagnostics.ts +62 -0
- package/src/frontend/discord/guild-policy.ts +89 -0
- package/src/frontend/discord/index.ts +44 -305
- package/src/frontend/discord/outbound.ts +61 -0
- package/src/frontend/discord/ready.ts +73 -0
- package/src/frontend/discord/runtime.ts +55 -0
- package/src/frontend/native/chat-lifecycle.ts +39 -0
- package/src/frontend/native/chat-wire.ts +69 -0
- package/src/frontend/native/context.ts +106 -0
- package/src/frontend/native/control.ts +78 -0
- package/src/frontend/native/emit.ts +167 -0
- package/src/frontend/native/empty-chat-sweep.ts +50 -0
- package/src/frontend/native/handlers.ts +121 -0
- package/src/frontend/native/history.ts +101 -0
- package/src/frontend/native/index.ts +90 -1293
- package/src/frontend/native/media.ts +43 -0
- package/src/frontend/native/models.ts +221 -0
- package/src/frontend/native/queue.ts +47 -0
- package/src/frontend/native/reset.ts +50 -0
- package/src/frontend/native/routes/chats.ts +115 -0
- package/src/frontend/native/routes/daemon.ts +70 -0
- package/src/frontend/native/routes/host.ts +151 -0
- package/src/frontend/native/routes/index.ts +22 -0
- package/src/frontend/native/routes/mesh.ts +72 -0
- package/src/frontend/native/routes/models.ts +54 -0
- package/src/frontend/native/routes/params.ts +29 -0
- package/src/frontend/native/routes/pre-auth.ts +94 -0
- package/src/frontend/native/routes/table.ts +92 -0
- package/src/frontend/native/runtime.ts +109 -0
- package/src/frontend/native/server.ts +30 -555
- package/src/frontend/native/status.ts +26 -0
- package/src/frontend/native/tool-result.ts +48 -0
- package/src/frontend/native/turn.ts +341 -0
- package/src/frontend/telegram/admin.ts +75 -52
- package/src/frontend/whatsapp/access.ts +67 -0
- package/src/frontend/whatsapp/connection.ts +280 -0
- package/src/frontend/whatsapp/inbound.ts +327 -0
- package/src/frontend/whatsapp/index.ts +28 -599
- package/src/frontend/whatsapp/runtime.ts +74 -0
- package/src/storage/metrics.ts +18 -0
- package/src/storage/session-record.ts +29 -0
- package/src/storage/sessions.ts +24 -0
- package/src/storage/trigger-store.ts +8 -0
- package/src/util/boot-timer.ts +31 -0
- package/src/util/concurrency.ts +28 -0
- package/src/util/watchdog.ts +32 -7
- package/src/frontend/discord/callbacks/components.ts +0 -793
- package/src/frontend/discord/callbacks/shared.ts +0 -22
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `effort:select` — the standalone /effort select menu.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { StringSelectMenuInteraction } from "discord.js";
|
|
6
|
+
import {
|
|
7
|
+
setChatEffort,
|
|
8
|
+
type EffortLevel,
|
|
9
|
+
} from "../../../../storage/chat-settings.js";
|
|
10
|
+
import {
|
|
11
|
+
getBackendIdForChat,
|
|
12
|
+
resolveChatBackend,
|
|
13
|
+
} from "../../../../core/engine/backend-controller/index.js";
|
|
14
|
+
import {
|
|
15
|
+
getActiveReasoningLevels,
|
|
16
|
+
supportsReasoningLevel,
|
|
17
|
+
} from "../../../shared/reasoning-levels.js";
|
|
18
|
+
import type { ComponentContext, ComponentInteraction } from "./types.js";
|
|
19
|
+
|
|
20
|
+
async function selectEffort(
|
|
21
|
+
interaction: StringSelectMenuInteraction,
|
|
22
|
+
{ config, gateway, chatId }: ComponentContext,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const level = interaction.values[0];
|
|
25
|
+
const be = resolveChatBackend(chatId, gateway?.backend);
|
|
26
|
+
const beId = getBackendIdForChat(chatId);
|
|
27
|
+
const reasoning = await getActiveReasoningLevels({
|
|
28
|
+
chatId,
|
|
29
|
+
backend: be,
|
|
30
|
+
backendId: beId,
|
|
31
|
+
config,
|
|
32
|
+
});
|
|
33
|
+
if (reasoning.levels.length === 0) {
|
|
34
|
+
await interaction.update({
|
|
35
|
+
content: "No valid reasoning levels found for this model.",
|
|
36
|
+
components: [],
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (level === "adaptive") setChatEffort(chatId, undefined);
|
|
41
|
+
else if (supportsReasoningLevel(level, reasoning.levels))
|
|
42
|
+
setChatEffort(chatId, level as EffortLevel);
|
|
43
|
+
else {
|
|
44
|
+
await interaction.update({
|
|
45
|
+
content: "Invalid reasoning level for this model.",
|
|
46
|
+
components: [],
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await interaction.update({
|
|
52
|
+
content: `**Effort:** ${level}`,
|
|
53
|
+
components: [],
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function handleEffortComponent(
|
|
61
|
+
interaction: ComponentInteraction,
|
|
62
|
+
context: ComponentContext,
|
|
63
|
+
): Promise<boolean> {
|
|
64
|
+
if (
|
|
65
|
+
interaction.customId === "effort:select" &&
|
|
66
|
+
interaction.isStringSelectMenu()
|
|
67
|
+
) {
|
|
68
|
+
await selectEffort(interaction, context);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component (button + select menu) interaction router.
|
|
3
|
+
*
|
|
4
|
+
* Custom ids are namespaced by a `:`-terminated prefix; the table below maps
|
|
5
|
+
* each prefix to the module that owns it. A handler returns `false` for an
|
|
6
|
+
* id under its prefix it doesn't recognise (a stale button from an older
|
|
7
|
+
* panel, a select-menu id arriving from a button), and the router acks
|
|
8
|
+
* those exactly as it acks a prefix with no handler.
|
|
9
|
+
*
|
|
10
|
+
* Custom-id namespace:
|
|
11
|
+
* settings:done
|
|
12
|
+
* settings:model (select menu, value=modelId)
|
|
13
|
+
* settings:effort:select (select menu)
|
|
14
|
+
* settings:proactive:on|off
|
|
15
|
+
* pulse:on | pulse:off | pulse:interval (button → opens modal)
|
|
16
|
+
* effort:select (select menu, standalone /effort)
|
|
17
|
+
* model:select (select menu, standalone /model)
|
|
18
|
+
* model:nav:* (pager)
|
|
19
|
+
* model:backend-select (select menu)
|
|
20
|
+
* metrics:today | metrics:all
|
|
21
|
+
* ai:<id> (AI-generated buttons — forwarded to agent)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { MessageFlags } from "discord.js";
|
|
25
|
+
import type { TalonConfig } from "../../../../util/config.js";
|
|
26
|
+
import type { Gateway } from "../../../../core/engine/gateway.js";
|
|
27
|
+
import { deriveNumericChatId } from "../../../../util/chat-id.js";
|
|
28
|
+
import { logError } from "../../../../util/log.js";
|
|
29
|
+
import {
|
|
30
|
+
isInteractionAllowed,
|
|
31
|
+
registerDiscordChat,
|
|
32
|
+
} from "../../handlers/index.js";
|
|
33
|
+
import { forwardToAgent } from "./agent-buttons.js";
|
|
34
|
+
import { handleEffortComponent } from "./effort.js";
|
|
35
|
+
import { handleMetricsComponent } from "./metrics.js";
|
|
36
|
+
import { handleModelComponent } from "./model.js";
|
|
37
|
+
import { handlePulseComponent } from "./pulse.js";
|
|
38
|
+
import { handleSettingsComponent } from "./settings.js";
|
|
39
|
+
import type { ComponentHandlers, ComponentInteraction } from "./types.js";
|
|
40
|
+
|
|
41
|
+
// Null-prototype so a custom id of "toString:" / "constructor:" / etc.
|
|
42
|
+
// can't resolve an inherited Object.prototype method via `handlers[key]`.
|
|
43
|
+
export const COMPONENT_HANDLERS: ComponentHandlers = Object.assign(
|
|
44
|
+
Object.create(null),
|
|
45
|
+
{
|
|
46
|
+
"settings:": handleSettingsComponent,
|
|
47
|
+
"pulse:": handlePulseComponent,
|
|
48
|
+
"effort:": handleEffortComponent,
|
|
49
|
+
"model:": handleModelComponent,
|
|
50
|
+
"metrics:": handleMetricsComponent,
|
|
51
|
+
"ai:": forwardToAgent,
|
|
52
|
+
} satisfies ComponentHandlers,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The table key for a custom id: its first segment, colon included
|
|
57
|
+
* (`"settings:done"` → `"settings:"`). An id with no colon keys to `""`,
|
|
58
|
+
* which no handler claims.
|
|
59
|
+
*/
|
|
60
|
+
export function componentRouteKey(customId: string): string {
|
|
61
|
+
return customId.slice(0, customId.indexOf(":") + 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function chatIdFromInteraction(interaction: ComponentInteraction): {
|
|
65
|
+
chatId: string;
|
|
66
|
+
numericChatId: number;
|
|
67
|
+
} {
|
|
68
|
+
const chatId = interaction.guildId
|
|
69
|
+
? `discord_guild_${interaction.guildId}_${interaction.channelId}`
|
|
70
|
+
: `discord_dm_${interaction.user.id}`;
|
|
71
|
+
return { chatId, numericChatId: deriveNumericChatId(chatId) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handleComponentInteraction(
|
|
75
|
+
interaction: ComponentInteraction,
|
|
76
|
+
config: TalonConfig,
|
|
77
|
+
gateway: Gateway,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
// Access control
|
|
80
|
+
const access = isInteractionAllowed(
|
|
81
|
+
interaction.inGuild(),
|
|
82
|
+
interaction.user.id,
|
|
83
|
+
interaction.guildId,
|
|
84
|
+
interaction.channelId,
|
|
85
|
+
);
|
|
86
|
+
if (!access.ok) {
|
|
87
|
+
await interaction.reply({
|
|
88
|
+
content: `⚠️ ${access.reason}`,
|
|
89
|
+
flags: MessageFlags.Ephemeral,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { chatId, numericChatId } = chatIdFromInteraction(interaction);
|
|
95
|
+
registerDiscordChat({
|
|
96
|
+
channelId: interaction.channelId!,
|
|
97
|
+
guildId: interaction.guildId,
|
|
98
|
+
userId: interaction.guildId ? null : interaction.user.id,
|
|
99
|
+
numericChatId,
|
|
100
|
+
chatId,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const customId = interaction.customId;
|
|
104
|
+
const handler = COMPONENT_HANDLERS[componentRouteKey(customId)];
|
|
105
|
+
if (
|
|
106
|
+
handler &&
|
|
107
|
+
(await handler(interaction, { config, gateway, chatId, numericChatId }))
|
|
108
|
+
) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Unknown custom_id — log + silently ack so Discord doesn't show
|
|
113
|
+
// "Interaction failed". This catches stale buttons from older panels.
|
|
114
|
+
logError("discord", `Unknown custom_id: ${customId}`);
|
|
115
|
+
try {
|
|
116
|
+
await interaction.deferUpdate();
|
|
117
|
+
} catch {
|
|
118
|
+
/* ignore */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `metrics:today` | `metrics:all` — the /metrics today ↔ all-time toggle.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { metricsViewRow, renderMetricsView } from "../../commands/admin.js";
|
|
6
|
+
import type { ComponentInteraction } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export async function handleMetricsComponent(
|
|
9
|
+
interaction: ComponentInteraction,
|
|
10
|
+
): Promise<boolean> {
|
|
11
|
+
const customId = interaction.customId;
|
|
12
|
+
if (customId !== "metrics:today" && customId !== "metrics:all") return false;
|
|
13
|
+
const view = customId === "metrics:all" ? "all" : "today";
|
|
14
|
+
const messages = renderMetricsView(view);
|
|
15
|
+
try {
|
|
16
|
+
await interaction.update({
|
|
17
|
+
content: messages[0]!,
|
|
18
|
+
components: [metricsViewRow(view).toJSON()],
|
|
19
|
+
});
|
|
20
|
+
} catch {
|
|
21
|
+
/* ignore */
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `model:nav:*` — the /model pager: page arrows, free-tier toggle, provider
|
|
3
|
+
* drill, and the backend submenu. Pure navigation: re-renders the same
|
|
4
|
+
* panel at the requested page without touching the chat's selected model.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildModelPickerView,
|
|
9
|
+
buildBackendPickerView,
|
|
10
|
+
decodeModelNav,
|
|
11
|
+
MODEL_NAV_PREFIX,
|
|
12
|
+
MODEL_PAGE_SIZE,
|
|
13
|
+
} from "../../model-picker.js";
|
|
14
|
+
import {
|
|
15
|
+
getBackendIdForChat,
|
|
16
|
+
resolveChatBackend,
|
|
17
|
+
listAvailableBackends,
|
|
18
|
+
} from "../../../../core/engine/backend-controller/index.js";
|
|
19
|
+
import { resolveActiveModelForChat } from "../../../../core/models/active-model.js";
|
|
20
|
+
import type { ComponentContext, ComponentInteraction } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export async function handleModelNav(
|
|
23
|
+
interaction: ComponentInteraction,
|
|
24
|
+
{ config, gateway, chatId }: ComponentContext,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const customId = interaction.customId;
|
|
27
|
+
// Backend submenu — the one nav action that isn't a re-page.
|
|
28
|
+
if (customId === `${MODEL_NAV_PREFIX}:backends`) {
|
|
29
|
+
const view = buildBackendPickerView(
|
|
30
|
+
listAvailableBackends(config),
|
|
31
|
+
getBackendIdForChat(chatId),
|
|
32
|
+
);
|
|
33
|
+
try {
|
|
34
|
+
await interaction.update({
|
|
35
|
+
content: view.content,
|
|
36
|
+
components: view.components,
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const target = decodeModelNav(customId);
|
|
45
|
+
if (!target) {
|
|
46
|
+
try {
|
|
47
|
+
await interaction.deferUpdate();
|
|
48
|
+
} catch {
|
|
49
|
+
/* ignore */
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const be = resolveChatBackend(chatId, gateway?.backend);
|
|
54
|
+
const beId = getBackendIdForChat(chatId);
|
|
55
|
+
const { model: current } = await resolveActiveModelForChat(
|
|
56
|
+
chatId,
|
|
57
|
+
be,
|
|
58
|
+
beId,
|
|
59
|
+
config,
|
|
60
|
+
);
|
|
61
|
+
if (!be?.models?.getSettingsPresentation) {
|
|
62
|
+
try {
|
|
63
|
+
await interaction.deferUpdate();
|
|
64
|
+
} catch {
|
|
65
|
+
/* ignore */
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const pres = await be.models.getSettingsPresentation(current ?? "", {
|
|
70
|
+
callbackPrefix: "model:",
|
|
71
|
+
navCallbackPrefix: MODEL_NAV_PREFIX,
|
|
72
|
+
pageSize: MODEL_PAGE_SIZE,
|
|
73
|
+
page: target.page,
|
|
74
|
+
filter: target.filter,
|
|
75
|
+
...(target.provider ? { provider: target.provider } : {}),
|
|
76
|
+
});
|
|
77
|
+
const modelInfo = current
|
|
78
|
+
? await be.models?.getRawModelInfo?.(current)
|
|
79
|
+
: undefined;
|
|
80
|
+
const view = buildModelPickerView(
|
|
81
|
+
pres,
|
|
82
|
+
modelInfo?.displayName ?? current ?? "_No model selected_",
|
|
83
|
+
beId,
|
|
84
|
+
);
|
|
85
|
+
try {
|
|
86
|
+
await interaction.update({
|
|
87
|
+
content: view.content,
|
|
88
|
+
components: view.components,
|
|
89
|
+
});
|
|
90
|
+
} catch {
|
|
91
|
+
/* ignore */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `model:select` — the /model select menu (separate from settings).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type StringSelectMenuInteraction, MessageFlags } from "discord.js";
|
|
6
|
+
import {
|
|
7
|
+
buildModelPickerView,
|
|
8
|
+
MODEL_NAV_PREFIX,
|
|
9
|
+
MODEL_PAGE_SIZE,
|
|
10
|
+
} from "../../model-picker.js";
|
|
11
|
+
import {
|
|
12
|
+
setChatModelForBackend,
|
|
13
|
+
setChatBackend,
|
|
14
|
+
} from "../../../../storage/chat-settings.js";
|
|
15
|
+
import { resolveModelId as resolveModelName } from "../../../../core/models/catalog.js";
|
|
16
|
+
import {
|
|
17
|
+
getBackendIdForChat,
|
|
18
|
+
resolveChatBackend,
|
|
19
|
+
} from "../../../../core/engine/backend-controller/index.js";
|
|
20
|
+
import { resolveActiveModelForChat } from "../../../../core/models/active-model.js";
|
|
21
|
+
import type { ComponentContext } from "./types.js";
|
|
22
|
+
|
|
23
|
+
export async function handleModelSelect(
|
|
24
|
+
interaction: StringSelectMenuInteraction,
|
|
25
|
+
{ config, gateway, chatId }: ComponentContext,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const value = interaction.values[0];
|
|
28
|
+
// Resolve and snapshot against the per-chat backend — never the
|
|
29
|
+
// global default — so this works in channels with a backend
|
|
30
|
+
// override pinned (e.g. switched to openai-agents to use
|
|
31
|
+
// OpenRouter models).
|
|
32
|
+
const be = resolveChatBackend(chatId, gateway?.backend);
|
|
33
|
+
const beId = getBackendIdForChat(chatId);
|
|
34
|
+
|
|
35
|
+
// In the provider view the select carries chips, not models — picking
|
|
36
|
+
// one drills into that provider rather than choosing anything.
|
|
37
|
+
if (value?.startsWith("provider:") && be?.models?.getSettingsPresentation) {
|
|
38
|
+
const providerId = value.slice("provider:".length);
|
|
39
|
+
const { model: activeNow } = await resolveActiveModelForChat(
|
|
40
|
+
chatId,
|
|
41
|
+
be,
|
|
42
|
+
beId,
|
|
43
|
+
config,
|
|
44
|
+
);
|
|
45
|
+
const pres = await be.models.getSettingsPresentation(activeNow ?? "", {
|
|
46
|
+
callbackPrefix: "model:",
|
|
47
|
+
navCallbackPrefix: MODEL_NAV_PREFIX,
|
|
48
|
+
pageSize: MODEL_PAGE_SIZE,
|
|
49
|
+
provider: providerId,
|
|
50
|
+
});
|
|
51
|
+
const view = buildModelPickerView(
|
|
52
|
+
pres,
|
|
53
|
+
activeNow ?? "_No model selected_",
|
|
54
|
+
beId,
|
|
55
|
+
);
|
|
56
|
+
try {
|
|
57
|
+
await interaction.update({
|
|
58
|
+
content: view.content,
|
|
59
|
+
components: view.components,
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
/* ignore */
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (value === "reset") {
|
|
68
|
+
// Clear THIS backend's slot only — other backends stay intact.
|
|
69
|
+
setChatModelForBackend(chatId, beId, undefined);
|
|
70
|
+
} else if (be?.models?.resolveModelInfo) {
|
|
71
|
+
const resolution = await be.models?.resolveModelInfo(value);
|
|
72
|
+
if (resolution.kind === "exact" && resolution.model.selectable) {
|
|
73
|
+
setChatModelForBackend(chatId, beId, resolution.storedValue);
|
|
74
|
+
setChatBackend(chatId, beId);
|
|
75
|
+
} else {
|
|
76
|
+
await interaction.reply({
|
|
77
|
+
content: "Model unavailable.",
|
|
78
|
+
flags: MessageFlags.Ephemeral,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
setChatModelForBackend(chatId, beId, resolveModelName(value));
|
|
84
|
+
setChatBackend(chatId, beId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Resolve display through the active-model helper so the panel
|
|
88
|
+
// matches what queries will actually run. Null surfaces as
|
|
89
|
+
// "No model selected".
|
|
90
|
+
const { model: resolvedCurrent } = await resolveActiveModelForChat(
|
|
91
|
+
chatId,
|
|
92
|
+
be,
|
|
93
|
+
beId,
|
|
94
|
+
config,
|
|
95
|
+
);
|
|
96
|
+
if (be?.models?.getSettingsPresentation) {
|
|
97
|
+
const current = resolvedCurrent ?? "";
|
|
98
|
+
const pres = await be.models?.getSettingsPresentation(current, {
|
|
99
|
+
callbackPrefix: "model:",
|
|
100
|
+
navCallbackPrefix: MODEL_NAV_PREFIX,
|
|
101
|
+
pageSize: MODEL_PAGE_SIZE,
|
|
102
|
+
});
|
|
103
|
+
const modelInfo = resolvedCurrent
|
|
104
|
+
? await be.models?.getRawModelInfo?.(resolvedCurrent)
|
|
105
|
+
: undefined;
|
|
106
|
+
const displayName =
|
|
107
|
+
modelInfo?.displayName ?? resolvedCurrent ?? "_No model selected_";
|
|
108
|
+
const view = buildModelPickerView(pres, displayName, beId);
|
|
109
|
+
try {
|
|
110
|
+
await interaction.update({
|
|
111
|
+
content: view.content,
|
|
112
|
+
components: view.components,
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
/* ignore */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `model:*` — the /model panel, three ids under one prefix:
|
|
3
|
+
*
|
|
4
|
+
* model:select (select menu → model-select.ts)
|
|
5
|
+
* model:nav:* (pager buttons → model-nav.ts)
|
|
6
|
+
* model:backend-select (select menu → backend-select.ts)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { MODEL_NAV_PREFIX } from "../../model-picker.js";
|
|
10
|
+
import { handleBackendSelect } from "./backend-select.js";
|
|
11
|
+
import { handleModelNav } from "./model-nav.js";
|
|
12
|
+
import { handleModelSelect } from "./model-select.js";
|
|
13
|
+
import type { ComponentContext, ComponentInteraction } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export async function handleModelComponent(
|
|
16
|
+
interaction: ComponentInteraction,
|
|
17
|
+
context: ComponentContext,
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
const customId = interaction.customId;
|
|
20
|
+
if (customId === "model:select" && interaction.isStringSelectMenu()) {
|
|
21
|
+
await handleModelSelect(interaction, context);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (customId.startsWith(`${MODEL_NAV_PREFIX}:`)) {
|
|
25
|
+
await handleModelNav(interaction, context);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (customId === "model:backend-select" && interaction.isStringSelectMenu()) {
|
|
29
|
+
await handleBackendSelect(interaction, context);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pulse:*` — the standalone /pulse panel.
|
|
3
|
+
*
|
|
4
|
+
* pulse:interval (button → opens the interval modal)
|
|
5
|
+
* pulse:on | pulse:off
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ButtonInteraction,
|
|
10
|
+
ActionRowBuilder,
|
|
11
|
+
ButtonBuilder,
|
|
12
|
+
ButtonStyle,
|
|
13
|
+
ModalBuilder,
|
|
14
|
+
TextInputBuilder,
|
|
15
|
+
TextInputStyle,
|
|
16
|
+
} from "discord.js";
|
|
17
|
+
import {
|
|
18
|
+
registerChat,
|
|
19
|
+
disablePulse,
|
|
20
|
+
enablePulse,
|
|
21
|
+
isPulseEnabled,
|
|
22
|
+
} from "../../../../core/background/pulse.js";
|
|
23
|
+
import type { ComponentContext, ComponentInteraction } from "./types.js";
|
|
24
|
+
|
|
25
|
+
// ── /pulse "Set interval…" button → opens modal ───────────────────────
|
|
26
|
+
async function openIntervalModal(
|
|
27
|
+
interaction: ButtonInteraction,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const modal = new ModalBuilder()
|
|
30
|
+
.setCustomId("modal:pulse-interval")
|
|
31
|
+
.setTitle("Pulse interval");
|
|
32
|
+
const input = new TextInputBuilder()
|
|
33
|
+
.setCustomId("interval")
|
|
34
|
+
.setLabel("How often (min 5m)")
|
|
35
|
+
.setPlaceholder("30m, 2h, 90m, 1d…")
|
|
36
|
+
.setStyle(TextInputStyle.Short)
|
|
37
|
+
.setMinLength(2)
|
|
38
|
+
.setMaxLength(16)
|
|
39
|
+
.setRequired(true);
|
|
40
|
+
modal.addComponents(
|
|
41
|
+
new ActionRowBuilder<TextInputBuilder>().addComponents(input),
|
|
42
|
+
);
|
|
43
|
+
await interaction.showModal(modal);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Standalone /pulse on|off buttons ──────────────────────────────────────
|
|
47
|
+
async function togglePulse(
|
|
48
|
+
interaction: ComponentInteraction,
|
|
49
|
+
chatId: string,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const val = interaction.customId.slice(6);
|
|
52
|
+
if (val === "on") {
|
|
53
|
+
enablePulse(chatId);
|
|
54
|
+
registerChat(chatId);
|
|
55
|
+
} else if (val === "off") {
|
|
56
|
+
disablePulse(chatId);
|
|
57
|
+
}
|
|
58
|
+
const enabled = isPulseEnabled(chatId);
|
|
59
|
+
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
60
|
+
new ButtonBuilder()
|
|
61
|
+
.setCustomId("pulse:on")
|
|
62
|
+
.setLabel(enabled ? "✓ On" : "On")
|
|
63
|
+
.setStyle(enabled ? ButtonStyle.Success : ButtonStyle.Secondary),
|
|
64
|
+
new ButtonBuilder()
|
|
65
|
+
.setCustomId("pulse:off")
|
|
66
|
+
.setLabel(!enabled ? "✓ Off" : "Off")
|
|
67
|
+
.setStyle(ButtonStyle.Secondary),
|
|
68
|
+
);
|
|
69
|
+
try {
|
|
70
|
+
await (interaction as ButtonInteraction).update({
|
|
71
|
+
content: [
|
|
72
|
+
`**🔔 Pulse:** ${enabled ? "on" : "off"}`,
|
|
73
|
+
"",
|
|
74
|
+
"Reads along every few minutes and jumps in when there's something to add.",
|
|
75
|
+
].join("\n"),
|
|
76
|
+
components: [row],
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function handlePulseComponent(
|
|
84
|
+
interaction: ComponentInteraction,
|
|
85
|
+
{ chatId }: ComponentContext,
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
if (interaction.customId === "pulse:interval" && interaction.isButton()) {
|
|
88
|
+
await openIntervalModal(interaction);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
await togglePulse(interaction, chatId);
|
|
92
|
+
return true;
|
|
93
|
+
}
|