@geminixiang/mikan 0.3.0-beta.0 → 0.3.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 (134) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/adapter.d.ts +5 -0
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/context.d.ts +0 -1
  6. package/dist/adapters/discord/context.d.ts.map +1 -1
  7. package/dist/adapters/discord/context.js +62 -84
  8. package/dist/adapters/discord/context.js.map +1 -1
  9. package/dist/adapters/shared.d.ts +1 -2
  10. package/dist/adapters/shared.d.ts.map +1 -1
  11. package/dist/adapters/shared.js +3 -2
  12. package/dist/adapters/shared.js.map +1 -1
  13. package/dist/adapters/slack/bot.d.ts +9 -34
  14. package/dist/adapters/slack/bot.d.ts.map +1 -1
  15. package/dist/adapters/slack/bot.js +374 -358
  16. package/dist/adapters/slack/bot.js.map +1 -1
  17. package/dist/adapters/slack/context.d.ts +0 -1
  18. package/dist/adapters/slack/context.d.ts.map +1 -1
  19. package/dist/adapters/slack/context.js +110 -113
  20. package/dist/adapters/slack/context.js.map +1 -1
  21. package/dist/adapters/slack/session.d.ts +0 -3
  22. package/dist/adapters/slack/session.d.ts.map +1 -1
  23. package/dist/adapters/slack/session.js +2 -8
  24. package/dist/adapters/slack/session.js.map +1 -1
  25. package/dist/adapters/slack/thread-manager.d.ts +0 -1
  26. package/dist/adapters/slack/thread-manager.d.ts.map +1 -1
  27. package/dist/adapters/slack/thread-manager.js +1 -4
  28. package/dist/adapters/slack/thread-manager.js.map +1 -1
  29. package/dist/adapters/slack/tools/block-kit.d.ts +16 -0
  30. package/dist/adapters/slack/tools/block-kit.d.ts.map +1 -0
  31. package/dist/adapters/slack/tools/block-kit.js +105 -0
  32. package/dist/adapters/slack/tools/block-kit.js.map +1 -0
  33. package/dist/adapters/telegram/context.d.ts +0 -1
  34. package/dist/adapters/telegram/context.d.ts.map +1 -1
  35. package/dist/adapters/telegram/context.js +44 -54
  36. package/dist/adapters/telegram/context.js.map +1 -1
  37. package/dist/admin/portal.d.ts.map +1 -1
  38. package/dist/admin/portal.js +2 -3
  39. package/dist/admin/portal.js.map +1 -1
  40. package/dist/agent.d.ts +0 -1
  41. package/dist/agent.d.ts.map +1 -1
  42. package/dist/agent.js +47 -80
  43. package/dist/agent.js.map +1 -1
  44. package/dist/commands/admin.d.ts +0 -3
  45. package/dist/commands/admin.d.ts.map +1 -1
  46. package/dist/commands/admin.js +5 -30
  47. package/dist/commands/admin.js.map +1 -1
  48. package/dist/commands/session-view.d.ts.map +1 -1
  49. package/dist/commands/session-view.js +4 -17
  50. package/dist/commands/session-view.js.map +1 -1
  51. package/dist/commands/types.d.ts +3 -2
  52. package/dist/commands/types.d.ts.map +1 -1
  53. package/dist/commands/types.js.map +1 -1
  54. package/dist/commands/utils.d.ts +3 -1
  55. package/dist/commands/utils.d.ts.map +1 -1
  56. package/dist/commands/utils.js +15 -5
  57. package/dist/commands/utils.js.map +1 -1
  58. package/dist/context.d.ts +0 -1
  59. package/dist/context.d.ts.map +1 -1
  60. package/dist/context.js +1 -23
  61. package/dist/context.js.map +1 -1
  62. package/dist/html.d.ts +2 -0
  63. package/dist/html.d.ts.map +1 -0
  64. package/dist/html.js +4 -0
  65. package/dist/html.js.map +1 -0
  66. package/dist/login/index.d.ts +2 -1
  67. package/dist/login/index.d.ts.map +1 -1
  68. package/dist/login/index.js.map +1 -1
  69. package/dist/login/portal.d.ts +1 -1
  70. package/dist/login/portal.d.ts.map +1 -1
  71. package/dist/login/portal.js +2 -3
  72. package/dist/login/portal.js.map +1 -1
  73. package/dist/login/{session.d.ts → store.d.ts} +1 -1
  74. package/dist/login/store.d.ts.map +1 -0
  75. package/dist/login/{session.js → store.js} +1 -1
  76. package/dist/login/store.js.map +1 -0
  77. package/dist/main.d.ts.map +1 -1
  78. package/dist/main.js +1 -1
  79. package/dist/main.js.map +1 -1
  80. package/dist/portal-shell.d.ts +2 -2
  81. package/dist/portal-shell.d.ts.map +1 -1
  82. package/dist/portal-shell.js +11 -16
  83. package/dist/portal-shell.js.map +1 -1
  84. package/dist/sandbox/cloudflare.d.ts +0 -2
  85. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  86. package/dist/sandbox/cloudflare.js +2 -2
  87. package/dist/sandbox/cloudflare.js.map +1 -1
  88. package/dist/sandbox/container.d.ts +0 -3
  89. package/dist/sandbox/container.d.ts.map +1 -1
  90. package/dist/sandbox/container.js +3 -3
  91. package/dist/sandbox/container.js.map +1 -1
  92. package/dist/sandbox/firecracker.d.ts +0 -2
  93. package/dist/sandbox/firecracker.d.ts.map +1 -1
  94. package/dist/sandbox/firecracker.js +2 -2
  95. package/dist/sandbox/firecracker.js.map +1 -1
  96. package/dist/sandbox/host.d.ts +0 -2
  97. package/dist/sandbox/host.d.ts.map +1 -1
  98. package/dist/sandbox/host.js +2 -2
  99. package/dist/sandbox/host.js.map +1 -1
  100. package/dist/sandbox/image.d.ts +0 -2
  101. package/dist/sandbox/image.d.ts.map +1 -1
  102. package/dist/sandbox/image.js +2 -2
  103. package/dist/sandbox/image.js.map +1 -1
  104. package/dist/sandbox/index.d.ts +1 -6
  105. package/dist/sandbox/index.d.ts.map +1 -1
  106. package/dist/sandbox/index.js +0 -5
  107. package/dist/sandbox/index.js.map +1 -1
  108. package/dist/sandbox/path-context.d.ts +0 -1
  109. package/dist/sandbox/path-context.d.ts.map +1 -1
  110. package/dist/sandbox/path-context.js +1 -1
  111. package/dist/sandbox/path-context.js.map +1 -1
  112. package/dist/sentry.d.ts +2 -2
  113. package/dist/sentry.d.ts.map +1 -1
  114. package/dist/sentry.js.map +1 -1
  115. package/dist/session-view/portal.d.ts.map +1 -1
  116. package/dist/session-view/portal.js +2 -8
  117. package/dist/session-view/portal.js.map +1 -1
  118. package/dist/sessions/chat-session-manager.d.ts.map +1 -1
  119. package/dist/sessions/chat-session-manager.js +5 -9
  120. package/dist/sessions/chat-session-manager.js.map +1 -1
  121. package/dist/tools/index.d.ts +2 -0
  122. package/dist/tools/index.d.ts.map +1 -1
  123. package/dist/tools/index.js +4 -0
  124. package/dist/tools/index.js.map +1 -1
  125. package/dist/vault-routing.d.ts +0 -1
  126. package/dist/vault-routing.d.ts.map +1 -1
  127. package/dist/vault-routing.js +1 -4
  128. package/dist/vault-routing.js.map +1 -1
  129. package/dist/vault.d.ts +2 -1
  130. package/dist/vault.d.ts.map +1 -1
  131. package/dist/vault.js.map +1 -1
  132. package/package.json +3 -1
  133. package/dist/login/session.d.ts.map +0 -1
  134. package/dist/login/session.js.map +0 -1
