lazy-gravity 0.0.4 → 0.1.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <img src="https://img.shields.io/badge/version-0.0.1-blue?style=flat-square" alt="Version" />
6
+ <img src="https://img.shields.io/badge/version-0.0.4-blue?style=flat-square" alt="Version" />
7
7
  <img src="https://img.shields.io/badge/node-18.x+-brightgreen?style=flat-square&logo=node.js" alt="Node.js" />
8
8
  <img src="https://img.shields.io/badge/discord.js-14.x-5865F2?style=flat-square&logo=discord&logoColor=white" alt="discord.js" />
9
9
  <img src="https://img.shields.io/badge/protocol-CDP%20%2F%20WebSocket-orange?style=flat-square" alt="CDP/WebSocket" />
@@ -117,7 +117,10 @@ lazy-gravity setup
117
117
 
118
118
  The wizard guides you through 4 steps:
119
119
 
120
- 1. **Discord Bot Token** — create a bot at the [Discord Developer Portal](https://discord.com/developers/applications), enable Privileged Gateway Intents (PRESENCE, SERVER MEMBERS, MESSAGE CONTENT), and copy the token. Client ID is extracted from the token automatically.
120
+ 1. **Discord Bot Token** — create a bot at the [Discord Developer Portal](https://discord.com/developers/applications).
121
+ - Enable Privileged Gateway Intents: **PRESENCE, SERVER MEMBERS, MESSAGE CONTENT**.
122
+ - Generate an OAuth2 invite URL with the following bot permissions: **Manage Channels** (required for `/project`), **Send Messages**, **Embed Links**, **Attach Files**, **Read Message History**, and **Add Reactions**.
123
+ - Invite the bot to your server, then copy the bot token. Client ID is extracted from the token automatically.
121
124
  2. **Guild (Server) ID** — for instant slash command registration (optional; press Enter to skip).
122
125
  3. **Allowed User IDs** — Discord users authorized to interact with the bot.
123
126
  4. **Workspace Directory** — parent directory where your coding projects live.
@@ -39,6 +39,11 @@ const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const cdpPorts_1 = require("../../utils/cdpPorts");
41
41
  const configLoader_1 = require("../../utils/configLoader");
42
+ const logger_1 = require("../../utils/logger");
43
+ const ok = (msg) => console.log(` ${logger_1.COLORS.green}[OK]${logger_1.COLORS.reset} ${msg}`);
44
+ const warn = (msg) => console.log(` ${logger_1.COLORS.yellow}[--]${logger_1.COLORS.reset} ${msg}`);
45
+ const fail = (msg) => console.log(` ${logger_1.COLORS.red}[!!]${logger_1.COLORS.reset} ${msg}`);
46
+ const hint = (msg) => console.log(` ${logger_1.COLORS.dim}${msg}${logger_1.COLORS.reset}`);
42
47
  function checkPort(port) {
43
48
  return new Promise((resolve) => {
44
49
  const req = http.get(`http://127.0.0.1:${port}/json/list`, (res) => {
@@ -73,84 +78,84 @@ function checkRequiredEnvVars() {
73
78
  }));
74
79
  }
75
80
  async function doctorAction() {
76
- console.log('lazy-gravity doctor\n');
81
+ console.log(`\n${logger_1.COLORS.cyan}lazy-gravity doctor${logger_1.COLORS.reset}\n`);
77
82
  let allOk = true;
78
83
  // 1. Config directory check
79
84
  const configDir = configLoader_1.ConfigLoader.getConfigDir();
80
85
  if (fs.existsSync(configDir)) {
81
- console.log(` [OK] Config directory exists: ${configDir}`);
86
+ ok(`Config directory exists: ${configDir}`);
82
87
  }
83
88
  else {
84
- console.log(` [--] Config directory not found: ${configDir}`);
85
- console.log(' Run: lazy-gravity setup (optional if using .env)');
89
+ warn(`Config directory not found: ${configDir}`);
90
+ hint('Run: lazy-gravity setup (optional if using .env)');
86
91
  }
87
92
  // 2. Config file check
88
93
  const configFilePath = configLoader_1.ConfigLoader.getConfigFilePath();
89
94
  if (configLoader_1.ConfigLoader.configExists()) {
90
- console.log(` [OK] Config file found: ${configFilePath}`);
95
+ ok(`Config file found: ${configFilePath}`);
91
96
  }
92
97
  else {
93
- console.log(` [--] Config file not found: ${configFilePath} (optional — .env fallback used)`);
98
+ warn(`Config file not found: ${configFilePath} (optional — .env fallback used)`);
94
99
  }
95
100
  // 3. .env file check
96
101
  const env = checkEnvFile();
97
102
  if (env.exists) {
98
103
  // Load .env so subsequent checks can see the variables
99
104
  require('dotenv').config({ path: env.path });
100
- console.log(` [OK] .env file found: ${env.path}`);
105
+ ok(`.env file found: ${env.path}`);
101
106
  }
102
107
  else {
103
108
  if (!configLoader_1.ConfigLoader.configExists()) {
104
- console.log(` [!!] .env file not found: ${env.path}`);
109
+ fail(`.env file not found: ${env.path}`);
105
110
  allOk = false;
106
111
  }
107
112
  else {
108
- console.log(` [--] .env file not found: ${env.path} (not needed — config.json used)`);
113
+ warn(`.env file not found: ${env.path} (not needed — config.json used)`);
109
114
  }
110
115
  }
111
116
  // 4. Required environment variables (check both env and config.json sources)
112
117
  const vars = checkRequiredEnvVars();
113
118
  for (const v of vars) {
114
119
  if (v.set) {
115
- console.log(` [OK] ${v.name} is set`);
120
+ ok(`${v.name} is set`);
116
121
  }
117
122
  else {
118
- console.log(` [!!] ${v.name} is NOT set`);
123
+ fail(`${v.name} is NOT set`);
119
124
  allOk = false;
120
125
  }
121
126
  }
122
127
  // 5. CDP port check
123
- console.log('\n Checking CDP ports...');
128
+ console.log(`\n ${logger_1.COLORS.dim}Checking CDP ports...${logger_1.COLORS.reset}`);
124
129
  let cdpOk = false;
125
130
  for (const port of cdpPorts_1.CDP_PORTS) {
126
131
  const alive = await checkPort(port);
127
132
  if (alive) {
128
- console.log(` [OK] CDP port ${port} is responding`);
133
+ ok(`CDP port ${port} is responding`);
129
134
  cdpOk = true;
130
135
  }
131
136
  }
132
137
  if (!cdpOk) {
133
- console.log(' [!!] No CDP ports responding');
134
- console.log(' Run: open -a Antigravity --args --remote-debugging-port=9222');
138
+ fail('No CDP ports responding');
139
+ hint('Run: open -a Antigravity --args --remote-debugging-port=9222');
135
140
  allOk = false;
136
141
  }
137
142
  // 6. Node.js version check
138
143
  const nodeVersion = process.versions.node;
139
144
  const major = parseInt(nodeVersion.split('.')[0], 10);
140
145
  if (major >= 18) {
141
- console.log(`\n [OK] Node.js ${nodeVersion}`);
146
+ ok(`Node.js ${nodeVersion}`);
142
147
  }
143
148
  else {
144
- console.log(`\n [!!] Node.js ${nodeVersion} (>= 18.0.0 required)`);
149
+ fail(`Node.js ${nodeVersion} (>= 18.0.0 required)`);
145
150
  allOk = false;
146
151
  }
147
152
  // Summary
148
153
  console.log('');
149
154
  if (allOk) {
150
- console.log(' All checks passed!');
155
+ console.log(` ${logger_1.COLORS.green}All checks passed!${logger_1.COLORS.reset}`);
151
156
  }
152
157
  else {
153
- console.log(' Some checks failed. Please fix the issues above.');
158
+ console.log(` ${logger_1.COLORS.red}Some checks failed. Please fix the issues above.${logger_1.COLORS.reset}`);
154
159
  process.exitCode = 1;
155
160
  }
156
161
  }
package/dist/bot/index.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -60,6 +93,8 @@ const PHASE_ICONS = {
60
93
  };
61
94
  const MAX_OUTBOUND_GENERATED_IMAGES = 4;
62
95
  const RESPONSE_DELIVERY_MODE = (0, config_1.resolveResponseDeliveryMode)();
96
+ /** Tracks channel IDs where /stop was explicitly invoked by the user */
97
+ const userStopRequestedChannels = new Set();
63
98
  const getResponseDeliveryModeForTest = () => RESPONSE_DELIVERY_MODE;
64
99
  exports.getResponseDeliveryModeForTest = getResponseDeliveryModeForTest;
65
100
  function createSerialTaskQueueForTest(queueName, traceId) {
@@ -387,7 +422,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
387
422
  onProgress: (text) => {
388
423
  if (isFinalized)
389
424
  return;
390
- // TODO: Re-enable live output streaming after RESPONSE_TEXT reliably excludes process logs.
425
+ // Live output streaming disabled: RESPONSE_TEXT currently includes process logs (see #1).
391
426
  const separated = (0, discordFormatter_1.splitOutputAndLogs)(text);
392
427
  if (separated.output && separated.output.trim().length > 0) {
393
428
  lastProgressText = separated.output;
@@ -395,8 +430,49 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
395
430
  },
396
431
  onComplete: async (finalText) => {
397
432
  isFinalized = true;
433
+ // If the user explicitly pressed /stop, skip output display entirely
434
+ const wasStoppedByUser = userStopRequestedChannels.delete(message.channelId);
435
+ if (wasStoppedByUser) {
436
+ logger_1.logger.info(`[sendPromptToAntigravity:${monitorTraceId}] Stopped by user — skipping output`);
437
+ await clearWatchingReaction();
438
+ await message.react('⏹️').catch(() => { });
439
+ return;
440
+ }
398
441
  try {
399
442
  const elapsed = Math.round((Date.now() - startTime) / 1000);
443
+ const isQuotaError = monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected();
444
+ // Quota early exit — skip text extraction, output logging, and embed entirely
445
+ if (isQuotaError) {
446
+ const finalLogText = lastActivityLogText || processLogBuffer.snapshot();
447
+ if (finalLogText && finalLogText.trim().length > 0) {
448
+ logger_1.logger.divider('Process Log');
449
+ console.info(finalLogText);
450
+ }
451
+ logger_1.logger.divider();
452
+ liveActivityUpdateVersion += 1;
453
+ await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, finalLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Process log`), {
454
+ source: 'complete',
455
+ expectedVersion: liveActivityUpdateVersion,
456
+ });
457
+ liveResponseUpdateVersion += 1;
458
+ await upsertLiveResponseEmbeds('⚠️ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model.', 0xFF6B6B, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Quota Reached`), {
459
+ source: 'complete',
460
+ expectedVersion: liveResponseUpdateVersion,
461
+ });
462
+ try {
463
+ const modelsPayload = await (0, modelsUi_1.buildModelsUI)(cdp, () => bridge.quota.fetchQuota());
464
+ if (modelsPayload && channel) {
465
+ await channel.send({ ...modelsPayload });
466
+ }
467
+ }
468
+ catch (e) {
469
+ logger_1.logger.error('[Quota] Failed to send model selection UI:', e);
470
+ }
471
+ await clearWatchingReaction();
472
+ await message.react('⚠️').catch(() => { });
473
+ return;
474
+ }
475
+ // Normal path — extract final text
400
476
  const responseText = (finalText && finalText.trim().length > 0)
401
477
  ? finalText
402
478
  : lastProgressText;
@@ -464,12 +540,6 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
464
540
  logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
465
541
  }
466
542
  }
467
- if (monitor.getPhase() === 'quotaReached' || monitor.getQuotaDetected()) {
468
- await sendEmbed('⚠️ Model Quota Reached', 'Model quota limit reached. Please wait or switch to a different model with `/model`.', 0xFF6B6B, undefined, 'Quota Reached — consider switching models');
469
- await clearWatchingReaction();
470
- await message.react('⚠️').catch(() => { });
471
- return;
472
- }
473
543
  await sendGeneratedImages(finalOutputText || '');
474
544
  await clearWatchingReaction();
475
545
  await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : '⚠️').catch(() => { });
@@ -511,6 +581,21 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
511
581
  },
512
582
  });
