@teamvibe/poller 0.1.30 → 0.1.32

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,44 @@ 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__upload_snippet', status: 'Uploading snippet...' },
17
+ { pattern: /^mcp__slack__/, status: 'Working with Slack...' },
18
+ // Web tools
19
+ { pattern: 'WebSearch', status: 'Searching the web...' },
20
+ { pattern: 'WebFetch', status: 'Reading a webpage...' },
21
+ // File/code tools
22
+ { pattern: 'Read', status: 'Reading files...' },
23
+ { pattern: 'Grep', status: 'Searching code...' },
24
+ { pattern: 'Glob', status: 'Finding files...' },
25
+ { pattern: 'Edit', status: 'Editing code...' },
26
+ { pattern: 'Write', status: 'Writing code...' },
27
+ { pattern: 'Bash', status: 'Running a command...' },
28
+ { pattern: 'Agent', status: 'Delegating to a sub-agent...' },
29
+ // Generic MCP fallback
30
+ { pattern: /^mcp__/, status: 'Using an integration...' },
31
+ ];
32
+ function getStatusForTool(toolName) {
33
+ for (const { pattern, status } of TOOL_STATUS_MAP) {
34
+ if (typeof pattern === 'string') {
35
+ if (toolName === pattern)
36
+ return status;
37
+ }
38
+ else {
39
+ if (pattern.test(toolName))
40
+ return status;
41
+ }
42
+ }
43
+ return null;
44
+ }
7
45
  // Per-token Slack client cache for thread context
8
46
  const slackClients = new Map();
9
47
  function getSlackClient(botToken) {
@@ -126,7 +164,7 @@ function formatStreamEvent(event) {
126
164
  return null;
127
165
  }
128
166
  }
129
- async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
167
+ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
130
168
  const slackContext = msg.response_context.slack;
131
169
  const isCronMessage = msg.source === 'cron';
132
170
  if (!slackContext && !isCronMessage) {
@@ -231,6 +269,20 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
231
269
  event.name === 'mcp__slack__send_message') {
232
270
  onMessageSent();
233
271
  }
272
+ // Auto-update typing indicator status based on tool usage
273
+ if (typingIndicator) {
274
+ if (event.type === 'tool_use' && event.name) {
275
+ const status = getStatusForTool(event.name);
276
+ if (status !== null && status !== '') {
277
+ typingIndicator.updateStatus(status);
278
+ }
279
+ // When status is '' (e.g. set_status tool), don't override — Claude is managing it
280
+ }
281
+ else if (event.type === 'assistant') {
282
+ // Reset to "Thinking..." when Claude is reasoning between tool calls
283
+ typingIndicator.updateStatus('Thinking...');
284
+ }
285
+ }
234
286
  }
235
287
  catch {
236
288
  const truncated = line.length > 500 ? line.slice(0, 500) + '...' : line;
@@ -279,20 +331,20 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
279
331
  });
280
332
  });
281
333
  }
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);
334
+ export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent, typingIndicator) {
335
+ const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent, typingIndicator);
284
336
  // If --resume failed, retry as a fresh session (session files may have been lost on container restart)
285
337
  let retryResult = result;
286
338
  if (!result.success && sessionId && !isFirstMessage) {
287
339
  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);
340
+ retryResult = await runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent, typingIndicator);
289
341
  }
290
342
  // If session ID is "already in use" (stale lock file), retry with a fresh session ID
291
343
  if (!retryResult.success && sessionId && retryResult.error?.includes('already in use')) {
292
344
  sessionLog.info('Session ID already in use (stale lock), retrying with fresh session ID');
293
345
  const { randomUUID } = await import('crypto');
294
346
  const freshId = randomUUID();
295
- const freshResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent);
347
+ const freshResult = await runClaudeCode(msg, sessionLog, cwd, freshId, true, lastMessageTs, onMessageSent, typingIndicator);
296
348
  return { ...freshResult, newSessionId: freshId };
297
349
  }
298
350
  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.32",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {