feat(#1766): support OpenCode steer delivery / follow-up behavior settings (#1781)

* feat: support OpenCode steer delivery / follow-up behavior settings

Implements issue #1766 — steer delivery mode for mid-turn message
insertion, replacing the old boolean queue-mode toggle with a tri-state
follow-up behavior setting (Steer / Queue / Send immediately).

- Plumbing: threaded optional delivery: 'steer' through sendMessage
  -> routeMessage -> opencodeClient.sendMessage -> promptAsync
- Store: messageQueueStore stores followUpBehavior; migration from
  legacy queueModeEnabled persisted state
- Settings: Chat -> Follow-up behavior shows three radio options
  using existing settings UI patterns
- Composer: when session is busy, a floating queue button remains;
  force-sending a queued message (via chip click) uses delivery: 'steer'
  during a busy session; Steer button intentionally omitted — steer is
  available via the two-gesture path (Enter to queue -> chip to steer)
- Keyboard: queue mode = Enter queues, Ctrl+Enter sends; otherwise
  Enter sends, Ctrl+Enter queues
- Persistence: DesktopSettings, web settings payload, and server-side
  sanitizer handle the new key with legacy fallback
- i18n: follow-up behavior section and option labels in all 9 locales
  plus new chat.chatInput.actions.queue label
- Search: settings registry updated from chat.queue-mode to
  chat.follow-up-behavior

Validation: type-check passes (no new errors), lint clean.

* fix(#1766): make steer mode actually steer

The followUpBehavior === 'steer' branch in handlePrimaryAction and the
keyboard handler was a no-op — both fell into the else branch and sent
without the delivery: 'steer' flag, so selecting 'Steer (insert into
the running turn)' in settings produced identical behavior to 'Send
immediately'.

- handlePrimaryAction: when steer mode is selected and the session is
  busy, call handleSubmit({ delivery: 'steer' }) directly
- Keyboard handler: in steer mode, Enter steers and Ctrl+Enter sends
  immediately (consistent with queue mode where Ctrl+Enter bypasses
  the special handling)

Also removes the unused chat.chatInput.actions.queue i18n key from all
9 locales (it was a dead key after the Steer button was removed from
the composer).

Validation: type-check clean, lint clean.

* refactor(#1766): flatten nested ternary in followUpBehavior resolution

Replace nested ternary with explicit if/else chain per project code style
(CONTRIBUTING.md). Import FollowUpBehavior type explicitly for the new
let declaration.

* feat(chat): drop redundant 'immediate' follow-up mode, keep Queue + Steer

'Immediate' was wire-identical to 'Steer' on a busy session: OpenCode only
supports delivery 'steer' | 'queue' and defaults to 'steer', so an immediate
send (no delivery flag) already steered into the running turn. The three-mode
UI therefore exposed two settings that did the same thing.

Collapse to two modes — Queue (unchanged: client-side queue with edit/reorder)
and Steer. Any persisted/legacy 'immediate' (and legacy queueModeEnabled=false)
now maps to 'steer', preserving prior behavior. Removes the immediate option,
its keyboard branch, the i18n label across all locales, and narrows the
followUpBehavior union to 'steer' | 'queue'.

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
bashrusakh
2026-06-29 01:28:20 +03:00
committed by GitHub
co-authored by Leonid Skorobogatyy Bohdan Triapitsyn
parent c184ddf185
commit f13f6d5540
22 changed files with 244 additions and 78 deletions
+13 -5
View File
@@ -2,7 +2,7 @@ import type { DesktopSettings } from '@/lib/desktop';
import { createProjectIdFromPath } from '@/lib/projectId';
import { useUIStore } from '@/stores/useUIStore';
import { isMonoFontOption, isUiFontOption } from '@/lib/fontOptions';
import { useMessageQueueStore } from '@/stores/messageQueueStore';
import { isFollowUpBehavior, normalizeFollowUpBehavior, useMessageQueueStore, type FollowUpBehavior } from '@/stores/messageQueueStore';
import { setDirectoryShowHidden } from '@/lib/directoryShowHidden';
import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence';
@@ -444,8 +444,14 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
}
}
if (typeof settings.queueModeEnabled === 'boolean' && settings.queueModeEnabled !== queueStore.queueModeEnabled) {
queueStore.setQueueMode(settings.queueModeEnabled);
let nextFollowUpBehavior: FollowUpBehavior | null = null;
if (isFollowUpBehavior(settings.followUpBehavior)) {
nextFollowUpBehavior = settings.followUpBehavior;
} else if (typeof settings.queueModeEnabled === 'boolean') {
nextFollowUpBehavior = normalizeFollowUpBehavior(undefined, settings.queueModeEnabled);
}
if (nextFollowUpBehavior && nextFollowUpBehavior !== queueStore.followUpBehavior) {
queueStore.setFollowUpBehavior(nextFollowUpBehavior);
}
if (typeof settings.showDeletionDialog === 'boolean' && settings.showDeletionDialog !== store.showDeletionDialog) {
@@ -838,8 +844,10 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.gitmojiEnabled === 'boolean') {
result.gitmojiEnabled = candidate.gitmojiEnabled;
}
if (typeof candidate.queueModeEnabled === 'boolean') {
result.queueModeEnabled = candidate.queueModeEnabled;
if (isFollowUpBehavior(candidate.followUpBehavior)) {
result.followUpBehavior = candidate.followUpBehavior;
} else if (typeof candidate.queueModeEnabled === 'boolean') {
result.followUpBehavior = normalizeFollowUpBehavior(undefined, candidate.queueModeEnabled);
}
if (typeof candidate.showDeletionDialog === 'boolean') {
result.showDeletionDialog = candidate.showDeletionDialog;