513
583
  await monitor.start();
584
+ // 1-second elapsed timer — updates footer independently of process log events
585
+ const elapsedTimer = setInterval(() => {
586
+ if (isFinalized) {
587
+ clearInterval(elapsedTimer);
588
+ return;
589
+ }
590
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
591
+ liveActivityUpdateVersion += 1;
592
+ const activityVersion = liveActivityUpdateVersion;
593
+ upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, lastActivityLogText || ACTIVITY_PLACEHOLDER, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Elapsed: ${elapsed}s | Process log`), {
594
+ source: 'elapsed-tick',
595
+ expectedVersion: activityVersion,
596
+ skipWhenFinalized: true,
597
+ }).catch(() => { });
598
+ }, 1000);
514
599
  }
515
600
  catch (e) {
516
601
  isFinalized = true;
@@ -559,13 +644,43 @@ const startBot = async () => {
559
644
  ]
560
645
  });
561
646
  client.once(discord_js_1.Events.ClientReady, async (readyClient) => {
562
- logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag}`);
647
+ logger_1.logger.info(`Ready! Logged in as ${readyClient.user.tag} | extractionMode=${config.extractionMode}`);
563
648
  try {
564
649
  await (0, registerSlashCommands_1.registerSlashCommands)(config.discordToken, config.clientId, config.guildId);
565
650
  }
566
651
  catch (error) {
567
652
  logger_1.logger.warn('Failed to register slash commands, but text commands remain available.');
568
653
  }
654
+ // Startup dashboard embed
655
+ try {
656
+ const os = await Promise.resolve().then(() => __importStar(require('os')));
657
+ const pkg = await Promise.resolve().then(() => __importStar(require('../../package.json')));
658
+ const version = pkg.default?.version ?? pkg.version ?? 'unknown';
659
+ const projects = workspaceService.scanWorkspaces();
660
+ // Check CDP connection status
661
+ const activeWorkspaces = bridge.pool.getActiveWorkspaceNames();
662
+ const cdpStatus = activeWorkspaces.length > 0
663
+ ? `Connected (${activeWorkspaces.join(', ')})`
664
+ : 'Not connected';
665
+ const dashboardEmbed = new discord_js_1.EmbedBuilder()
666
+ .setTitle('LazyGravity Online')
667
+ .setColor(0x57F287)
668
+ .addFields({ name: 'Version', value: version, inline: true }, { name: 'Node.js', value: process.versions.node, inline: true }, { name: 'OS', value: `${os.platform()} ${os.release()}`, inline: true }, { name: 'CDP', value: cdpStatus, inline: true }, { name: 'Model', value: modelService.getCurrentModel(), inline: true }, { name: 'Mode', value: modeService.getCurrentMode(), inline: true }, { name: 'Projects', value: `${projects.length} registered`, inline: true }, { name: 'Extraction', value: config.extractionMode, inline: true })
669
+ .setFooter({ text: `Started at ${new Date().toLocaleString()}` })
670
+ .setTimestamp();
671
+ // Send to the first available text channel in the guild
672
+ const guild = readyClient.guilds.cache.first();
673
+ if (guild) {
674
+ const channel = guild.channels.cache.find((ch) => ch.isTextBased() && !ch.isVoiceBased() && ch.permissionsFor(readyClient.user)?.has('SendMessages'));
675
+ if (channel && channel.isTextBased()) {
676
+ await channel.send({ embeds: [dashboardEmbed] });
677
+ logger_1.logger.info('Startup dashboard embed sent.');
678
+ }
679
+ }
680
+ }
681
+ catch (error) {
682
+ logger_1.logger.warn('Failed to send startup dashboard embed:', error);
683
+ }
569
684
  });
570
685
  // [Discord Interactions API] Slash command interaction handler
571
686
  client.on(discord_js_1.Events.InteractionCreate, (0, interactionCreateHandler_1.createInteractionCreateHandler)({
@@ -583,6 +698,8 @@ const startBot = async () => {
583
698
  sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
584
699
  getCurrentCdp: cdpBridgeManager_1.getCurrentCdp,
585
700
  parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
701
+ parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
702
+ parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
586
703
  handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo),
587
704
  handleTemplateUse: async (interaction, templateId) => {
588
705
  const template = templateRepo.findById(templateId);
@@ -609,6 +726,8 @@ const startBot = async () => {
609
726
  (0, cdpBridgeManager_1.registerApprovalSessionChannel)(bridge, dirName, session.displayName, interaction.channel);
610
727
  }
611
728
  (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, dirName, client);
729
+ (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, dirName, client);
730
+ (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, dirName, client);
612
731
  }
613
732
  catch (e) {
614
733
  await interaction.followUp({
@@ -858,6 +977,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
858
977
  const result = await cdp.call('Runtime.evaluate', callParams);
859
978
  const value = result?.result?.value;
860
979
  if (value?.ok) {
980
+ userStopRequestedChannels.add(interaction.channelId);
861
981
  const embed = new discord_js_1.EmbedBuilder()
862
982
  .setTitle('⏹️ Generation Interrupted')
863
983
  .setDescription('AI response generation was safely stopped.')
@@ -906,6 +1026,11 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
906
1026
  await cleanupHandler.handleCleanup(interaction);
907
1027
  break;
908
1028
  }
1029
+ case 'ping': {
1030
+ const apiLatency = interaction.client.ws.ping;
1031
+ await interaction.editReply({ content: `🏓 Pong! API Latency is **${apiLatency}ms**.` });
1032
+ break;
1033
+ }
909
1034
  default:
910
1035
  await interaction.editReply({
911
1036
  content: `Unknown command: /${commandName}`,
@@ -102,6 +102,10 @@ const cleanupCommand = new discord_js_1.SlashCommandBuilder()
102
102
  const helpCommand = new discord_js_1.SlashCommandBuilder()
103
103
  .setName('help')
104
104
  .setDescription((0, i18n_1.t)('Display list of available commands'));
105
+ /** /ping command definition */
106
+ const pingCommand = new discord_js_1.SlashCommandBuilder()
107
+ .setName('ping')
108
+ .setDescription((0, i18n_1.t)('Check bot latency'));
105
109
  /** Array of commands to register */
106
110
  exports.slashCommands = [
107
111
  helpCommand,
@@ -116,6 +120,7 @@ exports.slashCommands = [
116
120
  newCommand,
117
121
  chatCommand,
118
122
  cleanupCommand,
123
+ pingCommand,
119
124
  ];
120
125
  /**
121
126
  * Register slash commands with Discord
@@ -7,10 +7,11 @@ exports.WorkspaceCommandHandler = exports.WORKSPACE_SELECT_ID = exports.PROJECT_
7
7
  const i18n_1 = require("../utils/i18n");
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const discord_js_1 = require("discord.js");
10
- /** Select menu custom ID */
11
- exports.PROJECT_SELECT_ID = 'project_select';
12
- /** Backward compatibility: also accept old ID */
13
- exports.WORKSPACE_SELECT_ID = 'workspace_select';
10
+ const projectListUi_1 = require("../ui/projectListUi");
11
+ // Re-export for backward compatibility
12
+ var projectListUi_2 = require("../ui/projectListUi");
13
+ Object.defineProperty(exports, "PROJECT_SELECT_ID", { enumerable: true, get: function () { return projectListUi_2.PROJECT_SELECT_ID; } });
14
+ Object.defineProperty(exports, "WORKSPACE_SELECT_ID", { enumerable: true, get: function () { return projectListUi_2.WORKSPACE_SELECT_ID; } });
14
15
  /**
15
16
  * Handler for the /project slash command.
16
17
  * When a project is selected, auto-creates a Discord category + session-1 channel and binds them.
@@ -31,31 +32,19 @@ class WorkspaceCommandHandler {
31
32
  * /project list -- Display project list via select menu
32
33
  */
33
34
  async handleShow(interaction) {
34
- const embed = new discord_js_1.EmbedBuilder()
35
- .setTitle('📁 Projects')
36
- .setColor(0x5865F2)
37
- .setDescription((0, i18n_1.t)('Select a project to auto-create a category and session channel'))
38
- .setTimestamp();
39
- const components = [];
40
35
  const workspaces = this.workspaceService.scanWorkspaces();
41
- if (workspaces.length > 0) {
42
- const options = workspaces.slice(0, 25).map((ws) => ({
43
- label: ws,
44
- value: ws,
45
- }));
46
- const selectMenu = new discord_js_1.StringSelectMenuBuilder()
47
- .setCustomId(exports.PROJECT_SELECT_ID)
48
- .setPlaceholder((0, i18n_1.t)('Select a project...'))
49
- .addOptions(options);
50
- if (workspaces.length > 25) {
51
- selectMenu.setPlaceholder((0, i18n_1.t)(`Select a project... (Showing 25 of ${workspaces.length})`));
52
- }
53
- components.push(new discord_js_1.ActionRowBuilder().addComponents(selectMenu));
54
- }
55
- await interaction.editReply({
56
- embeds: [embed],
57
- components,
58
- });
36
+ const { embeds, components } = (0, projectListUi_1.buildProjectListUI)(workspaces, 0);
37
+ await interaction.editReply({ embeds, components });
38
+ }
39
+ /**
40
+ * Handle page navigation button press.
41
+ * Re-scans workspaces and renders the requested page.
42
+ */
43
+ async handlePageButton(interaction, page) {
44
+ await interaction.deferUpdate();
45
+ const workspaces = this.workspaceService.scanWorkspaces();
46
+ const { embeds, components } = (0, projectListUi_1.buildProjectListUI)(workspaces, page);
47
+ await interaction.editReply({ embeds, components });
59
48
  }
60
49
  /**
61
50
  * Handler for when a project is selected from the select menu.