@otto-assistant/otto 0.7.16 → 0.7.17
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/commands/model-variant.js +55 -14
- package/dist/commands/model.js +55 -15
- package/dist/commands/unset-model.js +48 -9
- package/dist/interaction-handler.js +15 -3
- package/package.json +1 -1
- package/src/commands/model-variant.ts +68 -13
- package/src/commands/model.ts +68 -14
- package/src/commands/unset-model.ts +60 -8
- package/src/interaction-handler.ts +21 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Cross-menu state: Discord doesn't expose already-selected values on sibling
|
|
6
6
|
// select menus in the same message. We track partial selections in the context
|
|
7
7
|
// Map. Whichever menu fires second sees the first selection stored and applies.
|
|
8
|
-
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
|
|
8
|
+
import { ChatInputCommandInteraction, StringSelectMenuInteraction, ButtonInteraction, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
10
|
import { setChannelModel, setSessionModel, getThreadSession, setGlobalModel, getVariantCascade, } from '../database.js';
|
|
11
11
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
@@ -313,21 +313,20 @@ async function applyVariant({ interaction, context, variant, scope, contextHash,
|
|
|
313
313
|
variant,
|
|
314
314
|
});
|
|
315
315
|
logger.log(`Set variant ${variant ?? 'none'} for session ${context.sessionId} (model ${modelId})`);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
? '\n_Restarting current request with new variant..._'
|
|
325
|
-
: '';
|
|
316
|
+
// Show a button so the user can choose to resend the last message.
|
|
317
|
+
// Do not auto-retry — the user may want to add context in a new message instead.
|
|
318
|
+
// Context is kept alive (TTL handles cleanup) so the button can access it.
|
|
319
|
+
const retryButton = new ButtonBuilder()
|
|
320
|
+
.setCustomId(`variant_retry:${contextHash}`)
|
|
321
|
+
.setLabel('Resend last message')
|
|
322
|
+
.setStyle(ButtonStyle.Secondary);
|
|
323
|
+
const actionRow = new ActionRowBuilder().addComponents(retryButton);
|
|
326
324
|
await interaction.editReply({
|
|
327
|
-
content: `Variant set for this session:\n**${context.providerName}** / **${context.modelName}**${variantSuffix}\n\`${modelId}\`${
|
|
325
|
+
content: `Variant set for this session:\n**${context.providerName}** / **${context.modelName}**${variantSuffix}\n\`${modelId}\`${agentTip}`,
|
|
328
326
|
flags: MessageFlags.SuppressEmbeds,
|
|
329
|
-
components: [],
|
|
327
|
+
components: [actionRow],
|
|
330
328
|
});
|
|
329
|
+
return;
|
|
331
330
|
}
|
|
332
331
|
else if (scope === 'global') {
|
|
333
332
|
await setGlobalModel({ appId: context.appId, modelId, variant });
|
|
@@ -357,7 +356,11 @@ async function applyVariant({ interaction, context, variant, scope, contextHash,
|
|
|
357
356
|
components: [],
|
|
358
357
|
});
|
|
359
358
|
}
|
|
360
|
-
|
|
359
|
+
// Clean up the context from memory, except for session scope where the
|
|
360
|
+
// retry button still needs it. TTL handles cleanup in that case.
|
|
361
|
+
if (scope !== 'session') {
|
|
362
|
+
pendingVariantContexts.delete(contextHash);
|
|
363
|
+
}
|
|
361
364
|
}
|
|
362
365
|
catch (error) {
|
|
363
366
|
logger.error('Error applying variant:', error);
|
|
@@ -367,3 +370,41 @@ async function applyVariant({ interaction, context, variant, scope, contextHash,
|
|
|
367
370
|
});
|
|
368
371
|
}
|
|
369
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Handle the "Resend last message" button shown after a session-scope variant change.
|
|
375
|
+
* Aborts any active run and resends the last user prompt with the new variant.
|
|
376
|
+
*/
|
|
377
|
+
export async function handleVariantRetryButton(interaction) {
|
|
378
|
+
const customId = interaction.customId;
|
|
379
|
+
if (!customId.startsWith('variant_retry:')) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
await interaction.deferUpdate();
|
|
383
|
+
const contextHash = customId.replace('variant_retry:', '');
|
|
384
|
+
const context = pendingVariantContexts.get(contextHash);
|
|
385
|
+
// Context may have expired (TTL) — still allow the action via runtime alone.
|
|
386
|
+
const threadId = context?.thread?.id;
|
|
387
|
+
if (!threadId) {
|
|
388
|
+
await interaction.editReply({
|
|
389
|
+
content: 'Could not find the session thread. Please resend your message manually.',
|
|
390
|
+
components: [],
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const runtime = getRuntime(threadId);
|
|
395
|
+
if (!runtime) {
|
|
396
|
+
await interaction.editReply({
|
|
397
|
+
content: 'No active session found. Please resend your message manually.',
|
|
398
|
+
components: [],
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const retried = await runtime.retryLastUserPrompt();
|
|
403
|
+
pendingVariantContexts.delete(contextHash);
|
|
404
|
+
await interaction.editReply({
|
|
405
|
+
content: retried
|
|
406
|
+
? `Restarting with new variant...`
|
|
407
|
+
: `Variant changed. Send a new message to continue.`,
|
|
408
|
+
components: [],
|
|
409
|
+
});
|
|
410
|
+
}
|
package/dist/commands/model.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// /model command - Set the preferred model for this channel or session.
|
|
2
|
-
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
|
|
2
|
+
import { ChatInputCommandInteraction, StringSelectMenuInteraction, ButtonInteraction, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { setChannelModel, setSessionModel, setSessionAgent, getChannelModel, getSessionModel, getSessionAgent, getChannelAgent, getThreadSession, getGlobalModel, setGlobalModel, getVariantCascade, } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
@@ -741,21 +741,20 @@ export async function handleModelScopeSelectMenu(interaction) {
|
|
|
741
741
|
}
|
|
742
742
|
await setSessionModel({ sessionId: context.sessionId, modelId, variant });
|
|
743
743
|
modelLogger.log(`Set model ${modelId}${variantSuffix} for session ${context.sessionId}`);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
? '\n_Restarting current request with new model..._'
|
|
753
|
-
: '';
|
|
744
|
+
// Show a button so the user can choose to resend the last message.
|
|
745
|
+
// Do not auto-retry — the user may want to add context in a new message instead.
|
|
746
|
+
// Context is kept alive (TTL handles cleanup) so the button can access it.
|
|
747
|
+
const retryButton = new ButtonBuilder()
|
|
748
|
+
.setCustomId(`model_retry:${contextHash}`)
|
|
749
|
+
.setLabel('Resend last message')
|
|
750
|
+
.setStyle(ButtonStyle.Secondary);
|
|
751
|
+
const actionRow = new ActionRowBuilder().addComponents(retryButton);
|
|
754
752
|
await interaction.editReply({
|
|
755
|
-
content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${
|
|
753
|
+
content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${agentTip}`,
|
|
756
754
|
flags: MessageFlags.SuppressEmbeds,
|
|
757
|
-
components: [],
|
|
755
|
+
components: [actionRow],
|
|
758
756
|
});
|
|
757
|
+
return;
|
|
759
758
|
}
|
|
760
759
|
else if (selectedScope === 'global') {
|
|
761
760
|
if (!context.appId) {
|
|
@@ -785,8 +784,11 @@ export async function handleModelScopeSelectMenu(interaction) {
|
|
|
785
784
|
components: [],
|
|
786
785
|
});
|
|
787
786
|
}
|
|
788
|
-
// Clean up the context from memory
|
|
789
|
-
|
|
787
|
+
// Clean up the context from memory, except for session scope where the
|
|
788
|
+
// retry button still needs it. TTL handles cleanup in that case.
|
|
789
|
+
if (selectedScope !== 'session') {
|
|
790
|
+
deleteModelContext(contextHash);
|
|
791
|
+
}
|
|
790
792
|
}
|
|
791
793
|
catch (error) {
|
|
792
794
|
modelLogger.error('Error saving model preference:', error);
|
|
@@ -796,3 +798,41 @@ export async function handleModelScopeSelectMenu(interaction) {
|
|
|
796
798
|
});
|
|
797
799
|
}
|
|
798
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* Handle the "Resend last message" button shown after a session-scope model change.
|
|
803
|
+
* Aborts any active run and resends the last user prompt with the new model.
|
|
804
|
+
*/
|
|
805
|
+
export async function handleModelRetryButton(interaction) {
|
|
806
|
+
const customId = interaction.customId;
|
|
807
|
+
if (!customId.startsWith('model_retry:')) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
await interaction.deferUpdate();
|
|
811
|
+
const contextHash = customId.replace('model_retry:', '');
|
|
812
|
+
const context = pendingModelContexts.get(contextHash);
|
|
813
|
+
// Context may have expired (10 min TTL) — still allow the action via runtime alone.
|
|
814
|
+
const threadId = context?.thread?.id;
|
|
815
|
+
if (!threadId) {
|
|
816
|
+
await interaction.editReply({
|
|
817
|
+
content: 'Could not find the session thread. Please resend your message manually.',
|
|
818
|
+
components: [],
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const runtime = getRuntime(threadId);
|
|
823
|
+
if (!runtime) {
|
|
824
|
+
await interaction.editReply({
|
|
825
|
+
content: 'No active session found. Please resend your message manually.',
|
|
826
|
+
components: [],
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const retried = await runtime.retryLastUserPrompt();
|
|
831
|
+
deleteModelContext(contextHash);
|
|
832
|
+
await interaction.editReply({
|
|
833
|
+
content: retried
|
|
834
|
+
? `Restarting with new model...`
|
|
835
|
+
: `Model changed. Send a new message to continue.`,
|
|
836
|
+
components: [],
|
|
837
|
+
});
|
|
838
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// /unset-model-override command - Remove model overrides and use default instead.
|
|
2
|
-
import { ChatInputCommandInteraction, ChannelType, MessageFlags, } from 'discord.js';
|
|
2
|
+
import { ChatInputCommandInteraction, ButtonInteraction, ButtonBuilder, ButtonStyle, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
|
|
3
3
|
import { getChannelModel, getSessionModel, getThreadSession, clearSessionModel, } from '../database.js';
|
|
4
4
|
import { getPrisma } from '../db.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
@@ -121,19 +121,58 @@ export async function handleUnsetModelCommand({ interaction, appId, }) {
|
|
|
121
121
|
? 'none'
|
|
122
122
|
: `\`${newModelInfo.model}\` (${formatModelSource(newModelInfo.type, 'agentName' in newModelInfo ? newModelInfo.agentName : undefined)})`;
|
|
123
123
|
}
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const clearedTypeText = clearedType === 'session' ? 'Session' : 'Channel';
|
|
125
|
+
// Show a button so the user can choose to resend the last message (session scope only).
|
|
126
|
+
// Do not auto-retry — the user may want to add context in a new message instead.
|
|
126
127
|
if (isThread && clearedType === 'session' && sessionId) {
|
|
127
128
|
const runtime = getRuntime(channel.id);
|
|
128
129
|
if (runtime) {
|
|
129
|
-
|
|
130
|
+
const retryButton = new ButtonBuilder()
|
|
131
|
+
.setCustomId(`unset_model_retry:${channel.id}`)
|
|
132
|
+
.setLabel('Resend last message')
|
|
133
|
+
.setStyle(ButtonStyle.Secondary);
|
|
134
|
+
const actionRow = new ActionRowBuilder().addComponents(retryButton);
|
|
135
|
+
await interaction.editReply({
|
|
136
|
+
content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}`,
|
|
137
|
+
components: [actionRow],
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
|
-
const clearedTypeText = clearedType === 'session' ? 'Session' : 'Channel';
|
|
133
|
-
const retriedText = retried
|
|
134
|
-
? '\n_Restarting current request with new model..._'
|
|
135
|
-
: '';
|
|
136
142
|
await interaction.editReply({
|
|
137
|
-
content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}
|
|
143
|
+
content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Handle the "Resend last message" button after clearing a session model override.
|
|
148
|
+
*/
|
|
149
|
+
export async function handleUnsetModelRetryButton(interaction) {
|
|
150
|
+
const customId = interaction.customId;
|
|
151
|
+
if (!customId.startsWith('unset_model_retry:')) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await interaction.deferUpdate();
|
|
155
|
+
const threadId = customId.replace('unset_model_retry:', '');
|
|
156
|
+
if (!threadId) {
|
|
157
|
+
await interaction.editReply({
|
|
158
|
+
content: 'Could not find the session. Please resend your message manually.',
|
|
159
|
+
components: [],
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const runtime = getRuntime(threadId);
|
|
164
|
+
if (!runtime) {
|
|
165
|
+
await interaction.editReply({
|
|
166
|
+
content: 'No active session found. Please resend your message manually.',
|
|
167
|
+
components: [],
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const retried = await runtime.retryLastUserPrompt();
|
|
172
|
+
await interaction.editReply({
|
|
173
|
+
content: retried
|
|
174
|
+
? 'Restarting with new model...'
|
|
175
|
+
: 'Model changed. Send a new message to continue.',
|
|
176
|
+
components: [],
|
|
138
177
|
});
|
|
139
178
|
}
|
|
@@ -21,8 +21,8 @@ import { handleDiffCommand } from './commands/diff.js';
|
|
|
21
21
|
import { handleForkCommand, handleForkSelectMenu, } from './commands/fork.js';
|
|
22
22
|
import { handleForkSubagentCommand, handleForkSubagentSelectMenu, } from './commands/fork-subagent.js';
|
|
23
23
|
import { handleBtwCommand } from './commands/btw.js';
|
|
24
|
-
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, handleModelScopeSelectMenu, } from './commands/model.js';
|
|
25
|
-
import { handleUnsetModelCommand } from './commands/unset-model.js';
|
|
24
|
+
import { handleModelCommand, handleProviderSelectMenu, handleModelSelectMenu, handleModelScopeSelectMenu, handleModelRetryButton, } from './commands/model.js';
|
|
25
|
+
import { handleUnsetModelCommand, handleUnsetModelRetryButton, } from './commands/unset-model.js';
|
|
26
26
|
import { handleLoginCommand, handleLoginSelect, handleLoginTextButton, handleLoginTextModalSubmit, handleLoginApiKeyButton, handleOAuthCodeButton, handleOAuthCodeModalSubmit, handleApiKeyModalSubmit, } from './commands/login.js';
|
|
27
27
|
import { handleTranscriptionApiKeyButton, handleTranscriptionApiKeyCommand, handleTranscriptionApiKeyModalSubmit, } from './commands/gemini-apikey.js';
|
|
28
28
|
import { handleAgentCommand, handleAgentSelectMenu, handleQuickAgentCommand, } from './commands/agent.js';
|
|
@@ -44,7 +44,7 @@ import { handleMcpCommand, handleMcpSelectMenu } from './commands/mcp.js';
|
|
|
44
44
|
import { handleScreenshareCommand, handleScreenshareStopCommand, } from './commands/screenshare.js';
|
|
45
45
|
import { handleVscodeCommand } from './commands/vscode.js';
|
|
46
46
|
import { handleModelVariantSelectMenu } from './commands/model.js';
|
|
47
|
-
import { handleModelVariantCommand, handleVariantQuickSelectMenu, handleVariantScopeSelectMenu, } from './commands/model-variant.js';
|
|
47
|
+
import { handleModelVariantCommand, handleVariantQuickSelectMenu, handleVariantScopeSelectMenu, handleVariantRetryButton, } from './commands/model-variant.js';
|
|
48
48
|
import { hasOttoBotPermission } from './discord-utils.js';
|
|
49
49
|
import { createLogger, LogPrefix } from './logger.js';
|
|
50
50
|
import { notifyError } from './sentry.js';
|
|
@@ -302,6 +302,18 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
302
302
|
await handleHtmlActionButton(interaction);
|
|
303
303
|
return;
|
|
304
304
|
}
|
|
305
|
+
if (customId.startsWith('model_retry:')) {
|
|
306
|
+
await handleModelRetryButton(interaction);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (customId.startsWith('variant_retry:')) {
|
|
310
|
+
await handleVariantRetryButton(interaction);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (customId.startsWith('unset_model_retry:')) {
|
|
314
|
+
await handleUnsetModelRetryButton(interaction);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
305
317
|
return;
|
|
306
318
|
}
|
|
307
319
|
if (interaction.isStringSelectMenu()) {
|
package/package.json
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
import {
|
|
10
10
|
ChatInputCommandInteraction,
|
|
11
11
|
StringSelectMenuInteraction,
|
|
12
|
+
ButtonInteraction,
|
|
12
13
|
StringSelectMenuBuilder,
|
|
14
|
+
ButtonBuilder,
|
|
15
|
+
ButtonStyle,
|
|
13
16
|
ActionRowBuilder,
|
|
14
17
|
ChannelType,
|
|
15
18
|
type ThreadChannel,
|
|
@@ -427,22 +430,22 @@ async function applyVariant({
|
|
|
427
430
|
`Set variant ${variant ?? 'none'} for session ${context.sessionId} (model ${modelId})`,
|
|
428
431
|
)
|
|
429
432
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
433
|
+
// Show a button so the user can choose to resend the last message.
|
|
434
|
+
// Do not auto-retry — the user may want to add context in a new message instead.
|
|
435
|
+
// Context is kept alive (TTL handles cleanup) so the button can access it.
|
|
436
|
+
const retryButton = new ButtonBuilder()
|
|
437
|
+
.setCustomId(`variant_retry:${contextHash}`)
|
|
438
|
+
.setLabel('Resend last message')
|
|
439
|
+
.setStyle(ButtonStyle.Secondary)
|
|
440
|
+
|
|
441
|
+
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(retryButton)
|
|
437
442
|
|
|
438
|
-
const retryNote = retried
|
|
439
|
-
? '\n_Restarting current request with new variant..._'
|
|
440
|
-
: ''
|
|
441
443
|
await interaction.editReply({
|
|
442
|
-
content: `Variant set for this session:\n**${context.providerName}** / **${context.modelName}**${variantSuffix}\n\`${modelId}\`${
|
|
444
|
+
content: `Variant set for this session:\n**${context.providerName}** / **${context.modelName}**${variantSuffix}\n\`${modelId}\`${agentTip}`,
|
|
443
445
|
flags: MessageFlags.SuppressEmbeds,
|
|
444
|
-
components: [],
|
|
446
|
+
components: [actionRow],
|
|
445
447
|
})
|
|
448
|
+
return
|
|
446
449
|
} else if (scope === 'global') {
|
|
447
450
|
await setGlobalModel({ appId: context.appId, modelId, variant })
|
|
448
451
|
await setChannelModel({
|
|
@@ -477,7 +480,11 @@ async function applyVariant({
|
|
|
477
480
|
})
|
|
478
481
|
}
|
|
479
482
|
|
|
480
|
-
|
|
483
|
+
// Clean up the context from memory, except for session scope where the
|
|
484
|
+
// retry button still needs it. TTL handles cleanup in that case.
|
|
485
|
+
if (scope !== 'session') {
|
|
486
|
+
pendingVariantContexts.delete(contextHash)
|
|
487
|
+
}
|
|
481
488
|
} catch (error) {
|
|
482
489
|
logger.error('Error applying variant:', error)
|
|
483
490
|
await interaction.editReply({
|
|
@@ -486,3 +493,51 @@ async function applyVariant({
|
|
|
486
493
|
})
|
|
487
494
|
}
|
|
488
495
|
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Handle the "Resend last message" button shown after a session-scope variant change.
|
|
499
|
+
* Aborts any active run and resends the last user prompt with the new variant.
|
|
500
|
+
*/
|
|
501
|
+
export async function handleVariantRetryButton(
|
|
502
|
+
interaction: ButtonInteraction,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
const customId = interaction.customId
|
|
505
|
+
if (!customId.startsWith('variant_retry:')) {
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
await interaction.deferUpdate()
|
|
510
|
+
|
|
511
|
+
const contextHash = customId.replace('variant_retry:', '')
|
|
512
|
+
const context = pendingVariantContexts.get(contextHash)
|
|
513
|
+
|
|
514
|
+
// Context may have expired (TTL) — still allow the action via runtime alone.
|
|
515
|
+
const threadId = context?.thread?.id
|
|
516
|
+
|
|
517
|
+
if (!threadId) {
|
|
518
|
+
await interaction.editReply({
|
|
519
|
+
content: 'Could not find the session thread. Please resend your message manually.',
|
|
520
|
+
components: [],
|
|
521
|
+
})
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const runtime = getRuntime(threadId)
|
|
526
|
+
if (!runtime) {
|
|
527
|
+
await interaction.editReply({
|
|
528
|
+
content: 'No active session found. Please resend your message manually.',
|
|
529
|
+
components: [],
|
|
530
|
+
})
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const retried = await runtime.retryLastUserPrompt()
|
|
535
|
+
pendingVariantContexts.delete(contextHash)
|
|
536
|
+
|
|
537
|
+
await interaction.editReply({
|
|
538
|
+
content: retried
|
|
539
|
+
? `Restarting with new variant...`
|
|
540
|
+
: `Variant changed. Send a new message to continue.`,
|
|
541
|
+
components: [],
|
|
542
|
+
})
|
|
543
|
+
}
|
package/src/commands/model.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import {
|
|
4
4
|
ChatInputCommandInteraction,
|
|
5
5
|
StringSelectMenuInteraction,
|
|
6
|
+
ButtonInteraction,
|
|
6
7
|
StringSelectMenuBuilder,
|
|
8
|
+
ButtonBuilder,
|
|
9
|
+
ButtonStyle,
|
|
7
10
|
ActionRowBuilder,
|
|
8
11
|
ChannelType,
|
|
9
12
|
type ThreadChannel,
|
|
@@ -1020,22 +1023,22 @@ export async function handleModelScopeSelectMenu(
|
|
|
1020
1023
|
`Set model ${modelId}${variantSuffix} for session ${context.sessionId}`,
|
|
1021
1024
|
)
|
|
1022
1025
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1026
|
+
// Show a button so the user can choose to resend the last message.
|
|
1027
|
+
// Do not auto-retry — the user may want to add context in a new message instead.
|
|
1028
|
+
// Context is kept alive (TTL handles cleanup) so the button can access it.
|
|
1029
|
+
const retryButton = new ButtonBuilder()
|
|
1030
|
+
.setCustomId(`model_retry:${contextHash}`)
|
|
1031
|
+
.setLabel('Resend last message')
|
|
1032
|
+
.setStyle(ButtonStyle.Secondary)
|
|
1033
|
+
|
|
1034
|
+
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(retryButton)
|
|
1030
1035
|
|
|
1031
|
-
const retryNote = retried
|
|
1032
|
-
? '\n_Restarting current request with new model..._'
|
|
1033
|
-
: ''
|
|
1034
1036
|
await interaction.editReply({
|
|
1035
|
-
content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${
|
|
1037
|
+
content: `Model set for this session:\n**${context.providerName}** / **${modelDisplay}**${variantSuffix}\n\`${modelId}\`${agentTip}`,
|
|
1036
1038
|
flags: MessageFlags.SuppressEmbeds,
|
|
1037
|
-
components: [],
|
|
1039
|
+
components: [actionRow],
|
|
1038
1040
|
})
|
|
1041
|
+
return
|
|
1039
1042
|
} else if (selectedScope === 'global') {
|
|
1040
1043
|
if (!context.appId) {
|
|
1041
1044
|
deleteModelContext(contextHash)
|
|
@@ -1070,8 +1073,11 @@ export async function handleModelScopeSelectMenu(
|
|
|
1070
1073
|
})
|
|
1071
1074
|
}
|
|
1072
1075
|
|
|
1073
|
-
// Clean up the context from memory
|
|
1074
|
-
|
|
1076
|
+
// Clean up the context from memory, except for session scope where the
|
|
1077
|
+
// retry button still needs it. TTL handles cleanup in that case.
|
|
1078
|
+
if (selectedScope !== 'session') {
|
|
1079
|
+
deleteModelContext(contextHash)
|
|
1080
|
+
}
|
|
1075
1081
|
} catch (error) {
|
|
1076
1082
|
modelLogger.error('Error saving model preference:', error)
|
|
1077
1083
|
await interaction.editReply({
|
|
@@ -1080,3 +1086,51 @@ export async function handleModelScopeSelectMenu(
|
|
|
1080
1086
|
})
|
|
1081
1087
|
}
|
|
1082
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Handle the "Resend last message" button shown after a session-scope model change.
|
|
1092
|
+
* Aborts any active run and resends the last user prompt with the new model.
|
|
1093
|
+
*/
|
|
1094
|
+
export async function handleModelRetryButton(
|
|
1095
|
+
interaction: ButtonInteraction,
|
|
1096
|
+
): Promise<void> {
|
|
1097
|
+
const customId = interaction.customId
|
|
1098
|
+
if (!customId.startsWith('model_retry:')) {
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
await interaction.deferUpdate()
|
|
1103
|
+
|
|
1104
|
+
const contextHash = customId.replace('model_retry:', '')
|
|
1105
|
+
const context = pendingModelContexts.get(contextHash)
|
|
1106
|
+
|
|
1107
|
+
// Context may have expired (10 min TTL) — still allow the action via runtime alone.
|
|
1108
|
+
const threadId = context?.thread?.id
|
|
1109
|
+
|
|
1110
|
+
if (!threadId) {
|
|
1111
|
+
await interaction.editReply({
|
|
1112
|
+
content: 'Could not find the session thread. Please resend your message manually.',
|
|
1113
|
+
components: [],
|
|
1114
|
+
})
|
|
1115
|
+
return
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const runtime = getRuntime(threadId)
|
|
1119
|
+
if (!runtime) {
|
|
1120
|
+
await interaction.editReply({
|
|
1121
|
+
content: 'No active session found. Please resend your message manually.',
|
|
1122
|
+
components: [],
|
|
1123
|
+
})
|
|
1124
|
+
return
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const retried = await runtime.retryLastUserPrompt()
|
|
1128
|
+
deleteModelContext(contextHash)
|
|
1129
|
+
|
|
1130
|
+
await interaction.editReply({
|
|
1131
|
+
content: retried
|
|
1132
|
+
? `Restarting with new model...`
|
|
1133
|
+
: `Model changed. Send a new message to continue.`,
|
|
1134
|
+
components: [],
|
|
1135
|
+
})
|
|
1136
|
+
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ChatInputCommandInteraction,
|
|
5
|
+
ButtonInteraction,
|
|
6
|
+
ButtonBuilder,
|
|
7
|
+
ButtonStyle,
|
|
8
|
+
ActionRowBuilder,
|
|
5
9
|
ChannelType,
|
|
6
10
|
type ThreadChannel,
|
|
7
11
|
type TextChannel,
|
|
@@ -154,21 +158,69 @@ export async function handleUnsetModelCommand({
|
|
|
154
158
|
: `\`${newModelInfo.model}\` (${formatModelSource(newModelInfo.type, 'agentName' in newModelInfo ? newModelInfo.agentName : undefined)})`
|
|
155
159
|
}
|
|
156
160
|
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
const clearedTypeText = clearedType === 'session' ? 'Session' : 'Channel'
|
|
162
|
+
// Show a button so the user can choose to resend the last message (session scope only).
|
|
163
|
+
// Do not auto-retry — the user may want to add context in a new message instead.
|
|
159
164
|
if (isThread && clearedType === 'session' && sessionId) {
|
|
160
165
|
const runtime = getRuntime(channel.id)
|
|
161
166
|
if (runtime) {
|
|
162
|
-
|
|
167
|
+
const retryButton = new ButtonBuilder()
|
|
168
|
+
.setCustomId(`unset_model_retry:${channel.id}`)
|
|
169
|
+
.setLabel('Resend last message')
|
|
170
|
+
.setStyle(ButtonStyle.Secondary)
|
|
171
|
+
|
|
172
|
+
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(retryButton)
|
|
173
|
+
|
|
174
|
+
await interaction.editReply({
|
|
175
|
+
content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}`,
|
|
176
|
+
components: [actionRow],
|
|
177
|
+
})
|
|
178
|
+
return
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
await interaction.editReply({
|
|
183
|
+
content: `${clearedTypeText} model override removed.\n**Was:** \`${clearedModel}\`\n**Now using:** ${newModelText}`,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Handle the "Resend last message" button after clearing a session model override.
|
|
189
|
+
*/
|
|
190
|
+
export async function handleUnsetModelRetryButton(
|
|
191
|
+
interaction: ButtonInteraction,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
const customId = interaction.customId
|
|
194
|
+
if (!customId.startsWith('unset_model_retry:')) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await interaction.deferUpdate()
|
|
199
|
+
|
|
200
|
+
const threadId = customId.replace('unset_model_retry:', '')
|
|
201
|
+
if (!threadId) {
|
|
202
|
+
await interaction.editReply({
|
|
203
|
+
content: 'Could not find the session. Please resend your message manually.',
|
|
204
|
+
components: [],
|
|
205
|
+
})
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const runtime = getRuntime(threadId)
|
|
210
|
+
if (!runtime) {
|
|
211
|
+
await interaction.editReply({
|
|
212
|
+
content: 'No active session found. Please resend your message manually.',
|
|
213
|
+
components: [],
|
|
214
|
+
})
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const retried = await runtime.retryLastUserPrompt()
|
|
170
219
|
|
|
171
220
|
await interaction.editReply({
|
|
172
|
-
content:
|
|
221
|
+
content: retried
|
|
222
|
+
? 'Restarting with new model...'
|
|
223
|
+
: 'Model changed. Send a new message to continue.',
|
|
224
|
+
components: [],
|
|
173
225
|
})
|
|
174
226
|
}
|
|
@@ -57,8 +57,12 @@ import {
|
|
|
57
57
|
handleProviderSelectMenu,
|
|
58
58
|
handleModelSelectMenu,
|
|
59
59
|
handleModelScopeSelectMenu,
|
|
60
|
+
handleModelRetryButton,
|
|
60
61
|
} from './commands/model.js'
|
|
61
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
handleUnsetModelCommand,
|
|
64
|
+
handleUnsetModelRetryButton,
|
|
65
|
+
} from './commands/unset-model.js'
|
|
62
66
|
import {
|
|
63
67
|
handleLoginCommand,
|
|
64
68
|
handleLoginSelect,
|
|
@@ -116,6 +120,7 @@ import {
|
|
|
116
120
|
handleModelVariantCommand,
|
|
117
121
|
handleVariantQuickSelectMenu,
|
|
118
122
|
handleVariantScopeSelectMenu,
|
|
123
|
+
handleVariantRetryButton,
|
|
119
124
|
} from './commands/model-variant.js'
|
|
120
125
|
import { hasOttoBotPermission } from './discord-utils.js'
|
|
121
126
|
import { createLogger, LogPrefix } from './logger.js'
|
|
@@ -463,6 +468,21 @@ export function registerInteractionHandler({
|
|
|
463
468
|
return
|
|
464
469
|
}
|
|
465
470
|
|
|
471
|
+
if (customId.startsWith('model_retry:')) {
|
|
472
|
+
await handleModelRetryButton(interaction)
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (customId.startsWith('variant_retry:')) {
|
|
477
|
+
await handleVariantRetryButton(interaction)
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (customId.startsWith('unset_model_retry:')) {
|
|
482
|
+
await handleUnsetModelRetryButton(interaction)
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
466
486
|
return
|
|
467
487
|
}
|
|
468
488
|
|