Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions docs/engineering/chat_steering_and_queueing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
- **Lifecycle:** Active
- **Scope:** Ordinary adaptive chat turns; text steering and existing prompt queue
- **Owner:** Von maintainers
- **Last reviewed:** 11 September 2026
- **Last reviewed:** 13 September 2026
- **Review trigger:** Changes to turn admission, model continuation or composer submission

“Q” means queueing here. The existing chat code provides a durable FIFO prompt
Expand All @@ -19,17 +19,32 @@ authority. No represented workflow or new prompt policy is needed.

## Interaction

During a running turn, **Steer** sends composer text to that exact attempt.
**Queue Prompt** (including the ordinary Enter/send action) continues to submit
an independent FIFO request. Steering neither consumes nor reorders the queue.
When idle, the ordinary Send Prompt action starts a turn.

The composer shows pending steering and allows **Cancel steer** until the active
turn has taken it. After delivery, withdrawal is rejected: another steer can
correct it, or the existing Stop action can request turn cancellation. Neither
steering nor Stop claims to undo completed tool effects. Steering waits for the
next model boundary, including completion of a running tool batch; it does not
interrupt an in-flight provider request or tool execution.
The icon-only submit arrow uses a saved **Queue / Steering** default while Von
is working. Queue remains the initial default. The setting is in **More actions**
and explicitly applies to this browser and signed-in user, with an in-memory
fallback when storage is unavailable. The current user-preferences endpoint
supports language settings, so this slice does not claim cross-device sync.
Only an explicit settings choice changes the preference; Shift-click selects the
opposite action once. Idle submit remains ordinary Send. Desktop Enter follows
the default, Shift+Enter inserts a newline, and compact/touch Enter remains a
newline with IME protection. More actions exposes named one-shot steering and
queue buttons for keyboard and touch access.

The arrow has a curved variant for steering and a queue mark for queueing, with
an accessible action name and explanatory tooltip/help. Unavailable steering
retains the draft and explains the explicit Queue alternative. Steering never
silently becomes queued work. Activation captures the exact current attempt;
background queue dispatch keeps its existing path.

Short, polite overlay notices announce steering transitions without consuming
draft width. **More actions → Steering activity** retains receipt text and
**Cancel steer** for pending guidance. A dot on More actions indicates pending
work or an error. Unchanged polling does not repeat notices or replace focused
receipt controls. Errors remain in Activity after the notice expires. After
delivery, withdrawal is rejected: another steer can correct it, or the existing
Stop action can request cancellation. Neither action undoes completed tools.
Steering waits for the next model boundary, including completion of a running
tool batch; it does not interrupt an in-flight provider request or tool execution.

Text-only steering is disabled while attachments or unfinished dictation are
present. The established queue/send path handles those inputs. Imported
Expand Down Expand Up @@ -95,3 +110,17 @@ The real Flask generation route is exercised with a test model and mock storage;
no live model is required. A Chromium fixture uses the repository composer
markup and styles at 375px to exercise submit, pending feedback and cancellation.
This is fixture-backed acceptance, not authenticated public-server validation.

Conversation headers use a native **Conversation actions** disclosure with
ordinary Tab navigation, Escape dismissal and focus return. Chat context remains
its existing disclosure; secondary situation, task, invitation, export and
profile actions have named entries. Imports remain in conversation navigation's
new-conversation menu. The message-exchange menu preserves Refresh, referenced
tasks and the distinct participant/own profile destinations.

