gibi-bot 1.0.0 → 1.1.1

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 (48) hide show
  1. package/.context.json +47 -3
  2. package/.github/workflows/npm-publish.yml +33 -0
  3. package/.github/workflows/release.yml +45 -0
  4. package/.husky/commit-msg +1 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.prettierignore +3 -0
  7. package/.prettierrc +7 -0
  8. package/CHANGELOG.md +45 -0
  9. package/DISTRIBUTION.md +9 -1
  10. package/GEMINI.md +28 -9
  11. package/README.md +55 -28
  12. package/commitlint.config.js +3 -0
  13. package/conductor/code_styleguides/general.md +6 -1
  14. package/conductor/code_styleguides/ts.md +42 -35
  15. package/conductor/product-guidelines.md +16 -12
  16. package/conductor/product.md +12 -7
  17. package/conductor/setup_state.json +1 -1
  18. package/conductor/tech-stack.md +4 -1
  19. package/conductor/tracks/slack_bot_20260107/metadata.json +1 -1
  20. package/conductor/tracks/slack_bot_20260107/plan.md +6 -1
  21. package/conductor/tracks/slack_bot_20260107/spec.md +9 -6
  22. package/conductor/tracks.md +2 -1
  23. package/conductor/workflow.md +74 -53
  24. package/dist/agents.js +7 -10
  25. package/dist/agents.test.js +17 -12
  26. package/dist/app.js +212 -135
  27. package/dist/config.js +5 -5
  28. package/dist/context.js +4 -3
  29. package/dist/context.test.js +2 -4
  30. package/eslint.config.mjs +17 -0
  31. package/jest.config.js +4 -3
  32. package/nodemon.json +5 -9
  33. package/package.json +34 -4
  34. package/release.config.js +22 -0
  35. package/src/agents.test.ts +62 -57
  36. package/src/agents.ts +94 -82
  37. package/src/app.d.ts +1 -1
  38. package/src/app.ts +298 -178
  39. package/src/config.ts +48 -48
  40. package/src/context.test.ts +54 -56
  41. package/src/context.ts +123 -114
  42. package/test_gemini.js +13 -9
  43. package/test_gemini_approval.js +13 -9
  44. package/test_gemini_write.js +19 -9
  45. package/tests/context.test.ts +145 -0
  46. package/tsconfig.json +1 -1
  47. package/src/app.js +0 -55
  48. package/src/app.js.map +0 -1
package/dist/app.js CHANGED
@@ -57,7 +57,7 @@ const start = async () => {
57
57
  token: process.env.SLACK_BOT_TOKEN || '',
58
58
  signingSecret: process.env.SLACK_SIGNING_SECRET || '',
59
59
  socketMode: true,
60
- appToken: process.env.SLACK_APP_TOKEN || ''
60
+ appToken: process.env.SLACK_APP_TOKEN || '',
61
61
  });
62
62
  // Fetch Bot User ID on startup
63
63
  (async () => {
@@ -101,23 +101,23 @@ const start = async () => {
101
101
  type: 'section',
102
102
  text: {
103
103
  type: 'mrkdwn',
104
- text: `_Thinking..._ (using \`${agentName}\`)`
105
- }
106
- }
104
+ text: `_Thinking..._ (using \`${agentName}\`)`,
105
+ },
106
+ },
107
107
  ];
108
108
  if (processingMsgTs) {
109
109
  await client.chat.update({
110
110
  channel: channelId,
111
111
  ts: processingMsgTs,
112
112
  text: 'Thinking...',
113
- blocks: thinkingBlocks
113
+ blocks: thinkingBlocks,
114
114
  });
115
115
  }
116
116
  else {
117
117
  const processingMsg = await say({
118
118
  blocks: thinkingBlocks,
119
119
  text: 'Thinking...',
120
- thread_ts: threadTs
120
+ thread_ts: threadTs,
121
121
  });
122
122
  processingMsgTs = processingMsg.ts;
123
123
  }
@@ -128,7 +128,7 @@ const start = async () => {
128
128
  }
129
129
  const responseText = await agent.run(context.messages, {
130
130
  model: context.data.model,
131
- mode: context.data.mode
131
+ mode: context.data.mode,
132
132
  });
133
133
  if (responseText) {
134
134
  // Append Assistant Response
@@ -139,17 +139,17 @@ const start = async () => {
139
139
  type: 'section',
140
140
  text: {
141
141
  type: 'mrkdwn',
142
- text: responseText
143
- }
142
+ text: responseText,
143
+ },
144
144
  },
145
145
  {
146
146
  type: 'context',
147
147
  elements: [
148
148
  {
149
149
  type: 'mrkdwn',
150
- text: `🤖 *Agent:* \`${agentName}\` | 🧠 *Model:* \`${context.data.model || 'default'}\` | 📂 *CWD:* \`${context.data.cwd || 'root'}\``
151
- }
152
- ]
150
+ text: `🤖 *Agent:* \`${agentName}\` | 🧠 *Model:* \`${context.data.model || 'default'}\` | 📂 *CWD:* \`${context.data.cwd || 'root'}\``,
151
+ },
152
+ ],
153
153
  },
154
154
  {
155
155
  type: 'actions',
@@ -157,15 +157,15 @@ const start = async () => {
157
157
  {
158
158
  type: 'button',
159
159
  text: { type: 'plain_text', text: '🔄 Retry', emoji: true },
160
- action_id: 'retry_turn'
160
+ action_id: 'retry_turn',
161
161
  },
162
162
  {
163
163
  type: 'button',
164
164
  text: { type: 'plain_text', text: '↩️ Revert', emoji: true },
165
- action_id: 'revert_turn'
166
- }
167
- ]
168
- }
165
+ action_id: 'revert_turn',
166
+ },
167
+ ],
168
+ },
169
169
  ];
170
170
  // Update the processing message with the actual response
171
171
  if (processingMsgTs) {
@@ -173,14 +173,14 @@ const start = async () => {
173
173
  channel: channelId,
174
174
  ts: processingMsgTs,
175
175
  text: responseText,
176
- blocks: responseBlocks
176
+ blocks: responseBlocks,
177
177
  });
178
178
  }
179
179
  else {
180
180
  await say({
181
181
  text: responseText,
182
182
  blocks: responseBlocks,
183
- thread_ts: threadTs
183
+ thread_ts: threadTs,
184
184
  });
185
185
  }
186
186
  }
@@ -189,7 +189,7 @@ const start = async () => {
189
189
  await client.chat.update({
190
190
  channel: channelId,
191
191
  ts: processingMsgTs,
192
- text: `Received empty response from ${agentName}.`
192
+ text: `Received empty response from ${agentName}.`,
193
193
  });
194
194
  }
195
195
  }
@@ -200,13 +200,13 @@ const start = async () => {
200
200
  await client.chat.update({
201
201
  channel: channelId,
202
202
  ts: processingMsgTs,
203
- text: errorMessage
203
+ text: errorMessage,
204
204
  });
205
205
  }
206
206
  else {
207
207
  await say({
208
208
  text: errorMessage,
209
- thread_ts: threadTs
209
+ thread_ts: threadTs,
210
210
  });
211
211
  }
212
212
  }
@@ -246,7 +246,7 @@ const start = async () => {
246
246
  if (BOT_ID)
247
247
  text = text.replace(new RegExp(`<@${BOT_ID}>`, 'g'), '').trim();
248
248
  const replyThreadTs = msg.thread_ts || msg.ts;
249
- const targetContextId = msg.thread_ts || msg.ts;
249
+ const targetContextId = msg.thread_ts || msg.ts || msg.channel;
250
250
  await processAgentTurn(targetContextId, msg.channel, replyThreadTs, text, say, client);
251
251
  });
252
252
  // Helper for CD command (shared between Slash command and Message)
