Skip to content

feat(chat): route active input steering#1555

Merged
zerob13 merged 2 commits intodevfrom
codex-active-input-routing
Apr 28, 2026
Merged

feat(chat): route active input steering#1555
zerob13 merged 2 commits intodevfrom
codex-active-input-routing

Conversation

@zhangmo8
Copy link
Copy Markdown
Collaborator

@zhangmo8 zhangmo8 commented Apr 28, 2026

Summary by CodeRabbit

  • New Features

    • Active input routing: Enter steers the current turn during generation, Tab queues input for the next turn, and Enter sends normally when idle.
    • New "Steer" and "Queue" actions in the input toolbar to redirect or enqueue drafts during generation.
  • UI Updates

    • Removed "convert queue to steer" option from pending input lane.
    • Added localized "Send" and "Steer" labels for 13 locales.
  • Documentation

    • New spec, plan, and task docs describing active input routing.
  • Tests

    • Added/updated tests covering steer, queue, routing, and regression cases.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

This PR implements Active Input Routing: Enter sends when idle, Enter steers active generations, and Tab queues drafts. It adds a steer IPC route, presenter/runtime steer handling to interrupt and restart streams, renderer event/toolbar updates, pending-input UI changes, i18n additions, and tests for routing and interruption.

Changes

Cohort / File(s) Summary
Specification Documents
docs/specs/active-input-routing/plan.md, docs/specs/active-input-routing/spec.md, docs/specs/active-input-routing/tasks.md
New spec and task artifacts describing modes (send/steer/queue), routing rules, required IPC (chat.steerActiveTurn), UI behavior, and implementation checklist.
Runtime Presenter
src/main/presenter/agentRuntimePresenter/index.ts
Adds per-session steer-input buffer and abort-reason tracking; new steerActiveTurn entrypoint; aborts active generation for steer, settles interrupted assistant message, then restarts processing with steer content; clears buffers on cancel/teardown.
Session Presenter
src/main/presenter/agentSessionPresenter/index.ts
Non-blocking sendMessage fire-and-forget behavior; adds steerActiveTurn to normalize input, update draft state/title, ensure ACP workdir, and forward to agent runtime.
Service / Routes / Hot-path
src/main/routes/chat/chatService.ts, src/main/routes/hotPathPorts.ts, src/main/routes/index.ts
ChatService steerActiveTurn method with session lookup and bounded scheduler timeout; ProviderExecutionPort extended with steerActiveTurn; dispatcher wired for chat.steerActiveTurn route.
Route Contracts & Types
src/shared/contracts/routes/chat.routes.ts, src/shared/contracts/routes.ts, src/shared/types/agent-interface.d.ts, src/shared/types/presenters/agent-session.presenter.d.ts
New chat.steerActiveTurn route contract added to catalog; IAgentImplementation and IAgentSessionPresenter interfaces extended with steerActiveTurn.
Renderer Client & Components
src/renderer/api/ChatClient.ts, src/renderer/src/components/chat/ChatInputBox.vue, src/renderer/src/components/chat/ChatInputToolbar.vue, src/renderer/src/pages/ChatPage.vue
ChatClient exposes steerActiveTurn. ChatInputBox emits queue-submit on Tab when enabled. Toolbar gains conditional steer button and queue/stop/send modes. ChatPage routes sends/steers/queue submissions per generation state and wires new events/props.
Pending Input UI
src/renderer/src/components/chat/PendingInputLane.vue
Removed convert-queue-to-steer button/event; queued items remain editable/reorderable but cannot be converted to steer via row control.
i18n
src/renderer/src/i18n/{da-DK,en-US,fa-IR,fr-FR,he-IL,ja-JP,ko-KR,pt-BR,ru-RU,zh-CN,zh-HK,zh-TW}/chat.json
Added input.send and input.steer translation keys across locales.
Backend Tests
test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts, test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts, test/main/routes/chatService.test.ts, test/main/routes/contracts.test.ts, test/main/routes/dispatcher.test.ts
Added and updated tests for steer interruption, pre-stream/tool-definition steer behavior, ChatService steer scheduling, route contract parsing, and dispatcher routing for steer.
Renderer Tests
test/renderer/api/clients.test.ts, test/renderer/components/ChatInputBox.test.ts, test/renderer/components/ChatInputToolbar.test.ts, test/renderer/components/ChatPage.test.ts
Added tests for ChatClient.steerActiveTurn bridge call, ChatInputBox queue-submit emission, toolbar steer/queue interactions, and ChatPage routing/queue-capacity scenarios.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer
    participant ChatService
    participant SessionPresenter
    participant RuntimePresenter
    participant Stream
    Note over Renderer,RuntimePresenter: Idle — Enter (send)
    Renderer->>ChatService: sendMessage(sessionId, content)
    ChatService->>SessionPresenter: sendMessage(sessionId, content)
    SessionPresenter->>RuntimePresenter: processMessage(sessionId, content)
    RuntimePresenter->>Stream: start generation

    Note over Renderer,RuntimePresenter: Generating — Enter (steer)
    Renderer->>ChatService: steerActiveTurn(sessionId, content)
    ChatService->>SessionPresenter: steerActiveTurn(sessionId, content)
    SessionPresenter->>RuntimePresenter: steerActiveTurn(sessionId, content)
    RuntimePresenter->>Stream: abort (reason="steer")
    Stream-->>RuntimePresenter: aborted
    RuntimePresenter->>RuntimePresenter: settle partial assistant message
    RuntimePresenter->>RuntimePresenter: consume steer buffer
    RuntimePresenter->>Stream: restart generation with steer content

    Note over Renderer: Generating — Tab (queue)
    Renderer->>Renderer: emit queue-submit
    Renderer->>Renderer: pendingInputStore.queueInput(content)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

