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:
@@ -15,7 +15,7 @@ While the agent is running, editor submissions call `agent.steer()`; otherwise t
|
||||
| Key | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `welcome` | `ready.` | Header subtitle |
|
||||
| `agent` | `main` | Agent id driven by the terminal |
|
||||
| `sessionId` | `main` | Exact shared agent/session identity driven by the terminal |
|
||||
| `showReasoning` | `true` | Render reasoning blocks |
|
||||
| `maxToolOutputLines` | `12` | Collapsed tool-card output limit |
|
||||
| `maxQuestionOptions` | `8` | Visible options in a question overlay |
|
||||
@@ -30,12 +30,12 @@ While the agent is running, editor submissions call `agent.steer()`; otherwise t
|
||||
name: '@deepseek-ai/dsh-tui'
|
||||
config:
|
||||
welcome: 'Coding agent ready.'
|
||||
agent: main
|
||||
sessionId: main-session-123
|
||||
showReasoning: true
|
||||
maxToolOutputLines: 12
|
||||
```
|
||||
|
||||
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.
|
||||
Startup fails before mounting when either process stream is not a TTY. The composing app must mount the TUI before its config-created agent so the front door can observe `agent-loop/config-start-failed`; a matching exact-session 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.
|
||||
|
||||
## Color
|
||||
|
||||
@@ -57,6 +57,6 @@ The palette uses the standard 16-color ANSI foregrounds and SGR attributes, whic
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **One configured agent owns the transcript and editor** — questions from other agents can still use the shared overlay provider, but session rendering and prompt input remain bound to `agent`.
|
||||
- **One configured session owns the transcript and editor** — questions from other agents can still use the shared overlay provider, but session rendering and prompt input remain bound to `sessionId`.
|
||||
- **Tool cards are text terminal presentations** — terminal, diff, and generic cards use tool-owned titles/content, but session content currently has no image block for inline image rendering.
|
||||
- **Non-TTY operation is intentionally unsupported** — app bundles that need automation must select `dsh-stdio` before mounting this plugin rather than expecting an internal fallback.
|
||||
|
||||
@@ -23,11 +23,12 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-tui": "0.80.7",
|
||||
@@ -36,6 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-loader": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
@@ -45,6 +47,6 @@
|
||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||
"@deepseek-ai/dsh-workflow": "workspace:^",
|
||||
"@xterm/headless": "5.5.0",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ import {
|
||||
} from '@earendil-works/pi-tui'
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { AgentId, observeAgentStart, type Agent, type AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-agent-loop'
|
||||
import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type { Session, SessionEvent, TodoItem } from '@deepseek-ai/dsh-session'
|
||||
import { SessionId, type Session, type SessionEvent, type TodoItem } from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
FileDiff,
|
||||
TerminalCallView,
|
||||
@@ -99,13 +100,13 @@ export const TuiConfigSchema: z<TuiConfig> = z.object({
|
||||
export interface Config extends TuiConfig {
|
||||
/** Header subtitle. Defaults to `ready.`. */
|
||||
welcome?: string
|
||||
/** Agent id driven by this terminal. Defaults to `main`. */
|
||||
agent?: string
|
||||
/** Exact shared agent/session identity driven by this terminal. Defaults to `main`. */
|
||||
sessionId?: string
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
welcome: z.string().default('ready.'),
|
||||
agent: z.string().default('main'),
|
||||
sessionId: z.string().default('main'),
|
||||
showReasoning: showReasoningSchema,
|
||||
maxToolOutputLines: maxToolOutputLinesSchema,
|
||||
maxQuestionOptions: maxQuestionOptionsSchema,
|
||||
@@ -190,6 +191,15 @@ function displayText(text: string): string {
|
||||
`\\x${control.charCodeAt(0).toString(16).padStart(2, '0')}`)
|
||||
}
|
||||
|
||||
/** Render an arbitrary failure without allowing hostile coercion to escape the UI boundary. */
|
||||
function renderThrown(value: unknown): string {
|
||||
try {
|
||||
return String(value)
|
||||
} catch {
|
||||
return '<unrenderable thrown value>'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme-agnostic palette built from the standard 16-color ANSI set plus SGR
|
||||
* attributes, which every terminal remaps to its active color scheme. Body
|
||||
@@ -293,7 +303,7 @@ class HeaderComponent implements Component {
|
||||
const usable = Math.max(1, width - 4)
|
||||
const title = `${this.palette.bold(this.palette.accent('DEEPSEEK'))} ${this.palette.bold('HARNESS')}`
|
||||
const model = displayText(this.agent.options.model ?? 'model unset')
|
||||
const detail = `${displayText(this.agent.id)} • ${model} • ${displayText(this.agent.session.id)}`
|
||||
const detail = `${model} • ${displayText(this.agent.session.id)}`
|
||||
const top = this.palette.accent(`╭${'─'.repeat(Math.max(0, width - 2))}╮`)
|
||||
const bottom = this.palette.accent(`╰${'─'.repeat(Math.max(0, width - 2))}╯`)
|
||||
const lines = [title, this.palette.muted(displayText(this.welcome)), this.palette.dim(detail)]
|
||||
@@ -803,9 +813,9 @@ export function createTuiChat(
|
||||
config: Config,
|
||||
runtime: TuiRuntime,
|
||||
): TuiController {
|
||||
const agentId = AgentId(config.agent ?? 'main')
|
||||
const agent = ctx.agents.get(agentId)
|
||||
if (agent === undefined) throw new Error(`ui-tui: agent "${agentId}" is not running`)
|
||||
const sessionId = SessionId(config.sessionId ?? 'main')
|
||||
const agent = ctx.agents.get(sessionId)
|
||||
if (agent === undefined) throw new Error(`ui-tui: session "${sessionId}" is not running`)
|
||||
const resolved = resolveTuiConfig(config)
|
||||
const palette = createPalette(resolved.color)
|
||||
const mdTheme = markdownTheme(palette)
|
||||
@@ -1298,19 +1308,36 @@ export function createTuiChat(
|
||||
* @param runtime - Terminal and process-exit boundary.
|
||||
*/
|
||||
export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): void {
|
||||
const agentId = AgentId(config.agent ?? 'main')
|
||||
observeAgentStart(ctx, agentId, {
|
||||
onStarted: () => {
|
||||
ctx.effect(() => {
|
||||
const controller = createTuiChat(ctx, config, runtime)
|
||||
return () => controller.dispose()
|
||||
}, 'ui-tui')
|
||||
},
|
||||
onFailed: (error) => {
|
||||
runtime.terminal.write(displayText(`ui-tui: agent "${agentId}" failed to start: ${error.message}\n`))
|
||||
runtime.exit(1)
|
||||
},
|
||||
})
|
||||
const sessionId = SessionId(config.sessionId ?? 'main')
|
||||
const matchesConfiguredIdentity = (agent: Agent): boolean =>
|
||||
agent.id === sessionId && ctx.agents.roots().includes(agent)
|
||||
let settled = false
|
||||
|
||||
const stopWaiting = (): void => {
|
||||
disposeCreated()
|
||||
disposeFailure()
|
||||
}
|
||||
const start = (agent: Agent): void => {
|
||||
if (settled || !matchesConfiguredIdentity(agent)) return
|
||||
settled = true
|
||||
stopWaiting()
|
||||
ctx.effect(() => {
|
||||
const controller = createTuiChat(ctx, config, runtime)
|
||||
return () => controller.dispose()
|
||||
}, 'ui-tui')
|
||||
}
|
||||
const fail = (failedSessionId: SessionId, error: unknown): void => {
|
||||
if (settled || failedSessionId !== sessionId) return
|
||||
settled = true
|
||||
stopWaiting()
|
||||
runtime.terminal.write(displayText(`ui-tui: session "${sessionId}" failed to start: ${renderThrown(error)}\n`))
|
||||
runtime.exit(1)
|
||||
}
|
||||
|
||||
const disposeCreated = ctx.on('agent/created', start)
|
||||
const disposeFailure = ctx.on('agent-loop/config-start-failed', fail)
|
||||
const existing = ctx.agents.roots().find(agent => agent.id === sessionId)
|
||||
if (existing !== undefined) start(existing)
|
||||
}
|
||||
|
||||
/** Cordis entry point using the process terminal; explicit TUI composition requires a TTY pair. */
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent-loop"
|
||||
},
|
||||
{
|
||||
"path": "../../core/session"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user