diff --git a/ui-tui-opentui-v2/src/logic/store.ts b/ui-tui-opentui-v2/src/logic/store.ts
index 1ce8a7ae25d..ade5cd387f2 100644
--- a/ui-tui-opentui-v2/src/logic/store.ts
+++ b/ui-tui-opentui-v2/src/logic/store.ts
@@ -123,6 +123,9 @@ export interface StoreState {
subagents: SubagentInfo[]
/** Whether the agents dashboard overlay is open (/agents). */
dashboard: boolean
+ /** Transient busy indicator (the kaomoji face/verb from `thinking.delta`/`status.update`);
+ * shown above the composer WHILE a turn runs, cleared on `message.complete`. NOT transcript. */
+ status: string | undefined
}
const LRU_LIMIT = 1000
@@ -159,7 +162,8 @@ export function createSessionStore() {
picker: undefined,
completions: undefined,
subagents: [],
- dashboard: false
+ dashboard: false,
+ status: undefined
})
// Monotonic part id (stable `key` per part so a new tool part below a streaming
@@ -319,6 +323,7 @@ export function createSessionStore() {
setSkin(event.payload)
break
case 'message.start':
+ setState('status', undefined)
setState(
produce(draft => {
draft.messages.push({ role: 'assistant', text: '', parts: [], streaming: true })
@@ -349,9 +354,19 @@ export function createSessionStore() {
live.streaming = false
})
)
+ setState('status', undefined)
break
- case 'reasoning.delta':
- case 'thinking.delta': {
+ // thinking.delta / status.update are the TRANSIENT busy indicator (kaomoji
+ // face/verb) — route them to the status line, NOT the transcript (gotcha: Ink
+ // shows these as a FaceTicker, not message content).
+ case 'thinking.delta':
+ case 'status.update': {
+ const text = event.payload?.text ?? ''
+ if (text) setState('status', text)
+ break
+ }
+ // reasoning.delta is the model's actual reasoning — a (dim) transcript part.
+ case 'reasoning.delta': {
const text = event.payload?.text ?? ''
if (!text) break
setState(
diff --git a/ui-tui-opentui-v2/src/test/store.test.ts b/ui-tui-opentui-v2/src/test/store.test.ts
index 3249dd2fb6f..89f16375d5a 100644
--- a/ui-tui-opentui-v2/src/test/store.test.ts
+++ b/ui-tui-opentui-v2/src/test/store.test.ts
@@ -118,6 +118,25 @@ describe('session store — ordered parts (Phase 2b)', () => {
const parts = store.state.messages.at(-1)!.parts ?? []
expect(parts[0]).toMatchObject({ type: 'reasoning', text: 'thinking hard' })
})
+
+ test('thinking.delta (kaomoji face) → transient status, NOT a transcript part; complete clears it', () => {
+ const store = createSessionStore()
+ store.apply({ type: 'message.start' })
+ store.apply({ type: 'thinking.delta', payload: { text: '(´・_・`) formulating...' } })
+ expect(store.state.status).toBe('(´・_・`) formulating...')
+ expect(store.state.messages.at(-1)!.parts ?? []).toHaveLength(0) // no reasoning row from the face
+ store.apply({ type: 'message.delta', payload: { text: 'Hi!' } })
+ store.apply({ type: 'message.complete' })
+ expect(store.state.status).toBeUndefined() // cleared when the turn ends
+ // only the real reply text part remains — the face never entered the transcript
+ expect((store.state.messages.at(-1)!.parts ?? []).map(p => p.type)).toEqual(['text'])
+ })
+
+ test('status.update also drives the transient status line', () => {
+ const store = createSessionStore()
+ store.apply({ type: 'status.update', payload: { kind: 'tool', text: 'running terminal…' } })
+ expect(store.state.status).toBe('running terminal…')
+ })
})
describe('session store — blocking prompts (Phase 3)', () => {
diff --git a/ui-tui-opentui-v2/src/view/App.tsx b/ui-tui-opentui-v2/src/view/App.tsx
index 43829510465..4dd4468b97d 100644
--- a/ui-tui-opentui-v2/src/view/App.tsx
+++ b/ui-tui-opentui-v2/src/view/App.tsx
@@ -23,6 +23,7 @@ import { Pager } from './overlays/pager.tsx'
import { Picker } from './overlays/picker.tsx'
import { SessionSwitcher } from './overlays/sessionSwitcher.tsx'
import { PromptOverlay } from './prompts/promptOverlay.tsx'
+import { StatusLine } from './statusLine.tsx'
import { Transcript } from './transcript.tsx'
export interface AppProps {
@@ -64,6 +65,7 @@ export function App(props: AppProps) {
fallback={
<>
+
+ {status => (
+
+
+ {status()}
+
+
+ )}
+
+ )
+}