@@ -238,6 +238,16 @@ export class SlackBot {
238
238
  return result.ts;
239
239
  });
240
240
  }
241
+ async postBlocks(channel, text, blocks) {
242
+ return slackRetry(async () => {
243
+ const result = await this.webClient.chat.postMessage({
244
+ channel,
245
+ text,
246
+ blocks: blocks,
247
+ });
248
+ return result.ts;
249
+ });
250
+ }
241
251
  async uploadFile(channel, filePath, title, threadTs) {
242
252
  return slackRetry(async () => {
243
253
  const fileName = title || basename(filePath);
@@ -254,8 +264,11 @@ export class SlackBot {
254
264
  logToFile(channel, entry) {
255
265
  appendChannelLog(this.workingDir, channel, entry);
256
266
  }
257
- logBotResponse(channel, text, ts, threadTs) {
258
- appendBotResponseLog(this.workingDir, channel, text, ts, threadTs);
267
+ logBotResponse(channel, text, ts, threadTs, slackBlocks) {
268
+ appendBotResponseLog(this.workingDir, channel, text, ts, threadTs, {
269
+ platform: "slack",
270
+ ...(slackBlocks ? { slackBlocks } : {}),
271
+ });
259
272
  }
260
273
  getPlatformInfo() {
261
274
  return {
@@ -370,14 +383,6 @@ export class SlackBot {
370
383
  sessionKey,
371
384
  });
372
385
  }
373
- shouldTriggerSharedThreadReply(channelId, threadTs) {
374
- if (!threadTs)
375
- return false;
376
- const sessionKey = resolveSlackSessionKey(channelId, threadTs);
377
- if (this.handler.isRunning(sessionKey))
378
- return true;
379
- return this.hasKnownThreadSession(channelId, sessionKey);
380
- }
381
386
  buildHomeView() {
382
387
  const blocks = [
383
388
  {
@@ -586,31 +591,19 @@ export class SlackBot {
586
591
  platform: this.getPlatformInfo(),
587
592
  };
588
593
  }
589
- createSlashCommandBot(conversationId, threadTs) {
590
- return {
591
- start: async () => { },
592
- postMessage: async (_channel, text) => {
593
- if (threadTs) {
594
- return this.postInThread(conversationId, threadTs, text);
595
- }
596
- return this.postMessage(conversationId, text);
597
- },
598
- updateMessage: async (channel, ts, text) => {
599
- await this.updateMessage(channel, ts, text);
600
- },
601
- enqueueEvent: (event) => this.enqueueEvent(event),
602
- getPlatformInfo: () => this.getPlatformInfo(),
603
- };
604
- }
605
- async routeSlashLoginCommand(payload) {
606
- const commandSuffix = payload.text?.trim();
607
- const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
594
+ buildSlashCommandEvent(payload, options = {}) {
595
+ const conversationId = payload.channel_id;
596
+ const isDirectMessage = conversationId.startsWith("D");
608
597
  const createdAt = new Date();
609
598
  const eventTs = (createdAt.getTime() / 1000).toFixed(6);
610
- const sourceChannelId = payload.channel_id;
611
- const isDirectMessage = sourceChannelId.startsWith("D");
612
599
  const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
613
- this.logToFile(sourceChannelId, {
600
+ const commandSuffix = options.includeText ? payload.text?.trim() : undefined;
601
+ const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
602
+ const threadTs = options.thread ? payload.thread_ts : undefined;
603
+ const sessionKey = options.thread
604
+ ? resolveSlackSessionKey(conversationId, threadTs)
605
+ : conversationId;
606
+ this.logToFile(conversationId, {
614
607
  date: createdAt.toISOString(),
615
608
  ts: eventTs,
616
609
  user: payload.user_id,
@@ -618,18 +611,43 @@ export class SlackBot {
618
611
  text: commandText,
619
612
  attachments: [],
620
613
  isBot: false,
614
+ ...(threadTs ? { threadTs } : {}),
621
615
  });
622
616
  const event = {
623
- type: isDirectMessage ? "dm" : "private_command",
624
- conversationId: sourceChannelId,
617
+ type: options.type ?? (isDirectMessage ? "dm" : "mention"),
618
+ conversationId,
625
619
  conversationKind: isDirectMessage ? "direct" : "shared",
626
620
  ts: eventTs,
627
621
  user: payload.user_id,
628
622
  text: commandText,
629
623
  attachments: [],
630
- sessionKey: sourceChannelId,
624
+ ...(threadTs ? { thread_ts: threadTs } : {}),
625
+ sessionKey,
631
626
  };
632
- const adapters = this.createCommandAdapters(sourceChannelId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: sourceChannelId });
627
+ const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? { threadTs } : { ephemeralChannelId: conversationId, threadTs });
628
+ return { event, adapters };
629
+ }
630
+ createSlashCommandBot(conversationId, threadTs) {
631
+ return {
632
+ start: async () => { },
633
+ postMessage: async (_channel, text) => {
634
+ if (threadTs) {
635
+ return this.postInThread(conversationId, threadTs, text);
636
+ }
637
+ return this.postMessage(conversationId, text);
638
+ },
639
+ updateMessage: async (channel, ts, text) => {
640
+ await this.updateMessage(channel, ts, text);
641
+ },
642
+ enqueueEvent: (event) => this.enqueueEvent(event),
643
+ getPlatformInfo: () => this.getPlatformInfo(),
644
+ };
645
+ }
646
+ async routeSlashLoginCommand(payload) {
647
+ const { event, adapters } = this.buildSlashCommandEvent(payload, {
648
+ type: payload.channel_id.startsWith("D") ? "dm" : "private_command",
649
+ includeText: true,
650
+ });
633
651
  await this.handler.handleEvent(event, this, adapters);
634
652
  }
635
653
  async routeSlashNewCommand(payload) {
@@ -654,34 +672,7 @@ export class SlackBot {
654
672
  await this.handler.handleNewCommand(conversationId, conversationId, commandBot);
655
673
  }
656
674
  async routeSlashModelCommand(payload) {
657
- const conversationId = payload.channel_id;
658
- const isDirectMessage = conversationId.startsWith("D");
659
- const createdAt = new Date();
660
- const eventTs = (createdAt.getTime() / 1000).toFixed(6);
661
- const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
662
- const commandSuffix = payload.text?.trim();
663
- const commandText = commandSuffix ? `${payload.command} ${commandSuffix}` : payload.command;
664
- this.logToFile(conversationId, {
665
- date: createdAt.toISOString(),
666
- ts: eventTs,
667
- user: payload.user_id,
668
- userName,
669
- text: commandText,
670
- attachments: [],
671
- isBot: false,
672
- });
673
- const sessionKey = conversationId;
674
- const event = {
675
- type: isDirectMessage ? "dm" : "mention",
676
- conversationId,
677
- conversationKind: isDirectMessage ? "direct" : "shared",
678
- ts: eventTs,
679
- user: payload.user_id,
680
- text: commandText,
681
- attachments: [],
682
- sessionKey,
683
- };
684
- const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage ? {} : { ephemeralChannelId: conversationId });
675
+ const { event, adapters } = this.buildSlashCommandEvent(payload, { includeText: true });
685
676
  await this.handler.handleEvent(event, this, adapters);
686
677
  }
687
678
  async routeSlashSandboxCommand(payload) {
@@ -691,71 +682,11 @@ export class SlackBot {
691
682
  await this.routeSlashModelCommand(payload);
692
683
  }
693
684
  async routeSlashSessionCommand(payload) {
694
- const conversationId = payload.channel_id;
695
- const isDirectMessage = conversationId.startsWith("D");
696
- const createdAt = new Date();
697
- const eventTs = (createdAt.getTime() / 1000).toFixed(6);
698
- const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
699
- const commandText = payload.command;
700
- this.logToFile(conversationId, {
701
- date: createdAt.toISOString(),
702
- ts: eventTs,
703
- user: payload.user_id,
704
- userName,
705
- text: commandText,
706
- attachments: [],
707
- isBot: false,
708
- threadTs: payload.thread_ts,
709
- });
710
- const sessionKey = resolveSlackSessionKey(conversationId, payload.thread_ts);
711
- const event = {
712
- type: isDirectMessage ? "dm" : "mention",
713
- conversationId,
714
- conversationKind: isDirectMessage ? "direct" : "shared",
715
- ts: eventTs,
716
- user: payload.user_id,
717
- text: commandText,
718
- attachments: [],
719
- thread_ts: payload.thread_ts,
720
- sessionKey,
721
- };
722
- const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage
723
- ? { threadTs: payload.thread_ts }
724
- : { ephemeralChannelId: conversationId, threadTs: payload.thread_ts });
685
+ const { event, adapters } = this.buildSlashCommandEvent(payload, { thread: true });
725
686
  await this.handler.handleEvent(event, this, adapters);
726
687
  }
727
688
  async routeSlashAdminCommand(payload) {
728
- const conversationId = payload.channel_id;
729
- const isDirectMessage = conversationId.startsWith("D");
730
- const createdAt = new Date();
731
- const eventTs = (createdAt.getTime() / 1000).toFixed(6);
732
- const userName = payload.user_name ?? this.getUser(payload.user_id)?.userName;
733
- const commandText = payload.command;
734
- this.logToFile(conversationId, {
735
- date: createdAt.toISOString(),
736
- ts: eventTs,
737
- user: payload.user_id,
738
- userName,
739
- text: commandText,
740
- attachments: [],
741
- isBot: false,
742
- threadTs: payload.thread_ts,
743
- });
744
- const sessionKey = resolveSlackSessionKey(conversationId, payload.thread_ts);
745
- const event = {
746
- type: isDirectMessage ? "dm" : "mention",
747
- conversationId,
748
- conversationKind: isDirectMessage ? "direct" : "shared",
749
- ts: eventTs,
750
- user: payload.user_id,
751
- text: commandText,
752
- attachments: [],
753
- thread_ts: payload.thread_ts,
754
- sessionKey,
755
- };
756
- const adapters = this.createCommandAdapters(conversationId, payload.user_id, userName, commandText, eventTs, isDirectMessage
757
- ? { threadTs: payload.thread_ts }
758
- : { ephemeralChannelId: conversationId, threadTs: payload.thread_ts });
689
+ const { event, adapters } = this.buildSlashCommandEvent(payload, { thread: true });
759
690
  await this.handler.handleEvent(event, this, adapters);
760
691
  }
761
692
  setupEventHandlers() {
@@ -768,92 +699,80 @@ export class SlackBot {
768
699
  this.socketClient.on("unable_to_socket_mode_start", (err) => {
769
700
  log.logWarning("Slack socket unable_to_start", err ? String(err) : "");
770
701
  });
771
- // Channel @mentions
772
- this.socketClient.on("app_mention", ({ event, ack }) => {
773
- const e = event;
774
- // Skip DMs (handled by message event)
775
- if (e.channel.startsWith("D")) {
776
- ack();
777
- return;
778
- }
779
- // Top-level mentions use a persistent channel session.
780
- // Thread replies get their own isolated session (channelId:thread_ts).
781
- const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
782
- const slackEvent = {
783
- type: "mention",
784
- conversationId: e.channel,
785
- conversationKind: "shared",
786
- channel: e.channel,
787
- ts: e.ts,
788
- thread_ts: e.thread_ts,
789
- user: e.user,
790
- text: this.stripOwnMention(e.text),
791
- files: e.files,
792
- sessionKey,
793
- };
794
- const attachmentsPromise = this.logUserMessage(slackEvent);
795
- // Only trigger processing for messages AFTER startup (not replayed old messages)
796
- if (this.startupTs && e.ts < this.startupTs) {
797
- log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
798
- void attachmentsPromise.catch((err) => {
799
- log.logWarning("Failed to log Slack message", String(err));
800
- });
801
- ack();
802
- return;
702
+ this.socketClient.on("app_mention", (payload) => this.handleAppMention(payload));
703
+ this.socketClient.on("message", (payload) => this.handleMessageEvent(payload));
704
+ this.socketClient.on("slash_commands", (payload) => void this.handleSlashCommand(payload));
705
+ this.socketClient.on("app_home_opened", (payload) => this.handleAppHomeOpened(payload));
706
+ this.socketClient.on("block_actions", (payload) => void this.handleBlockAction(payload));
707
+ this.socketClient.on("interactive", (payload) => void this.handleBlockAction(payload));
708
+ }
709
+ handleAppMention({ event, ack }) {
710
+ const e = event;
711
+ // Skip DMs (handled by message event)
712
+ if (e.channel.startsWith("D")) {
713
+ ack();
714
+ return;
715
+ }
716
+ // Top-level mentions use a persistent channel session.
717
+ // Thread replies get their own isolated session (channelId:thread_ts).
718
+ const sessionKey = resolveSlackSessionKey(e.channel, e.thread_ts);
719
+ const mentionText = this.stripOwnMention(e.text);
720
+ const slackEvent = {
721
+ type: "mention",
722
+ conversationId: e.channel,
723
+ conversationKind: "shared",
724
+ channel: e.channel,
725
+ ts: e.ts,
726
+ thread_ts: e.thread_ts,
727
+ user: e.user,
728
+ text: mentionText || "Please respond to the recent conversation context.",
729
+ files: e.files,
730
+ sessionKey,
731
+ };
732
+ const attachmentsPromise = this.logUserMessage(slackEvent);
733
+ // Only trigger processing for messages AFTER startup (not replayed old messages)
734
+ if (this.startupTs && e.ts < this.startupTs) {
735
+ log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
736
+ void attachmentsPromise.catch((err) => {
737
+ log.logWarning("Failed to log Slack message", String(err));
738
+ });
739
+ ack();
740
+ return;
741
+ }
742
+ // Check for stop command - execute immediately, don't queue!
743
+ if (this.isStopText(slackEvent.text)) {
744
+ const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
745
+ if (stopTarget) {
746
+ this.handler.handleStop(stopTarget, e.channel, this);
803
747
  }
804
- // Check for stop command - execute immediately, don't queue!
805
- if (this.isStopText(slackEvent.text)) {
806
- const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
807
- if (stopTarget) {
808
- this.handler.handleStop(stopTarget, e.channel, this);
809
- }
810
- else {
811
- this.postMessage(e.channel, formatNothingRunning("slack"));
812
- }
813
- void attachmentsPromise.catch((err) => {
814
- log.logWarning("Failed to log Slack message", String(err));
815
- });
816
- ack();
817
- return;
748
+ else {
749
+ this.postMessage(e.channel, formatNothingRunning("slack"));
818
750
  }
819
- this.getQueue(this.resolveQueueKey(e.channel, sessionKey)).enqueue(async () => {
820
- slackEvent.attachments = await attachmentsPromise;
821
- const adapters = createSlackAdapters(slackEvent, this);
822
- return this.handler.handleEvent(slackEvent, this, adapters);
751
+ void attachmentsPromise.catch((err) => {
752
+ log.logWarning("Failed to log Slack message", String(err));
823
753
  });
824
754
  ack();
755
+ return;
756
+ }
757
+ this.getQueue(this.resolveQueueKey(e.channel, sessionKey)).enqueue(async () => {
758
+ slackEvent.attachments = await attachmentsPromise;
759
+ const adapters = createSlackAdapters(slackEvent, this);
760
+ return this.handler.handleEvent(slackEvent, this, adapters);
825
761
  });
826
- // All messages (for logging) + DMs (for triggering)
827
- this.socketClient.on("message", ({ event, ack }) => {
828
- const e = event;
829
- const hasFiles = !!e.files && e.files.length > 0;
830
- const hasSlackContent = !!e.text || hasFiles || !!e.blocks?.length || !!e.attachments?.length;
831
- const isOwnBotMessage = (!!e.user && e.user === this.botUserId) || (!!this.botId && e.bot_id === this.botId);
832
- if (isOwnBotMessage) {
833
- ack();
834
- return;
835
- }
836
- const isExternalBotMessage = !!e.bot_id || e.subtype === "bot_message";
837
- if (isExternalBotMessage) {
838
- if (e.subtype !== undefined && e.subtype !== "bot_message" && e.subtype !== "file_share") {
839
- ack();
840
- return;
841
- }
842
- if (!hasSlackContent) {
843
- ack();
844
- return;
845
- }
846
- void this.logExternalBotMessage(e).catch((err) => {
847
- log.logWarning("Failed to log Slack bot message", String(err));
848
- });
849
- ack();
850
- return;
851
- }
852
- if (!e.user) {
853
- ack();
854
- return;
855
- }
856
- if (e.subtype !== undefined && e.subtype !== "file_share") {
762
+ ack();
763
+ }
764
+ handleMessageEvent({ event, ack }) {
765
+ const e = event;
766
+ const hasFiles = !!e.files && e.files.length > 0;
767
+ const hasSlackContent = !!e.text || hasFiles || !!e.blocks?.length || !!e.attachments?.length;
768
+ const isOwnBotMessage = (!!e.user && e.user === this.botUserId) || (!!this.botId && e.bot_id === this.botId);
769
+ if (isOwnBotMessage) {
770
+ ack();
771
+ return;
772
+ }
773
+ const isExternalBotMessage = !!e.bot_id || e.subtype === "bot_message";
774
+ if (isExternalBotMessage) {
775
+ if (e.subtype !== undefined && e.subtype !== "bot_message" && e.subtype !== "file_share") {
857
776
  ack();
858
777
  return;
859
778
  }
@@ -861,203 +780,300 @@ export class SlackBot {
861
780
  ack();
862
781
  return;
863
782
  }
864
- const isDM = e.channel_type === "im";
865
- const conversationKind = isDM ? "direct" : "shared";
866
- const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
867
- // Skip channel @mentions - already handled by app_mention event
868
- if (!isDM && isBotMention) {
869
- ack();
870
- return;
871
- }
872
- const isSharedThreadReply = !isDM && this.shouldTriggerSharedThreadReply(e.channel, e.thread_ts);
873
- const sessionKey = isDM || isSharedThreadReply ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
874
- const slackEvent = {
875
- type: isDM ? "dm" : "mention",
876
- conversationId: e.channel,
877
- conversationKind,
878
- channel: e.channel,
879
- ts: e.ts,
880
- thread_ts: e.thread_ts,
881
- user: e.user,
882
- text: this.stripOwnMention(e.text),
883
- files: e.files,
884
- sessionKey,
885
- };
886
- const attachmentsPromise = this.logUserMessage(slackEvent);
887
- // Only trigger processing for messages AFTER startup (not replayed old messages)
888
- if (this.startupTs && e.ts < this.startupTs) {
889
- log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
890
- void attachmentsPromise.catch((err) => {
891
- log.logWarning("Failed to log Slack message", String(err));
892
- });
893
- ack();
894
- return;
895
- }
896
- // Stop command for DM or shared-channel thread reply (app_mention handles "@mikan stop").
897
- if ((isDM || (!isDM && e.thread_ts)) && this.isStopText(slackEvent.text)) {
898
- const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
899
- if (stopTarget) {
900
- this.handler.handleStop(stopTarget, e.channel, this);
901
- }
902
- else {
903
- this.postMessage(e.channel, formatNothingRunning("slack"));
904
- }
905
- void attachmentsPromise.catch((err) => {
906
- log.logWarning("Failed to log Slack message", String(err));
907
- });
908
- ack();
909
- return;
783
+ void this.logExternalBotMessage(e).catch((err) => {
784
+ log.logWarning("Failed to log Slack bot message", String(err));
785
+ });
786
+ ack();
787
+ return;
788
+ }
789
+ if (!e.user) {
790
+ ack();
791
+ return;
792
+ }
793
+ if (e.subtype !== undefined && e.subtype !== "file_share") {
794
+ ack();
795
+ return;
796
+ }
797
+ if (!hasSlackContent) {
798
+ ack();
799
+ return;
800
+ }
801
+ const isDM = e.channel_type === "im";
802
+ const conversationKind = isDM ? "direct" : "shared";
803
+ const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
804
+ // Skip channel @mentions - already handled by app_mention event
805
+ if (!isDM && isBotMention) {
806
+ ack();
807
+ return;
808
+ }
809
+ const isThreadReply = !!e.thread_ts;
810
+ const sessionKey = isDM ? resolveSlackSessionKey(e.channel, e.thread_ts) : undefined;
811
+ const slackEvent = {
812
+ type: isDM ? "dm" : "mention",
813
+ conversationId: e.channel,
814
+ conversationKind,
815
+ channel: e.channel,
816
+ ts: e.ts,
817
+ thread_ts: e.thread_ts,
818
+ user: e.user,
819
+ text: this.stripOwnMention(e.text),
820
+ files: e.files,
821
+ sessionKey,
822
+ };
823
+ const attachmentsPromise = this.logUserMessage(slackEvent);
824
+ // Only trigger processing for messages AFTER startup (not replayed old messages)
825
+ if (this.startupTs && e.ts < this.startupTs) {
826
+ log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
827
+ void attachmentsPromise.catch((err) => {
828
+ log.logWarning("Failed to log Slack message", String(err));
829
+ });
830
+ ack();
831
+ return;
832
+ }
833
+ // Stop command for DM only (app_mention handles shared-channel "@mikan stop").
834
+ if (isDM && this.isStopText(slackEvent.text)) {
835
+ const stopTarget = this.resolveStopTarget(e.channel, e.thread_ts);
836
+ if (stopTarget) {
837
+ this.handler.handleStop(stopTarget, e.channel, this);
910
838
  }
911
- const enqueueTriggered = () => {
912
- const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
913
- // Auto-reply top-level channel messages start with no sessionKey because
914
- // they are only candidates until the policy allows them. Once triggered,
915
- // persist the resolved key on the event; otherwise the runtime fallback
916
- // treats the message ts as a thread session (`channel:ts`) instead of the
917
- // persistent top-level channel session.
918
- slackEvent.sessionKey = activeSessionKey;
919
- this.getQueue(this.resolveQueueKey(e.channel, activeSessionKey)).enqueue(async () => {
920
- slackEvent.attachments = await attachmentsPromise;
921
- const adapters = createSlackAdapters(slackEvent, this);
922
- return this.handler.handleEvent(slackEvent, this, adapters);
923
- });
924
- };
925
- const logOnly = () => {
926
- void attachmentsPromise.catch((err) => {
927
- log.logWarning("Failed to log Slack message", String(err));
928
- });
929
- };
930
- if (isDM || isSharedThreadReply) {
931
- enqueueTriggered();
932
- ack();
933
- return;
839
+ else {
840
+ this.postMessage(e.channel, formatNothingRunning("slack"));
934
841
  }
935
- // Shared-channel non-mention, non-thread: gate via auto-reply policy.
936
- // evaluateAutoReplyPolicy never throws judge errors/timeouts surface as
937
- // trigger:false with a distinct reason, and the user message has already
938
- // been queued for logging via logUserMessage above.
939
- evaluateAutoReplyPolicy({
940
- event: slackEvent,
941
- workingDir: this.workingDir,
942
- }).then((triggerResult) => {
943
- if (triggerResult.trigger)
944
- enqueueTriggered();
945
- else
946
- logOnly();
842
+ void attachmentsPromise.catch((err) => {
843
+ log.logWarning("Failed to log Slack message", String(err));
844
+ });
845
+ ack();
846
+ return;
847
+ }
848
+ const enqueueTriggered = () => {
849
+ const activeSessionKey = slackEvent.sessionKey ?? resolveSlackSessionKey(e.channel, e.thread_ts);
850
+ // Auto-reply top-level channel messages start with no sessionKey because
851
+ // they are only candidates until the policy allows them. Once triggered,
852
+ // persist the resolved key on the event; otherwise the runtime fallback
853
+ // treats the message ts as a thread session (`channel:ts`) instead of the
854
+ // persistent top-level channel session.
855
+ slackEvent.sessionKey = activeSessionKey;
856
+ this.getQueue(this.resolveQueueKey(e.channel, activeSessionKey)).enqueue(async () => {
857
+ slackEvent.attachments = await attachmentsPromise;
858
+ const adapters = createSlackAdapters(slackEvent, this);
859
+ return this.handler.handleEvent(slackEvent, this, adapters);
947
860
  });
861
+ };
862
+ const logOnly = () => {
863
+ void attachmentsPromise.catch((err) => {
864
+ log.logWarning("Failed to log Slack message", String(err));
865
+ });
866
+ };
867
+ if (isDM) {
868
+ enqueueTriggered();
869
+ ack();
870
+ return;
871
+ }
872
+ if (isThreadReply) {
873
+ logOnly();
948
874
  ack();
875
+ return;
876
+ }
877
+ // Shared-channel non-mention top-level messages: gate via auto-reply policy.
878
+ // evaluateAutoReplyPolicy never throws — judge errors/timeouts surface as
879
+ // trigger:false with a distinct reason, and the user message has already
880
+ // been queued for logging via logUserMessage above.
881
+ evaluateAutoReplyPolicy({
882
+ event: slackEvent,
883
+ workingDir: this.workingDir,
884
+ }).then((triggerResult) => {
885
+ if (triggerResult.trigger)
886
+ enqueueTriggered();
887
+ else
888
+ logOnly();
949
889
  });
950
- this.socketClient.on("slash_commands", async ({ body, ack }) => {
951
- const payload = body;
952
- await ack();
953
- if (!payload.command || !payload.channel_id || !payload.user_id) {
954
- return;
955
- }
956
- const handlerPromise = payload.command === "/pi-login"
957
- ? this.routeSlashLoginCommand({
890
+ ack();
891
+ }
892
+ async handleSlashCommand({ body, ack, }) {
893
+ const payload = body;
894
+ await ack();
895
+ if (!payload.command || !payload.channel_id || !payload.user_id) {
896
+ return;
897
+ }
898
+ const handlerPromise = payload.command === "/pi-login"
899
+ ? this.routeSlashLoginCommand({
900
+ command: payload.command,
901
+ text: payload.text,
902
+ channel_id: payload.channel_id,
903
+ user_id: payload.user_id,
904
+ user_name: payload.user_name,
905
+ })
906
+ : payload.command === "/pi-new"
907
+ ? this.routeSlashNewCommand({
958
908
  command: payload.command,
959
- text: payload.text,
960
909
  channel_id: payload.channel_id,
961
910
  user_id: payload.user_id,
962
911
  user_name: payload.user_name,
963
912
  })
964
- : payload.command === "/pi-new"
965
- ? this.routeSlashNewCommand({
913
+ : payload.command === "/pi-session"
914
+ ? this.routeSlashSessionCommand({
966
915
  command: payload.command,
967
916
  channel_id: payload.channel_id,
968
917
  user_id: payload.user_id,
969
918
  user_name: payload.user_name,
919
+ thread_ts: payload.thread_ts,
970
920
  })
971
- : payload.command === "/pi-session"
972
- ? this.routeSlashSessionCommand({
921
+ : payload.command === "/pi-model"
922
+ ? this.routeSlashModelCommand({
973
923
  command: payload.command,
924
+ text: payload.text,
974
925
  channel_id: payload.channel_id,
975
926
  user_id: payload.user_id,
976
927
  user_name: payload.user_name,
977
- thread_ts: payload.thread_ts,
978
928
  })
979
- : payload.command === "/pi-model"
980
- ? this.routeSlashModelCommand({
929
+ : payload.command === "/pi-sandbox"
930
+ ? this.routeSlashSandboxCommand({
981
931
  command: payload.command,
982
932
  text: payload.text,
983
933
  channel_id: payload.channel_id,
984
934
  user_id: payload.user_id,
985
935
  user_name: payload.user_name,
986
936
  })
987
- : payload.command === "/pi-sandbox"
988
- ? this.routeSlashSandboxCommand({
937
+ : payload.command === "/pi-auto-reply"
938
+ ? this.routeSlashAutoReplyCommand({
989
939
  command: payload.command,
990
940
  text: payload.text,
991
941
  channel_id: payload.channel_id,
992
942
  user_id: payload.user_id,
993
943
  user_name: payload.user_name,
994
944
  })
995
- : payload.command === "/pi-auto-reply"
996
- ? this.routeSlashAutoReplyCommand({
945
+ : payload.command === "/pi-admin"
946
+ ? this.routeSlashAdminCommand({
997
947
  command: payload.command,
998
- text: payload.text,
999
948
  channel_id: payload.channel_id,
1000
949
  user_id: payload.user_id,
1001
950
  user_name: payload.user_name,
951
+ thread_ts: payload.thread_ts,
1002
952
  })
1003
- : payload.command === "/pi-admin"
1004
- ? this.routeSlashAdminCommand({
1005
- command: payload.command,
1006
- channel_id: payload.channel_id,
1007
- user_id: payload.user_id,
1008
- user_name: payload.user_name,
1009
- thread_ts: payload.thread_ts,
1010
- })
1011
- : null;
1012
- if (!handlerPromise) {
1013
- return;
1014
- }
1015
- handlerPromise.catch((err) => {
1016
- log.logWarning("Slack slash command error", err instanceof Error ? err.message : String(err));
1017
- });
953
+ : null;
954
+ if (!handlerPromise) {
955
+ return;
956
+ }
957
+ handlerPromise.catch((err) => {
958
+ log.logWarning("Slack slash command error", err instanceof Error ? err.message : String(err));
1018
959
  });
1019
- // App Home tab
1020
- this.socketClient.on("app_home_opened", ({ event, ack }) => {
1021
- const e = event;
960
+ }
961
+ handleAppHomeOpened({ event, ack }) {
962
+ const e = event;
963
+ ack();
964
+ if (e.tab !== "home")
965
+ return;
966
+ this.webClient.views
967
+ .publish({
968
+ user_id: e.user,
969
+ view: this.buildHomeView(),
970
+ })
971
+ .catch((err) => {
972
+ log.logWarning(`Failed to publish App Home view`, String(err));
973
+ });
974
+ }
975
+ async handleBlockAction({ body, ack }) {
976
+ const action = body.actions?.[0];
977
+ if (!action) {
1022
978
  ack();
1023
- if (e.tab !== "home")
1024
- return;
979
+ return;
980
+ }
981
+ if (!action.action_id?.startsWith("force_stop_")) {
982
+ ack();
983
+ this.handleSlackInteraction(body, action);
984
+ return;
985
+ }
986
+ ack();
987
+ const sessionKey = action.action_id.replace("force_stop_", "").replace(/_/g, ":");
988
+ const userId = body.user?.id;
989
+ const channelId = body.container?.channel_id || sessionKey.split(":")[0];
990
+ log.logInfo(`[Force Stop] User ${userId} requested force stop for ${sessionKey}`);
991
+ // Use handler's forceStop method
992
+ this.handler.forceStop(sessionKey);
993
+ // Notify in channel
994
+ await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
995
+ // Refresh home tab
996
+ if (userId) {
1025
997
  this.webClient.views
1026
998
  .publish({
1027
- user_id: e.user,
999
+ user_id: userId,
1028
1000
  view: this.buildHomeView(),
1029
1001
  })
1030
1002
  .catch((err) => {
1031
- log.logWarning(`Failed to publish App Home view`, String(err));
1003
+ log.logWarning(`Failed to refresh App Home view`, String(err));
1032
1004
  });
1005
+ }
1006
+ }
1007
+ handleSlackInteraction(body, action) {
1008
+ const container = body.container ?? {};
1009
+ const channelId = container.channel_id;
1010
+ const userId = body.user?.id;
1011
+ if (!channelId || !userId)
1012
+ return;
1013
+ const selectedOption = action.selected_option;
1014
+ const selectedOptions = Array.isArray(action.selected_options)
1015
+ ? action.selected_options
1016
+ : undefined;
1017
+ const selectedText = selectedOption?.text?.text ?? selectedOption?.value;
1018
+ const selectedTexts = selectedOptions?.map((option) => option.text?.text ?? option.value);
1019
+ const valueText = selectedTexts?.length
1020
+ ? selectedTexts.join(", ")
1021
+ : (selectedText ?? action.value ?? action.action_id);
1022
+ const text = `[Slack action] ${action.action_id}: ${valueText}`;
1023
+ const ts = `action:${Date.now()}`;
1024
+ const threadTs = container.thread_ts;
1025
+ const sessionKey = resolveSlackSessionKey(channelId, threadTs);
1026
+ this.logToFile(channelId, {
1027
+ date: new Date().toISOString(),
1028
+ ts,
1029
+ ...(threadTs ? { threadTs } : {}),
1030
+ user: userId,
1031
+ userName: body.user?.username ?? body.user?.name,
1032
+ text,
1033
+ attachments: [],
1034
+ isBot: false,
1035
+ platform: "slack",
1036
+ slackInteraction: {
1037
+ type: "block_actions",
1038
+ actionId: action.action_id,
1039
+ blockId: action.block_id,
1040
+ actionType: action.type,
1041
+ value: action.value,
1042
+ selectedOption: selectedOption
1043
+ ? { text: selectedOption.text?.text, value: selectedOption.value }
1044
+ : undefined,
1045
+ selectedOptions: selectedOptions?.map((option) => ({
1046
+ text: option.text?.text,
1047
+ value: option.value,
1048
+ })),
1049
+ messageTs: container.message_ts,
1050
+ },
1033
1051
  });
1034
- // Handle button clicks (Force Stop)
1035
- this.socketClient.on("block_actions", async ({ body, ack }) => {
1036
- const action = body.actions?.[0];
1037
- if (!action || !action.action_id?.startsWith("force_stop_")) {
1038
- ack();
1039
- return;
1040
- }
1041
- ack();
1042
- const sessionKey = action.action_id.replace("force_stop_", "").replace(/_/g, ":");
1043
- const userId = body.user?.id;
1044
- const channelId = body.container?.channel_id || sessionKey.split(":")[0];
1045
- log.logInfo(`[Force Stop] User ${userId} requested force stop for ${sessionKey}`);
1046
- // Use handler's forceStop method
1047
- this.handler.forceStop(sessionKey);
1048
- // Notify in channel
1049
- await this.postMessage(channelId, formatForceStopped("slack", userId ?? "unknown"));
1050
- // Refresh home tab
1051
- if (userId) {
1052
- this.webClient.views
1053
- .publish({
1054
- user_id: userId,
1055
- view: this.buildHomeView(),
1056
- })
1057
- .catch((err) => {
1058
- log.logWarning(`Failed to refresh App Home view`, String(err));
1059
- });
1060
- }
1052
+ const event = {
1053
+ type: "slack_action",
1054
+ conversationId: channelId,
1055
+ conversationKind: channelId.startsWith("D") ? "direct" : "shared",
1056
+ ts,
1057
+ user: userId,
1058
+ text,
1059
+ attachments: [],
1060
+ ...(threadTs ? { thread_ts: threadTs } : {}),
1061
+ sessionKey,
1062
+ };
1063
+ this.getQueue(this.resolveQueueKey(channelId, sessionKey)).enqueue(async () => {
1064
+ const slackEvent = {
1065
+ type: event.conversationKind === "direct" ? "dm" : "mention",
1066
+ conversationId: channelId,
1067
+ conversationKind: event.conversationKind,
1068
+ channel: channelId,
1069
+ ts,
1070
+ thread_ts: threadTs,
1071
+ user: userId,
1072
+ text,
1073
+ attachments: [],
1074
+ sessionKey,
1075
+ };
1076
+ return this.handler.handleEvent(event, this, createSlackAdapters(slackEvent, this));
1061
1077
  });
1062
1078
  }
1063
1079
  /**