feat(tui): add path-only @file autocomplete
This commit is contained in:
@@ -13,14 +13,24 @@ const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
|
||||
/**
|
||||
* Seed the harness workspace: personal files land in the isolated Harness home
|
||||
* (`.dsh`), skill bundles under the agents home's `skills/` root — the same
|
||||
* trees `$DSH_HOME` / `$DSH_AGENTS_HOME` point the child at.
|
||||
* Seed the isolated process workspace: ordinary files land in `cwd`, personal
|
||||
* files in the Harness home (`.dsh`), and skill bundles under the agents
|
||||
* home's `skills/` root — the same trees `$DSH_HOME` /
|
||||
* `$DSH_AGENTS_HOME` point the child at.
|
||||
*/
|
||||
function seedWorkspace(
|
||||
files: { personal?: Record<string, string>; skills?: Record<string, string> },
|
||||
files: {
|
||||
workspace?: Record<string, string>
|
||||
personal?: Record<string, string>
|
||||
skills?: Record<string, string>
|
||||
},
|
||||
): (cwd: string) => Promise<void> {
|
||||
return async (cwd) => {
|
||||
for (const [name, content] of Object.entries(files.workspace ?? {})) {
|
||||
const file = join(cwd, name)
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
await writeFile(file, content)
|
||||
}
|
||||
for (const [name, content] of Object.entries(files.personal ?? {})) {
|
||||
const file = join(cwd, '.dsh', name)
|
||||
await mkdir(dirname(file), { recursive: true })
|
||||
@@ -168,6 +178,27 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => {
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('fuzzy-completes an @file path without reading or submitting the file', async () => {
|
||||
const output = await smoke({
|
||||
label: 'tui-agent file autocomplete',
|
||||
tempDirPrefix: 'tui-agent-file-autocomplete-',
|
||||
prepare: seedWorkspace({
|
||||
workspace: {
|
||||
'src/terminal-special-case.ts': 'export const marker = true\n',
|
||||
'src/other.ts': 'export const other = true\n',
|
||||
},
|
||||
}),
|
||||
actions: [
|
||||
{ waitFor: 'main-session-', send: '@tsc' },
|
||||
{ waitFor: 'File · terminal-special-case.t', send: '\t' },
|
||||
{ waitFor: '@src/terminal-special-case.ts', send: '\x03/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(output).toContain('File · terminal-special-case.t')
|
||||
expect(output).toContain('@src/terminal-special-case.ts')
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('boots the Code Mode overlay tree, renders its banner, and exits cleanly', async () => {
|
||||
// The overlay's only keyless composition proof: the include+patch tree,
|
||||
// worker code runtime, and one-tool registry all mount before the banner.
|
||||
|
||||
@@ -26,7 +26,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph'
|
||||
import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow'
|
||||
import { createTuiChat } from '@deepseek-ai/dsh-tui'
|
||||
import { createTuiChat, FILE_REFERENCE_PROMPT } from '@deepseek-ai/dsh-tui'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
|
||||
import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts'
|
||||
@@ -302,6 +302,9 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
}
|
||||
|
||||
const events: SessionEvent[] = [...agent.session.events]
|
||||
const firstHeader = events.find(event => event.type === 'request/header')
|
||||
expect(firstHeader?.type === 'request/header' && firstHeader.data.header.system)
|
||||
.toContain(FILE_REFERENCE_PROMPT)
|
||||
expect(events.filter(event => event.type === 'tool/call').map(event => event.data.name)).toEqual(scenario.expectedTools)
|
||||
for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) {
|
||||
expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count)
|
||||
@@ -309,7 +312,6 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
|
||||
if (scenario.enterPlanMode === true) {
|
||||
expect(ctx.planMode.get(agent)).toEqual({ active: true })
|
||||
const planMode = events.find(event => event.type === 'plan/mode')
|
||||
const firstHeader = events.find(event => event.type === 'request/header')
|
||||
if (planMode === undefined || firstHeader === undefined) {
|
||||
throw new Error('plan-mode command snapshot needs plan/mode before its first request/header')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user