From bdb7bb91e8bdde9c36c4e2f3dc317be5731a1370 Mon Sep 17 00:00:00 2001 From: Flood Sung Date: Thu, 14 May 2026 01:16:18 +0000 Subject: [PATCH] feat(ui): minimal Done/Failed push, skip for agent-continuation turns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The completion notice was repeating everything that already sits in the card's grey footer: ✅ Done (52s · $340.41 · opus-4-7 · 377.3k/1000k (38%)) The whole point of the notice is the *mobile push*: card edits don't ring on Feishu mobile, but text messages do. Once the user opens the chat the card itself shows duration / cost / model / context usage in its footer — the push banner just needs to say "the thing you asked about finished". So the message body is now exactly: ✅ Done (or ❌ Failed) Also stop sending the notice for continuation turns (`handleContinuationTurn`). Those are between-turn agent activity the user didn't explicitly initiate — a background-task summary, a teammate's reply, a `/goal` evaluator emitting. Pushing "✅ Done" for those is meaningless: the user never asked for the work and there's no spinning task they're waiting on. The blue-→-green card itself remains as the visual signal in-chat; we just don't broadcast it. The 10-second threshold and the `skipCompletionNotice` opt-out for WeChat (which already routes the final response as its own message) still hold. Quick tasks (<10s) stay silent. Side cleanup: `startTime` in `handleContinuationTurn` was only used to compute `durationMs` for the notice — both go away. 291 / 291 vitest pass. Build + lint clean (2 pre-existing warnings). Co-Authored-By: Claude Opus 4.7 --- src/bridge/message-bridge.ts | 65 +++++++++++++++++------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/src/bridge/message-bridge.ts b/src/bridge/message-bridge.ts index f165f204..1c952c4d 100644 --- a/src/bridge/message-bridge.ts +++ b/src/bridge/message-bridge.ts @@ -674,7 +674,6 @@ export class MessageBridge { this.logger.info({ chatId, messageId }, 'MessageBridge: continuation card opened'); let lastState: CardState = initialState; - const startTime = Date.now(); const outputsDir = this.outputsManager.prepareDir(chatId); try { @@ -703,10 +702,14 @@ export class MessageBridge { } await this.sendFinalCard(messageId, lastState, chatId); - const durationMs = Date.now() - startTime; - await this.sendCompletionNotice(chatId, lastState, durationMs); - // Output files for continuation turns too — agent may have produced - // artifacts in the bash background task whose summary triggered this. + // Intentionally NO sendCompletionNotice here. Continuation turns are + // between-turn agent activity the user didn't initiate — the card + // itself (blue → green lifecycle, complete with timestamps in the + // footer) is enough signal. A separate "✅ Done" push for every + // background-task return would be noise; the user only opted into + // pushes for the work they explicitly asked for. + // Output files still get sent — agent may have produced artifacts + // in the bash background task whose summary triggered this. await this.outputHandler.sendOutputFiles(chatId, outputsDir, processor, lastState); } catch (err: any) { this.logger.error({ err, chatId }, 'MessageBridge: continuation stream errored'); @@ -2339,41 +2342,33 @@ export class MessageBridge { } } + /** + * Send a separate text message after the card update so the user gets a + * mobile push notification when a long task finishes. Card edits don't + * trigger Feishu push, but text messages do. + * + * Body is intentionally a single emoji + word (`✅ Done` / `❌ Failed`) + * — duration, cost, model, and context-usage all live in the card's grey + * footer already, so repeating them here just made the push banner + * harder to read at a glance. Don't re-add stats without first + * confirming the card footer is unreachable on the surface this push + * lands on. + * + * Skipped for: + * - durations under 10s (the user is almost certainly still looking + * at the screen and doesn't need a push for those) + * - senders that route the final response as its own message + * already (WeChat — `skipCompletionNotice`) + * - continuation turns (between-turn agent activity); see the + * handleContinuationTurn call site for the rationale. + */ private async sendCompletionNotice(chatId: string, state: CardState, durationMs: number): Promise { - // Some senders (WeChat) already send the final response as a standalone message, so skip if (this.sender.skipCompletionNotice) return; - // Only notify for tasks that took a while — quick tasks don't need it if (durationMs < 10_000) return; const statusEmoji = state.status === 'complete' ? '✅' : '❌'; - const durationStr = durationMs >= 60_000 - ? `${(durationMs / 60_000).toFixed(1)}min` - : `${(durationMs / 1000).toFixed(0)}s`; - const costStr = state.sessionCostUsd ? ` · $${state.sessionCostUsd.toFixed(2)}` : (state.costUsd ? ` · $${state.costUsd.toFixed(2)}` : ''); - const statusWord = state.status === 'complete' ? 'Done' : 'Failed'; - - // Model display name: strip "claude-" prefix for brevity (e.g. "opus-4-7") - const modelStr = state.model - ? ` · ${state.model.replace(/^claude-/, '')}` - : ''; - - // Context usage: show totalTokens / contextWindow as percentage - let usageStr = ''; - if (state.totalTokens && state.contextWindow) { - const pct = Math.round((state.totalTokens / state.contextWindow) * 100); - const tokensK = state.totalTokens >= 1000 - ? `${(state.totalTokens / 1000).toFixed(1)}k` - : `${state.totalTokens}`; - const ctxK = `${Math.round(state.contextWindow / 1000)}k`; - usageStr = ` · ${tokensK}/${ctxK} (${pct}%)`; - } else if (state.totalTokens) { - const tokensK = state.totalTokens >= 1000 - ? `${(state.totalTokens / 1000).toFixed(1)}k` - : `${state.totalTokens}`; - usageStr = ` · ${tokensK} tokens`; - } - - const message = `${statusEmoji} ${statusWord} (${durationStr}${costStr}${modelStr}${usageStr})`; + const statusWord = state.status === 'complete' ? 'Done' : 'Failed'; + const message = `${statusEmoji} ${statusWord}`; try { await this.sender.sendText(chatId, message);