@shakudo/opencode-mattermost-control 0.3.45

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 (69) hide show
  1. package/.opencode/command/mattermost-connect.md +5 -0
  2. package/.opencode/command/mattermost-disconnect.md +5 -0
  3. package/.opencode/command/mattermost-monitor.md +12 -0
  4. package/.opencode/command/mattermost-status.md +5 -0
  5. package/.opencode/command/speckit.analyze.md +184 -0
  6. package/.opencode/command/speckit.checklist.md +294 -0
  7. package/.opencode/command/speckit.clarify.md +181 -0
  8. package/.opencode/command/speckit.constitution.md +82 -0
  9. package/.opencode/command/speckit.implement.md +135 -0
  10. package/.opencode/command/speckit.plan.md +89 -0
  11. package/.opencode/command/speckit.specify.md +258 -0
  12. package/.opencode/command/speckit.tasks.md +137 -0
  13. package/.opencode/command/speckit.taskstoissues.md +30 -0
  14. package/.opencode/plugin/mattermost-control/event-handlers/compaction.ts +61 -0
  15. package/.opencode/plugin/mattermost-control/event-handlers/file.ts +36 -0
  16. package/.opencode/plugin/mattermost-control/event-handlers/index.ts +14 -0
  17. package/.opencode/plugin/mattermost-control/event-handlers/message.ts +124 -0
  18. package/.opencode/plugin/mattermost-control/event-handlers/permission.ts +34 -0
  19. package/.opencode/plugin/mattermost-control/event-handlers/question.ts +92 -0
  20. package/.opencode/plugin/mattermost-control/event-handlers/session.ts +100 -0
  21. package/.opencode/plugin/mattermost-control/event-handlers/todo.ts +33 -0
  22. package/.opencode/plugin/mattermost-control/event-handlers/tool.ts +76 -0
  23. package/.opencode/plugin/mattermost-control/formatters.ts +202 -0
  24. package/.opencode/plugin/mattermost-control/index.ts +964 -0
  25. package/.opencode/plugin/mattermost-control/package.json +12 -0
  26. package/.opencode/plugin/mattermost-control/state.ts +180 -0
  27. package/.opencode/plugin/mattermost-control/timers.ts +96 -0
  28. package/.opencode/plugin/mattermost-control/tools/connect.ts +563 -0
  29. package/.opencode/plugin/mattermost-control/tools/file.ts +41 -0
  30. package/.opencode/plugin/mattermost-control/tools/index.ts +12 -0
  31. package/.opencode/plugin/mattermost-control/tools/monitor.ts +183 -0
  32. package/.opencode/plugin/mattermost-control/tools/schedule.ts +253 -0
  33. package/.opencode/plugin/mattermost-control/tools/session.ts +120 -0
  34. package/.opencode/plugin/mattermost-control/types.ts +107 -0
  35. package/LICENSE +21 -0
  36. package/README.md +1280 -0
  37. package/opencode-shared +359 -0
  38. package/opencode-shared-restart +495 -0
  39. package/opencode-shared-stop +90 -0
  40. package/package.json +65 -0
  41. package/src/clients/mattermost-client.ts +221 -0
  42. package/src/clients/websocket-client.ts +199 -0
  43. package/src/command-handler.ts +1035 -0
  44. package/src/config.ts +170 -0
  45. package/src/context-builder.ts +309 -0
  46. package/src/file-completion-handler.ts +521 -0
  47. package/src/file-handler.ts +242 -0
  48. package/src/guest-approval-handler.ts +223 -0
  49. package/src/logger.ts +73 -0
  50. package/src/merge-handler.ts +335 -0
  51. package/src/message-router.ts +151 -0
  52. package/src/models/index.ts +197 -0
  53. package/src/models/routing.ts +50 -0
  54. package/src/models/thread-mapping.ts +40 -0
  55. package/src/monitor-service.ts +222 -0
  56. package/src/notification-service.ts +118 -0
  57. package/src/opencode-session-registry.ts +370 -0
  58. package/src/persistence/team-store.ts +396 -0
  59. package/src/persistence/thread-mapping-store.ts +258 -0
  60. package/src/question-handler.ts +401 -0
  61. package/src/reaction-handler.ts +111 -0
  62. package/src/response-streamer.ts +364 -0
  63. package/src/scheduler/schedule-store.ts +261 -0
  64. package/src/scheduler/scheduler-service.ts +349 -0
  65. package/src/session-manager.ts +142 -0
  66. package/src/session-ownership-handler.ts +253 -0
  67. package/src/status-indicator.ts +279 -0
  68. package/src/thread-manager.ts +231 -0
  69. package/src/todo-manager.ts +162 -0
