diff --git a/apps/desktop/src/app/chat/composer/focus.test.ts b/apps/desktop/src/app/chat/composer/focus.test.ts
new file mode 100644
index 00000000000..f3987982274
--- /dev/null
+++ b/apps/desktop/src/app/chat/composer/focus.test.ts
@@ -0,0 +1,50 @@
+import { afterEach, describe, expect, it } from 'vitest'
+
+import { blurComposerInput } from './focus'
+import { RICH_INPUT_SLOT } from './rich-editor'
+
+/**
+ * Inactive tabs keep their composer mounted, so an unscoped lookup can blur a
+ * background input and leave the one the user is typing in focused.
+ */
+
+/** A composer input inside its own pane layer, hidden or not. */
+function mountInput(hidden = false) {
+ const layer = document.createElement('div')
+ const input = document.createElement('div')
+ input.dataset.slot = RICH_INPUT_SLOT
+ input.tabIndex = 0
+ layer.toggleAttribute('data-pane-hidden', hidden)
+ layer.append(input)
+ document.body.append(layer)
+
+ return input
+}
+
+afterEach(() => {
+ document.body.innerHTML = ''
+})
+
+describe('blurComposerInput', () => {
+ it('blurs the foreground composer while a hidden tab matches first', () => {
+ const background = mountInput(true)
+ const foreground = mountInput()
+
+ foreground.focus()
+ blurComposerInput()
+
+ expect(document.activeElement).not.toBe(foreground)
+ expect(document.activeElement).not.toBe(background)
+ })
+
+ it('leaves focus alone when the composer does not hold it', () => {
+ const outside = document.createElement('button')
+ document.body.append(outside)
+ mountInput()
+
+ outside.focus()
+ blurComposerInput()
+
+ expect(document.activeElement).toBe(outside)
+ })
+})
diff --git a/apps/desktop/src/app/chat/session-drag.test.ts b/apps/desktop/src/app/chat/session-drag.test.ts
new file mode 100644
index 00000000000..5e4b7955ee5
--- /dev/null
+++ b/apps/desktop/src/app/chat/session-drag.test.ts
@@ -0,0 +1,113 @@
+import type { PointerEvent as ReactPointerEvent } from 'react'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { group } from '@/components/pane-shell/tree/model'
+import { $layoutTree } from '@/components/pane-shell/tree/store'
+import { openSessionTile } from '@/store/session-states'
+
+import { requestComposerInsertRefs } from './composer/focus'
+import { startSessionDrag } from './session-drag'
+
+/**
+ * A session drop resolves its target by rect-testing the chat surfaces in the
+ * document. A tab group keeps inactive tabs MOUNTED with their layout box
+ * intact, so a background tab's rect is identical to the foreground tab's —
+ * the drop has to land on the tab the user can actually see.
+ */
+
+vi.mock('@/store/session-states', () => ({ openSessionTile: vi.fn() }))
+vi.mock('./composer/focus', () => ({ requestComposerInsertRefs: vi.fn() }))
+
+const ZONE = { left: 0, top: 0, right: 1000, bottom: 800 }
+const COMPOSER = { left: 100, top: 700, right: 900, bottom: 780 }
+
+const stubRect = (el: Element, box: { left: number; top: number; right: number; bottom: number }) => {
+ el.getBoundingClientRect = () =>
+ ({ ...box, width: box.right - box.left, height: box.bottom - box.top, x: box.left, y: box.top }) as DOMRect
+}
+
+/** The workspace tab kept alive behind an active session tile tab. */
+function mountStackedTabs() {
+ document.body.innerHTML = `
+
+
+ `
+
+ stubRect(document.querySelector('[data-tree-group]')!, ZONE)
+
+ for (const surface of document.querySelectorAll('[data-session-anchor]')) {
+ stubRect(surface, ZONE)
+ }
+
+ for (const composer of document.querySelectorAll('[data-slot="composer-root"]')) {
+ stubRect(composer, COMPOSER)
+ }
+
+ $layoutTree.set(group(['workspace', 'session-tile:visible'], { id: 'g1' }))
+
+ return document.getElementById('row')!
+}
+
+/** Press on `source`, drag to (x, y), release. The drag session flushes its
+ * pending move synchronously on release, so no frame wait is needed. */
+function dragTo(source: HTMLElement, x: number, y: number) {
+ startSessionDrag({ id: 'dragged', profile: 'default', title: 'Dragged chat' }, {
+ button: 0,
+ clientX: 0,
+ clientY: 0,
+ currentTarget: source,
+ pointerId: 1
+ } as unknown as ReactPointerEvent)
+
+ window.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: x, clientY: y }))
+ window.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, clientX: x, clientY: y }))
+}
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+afterEach(() => {
+ document.body.innerHTML = ''
+ $layoutTree.set(null)
+})
+
+describe('session drop targeting across stacked tabs', () => {
+ it('links into the visible tab’s composer, not the tab kept alive behind it', () => {
+ const row = mountStackedTabs()
+
+ dragTo(row, 500, 740)
+
+ expect(requestComposerInsertRefs).toHaveBeenCalledWith(expect.anything(), { target: 'tile:visible' })
+ })
+
+ it('docks a split against the visible tab’s pane', () => {
+ const row = mountStackedTabs()
+
+ dragTo(row, 980, 400)
+
+ expect(openSessionTile).toHaveBeenCalledWith('dragged', 'right', 'session-tile:visible', undefined)
+ expect(requestComposerInsertRefs).not.toHaveBeenCalled()
+ })
+
+ it('commits nothing over a zone that hosts no chat surface', () => {
+ mountStackedTabs()
+ $layoutTree.set(group(['terminal'], { id: 'g1' }))
+
+ dragTo(document.getElementById('row')!, 500, 740)
+
+ expect(requestComposerInsertRefs).not.toHaveBeenCalled()
+ expect(openSessionTile).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/desktop/src/components/assistant-ui/thread/timeline.test.ts b/apps/desktop/src/components/assistant-ui/thread/timeline.test.ts
new file mode 100644
index 00000000000..ced51f60f19
--- /dev/null
+++ b/apps/desktop/src/components/assistant-ui/thread/timeline.test.ts
@@ -0,0 +1,42 @@
+import { afterEach, describe, expect, it } from 'vitest'
+
+import { ownViewport } from './timeline'
+
+/**
+ * Several chat surfaces are mounted at once — side by side in a split, and
+ * stacked as kept-alive inactive tabs. A timeline scrolls its OWN thread.
+ */
+
+afterEach(() => {
+ document.body.innerHTML = ''
+})
+
+const surface = (id: string, hidden = false) => `
+
+`
+
+describe('ownViewport', () => {
+ it('resolves the viewport of the surface the timeline lives in', () => {
+ document.body.innerHTML = surface('workspace') + surface('session-tile:b')
+
+ expect(ownViewport(document.getElementById('timeline-session-tile:b'))?.id).toBe('viewport-session-tile:b')
+ expect(ownViewport(document.getElementById('timeline-workspace'))?.id).toBe('viewport-workspace')
+ })
+
+ it('ignores a kept-alive tab that matches first', () => {
+ document.body.innerHTML = surface('workspace', true) + surface('session-tile:b')
+
+ expect(ownViewport(document.getElementById('timeline-session-tile:b'))?.id).toBe('viewport-session-tile:b')
+ })
+
+ it('falls back to the document when there is no surface around it', () => {
+ document.body.innerHTML = ''
+
+ expect(ownViewport(null)?.id).toBe('viewport-lone')
+ })
+})
diff --git a/apps/desktop/src/components/pane-shell/pane-visibility.test.ts b/apps/desktop/src/components/pane-shell/pane-visibility.test.ts
new file mode 100644
index 00000000000..030aa2d8ec6
--- /dev/null
+++ b/apps/desktop/src/components/pane-shell/pane-visibility.test.ts
@@ -0,0 +1,41 @@
+import { afterEach, describe, expect, it } from 'vitest'
+
+import { hiddenPaneProps, PANE_HIDDEN_ATTR, queryAllVisible, queryVisible } from './pane-visibility'
+
+/**
+ * Inactive tabs stay mounted with their layout box intact, so they answer
+ * document-wide lookups exactly like the visible tab. These helpers are the one
+ * place that difference is decided.
+ */
+
+const COMPOSER = '[data-slot="composer-root"]'
+
+const tab = (id: string, hidden = false) => `
+
+`
+
+afterEach(() => {
+ document.body.innerHTML = ''
+})
+
+describe('pane visibility lookups', () => {
+ it('resolves the foreground element even when a hidden tab matches first', () => {
+ document.body.innerHTML = tab('background', true) + tab('foreground')
+
+ expect(queryVisible(COMPOSER)?.id).toBe('foreground')
+ expect(queryAllVisible(COMPOSER).map(el => el.id)).toEqual(['foreground'])
+ })
+
+ it('answers normally when nothing is hidden', () => {
+ document.body.innerHTML = tab('only')
+
+ expect(queryVisible(COMPOSER)?.id).toBe('only')
+ })
+
+ it('marks a pane hidden only while it is inactive', () => {
+ expect(hiddenPaneProps(true)).toEqual({ [PANE_HIDDEN_ATTR]: '' })
+ expect(hiddenPaneProps(false)).toEqual({})
+ })
+})
diff --git a/apps/desktop/src/lib/keybinds/composer-focus-keys.test.ts b/apps/desktop/src/lib/keybinds/composer-focus-keys.test.ts
index cebf54950e4..7594a12b06d 100644
--- a/apps/desktop/src/lib/keybinds/composer-focus-keys.test.ts
+++ b/apps/desktop/src/lib/keybinds/composer-focus-keys.test.ts
@@ -88,6 +88,17 @@ describe('composerFocusBlockedBySurface', () => {
expect(composerFocusBlockedBySurface()).toBe(true)
})
+ it('ignores a clarify card waiting in a kept-alive background tab', () => {
+ const tab = document.createElement('div')
+ tab.setAttribute('data-pane-hidden', '')
+ const card = document.createElement('div')
+ card.setAttribute('data-clarify-choices', '')
+ tab.append(card)
+ document.body.append(tab)
+
+ expect(composerFocusBlockedBySurface()).toBe(false)
+ })
+
it('blocks when focus is inside a terminal', () => {
const term = document.createElement('div')
term.setAttribute('data-terminal', '')