Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 5 additions & 28 deletions src/components/ResumeSessionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export function ResumeSessionDialog({ open, onClose, onNavigate }: ResumeSession
const store = useStore<RootState>()
const [input, setInput] = useState('')
const [agent, setAgent] = useState<string>(providers[0])
const [agentTouched, setAgentTouched] = useState(false)
const [anywayCwd, setAnywayCwd] = useState('~')
const [phase, setPhase] = useState<Phase>({ kind: 'idle' })
// Inline error for confirming a cwd-less match with a blank cwd field.
Expand All @@ -82,12 +81,14 @@ export function ResumeSessionDialog({ open, onClose, onNavigate }: ResumeSession
// homeDir prefill never overwrites a USER-edited working directory.
const cwdTouchedRef = useRef(false)

// Advisory hint pre-fills the picker; never overrides a manual choice.
// Advisory parse hint drives the internal agent guess. There is no visible
// picker (kata 1ffd): the guess surfaces only on the no-match escape hatch's
// "Resume anyway with {agent}" button, which is the disclosure point.
useEffect(() => {
if (agentTouched || !input) return
if (!input) return
const { hint } = parseResumeInput(input)
if (hint && providers.includes(hint.provider)) setAgent(hint.provider)
}, [input, agentTouched])
}, [input])

const finishResume = useCallback(
(target: ResumeTarget, note: string) => {
Expand Down Expand Up @@ -344,30 +345,6 @@ export function ResumeSessionDialog({ open, onClose, onNavigate }: ResumeSession
}, 0)
}}
/>
<div className="flex items-center gap-2">
<label className="text-xs text-muted-foreground" htmlFor="resume-agent-picker">
Agent
</label>
<select
id="resume-agent-picker"
data-testid="resume-agent-picker"
value={agent}
onChange={(event) => {
setAgent(event.target.value)
setAgentTouched(true)
}}
className={controlClass}
>
{providers.map((provider) => (
<option key={provider} value={provider}>
{provider}
</option>
))}
</select>
</div>
<p className="text-[10px] text-muted-foreground">
Unverified guess — the session store decides the agent.
</p>
<button
type="button"
data-testid="resume-resolve-button"
Expand Down
40 changes: 27 additions & 13 deletions test/unit/client/components/ResumeSessionDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ describe('ResumeSessionDialog', () => {
expect(screen.getByTestId('resume-note').textContent).toContain('codex')
})

it('evidence wins over the picker, with a note', async () => {
it('server evidence decides the agent even when the parse hint disagrees', async () => {
apiPost.mockReturnValue(ok([match({ provider: 'opencode', sessionId: SES, sessionType: undefined })]))
renderDialog()
fireEvent.change(screen.getByTestId('resume-agent-picker'), { target: { value: 'claude' } })
typeAndResolve(SES)
// "claude --resume" hints claude, but the session store says opencode.
typeAndResolve(`claude --resume ${SES}`)
await waitFor(() => expect(resumeSessionInTab).toHaveBeenCalled())
expect(resumeSessionInTab.mock.calls[0][2]).toMatchObject({ provider: 'opencode' })
expect(screen.getByTestId('resume-note').textContent).toContain('opencode')
Expand All @@ -116,25 +116,40 @@ describe('ResumeSessionDialog', () => {
})
})

it('zero matches: inline error, input preserved, resume-anyway uses picker agent', async () => {
it('zero matches: inline error, input preserved, resume-anyway uses the parse-hint agent', async () => {
apiPost.mockReturnValue(ok([]))
renderDialog()
typeAndResolve(V4)
await screen.findByTestId('resume-error')
expect((screen.getByTestId('resume-input') as HTMLTextAreaElement).value).toBe(V4)
// hint pre-filled the picker to claude (v4 shape); user switches to amplifier
fireEvent.change(screen.getByTestId('resume-agent-picker'), { target: { value: 'amplifier' } })
// The v4 id shape hints claude; the button discloses that guess before launch.
expect(screen.getByTestId('resume-anyway-button').textContent).toBe('Resume anyway with claude')
expect((screen.getByTestId('resume-anyway-cwd') as HTMLInputElement).value).toBe('~')
fireEvent.click(screen.getByTestId('resume-anyway-button'))
expect(resumeSessionInTab).toHaveBeenCalledTimes(1)
expect(resumeSessionInTab.mock.calls[0][2]).toMatchObject({
provider: 'amplifier',
provider: 'claude',
sessionId: V4,
sessionType: 'amplifier',
sessionType: 'claude',
cwd: undefined, // '~' means server default (home directory)
})
})

it('the command form steers the guess: "codex resume <id>" discloses codex on the escape hatch', async () => {
apiPost.mockReturnValue(ok([]))
renderDialog()
typeAndResolve('codex resume abc123def0')
await screen.findByTestId('resume-error')
expect(screen.getByTestId('resume-anyway-button').textContent).toBe('Resume anyway with codex')
fireEvent.click(screen.getByTestId('resume-anyway-button'))
expect(resumeSessionInTab).toHaveBeenCalledTimes(1)
expect(resumeSessionInTab.mock.calls[0][2]).toMatchObject({
provider: 'codex',
sessionId: 'abc123def0',
sessionType: 'codex',
})
})

it('warming is not "not found": shows retry state and re-resolves', async () => {
apiPost
.mockReturnValueOnce(Promise.resolve({ status: 'warming', matches: [], hint: null }))
Expand Down Expand Up @@ -174,12 +189,11 @@ describe('ResumeSessionDialog', () => {
expect(resumeSessionInTab).not.toHaveBeenCalled()
})

it('pre-fills the agent picker from the hint', async () => {
it('renders NO always-visible agent picker or unverified-guess hint (kata 1ffd removal)', () => {
renderDialog()
fireEvent.change(screen.getByTestId('resume-input'), {
target: { value: `codex resume ${V7}` },
})
expect((screen.getByTestId('resume-agent-picker') as HTMLSelectElement).value).toBe('codex')
expect(screen.queryByTestId('resume-agent-picker')).toBeNull()
expect(screen.queryByLabelText(/agent/i)).toBeNull()
expect(screen.queryByText(/unverified guess/i)).toBeNull()
})

it('closes on Escape', () => {
Expand Down
Loading