@@ -0,0 +1,253 @@
1
+ import { log } from "./logger.js";
2
+ import type { Post } from "./models/index.js";
3
+
4
+ export type ConfirmationStep = "confirm_create" | "select_approval";
5
+
6
+ export type ApprovalPolicy = "none" | "approve_next" | "approve_all";
7
+
8
+ export interface PendingOwnershipConfirmation {
9
+ requestPostId: string;
10
+ userId: string;
11
+ username: string;
12
+ originalPost: Post;
13
+ threadRootPostId: string;
14
+ channelId: string;
15
+ createdAt: Date;
16
+ step: ConfirmationStep;
17
+ }
18
+
19
+ export interface ExistingSessionOwner {
20
+ username: string;
21
+ found: boolean;
22
+ }
23
+
24
+ export class SessionOwnershipHandler {
25
+ private mmClient: any;
26
+ private botUserId: string | null = null;
27
+ private pendingConfirmations: Map<string, PendingOwnershipConfirmation> = new Map();
28
+ private readonly CONFIRMATION_TIMEOUT_MS = 5 * 60 * 1000;
29
+
30
+ constructor(mmClient: any) {
31
+ this.mmClient = mmClient;
32
+ }
33
+
34
+ setBotUserId(botUserId: string): void {
35
+ this.botUserId = botUserId;
36
+ }
37
+
38
+ private getKey(channelId: string, threadRootPostId: string): string {
39
+ return `${channelId}:${threadRootPostId}`;
40
+ }
41
+
42
+ async checkExistingSessionOwner(
43
+ threadRootPostId: string,
44
+ currentUsername: string
45
+ ): Promise<ExistingSessionOwner> {
46
+ try {
47
+ const thread = await this.mmClient.getPostThread(threadRootPostId);
48
+ if (!thread || !thread.posts) {
49
+ return { found: false, username: "" };
50
+ }
51
+
52
+ const posts = Object.values(thread.posts) as Post[];
53
+ const SESSION_ANNOUNCEMENT_MARKER = "OpenCode Session Started";
54
+ const OWNER_FIELD_MARKER = "**Owner**:";
55
+ const OWNER_PATTERN = /\*\*Owner\*\*:\s*@(\w+)/;
56
+
57
+ for (const post of posts) {
58
+ const isBotMessage = this.botUserId && post.user_id === this.botUserId;
59
+ if (!isBotMessage) continue;
60
+
61
+ const message = post.message || "";
62
+ const isSessionAnnouncement = message.includes(SESSION_ANNOUNCEMENT_MARKER) && message.includes(OWNER_FIELD_MARKER);
63
+ if (!isSessionAnnouncement) continue;
64
+
65
+ const ownerMatch = message.match(OWNER_PATTERN);
66
+ if (ownerMatch) {
67
+ const existingOwner = ownerMatch[1];
68
+ if (existingOwner.toLowerCase() !== currentUsername.toLowerCase()) {
69
+ log.info(`[SessionOwnership] Found existing session owner @${existingOwner} (current user: @${currentUsername})`);
70
+ return { found: true, username: existingOwner };
71
+ }
72
+ }
73
+ }
74
+
75
+ return { found: false, username: "" };
76
+ } catch (error) {
77
+ log.warn(`[SessionOwnership] Failed to check existing session owner: ${error}`);
78
+ return { found: false, username: "" };
79
+ }
80
+ }
81
+
82
+ async requestOwnershipConfirmation(
83
+ post: Post,
84
+ username: string,
85
+ threadRootPostId: string,
86
+ channelId: string
87
+ ): Promise<string | null> {
88
+ const key = this.getKey(channelId, threadRootPostId);
89
+
90
+ const existingOwner = await this.checkExistingSessionOwner(threadRootPostId, username);
91
+ if (existingOwner.found) {
92
+ log.info(`[SessionOwnership] Skipping ownership confirmation - thread already owned by @${existingOwner.username}`);
93
+ return null;
94
+ }
95
+
96
+ const confirmationMessage = `No session exists for this thread yet.
97
+
98
+ **Do you want to create a session with your OpenCode instance?**
99
+ - Reply \`yes\` to create a session now
100
+ - Reply \`no\` if you want someone else to be the session owner
101
+
102
+ _Request expires in 5 minutes_`;
103
+
104
+ const requestPost = await this.mmClient.createPost(
105
+ channelId,
106
+ confirmationMessage,
107
+ threadRootPostId
108
+ );
109
+
110
+ const pending: PendingOwnershipConfirmation = {
111
+ requestPostId: requestPost.id,
112
+ userId: post.user_id,
113
+ username,
114
+ originalPost: post,
115
+ threadRootPostId,
116
+ channelId,
117
+ createdAt: new Date(),
118
+ step: "confirm_create",
119
+ };
120
+
121
+ this.pendingConfirmations.set(key, pending);
122
+ log.info(`[SessionOwnership] Requested confirmation from @${username} for thread ${threadRootPostId.substring(0, 8)}`);
123
+
124
+ return requestPost.id;
125
+ }
126
+
127
+ hasPendingConfirmation(channelId: string, threadRootPostId: string, userId: string): boolean {
128
+ const key = this.getKey(channelId, threadRootPostId);
129
+ const pending = this.pendingConfirmations.get(key);
130
+ if (!pending) return false;
131
+
132
+ if (pending.userId !== userId) return false;
133
+
134
+ const elapsed = Date.now() - pending.createdAt.getTime();
135
+ if (elapsed > this.CONFIRMATION_TIMEOUT_MS) {
136
+ this.pendingConfirmations.delete(key);
137
+ log.info(`[SessionOwnership] Confirmation request expired for thread ${threadRootPostId.substring(0, 8)}`);
138
+ return false;
139
+ }
140
+
141
+ return true;
142
+ }
143
+
144
+ getPendingConfirmation(channelId: string, threadRootPostId: string): PendingOwnershipConfirmation | undefined {
145
+ const key = this.getKey(channelId, threadRootPostId);
146
+ const pending = this.pendingConfirmations.get(key);
147
+ if (!pending) return undefined;
148
+
149
+ const elapsed = Date.now() - pending.createdAt.getTime();
150
+ if (elapsed > this.CONFIRMATION_TIMEOUT_MS) {
151
+ this.pendingConfirmations.delete(key);
152
+ return undefined;
153
+ }
154
+
155
+ return pending;
156
+ }
157
+
158
+ async handleReply(
159
+ channelId: string,
160
+ threadRootPostId: string,
161
+ replyText: string
162
+ ): Promise<{ confirmed: boolean; post?: Post; message: string; approvalPolicy?: ApprovalPolicy }> {
163
+ const key = this.getKey(channelId, threadRootPostId);
164
+ const pending = this.pendingConfirmations.get(key);
165
+ if (!pending) {
166
+ return { confirmed: false, message: "No pending confirmation request found." };
167
+ }
168
+
169
+ const trimmed = replyText.trim().toLowerCase();
170
+
171
+ // Step 1: Confirm session creation
172
+ if (pending.step === "confirm_create") {
173
+ if (trimmed === "yes" || trimmed === "y") {
174
+ // Transition to step 2: ask about pre-approving users
175
+ pending.step = "select_approval";
176
+ pending.createdAt = new Date(); // Reset timeout
177
+ this.pendingConfirmations.set(key, pending);
178
+
179
+ const approvalMessage = `Great! **Do you want to pre-approve other people to use this session?**
180
+
181
+ \`1\` - No pre-approval (only you can use this session initially)
182
+ \`2\` - Pre-approve the next message (one-time approval for the next person)
183
+ \`3\` - Approve all subsequent messages (anyone can send prompts)
184
+
185
+ _Reply with a number (1, 2, or 3)_`;
186
+
187
+ await this.mmClient.createPost(
188
+ channelId,
189
+ approvalMessage,
190
+ threadRootPostId
191
+ );
192
+ log.info(`[SessionOwnership] @${pending.username} confirmed session creation, now asking for approval policy`);
193
+ return { confirmed: false, message: "Waiting for approval policy selection." };
194
+ }
195
+
196
+ if (trimmed === "no" || trimmed === "n") {
197
+ this.pendingConfirmations.delete(key);
198
+ await this.mmClient.createPost(
199
+ channelId,
200
+ `Got it. Ask someone else to @mention me to create a session for this thread.`,
201
+ threadRootPostId
202
+ );
203
+ log.info(`[SessionOwnership] @${pending.username} declined session creation for thread ${threadRootPostId.substring(0, 8)}`);
204
+ return { confirmed: false, message: "Session creation declined." };
205
+ }
206
+
207
+ return { confirmed: false, message: "Invalid response. Reply with yes or no." };
208
+ }
209
+
210
+ // Step 2: Select approval policy
211
+ if (pending.step === "select_approval") {
212
+ let approvalPolicy: ApprovalPolicy;
213
+
214
+ if (trimmed === "1") {
215
+ approvalPolicy = "none";
216
+ log.info(`[SessionOwnership] @${pending.username} selected approval policy: none`);
217
+ } else if (trimmed === "2") {
218
+ approvalPolicy = "approve_next";
219
+ log.info(`[SessionOwnership] @${pending.username} selected approval policy: approve_next`);
220
+ } else if (trimmed === "3") {
221
+ approvalPolicy = "approve_all";
222
+ log.info(`[SessionOwnership] @${pending.username} selected approval policy: approve_all`);
223
+ } else {
224
+ return { confirmed: false, message: "Invalid response. Reply with 1, 2, or 3." };
225
+ }
226
+
227
+ this.pendingConfirmations.delete(key);
228
+ log.info(`[SessionOwnership] @${pending.username} confirmed session creation with policy '${approvalPolicy}' for thread ${threadRootPostId.substring(0, 8)}`);
229
+ return { confirmed: true, post: pending.originalPost, message: "Session will be created.", approvalPolicy };
230
+ }
231
+
232
+ return { confirmed: false, message: "Invalid state." };
233
+ }
234
+
235
+ clearPendingConfirmation(channelId: string, threadRootPostId: string): void {
236
+ const key = this.getKey(channelId, threadRootPostId);
237
+ this.pendingConfirmations.delete(key);
238
+ }
239
+
240
+ cleanupExpired(): number {
241
+ let cleaned = 0;
242
+ const now = Date.now();
243
+
244
+ for (const [key, pending] of this.pendingConfirmations.entries()) {
245
+ if (now - pending.createdAt.getTime() > this.CONFIRMATION_TIMEOUT_MS) {
246
+ this.pendingConfirmations.delete(key);
247
+ cleaned++;
248
+ }
249
+ }
250
+
251
+ return cleaned;
252
+ }
253
+ }
@@ -0,0 +1,279 @@
1
+ import type { MattermostClient } from "./clients/mattermost-client.js";
2
+ import { log } from "./logger.js";
3
+
4
+ export type PromptState =
5
+ | { state: "queued"; reason: string; position?: number }
6
+ | { state: "connecting"; sessionId: string; shortId: string }
7
+ | { state: "processing"; startedAt: number }
8
+ | { state: "tool_running"; tool: string; startedAt: number }
9
+ | { state: "waiting"; reason: "permission" | "question"; details?: string }
10
+ | { state: "retrying"; attempt: number; maxAttempts: number; nextRetryAt: number; error: string }
11
+ | { state: "error"; error: string; recoverable: boolean }
12
+ | { state: "complete"; duration: number };
13
+
14
+ const STATUS_EMOJI: Record<PromptState["state"], string> = {
15
+ queued: "⏳",
16
+ connecting: "🔗",
17
+ processing: "💻",
18
+ tool_running: "🔧",
19
+ waiting: "⏸️",
20
+ retrying: "🔄",
21
+ error: "❌",
22
+ complete: "✅",
23
+ };
24
+
25
+ const STATUS_LABELS: Record<PromptState["state"], string> = {
26
+ queued: "Queued",
27
+ connecting: "Connecting",
28
+ processing: "Processing",
29
+ tool_running: "Running Tool",
30
+ waiting: "Waiting",
31
+ retrying: "Retrying",
32
+ error: "Error",
33
+ complete: "Complete",
34
+ };
35
+
36
+ export interface StatusIndicatorConfig {
37
+ postId: string;
38
+ channelId: string;
39
+ threadRootPostId?: string;
40
+ sessionShortId?: string;
41
+ projectName?: string;
42
+ }
43
+
44
+ export class StatusIndicator {
45
+ private mmClient: MattermostClient;
46
+ private config: StatusIndicatorConfig;
47
+ private currentState: PromptState;
48
+ private startTime: number;
49
+ private lastUpdateTime: number;
50
+ private updateThrottleMs: number = 500;
51
+ private contentStarted: boolean = false;
52
+ private processingStartedAt: number | null = null;
53
+
54
+ constructor(mmClient: MattermostClient, config: StatusIndicatorConfig) {
55
+ this.mmClient = mmClient;
56
+ this.config = config;
57
+ this.startTime = Date.now();
58
+ this.lastUpdateTime = 0;
59
+ this.currentState = { state: "queued", reason: "Initializing..." };
60
+ }
61
+
62
+ hasContentStarted(): boolean {
63
+ return this.contentStarted;
64
+ }
65
+
66
+ markContentStarted(): void {
67
+ this.contentStarted = true;
68
+ }
69
+
70
+ getState(): PromptState {
71
+ return this.currentState;
72
+ }
73
+
74
+ getPostId(): string {
75
+ return this.config.postId;
76
+ }
77
+
78
+ updatePostId(newPostId: string): void {
79
+ this.config.postId = newPostId;
80
+ }
81
+
82
+ formatStatusMessage(includeContent: boolean = false, content?: string): string {
83
+ const emoji = STATUS_EMOJI[this.currentState.state];
84
+ const label = STATUS_LABELS[this.currentState.state];
85
+
86
+ let statusLine = `${emoji} **${label}**`;
87
+ let details = "";
88
+
89
+ switch (this.currentState.state) {
90
+ case "queued":
91
+ details = this.currentState.reason;
92
+ if (this.currentState.position !== undefined) {
93
+ details += ` (position ${this.currentState.position})`;
94
+ }
95
+ break;
96
+
97
+ case "connecting":
98
+ details = `Session: ${this.currentState.shortId}`;
99
+ break;
100
+
101
+ case "processing":
102
+ const elapsed = Math.round((Date.now() - this.currentState.startedAt) / 1000);
103
+ details = `${elapsed}s elapsed`;
104
+ break;
105
+
106
+ case "tool_running":
107
+ const toolElapsed = Math.round((Date.now() - this.currentState.startedAt) / 1000);
108
+ details = `\`${this.currentState.tool}\` (${toolElapsed}s)`;
109
+ break;
110
+
111
+ case "waiting":
112
+ if (this.currentState.reason === "permission") {
113
+ details = "Awaiting permission approval";
114
+ } else {
115
+ details = "Awaiting your response";
116
+ }
117
+ if (this.currentState.details) {
118
+ details += `\n> ${this.currentState.details}`;
119
+ }
120
+ break;
121
+
122
+ case "retrying":
123
+ const retryIn = Math.max(0, Math.round((this.currentState.nextRetryAt - Date.now()) / 1000));
124
+ details = `Attempt ${this.currentState.attempt}/${this.currentState.maxAttempts}`;
125
+ if (retryIn > 0) {
126
+ details += ` - retry in ${retryIn}s`;
127
+ }
128
+ details += `\n> ${this.currentState.error}`;
129
+ break;
130
+
131
+ case "error":
132
+ details = this.currentState.error;
133
+ if (this.currentState.recoverable) {
134
+ details += "\n\n_React with 🔁 to retry_";
135
+ }
136
+ break;
137
+
138
+ case "complete":
139
+ const duration = Math.round(this.currentState.duration / 1000);
140
+ details = `Completed in ${duration}s`;
141
+ break;
142
+ }
143
+
144
+ let message = statusLine;
145
+ if (details) {
146
+ message += `\n${details}`;
147
+ }
148
+
149
+ if (includeContent && content) {
150
+ message += `\n\n---\n\n${content}`;
151
+ }
152
+
153
+ if (["processing", "tool_running", "connecting"].includes(this.currentState.state)) {
154
+ if (!includeContent) {
155
+ message += " ...";
156
+ }
157
+ }
158
+
159
+ return message;
160
+ }
161
+
162
+ private async updatePost(content?: string): Promise<void> {
163
+ const now = Date.now();
164
+
165
+ if (now - this.lastUpdateTime < this.updateThrottleMs) {
166
+ return;
167
+ }
168
+
169
+ try {
170
+ const message = this.formatStatusMessage(!!content, content);
171
+ await this.mmClient.updatePost(this.config.postId, message);
172
+ this.lastUpdateTime = now;
173
+ } catch (e) {
174
+ log.error("[StatusIndicator] Failed to update post:", e);
175
+ }
176
+ }
177
+
178
+ async setQueued(reason: string, position?: number): Promise<void> {
179
+ this.currentState = { state: "queued", reason, position };
180
+ log.debug(`[StatusIndicator] State -> queued: ${reason}`);
181
+ await this.updatePost();
182
+ }
183
+
184
+ async setConnecting(sessionId: string, shortId: string): Promise<void> {
185
+ if (this.processingStartedAt === null) {
186
+ this.processingStartedAt = Date.now();
187
+ }
188
+ this.currentState = { state: "connecting", sessionId, shortId };
189
+ log.debug(`[StatusIndicator] State -> connecting: ${shortId}`);
190
+ await this.updatePost();
191
+ }
192
+
193
+ async setProcessing(): Promise<void> {
194
+ if (this.processingStartedAt === null) {
195
+ this.processingStartedAt = Date.now();
196
+ }
197
+ this.currentState = { state: "processing", startedAt: this.processingStartedAt };
198
+ log.debug("[StatusIndicator] State -> processing");
199
+ if (!this.contentStarted) {
200
+ await this.updatePost();
201
+ }
202
+ }
203
+
204
+ async setToolRunning(tool: string): Promise<void> {
205
+ const startedAt = this.processingStartedAt || Date.now();
206
+ this.currentState = { state: "tool_running", tool, startedAt };
207
+ log.debug(`[StatusIndicator] State -> tool_running: ${tool}`);
208
+ if (!this.contentStarted) {
209
+ await this.updatePost();
210
+ }
211
+ }
212
+
213
+ async setWaiting(reason: "permission" | "question", details?: string): Promise<void> {
214
+ this.currentState = { state: "waiting", reason, details };
215
+ log.debug(`[StatusIndicator] State -> waiting: ${reason}`);
216
+ await this.updatePost();
217
+ }
218
+
219
+ async setRetrying(attempt: number, maxAttempts: number, error: string, nextRetryMs: number): Promise<void> {
220
+ this.currentState = {
221
+ state: "retrying",
222
+ attempt,
223
+ maxAttempts,
224
+ error,
225
+ nextRetryAt: Date.now() + nextRetryMs,
226
+ };
227
+ log.debug(`[StatusIndicator] State -> retrying: attempt ${attempt}/${maxAttempts}`);
228
+ await this.updatePost();
229
+ }
230
+
231
+ async setError(error: string, recoverable: boolean = true): Promise<void> {
232
+ this.currentState = { state: "error", error, recoverable };
233
+ log.debug(`[StatusIndicator] State -> error: ${error}`);
234
+ await this.updatePost();
235
+ }
236
+
237
+ async setComplete(): Promise<void> {
238
+ const duration = Date.now() - this.startTime;
239
+ this.currentState = { state: "complete", duration };
240
+ log.debug(`[StatusIndicator] State -> complete: ${duration}ms`);
241
+ }
242
+
243
+ async updateWithContent(content: string): Promise<void> {
244
+ if (this.currentState.state !== "processing" && this.currentState.state !== "tool_running") {
245
+ this.currentState = { state: "processing", startedAt: Date.now() };
246
+ }
247
+ await this.updatePost(content);
248
+ }
249
+
250
+ getElapsedTime(): number {
251
+ return Date.now() - this.startTime;
252
+ }
253
+
254
+ isTerminal(): boolean {
255
+ return this.currentState.state === "complete" || this.currentState.state === "error";
256
+ }
257
+
258
+ isActive(): boolean {
259
+ return ["connecting", "processing", "tool_running"].includes(this.currentState.state);
260
+ }
261
+ }
262
+
263
+ export async function createStatusIndicator(
264
+ mmClient: MattermostClient,
265
+ channelId: string,
266
+ threadRootPostId?: string,
267
+ initialReason: string = "Received message..."
268
+ ): Promise<StatusIndicator> {
269
+ const initialMessage = `⏳ **Queued**\n${initialReason} ...`;
270
+ const post = await mmClient.createPost(channelId, initialMessage, threadRootPostId);
271
+
272
+ const indicator = new StatusIndicator(mmClient, {
273
+ postId: post.id,
274
+ channelId,
275
+ threadRootPostId,
276
+ });
277
+
278
+ return indicator;
279
+ }