"🐰
Hops with glee, whiskers twitching bright,
I steer the turn and queue by Tab tonight.
Enter nudges streams, Tab holds dreams in line,
Small paws route inputs so the chat will refine."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(chat): route active input steering' accurately summarizes the main change—implementing active input steering routing for the chat system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex-active-input-routing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
test/main/routes/dispatcher.test.ts (1)

690-706: Assert the steer route response payload too.

Line [690] currently validates only forwarding. Capturing and asserting the route result ({ accepted: true }) would better protect the route contract.

Proposed test tightening
-    await dispatchDeepchatRoute(
+    const steerResult = await dispatchDeepchatRoute(
       runtime,
       'chat.steerActiveTurn',
       {
         sessionId: 'session-1',
         content: 'refine the active answer'
@@
     expect(agentSessionPresenter.steerActiveTurn).toHaveBeenCalledWith(
       'session-1',
       'refine the active answer'
     )
+    expect(steerResult).toEqual({ accepted: true })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/routes/dispatcher.test.ts` around lines 690 - 706, The test
currently only asserts that agentSessionPresenter.steerActiveTurn was called;
capture the result returned by dispatchDeepchatRoute for the
'chat.steerActiveTurn' call and add an assertion that the response payload
equals { accepted: true } to validate the route contract. Locate the invocation
of dispatchDeepchatRoute in the test and store its return value, then add an
expect(result).toEqual({ accepted: true }) (or equivalent) in addition to the
existing expect for agentSessionPresenter.steerActiveTurn.
test/renderer/components/ChatInputBox.test.ts (1)

282-313: Add the queueSubmitEnabled: false branch to match the test intent.

Current assertions cover Shift+Tab and disabled=true, but not the explicit disabled-by-feature-flag path.

♻️ Suggested test expansion
   it('emits queue-submit on Tab only when queue submit is available', async () => {
     const wrapper = await mountComponent()

     await wrapper.setProps({
       queueSubmitEnabled: true,
       queueSubmitDisabled: false
     })
     await wrapper.get('[data-testid="chat-input-editor"]').trigger('keydown', {
       key: 'Tab'
     })

     expect(wrapper.emitted('queue-submit')).toEqual([[]])

+    await wrapper.setProps({
+      queueSubmitEnabled: false,
+      queueSubmitDisabled: false
+    })
+    await wrapper.get('[data-testid="chat-input-editor"]').trigger('keydown', {
+      key: 'Tab'
+    })
+
+    expect(wrapper.emitted('queue-submit')).toEqual([[]])
+
     await wrapper.setProps({
-      queueSubmitDisabled: false
+      queueSubmitEnabled: true,
+      queueSubmitDisabled: false
     })
     await wrapper.get('[data-testid="chat-input-editor"]').trigger('keydown', {
       key: 'Tab',
       shiftKey: true
     })

     expect(wrapper.emitted('queue-submit')).toEqual([[]])

     await wrapper.setProps({
+      queueSubmitEnabled: true,
       queueSubmitDisabled: true
     })
     await wrapper.get('[data-testid="chat-input-editor"]').trigger('keydown', {
       key: 'Tab'
     })

     expect(wrapper.emitted('queue-submit')).toEqual([[]])
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/ChatInputBox.test.ts` around lines 282 - 313, The
test 'emits queue-submit on Tab only when queue submit is available' needs an
extra branch that sets queueSubmitEnabled: false and verifies no 'queue-submit'
emission; update the test to setProps({ queueSubmitEnabled: false,
queueSubmitDisabled: false }) (or just queueSubmitEnabled: false) then trigger
the Tab key on the '[data-testid="chat-input-editor"]' and assert
wrapper.emitted('queue-submit') remains unchanged, referencing the existing test
function name and the wrapper.get('[data-testid="chat-input-editor"]') trigger
to locate where to add this case.
src/main/presenter/agentSessionPresenter/index.ts (1)

744-794: Extract the shared send/steer preflight.

steerActiveTurn now duplicates most of sendMessage's draft promotion, ACP provider resolution, workdir sync, and title-generation setup. Pulling that into one helper would make the new routing behavior much easier to keep aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/agentSessionPresenter/index.ts` around lines 744 - 794,
steerActiveTurn duplicates sendMessage preflight (draft promotion, normalize
input, resolve agent/state/provider, acp workdir checks/sync, and
title-generation flag); extract a shared helper (e.g., prepareSendOrSteer) that
takes sessionId and content and returns { session, normalizedInput, agent,
state, providerId, wasDraft, hadMessages } after doing session lookup,
normalizeSendMessageInput, sessionManager.update promotion,
resolveAgentImplementation, agent.getSessionState/getMessages, providerId
fallback for 'acp', assertAcpSessionHasWorkdir and syncAcpSessionWorkdir; call
that helper from both steerActiveTurn and sendMessage and keep the same
generateSessionTitle invocation logic (use generateSessionTitle(sessionId,
session.title, providerId, state?.modelId ?? '') where appropriate).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/agentRuntimePresenter/index.ts`:
- Around line 424-463: The steer flow currently only triggers the steer-abort
handling in processMessage(), so when a streaming run started by
resumeAssistantMessage() is steered it is treated as a user_stop and never goes
through the consumeAbortSteerInput()/restart path, leaving
activeGenerationAbortReasons and steerInterruptInputs uncleared and losing the
steer payload; modify resumeAssistantMessage() (and the other similar resume
paths around the referenced ranges) to detect steer interrupts the same way
steerActiveTurn() does by calling enqueueSteerInterruptInput(sessionId,
normalizedInput) and routing the abort through consumeAbortSteerInput()/the
restart logic (instead of directly marking user_stop/cancel), ensure you set
activeGenerationAbortReasons.set(sessionId, 'steer') and abort the same
abortController used by activeGenerations.get(sessionId) so the existing
drain/cleanup of steerInterruptInputs and activeGenerationAbortReasons runs
consistently.

In `@src/main/presenter/agentSessionPresenter/index.ts`:
- Around line 728-731: The current interrupt path performs a heavy
getMessages(sessionId) before calling agent.steerActiveTurn, adding latency and
risk of rejecting interrupts on transient read failures; change the flow in the
block checking state?.status === 'generating' so it calls await
agent.steerActiveTurn(sessionId, normalizedInput) immediately (using the
existing sessionId/normalizedInput) and defer any full getMessages(sessionId) or
title-generation logic until after dispatch; alternatively, if a lightweight
check is needed before steering, call hasSessionMessages() or getMessageIds()
instead of getMessages() to avoid the full-history read; update the same pattern
in the adjacent region covering lines ~744-777 as well.

In `@src/renderer/src/components/chat/ChatInputBox.vue`:
- Around line 312-320: The handler currently treats plain Tab (isPlainTab) as
the "queue-submit" shortcut which blocks normal focus traversal; change the
shortcut detection so plain Tab is no longer intercepted and instead use a
non-Tab combo (for example Ctrl+Enter or Ctrl+Q). Update the condition that
checks isPlainTab to a new predicate (e.g., isQueueShortcut) that requires a
modifier (e.g., e.ctrlKey) and still respects
props.queueSubmitEnabled/props.queueSubmitDisabled and the mentions guards
(mentions.isSuggestionMenuOpen.value / mentions.shouldSuppressSubmit()), then
call emit('queue-submit') only when that new non-Tab shortcut is detected.
Ensure you remove e.preventDefault() for plain Tab so browser focus traversal
remains intact.

In `@src/renderer/src/pages/ChatPage.vue`:
- Around line 918-927: The queue path in onQueueSubmit currently calls
pendingInputStore.queueInput(props.sessionId, { text, files }) without source
metadata; update onQueueSubmit to pass an explicit source (e.g., include source:
'queue' on the input object) and then update pendingInputStore.queueInput and
the renderer→client forwarding to accept and forward that source through to
queuePendingInput(sessionId, input, options) (or the existing signature that
carries options.source) so the main-side handler can see options.source ===
'queue' instead of defaulting to 'send'. Ensure the symbol names to change are
onQueueSubmit, pendingInputStore.queueInput, and the code path that invokes
queuePendingInput/options.source so the source is preserved end-to-end.

In `@test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts`:
- Around line 582-595: The test is selecting the first user insert (which can be
the original prompt) instead of the steered user insert; update the call that
finds the insert in sqlitePresenter.deepchatMessagesTable.insert.mock.calls to
filter for the user row whose parsed content.text equals the steered text (e.g.,
'Refine before stream') so steeredUserInsert is selected reliably, and keep the
rest of the wait/assert logic (referencing steeredUserInsert and processStream)
unchanged.

---

Nitpick comments:
In `@src/main/presenter/agentSessionPresenter/index.ts`:
- Around line 744-794: steerActiveTurn duplicates sendMessage preflight (draft
promotion, normalize input, resolve agent/state/provider, acp workdir
checks/sync, and title-generation flag); extract a shared helper (e.g.,
prepareSendOrSteer) that takes sessionId and content and returns { session,
normalizedInput, agent, state, providerId, wasDraft, hadMessages } after doing
session lookup, normalizeSendMessageInput, sessionManager.update promotion,
resolveAgentImplementation, agent.getSessionState/getMessages, providerId
fallback for 'acp', assertAcpSessionHasWorkdir and syncAcpSessionWorkdir; call
that helper from both steerActiveTurn and sendMessage and keep the same
generateSessionTitle invocation logic (use generateSessionTitle(sessionId,
session.title, providerId, state?.modelId ?? '') where appropriate).

In `@test/main/routes/dispatcher.test.ts`:
- Around line 690-706: The test currently only asserts that
agentSessionPresenter.steerActiveTurn was called; capture the result returned by
dispatchDeepchatRoute for the 'chat.steerActiveTurn' call and add an assertion
that the response payload equals { accepted: true } to validate the route
contract. Locate the invocation of dispatchDeepchatRoute in the test and store
its return value, then add an expect(result).toEqual({ accepted: true }) (or
equivalent) in addition to the existing expect for
agentSessionPresenter.steerActiveTurn.

In `@test/renderer/components/ChatInputBox.test.ts`:
- Around line 282-313: The test 'emits queue-submit on Tab only when queue
submit is available' needs an extra branch that sets queueSubmitEnabled: false
and verifies no 'queue-submit' emission; update the test to setProps({
queueSubmitEnabled: false, queueSubmitDisabled: false }) (or just
queueSubmitEnabled: false) then trigger the Tab key on the
'[data-testid="chat-input-editor"]' and assert wrapper.emitted('queue-submit')
remains unchanged, referencing the existing test function name and the
wrapper.get('[data-testid="chat-input-editor"]') trigger to locate where to add
this case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90faec92-c455-4f41-a3fc-ee8eec35038e

📥 Commits

Reviewing files that changed from the base of the PR and between 18b985b and c6858c3.

📒 Files selected for processing (38)
  • docs/specs/active-input-routing/plan.md
  • docs/specs/active-input-routing/spec.md
  • docs/specs/active-input-routing/tasks.md
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/agentSessionPresenter/index.ts
  • src/main/routes/chat/chatService.ts
  • src/main/routes/hotPathPorts.ts
  • src/main/routes/index.ts
  • src/renderer/api/ChatClient.ts
  • src/renderer/src/components/chat/ChatInputBox.vue
  • src/renderer/src/components/chat/ChatInputToolbar.vue
  • src/renderer/src/components/chat/PendingInputLane.vue
  • src/renderer/src/i18n/da-DK/chat.json
  • src/renderer/src/i18n/en-US/chat.json
  • src/renderer/src/i18n/fa-IR/chat.json
  • src/renderer/src/i18n/fr-FR/chat.json
  • src/renderer/src/i18n/he-IL/chat.json
  • src/renderer/src/i18n/ja-JP/chat.json
  • src/renderer/src/i18n/ko-KR/chat.json
  • src/renderer/src/i18n/pt-BR/chat.json
  • src/renderer/src/i18n/ru-RU/chat.json
  • src/renderer/src/i18n/zh-CN/chat.json
  • src/renderer/src/i18n/zh-HK/chat.json
  • src/renderer/src/i18n/zh-TW/chat.json
  • src/renderer/src/pages/ChatPage.vue
  • src/shared/contracts/routes.ts
  • src/shared/contracts/routes/chat.routes.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/presenters/agent-session.presenter.d.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts
  • test/main/routes/chatService.test.ts
  • test/main/routes/contracts.test.ts
  • test/main/routes/dispatcher.test.ts
  • test/renderer/api/clients.test.ts
  • test/renderer/components/ChatInputBox.test.ts
  • test/renderer/components/ChatInputToolbar.test.ts
  • test/renderer/components/ChatPage.test.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/chat/PendingInputLane.vue

Comment on lines +424 to +463
async steerActiveTurn(sessionId: string, content: string | SendMessageInput): Promise<void> {
const state = await this.getSessionState(sessionId)
if (!state) {
throw new Error(`Session ${sessionId} not found`)
}
if (this.isAwaitingToolQuestionFollowUp(sessionId) || this.hasPendingInteractions(sessionId)) {
throw new Error('Please resolve pending tool interactions before steering.')
}

const normalizedInput = this.normalizeUserMessageInput(content)
if (!normalizedInput.text.trim() && (normalizedInput.files?.length ?? 0) === 0) {
return
}

const activeGeneration = this.activeGenerations.get(sessionId)
if (!activeGeneration) {
const preStreamController = this.abortControllers.get(sessionId)
if (state.status === 'generating' && preStreamController) {
this.enqueueSteerInterruptInput(sessionId, normalizedInput)
this.activeGenerationAbortReasons.set(sessionId, 'steer')
preStreamController.abort()
this.abortDeferredToolAbortControllers(sessionId)
this.clearActiveProviderPermissionsForSession(sessionId)
return
}

void this.processMessage(sessionId, normalizedInput, {
projectDir: this.resolveProjectDir(sessionId)
}).catch((error) => {
console.error('[AgentRuntime] Failed to process steer input:', error)
})
return
}

this.enqueueSteerInterruptInput(sessionId, normalizedInput)
this.activeGenerationAbortReasons.set(sessionId, 'steer')
activeGeneration.abortController.abort()
this.abortDeferredToolAbortControllers(sessionId)
this.clearActiveProviderPermissionsForSession(sessionId)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle steer-triggered aborts from resumed generations too.

This new steer flow is only completed in processMessage(). If the user steers while a run started by resumeAssistantMessage() is streaming, that path still treats the abort as user_stop, marks the assistant message canceled, and never drains activeGenerationAbortReasons / steerInterruptInputs. The steer payload can then be lost or leak into a later unrelated abort.

Please route resumeAssistantMessage() through the same consumeAbortSteerInput() / restart path.

Also applies to: 684-693, 721-751

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/agentRuntimePresenter/index.ts` around lines 424 - 463,
The steer flow currently only triggers the steer-abort handling in
processMessage(), so when a streaming run started by resumeAssistantMessage() is
steered it is treated as a user_stop and never goes through the
consumeAbortSteerInput()/restart path, leaving activeGenerationAbortReasons and
steerInterruptInputs uncleared and losing the steer payload; modify
resumeAssistantMessage() (and the other similar resume paths around the
referenced ranges) to detect steer interrupts the same way steerActiveTurn()
does by calling enqueueSteerInterruptInput(sessionId, normalizedInput) and
routing the abort through consumeAbortSteerInput()/the restart logic (instead of
directly marking user_stop/cancel), ensure you set
activeGenerationAbortReasons.set(sessionId, 'steer') and abort the same
abortController used by activeGenerations.get(sessionId) so the existing
drain/cleanup of steerInterruptInputs and activeGenerationAbortReasons runs
consistently.

Comment on lines +728 to +731
if (state?.status === 'generating' && agent.steerActiveTurn) {
await agent.steerActiveTurn(sessionId, normalizedInput)
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep steering off the full-history path.

These entry points now do a full getMessages(sessionId) read before they can call agent.steerActiveTurn, even though that lookup is only used later for title generation. That adds avoidable latency to the new interrupt path, and a transient history-read failure will now reject the send/steer request before it ever reaches the runtime. Please defer the first-message check until after dispatch, or reuse the lighter hasSessionMessages() / getMessageIds() path here.

Also applies to: 744-777

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/agentSessionPresenter/index.ts` around lines 728 - 731,
The current interrupt path performs a heavy getMessages(sessionId) before
calling agent.steerActiveTurn, adding latency and risk of rejecting interrupts
on transient read failures; change the flow in the block checking state?.status
=== 'generating' so it calls await agent.steerActiveTurn(sessionId,
normalizedInput) immediately (using the existing sessionId/normalizedInput) and
defer any full getMessages(sessionId) or title-generation logic until after
dispatch; alternatively, if a lightweight check is needed before steering, call
hasSessionMessages() or getMessageIds() instead of getMessages() to avoid the
full-history read; update the same pattern in the adjacent region covering lines
~744-777 as well.

Comment on lines +312 to +320
const isPlainTab = e.key === 'Tab' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey
if (isPlainTab && props.queueSubmitEnabled && !props.queueSubmitDisabled) {
if (mentions.isSuggestionMenuOpen.value || mentions.shouldSuppressSubmit()) {
return
}
e.preventDefault()
emit('queue-submit')
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep plain Tab for focus traversal.

Intercepting plain Tab here blocks normal keyboard navigation whenever streaming + draft input are active, so users can no longer tab out of the editor to the stop/queue controls or the rest of the page. Please move queueing to a non-Tab shortcut and leave plain Tab to the browser.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/chat/ChatInputBox.vue` around lines 312 - 320,
The handler currently treats plain Tab (isPlainTab) as the "queue-submit"
shortcut which blocks normal focus traversal; change the shortcut detection so
plain Tab is no longer intercepted and instead use a non-Tab combo (for example
Ctrl+Enter or Ctrl+Q). Update the condition that checks isPlainTab to a new
predicate (e.g., isQueueShortcut) that requires a modifier (e.g., e.ctrlKey) and
still respects props.queueSubmitEnabled/props.queueSubmitDisabled and the
mentions guards (mentions.isSuggestionMenuOpen.value /
mentions.shouldSuppressSubmit()), then call emit('queue-submit') only when that
new non-Tab shortcut is detected. Ensure you remove e.preventDefault() for plain
Tab so browser focus traversal remains intact.

Comment on lines +918 to 927
async function onQueueSubmit() {
if (isReadOnlySession.value) return
if (isAcpWorkdirMissing.value) return
if (activePendingInteraction.value || isHandlingInteraction.value) return
const text = message.value.trim()
const files = [...attachedFiles.value].map((f) => toRaw(f))
if (!text && files.length === 0) return
await pendingInputStore.queueInput(props.sessionId, { text, files })
message.value = ''
attachedFiles.value = []
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pass an explicit queue source here.

onQueueSubmit() currently goes through pendingInputStore.queueInput(...), and that store forwards to queuePendingInput(sessionId, input) without source metadata. On the main side, missing options.source is treated as 'send', so this “queue” action can be claimed immediately during tool follow-up instead of remaining an editable future turn.

Please plumb a dedicated queue source (for example, 'queue') through the renderer store/client for this path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/pages/ChatPage.vue` around lines 918 - 927, The queue path
in onQueueSubmit currently calls pendingInputStore.queueInput(props.sessionId, {
text, files }) without source metadata; update onQueueSubmit to pass an explicit
source (e.g., include source: 'queue' on the input object) and then update
pendingInputStore.queueInput and the renderer→client forwarding to accept and
forward that source through to queuePendingInput(sessionId, input, options) (or
the existing signature that carries options.source) so the main-side handler can
see options.source === 'queue' instead of defaulting to 'send'. Ensure the
symbol names to change are onQueueSubmit, pendingInputStore.queueInput, and the
code path that invokes queuePendingInput/options.source so the source is
preserved end-to-end.

Comment on lines +582 to +595
let steeredUserInsert: any = null
for (let attempt = 0; attempt < 20; attempt += 1) {
steeredUserInsert = sqlitePresenter.deepchatMessagesTable.insert.mock.calls.find(
([row]) => row.role === 'user'
)?.[0]
if (steeredUserInsert) {
break
}
await new Promise((resolve) => setTimeout(resolve, 0))
}

expect(steeredUserInsert).toBeTruthy()
expect(JSON.parse(steeredUserInsert.content).text).toBe('Refine before stream')
expect(processStream).toHaveBeenCalledTimes(1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Select the steered user insert, not the first user insert.

processMessage('First prompt') already inserts a user row, so .find(([row]) => row.role === 'user') will usually grab the original prompt instead of the steered one. That makes this assertion validate the wrong record and can fail even when steering works.

Suggested fix
       let steeredUserInsert: any = null
       for (let attempt = 0; attempt < 20; attempt += 1) {
-        steeredUserInsert = sqlitePresenter.deepchatMessagesTable.insert.mock.calls.find(
-          ([row]) => row.role === 'user'
-        )?.[0]
-        if (steeredUserInsert) {
+        const userInserts = sqlitePresenter.deepchatMessagesTable.insert.mock.calls
+          .map(([row]) => row)
+          .filter((row) => row.role === 'user')
+        steeredUserInsert = userInserts[userInserts.length - 1] ?? null
+        if (
+          steeredUserInsert &&
+          JSON.parse(steeredUserInsert.content).text === 'Refine before stream'
+        ) {
           break
         }
         await new Promise((resolve) => setTimeout(resolve, 0))
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let steeredUserInsert: any = null
for (let attempt = 0; attempt < 20; attempt += 1) {
steeredUserInsert = sqlitePresenter.deepchatMessagesTable.insert.mock.calls.find(
([row]) => row.role === 'user'
)?.[0]
if (steeredUserInsert) {
break
}
await new Promise((resolve) => setTimeout(resolve, 0))
}
expect(steeredUserInsert).toBeTruthy()
expect(JSON.parse(steeredUserInsert.content).text).toBe('Refine before stream')
expect(processStream).toHaveBeenCalledTimes(1)
let steeredUserInsert: any = null
for (let attempt = 0; attempt < 20; attempt += 1) {
const userInserts = sqlitePresenter.deepchatMessagesTable.insert.mock.calls
.map(([row]) => row)
.filter((row) => row.role === 'user')
steeredUserInsert = userInserts[userInserts.length - 1] ?? null
if (
steeredUserInsert &&
JSON.parse(steeredUserInsert.content).text === 'Refine before stream'
) {
break
}
await new Promise((resolve) => setTimeout(resolve, 0))
}
expect(steeredUserInsert).toBeTruthy()
expect(JSON.parse(steeredUserInsert.content).text).toBe('Refine before stream')
expect(processStream).toHaveBeenCalledTimes(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts`
around lines 582 - 595, The test is selecting the first user insert (which can
be the original prompt) instead of the steered user insert; update the call that
finds the insert in sqlitePresenter.deepchatMessagesTable.insert.mock.calls to
filter for the user row whose parsed content.text equals the steered text (e.g.,
'Refine before stream') so steeredUserInsert is selected reliably, and keep the
rest of the wait/assert logic (referencing steeredUserInsert and processStream)
unchanged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/renderer/components/ChatPage.test.ts (1)

287-311: ⚠️ Potential issue | 🟡 Minor

ChatInputToolbar mock doesn't render the steer button needed by the capacity test.

The mock template is a simple <div class="chat-input-toolbar-stub" />, but the test at line 718-719 expects to find [data-testid="chat-steer-button"]. This assertion will always fail because the mock doesn't render any internal elements.

Either update the mock to conditionally render a steer button element when isGenerating && hasInput, or change the test to verify steer availability through a different mechanism (e.g., checking that queueDisabled doesn't imply steer is disabled).

🔧 Suggested mock update
   vi.doMock('@/components/chat/ChatInputToolbar.vue', () => ({
     default: defineComponent({
       name: 'ChatInputToolbar',
       props: {
         isGenerating: {
           type: Boolean,
           default: false
         },
         hasInput: {
           type: Boolean,
           default: false
         },
         sendDisabled: {
           type: Boolean,
           default: false
         },
         queueDisabled: {
           type: Boolean,
           default: false
         }
       },
-      emits: ['attach', 'queue', 'send', 'stop'],
-      template: '<div class="chat-input-toolbar-stub" />'
+      emits: ['attach', 'queue', 'steer', 'send', 'stop'],
+      template: `<div class="chat-input-toolbar-stub">
+        <button v-if="isGenerating && hasInput" data-testid="chat-steer-button" `@click`="$emit('steer')" />
+      </div>`
     })
   }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/ChatPage.test.ts` around lines 287 - 311, The
ChatInputToolbar mock currently renders only a stub div so tests that query
[data-testid="chat-steer-button"] fail; update the mock (the defineComponent
used in vi.doMock for ChatInputToolbar) so its template conditionally includes a
button element with data-testid="chat-steer-button" when the props isGenerating
and hasInput are true (keep the existing props and emits), or alternatively
adjust the test to assert steer availability by inspecting the
queueDisabled/sendDisabled props rather than querying the DOM; implementing the
conditional button in the ChatInputToolbar mock is the recommended fix.
♻️ Duplicate comments (2)
src/main/presenter/agentRuntimePresenter/index.ts (1)

688-696: ⚠️ Potential issue | 🔴 Critical

Handle steer aborts in resumeAssistantMessage() too.

The new steer restart path is only wired into processMessage(). A steer issued while a stream resumed by resumeAssistantMessage() is running still falls through the old user_stop path, which cancels the assistant message and leaves the buffered steer input behind for a later abort. Please route resumed runs through the same consumeAbortSteerInput() / restart flow.

Also applies to: 725-755

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/agentRuntimePresenter/index.ts` around lines 688 - 696,
resumeAssistantMessage() currently leaves steer-abort inputs unhandled and falls
back to the old user_stop path; update resumeAssistantMessage() to mirror the
steer-abort restart flow used in processMessage(): call
consumeAbortSteerInput(sessionId) at the start of resumed runs, and if it
returns a steerInput, invoke settleSteerInterruptedAssistant(sessionId,
assistantMessageId), setSessionStatus(sessionId, 'idle'), ensure
clearActiveGeneration(sessionId, runId) is called in a finally block, and then
call continueWithSteerInput(sessionId, steerInput, projectDir) so resumed
streams are restarted the same way as in processMessage().
test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts (1)

592-600: ⚠️ Potential issue | 🟠 Major

Select the steered user insert, not the first user insert.

At Line 594, .find(([row]) => row.role === 'user') can grab the original 'First prompt' row, so Line 604 may assert against the wrong record.

Proposed fix
       let steeredUserInsert: any = null
       for (let attempt = 0; attempt < 20; attempt += 1) {
-        steeredUserInsert = sqlitePresenter.deepchatMessagesTable.insert.mock.calls.find(
-          ([row]) => row.role === 'user'
-        )?.[0]
+        const userInserts = sqlitePresenter.deepchatMessagesTable.insert.mock.calls
+          .map(([row]) => row)
+          .filter((row) => row.role === 'user')
+        steeredUserInsert =
+          userInserts.find((row) => JSON.parse(row.content).text === 'Refine before stream') ?? null
         if (steeredUserInsert) {
           break
         }
         await new Promise((resolve) => setTimeout(resolve, 0))
       }

Also applies to: 603-604

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts`
around lines 592 - 600, The test currently picks the first user insert via
sqlitePresenter.deepchatMessagesTable.insert.mock.calls.find(([row]) => row.role
=== 'user'), which may return the original "First prompt" row; update the
predicate used in that find to specifically identify the steered user insert
(for example check row.role === 'user' && (row.metadata?.steered === true ||
row.content !== 'First prompt' or another unique marker produced by the steered
flow)) so steeredUserInsert references the actual steered record; adjust the
similar selection later in the test the same way.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/agentRuntimePresenter/index.ts`:
- Around line 688-696: The abort path currently calls
rollbackClaimedQueueInputTurn(...) before handling steer restarts, causing
queued inputs to be requeued for steer-triggered aborts; change the control flow
in the abort handling around consumeAbortSteerInput(sessionId) so that if
consumeAbortSteerInput returns a steerInput you do NOT call
rollbackClaimedQueueInputTurn or requeue the pendingQueueItem (treat it as
consumed), but still call settleSteerInterruptedAssistant(sessionId,
assistantMessageId), setSessionStatus(sessionId, 'idle'),
clearActiveGeneration(sessionId, runId), and then
continueWithSteerInput(sessionId, steerInput, projectDir); ensure the same
adjustment is applied to the other abort block(s) referenced (the 709-755
region) so steer-triggered aborts skip rollbackClaimedQueueInputTurn.

In `@src/main/presenter/agentSessionPresenter/index.ts`:
- Around line 792-794: The presenter currently silently returns when
agent.steerActiveTurn is missing; change it to fail fast by checking for the
capability and throwing the same unsupported-operation error used by other
capability-gated entry points instead of returning. In the method containing the
agent.steerActiveTurn call, replace the no-op path with an explicit throw (e.g.,
throw new Error or the project’s UnsupportedOperationError) with a clear message
like "Agent does not support steering", and only call await
agent.steerActiveTurn(sessionId, normalizedInput) when the property exists.

In `@src/renderer/src/pages/ChatPage.vue`:
- Around line 894-898: The queued-input code path is missing source metadata
when calling pendingInputStore.queueInput from onSubmit and onCommandSubmit
while isGenerating is true, causing queued items to be claimed immediately;
update both onSubmit and onCommandSubmit to pass the same source options you use
for chatClient.sendMessage (or create a small helper e.g.,
queueOrSend(sessionId, payload, source)) so that
pendingInputStore.queueInput(sessionId, { text, files, source:
<appropriateSource> }) is always called, or alternatively extend
pendingInputStore.queueInput to accept and forward a source option; reference
onSubmit, onCommandSubmit, pendingInputStore.queueInput, isGenerating and
chatClient.sendMessage when making the change.

---

Outside diff comments:
In `@test/renderer/components/ChatPage.test.ts`:
- Around line 287-311: The ChatInputToolbar mock currently renders only a stub
div so tests that query [data-testid="chat-steer-button"] fail; update the mock
(the defineComponent used in vi.doMock for ChatInputToolbar) so its template
conditionally includes a button element with data-testid="chat-steer-button"
when the props isGenerating and hasInput are true (keep the existing props and
emits), or alternatively adjust the test to assert steer availability by
inspecting the queueDisabled/sendDisabled props rather than querying the DOM;
implementing the conditional button in the ChatInputToolbar mock is the
recommended fix.

---

Duplicate comments:
In `@src/main/presenter/agentRuntimePresenter/index.ts`:
- Around line 688-696: resumeAssistantMessage() currently leaves steer-abort
inputs unhandled and falls back to the old user_stop path; update
resumeAssistantMessage() to mirror the steer-abort restart flow used in
processMessage(): call consumeAbortSteerInput(sessionId) at the start of resumed
runs, and if it returns a steerInput, invoke
settleSteerInterruptedAssistant(sessionId, assistantMessageId),
setSessionStatus(sessionId, 'idle'), ensure clearActiveGeneration(sessionId,
runId) is called in a finally block, and then call
continueWithSteerInput(sessionId, steerInput, projectDir) so resumed streams are
restarted the same way as in processMessage().

In `@test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts`:
- Around line 592-600: The test currently picks the first user insert via
sqlitePresenter.deepchatMessagesTable.insert.mock.calls.find(([row]) => row.role
=== 'user'), which may return the original "First prompt" row; update the
predicate used in that find to specifically identify the steered user insert
(for example check row.role === 'user' && (row.metadata?.steered === true ||
row.content !== 'First prompt' or another unique marker produced by the steered
flow)) so steeredUserInsert references the actual steered record; adjust the
similar selection later in the test the same way.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b92f4634-e2fc-44b5-a2ff-0a3b14c0f3f8

📥 Commits

Reviewing files that changed from the base of the PR and between c6858c3 and 33f789d.

📒 Files selected for processing (9)
  • src/main/presenter/agentRuntimePresenter/index.ts
  • src/main/presenter/agentSessionPresenter/index.ts
  • src/renderer/src/components/chat/ChatInputToolbar.vue
  • src/renderer/src/pages/ChatPage.vue
  • src/shared/types/agent-interface.d.ts
  • test/main/presenter/agentRuntimePresenter/agentRuntimePresenter.test.ts
  • test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts
  • test/renderer/components/ChatInputToolbar.test.ts
  • test/renderer/components/ChatPage.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/shared/types/agent-interface.d.ts
  • test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts

Comment on lines +688 to +696
const steerInput = result.status === 'aborted' ? this.consumeAbortSteerInput(sessionId) : null
if (steerInput) {
try {
this.settleSteerInterruptedAssistant(sessionId, assistantMessageId)
this.setSessionStatus(sessionId, 'idle')
} finally {
this.clearActiveGeneration(sessionId, runId)
}
this.continueWithSteerInput(sessionId, steerInput, projectDir)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not rollback claimed queue items when the abort reason is steer.

For immediately claimed queued inputs (pendingQueueItemSource === 'queue'), the aborted path rolls the item back before the steer restart is checked. That deletes the active user turn and re-queues the old draft, so after the steer restart the original queued input can resurface and run later out of order. Treat steer-triggered aborts as a consumed turn instead of calling rollbackClaimedQueueInputTurn(...).

Also applies to: 709-755

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/agentRuntimePresenter/index.ts` around lines 688 - 696,
The abort path currently calls rollbackClaimedQueueInputTurn(...) before
handling steer restarts, causing queued inputs to be requeued for
steer-triggered aborts; change the control flow in the abort handling around
consumeAbortSteerInput(sessionId) so that if consumeAbortSteerInput returns a
steerInput you do NOT call rollbackClaimedQueueInputTurn or requeue the
pendingQueueItem (treat it as consumed), but still call
settleSteerInterruptedAssistant(sessionId, assistantMessageId),
setSessionStatus(sessionId, 'idle'), clearActiveGeneration(sessionId, runId),
and then continueWithSteerInput(sessionId, steerInput, projectDir); ensure the
same adjustment is applied to the other abort block(s) referenced (the 709-755
region) so steer-triggered aborts skip rollbackClaimedQueueInputTurn.

Comment on lines +792 to +794
if (agent.steerActiveTurn) {
await agent.steerActiveTurn(sessionId, normalizedInput)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fail fast when an agent does not support steering.

This method currently returns successfully when agent.steerActiveTurn is absent, so the caller gets no signal that the steer was dropped. The other capability-gated entry points in this presenter throw explicit unsupported-operation errors; this should do the same.

Suggested fix
-    if (agent.steerActiveTurn) {
-      await agent.steerActiveTurn(sessionId, normalizedInput)
-    }
+    if (!agent.steerActiveTurn) {
+      throw new Error(`Agent ${session.agentId} does not support active-turn steering.`)
+    }
+
+    await agent.steerActiveTurn(sessionId, normalizedInput)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/agentSessionPresenter/index.ts` around lines 792 - 794,
The presenter currently silently returns when agent.steerActiveTurn is missing;
change it to fail fast by checking for the capability and throwing the same
unsupported-operation error used by other capability-gated entry points instead
of returning. In the method containing the agent.steerActiveTurn call, replace
the no-op path with an explicit throw (e.g., throw new Error or the project’s
UnsupportedOperationError) with a clear message like "Agent does not support
steering", and only call await agent.steerActiveTurn(sessionId, normalizedInput)
when the property exists.

Comment on lines +894 to +898
if (isGenerating.value) {
await pendingInputStore.queueInput(props.sessionId, { text, files })
} else {
await chatClient.sendMessage(props.sessionId, { text, files })
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same missing source metadata issue in onSubmit and onCommandSubmit queue paths.

Both onSubmit (line 895) and onCommandSubmit (line 912) now route to pendingInputStore.queueInput(...) when isGenerating is true, but neither passes source metadata. This has the same implication as onQueueSubmit — the queued input may be claimed immediately rather than remaining editable.

Consider consolidating these queue calls through a helper that always passes the appropriate source, or update pendingInputStore.queueInput to accept and forward source options.

Also applies to: 911-916

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/pages/ChatPage.vue` around lines 894 - 898, The queued-input
code path is missing source metadata when calling pendingInputStore.queueInput
from onSubmit and onCommandSubmit while isGenerating is true, causing queued
items to be claimed immediately; update both onSubmit and onCommandSubmit to
pass the same source options you use for chatClient.sendMessage (or create a
small helper e.g., queueOrSend(sessionId, payload, source)) so that
pendingInputStore.queueInput(sessionId, { text, files, source:
<appropriateSource> }) is always called, or alternatively extend
pendingInputStore.queueInput to accept and forward a source option; reference
onSubmit, onCommandSubmit, pendingInputStore.queueInput, isGenerating and
chatClient.sendMessage when making the change.

@zerob13 zerob13 merged commit 7404160 into dev Apr 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants