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.
Files changed (56) hide show
  1. package/README.md +76 -15
  2. package/dist/bin/commands/doctor.js +19 -2
  3. package/dist/bin/commands/setup.js +286 -70
  4. package/dist/bot/eventRouter.js +70 -0
  5. package/dist/bot/index.js +353 -147
  6. package/dist/bot/telegramCommands.js +428 -0
  7. package/dist/bot/telegramMessageHandler.js +304 -0
  8. package/dist/bot/telegramProjectCommand.js +137 -0
  9. package/dist/bot/workspaceQueue.js +61 -0
  10. package/dist/commands/joinCommandHandler.js +4 -1
  11. package/dist/database/telegramBindingRepository.js +97 -0
  12. package/dist/database/userPreferenceRepository.js +46 -1
  13. package/dist/events/interactionCreateHandler.js +36 -0
  14. package/dist/events/messageCreateHandler.js +11 -7
  15. package/dist/handlers/approvalButtonAction.js +99 -0
  16. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  17. package/dist/handlers/buttonHandler.js +55 -0
  18. package/dist/handlers/commandHandler.js +44 -0
  19. package/dist/handlers/errorPopupButtonAction.js +137 -0
  20. package/dist/handlers/messageHandler.js +70 -0
  21. package/dist/handlers/modeSelectAction.js +63 -0
  22. package/dist/handlers/modelButtonAction.js +102 -0
  23. package/dist/handlers/planningButtonAction.js +118 -0
  24. package/dist/handlers/selectHandler.js +41 -0
  25. package/dist/handlers/templateButtonAction.js +54 -0
  26. package/dist/platform/adapter.js +8 -0
  27. package/dist/platform/discord/discordAdapter.js +99 -0
  28. package/dist/platform/discord/index.js +15 -0
  29. package/dist/platform/discord/wrappers.js +331 -0
  30. package/dist/platform/index.js +18 -0
  31. package/dist/platform/richContentBuilder.js +76 -0
  32. package/dist/platform/telegram/index.js +16 -0
  33. package/dist/platform/telegram/telegramAdapter.js +195 -0
  34. package/dist/platform/telegram/telegramFormatter.js +134 -0
  35. package/dist/platform/telegram/wrappers.js +329 -0
  36. package/dist/platform/types.js +28 -0
  37. package/dist/services/approvalDetector.js +15 -2
  38. package/dist/services/cdpBridgeManager.js +91 -146
  39. package/dist/services/defaultModelApplicator.js +54 -0
  40. package/dist/services/modeService.js +16 -1
  41. package/dist/services/modelService.js +57 -16
  42. package/dist/services/notificationSender.js +149 -0
  43. package/dist/services/responseMonitor.js +1 -2
  44. package/dist/ui/autoAcceptUi.js +37 -0
  45. package/dist/ui/modeUi.js +38 -1
  46. package/dist/ui/modelsUi.js +96 -0
  47. package/dist/ui/outputUi.js +32 -0
  48. package/dist/ui/projectListUi.js +55 -0
  49. package/dist/ui/screenshotUi.js +26 -0
  50. package/dist/ui/sessionPickerUi.js +35 -1
  51. package/dist/ui/templateUi.js +41 -0
  52. package/dist/utils/configLoader.js +63 -12
  53. package/dist/utils/lockfile.js +5 -5
  54. package/dist/utils/logger.js +7 -0
  55. package/dist/utils/telegramImageHandler.js +127 -0
  56. 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;
@@ -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
  }
@@ -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.
@@ -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()
@@ -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
  *
@@ -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)(`Select a session to join (${sessions.length} found)`));
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),
@@ -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.