fix(ui): surface declarative startup failures
This commit is contained in:
@@ -33,7 +33,7 @@ While the agent is running, editor submissions call `agent.steer()`; otherwise t
|
||||
maxToolOutputLines: 12
|
||||
```
|
||||
|
||||
Startup fails before mounting when either process stream is not a TTY. Disposal stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR.
|
||||
Startup fails before mounting when either process stream is not a TTY. While waiting for its configured agent, the front door also observes retained and live `agent/start-failed` notifications; a matching failure is written before fullscreen mode starts and exits with status 1 instead of leaving a blank terminal. Disposal stops loaders, rejects pending questions, drains terminal input, restores terminal state, unregisters event listeners and the user-interaction provider, and never exits a replacement process during HMR.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from '@earendil-works/pi-tui'
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { AgentId, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import { AgentId, observeAgentStart, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEvent, TodoItem } from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
@@ -133,7 +133,7 @@ export interface ResolvedTuiConfig {
|
||||
export interface TuiRuntime {
|
||||
/** Terminal implementation; production uses pi-tui's `ProcessTerminal`. */
|
||||
terminal: Terminal
|
||||
/** Exit hook used by `/exit`, Ctrl+D, or Ctrl+C while idle. */
|
||||
/** Exit hook used by terminal shutdown or a target-agent startup failure. */
|
||||
exit(code: number): void
|
||||
}
|
||||
|
||||
@@ -1252,20 +1252,17 @@ export function createTuiChat(
|
||||
*/
|
||||
export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): void {
|
||||
const agentId = AgentId(config.agent ?? 'main')
|
||||
const start = (): void => {
|
||||
ctx.effect(() => {
|
||||
const controller = createTuiChat(ctx, config, runtime)
|
||||
return () => controller.dispose()
|
||||
}, 'ui-tui')
|
||||
}
|
||||
if (ctx.agents.get(agentId) !== undefined) {
|
||||
start()
|
||||
return
|
||||
}
|
||||
const dispose = ctx.on('agent/created', (agent) => {
|
||||
if (agent.id !== agentId) return
|
||||
dispose()
|
||||
start()
|
||||
observeAgentStart(ctx, agentId, {
|
||||
onStarted: () => {
|
||||
ctx.effect(() => {
|
||||
const controller = createTuiChat(ctx, config, runtime)
|
||||
return () => controller.dispose()
|
||||
}, 'ui-tui')
|
||||
},
|
||||
onFailed: (error) => {
|
||||
runtime.terminal.write(`ui-tui: agent "${agentId}" failed to start: ${error.message}\n`)
|
||||
runtime.exit(1)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -910,6 +910,51 @@ describe('terminal mounting', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('prints a matching live startup failure and exits instead of waiting forever', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(UserInteractionService)
|
||||
ctx.provide('tools', { get: () => undefined } as never)
|
||||
const terminal = new FakeTerminal()
|
||||
const exit = vi.fn()
|
||||
mountTui(ctx, { agent: 'main', color: false }, { terminal, exit })
|
||||
|
||||
ctx.agents.reportStartFailure(AgentId('other'), new Error('other failed'))
|
||||
expect(terminal.output).toBe('')
|
||||
expect(exit).not.toHaveBeenCalled()
|
||||
ctx.agents.reportStartFailure(AgentId('main'), new Error('resume failed'))
|
||||
expect(terminal.output).toBe('ui-tui: agent "main" failed to start: resume failed\n')
|
||||
expect(exit).toHaveBeenCalledWith(1)
|
||||
|
||||
const session = ctx.sessions.create(SessionId('must-not-start'))
|
||||
ctx.agents.register({
|
||||
id: AgentId('main'), options: {}, session, status: 'idle', ctx,
|
||||
send() {}, steer() {}, inject() {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
await tick()
|
||||
expect(terminal.started).toBe(0)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('prints a retained startup failure for a late-mounted TUI', 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 })
|
||||
|
||||
expect(terminal.started).toBe(0)
|
||||
expect(terminal.output).toBe('ui-tui: agent "main" failed to start: already failed\n')
|
||||
expect(exit).toHaveBeenCalledWith(1)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('rolls back providers, listeners, and terminal state when startup fails', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
|
||||
Reference in New Issue
Block a user