Merge remote-tracking branch 'origin/master' into feat/send-unify

This commit is contained in:
Turtle
2026-07-24 16:52:46 +08:00
192 changed files with 6925 additions and 2517 deletions

View File

@@ -15,6 +15,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import { createTuiChat, type Config, type TuiRuntime } from '../src/index.ts'
import { TestSessionQueryService } from './session-query.ts'
interface FakeAgent extends Agent {
status: AgentStatus
@@ -49,7 +50,13 @@ export interface TuiHarnessOptions {
resolveModelContext?: (provider: string, model: string) => Promise<LlmModelContext | undefined>
}
/** Provide a fake `sessionPersistence` service so resume surfaces can list sessions. */
sessionPersistence?: { list(): Promise<SessionHeader[]> }
sessionPersistence?: {
list(): Promise<SessionHeader[]>
load?(id: ReturnType<typeof SessionId>): Promise<{ meta: SessionHeader; events: Session['events'] }>
}
handoffResume?: TuiRuntime['handoffResume']
/** Set false to exercise the optional session-query degradation path. */
mountSessionQuery?: boolean
}
export interface TuiHarness<TerminalType extends Terminal, Exit extends (code: number) => void> {
@@ -119,7 +126,22 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
}
if (ctx.get('systemPrompt') === undefined) await ctx.plugin(SystemPrompt)
if (options.sessionPersistence !== undefined) {
ctx.provide('sessionPersistence', options.sessionPersistence as never)
const persistence = options.sessionPersistence
ctx.provide('sessionPersistence', {
...persistence,
locate: () => undefined,
create: () => Promise.resolve(),
append: () => Promise.resolve(),
load: persistence.load === undefined
? (id: ReturnType<typeof SessionId>) => Promise.reject(new Error(`session "${id}" not found`))
: (id: ReturnType<typeof SessionId>) => persistence.load!(id),
inspect: persistence.load === undefined
? (id: ReturnType<typeof SessionId>) => Promise.reject(new Error(`session "${id}" not found`))
: (id: ReturnType<typeof SessionId>) => persistence.load!(id),
} as never)
}
if (options.mountSessionQuery !== false && ctx.get('sessionQuery') === undefined) {
await ctx.plugin(TestSessionQueryService)
}
const sessionId = SessionId('main-session')
const session = ctx.sessions.create(
@@ -186,6 +208,7 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
// test pins the clock only by passing `now` explicitly.
...(options.now === undefined ? {} : { now: options.now }),
...(options.formatCwd === undefined ? {} : { formatCwd: options.formatCwd }),
...(options.handoffResume === undefined ? {} : { handoffResume: options.handoffResume }),
})
return { ctx, session, agent, terminal, exit, controller }
}

View File

