@teamvibe/poller 0.1.30 → 0.1.31

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.
@@ -1,12 +1,13 @@
1
1
  import type { TeamVibeQueueMessage } from './types.js';
2
2
  import { type SessionLogger } from './logger.js';
3
+ import type { TypingIndicator } from './slack-client.js';
3
4
  export interface SpawnResult {
4
5
  success: boolean;
5
6
  output: string;
6
7
  exitCode: number | null;
7
8
  error?: string;
8
9
  }
9
- export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void): Promise<SpawnResult & {
10
+ export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void, typingIndicator?: TypingIndicator): Promise<SpawnResult & {
10
11
  newSessionId?: string;
11
12
  }>;
12
13
  export declare function getActiveProcessCount(): number;
@@ -4,6 +4,45 @@ import { WebClient } from '@slack/web-api';
4
4
  import { config } from './config.js';
5
5
  import { logger } from './logger.js';
6
6
  import { getBaseBrainPath } from './brain-manager.js';
7
+ /**
8
+ * Maps tool names (or prefixes) to human-readable status messages.
9
+ * More specific patterns are checked first.
10
+ */
11
+ const TOOL_STATUS_MAP = [
12
+ // Slack MCP tools
13
+ { pattern: 'mcp__slack__search_messages', status: 'Searching Slack messages...' },
14
+ { pattern: 'mcp__slack__read_thread', status: 'Reading thread...' },
15
+ { pattern: 'mcp__slack__send_message', status: 'Sending message...' },
16
+ { pattern: 'mcp__slack__set_status', status: '' }, // Don't override Claude's own status
17
+ { pattern: 'mcp__slack__upload_snippet', status: 'Uploading snippet...' },
18
+ { pattern: /^mcp__slack__/, status: 'Working with Slack...' },
19
+ // Web tools
20
+ { pattern: 'WebSearch', status: 'Searching the web...' },
21
+ { pattern: 'WebFetch', status: 'Reading a webpage...' },
22
+ // File/code tools
23
+ { pattern: 'Read', status: 'Reading files...' },
24
+ { pattern: 'Grep', status: 'Searching code...' },
25
+ { pattern: 'Glob', status: 'Finding files...' },
26
+ { pattern: 'Edit', status: 'Editing code...' },
27
+ { pattern: 'Write', status: 'Writing code...' },
28
+ { pattern: 'Bash', status: 'Running a command...' },
29
+ { pattern: 'Agent', status: 'Delegating to a sub-agent...' },
30
+ // Generic MCP fallback
31
+ { pattern: /^mcp__/, status: 'Using an integration...' },
32
+ ];
33
+ function getStatusForTool(toolName) {
34
+ for (const { pattern, status } of TOOL_STATUS_MAP) {
35
+ if (typeof pattern === 'string') {
36
+ if (toolName === pattern)
37
+ return status;
38
+ }
39
+ else {
40
+ if (pattern.test(toolName))
41
+ return status;
42
+ }
43
+ }
44
+ return null;
45
+ }
7
46
  // Per-token Slack client cache for thread context
8
47
  const slackClients = new Map();
9
48
  function getSlackClient(botToken) {
@@ -126,7 +165,7 @@ function formatStreamEvent(event) {
126
165
  return null;
127
166
  }
128
167
  }
129
- async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
168
+ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
130
169
  const slackContext = msg.response_context.slack;
131
170
  const isCronMessage = msg.source === 'cron';
132
171
  if (!slackContext && !isCronMessage) {
@@ -231,6 +270,20 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
231
270
  event.name === 'mcp__slack__send_message') {
232
271
  onMessageSent();
233
272
  }
273
+ // Auto-update typing indicator status based on tool usage
274
+ if (typingIndicator) {
275
+ if (event.type === 'tool_use' && event.name) {
276
+ const status = getStatusForTool(event.name);
277
+ if (status !== null && status !== '') {
278
+ typingIndicator.updateStatus(status);
279
+ }
280
+ // When status is '' (e.g. set_status tool), don't override — Claude is managing it
281
+ }
282
+ else if (event.type === 'assistant') {
283
+ // Reset to "Thinking..." when Claude is reasoning between tool calls
284
+ typingIndicator.updateStatus('Thinking...');
285
+ }
286
+ }
234
287
  }
235
288
  catch {
236
289
  const truncated = line.length > 500 ? line.slice(0, 500) + '...' : line;
@@ -279,20 +332,20 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
279
332
  });
280
333
  });
281
334
  }
282
- export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
283
- const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent);
335
+ export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
336
+ const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent, typingIndicator);
284
337
  // If --resume failed, retry as a fresh session (session files may have been lost on container restart)
285
338
  let retryResult = result;