`tests/browser/conversationActions.cjs` exercises production template/styles and
chat steering, compact composer and message-panel modules at desktop, phone and
landscape sizes with deterministic API fixtures. It checks long-receipt draft
width, action reachability, preferences, one-shot alternatives, cancellation,
keyboard dismissal and exchange menu bounds. This is local fixture-backed UI
evidence, not public authentication, physical-device or deployment acceptance.
46 changes: 20 additions & 26 deletions src/frontend/web/von_interface/static/js/chatTab.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { initialiseConversationActions } from './components/conversationActions.js';
import { createLatestMessageButton, setNavigationVisible, focusConversationTarget } from './components/conversationNavigation.js';
import { initialiseCompactChatComposer, isCompactComposer, resizeCompactDraft, shouldSubmitComposerKey } from './components/compactComposer.js';
import { updateExecutionCost } from './components/executionCost.js';
Expand Down Expand Up @@ -6694,6 +6695,7 @@ function updateSendButtonForCurrentChatState() {
return;
}
updateSendButtonDraftReadiness();
sendButton.dataset.submitMode = 'send';
const chatCreationInFlight = Boolean(newChatCreationInFlight);
const uploadInFlight = !!getInFlightUploadStateForSession(activeChatSessionId);
const imagePreparationBlocked = imagesBlocked(activeChatSessionId);
Expand Down Expand Up @@ -6739,7 +6741,9 @@ function updateSendButtonForCurrentChatState() {
const input = getPromptInputElement();
if (input?.value === submitted) setPromptComposerValue('', { promptInput: input });
},
createId: createClientRequestId
createId: createClientRequestId,
onQueue: () => { setSelectedChatPromptQueueEntryId(null); return handleSendPrompt(); },
onModeChange: updateSendButtonForCurrentChatState
});
}
const steeringRecord = queuedChatPrompts.find(entry =>
Expand All @@ -6749,7 +6753,10 @@ function updateSendButtonForCurrentChatState() {
const steeringTarget = steeringRecord || (liveRequest?.promptQueueRecordId && liveRequest?.attemptId
? { queueId: liveRequest.promptQueueRecordId, attemptId: liveRequest.attemptId } : null);
chatSteeringControls.update({
scopeKey: `${chatOrganisationGeneration}:${activeChatSessionId}`,
scopeKey: `${getCurrentUserConceptId()}:${chatOrganisationGeneration}:${activeChatSessionId}`,
actorKey: getCurrentUserConceptId(),
busy: sessionBusy && !readOnly && !conceptQaSession && !getSelectedChatPromptQueueEntryForSend(),
queueDisabled: sendButton.disabled || pendingChatOrganisationSwitchId !== null,
target: readOnly || conceptQaSession ? null : steeringTarget,
disabled: sendButton.disabled || pendingChatOrganisationSwitchId !== null
|| pendingFileCopyConceptIdsBySession.has(getPendingFileCopySessionKey(activeChatSessionId))
Expand Down Expand Up @@ -6801,6 +6808,7 @@ function updateSendButtonForCurrentChatState() {
? 'Queue a separate request after the current turn finishes. Use Steer to guide the active turn.'
: 'Send Prompt';
}
chatSteeringControls.present();
}

function syncActiveChatSessionThinkingState() {
Expand Down Expand Up @@ -33793,7 +33801,8 @@ export function initializeChatTab() {
sessionTabsCache = normaliseConversationSessionViewModels([...new Map([...pinned, ...(selected ? [selected] : []), ...rows.map(row => ({ ...sessionTabsCache.find(previous => previous.session_id === row.session_id), ...row }))].map(row => [row.session_id, row])).values()]);
reconcileServerConversationPreferences(sessionTabsCache);
}, currentChat: () => sessionTabsCache.find(row => row.session_id === activeChatSessionId) });
document.querySelector('.chat-header-controls')?.append(profileButton());
document.querySelector('#conversationActions .conversation-actions-panel')?.append(profileButton());
initialiseConversationActions();
console.log("Initializing chat tab...");
bindFocusedConversationEvents();

Expand Down Expand Up @@ -33894,9 +33903,7 @@ export function initializeChatTab() {
const inviteProjectFilter = document.getElementById('inviteProjectFilter');
const incomingInvitesButton = document.getElementById('incomingInvitesBtn');
const incomingInvitesCloseButton = document.getElementById('closeIncomingInvites');
const importExternalConversationButton = document.getElementById('importExternalConversationBtn');
const importExternalConversationInput = document.getElementById('importExternalConversationInput');
const importAllExternalConversationsButton = document.getElementById('importAllExternalConversationsBtn');
const continueImportedConversationButton = document.getElementById('continueImportedConversationBtn');

if (!sendButton || !resetButton || !promptInput) {
Expand Down Expand Up @@ -33949,39 +33956,22 @@ export function initializeChatTab() {
renderPendingAttachmentState();
renderActiveUploadUi();

if (importExternalConversationButton && importExternalConversationInput) {
importExternalConversationButton.addEventListener('click', () => {
importExternalConversationInput.click();
});
if (importExternalConversationInput) {
importExternalConversationInput.addEventListener('change', async () => {
const file = importExternalConversationInput.files?.[0] || null;
importExternalConversationInput.value = '';
if (!file) {
return;
}
importExternalConversationButton.disabled = true;
try {
await handleExternalConversationFile(file);
} catch (error) {
console.error('[chatTab] External conversation import failed:', error);
showToast(error?.message || 'Unable to import this conversation.', 'error');
} finally {
importExternalConversationButton.disabled = false;
}
});
}
if (importAllExternalConversationsButton) {
importAllExternalConversationsButton.addEventListener('click', async () => {
importAllExternalConversationsButton.disabled = true;
try {
await startExternalConversationBulkImport();
} catch (error) {
console.error('[chatTab] Machine conversation import failed:', error);
showToast(error?.message || 'Unable to start the machine conversation import.', 'error');
} finally {
importAllExternalConversationsButton.disabled = false;
}
});
if (document.getElementById('externalConversationBulkImportPanel')) {
document.getElementById('pauseExternalConversationBulkImportBtn')?.addEventListener('click', () => {
void controlExternalConversationBulkImport('pause').catch((error) => showToast(error?.message || 'Unable to pause import.', 'error'));
});
Expand Down Expand Up @@ -34293,7 +34283,11 @@ export function initializeChatTab() {
initialiseCompactChatComposer(promptInput.closest('.chat-composer'), resizePromptComposer);

// Add event listeners
sendButton.addEventListener('click', handleSendPrompt);
const submitComposer = (event) => {
updateSendButtonForCurrentChatState();
if (!chatSteeringControls?.activate(event)) void handleSendPrompt();
};
sendButton.addEventListener('click', submitComposer);
resetButton.addEventListener('click', handleResetContext);

ensureAbortButtonBound();
Expand All @@ -34315,7 +34309,7 @@ export function initializeChatTab() {
promptInput.addEventListener('keypress', function (event) {
if (shouldSubmitComposerKey(event)) {
event.preventDefault();
handleSendPrompt();
submitComposer(event);
}
});

Expand Down
Loading
Loading