@@ -284,9 +284,9 @@ const start = async () => {
284
284
  type: 'section',
285
285
  text: {
286
286
  type: 'mrkdwn',
287
- text: `📂 *Directory:* \`${newPath}\``
288
- }
289
- }
287
+ text: `📂 *Directory:* \`${newPath}\``,
288
+ },
289
+ },
290
290
  ];
291
291
  // Add "Up" button if not root
292
292
  const parentPath = path.dirname(newPath);
@@ -299,28 +299,29 @@ const start = async () => {
299
299
  text: {
300
300
  type: 'plain_text',
301
301
  text: '⬆️ Up one level',
302
- emoji: true
302
+ emoji: true,
303
303
  },
304
304
  value: parentPath,
305
- action_id: 'navigate_dir'
306
- }
307
- ]
305
+ action_id: 'navigate_dir',
306
+ },
307
+ ],
308
308
  });
309
309
  }
310
310
  const dirButtons = [];
311
311
  const fileNames = [];
312
- files.forEach(file => {
312
+ files.forEach((file) => {
313
313
  if (file.isDirectory()) {
314
- if (dirButtons.length < 10) { // Limit buttons to avoid clutter
314
+ if (dirButtons.length < 10) {
315
+ // Limit buttons to avoid clutter
315
316
  dirButtons.push({
316
317
  type: 'button',
317
318
  text: {
318
319
  type: 'plain_text',
319
320
  text: `📁 ${file.name}`,
320
- emoji: true
321
+ emoji: true,
321
322
  },
322
323
  value: path.join(newPath, file.name),
323
- action_id: 'navigate_dir'
324
+ action_id: 'navigate_dir',
324
325
  });
325
326
  }
326
327
  else {
@@ -336,7 +337,7 @@ const start = async () => {
336
337
  for (let i = 0; i < dirButtons.length; i += 5) {
337
338
  blocks.push({
338
339
  type: 'actions',
339
- elements: dirButtons.slice(i, i + 5)
340
+ elements: dirButtons.slice(i, i + 5),
340
341
  });
341
342
  }
342
343
  }
@@ -349,13 +350,13 @@ const start = async () => {
349
350
  type: 'section',
350
351
  text: {
351
352
  type: 'mrkdwn',
352
- text: fileListText
353
- }
353
+ text: fileListText,
354
+ },
354
355
  });
355
356
  }
356
357
  return {
357
358
  text: `Directory: ${newPath}`,
358
- blocks: blocks
359
+ blocks: blocks,
359
360
  };
360
361
  }
361
362
  catch (error) {
@@ -367,28 +368,38 @@ const start = async () => {
367
368
  await ack();
368
369
  const action = body.actions[0];
369
370
  const contextId = body.container.thread_ts || body.container.channel_id;
370
- const output = await executeChangeDirectory(contextId, action.value);
371
+ const output = await executeChangeDirectory(contextId, action.value || '');
371
372
  await respond({
372
373
  ...output,
373
- replace_original: true
374
+ replace_original: true,
374
375
  });
375
376
  });
376
- // Handle /plan command
377
- app.command('/plan', async ({ command, ack, say, client }) => {
378
- await ack();
379
- const contextId = command.channel_id;
380
- const goal = command.text.trim();
377
+ // Helper for Plan Mode
378
+ const startPlanMode = async (contextId, goal, say, client) => {
381
379
  // Switch to Plan Mode
382
380
  contextManager.setContextData(contextId, { mode: 'plan' });
383
381
  // Notify initiation
384
382
  await say({
385
- text: `📝 *Entering Plan Mode*\nTarget: ${goal || "No specific goal provided. What would you like to plan?"}`,
386
- channel: command.channel_id
383
+ text: `📝 *Entering Plan Mode*\nTarget: ${goal || 'No specific goal provided. What would you like to plan?'}`,
384
+ channel: contextId,
387
385
  });
388
386
  // If goal provided, process it as the first message
389
387
  if (goal) {
390
- await processAgentTurn(contextId, command.channel_id, undefined, goal, say, client);
388
+ await processAgentTurn(contextId, contextId, undefined, goal, say, client);
391
389
  }
390
+ };
391
+ // Handle /plan command
392
+ app.command('/plan', async ({ command, ack, say, client }) => {
393
+ await ack();
394
+ await startPlanMode(command.channel_id, command.text.trim(), say, client);
395
+ });
396
+ // Handle "/plan" messages
397
+ app.message(/^\/plan\s*(.*)/, async ({ message, context, say, client }) => {
398
+ const msg = message;
399
+ const goal = context.matches[1].trim();
400
+ const contextId = msg.channel; // Plan mode usually tied to channel for now, or thread if used there.
401
+ // Using msg.channel generally for /plan.
402
+ await startPlanMode(contextId, goal, say, client);
392
403
  });
393
404
  // Handle /cd command
394
405
  app.command('/cd', async ({ command, ack, respond }) => {
@@ -405,20 +416,30 @@ const start = async () => {
405
416
  const contextId = msg.thread_ts || msg.channel;
406
417
  const output = await executeChangeDirectory(contextId, targetPath);
407
418
  await say({
408
- text: output,
409
- thread_ts: msg.thread_ts // Reply in thread if it was a thread message
419
+ text: output.text,
420
+ blocks: output.blocks,
421
+ ...(msg.thread_ts ? { thread_ts: msg.thread_ts } : {}), // Reply in thread if it was a thread message
410
422
  });
411
423
  });
412
- // Handle /model command
413
- app.command('/model', async ({ command, ack, client, respond }) => {
414
- await ack();
415
- const input = command.text.trim();
416
- const contextId = command.channel_id;
424
+ // Handle "/cd [path]" or "/cd" messages
425
+ app.message(/^\/cd\s*(.*)/, async ({ message, context, say }) => {
426
+ const msg = message;
427
+ const targetPath = context.matches[1].trim();
428
+ const contextId = msg.thread_ts || msg.channel;
429
+ const output = await executeChangeDirectory(contextId, targetPath);
430
+ await say({
431
+ text: output.text,
432
+ blocks: output.blocks,
433
+ ...(msg.thread_ts ? { thread_ts: msg.thread_ts } : {}),
434
+ });
435
+ });
436
+ // Helper for Model Switch
437
+ const switchModel = async (contextId, input, triggerId, client) => {
417
438
  const context = contextManager.getContext(contextId);
418
- // Show modal if no input
419
- if (!input) {
439
+ // Show modal if no input AND triggerId is available (Slash command)
440
+ if (!input && triggerId) {
420
441
  await client.views.open({
421
- trigger_id: command.trigger_id,
442
+ trigger_id: triggerId,
422
443
  view: {
423
444
  type: 'modal',
424
445
  callback_id: 'model_select_modal',
@@ -432,43 +453,62 @@ const start = async () => {
432
453
  element: {
433
454
  type: 'static_select',
434
455
  action_id: 'model_select',
435
- initial_option: AVAILABLE_MODELS.find(m => m.id === (context.data.model || 'gemini-1.5-pro')) ? {
436
- text: { type: 'plain_text', text: context.data.model || 'gemini-1.5-pro' },
437
- value: context.data.model || 'gemini-1.5-pro'
438
- } : undefined,
439
- options: AVAILABLE_MODELS.map(m => ({
456
+ initial_option: AVAILABLE_MODELS.find((m) => m.id === (context.data.model || 'gemini-1.5-pro'))
457
+ ? {
458
+ text: { type: 'plain_text', text: context.data.model || 'gemini-1.5-pro' },
459
+ value: context.data.model || 'gemini-1.5-pro',
460
+ }
461
+ : undefined,
462
+ options: AVAILABLE_MODELS.map((m) => ({
440
463
  text: { type: 'plain_text', text: m.id },
441
464
  value: m.id,
442
- description: { type: 'plain_text', text: m.desc.substring(0, 75) }
443
- }))
444
- }
445
- }
465
+ description: { type: 'plain_text', text: m.desc.substring(0, 75) },
466
+ })),
467
+ },
468
+ },
446
469
  ],
447
- submit: { type: 'plain_text', text: 'Update' }
448
- }
470
+ submit: { type: 'plain_text', text: 'Update' },
471
+ },
449
472
  });
450
- return;
473
+ return null; // Handled by modal
451
474
  }
452
- if (input === 'list' || input === 'help') {
475
+ if (input === 'list' || input === 'help' || (!input && !triggerId)) {
453
476
  const currentModel = context.data.model || 'default';
454
- const modelList = AVAILABLE_MODELS.map(m => `• \`${m.id}\`: ${m.desc}`).join('\n');
455
- await respond(`Current model: \`${currentModel}\`\n\n` +
477
+ const modelList = AVAILABLE_MODELS.map((m) => `• \`${m.id}\`: ${m.desc}`).join('\n');
478
+ return (`Current model: \`${currentModel}\`\n\n` +
456
479
  `*Available Models:*\n${modelList}\n\n` +
457
480
  `To switch, use \`/model <model_name>\``);
458
- return;
459
481
  }
460
482
  contextManager.setContextData(contextId, { model: input });
461
- await respond(`Switched model to: \`${input}\``);
462
- });
463
- // Handle /agent command
464
- app.command('/agent', async ({ command, ack, client, respond }) => {
483
+ return `Switched model to: \`${input}\``;
484
+ };
485
+ // Handle /model command
486
+ app.command('/model', async ({ command, ack, client, respond }) => {
465
487
  await ack();
466
- const input = command.text.trim();
467
- const contextId = command.channel_id;
488
+ const result = await switchModel(command.channel_id, command.text.trim(), command.trigger_id, client);
489
+ if (result)
490
+ await respond(result);
491
+ });
492
+ // Handle "/model" messages
493
+ app.message(/^\/model\s*(.*)/, async ({ message, context, say, client }) => {
494
+ const msg = message;
495
+ const input = context.matches[1].trim();
496
+ const contextId = msg.thread_ts || msg.channel;
497
+ // Pass undefined for triggerId so it doesn't try to open a modal (which requires trigger_id)
498
+ const result = await switchModel(contextId, input, undefined, client);
499
+ if (result) {
500
+ await say({
501
+ text: result,
502
+ thread_ts: msg.thread_ts,
503
+ });
504
+ }
505
+ });
506
+ // Helper for Agent Switch
507
+ const switchAgent = async (contextId, input, triggerId, client) => {
468
508
  const context = contextManager.getContext(contextId);
469
- if (!input) {
509
+ if (!input && triggerId) {
470
510
  await client.views.open({
471
- trigger_id: command.trigger_id,
511
+ trigger_id: triggerId,
472
512
  view: {
473
513
  type: 'modal',
474
514
  callback_id: 'agent_select_modal',
@@ -484,35 +524,55 @@ const start = async () => {
484
524
  action_id: 'agent_select',
485
525
  initial_option: {
486
526
  text: { type: 'plain_text', text: context.data.agent || 'gemini' },
487
- value: context.data.agent || 'gemini'
527
+ value: context.data.agent || 'gemini',
488
528
  },
489
- options: Object.keys(agents_1.agentRegistry).map(k => ({
529
+ options: Object.keys(agents_1.agentRegistry).map((k) => ({
490
530
  text: { type: 'plain_text', text: k },
491
- value: k
492
- }))
493
- }
494
- }
531
+ value: k,
532
+ })),
533
+ },
534
+ },
495
535
  ],
496
- submit: { type: 'plain_text', text: 'Update' }
497
- }
536
+ submit: { type: 'plain_text', text: 'Update' },
537
+ },
498
538
  });
499
- return;
539
+ return null;
500
540
  }
501
- if (input === 'list' || input === 'help') {
541
+ if (input === 'list' || input === 'help' || (!input && !triggerId)) {
502
542
  const currentAgent = context.data.agent || 'gemini';
503
- const agentList = Object.keys(agents_1.agentRegistry).map(k => `• \`${k}\``).join('\n');
504
- await respond(`Current agent: \`${currentAgent}\`\n\n` +
543
+ const agentList = Object.keys(agents_1.agentRegistry)
544
+ .map((k) => `• \`${k}\``)
545
+ .join('\n');
546
+ return (`Current agent: \`${currentAgent}\`\n\n` +
505
547
  `*Available Agents:*\n${agentList}\n\n` +
506
548
  `To switch, use \`/agent <agent_name>\``);
507
- return;
508
549
  }
509
550
  const agentName = input.toLowerCase();
510
551
  if (!agents_1.agentRegistry[agentName]) {
511
- await respond(`❌ Unknown agent: \`${agentName}\`. Available agents: ${Object.keys(agents_1.agentRegistry).join(', ')}`);
512
- return;
552
+ return `❌ Unknown agent: \`${agentName}\`. Available agents: ${Object.keys(agents_1.agentRegistry).join(', ')}`;
513
553
  }
514
554
  contextManager.setContextData(contextId, { agent: agentName });
515
- await respond(`Switched agent to: \`${agentName}\``);
555
+ return `Switched agent to: \`${agentName}\``;
556
+ };
557
+ // Handle /agent command
558
+ app.command('/agent', async ({ command, ack, client, respond }) => {
559
+ await ack();
560
+ const result = await switchAgent(command.channel_id, command.text.trim(), command.trigger_id, client);
561
+ if (result)
562
+ await respond(result);
563
+ });
564
+ // Handle "/agent" messages
565
+ app.message(/^\/agent\s*(.*)/, async ({ message, context, say, client }) => {
566
+ const msg = message;
567
+ const input = context.matches[1].trim();
568
+ const contextId = msg.thread_ts || msg.channel;
569
+ const result = await switchAgent(contextId, input, undefined, client);
570
+ if (result) {
571
+ await say({
572
+ text: result,
573
+ thread_ts: msg.thread_ts,
574
+ });
575
+ }
516
576
  });
517
577
  // Handle Modal Submissions
518
578
  app.view('model_select_modal', async ({ ack, view, client }) => {
@@ -523,7 +583,7 @@ const start = async () => {
523
583
  contextManager.setContextData(contextId, { model: selectedModel });
524
584
  await client.chat.postMessage({
525
585
  channel: contextId,
526
- text: `🔄 Model updated to \`${selectedModel}\``
586
+ text: `🔄 Model updated to \`${selectedModel}\``,
527
587
  });
528
588
  }
529
589
  });
@@ -535,7 +595,7 @@ const start = async () => {
535
595
  contextManager.setContextData(contextId, { agent: selectedAgent });
536
596
  await client.chat.postMessage({
537
597
  channel: contextId,
538
- text: `🔄 Agent updated to \`${selectedAgent}\``
598
+ text: `🔄 Agent updated to \`${selectedAgent}\``,
539
599
  });
540
600
  }
541
601
  });
@@ -543,17 +603,19 @@ const start = async () => {
543
603
  app.action('retry_turn', async ({ ack, body, client }) => {
544
604
  await ack();
545
605
  const action = body;
546
- const contextId = action.container.thread_ts || action.container.channel_id;
606
+ const contextId = action.container?.thread_ts || action.container?.channel_id;
547
607
  const channelId = action.container.channel_id;
548
608
  const messageTs = action.container.message_ts;
549
609
  const threadTs = action.container.thread_ts;
550
610
  const context = contextManager.getContext(contextId);
551
611
  // Remove last assistant message
552
- if (context.messages.length > 0 && context.messages[context.messages.length - 1].role === 'assistant') {
612
+ if (context.messages.length > 0 &&
613
+ context.messages[context.messages.length - 1].role === 'assistant') {
553
614
  context.messages.pop();
554
615
  }
555
616
  // Add a note to the last user message if not already present
556
- if (context.messages.length > 0 && context.messages[context.messages.length - 1].role === 'user') {
617
+ if (context.messages.length > 0 &&
618
+ context.messages[context.messages.length - 1].role === 'user') {
557
619
  const lastMsg = context.messages[context.messages.length - 1];
558
620
  if (!lastMsg.content.includes('[System: The user requested a retry')) {
559
621
  lastMsg.content += '\n\n[System: The user requested a retry of this instruction.]';
@@ -565,7 +627,8 @@ const start = async () => {
565
627
  const msg = typeof args === 'string' ? { text: args } : args;
566
628
  return await client.chat.postMessage({
567
629
  channel: channelId,
568
- ...msg
630
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
631
+ ...msg,
569
632
  });
570
633
  };
571
634
  // Re-run agent turn (without new user text, using existing message to update)
@@ -575,7 +638,7 @@ const start = async () => {
575
638
  app.action('revert_turn', async ({ ack, body, client }) => {
576
639
  await ack();
577
640
  const action = body;
578
- const contextId = action.container.thread_ts || action.container.channel_id;
641
+ const contextId = action.container?.thread_ts || action.container?.channel_id;
579
642
  const channelId = action.container.channel_id;
580
643
  const threadTs = action.container.thread_ts;
581
644
  const context = contextManager.getContext(contextId);
@@ -592,7 +655,8 @@ const start = async () => {
592
655
  const msg = typeof args === 'string' ? { text: args } : args;
593
656
  return await client.chat.postMessage({
594
657
  channel: channelId,
595
- ...msg
658
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
659
+ ...msg,
596
660
  });
597
661
  };
598
662
  const revertPrompt = `The user wants to revert the actions taken for the following request: "${lastUserText}". Please undo any file modifications or state changes made in that turn.`;
@@ -600,10 +664,9 @@ const start = async () => {
600
664
  // We do NOT pop messages here, we want the agent to see what it did and undo it.
601
665
  await processAgentTurn(contextId, channelId, threadTs, revertPrompt, say, client);
602
666
  });
603
- // Handle /help command
604
- app.command('/help', async ({ ack, respond }) => {
605
- await ack();
606
- const helpMessage = `
667
+ // Helper for Help
668
+ const getHelpMessage = () => {
669
+ return `
607
670
  *Available Commands:*
608
671
 
609
672
  - \`/plan [goal]\`: Enter plan mode to generate and execute coding plans.
@@ -612,7 +675,19 @@ const start = async () => {
612
675
  - \`/agent [name]\`: Switch the backing agent (e.g., \`gemini\`, \`claude\`, \`cursor\`).
613
676
  - \`/help\`: Show this help message.
614
677
  `;
615
- await respond(helpMessage);
678
+ };
679
+ // Handle /help command
680
+ app.command('/help', async ({ ack, respond }) => {
681
+ await ack();
682
+ await respond(getHelpMessage());
683
+ });
684
+ // Handle "/help" messages
685
+ app.message(/^\/help/, async ({ message, say }) => {
686
+ const msg = message;
687
+ await say({
688
+ text: getHelpMessage(),
689
+ thread_ts: msg.thread_ts,
690
+ });
616
691
  });
617
692
  // Handle App Home Tab
618
693
  app.event('app_home_opened', async ({ event, client, logger }) => {
@@ -620,12 +695,12 @@ const start = async () => {
620
695
  const contexts = contextManager.getAllContexts();
621
696
  // Sort by last active
622
697
  contexts.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
623
- const contextBlocks = contexts.slice(0, 5).map(ctx => ({
698
+ const contextBlocks = contexts.slice(0, 5).map((ctx) => ({
624
699
  type: 'section',
625
700
  text: {
626
701
  type: 'mrkdwn',
627
- text: `*Context:* \`${ctx.id}\`\n*Last Active:* ${ctx.lastActiveAt.toLocaleString()}\n*Agent:* \`${ctx.data.agent || 'gemini'}\` | *Model:* \`${ctx.data.model || 'default'}\` | *CWD:* \`${ctx.data.cwd || 'root'}\``
628
- }
702
+ text: `*Context:* \`${ctx.id}\`\n*Last Active:* ${ctx.lastActiveAt.toLocaleString()}\n*Agent:* \`${ctx.data.agent || 'gemini'}\` | *Model:* \`${ctx.data.model || 'default'}\` | *CWD:* \`${ctx.data.cwd || 'root'}\``,
703
+ },
629
704
  }));
630
705
  const blocks = [
631
706
  {
@@ -633,54 +708,56 @@ const start = async () => {
633
708
  text: {
634
709
  type: 'plain_text',
635
710
  text: '🏠 Gibi Dashboard',
636
- emoji: true
637
- }
711
+ emoji: true,
712
+ },
638
713
  },
639
714
  {
640
715
  type: 'section',
641
716
  text: {
642
717
  type: 'mrkdwn',
643
- text: `Welcome, <@${event.user}>! Here is a summary of your Gibi instance status.`
644
- }
718
+ text: `Welcome, <@${event.user}>! Here is a summary of your Gibi instance status.`,
719
+ },
645
720
  },
646
721
  {
647
- type: 'divider'
722
+ type: 'divider',
648
723
  },
649
724
  {
650
725
  type: 'section',
651
726
  fields: [
652
727
  {
653
728
  type: 'mrkdwn',
654
- text: `*Default Agent:*\n\`gemini\``
729
+ text: `*Default Agent:*\n\`gemini\``,
655
730
  },
656
731
  {
657
732
  type: 'mrkdwn',
658
- text: `*Available Agents:*\n${Object.keys(agents_1.agentRegistry).map(k => `\`${k}\``).join(', ')}`
659
- }
660
- ]
733
+ text: `*Available Agents:*\n${Object.keys(agents_1.agentRegistry)
734
+ .map((k) => `\`${k}\``)
735
+ .join(', ')}`,
736
+ },
737
+ ],
661
738
  },
662
739
  {
663
- type: 'divider'
740
+ type: 'divider',
664
741
  },
665
742
  {
666
743
  type: 'header',
667
744
  text: {
668
745
  type: 'plain_text',
669
746
  text: '🕒 Recent Activity',
670
- emoji: true
671
- }
672
- }
747
+ emoji: true,
748
+ },
749
+ },
673
750
  ];
674
751
  if (contextBlocks.length > 0) {
675
- blocks.push(...contextBlocks.flatMap(b => [b, { type: 'divider' }]));
752
+ blocks.push(...contextBlocks.flatMap((b) => [b, { type: 'divider' }]));
676
753
  }
677
754
  else {
678
755
  blocks.push({
679
756
  type: 'section',
680
757
  text: {
681
758
  type: 'mrkdwn',
682
- text: '_No active contexts found. Send a message to Gibi in any channel to start._'
683
- }
759
+ text: '_No active contexts found. Send a message to Gibi in any channel to start._',
760
+ },
684
761
  });
685
762
  }
686
763
  blocks.push({
@@ -688,16 +765,16 @@ const start = async () => {
688
765
  elements: [
689
766
  {
690
767
  type: 'mrkdwn',
691
- text: `Last updated: ${new Date().toLocaleString()}`
692
- }
693
- ]
768
+ text: `Last updated: ${new Date().toLocaleString()}`,
769
+ },
770
+ ],
694
771
  });
695
772
  await client.views.publish({
696
773
  user_id: event.user,
697
774
  view: {
698
775
  type: 'home',
699
- blocks: blocks
700
- }
776
+ blocks: blocks,
777
+ },
701
778
  });
702
779
  }
703
780
  catch (error) {