Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification

# Conflicts:
#	apps/cli/tests/snapshots/queued-manual-compact/terminal.expected.txt
#	apps/web/tests/snapshots/queue-actions/preserved.expected.md
#	packages/client/runtime/README.i18n.yaml
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
#	packages/ui/tui/README.i18n.yaml
#	packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt
This commit is contained in:
_Kerman
2026-08-03 17:15:34 +08:00
187 changed files with 4546 additions and 590 deletions
@@ -11,9 +11,14 @@ const HOME = '/home/u'
const DOCS = `${HOME}/Documents`
const HARNESS = `${DOCS}/harness`
/** Listing fake over a tiny fixed tree; unknown paths reject like the Host. */
/**
* Listing fake over a tiny fixed tree; unknown paths reject like the Host.
* A trailing separator is dropped the way the Host's own `resolve` drops it,
* so a directory part typed into the path editor addresses its level.
*/
function listingFor(path?: string): DirectoryListing {
const target = path ?? HOME
const asked = path ?? HOME
const target = asked.length > 1 && asked.endsWith('/') ? asked.slice(0, -1) : asked
const tree: Record<string, DirectoryListing> = {
[HOME]: {
path: HOME,
@@ -647,7 +652,7 @@ describe('DirectoryBrowser', () => {
})
it('prefix-filters the listed level from the draft tail, dot revealing hidden matches', async () => {
mount()
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
@@ -659,16 +664,29 @@ describe('DirectoryBrowser', () => {
// A dot-led prefix names hidden entries, so it reveals the match.
fireEvent.change(input, { target: { value: `${HOME}/.co` } })
expect(screen.getByRole('listitem').textContent).toBe('.config')
// A prefix matching nothing empties the level (no stale rows linger).
// A prefix nobody matches releases the filter: the level shows whole
// (hidden rows back under the toggle) instead of emptying under a name
// the operator is still spelling.
fireEvent.change(input, { target: { value: `${HOME}/zzz` } })
expect(screen.queryByRole('listitem')).toBeNull()
// A draft naming some other directory (or none) leaves the level whole.
expect(screen.getAllByRole('listitem').map(item => item.textContent)).toEqual(['Documents'])
// Its dot-led reveal lapses with it.
fireEvent.change(input, { target: { value: `${HOME}/.zzz` } })
expect(screen.getAllByRole('listitem').map(item => item.textContent)).toEqual(['Documents'])
// A tail inside the listed level names no level to walk to: the wait
// fires and finds nothing to scan.
const settled = b.listDirectory.mock.calls.length
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
expect(b.listDirectory.mock.calls).toHaveLength(settled)
// A draft naming some other directory (or none) leaves the level whole —
// and a draft with no separator at all addresses no directory either.
fireEvent.change(input, { target: { value: 'no-separator' } })
expect(screen.getByRole('listitem').textContent).toBe('Documents')
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
expect(b.listDirectory.mock.calls).toHaveLength(settled)
})
it('filters the child pane in two-pane mode and follows the draft back up a level', async () => {
mount()
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(rowButton(screen.getByRole('listitem')))
await waitFor(() => { expect(columns()).toHaveLength(2) })
@@ -679,15 +697,301 @@ describe('DirectoryBrowser', () => {
expect(input.value).toBe(`${DOCS}/`)
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
expect(within(columns()[1]!).getByText('harness')).toBeTruthy()
// The child pane already lists that directory: no scan follows, and both
// panes stay.
const settled = b.listDirectory.mock.calls.length
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
expect(b.listDirectory.mock.calls).toHaveLength(settled)
expect(columns()).toHaveLength(2)
// A miss releases the right pane's filter rather than emptying it.
fireEvent.change(input, { target: { value: `${DOCS}/zzz` } })
expect(within(columns()[1]!).queryAllByRole('listitem')).toHaveLength(0)
expect(within(columns()[0]!).getByText('Documents')).toBeTruthy()
// Erasing back into the parent's own path moves the filter to the LEFT
// pane and releases the right one. The selected row is exempt (it
// anchors the two-pane view), so it alone survives the miss.
fireEvent.change(input, { target: { value: `${HOME}/zz` } })
expect(within(columns()[0]!).getAllByRole('listitem').map(item => item.textContent)).toEqual(['Documents'])
expect(within(columns()[1]!).getByText('harness')).toBeTruthy()
expect(within(columns()[0]!).getByText('Documents')).toBeTruthy()
// Erasing back into the parent's own path re-lands on it rather than
// filtering the LEFT pane: the level being typed is always the last pane,
// never a pane with a deeper level standing to its right. Home is the
// display root, so it lands alone.
fireEvent.change(input, { target: { value: `${HOME}/zz` } })
await waitFor(() => { expect(columns()).toHaveLength(1) })
expect(screen.getAllByRole('listitem').map(item => item.textContent)).toEqual(['Documents'])
})
it('follows the draft into a directory no pane lists, landing the two-pane Miller view', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
expect(columns()).toHaveLength(1)
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
// Typing past a separator addresses a level nobody shows: the panes walk
// to it once the typing rests, landing the ordinary selection-anchored
// two-pane view (level | its children) with the tail filtering the right
// pane — a typed path moves the Miller view exactly as a crumb jump does.
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
await waitFor(() => { expect(columns()).toHaveLength(2) })
expect(b.listDirectory).toHaveBeenCalledWith(`${DOCS}/`, expect.anything())
expect(within(columns()[0]!).getByText('Documents')).toBeTruthy()
expect(within(columns()[1]!).getByText('harness')).toBeTruthy()
// Still editing: the panes moved under the draft, the editor stayed.
expect(screen.getByLabelText<HTMLInputElement>('browser.editPath').value).toBe(`${DOCS}/h`)
// Typing on inside a level the panes already list costs no scan at all:
// the prefix filter alone answers the draft, both panes stay.
const settled = b.listDirectory.mock.calls.length
fireEvent.change(input, { target: { value: `${DOCS}/ha` } })
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
expect(b.listDirectory.mock.calls).toHaveLength(settled)
expect(columns()).toHaveLength(2)
})
it('keeps the typed level in the last pane, its parent beside it, as the draft walks', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
// Two levels down: the typed level on the right, its parent on the left.
fireEvent.change(input, { target: { value: `${HARNESS}/` } })
await waitFor(() => { expect(within(columns()[0]!).getByText('harness')).toBeTruthy() })
expect(columns()).toHaveLength(2)
expect(within(columns()[1]!).queryAllByRole('listitem')).toHaveLength(0)
// Erasing back to the parent's own path re-lands on it: the level being
// typed moves BACK into the last pane instead of staying on the left with
// its own child pane still to the right.
fireEvent.change(input, { target: { value: `${DOCS}/ha` } })
await waitFor(() => { expect(within(columns()[0]!).getByText('Documents')).toBeTruthy() })
expect(columns()).toHaveLength(2)
expect(within(columns()[1]!).getAllByRole('listitem').map(item => item.textContent)).toEqual(['harness'])
expect(b.listDirectory).toHaveBeenCalledWith(`${DOCS}/`, expect.anything())
})
it('holds a stale pane still until its landing, instead of narrowing it first', async () => {
// Own three-level tree: the level that goes stale needs two rows for the
// narrowing this pins against to be visible at all.
const ROOT = '/u'
const MID = `${ROOT}/mid`
const LEAF = `${MID}/leaf`
const chain = [{ name: '/', path: '/', hidden: false }, { name: 'u', path: ROOT, hidden: false }]
const tree: Record<string, DirectoryListing> = {
[ROOT]: {
path: ROOT,
home: ROOT,
crumbs: chain,
entries: [{ name: 'mid', path: MID, hidden: false }, { name: 'other', path: `${ROOT}/other`, hidden: false }],
truncated: false,
},
[MID]: {
path: MID,
home: ROOT,
crumbs: [...chain, { name: 'mid', path: MID, hidden: false }],
entries: [{ name: 'leaf', path: LEAF, hidden: false }, { name: 'sibling', path: `${MID}/sibling`, hidden: false }],
truncated: false,
},
[LEAF]: {
path: LEAF,
home: ROOT,
crumbs: [...chain, { name: 'mid', path: MID, hidden: false }, { name: 'leaf', path: LEAF, hidden: false }],
entries: [],
truncated: false,
},
}
mount({
listDirectory: vi.fn(async (path?: string) => {
const asked = path ?? ROOT
const found = tree[asked.length > 1 && asked.endsWith('/') ? asked.slice(0, -1) : asked]
if (found === undefined) throw new Error(`cannot list ${asked}`)
return found
}),
})
await waitFor(() => { expect(screen.getByText('mid')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${LEAF}/` } })
await waitFor(() => { expect(columns()).toHaveLength(2) })
expect(within(columns()[0]!).getAllByRole('listitem').map(item => item.textContent)).toEqual(['leaf', 'sibling'])
// Deleting the separator names the level the LEFT pane lists. That pane
// is stale — its landing will move it right — so it must not narrow to
// the tail first: one deletion, one movement.
fireEvent.change(input, { target: { value: LEAF } })
expect(within(columns()[0]!).getAllByRole('listitem').map(item => item.textContent)).toEqual(['leaf', 'sibling'])
await waitFor(() => { expect(within(columns()[0]!).getByText('other')).toBeTruthy() })
expect(within(columns()[1]!).getAllByRole('listitem').map(item => item.textContent)).toEqual(['leaf'])
})
it('keeps the walked-to panes when the editor is cancelled, Open adopting where the walk ended', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
await waitFor(() => { expect(columns()).toHaveLength(2) })
fireEvent.keyDown(input, { key: 'Escape' })
// Cancel closes the editor; it does not rewind the walk. The operator
// watched the panes move, so the crumbs, the panes, and Open's target all
// stay where the walk ended.
expect(screen.queryByLabelText('browser.editPath', { selector: 'input' })).toBeNull()
expect(columns()).toHaveLength(2)
expect(within(columns()[0]!).getByText('Documents')).toBeTruthy()
expect(within(columns()[1]!).getByText('harness')).toBeTruthy()
expect(screen.getByRole('navigation').textContent).toContain('Documents')
const open = screen.getByRole<HTMLButtonElement>('button', { name: 'browser.open' })
expect(open.disabled).toBe(false)
fireEvent.click(open)
expect(b.onOpen).toHaveBeenCalledWith(DOCS)
})
it('waits both legs out for a walk: one keystroke never flashes a single pane', async () => {
let landParent = (): void => {}
const listDirectory = vi.fn(async (path?: string) => {
// The parent leg outlives the submitted-navigation wait bound; a walk
// has nothing waiting on it, so it holds the stale view instead of
// landing single-pane and upgrading.
if (path === HOME) return await new Promise<DirectoryListing>((resolve) => { landParent = () => { resolve(listingFor(HOME)) } })
return listingFor(path)
})
mount({ listDirectory })
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
await waitFor(() => { expect(listDirectory).toHaveBeenCalledWith(HOME, expect.anything()) })
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
// Well past the submitted-navigation bound: still the pre-walk view.
expect(columns()).toHaveLength(1)
expect(screen.getByText('Documents')).toBeTruthy()
await act(async () => { landParent() })
await waitFor(() => { expect(columns()).toHaveLength(2) })
expect(within(columns()[1]!).getByText('harness')).toBeTruthy()
})
it('walks the panes back up when erased segments leave the listed levels', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
await waitFor(() => { expect(columns()).toHaveLength(2) })
// Erasing back to a directory neither pane lists walks up to it; the
// filesystem root is the display root, so it lands the single wide level
// with the tail filtering it.
fireEvent.change(input, { target: { value: '/ho' } })
await waitFor(() => { expect(columns()).toHaveLength(1) })
expect(b.listDirectory).toHaveBeenCalledWith('/', expect.anything())
expect(screen.getAllByRole('listitem').map(item => item.textContent)).toEqual(['home'])
})
it('re-arms the draft-following scan after a keystroke superseded one in flight', async () => {
let started = 0
const listDirectory = vi.fn(async (path?: string) => {
if (path !== `${DOCS}/`) return listingFor(path)
started += 1
// The first scan never settles: the next keystroke aborts it, and only
// a re-armed wait can still land the level the draft names.
if (started === 1) return await new Promise<DirectoryListing>(() => {})
return listingFor(path)
})
mount({ listDirectory })
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
await waitFor(() => { expect(started).toBe(1) })
// A further tail keystroke supersedes the in-flight scan; the panes must
// still follow, not sit on the stale level until a separator is typed.
fireEvent.change(input, { target: { value: `${DOCS}/ha` } })
await waitFor(() => { expect(screen.getByText('harness')).toBeTruthy() })
})
it('follows the draft again after an edit releases a failed submission hold', async () => {
const listDirectory = vi.fn(async (path?: string) => {
if (path === HARNESS) throw new Error('target unreadable')
return listingFor(path)
})
mount({ listDirectory })
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
// Submitting inside the debounce window holds the pending scan back.
fireEvent.change(input, { target: { value: HARNESS } })
fireEvent.keyDown(input, { key: 'Enter' })
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('target unreadable') })
// Correcting only the final segment leaves the directory part unchanged;
// the edit must still release the hold and re-arm the wait.
fireEvent.change(input, { target: { value: `${HARNESS}x` } })
await waitFor(() => { expect(listDirectory).toHaveBeenCalledWith(`${DOCS}/`, expect.anything()) })
await waitFor(() => { expect(screen.getByText('harness')).toBeTruthy() })
})
it('re-parks focus on the editor when a landed scan unmounts the focused row', async () => {
mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
// Two levels down, so the walk replaces the LEFT pane the focused row
// lives in (a landing that re-lists the same level reuses its rows).
fireEvent.change(input, { target: { value: `${HARNESS}/` } })
// The keyboard path: focus Tabbed onto a row of the level about to be
// replaced. Without a re-park it would fall to body, outside a Modal that
// has no focus trap.
rowButton(screen.getByRole('listitem')).focus()
await waitFor(() => { expect(within(columns()[0]!).getByText('harness')).toBeTruthy() })
expect(document.activeElement).toBe(screen.getByLabelText('browser.editPath'))
})
it('keeps the panes and stays silent when a draft-following scan fails', async () => {
const b = mount()
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${HOME}/nope/x` } })
await waitFor(() => { expect(b.listDirectory).toHaveBeenCalledWith(`${HOME}/nope/`, expect.anything()) })
// A half-typed directory is unreadable most of the time: the last
// readable level keeps rendering and no error interrupts the typing.
expect(screen.getByText('Documents')).toBeTruthy()
expect(screen.queryByRole('alert')).toBeNull()
})
it('holds the draft-following scan while a submitted path is in flight', async () => {
const listDirectory = vi.fn(async (path?: string) => {
// The submitted leg never settles, so the debounce window elapses with
// the navigation still owning the view.
if (path === HARNESS) return await new Promise<DirectoryListing>(() => {})
return listingFor(path)
})
mount({ listDirectory })
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: HARNESS } })
fireEvent.keyDown(input, { key: 'Enter' })
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
// Only the initial home listing and the submitted path — the draft's
// directory part was never scanned behind the navigation's back.
expect(listDirectory.mock.calls.map(call => call[0])).toEqual([undefined, HARNESS])
})
it('discards draft-following scans that a newer edit superseded', async () => {
let landDocs = (): void => {}
let failRoot = (): void => {}
const listDirectory = vi.fn(async (path?: string) => {
if (path === `${DOCS}/`) return await new Promise<DirectoryListing>((resolve) => { landDocs = () => { resolve(listingFor(DOCS)) } })
if (path === '/') {
return await new Promise<DirectoryListing>((_, reject) => {
failRoot = () => { reject(new Error('root unreadable')) }
})
}
return listingFor(path)
})
mount({ listDirectory })
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
fireEvent.change(input, { target: { value: `${DOCS}/h` } })
await waitFor(() => { expect(listDirectory).toHaveBeenCalledWith(`${DOCS}/`, expect.anything()) })
fireEvent.change(input, { target: { value: '/x' } })
await waitFor(() => { expect(listDirectory).toHaveBeenCalledWith('/', expect.anything()) })
// Back onto the listed level: neither pending scan may still land.
fireEvent.change(input, { target: { value: `${HOME}/D` } })
await act(async () => { landDocs(); failRoot() })
expect(screen.getAllByRole('listitem').map(item => item.textContent)).toEqual(['Documents'])
expect(screen.queryByRole('alert')).toBeNull()
})
it('keeps the draft and filter through window focus loss and in-dialog focus moves', async () => {
@@ -813,7 +1117,8 @@ describe('DirectoryBrowser', () => {
],
truncated: false,
}
mount({ listDirectory: vi.fn(async () => windowsListing) })
const listDirectory = vi.fn(async () => windowsListing)
mount({ listDirectory })
await waitFor(() => { expect(screen.getAllByRole('listitem')).toHaveLength(2) })
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText<HTMLInputElement>('browser.editPath')
@@ -821,6 +1126,18 @@ describe('DirectoryBrowser', () => {
expect(input.value).toBe(ROOT)
fireEvent.change(input, { target: { value: `${ROOT}u` } })
expect(screen.getByRole('listitem').textContent).toBe('Users')
// Windows separates on a forward slash too (so does the Host's resolve),
// so a path typed that way names its directory; the level the Host
// answers with spells it back with a backslash, and once that scan lands
// the level answers the typed spelling — the tail filters it.
fireEvent.change(input, { target: { value: 'C:/p' } })
await waitFor(() => { expect(screen.getByRole('listitem').textContent).toBe('Program Files') })
// And the same spelling asks for no second scan.
const settled = listDirectory.mock.calls.length
fireEvent.change(input, { target: { value: 'C:/pr' } })
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
expect(listDirectory.mock.calls).toHaveLength(settled)
expect(screen.getByRole('listitem').textContent).toBe('Program Files')
})
it('clicking away from the path editor cancels it back to the crumb view', async () => {
@@ -977,6 +1294,10 @@ describe('DirectoryBrowser', () => {
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText('browser.editPath')
fireEvent.change(input, { target: { value: DOCS } })
// With no level listed there is no platform separator to read, so the
// draft-following wait resolves to nothing and the editor types blind.
await act(async () => { await new Promise((resolve) => { setTimeout(resolve, 400) }) })
expect(listDirectory).toHaveBeenCalledTimes(1)
listDirectory.mockImplementation(async (path?: string) => listingFor(path))
fireEvent.keyDown(input, { key: 'Enter' })
await waitFor(() => { expect(screen.getByText('harness')).toBeTruthy() })