@@ -14,6 +14,7 @@ describe('dsh-tui plugin export shape', () => {
expect(unwrapped.name).toBe('ui-tui')
expect(unwrapped.inject).toEqual([
'agents',
'sessions',
'commands',
'userInteraction',
'tools',

View File

@@ -1,32 +1,51 @@
terminal 92x32 buffer=normal length=32 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "DSH snapshot"
cursor hidden column=1 viewportRow=10 bufferRow=10
cursor hidden column=6 viewportRow=4 bufferRow=4
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold
style 10-16 bold
1| " Snapshot agent ready."
style 1-21 fg=bright-black
2| " deepseek-v4-flash • main-session"
style 1-34 dim
3| <blank>
4| " Resumable sessions "
style 1-18 fg=bright-blue bold
5| " 2024-01-02 03:04 (current) "
style 1-16 fg=bright-black
style 17-26 fg=green
6| " RESUME_SESSION_ID=main-session dsh "
7| " 2024-01-01 00:00 "
style 1-16 fg=bright-black
8| " RESUME_SESSION_ID=earlier-session dsh "
9| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
10| " "
style 1-1 inverse
11| "────────────────────────────────────────────────────────────────────────────────────────────"
style 0-91 dim
12| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
style 0-43 dim
style 65-91 dim
13-31| <blank>
0| " "
1| " Resume session (1 of 2) "
style 2-24 fg=bright-blue bold
2| " "
3| " ╭──────────────────────────────────────────────────────────────────────────────────────╮ "
style 2-89 dim
4| " │ ⌕ │ "
style 2-2 dim
style 6-6 inverse
style 89-89 dim
5| " ╰──────────────────────────────────────────────────────────────────────────────────────╯ "
style 2-89 dim
6| " "
7| " /workspace/project "
style 2-19 fg=bright-black
8| " "
9| " ❯ Untitled session "
style 2-19 fg=bright-blue bold
10| " 2026-07-23T08:00:00.000Z · no completed turn · route unavailable "
style 2-67 fg=bright-black
11| " current · live · main-session "
style 2-32 dim
12| " unavailable: current session "
style 2-31 fg=yellow
13| " Resume selector design "
14| " 2024-01-01T00:00:08.000Z · turn 1: completed · deepseek/deepseek-v4-pro "
style 2-74 fg=bright-black
15| " persisted · earlier-session "
style 2-30 dim
16| " "
17| " "
18| " "
19| " "
20| " "
21| " "
22| " "
23| " "
24| " "
25| " "
26| " "
27| " "
28| " "
29| " "
30| " Type to search • ↑/↓ navigate • Enter resume • Esc clear/cancel "
style 2-70 dim
31| " "

View File

@@ -646,13 +646,27 @@ describe('TUI terminal-state snapshots', () => {
await disposeSnapshot(harness)
})
it('lists this workspace\'s resumable sessions with their commands', async () => {
it('opens the searchable resume selector with log-backed session summaries', async () => {
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-07-23T08:00:00.000Z'))
const earlier = { version: 0, id: SessionId('earlier-session'), createdAt: Date.parse('2024-01-01T00:00:00Z'), cwd: '/workspace/project' }
const harness = await setupSnapshot({
config: { resumeCommand: 'RESUME_SESSION_ID={session} dsh' },
sessionPersistence: { list: async () => [
{ version: 0, id: SessionId('main-session'), createdAt: Date.parse('2024-01-02T03:04:00Z'), cwd: '/workspace/project' },
{ version: 0, id: SessionId('earlier-session'), createdAt: Date.parse('2024-01-01T00:00:00Z'), cwd: '/workspace/project' },
] },
sessionPersistence: {
list: async () => [earlier],
load: async () => ({
meta: earlier,
events: [
{ type: 'turn/start', seq: 0, time: Date.parse('2024-01-01T00:00:01Z'), data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
{ type: 'user/message', seq: 1, time: Date.parse('2024-01-01T00:00:02Z'), data: { content: [{ type: 'text', text: 'restore the selector' }], source: { kind: 'user' } }, surfaceOp: 'append' },
{ type: 'step/start', seq: 2, time: Date.parse('2024-01-01T00:00:03Z'), data: { turn: 1, step: 1 } },
{ type: 'request/header', seq: 3, time: Date.parse('2024-01-01T00:00:04Z'), data: { header: { config: { provider: 'deepseek', model: 'deepseek-v4-pro' } }, reason: 'initial' } },
{ type: 'assistant/message', seq: 4, time: Date.parse('2024-01-01T00:00:05Z'), data: { turn: 1, step: 1, content: [{ type: 'text', text: 'ready' }], provenance: { provider: 'deepseek', model: 'deepseek-v4-pro' } }, surfaceOp: 'append' },
{ type: 'step/end', seq: 5, time: Date.parse('2024-01-01T00:00:06Z'), data: { turn: 1, step: 1 } },
{ type: 'turn/end', seq: 6, time: Date.parse('2024-01-01T00:00:07Z'), data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'session/title', seq: 7, time: Date.parse('2024-01-01T00:00:08Z'), data: { title: 'Resume selector design', messageSeqs: [1], source: { kind: 'fallback' } } },
],
}),
},
}, { columns: 92, rows: 32 })
harness.terminal.send('/resume')
harness.terminal.send('\r')
@@ -662,6 +676,7 @@ describe('TUI terminal-state snapshots', () => {
await harness.terminal.flush()
await checkpoint('resume-sessions', harness.terminal, { includeScrollback: true })
await disposeSnapshot(harness)
dateNow.mockRestore()
})
it('pins the detailed session diagnostics card', async () => {

File diff suppressed because it is too large Load Diff