286
339
  if (!result.success && sessionId && !isFirstMessage) {
287
340
  sessionLog.info('Resume failed, retrying as fresh session (session files may have been lost)');
288
- retryResult = await runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent);
341
+ retryResult = await runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent, typingIndicator);
289
342
  }
290
343
  // If session ID is "already in use" (stale lock file), retry with a fresh session ID
291
344
  if (!retryResult.success && sessionId && retryResult.error?.includes('already in use')) {
292
345
  sessionLog.info('Session ID already in use (stale lock), retrying with fresh session ID');
293
346
  const { randomUUID } = await import('crypto');
294
347
  const freshId = randomUUID();
295
- const freshResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent);
348
+ const freshResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent, typingIndicator);
296
349
  return { ...freshResult, newSessionId: freshId };
297
350
  }
298
351
  return retryResult;
package/dist/poller.js CHANGED
@@ -125,14 +125,14 @@ async function processMessage(received) {
125
125
  processingMessages.add(messageId);
126
126
  const heartbeat = startHeartbeat(receiptHandle, sessionLog);
127
127
  // Start typing indicator
128
- const stopTyping = hasSlackContext
128
+ const typing = hasSlackContext
129
129
  ? startTypingIndicator(queueMessage)
130
130
  : undefined;
131
131
  try {
132
132
  // Refresh base brain if cooldown has elapsed (5 min default)
133
133
  await ensureBaseBrain();
134
- const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => stopTyping?.());
135
- stopTyping?.();
134
+ const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => typing?.stop(), typing);
135
+ typing?.stop();
136
136
  // If Claude generated a new session ID (stale lock recovery), persist it
137
137
  if (result.newSessionId && lockToken) {
138
138
  await updateSessionId(threadId, lockToken, result.newSessionId);
@@ -164,7 +164,7 @@ async function processMessage(received) {
164
164
  sessionLog.info('Message processed and deleted');
165
165
  }
166
166
  catch (error) {
167
- stopTyping?.();
167
+ typing?.stop();
168
168
  sessionLog.error(`Error processing message: ${error instanceof Error ? error.message : error}`);
169
169
  if (lockToken) {
170
170
  try {
@@ -7,5 +7,11 @@ export declare function sendSlackMessage(msg: TeamVibeQueueMessage, text: string
7
7
  export declare function sendSlackError(msg: TeamVibeQueueMessage, error: string): Promise<void>;
8
8
  export declare function addReaction(msg: TeamVibeQueueMessage, emoji: string): Promise<void>;
9
9
  export declare function setThreadStatus(msg: TeamVibeQueueMessage, status: string): Promise<void>;
10
- export declare function startTypingIndicator(msg: TeamVibeQueueMessage, statusText?: string): () => void;
10
+ export interface TypingIndicator {
11
+ /** Update the displayed status text */
12
+ updateStatus: (newStatus: string) => void;
13
+ /** Stop the typing indicator and clear the status */
14
+ stop: () => void;
15
+ }
16
+ export declare function startTypingIndicator(msg: TeamVibeQueueMessage, statusText?: string): TypingIndicator;
11
17
  export declare function removeReaction(msg: TeamVibeQueueMessage, emoji: string): Promise<void>;
@@ -103,13 +103,14 @@ export async function setThreadStatus(msg, status) {
103
103
  }
104
104
  }
105
105
  export function startTypingIndicator(msg, statusText = 'is thinking...') {
106
+ let currentStatus = statusText;
106
107
  // Set initial status
107
- setThreadStatus(msg, statusText);
108
+ setThreadStatus(msg, currentStatus);
108
109
  // Keepalive every 3 seconds (Slack clears after ~2 min timeout)
109
110
  let failures = 0;
110
111
  const interval = setInterval(async () => {
111
112
  try {
112
- await setThreadStatus(msg, statusText);
113
+ await setThreadStatus(msg, currentStatus);
113
114
  failures = 0;
114
115
  }
115
116
  catch {
@@ -119,10 +120,17 @@ export function startTypingIndicator(msg, statusText = 'is thinking...') {
119
120
  }
120
121
  }
121
122
  }, 3000);
122
- // Return cleanup function
123
- return () => {
124
- clearInterval(interval);
125
- setThreadStatus(msg, '');
123
+ return {
124
+ updateStatus: (newStatus) => {
125
+ if (newStatus !== currentStatus) {
126
+ currentStatus = newStatus;
127
+ setThreadStatus(msg, currentStatus);
128
+ }
129
+ },
130
+ stop: () => {
131
+ clearInterval(interval);
132
+ setThreadStatus(msg, '');
133
+ },
126
134
  };
127
135
  }
128
136
  export async function removeReaction(msg, emoji) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {