From 1deaf524c7da109ac6354ce0b01a6412d939e86b Mon Sep 17 00:00:00 2001 From: Adam Dalloul <47503782+Adam-Dalloul@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:37:17 -0700 Subject: [PATCH] feat(chat): offer the mid-turn send on every session with a delivery channel On a session whose live-feedback channel is the check_user_feedback pull tool (codex, grok, gemini, opencode -- every agent without proven native steering), the composer offered no mid-turn action at all: Stop, and Enter queueing for the next turn. The split button that sends the draft into the running turn was gated on the native channel, so the only way to reach the pull channel mid-turn was retyping the draft into the "+" menu dialog. The delivery machinery underneath is channel-aware already: submit_feedback records a pending note for pull sessions, gated on turn_in_flight, and the agent reads it on its next check_user_feedback call. So the gate moves from "channel is native" to "a working delivery channel exists" (useSessionFeedback's new steerAvailable), and the copy stays honest per channel: a native session keeps "Insert into current turn", a pull session says "Send note for next check" (new steerChannel prop threaded to the composer), and the note renders in the existing strip as waiting until the agent actually reads it -- nothing fakes delivered, and the pull failure toast talks about the note, not an insert. Sessions with neither channel (feature off at spawn, no MCP wire) keep the historical Stop-only prompting form, and delegation is untouched. --- src/components/chat/chat-input.tsx | 19 +++-- src/components/chat/conversation-shell.tsx | 21 +++--- src/components/chat/message-input.test.tsx | 28 ++++++++ src/components/chat/message-input.tsx | 72 ++++++++++++------- .../conversation-detail-panel.tsx | 23 +++--- src/hooks/use-session-feedback.test.ts | 21 ++++++ src/hooks/use-session-feedback.ts | 19 +++-- src/i18n/messages/ar.json | 2 + src/i18n/messages/de.json | 2 + src/i18n/messages/en.json | 2 + src/i18n/messages/es.json | 2 + src/i18n/messages/fr.json | 2 + src/i18n/messages/ja.json | 2 + src/i18n/messages/ko.json | 2 + src/i18n/messages/pt.json | 2 + src/i18n/messages/zh-CN.json | 2 + src/i18n/messages/zh-TW.json | 2 + 17 files changed, 167 insertions(+), 56 deletions(-) diff --git a/src/components/chat/chat-input.tsx b/src/components/chat/chat-input.tsx index dbbb2545e..a8ce461e3 100644 --- a/src/components/chat/chat-input.tsx +++ b/src/components/chat/chat-input.tsx @@ -58,17 +58,20 @@ interface ChatInputProps { isEditingQueueItem?: boolean onSaveQueueEdit?: (draft: PromptDraft) => void onCancelQueueEdit?: () => void - /** Inject the draft into the RUNNING turn over the native steering channel. - * Present only when the session's live-feedback channel is native - * (`useSessionFeedback().channel === "native"`); resolves once recorded, - * rejects on any failure (incl. the turn-end race) so MessageInput can run - * its own enqueue fallback / draft preservation. `blocks` carries the full - * draft when it holds more than plain text (image attachments, file - * badges); `text` stays the recorded/display form. Must stay in sync with + /** Send the draft into the RUNNING turn over the session's live-feedback + * channel. Present only when the session has a working delivery channel + * (`useSessionFeedback().steerAvailable`); resolves once recorded, rejects + * on any failure (incl. the turn-end race) so MessageInput can run its own + * enqueue fallback / draft preservation. `blocks` carries the full draft + * when it holds more than plain text (image attachments, file badges); + * `text` stays the recorded/display form. Must stay in sync with * `MessageInputProps.onSteer` — the optional second parameter makes a * stale one-arg declaration here assignable, so tsc would NOT catch a * wrapper that silently drops the blocks. */ onSteer?: (text: string, blocks?: PromptInputBlock[]) => Promise + /** Which channel `onSteer` rides (`useSessionFeedback().channel`); picks + * the composer's honest copy. See `MessageInput`. */ + steerChannel?: "native" | "pull" onAddFeedback?: () => void feedbackAddDisabled?: boolean /** @@ -125,6 +128,7 @@ export const ChatInput = memo(function ChatInput({ onSaveQueueEdit, onCancelQueueEdit, onSteer, + steerChannel, onAddFeedback, feedbackAddDisabled, allowOfflineCompose = false, @@ -220,6 +224,7 @@ export const ChatInput = memo(function ChatInput({ onSaveQueueEdit={onSaveQueueEdit} onCancelQueueEdit={onCancelQueueEdit} onSteer={onSteer} + steerChannel={steerChannel} onAddFeedback={onAddFeedback} feedbackAddDisabled={feedbackAddDisabled} injectContent={injectContent} diff --git a/src/components/chat/conversation-shell.tsx b/src/components/chat/conversation-shell.tsx index 7e4eb5343..2fb3a5286 100644 --- a/src/components/chat/conversation-shell.tsx +++ b/src/components/chat/conversation-shell.tsx @@ -124,15 +124,18 @@ interface ConversationShellProps { isEditingQueueItem?: boolean onSaveQueueEdit?: (draft: PromptDraft) => void onCancelQueueEdit?: () => void - /** Inject the draft into the RUNNING turn (native live-feedback steering). - * Present only for sessions on the native channel; threaded straight - * through to the composer. `blocks` carries the full draft when it holds - * more than plain text (image attachments, file badges); `text` stays the - * recorded/display form. Must stay in sync with `MessageInputProps.onSteer` - * — the optional second parameter makes a stale one-arg declaration here - * assignable, so tsc would NOT catch a wrapper that silently drops the - * blocks. */ + /** Send the draft into the RUNNING turn over the session's live-feedback + * channel. Present only for sessions with a working delivery channel; + * threaded straight through to the composer. `blocks` carries the full + * draft when it holds more than plain text (image attachments, file + * badges); `text` stays the recorded/display form. Must stay in sync with + * `MessageInputProps.onSteer` — the optional second parameter makes a + * stale one-arg declaration here assignable, so tsc would NOT catch a + * wrapper that silently drops the blocks. */ onSteer?: (text: string, blocks?: PromptInputBlock[]) => Promise + /** Which channel `onSteer` rides (picks the composer's honest copy); + * threaded straight through. See `MessageInput`. */ + steerChannel?: "native" | "pull" /** Optional banner pinned to the top of the panel, above the message area * (e.g. the "restart to apply" config-stale banner). Renders nothing when * omitted. */ @@ -200,6 +203,7 @@ export function ConversationShell({ onSaveQueueEdit, onCancelQueueEdit, onSteer, + steerChannel, topBanner, injectContent, onInjectConsumed, @@ -370,6 +374,7 @@ export function ConversationShell({ onSaveQueueEdit={onSaveQueueEdit} onCancelQueueEdit={onCancelQueueEdit} onSteer={onSteer} + steerChannel={steerChannel} onAddFeedback={onAddFeedback} feedbackAddDisabled={feedbackAddDisabled} injectContent={injectContent} diff --git a/src/components/chat/message-input.test.tsx b/src/components/chat/message-input.test.tsx index f6512c0f6..e635b34fb 100644 --- a/src/components/chat/message-input.test.tsx +++ b/src/components/chat/message-input.test.tsx @@ -1281,4 +1281,32 @@ describe("MessageInput native steering (insert into current turn)", () => { expect(serializeDocToText(editor.state.doc)).not.toContain("late note") ) }) + + it("labels the mid-turn action honestly on the pull channel", async () => { + // A pull-tool session gets the same split, but its action must never + // promise an instant insert: the note is recorded as waiting and read on + // the agent's next check, so the copy says exactly that. + const user = userEvent.setup() + const onSteer = vi.fn().mockResolvedValue(undefined) + const editor = await mountPrompting({ onSteer, steerChannel: "pull" }) + typeDraft(editor, "check the tests") + await waitFor(() => + expect(screen.getByLabelText(MI.steerAsNote)).toBeInTheDocument() + ) + expect(screen.queryByLabelText(MI.steerIntoTurn)).toBeNull() + + // The action itself rides the same steer path — only the copy differs. + await user.click(screen.getByLabelText(MI.steerAsNote)) + await user.click( + await screen.findByRole("menuitem", { name: MI.steerAsNote }) + ) + await waitFor(() => + expect(onSteer).toHaveBeenCalledWith("check the tests", undefined) + ) + await waitFor(() => + expect(serializeDocToText(editor.state.doc)).not.toContain( + "check the tests" + ) + ) + }) }) diff --git a/src/components/chat/message-input.tsx b/src/components/chat/message-input.tsx index de21cfa85..aabc19d63 100644 --- a/src/components/chat/message-input.tsx +++ b/src/components/chat/message-input.tsx @@ -209,15 +209,22 @@ interface MessageInputProps { isEditingQueueItem?: boolean onSaveQueueEdit?: (draft: PromptDraft) => void onCancelQueueEdit?: () => void - /** Inject the draft into the RUNNING turn (native live-feedback steering). - * Present only on sessions whose feedback channel is native — when absent, - * the prompting branch renders its historical Stop-only form. `text` is - * the recorded/display form; `blocks` carries the full draft whenever it - * holds more than plain text (image attachments, file badges), encoded - * exactly like a normal send. Awaited: resolve = injected + recorded - * (clear the draft); reject = failure, where a turn-end `NoActiveTurn` - * race falls back to the queue and anything else keeps the draft. */ + /** Send the draft into the RUNNING turn over the session's live-feedback + * channel (see {@link steerChannel}). Present only on sessions with a + * working delivery channel — when absent, the prompting branch renders its + * historical Stop-only form. `text` is the recorded/display form; `blocks` + * carries the full draft whenever it holds more than plain text (image + * attachments, file badges), encoded exactly like a normal send. Awaited: + * resolve = recorded (clear the draft); reject = failure, where a turn-end + * `NoActiveTurn` race falls back to the queue and anything else keeps the + * draft. */ onSteer?: (text: string, blocks?: PromptInputBlock[]) => Promise + /** Which channel {@link onSteer} rides (`useSessionFeedback().channel`). + * Picks the honest copy for the mid-turn action: `native` = inserted into + * the turn immediately, `pull` = recorded as a note the agent reads on its + * next `check_user_feedback` call. Defaults to `native` (the only channel + * that offered the action before this prop existed). */ + steerChannel?: "native" | "pull" /** Open the live-feedback dialog (from the "+" menu). When omitted the entry * is hidden (feature off). */ onAddFeedback?: () => void @@ -323,6 +330,7 @@ export function MessageInput({ onSaveQueueEdit, onCancelQueueEdit, onSteer, + steerChannel = "native", onAddFeedback, feedbackAddDisabled, injectContent, @@ -1255,16 +1263,19 @@ export function MessageInput({ resetComposer, ]) - // Mid-turn "insert into current turn" (native steering). Awaited, unlike - // the synchronous send/enqueue paths: the draft clears ONLY once the - // backend confirms the injection was recorded — a turn-end race falls back - // to the queue (the note is never lost), any other failure keeps the draft - // for retry. A draft that holds more than plain text (image attachments, - // file badges) steers as its full block list — the same encoding a normal - // send uses, which the native wire carries verbatim — with the display text - // as the recorded note; nothing is silently stripped. Unsettled uploads are - // gated here exactly like `handleSend` (no server-side uri to hydrate from - // yet), since the enqueue fallback below bypasses its gate. + // Mid-turn send over the session's live-feedback channel: a native push + // inserts into the running turn; a pull-tool session records a waiting note + // the agent reads on its next check (the copy is keyed on `steerChannel` so + // neither overpromises). Awaited, unlike the synchronous send/enqueue + // paths: the draft clears ONLY once the backend confirms the note was + // recorded — a turn-end race falls back to the queue (the note is never + // lost), any other failure keeps the draft for retry. A draft that holds + // more than plain text (image attachments, file badges) steers as its full + // block list — the same encoding a normal send uses, carried verbatim on + // either channel — with the display text as the recorded note; nothing is + // silently stripped. Unsettled uploads are gated here exactly like + // `handleSend` (no server-side uri to hydrate from yet), since the enqueue + // fallback below bypasses its gate. const [steering, setSteering] = useState(false) const handleSteerClick = useCallback(async () => { if (!onSteer || steering) return @@ -1299,7 +1310,10 @@ export function MessageInput({ // The turn ended in the race window — reroute through the queue. enqueueInstead() } else { - toast.error(t("steerFailed"), { description: toErrorMessage(err) }) + toast.error( + t(steerChannel === "pull" ? "steerNoteFailed" : "steerFailed"), + { description: toErrorMessage(err) } + ) } } finally { setSteering(false) @@ -1314,6 +1328,7 @@ export function MessageInput({ showModeSelector, effectiveModeId, resetComposer, + steerChannel, t, ]) @@ -1629,11 +1644,14 @@ export function MessageInput({ ) : isPrompting && onCancel ? ( onSteer && onEnqueue && hasSendableContent ? ( - // Native-steering sessions surface the mid-turn actions that already - // exist but were keyboard-only/invisible: the primary half of the split - // queues the draft (what Enter has always done here), the dropdown - // injects it into the RUNNING turn. Without `onSteer` this branch stays - // pixel-identical to the historical Stop-only form below. + // Sessions with a working live-feedback channel surface the mid-turn + // actions that already exist but were keyboard-only/invisible: the + // primary half of the split queues the draft (what Enter has always + // done here), the dropdown sends it over the channel — a native push + // inserts into the RUNNING turn, a pull-tool session records a waiting + // note for the agent's next check (label keyed on `steerChannel`). + // Without `onSteer` this branch stays pixel-identical to the + // historical Stop-only form below.
@@ -1671,7 +1691,7 @@ export function MessageInput({ disabled={steering} > - {t("steerIntoTurn")} + {t(steerChannel === "pull" ? "steerAsNote" : "steerIntoTurn")} diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx index f6052b452..9c35d7095 100644 --- a/src/components/conversations/conversation-detail-panel.tsx +++ b/src/components/conversations/conversation-detail-panel.tsx @@ -2019,11 +2019,12 @@ const ConversationTabView = memo(function ConversationTabView({ steeredMessageIds: conn.steeredMessageIds, onResendAsPrompt: resendFeedbackAsPrompt, }) - // Composer "insert into current turn" (native steering only). Rethrows — - // MessageInput owns the enqueue fallback and draft-preservation policy, so - // this wrapper must not swallow the turn-end race the way `submit` does. - // `blocks` rides along when the draft carries attachments (images steer - // too); `text` stays the recorded/display form. + // Composer mid-turn send, over whichever live-feedback channel this session + // has (native push or the pull tool). Rethrows — MessageInput owns the + // enqueue fallback and draft-preservation policy, so this wrapper must not + // swallow the turn-end race the way `submit` does. `blocks` rides along when + // the draft carries attachments (images steer too); `text` stays the + // recorded/display form. const feedbackSteer = feedback.steer const handleSteer = useCallback( async (text: string, blocks?: PromptInputBlock[]) => { @@ -2115,13 +2116,17 @@ const ConversationTabView = memo(function ConversationTabView({ onSaveQueueEdit={handleSaveQueueEdit} onCancelQueueEdit={handleQueueCancelEdit} onSteer={ - // Native channel only: on pull sessions the prompting branch must - // stay pixel-identical (Stop button alone). The prompting scope - // itself is enforced where the button renders. - feedback.featureEnabled && feedback.channel === "native" + // Any working delivery channel, not just the native push: the pull + // tool records a waiting note the agent reads on its next check, and + // `steerChannel` swaps the copy so pull sessions never promise an + // instant insert. Sessions with NEITHER channel keep the historical + // prompting branch (Stop button alone, Enter queues). The prompting + // scope itself is enforced where the button renders. + feedback.featureEnabled && feedback.steerAvailable ? handleSteer : undefined } + steerChannel={feedback.channel} > {isWelcomeMode ? ( // Same overlay scrollbar as the sidebar / file lists (os-theme-codeg) diff --git a/src/hooks/use-session-feedback.test.ts b/src/hooks/use-session-feedback.test.ts index 9bfb756a0..91944c08c 100644 --- a/src/hooks/use-session-feedback.test.ts +++ b/src/hooks/use-session-feedback.test.ts @@ -282,6 +282,7 @@ describe("useSessionFeedback", () => { await waitFor(() => expect(result.current.canSubmit).toBe(true)) expect(result.current.channel).toBe("native") + expect(result.current.steerAvailable).toBe(true) }) it("stays on the pull channel when only the tool is available", async () => { @@ -295,6 +296,26 @@ describe("useSessionFeedback", () => { await waitFor(() => expect(result.current.canSubmit).toBe(true)) expect(result.current.channel).toBe("pull") + // The pull tool is a working delivery channel — the composer's mid-turn + // send must be offered here too, not only on native sessions. + expect(result.current.steerAvailable).toBe(true) + }) + + it("reports no steer channel when the session has neither", async () => { + // No tool (launched before the feature was enabled / agent without MCP) + // and no native steering: the composer must keep its historical Stop-only + // prompting form, so `steerAvailable` stays false even mid-turn. + mockSnapshot.mockResolvedValue( + snapshot({ + feedback_tool_available: false, + native_steering_available: false, + }) + ) + const { result } = renderHook(() => useSessionFeedback(baseProps)) + + await waitFor(() => expect(mockSnapshot).toHaveBeenCalled()) + expect(result.current.steerAvailable).toBe(false) + expect(result.current.canSubmit).toBe(false) }) it("steer appends optimistically on success and RETHROWS on failure", async () => { diff --git a/src/hooks/use-session-feedback.ts b/src/hooks/use-session-feedback.ts index 8d4db19e9..cc7469d20 100644 --- a/src/hooks/use-session-feedback.ts +++ b/src/hooks/use-session-feedback.ts @@ -77,9 +77,16 @@ export interface UseSessionFeedback { /** Which channel a note would ride: `native` = the ACP `_session/steering` * push (injected into the running turn immediately), `pull` = the * `check_user_feedback` MCP tool (read when the agent next checks). Drives - * copy and the composer's "insert into current turn" entry. Backend- - * synthesized — never derived from agent type here. */ + * copy and the composer's mid-turn send entry. Backend-synthesized — never + * derived from agent type here. */ channel: "native" | "pull" + /** Whether THIS session has a working mid-turn delivery channel at all: + * native push or the pull tool. Gates the composer's mid-turn send + * affordance (`channel` picks its copy); a session with neither keeps the + * historical Stop-only prompting form. Distinct from `canSubmit`, which + * additionally folds in the prompting scope — the composer enforces that + * where the button renders. */ + steerAvailable: boolean /** Whether to render the read-only notes list above the composer. */ showList: boolean /** Whether a submit is in flight (disables the dialog send button). */ @@ -402,11 +409,9 @@ export function useSessionFeedback({ const openDialog = useCallback(() => setDialogOpen(true), []) const closeDialog = useCallback(() => setDialogOpen(false), []) + const steerAvailable = toolAvailable || nativeSteering const canSubmit = - enabled && - Boolean(connectionId) && - (toolAvailable || nativeSteering) && - isPrompting + enabled && Boolean(connectionId) && steerAvailable && isPrompting const channel: "native" | "pull" = nativeSteering ? "native" : "pull" // Drop the notes the transcript is already rendering as user turns. Kept as // a derivation rather than a filter on `setNotes` so a note stays recoverable @@ -425,6 +430,7 @@ export function useSessionFeedback({ featureEnabled: enabled, canSubmit, channel, + steerAvailable, showList, submitting, dialogOpen, @@ -438,6 +444,7 @@ export function useSessionFeedback({ enabled, canSubmit, channel, + steerAvailable, showList, submitting, dialogOpen, diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index d596812f0..0b0d6eab5 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -2900,8 +2900,10 @@ "send": "إرسال", "queueMessage": "إضافة إلى قائمة الانتظار", "steerIntoTurn": "إدراج في الدور الحالي", + "steerAsNote": "إرسال ملاحظة للفحص التالي", "steerQueuedInstead": "أُضيفت إلى قائمة الانتظار — ستُرسل مع الدور التالي.", "steerFailed": "تعذّر الإدراج في الدور الحالي", + "steerNoteFailed": "تعذّر إرسال ملاحظتك", "slashCommands": "أوامر الشرطة المائلة", "slashSearchPlaceholder": "البحث عن الأوامر...", "slashSearchEmpty": "لا توجد أوامر مطابقة", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 7cc608930..917042e56 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -2900,8 +2900,10 @@ "send": "Senden", "queueMessage": "In Warteschlange stellen", "steerIntoTurn": "In laufenden Turn einfügen", + "steerAsNote": "Notiz für die nächste Prüfung senden", "steerQueuedInstead": "Stattdessen in die Warteschlange gestellt – wird mit dem nächsten Turn gesendet.", "steerFailed": "Konnte nicht in den laufenden Turn eingefügt werden", + "steerNoteFailed": "Deine Notiz konnte nicht gesendet werden", "slashCommands": "Slash-Befehle", "slashSearchPlaceholder": "Befehle suchen...", "slashSearchEmpty": "Keine passenden Befehle", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 6ed241707..ac1757fb1 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2902,8 +2902,10 @@ "send": "Send", "queueMessage": "Queue message", "steerIntoTurn": "Insert into current turn", + "steerAsNote": "Send note for next check", "steerQueuedInstead": "Queued instead — it will be sent with the next turn.", "steerFailed": "Couldn't insert into the current turn", + "steerNoteFailed": "Couldn't send your note", "slashCommands": "Slash commands", "slashSearchPlaceholder": "Search commands...", "slashSearchEmpty": "No matching commands", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 457db3b4f..7d747ecb7 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -2900,8 +2900,10 @@ "send": "Enviar", "queueMessage": "Poner en cola", "steerIntoTurn": "Insertar en el turno actual", + "steerAsNote": "Enviar nota para la próxima comprobación", "steerQueuedInstead": "Se puso en cola: se enviará con el siguiente turno.", "steerFailed": "No se pudo insertar en el turno actual", + "steerNoteFailed": "No se pudo enviar tu nota", "slashCommands": "Comandos de barra", "slashSearchPlaceholder": "Buscar comandos...", "slashSearchEmpty": "Sin comandos coincidentes", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index fd9c2f8a9..480e1635b 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -2900,8 +2900,10 @@ "send": "Envoyer", "queueMessage": "Mettre en file d'attente", "steerIntoTurn": "Insérer dans le tour en cours", + "steerAsNote": "Envoyer une note pour la prochaine vérification", "steerQueuedInstead": "Mis en file d'attente — il sera envoyé au tour suivant.", "steerFailed": "Impossible d'insérer dans le tour en cours", + "steerNoteFailed": "Impossible d'envoyer votre note", "slashCommands": "Commandes slash", "slashSearchPlaceholder": "Rechercher des commandes...", "slashSearchEmpty": "Aucune commande correspondante", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index ddaca4ccd..6ab41949f 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -2900,8 +2900,10 @@ "send": "送信", "queueMessage": "キューに追加", "steerIntoTurn": "現在のターンに挿入", + "steerAsNote": "次の確認用にメモを送信", "steerQueuedInstead": "代わりにキューに追加しました。次のターンで送信されます。", "steerFailed": "現在のターンに挿入できませんでした", + "steerNoteFailed": "メモを送信できませんでした", "slashCommands": "スラッシュコマンド", "slashSearchPlaceholder": "コマンドを検索...", "slashSearchEmpty": "一致するコマンドがありません", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 5fd2cd814..35875ca9c 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -2900,8 +2900,10 @@ "send": "보내기", "queueMessage": "대기열에 추가", "steerIntoTurn": "현재 턴에 삽입", + "steerAsNote": "다음 확인용 메모 보내기", "steerQueuedInstead": "대신 대기열에 추가되었습니다. 다음 턴에 전송됩니다.", "steerFailed": "현재 턴에 삽입하지 못했습니다", + "steerNoteFailed": "메모를 보내지 못했습니다", "slashCommands": "슬래시 명령", "slashSearchPlaceholder": "명령 검색...", "slashSearchEmpty": "일치하는 명령이 없습니다", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 7da8317f4..d93e60ee0 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -2900,8 +2900,10 @@ "send": "Enviar", "queueMessage": "Adicionar à fila", "steerIntoTurn": "Inserir no turno atual", + "steerAsNote": "Enviar nota para a próxima verificação", "steerQueuedInstead": "Adicionado à fila — será enviado no próximo turno.", "steerFailed": "Não foi possível inserir no turno atual", + "steerNoteFailed": "Não foi possível enviar a sua nota", "slashCommands": "Comandos de barra", "slashSearchPlaceholder": "Buscar comandos...", "slashSearchEmpty": "Nenhum comando correspondente", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 42a142673..2e4ed5822 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -2902,8 +2902,10 @@ "send": "发送", "queueMessage": "加入队列", "steerIntoTurn": "插入当前回合", + "steerAsNote": "发送留言,供下次检查时读取", "steerQueuedInstead": "已转入队列——将随下一回合发送。", "steerFailed": "无法插入当前回合", + "steerNoteFailed": "留言发送失败", "slashCommands": "斜杠命令", "slashSearchPlaceholder": "搜索命令...", "slashSearchEmpty": "没有匹配的命令", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 1d76b2266..840fbfd9f 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -2900,8 +2900,10 @@ "send": "傳送", "queueMessage": "加入佇列", "steerIntoTurn": "插入目前回合", + "steerAsNote": "傳送留言,供下次檢查時讀取", "steerQueuedInstead": "已轉入佇列——將隨下一回合傳送。", "steerFailed": "無法插入目前回合", + "steerNoteFailed": "留言送出失敗", "slashCommands": "斜線命令", "slashSearchPlaceholder": "搜尋命令...", "slashSearchEmpty": "沒有符合的指令",