@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.
- package/.opencode/command/mattermost-connect.md +5 -0
- package/.opencode/command/mattermost-disconnect.md +5 -0
- package/.opencode/command/mattermost-monitor.md +12 -0
- package/.opencode/command/mattermost-status.md +5 -0
- package/.opencode/command/speckit.analyze.md +184 -0
- package/.opencode/command/speckit.checklist.md +294 -0
- package/.opencode/command/speckit.clarify.md +181 -0
- package/.opencode/command/speckit.constitution.md +82 -0
- package/.opencode/command/speckit.implement.md +135 -0
- package/.opencode/command/speckit.plan.md +89 -0
- package/.opencode/command/speckit.specify.md +258 -0
- package/.opencode/command/speckit.tasks.md +137 -0
- package/.opencode/command/speckit.taskstoissues.md +30 -0
- package/.opencode/plugin/mattermost-control/event-handlers/compaction.ts +61 -0
- package/.opencode/plugin/mattermost-control/event-handlers/file.ts +36 -0
- package/.opencode/plugin/mattermost-control/event-handlers/index.ts +14 -0
- package/.opencode/plugin/mattermost-control/event-handlers/message.ts +124 -0
- package/.opencode/plugin/mattermost-control/event-handlers/permission.ts +34 -0
- package/.opencode/plugin/mattermost-control/event-handlers/question.ts +92 -0
- package/.opencode/plugin/mattermost-control/event-handlers/session.ts +100 -0
- package/.opencode/plugin/mattermost-control/event-handlers/todo.ts +33 -0
- package/.opencode/plugin/mattermost-control/event-handlers/tool.ts +76 -0
- package/.opencode/plugin/mattermost-control/formatters.ts +202 -0
- package/.opencode/plugin/mattermost-control/index.ts +964 -0
- package/.opencode/plugin/mattermost-control/package.json +12 -0
- package/.opencode/plugin/mattermost-control/state.ts +180 -0
- package/.opencode/plugin/mattermost-control/timers.ts +96 -0
- package/.opencode/plugin/mattermost-control/tools/connect.ts +563 -0
- package/.opencode/plugin/mattermost-control/tools/file.ts +41 -0
- package/.opencode/plugin/mattermost-control/tools/index.ts +12 -0
- package/.opencode/plugin/mattermost-control/tools/monitor.ts +183 -0
- package/.opencode/plugin/mattermost-control/tools/schedule.ts +253 -0
- package/.opencode/plugin/mattermost-control/tools/session.ts +120 -0
- package/.opencode/plugin/mattermost-control/types.ts +107 -0
- package/LICENSE +21 -0
- package/README.md +1280 -0
- package/opencode-shared +359 -0
- package/opencode-shared-restart +495 -0
- package/opencode-shared-stop +90 -0
- package/package.json +65 -0
- package/src/clients/mattermost-client.ts +221 -0
- package/src/clients/websocket-client.ts +199 -0
- package/src/command-handler.ts +1035 -0
- package/src/config.ts +170 -0
- package/src/context-builder.ts +309 -0
- package/src/file-completion-handler.ts +521 -0
- package/src/file-handler.ts +242 -0
- package/src/guest-approval-handler.ts +223 -0
- package/src/logger.ts +73 -0
- package/src/merge-handler.ts +335 -0
- package/src/message-router.ts +151 -0
- package/src/models/index.ts +197 -0
- package/src/models/routing.ts +50 -0
- package/src/models/thread-mapping.ts +40 -0
- package/src/monitor-service.ts +222 -0
- package/src/notification-service.ts +118 -0
- package/src/opencode-session-registry.ts +370 -0
- package/src/persistence/team-store.ts +396 -0
- package/src/persistence/thread-mapping-store.ts +258 -0
- package/src/question-handler.ts +401 -0
- package/src/reaction-handler.ts +111 -0
- package/src/response-streamer.ts +364 -0
- package/src/scheduler/schedule-store.ts +261 -0
- package/src/scheduler/scheduler-service.ts +349 -0
- package/src/session-manager.ts +142 -0
- package/src/session-ownership-handler.ts +253 -0
- package/src/status-indicator.ts +279 -0
- package/src/thread-manager.ts +231 -0
- package/src/todo-manager.ts +162 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ParsedCommand } from "../message-router.js";
|
|
2
|
+
|
|
3
|
+
export interface ThreadPromptRoute {
|
|
4
|
+
type: "thread_prompt";
|
|
5
|
+
sessionId: string;
|
|
6
|
+
threadRootPostId: string;
|
|
7
|
+
promptText: string;
|
|
8
|
+
fileIds?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MainDmCommandRoute {
|
|
12
|
+
type: "main_dm_command";
|
|
13
|
+
command: ParsedCommand;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MainDmPromptRoute {
|
|
17
|
+
type: "main_dm_prompt";
|
|
18
|
+
errorMessage: string;
|
|
19
|
+
suggestedAction: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UnknownThreadRoute {
|
|
23
|
+
type: "unknown_thread";
|
|
24
|
+
threadRootPostId: string;
|
|
25
|
+
errorMessage: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EndedSessionRoute {
|
|
29
|
+
type: "ended_session";
|
|
30
|
+
sessionId: string;
|
|
31
|
+
threadRootPostId: string;
|
|
32
|
+
errorMessage: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MergedSessionRoute {
|
|
36
|
+
type: "merged_session";
|
|
37
|
+
sessionId: string;
|
|
38
|
+
threadRootPostId: string;
|
|
39
|
+
mergedInto: string;
|
|
40
|
+
errorMessage: string;
|
|
41
|
+
redirectLink?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type InboundRouteResult =
|
|
45
|
+
| ThreadPromptRoute
|
|
46
|
+
| MainDmCommandRoute
|
|
47
|
+
| MainDmPromptRoute
|
|
48
|
+
| UnknownThreadRoute
|
|
49
|
+
| EndedSessionRoute
|
|
50
|
+
| MergedSessionRoute;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const ModelSelectionSchema = z.object({
|
|
4
|
+
providerID: z.string().min(1),
|
|
5
|
+
modelID: z.string().min(1),
|
|
6
|
+
displayName: z.string().optional(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type ModelSelection = z.infer<typeof ModelSelectionSchema>;
|
|
10
|
+
|
|
11
|
+
export const ThreadSessionMappingSchema = z.object({
|
|
12
|
+
sessionId: z.string().min(1),
|
|
13
|
+
threadRootPostId: z.string().min(1),
|
|
14
|
+
shortId: z.string().min(6).max(10),
|
|
15
|
+
mattermostUserId: z.string().min(1),
|
|
16
|
+
dmChannelId: z.string().min(1),
|
|
17
|
+
channelId: z.string().min(1).optional(),
|
|
18
|
+
projectName: z.string().min(1),
|
|
19
|
+
directory: z.string().min(1),
|
|
20
|
+
sessionTitle: z.string().optional(),
|
|
21
|
+
status: z.enum(["active", "ended", "disconnected", "orphaned", "merged"]),
|
|
22
|
+
createdAt: z.string().datetime(),
|
|
23
|
+
lastActivityAt: z.string().datetime(),
|
|
24
|
+
endedAt: z.string().datetime().optional(),
|
|
25
|
+
model: ModelSelectionSchema.optional(),
|
|
26
|
+
pendingModelSelection: z.boolean().optional(),
|
|
27
|
+
approvedUsers: z.array(z.string()).optional(),
|
|
28
|
+
approveAllUsers: z.boolean().optional(),
|
|
29
|
+
approveNextMessage: z.boolean().optional(),
|
|
30
|
+
mergedInto: z.string().optional(),
|
|
31
|
+
mergedAt: z.string().datetime().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const ThreadMappingFileSchema = z.object({
|
|
35
|
+
version: z.literal(1),
|
|
36
|
+
mappings: z.array(ThreadSessionMappingSchema),
|
|
37
|
+
lastModified: z.string().datetime(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type ThreadMappingFileV1 = z.infer<typeof ThreadMappingFileSchema>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor Service for Mattermost Alerts
|
|
3
|
+
*
|
|
4
|
+
* Enables ephemeral monitoring of OpenCode sessions. When a monitored session
|
|
5
|
+
* triggers an event (permission request, idle, question), sends a one-time
|
|
6
|
+
* DM alert to the specified Mattermost user.
|
|
7
|
+
*/
|
|
8
|
+
import { MattermostClient } from "./clients/mattermost-client.js";
|
|
9
|
+
import { loadConfig, type MattermostConfig } from "./config.js";
|
|
10
|
+
import { log } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
export interface MonitoredSession {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
shortId: string;
|
|
15
|
+
mattermostUserId: string;
|
|
16
|
+
mattermostUsername: string;
|
|
17
|
+
projectName: string;
|
|
18
|
+
sessionTitle?: string;
|
|
19
|
+
directory: string;
|
|
20
|
+
registeredAt: Date;
|
|
21
|
+
persistent: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Alert types that trigger notifications
|
|
26
|
+
*/
|
|
27
|
+
export type AlertType = "permission.asked" | "session.idle" | "question";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Alert context passed to formatAlertMessage
|
|
31
|
+
*/
|
|
32
|
+
export interface AlertContext {
|
|
33
|
+
type: AlertType;
|
|
34
|
+
session: MonitoredSession;
|
|
35
|
+
/** Additional details about what's waiting (e.g., permission description) */
|
|
36
|
+
details?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* In-memory registry of monitored sessions.
|
|
41
|
+
* Key: sessionId (full), Value: MonitoredSession
|
|
42
|
+
*/
|
|
43
|
+
class MonitorServiceImpl {
|
|
44
|
+
private monitoredSessions: Map<string, MonitoredSession> = new Map();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Register a session for monitoring
|
|
48
|
+
*/
|
|
49
|
+
register(session: MonitoredSession): void {
|
|
50
|
+
this.monitoredSessions.set(session.sessionId, session);
|
|
51
|
+
log.info(`[Monitor] Registered session ${session.shortId} (${session.projectName}) for user @${session.mattermostUsername} [fullId=${session.sessionId}]`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Unregister a session from monitoring
|
|
56
|
+
*/
|
|
57
|
+
unregister(sessionId: string): boolean {
|
|
58
|
+
const session = this.monitoredSessions.get(sessionId);
|
|
59
|
+
if (session) {
|
|
60
|
+
this.monitoredSessions.delete(sessionId);
|
|
61
|
+
log.info(`[Monitor] Unregistered session ${session.shortId}`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a session is being monitored
|
|
69
|
+
*/
|
|
70
|
+
isMonitored(sessionId: string): boolean {
|
|
71
|
+
return this.monitoredSessions.has(sessionId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a monitored session by ID
|
|
76
|
+
*/
|
|
77
|
+
get(sessionId: string): MonitoredSession | undefined {
|
|
78
|
+
return this.monitoredSessions.get(sessionId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all monitored sessions
|
|
83
|
+
*/
|
|
84
|
+
getAll(): MonitoredSession[] {
|
|
85
|
+
return Array.from(this.monitoredSessions.values());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear all monitored sessions
|
|
90
|
+
*/
|
|
91
|
+
clear(): void {
|
|
92
|
+
this.monitoredSessions.clear();
|
|
93
|
+
log.info("[Monitor] Cleared all monitored sessions");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get count of monitored sessions
|
|
98
|
+
*/
|
|
99
|
+
count(): number {
|
|
100
|
+
return this.monitoredSessions.size;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const MonitorService = new MonitorServiceImpl();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format an alert message for Mattermost
|
|
108
|
+
*/
|
|
109
|
+
export function formatAlertMessage(context: AlertContext): string {
|
|
110
|
+
const { type, session, details } = context;
|
|
111
|
+
|
|
112
|
+
const header = `:bell: **OpenCode Session Alert**`;
|
|
113
|
+
const projectLine = `**Project:** ${session.projectName}`;
|
|
114
|
+
const sessionLine = session.sessionTitle
|
|
115
|
+
? `**Session:** \`${session.shortId}\` - ${session.sessionTitle}`
|
|
116
|
+
: `**Session:** \`${session.shortId}\``;
|
|
117
|
+
const directoryLine = `**Directory:** \`${session.directory}\``;
|
|
118
|
+
|
|
119
|
+
let alertTypeText: string;
|
|
120
|
+
let icon: string;
|
|
121
|
+
|
|
122
|
+
switch (type) {
|
|
123
|
+
case "permission.asked":
|
|
124
|
+
icon = ":lock:";
|
|
125
|
+
alertTypeText = "Permission requested";
|
|
126
|
+
break;
|
|
127
|
+
case "session.idle":
|
|
128
|
+
icon = ":hourglass:";
|
|
129
|
+
alertTypeText = "Session is idle (waiting for input)";
|
|
130
|
+
break;
|
|
131
|
+
case "question":
|
|
132
|
+
icon = ":question:";
|
|
133
|
+
alertTypeText = "Question awaiting answer";
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
icon = ":bell:";
|
|
137
|
+
alertTypeText = "Session needs attention";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const alertLine = `${icon} **Alert:** ${alertTypeText}`;
|
|
141
|
+
const detailsLine = details ? `**Details:** ${details}` : "";
|
|
142
|
+
const actionLine = `\n_Use \`!use ${session.shortId}\` in DM to connect to this session._`;
|
|
143
|
+
|
|
144
|
+
const parts = [header, "", projectLine, sessionLine, directoryLine, "", alertLine];
|
|
145
|
+
if (detailsLine) parts.push(detailsLine);
|
|
146
|
+
parts.push(actionLine);
|
|
147
|
+
|
|
148
|
+
return parts.join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send an ephemeral alert to a Mattermost user.
|
|
153
|
+
* Creates a new MattermostClient, sends the DM, then discards the client.
|
|
154
|
+
*
|
|
155
|
+
* @param mattermostUserId - The Mattermost user ID to send the alert to
|
|
156
|
+
* @param message - The formatted message to send
|
|
157
|
+
* @returns true if sent successfully, false otherwise
|
|
158
|
+
*/
|
|
159
|
+
export async function sendEphemeralAlert(
|
|
160
|
+
mattermostUserId: string,
|
|
161
|
+
message: string
|
|
162
|
+
): Promise<boolean> {
|
|
163
|
+
const config = loadConfig();
|
|
164
|
+
|
|
165
|
+
if (!config.mattermost.token) {
|
|
166
|
+
log.error("[Monitor] Cannot send alert: MATTERMOST_TOKEN not configured");
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (config.mattermost.baseUrl.includes("your-mattermost-instance.example.com")) {
|
|
171
|
+
log.error("[Monitor] Cannot send alert: MATTERMOST_URL not configured");
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let client: MattermostClient | null = null;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
client = new MattermostClient(config.mattermost);
|
|
179
|
+
const dmChannel = await client.createDirectChannel(mattermostUserId);
|
|
180
|
+
await client.createPost(dmChannel.id, message);
|
|
181
|
+
|
|
182
|
+
log.info(`[Monitor] Sent ephemeral alert to user ${mattermostUserId}`);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
186
|
+
log.error(`[Monitor] Failed to send ephemeral alert: ${errorMsg}`);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function handleMonitorAlert(
|
|
192
|
+
sessionId: string,
|
|
193
|
+
alertType: AlertType,
|
|
194
|
+
details?: string,
|
|
195
|
+
connectedSessionId?: string
|
|
196
|
+
): Promise<boolean> {
|
|
197
|
+
if (connectedSessionId && sessionId === connectedSessionId) {
|
|
198
|
+
log.debug(`[Monitor] Skipping alert for connected session ${sessionId.slice(0, 6)}`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const monitoredSession = MonitorService.get(sessionId);
|
|
203
|
+
if (!monitoredSession) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
log.info(`[Monitor] Handling ${alertType} alert for session ${monitoredSession.shortId} (persistent: ${monitoredSession.persistent})`);
|
|
208
|
+
|
|
209
|
+
const message = formatAlertMessage({
|
|
210
|
+
type: alertType,
|
|
211
|
+
session: monitoredSession,
|
|
212
|
+
details,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const sent = await sendEphemeralAlert(monitoredSession.mattermostUserId, message);
|
|
216
|
+
|
|
217
|
+
if (sent && !monitoredSession.persistent) {
|
|
218
|
+
MonitorService.unregister(sessionId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return sent;
|
|
222
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { MattermostClient } from "./clients/mattermost-client.js";
|
|
2
|
+
import type { NotificationsConfig } from "./config.js";
|
|
3
|
+
import type { UserSession, PermissionRequest } from "./session-manager.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export type StatusType = "thinking" | "tool_execution" | "waiting" | "idle";
|
|
7
|
+
|
|
8
|
+
export interface StatusUpdate {
|
|
9
|
+
type: StatusType;
|
|
10
|
+
details?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class NotificationService {
|
|
14
|
+
private mmClient: MattermostClient;
|
|
15
|
+
private config: NotificationsConfig;
|
|
16
|
+
|
|
17
|
+
constructor(mmClient: MattermostClient, config: NotificationsConfig) {
|
|
18
|
+
this.mmClient = mmClient;
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async notifyCompletion(session: UserSession, summary: string, threadRootPostId?: string): Promise<void> {
|
|
23
|
+
if (!this.config.onCompletion) return;
|
|
24
|
+
|
|
25
|
+
const message = `:white_check_mark: **Task Completed**\n\n> ${summary}`;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await this.mmClient.createPost(session.dmChannelId, message, threadRootPostId);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
log.error("[NotificationService] Failed to send completion notification:", error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async notifyPermissionRequest(session: UserSession, request: PermissionRequest): Promise<void> {
|
|
35
|
+
if (!this.config.onPermissionRequest) return;
|
|
36
|
+
|
|
37
|
+
const riskEmoji = {
|
|
38
|
+
low: ":large_blue_circle:",
|
|
39
|
+
medium: ":warning:",
|
|
40
|
+
high: ":red_circle:",
|
|
41
|
+
}[request.risk];
|
|
42
|
+
|
|
43
|
+
const argsDisplay = JSON.stringify(request.args, null, 2);
|
|
44
|
+
const message = `:warning: **Permission Required**
|
|
45
|
+
|
|
46
|
+
OpenCode wants to execute:
|
|
47
|
+
\`\`\`
|
|
48
|
+
${request.tool}(${argsDisplay})
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
**Risk Level**: ${request.risk} ${riskEmoji}
|
|
52
|
+
**Description**: ${request.description}
|
|
53
|
+
|
|
54
|
+
React to respond:
|
|
55
|
+
- :white_check_mark: Approve
|
|
56
|
+
- :x: Deny`;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const post = await this.mmClient.createPost(session.dmChannelId, message);
|
|
60
|
+
session.pendingPermission = request;
|
|
61
|
+
session.currentResponsePostId = post.id;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
log.error("[NotificationService] Failed to send permission request:", error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async notifyError(session: UserSession, error: Error): Promise<void> {
|
|
68
|
+
if (!this.config.onError) return;
|
|
69
|
+
|
|
70
|
+
const message = `:x: **Error Occurred**
|
|
71
|
+
|
|
72
|
+
\`\`\`
|
|
73
|
+
${error.message}
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
React :arrows_counterclockwise: to retry or send a new message.`;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await this.mmClient.createPost(session.dmChannelId, message);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log.error("[NotificationService] Failed to send error notification:", err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async notifyStatus(session: UserSession, status: StatusUpdate): Promise<void> {
|
|
86
|
+
if (!this.config.onStatusUpdate) return;
|
|
87
|
+
|
|
88
|
+
const statusEmoji = {
|
|
89
|
+
thinking: ":thought_balloon:",
|
|
90
|
+
tool_execution: ":hammer_and_wrench:",
|
|
91
|
+
waiting: ":hourglass_flowing_sand:",
|
|
92
|
+
idle: ":zzz:",
|
|
93
|
+
}[status.type];
|
|
94
|
+
|
|
95
|
+
const message = `${statusEmoji} **${status.type.replace("_", " ")}**${status.details ? `\n${status.details}` : ""}`;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await this.mmClient.createPost(session.dmChannelId, message);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
log.error("[NotificationService] Failed to send status notification:", error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async notifySessionTakeover(session: UserSession, newProject: string): Promise<void> {
|
|
105
|
+
const message = `:information_source: **Session Transferred**
|
|
106
|
+
|
|
107
|
+
Mattermost control has been transferred to another OpenCode instance.
|
|
108
|
+
New project: \`${newProject}\`
|
|
109
|
+
|
|
110
|
+
DMs will now be handled by the new session.`;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await this.mmClient.createPost(session.dmChannelId, message);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
log.error("[NotificationService] Failed to send takeover notification:", error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|