Merge branch 'master' into feat/plan-mode
This commit is contained in:
@@ -12,7 +12,7 @@ import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
|
||||
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 } from '../src/index.ts'
|
||||
import { createTuiChat, type Config, type TuiRuntime } from '../src/index.ts'
|
||||
|
||||
interface FakeAgent extends Agent {
|
||||
status: AgentStatus
|
||||
@@ -28,6 +28,7 @@ export interface TuiHarnessOptions {
|
||||
configureContext?: (ctx: Context) => Promise<void>
|
||||
beforeMount?: (session: Session) => void
|
||||
cwd?: string | null
|
||||
formatCwd?: TuiRuntime['formatCwd']
|
||||
agentOptions?: AgentOptions
|
||||
contextWindow?: number
|
||||
contextTokens?: number
|
||||
@@ -144,7 +145,12 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
welcome: 'Coding agent ready.',
|
||||
sessionId,
|
||||
color: false,
|
||||
}, options.config), { terminal, exit, now: options.now ?? (() => 0) })
|
||||
}, options.config), {
|
||||
terminal,
|
||||
exit,
|
||||
now: options.now ?? (() => 0),
|
||||
...(options.formatCwd === undefined ? {} : { formatCwd: options.formatCwd }),
|
||||
})
|
||||
return { ctx, session, agent, terminal, exit, controller }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Terminal } from '@earendil-works/pi-tui'
|
||||
@@ -168,6 +168,10 @@ describe('TUI config', () => {
|
||||
describe('pi-tui chat lifecycle and transcript', () => {
|
||||
it('uses the latest log-backed title for the header subtitle and terminal window', async () => {
|
||||
const result = await setup({
|
||||
// A fixed short cwd keeps the footer's token counters inside the 88-column
|
||||
// fake terminal regardless of where the checkout lives; cwd rendering has
|
||||
// its own dedicated variants test below.
|
||||
cwd: '/workspace',
|
||||
beforeMount(session) {
|
||||
session.append('session/title', {
|
||||
title: 'Restored session title',
|
||||
@@ -413,6 +417,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
|
||||
it('renders the ANSI palette and every markdown/content style', async () => {
|
||||
const result = await setup({
|
||||
cwd: '/workspace',
|
||||
config: { color: true },
|
||||
beforeMount(session) {
|
||||
session.append('user/message', {
|
||||
@@ -496,9 +501,21 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(unsetResult.terminal.output).toContain('cwd unset')
|
||||
await dispose(unsetResult)
|
||||
|
||||
const homeParent = resolve(home, '..')
|
||||
const parentResult = await setup({ cwd: homeParent })
|
||||
expect(parentResult.terminal.output).toContain(homeParent)
|
||||
await dispose(parentResult)
|
||||
|
||||
const outsideResult = await setup({ cwd: '/opt' })
|
||||
expect(outsideResult.terminal.output).toContain('/opt')
|
||||
await dispose(outsideResult)
|
||||
|
||||
const logicalResult = await setup({
|
||||
cwd: '/w',
|
||||
formatCwd: cwd => `logical:${cwd}\x1b`,
|
||||
})
|
||||
expect(logicalResult.terminal.output).toContain('logical:/w\\x1b')
|
||||
await dispose(logicalResult)
|
||||
})
|
||||
|
||||
it('sends, steers, handles commands, global keys, and disposed-agent input', async () => {
|
||||
@@ -1175,8 +1192,9 @@ describe('TUI user-interaction dialogs', () => {
|
||||
result.terminal.send('x')
|
||||
result.terminal.send(' ')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Select at least one option')
|
||||
await vi.waitFor(() => {
|
||||
expect(result.terminal.output).toContain('Select at least one option')
|
||||
})
|
||||
result.terminal.send('c')
|
||||
await tick()
|
||||
result.terminal.send('\x1b')
|
||||
@@ -1400,4 +1418,57 @@ describe('terminal mounting', () => {
|
||||
expect(() => createTuiChat(ctx, { sessionId: 'missing' }, runtime)).toThrow('is not running')
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('detects a light terminal color scheme and switches from dark- to light-optimised ANSI codes', async () => {
|
||||
const result = await setup({ config: { color: true } })
|
||||
// Initial render uses dark-optimised palette: SGR 2 (dim) for dim text.
|
||||
expect(result.terminal.output).toContain('\x1b[2mdeepseek-v4-flash')
|
||||
|
||||
// A report matching the current scheme is a no-op: no palette rebuild or
|
||||
// re-render (ESC [?997;1n = dark, the startup default).
|
||||
const beforeSameScheme = result.terminal.output.length
|
||||
result.terminal.send('\x1b[?997;1n')
|
||||
await tick()
|
||||
expect(result.terminal.output.length).toBe(beforeSameScheme)
|
||||
|
||||
// Simulate the terminal responding with a light color scheme report
|
||||
// (ESC [?997;2n = light, ESC [?997;1n = dark).
|
||||
result.terminal.send('\x1b[?997;2n')
|
||||
await tick()
|
||||
await tick()
|
||||
|
||||
// After switching to light-optimised palette: palette.dim uses ANSI 90
|
||||
// (gray) instead of SGR 2. The header now uses \x1b[90m for the detail
|
||||
// line. The cumulative output still contains the initial SGR 2 render,
|
||||
// so we assert that a LATER write (appended after the scheme switch)
|
||||
// uses ANSI 90 for the same header text.
|
||||
expect(result.terminal.output).toContain('\x1b[90mdeepseek-v4-flash')
|
||||
|
||||
// Switch back to dark scheme.
|
||||
result.terminal.send('\x1b[?997;1n')
|
||||
await tick()
|
||||
await tick()
|
||||
// After switching back, a new write uses SGR 2 for the header detail.
|
||||
expect(result.terminal.output).toContain('\x1b[2mdeepseek-v4-flash')
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
it('keeps the dark palette when the terminal rejects the color-scheme query', async () => {
|
||||
class QueryFailTerminal extends FakeTerminal {
|
||||
override write(data: string): void {
|
||||
// The device-status query is the only write that fails; the promise
|
||||
// rejects and the swallowed `.catch` leaves the dark palette in place.
|
||||
if (data === '\x1b[?996n') throw new Error('query write failed')
|
||||
super.write(data)
|
||||
}
|
||||
}
|
||||
const terminal = new QueryFailTerminal()
|
||||
const result = await createTuiTestHarness(terminal, vi.fn(), {
|
||||
config: { color: true },
|
||||
cwd: process.cwd(),
|
||||
})
|
||||
await tick()
|
||||
expect(terminal.output).toContain('\x1b[2mdeepseek-v4-flash')
|
||||
await disposeTuiTestHarness(result)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user