lazy-gravity 0.0.4 → 0.2.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 (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. package/package.json +4 -4
@@ -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.
@@ -67,6 +67,16 @@ class ChatSessionRepository {
67
67
  const result = this.db.prepare('UPDATE chat_sessions SET display_name = ?, is_renamed = 1 WHERE channel_id = ?').run(displayName, channelId);
68
68
  return result.changes > 0;
69
69
  }
70
+ /**
71
+ * Find a session by display name within a workspace.
72
+ * Returns the first match (most recent).
73
+ */
74
+ findByDisplayName(workspacePath, displayName) {
75
+ const row = this.db.prepare('SELECT * FROM chat_sessions WHERE workspace_path = ? AND display_name = ? ORDER BY id DESC LIMIT 1').get(workspacePath, displayName);
76
+ if (!row)
77
+ return undefined;
78
+ return this.mapRow(row);
79
+ }
70
80
  deleteByChannelId(channelId) {
71
81
  const result = this.db.prepare('DELETE FROM chat_sessions WHERE channel_id = ?').run(channelId);
72
82
  return result.changes > 0;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserPreferenceRepository = void 0;
4
+ /**
5
+ * Repository class for SQLite persistence of per-user preferences.
6
+ * Currently stores output format preference (embed vs plain text).
7
+ */
8
+ class UserPreferenceRepository {
9
+ db;
10
+ constructor(db) {
11
+ this.db = db;
12
+ this.initialize();
13
+ }
14
+ /**
15
+ * Initialize table (create if not exists)
16
+ */
17
+ initialize() {
18
+ this.db.exec(`
19
+ CREATE TABLE IF NOT EXISTS user_preferences (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ user_id TEXT NOT NULL UNIQUE,
22
+ output_format TEXT NOT NULL DEFAULT 'embed',
23
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
24
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
25
+ )
26
+ `);
27
+ }
28
+ /**
29
+ * Get the output format preference for a user.
30
+ * Returns 'embed' as default if no preference is stored.
31
+ */
32
+ getOutputFormat(userId) {
33
+ const row = this.db.prepare('SELECT output_format FROM user_preferences WHERE user_id = ?').get(userId);
34
+ if (!row)
35
+ return 'embed';
36
+ return row.output_format === 'plain' ? 'plain' : 'embed';
37
+ }
38
+ /**
39
+ * Set the output format preference for a user (upsert).
40
+ */
41
+ setOutputFormat(userId, format) {
42
+ this.db.prepare(`
43
+ INSERT INTO user_preferences (user_id, output_format)
44
+ VALUES (?, ?)
45
+ ON CONFLICT(user_id)
46
+ DO UPDATE SET output_format = excluded.output_format,
47
+ updated_at = datetime('now')
48
+ `).run(userId, format);
49
+ }
50
+ /**
51
+ * Get full preference record for a user
52
+ */
53
+ findByUserId(userId) {
54
+ const row = this.db.prepare('SELECT * FROM user_preferences WHERE user_id = ?').get(userId);
55
+ if (!row)
56
+ return undefined;
57
+ return this.mapRow(row);
58
+ }
59
+ /**
60
+ * Map a DB row to UserPreferenceRecord
61
+ */
62
+ mapRow(row) {
63
+ return {
64
+ id: row.id,
65
+ userId: row.user_id,
66
+ outputFormat: row.output_format,
67
+ createdAt: row.created_at,
68
+ updatedAt: row.updated_at,
69
+ };
70
+ }
71
+ }
72
+ exports.UserPreferenceRepository = UserPreferenceRepository;
@@ -4,11 +4,14 @@ exports.createInteractionCreateHandler = createInteractionCreateHandler;
4
4
  const discord_js_1 = require("discord.js");
5
5
  const i18n_1 = require("../utils/i18n");
6
6
  const logger_1 = require("../utils/logger");
7
+ const discordButtonUtils_1 = require("../utils/discordButtonUtils");
7
8
  const templateUi_1 = require("../ui/templateUi");
8
9
  const autoAcceptUi_1 = require("../ui/autoAcceptUi");
10
+ const outputUi_1 = require("../ui/outputUi");
9
11
  const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
10
- const workspaceCommandHandler_1 = require("../commands/workspaceCommandHandler");
12
+ const projectListUi_1 = require("../ui/projectListUi");
11
13
  const modeService_1 = require("../services/modeService");
14
+ const sessionPickerUi_1 = require("../ui/sessionPickerUi");
12
15
  function createInteractionCreateHandler(deps) {
13
16
  return async (interaction) => {
14
17
  if (interaction.isButton()) {
@@ -26,9 +29,9 @@ function createInteractionCreateHandler(deps) {
26
29
  }).catch(logger_1.logger.error);
27
30
  return;
28
31
  }
29
- const workspaceDirName = approvalAction.workspaceDirName ?? deps.bridge.lastActiveWorkspace;
30
- const detector = workspaceDirName
31
- ? deps.bridge.pool.getApprovalDetector(workspaceDirName)
32
+ const projectName = approvalAction.projectName ?? deps.bridge.lastActiveWorkspace;
33
+ const detector = projectName
34
+ ? deps.bridge.pool.getApprovalDetector(projectName)
32
35
  : undefined;
33
36
  if (!detector) {
34
37
  try {
@@ -62,32 +65,9 @@ function createInteractionCreateHandler(deps) {
62
65
  .setColor(approvalAction.action === 'deny' ? 0xE74C3C : 0x2ECC71)
63
66
  .addFields({ name: 'Action History', value: historyText, inline: false })
64
67
  .setTimestamp();
65
- const disabledRows = interaction.message.components
66
- .map((row) => {
67
- const rowAny = row;
68
- if (!Array.isArray(rowAny.components))
69
- return null;
70
- const nextRow = new discord_js_1.ActionRowBuilder();
71
- const disabledButtons = rowAny.components
72
- .map((component) => {
73
- const componentType = component?.type ?? component?.data?.type;
74
- if (componentType !== 2)
75
- return null;
76
- const payload = typeof component?.toJSON === 'function'
77
- ? component.toJSON()
78
- : component;
79
- return discord_js_1.ButtonBuilder.from(payload).setDisabled(true);
80
- })
81
- .filter((button) => button !== null);
82
- if (disabledButtons.length === 0)
83
- return null;
84
- nextRow.addComponents(...disabledButtons);
85
- return nextRow;
86
- })
87
- .filter((row) => row !== null);
88
68
  await interaction.update({
89
69
  embeds: [updatedEmbed],
90
- components: disabledRows,
70
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
91
71
  });
92
72
  }
93
73
  else {
@@ -110,6 +90,282 @@ function createInteractionCreateHandler(deps) {
110
90
  }
111
91
  return;
112
92
  }
93
+ const planningAction = deps.parsePlanningCustomId(interaction.customId);
94
+ if (planningAction) {
95
+ if (planningAction.channelId && planningAction.channelId !== interaction.channelId) {
96
+ await interaction.reply({
97
+ content: (0, i18n_1.t)('This planning action is linked to a different session channel.'),
98
+ flags: discord_js_1.MessageFlags.Ephemeral,
99
+ }).catch(logger_1.logger.error);
100
+ return;
101
+ }
102
+ const planWorkspaceDirName = planningAction.projectName ?? deps.bridge.lastActiveWorkspace;
103
+ const planDetector = planWorkspaceDirName
104
+ ? deps.bridge.pool.getPlanningDetector(planWorkspaceDirName)
105
+ : undefined;
106
+ if (!planDetector) {
107
+ try {
108
+ await interaction.reply({ content: (0, i18n_1.t)('Planning detector not found.'), flags: discord_js_1.MessageFlags.Ephemeral });
109
+ }
110
+ catch { /* ignore */ }
111
+ return;
112
+ }
113
+ try {
114
+ if (planningAction.action === 'open') {
115
+ await interaction.deferUpdate();
116
+ const clicked = await planDetector.clickOpenButton();
117
+ if (!clicked) {
118
+ await interaction.followUp({ content: (0, i18n_1.t)('Open button not found.'), flags: discord_js_1.MessageFlags.Ephemeral });
119
+ return;
120
+ }
121
+ // Wait for DOM to update after Open click
122
+ await new Promise((resolve) => setTimeout(resolve, 500));
123
+ // Extract plan content with retry
124
+ let planContent = null;
125
+ for (let attempt = 0; attempt < 3; attempt++) {
126
+ planContent = await planDetector.extractPlanContent();
127
+ if (planContent)
128
+ break;
129
+ await new Promise((resolve) => setTimeout(resolve, 500));
130
+ }
131
+ // Update original embed with action history
132
+ const originalEmbed = interaction.message.embeds[0];
133
+ const updatedEmbed = originalEmbed
134
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
135
+ : new discord_js_1.EmbedBuilder().setTitle('Planning Mode');
136
+ const historyText = `Open by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
137
+ updatedEmbed
138
+ .setColor(0x3498DB)
139
+ .addFields({ name: 'Action History', value: historyText, inline: false })
140
+ .setTimestamp();
141
+ await interaction.editReply({
142
+ embeds: [updatedEmbed],
143
+ components: interaction.message.components,
144
+ });
145
+ // Send plan content as a new message in the same channel
146
+ if (planContent && interaction.channel && 'send' in interaction.channel) {
147
+ // Discord embed description limit is 4096 chars
148
+ const MAX_PLAN_CONTENT = 4096;
149
+ const truncated = planContent.length > MAX_PLAN_CONTENT
150
+ ? planContent.substring(0, MAX_PLAN_CONTENT - 15) + '\n\n(truncated)'
151
+ : planContent;
152
+ const planEmbed = new discord_js_1.EmbedBuilder()
153
+ .setTitle((0, i18n_1.t)('Plan Content'))
154
+ .setDescription(truncated)
155
+ .setColor(0x3498DB)
156
+ .setTimestamp();
157
+ await interaction.channel.send({ embeds: [planEmbed] }).catch(logger_1.logger.error);
158
+ }
159
+ else if (!planContent) {
160
+ await interaction.followUp({
161
+ content: (0, i18n_1.t)('Could not extract plan content from the editor.'),
162
+ flags: discord_js_1.MessageFlags.Ephemeral,
163
+ }).catch(logger_1.logger.error);
164
+ }
165
+ }
166
+ else {
167
+ // Proceed action
168
+ const clicked = await planDetector.clickProceedButton();
169
+ const originalEmbed = interaction.message.embeds[0];
170
+ const updatedEmbed = originalEmbed
171
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
172
+ : new discord_js_1.EmbedBuilder().setTitle('Planning Mode');
173
+ const historyText = `Proceed by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
174
+ updatedEmbed
175
+ .setColor(clicked ? 0x2ECC71 : 0xE74C3C)
176
+ .addFields({ name: 'Action History', value: historyText, inline: false })
177
+ .setTimestamp();
178
+ try {
179
+ await interaction.update({
180
+ embeds: [updatedEmbed],
181
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
182
+ });
183
+ }
184
+ catch (interactionError) {
185
+ if (interactionError?.code === 10062 || interactionError?.code === 40060) {
186
+ logger_1.logger.warn('[Planning] Interaction expired. Responding directly in the channel.');
187
+ if (interaction.channel && 'send' in interaction.channel) {
188
+ const fallbackMessage = clicked
189
+ ? (0, i18n_1.t)('Proceed completed. Implementation started.')
190
+ : (0, i18n_1.t)('Proceed button not found.');
191
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
192
+ }
193
+ }
194
+ else {
195
+ throw interactionError;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ catch (planError) {
201
+ if (planError?.code === 10062 || planError?.code === 40060) {
202
+ logger_1.logger.warn('[Planning] Interaction expired.');
203
+ }
204
+ else {
205
+ logger_1.logger.error('[Planning] Error handling planning button:', planError);
206
+ try {
207
+ if (!interaction.replied && !interaction.deferred) {
208
+ await interaction.reply({ content: (0, i18n_1.t)('An error occurred while processing the planning action.'), flags: discord_js_1.MessageFlags.Ephemeral });
209
+ }
210
+ else {
211
+ await interaction.followUp({ content: (0, i18n_1.t)('An error occurred while processing the planning action.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
212
+ }
213
+ }
214
+ catch { /* ignore */ }
215
+ }
216
+ }
217
+ return;
218
+ }
219
+ const errorPopupAction = deps.parseErrorPopupCustomId(interaction.customId);
220
+ if (errorPopupAction) {
221
+ if (errorPopupAction.channelId && errorPopupAction.channelId !== interaction.channelId) {
222
+ await interaction.reply({
223
+ content: (0, i18n_1.t)('This error popup action is linked to a different session channel.'),
224
+ flags: discord_js_1.MessageFlags.Ephemeral,
225
+ }).catch(logger_1.logger.error);
226
+ return;
227
+ }
228
+ const errorWorkspaceDirName = errorPopupAction.projectName ?? deps.bridge.lastActiveWorkspace;
229
+ const errorDetector = errorWorkspaceDirName
230
+ ? deps.bridge.pool.getErrorPopupDetector(errorWorkspaceDirName)
231
+ : undefined;
232
+ if (!errorDetector) {
233
+ try {
234
+ await interaction.reply({ content: (0, i18n_1.t)('Error popup detector not found.'), flags: discord_js_1.MessageFlags.Ephemeral });
235
+ }
236
+ catch { /* ignore */ }
237
+ return;
238
+ }
239
+ try {
240
+ if (errorPopupAction.action === 'dismiss') {
241
+ const clicked = await errorDetector.clickDismissButton();
242
+ const originalEmbed = interaction.message.embeds[0];
243
+ const updatedEmbed = originalEmbed
244
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
245
+ : new discord_js_1.EmbedBuilder().setTitle('Agent Error');
246
+ const historyText = `Dismiss by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
247
+ updatedEmbed
248
+ .setColor(clicked ? 0x95A5A6 : 0xE74C3C)
249
+ .addFields({ name: 'Action History', value: historyText, inline: false })
250
+ .setTimestamp();
251
+ try {
252
+ await interaction.update({
253
+ embeds: [updatedEmbed],
254
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
255
+ });
256
+ }
257
+ catch (interactionError) {
258
+ if (interactionError?.code === 10062 || interactionError?.code === 40060) {
259
+ logger_1.logger.warn('[ErrorPopup] Interaction expired. Responding directly in the channel.');
260
+ if (interaction.channel && 'send' in interaction.channel) {
261
+ const fallbackMessage = clicked
262
+ ? (0, i18n_1.t)('Error popup dismissed.')
263
+ : (0, i18n_1.t)('Dismiss button not found.');
264
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
265
+ }
266
+ }
267
+ else {
268
+ throw interactionError;
269
+ }
270
+ }
271
+ }
272
+ else if (errorPopupAction.action === 'copy_debug') {
273
+ await interaction.deferUpdate();
274
+ const clicked = await errorDetector.clickCopyDebugInfoButton();
275
+ if (!clicked) {
276
+ await interaction.followUp({ content: (0, i18n_1.t)('Copy debug info button not found.'), flags: discord_js_1.MessageFlags.Ephemeral });
277
+ return;
278
+ }
279
+ // Wait for clipboard to be populated
280
+ await new Promise((resolve) => setTimeout(resolve, 300));
281
+ const clipboardContent = await errorDetector.readClipboard();
282
+ // Update original embed with action history
283
+ const originalEmbed = interaction.message.embeds[0];
284
+ const updatedEmbed = originalEmbed
285
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
286
+ : new discord_js_1.EmbedBuilder().setTitle('Agent Error');
287
+ const historyText = `Copy debug info by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
288
+ updatedEmbed
289
+ .setColor(0x3498DB)
290
+ .addFields({ name: 'Action History', value: historyText, inline: false })
291
+ .setTimestamp();
292
+ await interaction.editReply({
293
+ embeds: [updatedEmbed],
294
+ components: interaction.message.components,
295
+ });
296
+ // Send debug info as a new message
297
+ if (clipboardContent && interaction.channel && 'send' in interaction.channel) {
298
+ const MAX_DEBUG_CONTENT = 4096;
299
+ const truncated = clipboardContent.length > MAX_DEBUG_CONTENT
300
+ ? clipboardContent.substring(0, MAX_DEBUG_CONTENT - 15) + '\n\n(truncated)'
301
+ : clipboardContent;
302
+ const debugEmbed = new discord_js_1.EmbedBuilder()
303
+ .setTitle((0, i18n_1.t)('Debug Info'))
304
+ .setDescription(`\`\`\`\n${truncated}\n\`\`\``)
305
+ .setColor(0x3498DB)
306
+ .setTimestamp();
307
+ await interaction.channel.send({ embeds: [debugEmbed] }).catch(logger_1.logger.error);
308
+ }
309
+ else if (!clipboardContent) {
310
+ await interaction.followUp({
311
+ content: (0, i18n_1.t)('Could not read debug info from clipboard.'),
312
+ flags: discord_js_1.MessageFlags.Ephemeral,
313
+ }).catch(logger_1.logger.error);
314
+ }
315
+ }
316
+ else {
317
+ // Retry action
318
+ const clicked = await errorDetector.clickRetryButton();
319
+ const originalEmbed = interaction.message.embeds[0];
320
+ const updatedEmbed = originalEmbed
321
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
322
+ : new discord_js_1.EmbedBuilder().setTitle('Agent Error');
323
+ const historyText = `Retry by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
324
+ updatedEmbed
325
+ .setColor(clicked ? 0x2ECC71 : 0xE74C3C)
326
+ .addFields({ name: 'Action History', value: historyText, inline: false })
327
+ .setTimestamp();
328
+ try {
329
+ await interaction.update({
330
+ embeds: [updatedEmbed],
331
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
332
+ });
333
+ }
334
+ catch (interactionError) {
335
+ if (interactionError?.code === 10062 || interactionError?.code === 40060) {
336
+ logger_1.logger.warn('[ErrorPopup] Interaction expired. Responding directly in the channel.');
337
+ if (interaction.channel && 'send' in interaction.channel) {
338
+ const fallbackMessage = clicked
339
+ ? (0, i18n_1.t)('Retry initiated.')
340
+ : (0, i18n_1.t)('Retry button not found.');
341
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
342
+ }
343
+ }
344
+ else {
345
+ throw interactionError;
346
+ }
347
+ }
348
+ }
349
+ }
350
+ catch (errorPopupError) {
351
+ if (errorPopupError?.code === 10062 || errorPopupError?.code === 40060) {
352
+ logger_1.logger.warn('[ErrorPopup] Interaction expired.');
353
+ }
354
+ else {
355
+ logger_1.logger.error('[ErrorPopup] Error handling error popup button:', errorPopupError);
356
+ try {
357
+ if (!interaction.replied && !interaction.deferred) {
358
+ await interaction.reply({ content: (0, i18n_1.t)('An error occurred while processing the error popup action.'), flags: discord_js_1.MessageFlags.Ephemeral });
359
+ }
360
+ else {
361
+ await interaction.followUp({ content: (0, i18n_1.t)('An error occurred while processing the error popup action.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
362
+ }
363
+ }
364
+ catch { /* ignore */ }
365
+ }
366
+ }
367
+ return;
368
+ }
113
369
  if (interaction.customId === cleanupCommandHandler_1.CLEANUP_ARCHIVE_BTN) {
114
370
  await deps.cleanupHandler.handleArchive(interaction);
115
371
  return;
@@ -167,6 +423,27 @@ function createInteractionCreateHandler(deps) {
167
423
  });
168
424
  return;
169
425
  }
426
+ if (interaction.customId === outputUi_1.OUTPUT_BTN_EMBED || interaction.customId === outputUi_1.OUTPUT_BTN_PLAIN) {
427
+ if (deps.userPrefRepo) {
428
+ await interaction.deferUpdate();
429
+ const format = interaction.customId === outputUi_1.OUTPUT_BTN_PLAIN ? 'plain' : 'embed';
430
+ deps.userPrefRepo.setOutputFormat(interaction.user.id, format);
431
+ await (0, outputUi_1.sendOutputUI)({ editReply: async (data) => await interaction.editReply(data) }, format);
432
+ const label = format === 'plain' ? 'Plain Text' : 'Embed';
433
+ await interaction.followUp({
434
+ content: `Output format changed to **${label}**.`,
435
+ flags: discord_js_1.MessageFlags.Ephemeral,
436
+ });
437
+ }
438
+ return;
439
+ }
440
+ if (interaction.customId.startsWith(`${projectListUi_1.PROJECT_PAGE_PREFIX}:`)) {
441
+ const page = (0, projectListUi_1.parseProjectPageId)(interaction.customId);
442
+ if (!isNaN(page) && page >= 0) {
443
+ await deps.wsHandler.handlePageButton(interaction, page);
444
+ }
445
+ return;
446
+ }
170
447
  if (interaction.customId.startsWith(templateUi_1.TEMPLATE_BTN_PREFIX)) {
171
448
  await interaction.deferUpdate();
172
449
  const templateId = (0, templateUi_1.parseTemplateButtonId)(interaction.customId);
@@ -233,7 +510,33 @@ function createInteractionCreateHandler(deps) {
233
510
  }
234
511
  return;
235
512
  }
236
- if (interaction.isStringSelectMenu() && (interaction.customId === workspaceCommandHandler_1.PROJECT_SELECT_ID || interaction.customId === workspaceCommandHandler_1.WORKSPACE_SELECT_ID)) {
513
+ if (interaction.isStringSelectMenu() && (0, sessionPickerUi_1.isSessionSelectId)(interaction.customId)) {
514
+ if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
515
+ await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
516
+ return;
517
+ }
518
+ try {
519
+ await interaction.deferUpdate();
520
+ }
521
+ catch (deferError) {
522
+ if (deferError?.code === 10062 || deferError?.code === 40060) {
523
+ logger_1.logger.warn('[SessionSelect] deferUpdate expired. Skipping.');
524
+ return;
525
+ }
526
+ logger_1.logger.error('[SessionSelect] deferUpdate failed:', deferError);
527
+ return;
528
+ }
529
+ try {
530
+ if (deps.joinHandler) {
531
+ await deps.joinHandler.handleJoinSelect(interaction, deps.bridge);
532
+ }
533
+ }
534
+ catch (error) {
535
+ logger_1.logger.error('Session selection error:', error);
536
+ }
537
+ return;
538
+ }
539
+ if (interaction.isStringSelectMenu() && (0, projectListUi_1.isProjectSelectId)(interaction.customId)) {
237
540
  if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
238
541
  await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
239
542
  return;
@@ -261,7 +564,12 @@ function createInteractionCreateHandler(deps) {
261
564
  return;
262
565
  }
263
566
  try {
264
- await commandInteraction.deferReply();
567
+ if (commandInteraction.commandName === 'logs') {
568
+ await commandInteraction.deferReply({ flags: discord_js_1.MessageFlags.Ephemeral });
569
+ }
570
+ else {
571
+ await commandInteraction.deferReply();
572
+ }
265
573
  }
266
574
  catch (deferError) {
267
575
  if (deferError?.code === 10062) {