Skip to content

Commit db70432

Browse files
authored
Merge pull request #46 from bradfeld/feature/FRE-22-no-timeout-promise-coding-cli
client: add 30s timeout to coding CLI creation Promise
2 parents f4e2d55 + 928979d commit db70432

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

src/store/codingCliThunks.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { RootState } from './store'
66
import type { CodingCliProviderName } from '@/lib/coding-cli-types'
77
import { nanoid } from 'nanoid'
88

9+
const CODING_CLI_CREATE_TIMEOUT_MS = 30_000
10+
911
export const createCodingCliTab = createAsyncThunk(
1012
'codingCli/createTab',
1113
async (
@@ -42,12 +44,16 @@ export const createCodingCliTab = createAsyncThunk(
4244
throw err
4345
}
4446

45-
return new Promise<string>((resolve, reject) => {
46-
const unsub = ws.onMessage((msg) => {
47+
let unsub: (() => void) | undefined
48+
let timeoutId: ReturnType<typeof setTimeout> | undefined
49+
50+
const mainPromise = new Promise<string>((resolve, reject) => {
51+
unsub = ws.onMessage((msg) => {
4752
if (msg.type === 'codingcli.created' && msg.requestId === requestId) {
53+
clearTimeout(timeoutId)
4854
const canceled = (getState() as RootState).codingCli.pendingRequests[requestId]?.canceled
4955
dispatch(resolveCodingCliRequest({ requestId }))
50-
unsub()
56+
unsub?.()
5157
if (canceled) {
5258
ws.send({ type: 'codingcli.kill', sessionId: msg.sessionId })
5359
reject(new Error('Canceled'))
@@ -76,9 +82,10 @@ export const createCodingCliTab = createAsyncThunk(
7682
resolve(msg.sessionId)
7783
}
7884
if (msg.type === 'error' && msg.requestId === requestId) {
85+
clearTimeout(timeoutId)
7986
const canceled = (getState() as RootState).codingCli.pendingRequests[requestId]?.canceled
8087
dispatch(resolveCodingCliRequest({ requestId }))
81-
unsub()
88+
unsub?.()
8289
if (!canceled && createdTabId) {
8390
dispatch(
8491
updateTab({
@@ -99,5 +106,18 @@ export const createCodingCliTab = createAsyncThunk(
99106
cwd,
100107
})
101108
})
109+
110+
const timeoutPromise = new Promise<never>((_, reject) => {
111+
timeoutId = setTimeout(() => {
112+
dispatch(resolveCodingCliRequest({ requestId }))
113+
unsub?.()
114+
if (createdTabId) {
115+
dispatch(updateTab({ id: createdTabId, updates: { status: 'error' } }))
116+
}
117+
reject(new Error('Coding CLI creation timed out after 30 seconds'))
118+
}, CODING_CLI_CREATE_TIMEOUT_MS)
119+
})
120+
121+
return Promise.race([mainPromise, timeoutPromise])
102122
}
103123
)

test/unit/client/store/codingCliThunks.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,37 @@ describe('codingCliThunks', () => {
8181
expect(store.getState().codingCli.sessions['session-123']).toBeDefined()
8282
})
8383

84+
it('times out after 30s and sets tab to error state', async () => {
85+
vi.useFakeTimers()
86+
try {
87+
const store = createStore()
88+
89+
const promise = store.dispatch(
90+
createCodingCliTab({ provider: 'codex', prompt: 'Slow creation' })
91+
)
92+
93+
const tab = store.getState().tabs.tabs[0]
94+
expect(tab.status).toBe('creating')
95+
96+
// Advance past the 30s timeout
97+
await vi.advanceTimersByTimeAsync(30_000)
98+
99+
const result = await promise
100+
expect(result.type).toBe('codingCli/createTab/rejected')
101+
expect(result.error?.message).toBe('Coding CLI creation timed out after 30 seconds')
102+
103+
// Tab should be in error state
104+
const updatedTab = store.getState().tabs.tabs[0]
105+
expect(updatedTab.status).toBe('error')
106+
107+
// Pending request should be cleaned up
108+
const requestId = tab.codingCliSessionId as string
109+
expect(store.getState().codingCli.pendingRequests[requestId]).toBeUndefined()
110+
} finally {
111+
vi.useRealTimers()
112+
}
113+
})
114+
84115
it('kills created session when request was canceled', async () => {
85116
const store = createStore()
86117

0 commit comments

Comments
 (0)