Merge origin/master into feat/tui-package

Master unifies every live agent with its exact SessionId and moves declarative startup failures to agent-loop/config-start-failed. Keeping the branch’s AgentId label binding would let the TUI target the wrong lifecycle after reload and would miss asynchronous resume failures.

Resolve that contract migration by giving the selected terminal front door the same generated or resumed SessionId as agent-core, mounting the front door first, and entering fullscreen only after the matching root appears. Refresh the source-derived catalogs and keyless terminal goldens so Code Mode, workflow, Cordis-tool, and transient UI scenarios all exercise the merged identity model.
This commit is contained in:
Tianyi Cui
2026-07-19 11:37:14 +08:00
280 changed files with 4311 additions and 4148 deletions

View File

@@ -1,6 +1,6 @@
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { AgentId, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
@@ -58,8 +58,9 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
} else {
await options.configureContext(ctx)
}
const sessionId = SessionId('main-session')
const session = ctx.sessions.create(
SessionId('main-session'),
sessionId,
options.cwd === null ? undefined : { meta: { cwd: options.cwd ?? '/workspace' } },
)
options.beforeMount?.(session)
@@ -67,7 +68,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
const steered: ContentBlock[][] = []
const cancelled: string[] = []
const agent: FakeAgent = {
id: AgentId('main'),
id: sessionId,
options: { model: 'deepseek-v4-flash' },
session,
status: options.status ?? 'idle',
@@ -92,7 +93,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
ctx.agents.register(agent)
const controller = createTuiChat(ctx, Object.assign({
welcome: 'Coding agent ready.',
agent: 'main',
sessionId,
color: false,
}, options.config), { terminal, exit })
return { ctx, session, agent, terminal, exit, controller }

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 99-99 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 99-99 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue

View File

@@ -14,9 +14,9 @@ viewport
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 91-91 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 91-91 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-91 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 95-95 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 95-95 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-95 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 91-91 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 91-91 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-91 fg=bright-blue

View File

@@ -14,9 +14,9 @@ viewport
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 55-55 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 55-55 fg=bright-blue
4| "╰───╭ Coverage ────────────────────────────────────╮───╯"
style 0-55 fg=bright-blue

View File

@@ -14,9 +14,9 @@ viewport
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 55-55 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 55-55 fg=bright-blue
4| "╰──────────────────────────────────────────────────────╯"
style 0-55 fg=bright-blue

View File

@@ -1,7 +1,7 @@
terminal 44x18 buffer=normal length=18 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=12 bufferRow=12
cursor hidden column=1 viewportRow=11 bufferRow=11
buffer
0| "╭──────────────────────────────────────────╮"
style 0-43 fg=bright-blue
@@ -14,32 +14,28 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 43-43 fg=bright-blue
3| "│ main • deepseek-v4-flash • │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-42 dim
style 43-43 fg=bright-blue dim
4| "│ main-session │"
style 0-0 fg=bright-blue
style 2-13 dim
style 2-35 dim
style 43-43 fg=bright-blue
5| "╰──────────────────────────────────────────╯"
4| "╰──────────────────────────────────────────╯"
style 0-43 fg=bright-blue
6| <blank>
7| " Context · compact "
5| <blank>
6| " Context · compact "
style 1-17 dim
8| " Compacted summary: the prior command "
7| " Compacted summary: the prior command "
style 1-43 fg=bright-black
9| " completed and its details were retired "
8| " completed and its details were retired "
style 1-43 fg=bright-black
10| " from the active surface. "
9| " from the active surface. "
style 1-24 fg=bright-black
11| "────────────────────────────────────────────"
10| "────────────────────────────────────────────"
style 0-43 dim
12| " "
11| " "
style 1-1 inverse
13| "────────────────────────────────────────────"
12| "────────────────────────────────────────────"
style 0-43 dim
14| "/workspace/project ↑0 ↓0 idle reasoning:o"
13| "/workspace/project ↑0 ↓0 idle reasoning:o"
style 0-24 dim
style 27-43 dim
15-17| <blank>
14-17| <blank>

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 103-103 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 103-103 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-103 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-22 fg=bright-black
style 79-79 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 79-79 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────╯"
style 0-79 fg=bright-blue

View File

@@ -14,9 +14,9 @@ buffer
style 0-0 fg=bright-blue
style 2-61 fg=bright-black
style 99-99 fg=bright-blue
3| "│ main • deepseek-v4-flash • main-session │"
3| "│ deepseek-v4-flash • main-session │"
style 0-0 fg=bright-blue
style 2-44 dim
style 2-35 dim
style 99-99 fg=bright-blue
4| "╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-99 fg=bright-blue

View File

@@ -3,7 +3,7 @@ import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { Terminal } from '@earendil-works/pi-tui'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
@@ -441,7 +441,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
const events = await setup()
const unrelatedSession = events.ctx.sessions.create(SessionId('unrelated-session'))
const unrelatedAgent = { ...events.agent, id: AgentId('unrelated'), session: unrelatedSession }
const unrelatedAgent = { ...events.agent, id: unrelatedSession.id, session: unrelatedSession }
unrelatedSession.append('todo/write', { todos: [{ content: 'hidden', status: 'pending' }] })
events.ctx.emit('agent/status', unrelatedAgent, 'running')
events.ctx.emit('agent/error', unrelatedAgent, 1, 1, new Error('hidden error'))
@@ -810,9 +810,9 @@ describe('terminal mounting', () => {
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const session = ctx.sessions.create(SessionId('mounted-session'))
const session = ctx.sessions.create(SessionId('main'))
ctx.agents.register({
id: AgentId('main'), options: {}, session, status: 'idle', ctx,
id: session.id, options: {}, session, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
@@ -829,19 +829,19 @@ describe('terminal mounting', () => {
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const terminal = new FakeTerminal()
mountTui(ctx, { agent: 'main', color: false }, { terminal, exit: vi.fn() })
mountTui(ctx, { sessionId: 'late-session', color: false }, { terminal, exit: vi.fn() })
expect(terminal.started).toBe(0)
const otherSession = ctx.sessions.create(SessionId('other-session'))
ctx.agents.register({
id: AgentId('other'), options: {}, session: otherSession, status: 'idle', ctx,
id: otherSession.id, options: {}, session: otherSession, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
expect(terminal.started).toBe(0)
const session = ctx.sessions.create(SessionId('late-session'))
const agent = {
id: AgentId('main'), options: {}, session, status: 'idle', ctx,
id: session.id, options: {}, session, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
} as Agent
ctx.agents.register(agent)
@@ -858,18 +858,18 @@ describe('terminal mounting', () => {
ctx.provide('tools', { get: () => undefined } as never)
const terminal = new FakeTerminal()
const exit = vi.fn()
mountTui(ctx, { agent: 'main', color: false }, { terminal, exit })
mountTui(ctx, { sessionId: 'main-session', color: false }, { terminal, exit })
ctx.agents.reportStartFailure(AgentId('other'), new Error('other failed'))
ctx.emit('agent-loop/config-start-failed', SessionId('other-session'), new Error('other failed'))
expect(terminal.output).toBe('')
expect(exit).not.toHaveBeenCalled()
ctx.agents.reportStartFailure(AgentId('main'), new Error('resume \u001b]2;failure-controlled\u0007'))
expect(terminal.output).toBe('ui-tui: agent "main" failed to start: resume \\x1b]2;failure-controlled\\x07\n')
ctx.emit('agent-loop/config-start-failed', SessionId('main-session'), new Error('resume \u001b]2;failure-controlled\u0007'))
expect(terminal.output).toBe('ui-tui: session "main-session" failed to start: Error: resume \\x1b]2;failure-controlled\\x07\n')
expect(exit).toHaveBeenCalledWith(1)
const session = ctx.sessions.create(SessionId('must-not-start'))
const session = ctx.sessions.create(SessionId('main-session'))
ctx.agents.register({
id: AgentId('main'), options: {}, session, status: 'idle', ctx,
id: session.id, options: {}, session, status: 'idle', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
await tick()
@@ -877,20 +877,22 @@ describe('terminal mounting', () => {
await ctx.fiber.dispose()
})
it('prints a retained startup failure for a late-mounted TUI', async () => {
it('renders an uncoercible startup failure without escaping the display boundary', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
ctx.agents.reportStartFailure(AgentId('main'), new Error('already failed'))
const terminal = new FakeTerminal()
const exit = vi.fn()
mountTui(ctx, { agent: 'main', color: false }, { terminal, exit })
mountTui(ctx, { sessionId: 'main-session', color: false }, { terminal, exit })
ctx.emit('agent-loop/config-start-failed', SessionId('main-session'), {
toString(): string { throw new Error('coercion failed') },
})
expect(terminal.started).toBe(0)
expect(terminal.output).toBe('ui-tui: agent "main" failed to start: already failed\n')
expect(terminal.output).toBe('ui-tui: session "main-session" failed to start: <unrenderable thrown value>\n')
expect(exit).toHaveBeenCalledWith(1)
await ctx.fiber.dispose()
})
@@ -903,13 +905,13 @@ describe('terminal mounting', () => {
ctx.provide('tools', { get: () => undefined } as never)
const session = ctx.sessions.create(SessionId('failed-start-session'))
ctx.agents.register({
id: AgentId('main'), options: {}, session, status: 'running', ctx,
id: session.id, options: {}, session, status: 'running', ctx,
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
})
const terminal = new FakeTerminal()
terminal.start = () => { throw new Error('terminal startup failed') }
expect(() => createTuiChat(ctx, { color: false }, { terminal, exit: vi.fn() }))
expect(() => createTuiChat(ctx, { sessionId: 'failed-start-session', color: false }, { terminal, exit: vi.fn() }))
.toThrow('terminal startup failed')
expect(terminal.stopped).toBe(1)
expect(terminal.progress).toEqual([false, true, false])
@@ -931,7 +933,7 @@ describe('terminal mounting', () => {
await ctx.plugin(UserInteractionService)
ctx.provide('tools', { get: () => undefined } as never)
const runtime: TuiRuntime = { terminal: new FakeTerminal(), exit: vi.fn() }
expect(() => createTuiChat(ctx, { agent: 'missing' }, runtime)).toThrow('is not running')
expect(() => createTuiChat(ctx, { sessionId: 'missing' }, runtime)).toThrow('is not running')
await ctx.fiber.dispose()
})
})