lazy-gravity 0.2.0 → 0.4.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 (66) hide show
  1. package/README.md +77 -15
  2. package/dist/bin/cli.js +0 -0
  3. package/dist/bin/commands/doctor.js +19 -2
  4. package/dist/bin/commands/open.js +1 -1
  5. package/dist/bin/commands/setup.js +286 -70
  6. package/dist/bot/eventRouter.js +70 -0
  7. package/dist/bot/index.js +355 -147
  8. package/dist/bot/telegramCommands.js +478 -0
  9. package/dist/bot/telegramMessageHandler.js +308 -0
  10. package/dist/bot/telegramProjectCommand.js +137 -0
  11. package/dist/bot/workspaceQueue.js +61 -0
  12. package/dist/commands/joinCommandHandler.js +4 -1
  13. package/dist/database/telegramBindingRepository.js +97 -0
  14. package/dist/database/userPreferenceRepository.js +46 -1
  15. package/dist/events/interactionCreateHandler.js +36 -0
  16. package/dist/events/messageCreateHandler.js +11 -7
  17. package/dist/handlers/approvalButtonAction.js +99 -0
  18. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  19. package/dist/handlers/buttonHandler.js +55 -0
  20. package/dist/handlers/commandHandler.js +44 -0
  21. package/dist/handlers/errorPopupButtonAction.js +137 -0
  22. package/dist/handlers/messageHandler.js +70 -0
  23. package/dist/handlers/modeSelectAction.js +63 -0
  24. package/dist/handlers/modelButtonAction.js +102 -0
  25. package/dist/handlers/planningButtonAction.js +118 -0
  26. package/dist/handlers/selectHandler.js +41 -0
  27. package/dist/handlers/templateButtonAction.js +54 -0
  28. package/dist/platform/adapter.js +8 -0
  29. package/dist/platform/discord/discordAdapter.js +99 -0
  30. package/dist/platform/discord/index.js +15 -0
  31. package/dist/platform/discord/wrappers.js +331 -0
  32. package/dist/platform/index.js +18 -0
  33. package/dist/platform/richContentBuilder.js +76 -0
  34. package/dist/platform/telegram/index.js +16 -0
  35. package/dist/platform/telegram/telegramAdapter.js +195 -0
  36. package/dist/platform/telegram/telegramFormatter.js +134 -0
  37. package/dist/platform/telegram/wrappers.js +333 -0
  38. package/dist/platform/types.js +28 -0
  39. package/dist/services/approvalDetector.js +15 -2
  40. package/dist/services/cdpBridgeManager.js +91 -146
  41. package/dist/services/cdpService.js +88 -2
  42. package/dist/services/chatSessionService.js +50 -10
  43. package/dist/services/defaultModelApplicator.js +54 -0
  44. package/dist/services/modeService.js +16 -1
  45. package/dist/services/modelService.js +57 -16
  46. package/dist/services/notificationSender.js +149 -0
  47. package/dist/services/responseMonitor.js +1 -2
  48. package/dist/services/screenshotService.js +2 -2
  49. package/dist/ui/autoAcceptUi.js +37 -0
  50. package/dist/ui/modeUi.js +38 -1
  51. package/dist/ui/modelsUi.js +96 -0
  52. package/dist/ui/outputUi.js +32 -0
  53. package/dist/ui/projectListUi.js +55 -0
  54. package/dist/ui/screenshotUi.js +26 -0
  55. package/dist/ui/sessionPickerUi.js +35 -1
  56. package/dist/ui/templateUi.js +41 -0
  57. package/dist/utils/configLoader.js +63 -12
  58. package/dist/utils/lockfile.js +5 -5
  59. package/dist/utils/logger.js +7 -0
  60. package/dist/utils/telegramImageHandler.js +127 -0
  61. package/package.json +6 -3
  62. package/dist/commands/joinDetachCommandHandler.js +0 -285
  63. package/dist/services/retryStore.js +0 -46
  64. package/dist/ui/buttonUtils.js +0 -33
  65. package/dist/utils/antigravityPaths.js +0 -94
  66. package/dist/utils/logFileTransport.js +0 -147
@@ -3,9 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelService = exports.DEFAULT_MODEL = exports.AVAILABLE_MODELS = void 0;
4
4
  const i18n_1 = require("../utils/i18n");
5
5
  /**
6
- * Available LLM models
7
- * Aligned with models selectable in the Antigravity (Cursor fork) UI
8
- * Note: Models may change with Antigravity version updates
6
+ * Fallback model list used when CDP is not connected.
7
+ * NOT used for validation CDP is the sole source of truth
8
+ * for available models. This list may become stale after
9
+ * Antigravity updates.
9
10
  */
10
11
  exports.AVAILABLE_MODELS = [
11
12
  'gemini-3.1-pro-high',
@@ -15,14 +16,20 @@ exports.AVAILABLE_MODELS = [
15
16
  'claude-opus-4.6-thinking',
16
17
  'gpt-oss-120b-medium'
17
18
  ];
18
- /** Default LLM model */
19
+ /** Default LLM model (initial value before CDP connects) */
19
20
  exports.DEFAULT_MODEL = 'gemini-3-flash';
20
21
  /**
21
22
  * Service class for managing LLM models.
22
23
  * Handles model switching via the /model command.
24
+ *
25
+ * Model validation is intentionally NOT performed here.
26
+ * The actual model list is dynamic (fetched from CDP via
27
+ * cdp.getUiModels()), so setModel() accepts any string.
23
28
  */
24
29
  class ModelService {
25
30
  currentModel = exports.DEFAULT_MODEL;
31
+ defaultModel = null;
32
+ pendingSync = false;
26
33
  /**
27
34
  * Get the current LLM model
28
35
  */
@@ -30,34 +37,68 @@ class ModelService {
30
37
  return this.currentModel;
31
38
  }
32
39
  /**
33
- * Switch LLM model
40
+ * Check if the current model is pending sync to Antigravity
41
+ */
42
+ isPendingSync() {
43
+ return this.pendingSync;
44
+ }
45
+ /**
46
+ * Mark the pending model as synced (clears pendingSync flag)
47
+ */
48
+ markSynced() {
49
+ this.pendingSync = false;
50
+ }
51
+ /**
52
+ * Switch LLM model.
53
+ * Accepts any model name — validation happens at the CDP layer
54
+ * (cdp.setUiModel) against the live model list.
55
+ *
34
56
  * @param modelName Model name to set (case-insensitive)
57
+ * @param synced Whether the model has been synced to Antigravity (default: false)
35
58
  */
36
- setModel(modelName) {
59
+ setModel(modelName, synced = false) {
37
60
  if (!modelName || modelName.trim() === '') {
38
61
  return {
39
62
  success: false,
40
- error: (0, i18n_1.t)('⚠️ Model name not specified. Available models: ') + exports.AVAILABLE_MODELS.join(', '),
41
- };
42
- }
43
- const normalized = modelName.trim().toLowerCase();
44
- if (!exports.AVAILABLE_MODELS.includes(normalized)) {
45
- return {
46
- success: false,
47
- error: (0, i18n_1.t)(`⚠️ Invalid model "${modelName}". Available models: ${exports.AVAILABLE_MODELS.join(', ')}`),
63
+ error: (0, i18n_1.t)('⚠️ Model name not specified.'),
48
64
  };
49
65
  }
50
- this.currentModel = normalized;
66
+ this.currentModel = modelName.trim().toLowerCase();
67
+ this.pendingSync = !synced;
51
68
  return {
52
69
  success: true,
53
70
  model: this.currentModel,
54
71
  };
55
72
  }
56
73
  /**
57
- * Get the list of available models
74
+ * Get the fallback list of available models.
75
+ * Prefer cdp.getUiModels() when CDP is connected.
58
76
  */
59
77
  getAvailableModels() {
60
78
  return exports.AVAILABLE_MODELS;
61
79
  }
80
+ /**
81
+ * Get the default model name (free-text, may not match current CDP models)
82
+ */
83
+ getDefaultModel() {
84
+ return this.defaultModel;
85
+ }
86
+ /**
87
+ * Set the default model name (free-text, persisted via DB separately)
88
+ * @param name Model name or null to clear
89
+ */
90
+ setDefaultModel(name) {
91
+ this.defaultModel = name ? name.trim() : null;
92
+ return { success: true, defaultModel: this.defaultModel };
93
+ }
94
+ /**
95
+ * Load the default model from an external source (e.g. DB).
96
+ * Only sets the in-memory value if not already set.
97
+ */
98
+ loadDefaultModel(name) {
99
+ if (this.defaultModel === null && name) {
100
+ this.defaultModel = name.trim();
101
+ }
102
+ }
62
103
  }
63
104
  exports.ModelService = ModelService;
@@ -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;
@@ -33,7 +33,7 @@ class ScreenshotService {
33
33
  if (options.captureBeyondViewport !== undefined) {
34
34
  params.captureBeyondViewport = options.captureBeyondViewport;
35
35
  }
36
- const result = await this.cdpService.call('Page.captureScreenshot', params);
36
+ const result = await this.cdpService.callWithRetry('Page.captureScreenshot', params);
37
37
  const base64Data = result?.data ?? '';
38
38
  if (!base64Data) {
39
39
  return {
@@ -73,7 +73,7 @@ class ScreenshotService {
73
73
  if (options.clip) {
74
74
  params.clip = options.clip;
75
75
  }
76
- const result = await this.cdpService.call('Page.captureScreenshot', params);
76
+ const result = await this.cdpService.callWithRetry('Page.captureScreenshot', params);
77
77
  return result?.data ?? null;
78
78
  }
79
79
  catch (error) {
@@ -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
  */