diff --git a/docs/superpowers/plans/2026-07-15-auto-reply.md b/docs/superpowers/plans/2026-07-15-auto-reply.md new file mode 100644 index 000000000..77da92baa --- /dev/null +++ b/docs/superpowers/plans/2026-07-15-auto-reply.md @@ -0,0 +1,174 @@ +# Auto Reply Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a frontend-only Auto Reply engine that recovers unattended agent tasks from 429/503-style interruptions by auto-sending a configured reply (default `继续`) after a visible countdown, with per-conversation enable, settings-managed rules, and loop protection. + +**Architecture:** Pure match/storage helpers under `src/lib/auto-reply/` feed a conversation-scoped `useAutoReplyEngine` hook. The engine watches error/status signals, schedules a countdown banner above the composer, and fires through the normal `onSend(PromptDraft)` path. Rules live in app-level localStorage; enable flags are per conversation/draft key. + +**Tech Stack:** React, TypeScript, next-intl, Vitest, localStorage, existing shadcn UI (`DropdownMenuCheckboxItem`, `Switch`, `Button`, `Input`, `Select`). + +## Global Constraints + +- Frontend-only; no backend/Rust protocol changes. +- Match **error/status signals only** (not assistant body text). +- Enable scope is **per conversation only** (composer `+` menu). +- Rule CRUD lives on a **Settings** page. +- Built-ins: HTTP 429 and HTTP 503 -> reply `继续`; `delayMs=3000`, `cooldownMs=15000`, `maxPerBurst=3`. +- Built-ins editable, **not deletable**. +- Pre-send banner above input with live countdown + cancel. +- Manual user send cancels pending auto-reply. +- Safe send only when connected-safe and no permission/question dialogs; never while `prompting`. +- Do not clobber composer draft; send `replyText` as a standalone draft. +- Prettier: no semicolons, trailing commas `es5`, 2-space indent. +- i18n key parity across all 10 locales (`en` is source of truth). +- `docs/superpowers/**` is gitignored — always `git add -f` for plan/spec commits. + +--- + +## File map + +| Path | Responsibility | +| --- | --- | +| `src/lib/auto-reply/types.ts` | Shared types + builtin seed factory | +| `src/lib/auto-reply/match.ts` | Pure matching, safety, burst helpers | +| `src/lib/auto-reply/match.test.ts` | Unit tests for match helpers | +| `src/lib/auto-reply/storage.ts` | localStorage load/save for settings + enable map | +| `src/lib/auto-reply/storage.test.ts` | Storage tests | +| `src/lib/auto-reply/settings-store.ts` | In-memory settings cache + subscribers | +| `src/hooks/use-auto-reply-engine.ts` | Countdown lifecycle hook | +| `src/hooks/use-auto-reply-engine.test.ts` | Fake-timer lifecycle tests | +| `src/components/chat/auto-reply-banner.tsx` | Countdown + stop-notice UI | +| `src/components/settings/auto-reply-settings.tsx` | Rules CRUD UI | +| `src/app/settings/auto-reply/page.tsx` | Settings route | +| `src/components/settings/settings-shell.tsx` | Nav entry | +| `src/components/chat/conversation-shell.tsx` | Host engine + banner | +| `src/components/chat/message-input.tsx` | `+` menu toggle | +| `src/components/chat/chat-input.tsx` | Pass enable props | +| `src/i18n/messages/*.json` | Strings (10 locales) | + +Storage keys: +- `codeg:auto-reply:settings:v1` +- `codeg:auto-reply:enabled:v1` + +Enable key: prefer `draftStorageKey` when present, else a stable conversation/virtual id. + +--- + +### Task 1: Pure types + match helpers + +**Files:** +- Create: `src/lib/auto-reply/types.ts` +- Create: `src/lib/auto-reply/match.ts` +- Test: `src/lib/auto-reply/match.test.ts` + +**Interfaces:** +- Produces: `AutoReplyRule`, `AutoReplySettings`, `AutoReplyMatchKind`, `AutoReplySignal`, `AutoReplySafetyInput`, `createBuiltinRules()`, `normalizeErrorText()`, `buildBurstKey()`, `findMatchingRule()`, `canScheduleAutoReply()`, `isSafeToAutoReply()`, `signalFromSources()`, `buildAutoReplyDraft()` + +- [ ] **Step 1: Write failing tests** in `match.test.ts` covering builtins, http_status, error_text first-match, burst key, cooldown/maxPerBurst, safety gates, signal source preference. +- [ ] **Step 2: Run** `pnpm exec vitest run src/lib/auto-reply/match.test.ts` (expect FAIL). +- [ ] **Step 3: Implement** types + match helpers as specified in design. +- [ ] **Step 4: Re-run tests** (expect PASS). +- [ ] **Step 5: Commit** `feat(auto-reply): add match helpers and builtin 429/503 rules` + +Builtin rule ids: `builtin-http-429`, `builtin-http-503`. +`isSafeToAutoReply`: status must be `"connected"` and no pending permission/question/ask-question. +`error_text` match is case-sensitive substring. +Burst key: `` `${httpStatus ?? "none"}|${normalize(errorText)}` ``. + +--- + +### Task 2: Storage + settings store + +**Files:** +- Create: `src/lib/auto-reply/storage.ts` +- Create: `src/lib/auto-reply/settings-store.ts` +- Test: `src/lib/auto-reply/storage.test.ts` + +- [ ] **Step 1: Tests** for defaults, corrupt JSON, round-trip, enable map, re-inject missing builtins. +- [ ] **Step 2: Implement** localStorage helpers + `useSyncExternalStore` settings store. +- [ ] **Step 3: Pass tests + commit** `feat(auto-reply): persist rules and per-conversation enable flags` + +--- + +### Task 3: Engine hook + +**Files:** +- Create: `src/hooks/use-auto-reply-engine.ts` +- Test: `src/hooks/use-auto-reply-engine.test.ts` + +API: + +```ts +useAutoReplyEngine({ + enabled, status, error, claudeApiRetry, + pendingPermission, pendingQuestion, pendingAskQuestion, + onSend, +}): { + pending, stopNotice, cancelPending, notifyManualSend, dismissStopNotice +} +``` + +- [ ] **Step 1: Fake-timer tests** for disable, schedule/fire, cancel, manual send, unsafe cancel, signal clear, cooldown, maxPerBurst, new burst. +- [ ] **Step 2: Implement hook** with refs for timers/burst counters; re-check safety at fire. +- [ ] **Step 3: Commit** `feat(auto-reply): add countdown engine with loop protection` + +--- + +### Task 4: i18n (all 10 locales) + +- SettingsShell.nav.auto_reply +- AutoReplySettings.* (settings page strings) +- Folder.chat.messageInput.autoReply (+ hint) +- Folder.chat.autoReply.* (banner/stop notice) + +- [ ] Add keys to en + zh-CN with real copy; other locales can mirror EN but must keep parity. +- [ ] Run `pnpm exec vitest run src/i18n/messages.test.ts` +- [ ] Commit `feat(auto-reply): add i18n strings for composer and settings` + +--- + +### Task 5: Banner + shell / menu wiring + +- Create `auto-reply-banner.tsx` +- Wire engine in `conversation-shell.tsx` (banner closest to input) +- Wrap composer send to call `notifyManualSend` +- Pass enable state through chat-input -> message-input +- `+` menu `DropdownMenuCheckboxItem` toggle; tint `+` when enabled + +- [ ] Implement + commit `feat(auto-reply): wire countdown banner and + menu toggle` + +--- + +### Task 6: Settings page + nav + +- `src/components/settings/auto-reply-settings.tsx` +- `src/app/settings/auto-reply/page.tsx` +- Nav item near Quick Messages; builtins not deletable; delay/cooldown in seconds UI + +- [ ] Implement + commit `feat(auto-reply): add settings page for rule management` + +--- + +### Task 7: Verify + bilingual PR + +```bash +pnpm exec vitest run src/lib/auto-reply src/hooks/use-auto-reply-engine.test.ts src/i18n/messages.test.ts +git add -f docs/superpowers/plans/2026-07-15-auto-reply.md +git push -u origin feat/auto-reply +gh pr create --base main --head feat/auto-reply --title "feat: auto-reply for recoverable agent interruptions (429/503)" --body "..." +``` + +PR body must include Chinese + English sections: Summary, Motivation, Behavior, Test plan. + +--- + +## Self-review + +1. Spec coverage: enable toggle, settings rules, 429/503 builtins, delay, banner, cancel, manual-send cancel, cooldown/maxPerBurst, normal onSend, bilingual PR — covered. +2. No intentional placeholders. +3. Shared types/names consistent across tasks. + +## Execution note + +User has repeatedly said 继续 — execute inline on `feat/auto-reply` after committing this plan. Do not restore the repeat-intent stash onto this branch. diff --git a/docs/superpowers/specs/2026-07-15-auto-reply-design.md b/docs/superpowers/specs/2026-07-15-auto-reply-design.md new file mode 100644 index 000000000..c230beda6 --- /dev/null +++ b/docs/superpowers/specs/2026-07-15-auto-reply-design.md @@ -0,0 +1,300 @@ +# Auto Reply Design + +**Date:** 2026-07-15 +**Status:** Approved for implementation planning +**Scope:** Chat composer + conversation shell + settings (frontend only) +**Approach:** Frontend-only rule engine watching error/status signals; per-conversation enable; normal user-send path + +## Problem + +Unattended agent tasks often stop on recoverable interruptions such as: + +- `429 Too Many Requests` +- `503 Service Unavailable` +- Claude API retry / connection error surfaces that leave the session idle until a human types something like `继续` + +If those interruptions are auto-handled with a delayed, explicit user-visible reply, unattended success rate improves without hiding what the client is about to send. + +## Goals + +1. Support configurable auto-reply **rules** (match condition, reply text, delay). +2. Ship built-in rules for **HTTP 429** and **HTTP 503** that reply with **`继续`**. +3. Support per-rule **delay** before send. +4. Allow enabling auto-reply **manually from the composer `+` menu**, scoped **per conversation**. +5. Show a clear **pre-send banner above the input** before any automatic message is sent. +6. Prevent infinite loops via **cooldown + max fires per interruption burst**. +7. Reuse the normal composer send path (no special system message type). +8. Cover matching, lifecycle, loop protection, and UI smoke with tests. +9. Deliver a PR with bilingual (EN + ZH) description. + +## Non-goals + +- Backend/Rust-owned auto-send. +- Matching assistant chat body text. +- Global enable (or global default + per-conversation override). +- Cross-device rule sync via backend settings API. +- Auto-answering permission / free-text question / ask-question dialogs. +- Inventing a separate message role or silent resume protocol. + +## Decisions (from product clarification) + +| Topic | Decision | +| --- | --- | +| Enable scope | Per conversation only | +| Rule management | Settings page | +| Match source | Error / status signals only | +| Cancel during countdown | Banner cancel + user manual send | +| Loop protection | Cooldown + max per error burst | +| Architecture | Frontend-only engine (Approach A) | + +## Architecture + +``` +Settings (rules CRUD) + | + v +auto-reply settings store (rules + defaults, app-level) + | +MessageInput + menu --> per-conversation enabled flag + | +useAutoReplyEngine(error / claudeApiRetry / status) + | + |- match first enabled rule + |- schedule countdown (rule.delayMs) + |- banner above composer + +- on fire -> onSend(rule.replyText) // normal user-send path +``` + +### Key integration points + +| Area | File(s) | Role | +| --- | --- | --- | +| Retry / error state | `src/contexts/acp-connections-context.tsx` | Source of `ClaudeApiRetryState` (`errorStatus`, `error`) and connection `error` / `status` | +| Composer dock | `src/components/chat/conversation-shell.tsx` | Host countdown banner above input | +| `+` menu toggle | `src/components/chat/message-input.tsx` | Per-conversation enable | +| Send path | existing `onSend` / queue plumbing | Auto reply sends as a normal user draft | +| Settings shell | `src/components/settings/settings-shell.tsx` + new settings page | Rule CRUD | +| i18n | `src/i18n/messages/*.json` | EN/ZH (and locale key parity as required by repo) | + +## Data model + +```ts +type AutoReplyMatchKind = + | "http_status" // exact HTTP status from ClaudeApiRetryState.errorStatus + | "error_text" // substring match against retry.error / connection error + +interface AutoReplyRule { + id: string + name: string + enabled: boolean + matchKind: AutoReplyMatchKind + matchValue: string // "429" | "503" | "Too Many Requests" | ... + replyText: string // default "继续" + delayMs: number + cooldownMs: number + maxPerBurst: number + builtin?: boolean // shipped rules: editable, not deletable +} + +interface AutoReplySettings { + version: 1 + rules: AutoReplyRule[] +} + +// Per conversation: +// conversationId -> enabled: boolean +``` + +### Built-in seed rules + +| Name | Match | Reply | delayMs | cooldownMs | maxPerBurst | +| --- | --- | --- | --- | --- | --- | +| HTTP 429 | `http_status=429` | `继续` | 3000 | 15000 | 3 | +| HTTP 503 | `http_status=503` | `继续` | 3000 | 15000 | 3 | + +Users may edit delay/reply/cooldown/max and disable builtins, but cannot delete them. + +### Persistence + +- **Rules:** app-level `localStorage` under a versioned key, e.g. `codeg:auto-reply:settings:v1` +- **Enable flag:** per conversation id (and draft/storage key for unsaved drafts if a stable key already exists for that composer instance) +- No backend schema change in v1 + +## Trigger flow + +### When evaluation runs + +Re-evaluate when any of these change: + +- per-conversation Auto Reply enabled +- `claudeApiRetry` +- connection `error` +- connection `status` +- rules list from settings +- pending permission / question / ask-question state (safety gates) + +### Match algorithm + +1. Skip if Auto Reply is **off** for this conversation. +2. Skip if a countdown is already scheduled for this conversation. +3. Skip if connection is not safe to send: + - status is `prompting`, `connecting`, `disconnected`, or `error` in a non-recoverable way that cannot accept a prompt + - pending permission, free-text question, or ask-question dialog is open +4. Build signal snapshot: + - `httpStatus = claudeApiRetry?.errorStatus` + - `errorText = claudeApiRetry?.error ?? connection.error ?? ""` +5. Walk **enabled rules** in list order; first match wins. + - `http_status`: `Number(matchValue) === httpStatus` + - `error_text`: case-sensitive substring of `errorText` containing `matchValue` (document exact policy in code; keep simple) +6. Apply loop protection for that rule + conversation: + - if now < lastSuccessfulSendAt + `cooldownMs` -> skip + - if same interruption burst already reached `maxPerBurst` -> skip and surface a dismissible stop notice +7. Start countdown for `rule.delayMs`. + +### Interruption burst identity + +```ts +burstKey = `${httpStatus ?? "none"}|${normalize(errorText)}` +``` + +`normalize` trims and collapses internal whitespace. Same outage retries share a burst. When the signal clears and a later distinct signal appears, a new burst may fire again. + +### Countdown lifecycle + +``` +matched + -> pending { ruleId, replyText, fireAt, burstKey, matchedLabel } + -> banner visible with live remaining seconds + -> timer fires + -> if still enabled + still safe + signal still relevant + -> onSend(plain text draft of replyText) + -> record lastSentAt + increment burst count + -> else cancel without sending +``` + +### Cancel conditions + +Cancel pending auto-send (do not send) when: + +- user clicks **Cancel** on the banner +- user **manually sends** a message +- Auto Reply is toggled off +- engine unmounts / conversation context is replaced +- connection becomes unsafe (`prompting`, disconnected, pending dialogs) +- matched signal disappears before fire (avoid sending after recovery) + +### Send semantics + +- Auto reply is a **normal user prompt** through the existing `onSend` (or queue-when-busy) path. +- Do **not** invent a system/hidden message type. +- Do **not** clobber composer draft contents; send `replyText` directly, independent of the editor buffer. +- If the session is busy and the product already queues user sends, auto-reply should follow the same busy-send policy as a manual send of that text. Prefer not to invent a second queue policy. + +## UI / UX + +### Composer `+` menu + +Add a menu item in the existing add-actions dropdown: + +- Label: `自动回复` / `Auto Reply` +- Checked / on-off for **this conversation only** +- Click toggles enable; does not open settings +- Optional secondary entry: "Manage rules..." -> Settings -> Auto Reply (nice-to-have) + +When enabled, show a low-noise indicator on the `+` control (tint or small badge) so unattended mode is visible without opening the menu. + +### Pre-send countdown banner (required) + +Render above the input, same width as the composer dock: + +**zh-CN** + +``` +即将自动回复「继续」· 3s [取消] +匹配:HTTP 429 +``` + +**en** + +``` +Auto-replying "continue" in 3s [Cancel] +Matched: HTTP 429 +``` + +Notes: + +- Live countdown +- Quote the exact reply text +- Info/warning styling, distinct from the existing Claude API retry destructive strip +- If both exist, auto-reply banner stays closest to the input (action context) + +### Settings page + +New settings nav item (near Quick Messages): + +- Rule list (builtins first) +- Per rule: enable, name, match kind, match value, reply text, delay (seconds UI), cooldown, max per burst +- Builtin badge; builtins not deletable +- Custom rules: add / delete / reorder (first match wins) +- Help text: matches error/status signals only; per-conversation enable lives in composer `+` menu + +### States + +| State | UI | +| --- | --- | +| Off for conversation | No banner; menu unchecked | +| On, no match | No banner | +| On, countdown | Banner + cancel | +| Hit maxPerBurst | Dismissible notice: auto-reply stopped for this burst | + +## Safety defaults + +| Field | Builtin default | +| --- | --- | +| `delayMs` | 3000 | +| `cooldownMs` | 15000 | +| `maxPerBurst` | 3 | + +## Testing + +1. **Rule matching** + - 429 / 503 http status + - error_text substring + - disabled rules ignored + - first match wins +2. **Engine lifecycle** + - disabled conversation never schedules + - schedule uses `delayMs` + - cancel / manual send prevents send + - fire only when still safe +3. **Loop protection** + - cooldown blocks re-fire + - maxPerBurst stops burst + - new burst can fire again +4. **UI smoke** + - `+` toggle flips per-conversation state + - banner copy + cancel + - settings edit of builtin delay/reply + +## PR delivery + +- Branch from `origin/main` (do not stack on unrelated feature branches). +- Implementation plan under `docs/superpowers/plans/`. +- PR description includes **Chinese + English** sections: + - Summary / 摘要 + - Motivation / 动机 + - Behavior / 行为 + - Test plan / 测试计划 + +## Success criteria + +- With Auto Reply on, an unattended conversation recovering from 429/503 sends `继续` after the configured delay. +- User always sees an explicit pre-send warning and can cancel. +- Persistent rate limits do not produce an infinite auto-send loop. +- No backend protocol changes required for v1. + +## Open implementation notes + +- Prefer pure helpers (`matchAutoReplyRule`, `shouldScheduleAutoReply`, burst key) for unit tests without mounting the full composer. +- Prefer a small dedicated store/hook module under `src/lib/auto-reply/` or `src/hooks/use-auto-reply.ts` + `src/stores/` only if an existing local pattern fits better. +- Follow existing i18n key parity rules used by the repo for settings/composer strings. diff --git a/src/app/settings/auto-reply/page.tsx b/src/app/settings/auto-reply/page.tsx new file mode 100644 index 000000000..93c18f9c2 --- /dev/null +++ b/src/app/settings/auto-reply/page.tsx @@ -0,0 +1,5 @@ +import { AutoReplySettings } from "@/components/settings/auto-reply-settings" + +export default function SettingsAutoReplyPage() { + return +} diff --git a/src/components/chat/auto-reply-banner.tsx b/src/components/chat/auto-reply-banner.tsx new file mode 100644 index 000000000..088af5bb2 --- /dev/null +++ b/src/components/chat/auto-reply-banner.tsx @@ -0,0 +1,96 @@ +"use client" + +import { useTranslations } from "next-intl" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" +import type { + AutoReplyPendingState, + AutoReplyStopNotice, +} from "@/hooks/use-auto-reply-engine" + +interface AutoReplyBannerProps { + pending: AutoReplyPendingState | null + stopNotice: AutoReplyStopNotice | null + onCancel: () => void + onDismissStopNotice: () => void + className?: string +} + +export function AutoReplyBanner({ + pending, + stopNotice, + onCancel, + onDismissStopNotice, + className, +}: AutoReplyBannerProps) { + const t = useTranslations("Folder.chat.autoReply") + + if (pending) { + const seconds = Math.max(1, Math.ceil(pending.remainingMs / 1000)) + return ( +
+
+
+
+ {t("bannerTitle", { + reply: pending.replyText, + seconds, + })} +
+
+ {t("bannerMatched", { label: pending.matchedLabel })} +
+
+ +
+
+ ) + } + + if (stopNotice) { + return ( +
+
+
+
{t("stopNotice")}
+
+ {t("bannerMatched", { label: stopNotice.matchedLabel })} +
+
+ +
+
+ ) + } + + return null +} diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx index cfcbdacad..2e82acc18 100644 --- a/src/components/chat/chat-input.tsx +++ b/src/components/chat/chat-input.tsx @@ -58,6 +58,8 @@ interface ChatInputProps { onForkSend?: (draft: PromptDraft, modeId?: string | null) => void onAddFeedback?: () => void feedbackAddDisabled?: boolean + autoReplyEnabled?: boolean + onAutoReplyEnabledChange?: (enabled: boolean) => void /** * Keep the composer usable even while disconnected. Set for a folderless chat * draft: it has no working dir yet (so it never auto-connects), and the FIRST @@ -113,6 +115,8 @@ export const ChatInput = memo(function ChatInput({ onForkSend, onAddFeedback, feedbackAddDisabled, + autoReplyEnabled = false, + onAutoReplyEnabledChange, allowOfflineCompose = false, injectContent, onInjectConsumed, @@ -177,6 +181,8 @@ export const ChatInput = memo(function ChatInput({ onForkSend={onForkSend} onAddFeedback={onAddFeedback} feedbackAddDisabled={feedbackAddDisabled} + autoReplyEnabled={autoReplyEnabled} + onAutoReplyEnabledChange={onAutoReplyEnabledChange} injectContent={injectContent} onInjectConsumed={onInjectConsumed} placeholder={ diff --git a/src/components/chat/conversation-shell.tsx b/src/components/chat/conversation-shell.tsx index c976af0bf..33fe08c02 100644 --- a/src/components/chat/conversation-shell.tsx +++ b/src/components/chat/conversation-shell.tsx @@ -1,4 +1,10 @@ -import { useMemo, type ReactNode } from "react" +import { + useCallback, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react" import { useTranslations } from "next-intl" import type { AgentType, @@ -23,6 +29,12 @@ import { ChatInput } from "@/components/chat/chat-input" import { PermissionDialog } from "@/components/chat/permission-dialog" import { QuestionDialog } from "@/components/chat/question-dialog" import { AskQuestionCard } from "@/components/chat/ask-question-card" +import { AutoReplyBanner } from "@/components/chat/auto-reply-banner" +import { useAutoReplyEngine } from "@/hooks/use-auto-reply-engine" +import { + isAutoReplyEnabled, + setAutoReplyEnabled, +} from "@/lib/auto-reply/storage" interface ConversationShellProps { status: ConnectionStatus | null @@ -194,6 +206,62 @@ export function ConversationShell({ }) }, [claudeApiRetry, tAcp]) + const enableKey = draftStorageKey ?? "unknown" + const [autoReplyEnabled, setAutoReplyEnabledState] = useState(false) + + useEffect(() => { + setAutoReplyEnabledState(isAutoReplyEnabled(enableKey)) + }, [enableKey]) + + const handleAutoReplyEnabledChange = useCallback( + (enabled: boolean) => { + setAutoReplyEnabled(enableKey, enabled) + setAutoReplyEnabledState(enabled) + }, + [enableKey] + ) + + const handleAutoSend = useCallback( + (draft: PromptDraft) => { + onSend(draft) + }, + [onSend] + ) + + const { + pending: autoReplyPending, + stopNotice: autoReplyStopNotice, + cancelPending: cancelAutoReplyPending, + notifyManualSend: notifyAutoReplyManualSend, + dismissStopNotice: dismissAutoReplyStopNotice, + } = useAutoReplyEngine({ + enabled: autoReplyEnabled, + status, + error, + claudeApiRetry, + pendingPermission: pendingPermission != null, + pendingQuestion: pendingQuestion != null, + pendingAskQuestion: + pendingAskQuestion != null && pendingAskQuestion.questions.length > 0, + onSend: handleAutoSend, + }) + + const handleComposerSend = useCallback( + (draft: PromptDraft, modeId?: string | null) => { + notifyAutoReplyManualSend() + onSend(draft, modeId) + }, + [notifyAutoReplyManualSend, onSend] + ) + + const handleComposerEnqueue = useCallback( + (draft: PromptDraft, modeId: string | null) => { + notifyAutoReplyManualSend() + onEnqueue?.(draft, modeId) + }, + [notifyAutoReplyManualSend, onEnqueue] + ) + return (
{topBanner} @@ -226,14 +294,24 @@ export function ConversationShell({ {!hideInput && (
+
+ +