* 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:
committed by
GitHub
co-authored by
Leonid Skorobogatyy
Bohdan Triapitsyn
parent
c184ddf185
commit
f13f6d5540
@@ -4,6 +4,40 @@ import { getSafeStorage } from './utils/safeStorage';
|
||||
import type { AttachedFile } from './types/sessionTypes';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
|
||||
export type FollowUpBehavior = 'steer' | 'queue';
|
||||
|
||||
export const DEFAULT_FOLLOW_UP_BEHAVIOR: FollowUpBehavior = 'queue';
|
||||
|
||||
export const isFollowUpBehavior = (value: unknown): value is FollowUpBehavior => (
|
||||
value === 'steer' || value === 'queue'
|
||||
);
|
||||
|
||||
export const normalizeFollowUpBehavior = (
|
||||
value: unknown,
|
||||
legacyQueueModeEnabled?: boolean | null,
|
||||
): FollowUpBehavior => {
|
||||
// "immediate" was removed: on a busy session it was wire-identical to
|
||||
// "steer" (OpenCode only supports delivery "steer" | "queue", defaulting
|
||||
// to "steer"), so collapse any persisted/legacy "immediate" onto "steer".
|
||||
if (value === 'immediate') {
|
||||
return 'steer';
|
||||
}
|
||||
|
||||
if (isFollowUpBehavior(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (legacyQueueModeEnabled === false) {
|
||||
return 'steer';
|
||||
}
|
||||
|
||||
if (legacyQueueModeEnabled === true) {
|
||||
return 'queue';
|
||||
}
|
||||
|
||||
return DEFAULT_FOLLOW_UP_BEHAVIOR;
|
||||
};
|
||||
|
||||
export interface QueuedMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
@@ -20,7 +54,7 @@ export interface QueuedMessage {
|
||||
|
||||
interface MessageQueueState {
|
||||
queuedMessages: Record<string, QueuedMessage[]>; // sessionId → queue
|
||||
queueModeEnabled: boolean; // global toggle
|
||||
followUpBehavior: FollowUpBehavior;
|
||||
}
|
||||
|
||||
interface MessageQueueActions {
|
||||
@@ -30,18 +64,24 @@ interface MessageQueueActions {
|
||||
popToInput: (sessionId: string, messageId: string) => QueuedMessage | null;
|
||||
clearQueue: (sessionId: string) => void;
|
||||
clearAllQueues: () => void;
|
||||
setQueueMode: (enabled: boolean) => void;
|
||||
setFollowUpBehavior: (behavior: FollowUpBehavior) => void;
|
||||
getQueueForSession: (sessionId: string) => QueuedMessage[];
|
||||
}
|
||||
|
||||
type MessageQueueStore = MessageQueueState & MessageQueueActions;
|
||||
|
||||
type PersistedMessageQueueState = {
|
||||
queuedMessages?: Record<string, QueuedMessage[]>;
|
||||
followUpBehavior?: FollowUpBehavior;
|
||||
queueModeEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const useMessageQueueStore = create<MessageQueueStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
queuedMessages: {},
|
||||
queueModeEnabled: true,
|
||||
followUpBehavior: DEFAULT_FOLLOW_UP_BEHAVIOR,
|
||||
|
||||
addToQueue: (sessionId, message) => {
|
||||
const id = `queued-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
@@ -149,10 +189,9 @@ export const useMessageQueueStore = create<MessageQueueStore>()(
|
||||
set({ queuedMessages: {} });
|
||||
},
|
||||
|
||||
setQueueMode: (enabled) => {
|
||||
set({ queueModeEnabled: enabled });
|
||||
// Persist to settings.json (async, fire-and-forget)
|
||||
void updateDesktopSettings({ queueModeEnabled: enabled });
|
||||
setFollowUpBehavior: (behavior) => {
|
||||
set({ followUpBehavior: behavior });
|
||||
void updateDesktopSettings({ followUpBehavior: behavior });
|
||||
},
|
||||
|
||||
getQueueForSession: (sessionId) => {
|
||||
@@ -161,11 +200,19 @@ export const useMessageQueueStore = create<MessageQueueStore>()(
|
||||
}),
|
||||
{
|
||||
name: 'message-queue-store',
|
||||
version: 1,
|
||||
storage: createJSONStorage(() => getSafeStorage()),
|
||||
partialize: (state) => ({
|
||||
queuedMessages: state.queuedMessages,
|
||||
queueModeEnabled: state.queueModeEnabled,
|
||||
followUpBehavior: state.followUpBehavior,
|
||||
}),
|
||||
migrate: (persistedState) => {
|
||||
const state = (persistedState ?? {}) as PersistedMessageQueueState;
|
||||
return {
|
||||
queuedMessages: state.queuedMessages ?? {},
|
||||
followUpBehavior: normalizeFollowUpBehavior(state.followUpBehavior, state.queueModeEnabled ?? null),
|
||||
};
|
||||
},
|
||||
}
|
||||
),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user