lazy-gravity 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -15
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +353 -147
- package/dist/bot/telegramCommands.js +428 -0
- package/dist/bot/telegramMessageHandler.js +304 -0
- package/dist/bot/telegramProjectCommand.js +137 -0
- package/dist/bot/workspaceQueue.js +61 -0
- package/dist/commands/joinCommandHandler.js +4 -1
- package/dist/database/telegramBindingRepository.js +97 -0
- package/dist/database/userPreferenceRepository.js +46 -1
- package/dist/events/interactionCreateHandler.js +36 -0
- package/dist/events/messageCreateHandler.js +11 -7
- package/dist/handlers/approvalButtonAction.js +99 -0
- package/dist/handlers/autoAcceptButtonAction.js +43 -0
- package/dist/handlers/buttonHandler.js +55 -0
- package/dist/handlers/commandHandler.js +44 -0
- package/dist/handlers/errorPopupButtonAction.js +137 -0
- package/dist/handlers/messageHandler.js +70 -0
- package/dist/handlers/modeSelectAction.js +63 -0
- package/dist/handlers/modelButtonAction.js +102 -0
- package/dist/handlers/planningButtonAction.js +118 -0
- package/dist/handlers/selectHandler.js +41 -0
- package/dist/handlers/templateButtonAction.js +54 -0
- package/dist/platform/adapter.js +8 -0
- package/dist/platform/discord/discordAdapter.js +99 -0
- package/dist/platform/discord/index.js +15 -0
- package/dist/platform/discord/wrappers.js +331 -0
- package/dist/platform/index.js +18 -0
- package/dist/platform/richContentBuilder.js +76 -0
- package/dist/platform/telegram/index.js +16 -0
- package/dist/platform/telegram/telegramAdapter.js +195 -0
- package/dist/platform/telegram/telegramFormatter.js +134 -0
- package/dist/platform/telegram/wrappers.js +329 -0
- package/dist/platform/types.js +28 -0
- package/dist/services/approvalDetector.js +15 -2
- package/dist/services/cdpBridgeManager.js +91 -146
- package/dist/services/defaultModelApplicator.js +54 -0
- package/dist/services/modeService.js +16 -1
- package/dist/services/modelService.js +57 -16
- package/dist/services/notificationSender.js +149 -0
- package/dist/services/responseMonitor.js +1 -2
- package/dist/ui/autoAcceptUi.js +37 -0
- package/dist/ui/modeUi.js +38 -1
- package/dist/ui/modelsUi.js +96 -0
- package/dist/ui/outputUi.js +32 -0
- package/dist/ui/projectListUi.js +55 -0
- package/dist/ui/screenshotUi.js +26 -0
- package/dist/ui/sessionPickerUi.js +35 -1
- package/dist/ui/templateUi.js +41 -0
- package/dist/utils/configLoader.js +63 -12
- package/dist/utils/lockfile.js +5 -5
- package/dist/utils/logger.js +7 -0
- package/dist/utils/telegramImageHandler.js +127 -0
- package/package.json +4 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic notification builders.
|
|
4
|
+
*
|
|
5
|
+
* Every exported function is **pure** — no side effects, no I/O.
|
|
6
|
+
* They return a `MessagePayload` that any platform adapter can render.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.buildApprovalNotification = buildApprovalNotification;
|
|
10
|
+
exports.buildPlanningNotification = buildPlanningNotification;
|
|
11
|
+
exports.buildErrorPopupNotification = buildErrorPopupNotification;
|
|
12
|
+
exports.buildAutoApprovedNotification = buildAutoApprovedNotification;
|
|
13
|
+
exports.buildResolvedOverlay = buildResolvedOverlay;
|
|
14
|
+
exports.buildStatusNotification = buildStatusNotification;
|
|
15
|
+
exports.buildProgressNotification = buildProgressNotification;
|
|
16
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Custom-ID prefix constants (must stay in sync with cdpBridgeManager)
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const APPROVE_ACTION_PREFIX = 'approve_action';
|
|
21
|
+
const ALWAYS_ALLOW_ACTION_PREFIX = 'always_allow_action';
|
|
22
|
+
const DENY_ACTION_PREFIX = 'deny_action';
|
|
23
|
+
const PLANNING_OPEN_ACTION_PREFIX = 'planning_open_action';
|
|
24
|
+
const PLANNING_PROCEED_ACTION_PREFIX = 'planning_proceed_action';
|
|
25
|
+
const ERROR_POPUP_DISMISS_ACTION_PREFIX = 'error_popup_dismiss_action';
|
|
26
|
+
const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
|
|
27
|
+
const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Notification colours
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/** Warning orange — used for approval requests. */
|
|
32
|
+
const COLOR_APPROVAL = 0xFFA500;
|
|
33
|
+
/** Blue — used for planning / informational notifications. */
|
|
34
|
+
const COLOR_PLANNING = 0x3498DB;
|
|
35
|
+
/** Red — used for error notifications. */
|
|
36
|
+
const COLOR_ERROR = 0xE74C3C;
|
|
37
|
+
/** Green — used for success / progress notifications. */
|
|
38
|
+
const COLOR_SUCCESS = 0x2ECC71;
|
|
39
|
+
/** Grey — used for neutral status notifications. */
|
|
40
|
+
const COLOR_NEUTRAL = 0x95A5A6;
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Phase → colour mapping for progress notifications
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
const PHASE_COLOURS = {
|
|
45
|
+
thinking: COLOR_PLANNING,
|
|
46
|
+
generating: COLOR_SUCCESS,
|
|
47
|
+
error: COLOR_ERROR,
|
|
48
|
+
waiting: COLOR_NEUTRAL,
|
|
49
|
+
complete: COLOR_SUCCESS,
|
|
50
|
+
};
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Internal helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/** Create a single button definition. */
|
|
55
|
+
function button(customId, label, style) {
|
|
56
|
+
return { type: 'button', customId, label, style };
|
|
57
|
+
}
|
|
58
|
+
/** Wrap one or more buttons into a component row. */
|
|
59
|
+
function buttonRow(...buttons) {
|
|
60
|
+
return { components: buttons };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a colon-separated customId following the project convention:
|
|
64
|
+
* `<prefix>:<projectName>` or `<prefix>:<projectName>:<channelId>`
|
|
65
|
+
*/
|
|
66
|
+
function customId(prefix, projectName, channelId) {
|
|
67
|
+
if (channelId !== null && channelId.trim().length > 0) {
|
|
68
|
+
return `${prefix}:${projectName}:${channelId}`;
|
|
69
|
+
}
|
|
70
|
+
return `${prefix}:${projectName}`;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Public API
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
/** Build the approval notification message. */
|
|
76
|
+
function buildApprovalNotification(opts) {
|
|
77
|
+
const { title, description, projectName, channelId, toolNames, extraFields } = opts;
|
|
78
|
+
const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, description), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_APPROVAL), (rc) => (0, richContentBuilder_1.addField)(rc, 'Project', projectName, true), (rc) => toolNames && toolNames.length > 0
|
|
79
|
+
? (0, richContentBuilder_1.addField)(rc, 'Tools', toolNames.join(', '), true)
|
|
80
|
+
: rc, (rc) => extraFields
|
|
81
|
+
? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
|
|
82
|
+
: rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Approval required'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
|
|
83
|
+
const components = [
|
|
84
|
+
buttonRow(button(customId(APPROVE_ACTION_PREFIX, projectName, channelId), 'Allow', 'success'), button(customId(ALWAYS_ALLOW_ACTION_PREFIX, projectName, channelId), 'Allow Chat', 'primary'), button(customId(DENY_ACTION_PREFIX, projectName, channelId), 'Deny', 'danger')),
|
|
85
|
+
];
|
|
86
|
+
return { richContent, components };
|
|
87
|
+
}
|
|
88
|
+
/** Build the planning mode notification message. */
|
|
89
|
+
function buildPlanningNotification(opts) {
|
|
90
|
+
const { title, description, projectName, channelId, extraFields } = opts;
|
|
91
|
+
const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, description), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_PLANNING), (rc) => extraFields
|
|
92
|
+
? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
|
|
93
|
+
: rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Planning mode detected'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
|
|
94
|
+
const components = [
|
|
95
|
+
buttonRow(button(customId(PLANNING_OPEN_ACTION_PREFIX, projectName, channelId), 'Open', 'primary'), button(customId(PLANNING_PROCEED_ACTION_PREFIX, projectName, channelId), 'Proceed', 'success')),
|
|
96
|
+
];
|
|
97
|
+
return { richContent, components };
|
|
98
|
+
}
|
|
99
|
+
/** Build the error popup notification message. */
|
|
100
|
+
function buildErrorPopupNotification(opts) {
|
|
101
|
+
const { title, errorMessage, projectName, channelId, extraFields } = opts;
|
|
102
|
+
const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, errorMessage), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_ERROR), (rc) => extraFields
|
|
103
|
+
? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
|
|
104
|
+
: rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Agent error detected'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
|
|
105
|
+
const components = [
|
|
106
|
+
buttonRow(button(customId(ERROR_POPUP_DISMISS_ACTION_PREFIX, projectName, channelId), 'Dismiss', 'secondary'), button(customId(ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX, projectName, channelId), 'Copy Debug', 'primary'), button(customId(ERROR_POPUP_RETRY_ACTION_PREFIX, projectName, channelId), 'Retry', 'success')),
|
|
107
|
+
];
|
|
108
|
+
return { richContent, components };
|
|
109
|
+
}
|
|
110
|
+
/** Build an auto-approved notification (shown when auto-accept fires). */
|
|
111
|
+
function buildAutoApprovedNotification(opts) {
|
|
112
|
+
const { accepted, projectName, description, approveText } = opts;
|
|
113
|
+
const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, accepted ? 'Auto-approved' : 'Auto-approve failed'), (rc) => (0, richContentBuilder_1.withDescription)(rc, accepted
|
|
114
|
+
? 'An action was automatically approved.'
|
|
115
|
+
: 'Auto-approve attempted but failed. Manual approval required.'), (rc) => (0, richContentBuilder_1.withColor)(rc, accepted ? COLOR_SUCCESS : 0xF39C12), (rc) => (0, richContentBuilder_1.addField)(rc, 'Auto-approve mode', 'ON', true), (rc) => (0, richContentBuilder_1.addField)(rc, 'Workspace', projectName, true), (rc) => (0, richContentBuilder_1.addField)(rc, 'Result', accepted ? 'Executed Always Allow/Allow' : 'Manual approval required', true), (rc) => description ? (0, richContentBuilder_1.addField)(rc, 'Action Detail', description.substring(0, 1024), false) : rc, (rc) => approveText ? (0, richContentBuilder_1.addField)(rc, 'Approved via', approveText, true) : rc, (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
|
|
116
|
+
return { richContent };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build a "resolved" overlay from an existing notification payload.
|
|
120
|
+
* Changes colour to grey, adds a Status field, and disables all buttons.
|
|
121
|
+
*/
|
|
122
|
+
function buildResolvedOverlay(original, statusText) {
|
|
123
|
+
const rc = (0, richContentBuilder_1.pipe)(original.richContent ?? (0, richContentBuilder_1.createRichContent)(), (r) => (0, richContentBuilder_1.withColor)(r, COLOR_NEUTRAL), (r) => (0, richContentBuilder_1.addField)(r, 'Status', statusText, false));
|
|
124
|
+
const disabledComponents = original.components
|
|
125
|
+
? original.components.map((row) => ({
|
|
126
|
+
components: row.components.map((comp) => comp.type === 'button' ? { ...comp, disabled: true } : comp),
|
|
127
|
+
}))
|
|
128
|
+
: undefined;
|
|
129
|
+
return {
|
|
130
|
+
...original,
|
|
131
|
+
richContent: rc,
|
|
132
|
+
components: disabledComponents,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Build a simple status embed. */
|
|
136
|
+
function buildStatusNotification(opts) {
|
|
137
|
+
const { title, description, color, fields } = opts;
|
|
138
|
+
const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, description), (rc) => (0, richContentBuilder_1.withColor)(rc, color ?? COLOR_NEUTRAL), (rc) => fields
|
|
139
|
+
? fields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
|
|
140
|
+
: rc);
|
|
141
|
+
return { richContent };
|
|
142
|
+
}
|
|
143
|
+
/** Build a progress / phase notification (e.g. "Thinking...", "Generating..."). */
|
|
144
|
+
function buildProgressNotification(opts) {
|
|
145
|
+
const { phase, projectName, detail } = opts;
|
|
146
|
+
const phaseColor = PHASE_COLOURS[phase.toLowerCase()] ?? COLOR_NEUTRAL;
|
|
147
|
+
const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, phase), (rc) => (detail ? (0, richContentBuilder_1.withDescription)(rc, detail) : rc), (rc) => (0, richContentBuilder_1.withColor)(rc, phaseColor), (rc) => (projectName ? (0, richContentBuilder_1.addField)(rc, 'Project', projectName, true) : rc));
|
|
148
|
+
return { richContent };
|
|
149
|
+
}
|
|
@@ -474,8 +474,7 @@ class ResponseMonitor {
|
|
|
474
474
|
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
475
475
|
this.maxDurationMs = options.maxDurationMs ?? 300000;
|
|
476
476
|
this.stopGoneConfirmCount = options.stopGoneConfirmCount ?? 3;
|
|
477
|
-
this.extractionMode = options.extractionMode
|
|
478
|
-
?? (process.env.EXTRACTION_MODE === 'legacy' ? 'legacy' : 'structured');
|
|
477
|
+
this.extractionMode = options.extractionMode ?? 'structured';
|
|
479
478
|
this.onProgress = options.onProgress;
|
|
480
479
|
this.onComplete = options.onComplete;
|
|
481
480
|
this.onTimeout = options.onTimeout;
|
package/dist/ui/autoAcceptUi.js
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AUTOACCEPT_BTN_REFRESH = exports.AUTOACCEPT_BTN_OFF = exports.AUTOACCEPT_BTN_ON = void 0;
|
|
4
|
+
exports.buildAutoAcceptPayload = buildAutoAcceptPayload;
|
|
4
5
|
exports.sendAutoAcceptUI = sendAutoAcceptUI;
|
|
5
6
|
const discord_js_1 = require("discord.js");
|
|
7
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
6
8
|
exports.AUTOACCEPT_BTN_ON = 'autoaccept_btn_on';
|
|
7
9
|
exports.AUTOACCEPT_BTN_OFF = 'autoaccept_btn_off';
|
|
8
10
|
exports.AUTOACCEPT_BTN_REFRESH = 'autoaccept_btn_refresh';
|
|
11
|
+
/**
|
|
12
|
+
* Build a platform-agnostic MessagePayload for auto-accept UI.
|
|
13
|
+
*/
|
|
14
|
+
function buildAutoAcceptPayload(enabled) {
|
|
15
|
+
const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withFooter)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Auto-accept Management'), enabled ? 0x2ECC71 : 0x95A5A6), `**Current Status:** ${enabled ? 'ON' : 'OFF'}\n\n` +
|
|
16
|
+
'ON: approval dialogs are automatically allowed.\n' +
|
|
17
|
+
'OFF: approval dialogs require manual action.'), 'Use buttons below to change mode'));
|
|
18
|
+
return {
|
|
19
|
+
richContent: rc,
|
|
20
|
+
components: [
|
|
21
|
+
{
|
|
22
|
+
components: [
|
|
23
|
+
{
|
|
24
|
+
type: 'button',
|
|
25
|
+
customId: exports.AUTOACCEPT_BTN_ON,
|
|
26
|
+
label: 'Turn ON',
|
|
27
|
+
style: enabled ? 'success' : 'secondary',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'button',
|
|
31
|
+
customId: exports.AUTOACCEPT_BTN_OFF,
|
|
32
|
+
label: 'Turn OFF',
|
|
33
|
+
style: !enabled ? 'danger' : 'secondary',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'button',
|
|
37
|
+
customId: exports.AUTOACCEPT_BTN_REFRESH,
|
|
38
|
+
label: 'Refresh',
|
|
39
|
+
style: 'primary',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
9
46
|
async function sendAutoAcceptUI(target, autoAcceptService) {
|
|
10
47
|
const enabled = autoAcceptService.isEnabled();
|
|
11
48
|
const embed = new discord_js_1.EmbedBuilder()
|
package/dist/ui/modeUi.js
CHANGED
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildModePayload = buildModePayload;
|
|
3
4
|
exports.sendModeUI = sendModeUI;
|
|
4
5
|
const discord_js_1 = require("discord.js");
|
|
5
6
|
const modeService_1 = require("../services/modeService");
|
|
7
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
8
|
+
/**
|
|
9
|
+
* Build a platform-agnostic MessagePayload for mode selection UI.
|
|
10
|
+
* @param currentMode The current mode name
|
|
11
|
+
* @param isPending Whether the mode is pending sync to Antigravity
|
|
12
|
+
*/
|
|
13
|
+
function buildModePayload(currentMode, isPending = false) {
|
|
14
|
+
const pendingSuffix = isPending ? ' (pending sync)' : '';
|
|
15
|
+
const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withFooter)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Mode Management'), 0x57F287), `**Current Mode:** ${modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode}${pendingSuffix}\n` +
|
|
16
|
+
`${modeService_1.MODE_DESCRIPTIONS[currentMode] || ''}\n\n` +
|
|
17
|
+
`**Available Modes (${modeService_1.AVAILABLE_MODES.length})**\n` +
|
|
18
|
+
modeService_1.AVAILABLE_MODES.map(m => {
|
|
19
|
+
const icon = m === currentMode ? '[x]' : '[ ]';
|
|
20
|
+
return `${icon} **${modeService_1.MODE_DISPLAY_NAMES[m] || m}** — ${modeService_1.MODE_DESCRIPTIONS[m] || ''}`;
|
|
21
|
+
}).join('\n')), 'Select a mode from the dropdown below'));
|
|
22
|
+
return {
|
|
23
|
+
richContent: rc,
|
|
24
|
+
components: [
|
|
25
|
+
{
|
|
26
|
+
components: [
|
|
27
|
+
{
|
|
28
|
+
type: 'selectMenu',
|
|
29
|
+
customId: 'mode_select',
|
|
30
|
+
placeholder: 'Select a mode...',
|
|
31
|
+
options: modeService_1.AVAILABLE_MODES.map(m => ({
|
|
32
|
+
label: modeService_1.MODE_DISPLAY_NAMES[m] || m,
|
|
33
|
+
description: modeService_1.MODE_DESCRIPTIONS[m] || '',
|
|
34
|
+
value: m,
|
|
35
|
+
isDefault: m === currentMode,
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
6
43
|
/**
|
|
7
44
|
* Build and send the interactive UI for the /mode command (dropdown style)
|
|
8
45
|
*/
|
|
@@ -13,7 +50,7 @@ async function sendModeUI(target, modeService, deps) {
|
|
|
13
50
|
if (cdp) {
|
|
14
51
|
const liveMode = await cdp.getCurrentMode();
|
|
15
52
|
if (liveMode) {
|
|
16
|
-
modeService.setMode(liveMode);
|
|
53
|
+
modeService.setMode(liveMode, true);
|
|
17
54
|
}
|
|
18
55
|
}
|
|
19
56
|
}
|
package/dist/ui/modelsUi.js
CHANGED
|
@@ -1,8 +1,104 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildModelsPayload = buildModelsPayload;
|
|
3
4
|
exports.buildModelsUI = buildModelsUI;
|
|
4
5
|
exports.sendModelsUI = sendModelsUI;
|
|
5
6
|
const discord_js_1 = require("discord.js");
|
|
7
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
8
|
+
/**
|
|
9
|
+
* Build a platform-agnostic MessagePayload for model selection UI.
|
|
10
|
+
*/
|
|
11
|
+
function buildModelsPayload(models, currentModel, quotaData, defaultModel = null) {
|
|
12
|
+
if (models.length === 0)
|
|
13
|
+
return null;
|
|
14
|
+
function formatQuota(mName, current) {
|
|
15
|
+
if (!mName)
|
|
16
|
+
return `${current ? '[x]' : '[ ]'} Unknown`;
|
|
17
|
+
const normalize = (s) => s.toLowerCase().replace(/[\s\-_]/g, '');
|
|
18
|
+
const nName = normalize(mName);
|
|
19
|
+
const q = quotaData.find(q => {
|
|
20
|
+
const nLabel = normalize(q.label);
|
|
21
|
+
const nModel = normalize(q.model || '');
|
|
22
|
+
return nLabel === nName || nModel === nName
|
|
23
|
+
|| nName.includes(nLabel) || nLabel.includes(nName)
|
|
24
|
+
|| (nModel && (nName.includes(nModel) || nModel.includes(nName)));
|
|
25
|
+
});
|
|
26
|
+
if (!q || !q.quotaInfo)
|
|
27
|
+
return `${current ? '[x]' : '[ ]'} ${mName}`;
|
|
28
|
+
const rem = q.quotaInfo.remainingFraction;
|
|
29
|
+
const resetTime = q.quotaInfo.resetTime ? new Date(q.quotaInfo.resetTime) : null;
|
|
30
|
+
const diffMs = resetTime ? resetTime.getTime() - Date.now() : 0;
|
|
31
|
+
let timeStr = 'Ready';
|
|
32
|
+
if (diffMs > 0) {
|
|
33
|
+
const mins = Math.ceil(diffMs / 60000);
|
|
34
|
+
if (mins < 60)
|
|
35
|
+
timeStr = `${mins}m`;
|
|
36
|
+
else
|
|
37
|
+
timeStr = `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
38
|
+
}
|
|
39
|
+
if (rem !== undefined && rem !== null) {
|
|
40
|
+
const percent = Math.round(rem * 100);
|
|
41
|
+
return `${current ? '[x]' : '[ ]'} ${mName} ${percent}% (${timeStr})`;
|
|
42
|
+
}
|
|
43
|
+
return `${current ? '[x]' : '[ ]'} ${mName} (${timeStr})`;
|
|
44
|
+
}
|
|
45
|
+
const currentModelFormatted = currentModel ? formatQuota(currentModel, true) : 'Unknown';
|
|
46
|
+
const defaultLine = defaultModel
|
|
47
|
+
? `\n**Default:** ⭐ ${defaultModel}`
|
|
48
|
+
: '\n**Default:** Not set';
|
|
49
|
+
const modelLines = models.map(m => {
|
|
50
|
+
const isCurrent = m === currentModel;
|
|
51
|
+
const isDefault = defaultModel != null && m.toLowerCase() === defaultModel.toLowerCase();
|
|
52
|
+
const star = isDefault ? ' ⭐' : '';
|
|
53
|
+
return `${formatQuota(m, isCurrent)}${star}`;
|
|
54
|
+
}).join('\n');
|
|
55
|
+
const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withFooter)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Model Management'), 0x5865F2), `**Current Model:**\n${currentModelFormatted}${defaultLine}\n\n` +
|
|
56
|
+
`**Available Models (${models.length})**\n` +
|
|
57
|
+
modelLines), 'Latest quota information retrieved'));
|
|
58
|
+
// Use 1 button per row so model names are fully readable on Telegram.
|
|
59
|
+
// Telegram inline keyboard buttons are narrow; 5-per-row truncates names.
|
|
60
|
+
const rows = [];
|
|
61
|
+
for (const mName of models.slice(0, 24)) {
|
|
62
|
+
const safeName = mName.length > 80 ? mName.substring(0, 77) + '...' : mName;
|
|
63
|
+
const isDefault = defaultModel != null && mName.toLowerCase() === defaultModel.toLowerCase();
|
|
64
|
+
const prefix = mName === currentModel ? '✓ ' : '';
|
|
65
|
+
const suffix = isDefault ? ' ⭐' : '';
|
|
66
|
+
rows.push({
|
|
67
|
+
components: [{
|
|
68
|
+
type: 'button',
|
|
69
|
+
customId: `model_btn_${mName}`,
|
|
70
|
+
label: `${prefix}${safeName}${suffix}`,
|
|
71
|
+
style: mName === currentModel ? 'success' : 'secondary',
|
|
72
|
+
}],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Default model action buttons
|
|
76
|
+
const defaultBtnRow = {
|
|
77
|
+
components: defaultModel
|
|
78
|
+
? [{
|
|
79
|
+
type: 'button',
|
|
80
|
+
customId: 'model_clear_default_btn',
|
|
81
|
+
label: 'Clear Default',
|
|
82
|
+
style: 'danger',
|
|
83
|
+
}]
|
|
84
|
+
: [{
|
|
85
|
+
type: 'button',
|
|
86
|
+
customId: 'model_set_default_btn',
|
|
87
|
+
label: 'Set Current as Default',
|
|
88
|
+
style: 'primary',
|
|
89
|
+
}],
|
|
90
|
+
};
|
|
91
|
+
rows.push(defaultBtnRow);
|
|
92
|
+
rows.push({
|
|
93
|
+
components: [{
|
|
94
|
+
type: 'button',
|
|
95
|
+
customId: 'model_refresh_btn',
|
|
96
|
+
label: 'Refresh',
|
|
97
|
+
style: 'primary',
|
|
98
|
+
}],
|
|
99
|
+
});
|
|
100
|
+
return { richContent: rc, components: rows };
|
|
101
|
+
}
|
|
6
102
|
/**
|
|
7
103
|
* Build the embed + button components for the models UI.
|
|
8
104
|
* Returns null when CDP is unavailable or no models are found.
|
package/dist/ui/outputUi.js
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OUTPUT_BTN_PLAIN = exports.OUTPUT_BTN_EMBED = void 0;
|
|
4
|
+
exports.buildOutputPayload = buildOutputPayload;
|
|
4
5
|
exports.sendOutputUI = sendOutputUI;
|
|
5
6
|
const discord_js_1 = require("discord.js");
|
|
7
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
6
8
|
exports.OUTPUT_BTN_EMBED = 'output_btn_embed';
|
|
7
9
|
exports.OUTPUT_BTN_PLAIN = 'output_btn_plain';
|
|
10
|
+
/**
|
|
11
|
+
* Build a platform-agnostic MessagePayload for output format UI.
|
|
12
|
+
*/
|
|
13
|
+
function buildOutputPayload(currentFormat) {
|
|
14
|
+
const isEmbed = currentFormat === 'embed';
|
|
15
|
+
const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withFooter)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Output Format'), isEmbed ? 0x5865F2 : 0x2ECC71), `**Current Format:** ${isEmbed ? 'Embed' : 'Plain Text'}\n\n` +
|
|
16
|
+
'Embed: Rich formatting with colored borders (default).\n' +
|
|
17
|
+
'Plain Text: Simple text output, easy to copy on mobile.'), 'Use buttons below to change format'));
|
|
18
|
+
return {
|
|
19
|
+
richContent: rc,
|
|
20
|
+
components: [
|
|
21
|
+
{
|
|
22
|
+
components: [
|
|
23
|
+
{
|
|
24
|
+
type: 'button',
|
|
25
|
+
customId: exports.OUTPUT_BTN_EMBED,
|
|
26
|
+
label: 'Embed',
|
|
27
|
+
style: isEmbed ? 'primary' : 'secondary',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'button',
|
|
31
|
+
customId: exports.OUTPUT_BTN_PLAIN,
|
|
32
|
+
label: 'Plain Text',
|
|
33
|
+
style: !isEmbed ? 'success' : 'secondary',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
8
40
|
async function sendOutputUI(target, currentFormat) {
|
|
9
41
|
const isEmbed = currentFormat === 'embed';
|
|
10
42
|
const embed = new discord_js_1.EmbedBuilder()
|
package/dist/ui/projectListUi.js
CHANGED
|
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ITEMS_PER_PAGE = exports.PROJECT_PAGE_PREFIX = exports.WORKSPACE_SELECT_ID = exports.PROJECT_SELECT_ID = void 0;
|
|
4
4
|
exports.parseProjectPageId = parseProjectPageId;
|
|
5
5
|
exports.isProjectSelectId = isProjectSelectId;
|
|
6
|
+
exports.buildProjectListPayload = buildProjectListPayload;
|
|
6
7
|
exports.buildProjectListUI = buildProjectListUI;
|
|
7
8
|
const discord_js_1 = require("discord.js");
|
|
8
9
|
const i18n_1 = require("../utils/i18n");
|
|
10
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
9
11
|
/** Select menu custom ID (legacy, page 0) */
|
|
10
12
|
exports.PROJECT_SELECT_ID = 'project_select';
|
|
11
13
|
/** Backward compatibility: also accept old ID */
|
|
@@ -33,6 +35,59 @@ function isProjectSelectId(customId) {
|
|
|
33
35
|
customId === exports.WORKSPACE_SELECT_ID ||
|
|
34
36
|
customId.startsWith(`${exports.PROJECT_SELECT_ID}:`));
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Build a platform-agnostic MessagePayload for project list UI.
|
|
40
|
+
*/
|
|
41
|
+
function buildProjectListPayload(workspaces, page = 0) {
|
|
42
|
+
const totalPages = Math.max(1, Math.ceil(workspaces.length / exports.ITEMS_PER_PAGE));
|
|
43
|
+
const safePage = Math.max(0, Math.min(page, totalPages - 1));
|
|
44
|
+
let rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.createRichContent)(), (0, i18n_1.t)('Select a project to auto-create a category and session channel')), 'Projects'), 0x5865F2));
|
|
45
|
+
if (workspaces.length === 0) {
|
|
46
|
+
return { richContent: rc, components: [] };
|
|
47
|
+
}
|
|
48
|
+
if (totalPages > 1) {
|
|
49
|
+
rc = (0, richContentBuilder_1.withFooter)(rc, `Page ${safePage + 1} / ${totalPages} (${workspaces.length} projects total)`);
|
|
50
|
+
}
|
|
51
|
+
const start = safePage * exports.ITEMS_PER_PAGE;
|
|
52
|
+
const end = Math.min(start + exports.ITEMS_PER_PAGE, workspaces.length);
|
|
53
|
+
const pageItems = workspaces.slice(start, end);
|
|
54
|
+
const components = [
|
|
55
|
+
{
|
|
56
|
+
components: [
|
|
57
|
+
{
|
|
58
|
+
type: 'selectMenu',
|
|
59
|
+
customId: `${exports.PROJECT_SELECT_ID}:${safePage}`,
|
|
60
|
+
placeholder: (0, i18n_1.t)('Select a project...'),
|
|
61
|
+
options: pageItems.map((ws) => ({
|
|
62
|
+
label: ws,
|
|
63
|
+
value: ws,
|
|
64
|
+
})),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
if (totalPages > 1) {
|
|
70
|
+
components.push({
|
|
71
|
+
components: [
|
|
72
|
+
{
|
|
73
|
+
type: 'button',
|
|
74
|
+
customId: `${exports.PROJECT_PAGE_PREFIX}:${Math.max(0, safePage - 1)}`,
|
|
75
|
+
label: '\u25C0 Prev',
|
|
76
|
+
style: 'secondary',
|
|
77
|
+
disabled: safePage === 0,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'button',
|
|
81
|
+
customId: `${exports.PROJECT_PAGE_PREFIX}:${safePage + 1}`,
|
|
82
|
+
label: 'Next \u25B6',
|
|
83
|
+
style: 'secondary',
|
|
84
|
+
disabled: safePage >= totalPages - 1,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { richContent: rc, components };
|
|
90
|
+
}
|
|
36
91
|
/**
|
|
37
92
|
* Build the project list UI with select menu and optional Prev/Next buttons.
|
|
38
93
|
*
|
package/dist/ui/screenshotUi.js
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildScreenshotPayload = buildScreenshotPayload;
|
|
3
4
|
exports.handleScreenshot = handleScreenshot;
|
|
4
5
|
const discord_js_1 = require("discord.js");
|
|
5
6
|
const screenshotService_1 = require("../services/screenshotService");
|
|
7
|
+
/**
|
|
8
|
+
* Build a platform-agnostic MessagePayload containing the screenshot.
|
|
9
|
+
* Returns a payload with the screenshot as a file attachment, or an error text.
|
|
10
|
+
*/
|
|
11
|
+
async function buildScreenshotPayload(cdp) {
|
|
12
|
+
if (!cdp) {
|
|
13
|
+
return { text: 'Not connected to Antigravity.' };
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const screenshot = new screenshotService_1.ScreenshotService({ cdpService: cdp });
|
|
17
|
+
const result = await screenshot.capture({ format: 'png' });
|
|
18
|
+
if (result.success && result.buffer) {
|
|
19
|
+
const file = {
|
|
20
|
+
name: 'screenshot.png',
|
|
21
|
+
data: result.buffer,
|
|
22
|
+
contentType: 'image/png',
|
|
23
|
+
};
|
|
24
|
+
return { files: [file] };
|
|
25
|
+
}
|
|
26
|
+
return { text: `Screenshot failed: ${result.error ?? 'Unknown error'}` };
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { text: `Screenshot error: ${e.message}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
6
32
|
/**
|
|
7
33
|
* Capture a screenshot and send it to Discord
|
|
8
34
|
*/
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SESSION_SELECT_ID = void 0;
|
|
4
4
|
exports.isSessionSelectId = isSessionSelectId;
|
|
5
|
+
exports.buildSessionPickerPayload = buildSessionPickerPayload;
|
|
5
6
|
exports.buildSessionPickerUI = buildSessionPickerUI;
|
|
6
7
|
const discord_js_1 = require("discord.js");
|
|
7
8
|
const i18n_1 = require("../utils/i18n");
|
|
9
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
8
10
|
/** Select menu custom ID for session picker */
|
|
9
11
|
exports.SESSION_SELECT_ID = 'session_select';
|
|
10
12
|
/** Maximum items per select menu (Discord limit) */
|
|
@@ -15,6 +17,38 @@ const MAX_SELECT_OPTIONS = 25;
|
|
|
15
17
|
function isSessionSelectId(customId) {
|
|
16
18
|
return customId === exports.SESSION_SELECT_ID;
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a platform-agnostic MessagePayload for session picker UI.
|
|
22
|
+
*/
|
|
23
|
+
function buildSessionPickerPayload(sessions) {
|
|
24
|
+
const MAX_OPTIONS = 25;
|
|
25
|
+
let rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), (0, i18n_1.t)('Join Session')), 0x5865F2));
|
|
26
|
+
if (sessions.length === 0) {
|
|
27
|
+
rc = (0, richContentBuilder_1.withDescription)(rc, (0, i18n_1.t)('No sessions found in the Antigravity side panel.'));
|
|
28
|
+
return { richContent: rc, components: [] };
|
|
29
|
+
}
|
|
30
|
+
rc = (0, richContentBuilder_1.withDescription)(rc, (0, i18n_1.t)('Select a session to join ({{count}} found)', { count: sessions.length }));
|
|
31
|
+
const pageItems = sessions.slice(0, MAX_OPTIONS);
|
|
32
|
+
return {
|
|
33
|
+
richContent: rc,
|
|
34
|
+
components: [
|
|
35
|
+
{
|
|
36
|
+
components: [
|
|
37
|
+
{
|
|
38
|
+
type: 'selectMenu',
|
|
39
|
+
customId: exports.SESSION_SELECT_ID,
|
|
40
|
+
placeholder: (0, i18n_1.t)('Select a session...'),
|
|
41
|
+
options: pageItems.map((session) => ({
|
|
42
|
+
label: session.title.slice(0, 100),
|
|
43
|
+
value: session.title.slice(0, 100),
|
|
44
|
+
description: session.isActive ? (0, i18n_1.t)('Current') : undefined,
|
|
45
|
+
})),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
18
52
|
/**
|
|
19
53
|
* Build the session picker UI with a select menu.
|
|
20
54
|
*
|
|
@@ -30,7 +64,7 @@ function buildSessionPickerUI(sessions) {
|
|
|
30
64
|
embed.setDescription((0, i18n_1.t)('No sessions found in the Antigravity side panel.'));
|
|
31
65
|
return { embeds: [embed], components: [] };
|
|
32
66
|
}
|
|
33
|
-
embed.setDescription((0, i18n_1.t)(
|
|
67
|
+
embed.setDescription((0, i18n_1.t)('Select a session to join ({{count}} found)', { count: sessions.length }));
|
|
34
68
|
const pageItems = sessions.slice(0, MAX_SELECT_OPTIONS);
|
|
35
69
|
const options = pageItems.map((session) => ({
|
|
36
70
|
label: session.title.slice(0, 100),
|
package/dist/ui/templateUi.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TEMPLATE_BTN_PREFIX = void 0;
|
|
4
4
|
exports.parseTemplateButtonId = parseTemplateButtonId;
|
|
5
|
+
exports.buildTemplatePayload = buildTemplatePayload;
|
|
5
6
|
exports.sendTemplateUI = sendTemplateUI;
|
|
6
7
|
const discord_js_1 = require("discord.js");
|
|
8
|
+
const richContentBuilder_1 = require("../platform/richContentBuilder");
|
|
7
9
|
/** Button customId prefix. Format: template_btn_<id> */
|
|
8
10
|
exports.TEMPLATE_BTN_PREFIX = 'template_btn_';
|
|
9
11
|
const MAX_PROMPT_PREVIEW_LEN = 60;
|
|
@@ -17,6 +19,45 @@ function parseTemplateButtonId(customId) {
|
|
|
17
19
|
return NaN;
|
|
18
20
|
return parseInt(customId.slice(exports.TEMPLATE_BTN_PREFIX.length), 10);
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a platform-agnostic MessagePayload for template list UI.
|
|
24
|
+
*/
|
|
25
|
+
function buildTemplatePayload(templates) {
|
|
26
|
+
if (templates.length === 0) {
|
|
27
|
+
const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Template Management'), 0x57F287), 'No templates registered.\n\n' +
|
|
28
|
+
'Use `/template add name:<name> prompt:<prompt>` to add one.'));
|
|
29
|
+
return { richContent: rc, components: [] };
|
|
30
|
+
}
|
|
31
|
+
const truncate = (text, max) => text.length > max ? `${text.substring(0, max - 3)}...` : text;
|
|
32
|
+
const displayTemplates = templates.slice(0, MAX_BUTTONS);
|
|
33
|
+
const hasMore = templates.length > MAX_BUTTONS;
|
|
34
|
+
const description = displayTemplates
|
|
35
|
+
.map((tpl, i) => `**${i + 1}. ${tpl.name}**\n> ${truncate(tpl.prompt, MAX_PROMPT_PREVIEW_LEN)}`)
|
|
36
|
+
.join('\n\n');
|
|
37
|
+
const footerText = hasMore
|
|
38
|
+
? `${templates.length - MAX_BUTTONS} templates are hidden. Use /template use <name> to execute directly.`
|
|
39
|
+
: 'Click a button to execute the template';
|
|
40
|
+
const rc = (0, richContentBuilder_1.withTimestamp)((0, richContentBuilder_1.withFooter)((0, richContentBuilder_1.withDescription)((0, richContentBuilder_1.withColor)((0, richContentBuilder_1.withTitle)((0, richContentBuilder_1.createRichContent)(), 'Template Management'), 0x57F287), `**Registered Templates (${templates.length})**\n\n${description}`), footerText));
|
|
41
|
+
const rows = [];
|
|
42
|
+
let currentButtons = [];
|
|
43
|
+
for (const tpl of displayTemplates) {
|
|
44
|
+
if (currentButtons.length === 5) {
|
|
45
|
+
rows.push({ components: currentButtons });
|
|
46
|
+
currentButtons = [];
|
|
47
|
+
}
|
|
48
|
+
const safeLabel = tpl.name.length > 80 ? `${tpl.name.substring(0, 77)}...` : tpl.name;
|
|
49
|
+
currentButtons.push({
|
|
50
|
+
type: 'button',
|
|
51
|
+
customId: `${exports.TEMPLATE_BTN_PREFIX}${tpl.id}`,
|
|
52
|
+
label: safeLabel,
|
|
53
|
+
style: 'primary',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (currentButtons.length > 0) {
|
|
57
|
+
rows.push({ components: currentButtons });
|
|
58
|
+
}
|
|
59
|
+
return { richContent: rc, components: rows };
|
|
60
|
+
}
|
|
20
61
|
/**
|
|
21
62
|
* Build and send the template list UI with clickable buttons.
|
|
22
63
|
* Follows the same pattern as modelsUi.ts.
|