Skip to content

Commit 8576b37

Browse files
committed
add esc and ctrl-c to cancel
1 parent d859b3c commit 8576b37

File tree

5 files changed

+111
-85
lines changed

5 files changed

+111
-85
lines changed

cli/src/hooks/use-keyboard-handlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export const useKeyboardHandlers = ({
4949
if (abortControllerRef.current) {
5050
abortControllerRef.current.abort()
5151
}
52+
53+
return
5254
}
5355

5456
if (isCtrlC) {

cli/src/hooks/use-send-message.ts

Lines changed: 61 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,46 @@ export const useSendMessage = ({
616616

617617
const abortController = new AbortController()
618618
abortControllerRef.current = abortController
619+
abortController.signal.addEventListener('abort', () => {
620+
setIsStreaming(false)
621+
setCanProcessQueue(true)
622+
updateChainInProgress(false)
623+
setIsWaitingForResponse(false)
624+
timerController.stop('aborted')
625+
626+
applyMessageUpdate((prev) =>
627+
prev.map((msg) => {
628+
if (msg.id !== aiMessageId) {
629+
return msg
630+
}
631+
632+
const blocks: ContentBlock[] = msg.blocks ? [...msg.blocks] : []
633+
const lastBlock = blocks[blocks.length - 1]
634+
635+
if (lastBlock && lastBlock.type === 'text') {
636+
const interruptedBlock: ContentBlock = {
637+
type: 'text',
638+
content: `${lastBlock.content}\n\n[response interrupted]`,
639+
}
640+
return {
641+
...msg,
642+
blocks: [...blocks.slice(0, -1), interruptedBlock],
643+
isComplete: true,
644+
}
645+
}
646+
647+
const interruptionNotice: ContentBlock = {
648+
type: 'text',
649+
content: '[response interrupted]',
650+
}
651+
return {
652+
...msg,
653+
blocks: [...blocks, interruptionNotice],
654+
isComplete: true,
655+
}
656+
}),
657+
)
658+
})
619659

620660
try {
621661
// Load local agent definitions from .agents directory
@@ -1306,6 +1346,11 @@ export const useSendMessage = ({
13061346
},
13071347
})
13081348

1349+
if (!result.success) {
1350+
logger.warn({ error: result.error }, 'Agent run failed')
1351+
return
1352+
}
1353+
13091354
setIsStreaming(false)
13101355
setCanProcessQueue(true)
13111356
updateChainInProgress(false)
@@ -1342,8 +1387,6 @@ export const useSendMessage = ({
13421387

13431388
previousRunStateRef.current = result
13441389
} catch (error) {
1345-
const isAborted = error instanceof Error && error.name === 'AbortError'
1346-
13471390
logger.error(
13481391
{ error: getErrorObject(error) },
13491392
'SDK client.run() failed',
@@ -1352,61 +1395,26 @@ export const useSendMessage = ({
13521395
setCanProcessQueue(true)
13531396
updateChainInProgress(false)
13541397
setIsWaitingForResponse(false)
1355-
timerController.stop(isAborted ? 'aborted' : 'error')
1356-
1357-
if (isAborted) {
1358-
applyMessageUpdate((prev) =>
1359-
prev.map((msg) => {
1360-
if (msg.id !== aiMessageId) {
1361-
return msg
1362-
}
1363-
1364-
const blocks: ContentBlock[] = msg.blocks ? [...msg.blocks] : []
1365-
const lastBlock = blocks[blocks.length - 1]
1398+
timerController.stop('error')
13661399

1367-
if (lastBlock && lastBlock.type === 'text') {
1368-
const interruptedBlock: ContentBlock = {
1369-
type: 'text',
1370-
content: `${lastBlock.content}\n\n[response interrupted]`,
1371-
}
1372-
return {
1400+
const errorMessage =
1401+
error instanceof Error ? error.message : 'Unknown error occurred'
1402+
applyMessageUpdate((prev) =>
1403+
prev.map((msg) =>
1404+
msg.id === aiMessageId
1405+
? {
13731406
...msg,
1374-
blocks: [...blocks.slice(0, -1), interruptedBlock],
1375-
isComplete: true,
1407+
content: msg.content + `\n\n**Error:** ${errorMessage}`,
13761408
}
1377-
}
1409+
: msg,
1410+
),
1411+
)
13781412

1379-
const interruptionNotice: ContentBlock = {
1380-
type: 'text',
1381-
content: '[response interrupted]',
1382-
}
1383-
return {
1384-
...msg,
1385-
blocks: [...blocks, interruptionNotice],
1386-
isComplete: true,
1387-
}
1388-
}),
1389-
)
1390-
} else {
1391-
const errorMessage =
1392-
error instanceof Error ? error.message : 'Unknown error occurred'
1393-
applyMessageUpdate((prev) =>
1394-
prev.map((msg) =>
1395-
msg.id === aiMessageId
1396-
? {
1397-
...msg,
1398-
content: msg.content + `\n\n**Error:** ${errorMessage}`,
1399-
}
1400-
: msg,
1401-
),
1402-
)
1403-
1404-
applyMessageUpdate((prev) =>
1405-
prev.map((msg) =>
1406-
msg.id === aiMessageId ? { ...msg, isComplete: true } : msg,
1407-
),
1408-
)
1409-
}
1413+
applyMessageUpdate((prev) =>
1414+
prev.map((msg) =>
1415+
msg.id === aiMessageId ? { ...msg, isComplete: true } : msg,
1416+
),
1417+
)
14101418
}
14111419
},
14121420
[

sdk/src/client.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { BACKEND_URL, WEBSITE_URL } from './constants'
1+
import { WEBSITE_URL } from './constants'
22
import { run } from './run'
33
import { API_KEY_ENV_VAR } from '../../common/src/old-constants'
44

55
import type { RunOptions, CodebuffClientOptions } from './run'
66
import type { RunState } from './run-state'
7+
import type { ErrorOr } from '@codebuff/common/util/error'
78

89
export class CodebuffClient {
910
public options: CodebuffClientOptions & {
@@ -48,11 +49,13 @@ export class CodebuffClient {
4849
* @param maxAgentSteps - (Optional) Maximum number of steps the agent can take before stopping. Use this as a safety measure in case your agent starts going off the rails. A reasonable number is around 20.
4950
* @param env - (Optional) Environment variables to pass to terminal commands executed by the agent. These will be merged with process.env, with the custom values taking precedence. Can also be provided in individual run() calls to override.
5051
*
51-
* @returns A Promise that resolves to a RunState JSON object which you can pass to a subsequent run() call to continue the run. Use result.output to get the agent's output.
52+
* @returns A Promise that resolves to one of:
53+
* - { "success": true, value: runState } (a RunState JSON object which you can pass to a subsequent run() call to continue the run. Use result.output to get the agent's output.)
54+
* - { "success": false, error: error } (a JSON object containing `name`, `message`, and `stack` properties)
5255
*/
5356
public async run(
5457
options: RunOptions & CodebuffClientOptions,
55-
): Promise<RunState> {
58+
): Promise<ErrorOr<RunState>> {
5659
return run({ ...this.options, ...options })
5760
}
5861

sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type * from '../../common/src/types/json'
22
export type * from '../../common/src/types/messages/codebuff-message'
33
export type * from '../../common/src/types/messages/data-content'
44
export type * from '../../common/src/types/print-mode'
5+
export type * from '../../common/src/util/error'
56
export type * from './run'
67
// Agent type exports
78
export type { AgentDefinition } from '../../common/src/templates/initial-agents-dir/types/agent-definition'

sdk/src/run.ts

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { toOptionalFile } from '@codebuff/common/old-constants'
77
import { toolNames } from '@codebuff/common/tools/constants'
88
import { clientToolCallSchema } from '@codebuff/common/tools/list'
99
import { AgentOutputSchema } from '@codebuff/common/types/session-state'
10+
import { failure, success } from '@codebuff/common/util/error'
1011
import { cloneDeep } from 'lodash'
1112

1213
import { getAgentRuntimeImpl } from './impl/agent-runtime'
@@ -44,6 +45,7 @@ import type {
4445
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
4546
import type { SessionState } from '@codebuff/common/types/session-state'
4647
import type { Source } from '@codebuff/common/types/source'
48+
import type { ErrorOr, Failure } from '@codebuff/common/util/error'
4749

4850
export type CodebuffClientOptions = {
4951
apiKey?: string
@@ -98,12 +100,13 @@ export type RunOptions = {
98100
signal?: AbortSignal
99101
}
100102

101-
function checkAborted(signal?: AbortSignal) {
102-
if (signal?.aborted) {
103-
const error = new Error('Run cancelled by user')
104-
error.name = 'AbortError'
105-
throw error
103+
function checkAborted(signal?: AbortSignal): Failure | null {
104+
if (!signal?.aborted) {
105+
return null
106106
}
107+
const error = new Error('Run cancelled by user')
108+
error.name = 'AbortError'
109+
return failure(error)
107110
}
108111

109112
type RunReturnType = Awaited<ReturnType<typeof run>>
@@ -137,8 +140,11 @@ export async function run({
137140
CodebuffClientOptions & {
138141
apiKey: string
139142
fingerprintId: string
140-
}): Promise<RunState> {
141-
checkAborted(signal)
143+
}): Promise<ErrorOr<RunState>> {
144+
const aborted = checkAborted(signal)
145+
if (aborted) {
146+
return aborted
147+
}
142148

143149
const fs = await (typeof fsSource === 'function' ? fsSource() : fsSource)
144150

@@ -168,7 +174,11 @@ export async function run({
168174
const onResponseChunk = async (
169175
action: ServerAction<'response-chunk'>,
170176
): Promise<void> => {
171-
checkAborted(signal)
177+
const aborted = checkAborted(signal)
178+
if (aborted) {
179+
resolve(aborted)
180+
return
181+
}
172182
const { chunk } = action
173183
if (typeof chunk !== 'string') {
174184
if (chunk.type === 'reasoning') {
@@ -205,7 +215,11 @@ export async function run({
205215
const onSubagentResponseChunk = async (
206216
action: ServerAction<'subagent-response-chunk'>,
207217
) => {
208-
checkAborted(signal)
218+
const aborted = checkAborted(signal)
219+
if (aborted) {
220+
resolve(aborted)
221+
return
222+
}
209223
const { agentId, agentType, chunk } = action
210224

211225
if (handleStreamChunk) {
@@ -381,7 +395,10 @@ export async function run({
381395
const promptId = Math.random().toString(36).substring(2, 15)
382396

383397
// Send input
384-
checkAborted(signal)
398+
const isAborted = checkAborted(signal)
399+
if (isAborted) {
400+
return isAborted
401+
}
385402

386403
const userInfo = await getUserInfoFromApiKey({
387404
...agentRuntimeImpl,
@@ -570,13 +587,15 @@ async function handlePromptResponse({
570587
}) {
571588
if (action.type === 'prompt-error') {
572589
onError({ message: action.message })
573-
resolve({
574-
sessionState: initialSessionState,
575-
output: {
576-
type: 'error',
577-
message: action.message,
578-
},
579-
})
590+
resolve(
591+
failure({
592+
sessionState: initialSessionState,
593+
output: {
594+
type: 'error',
595+
message: action.message,
596+
},
597+
}),
598+
)
580599
} else if (action.type === 'prompt-response') {
581600
// Stop enforcing session state schema! It's a black box we will pass back to the server.
582601
// Only check the output schema.
@@ -588,10 +607,7 @@ async function handlePromptResponse({
588607
'If this issues persists, please contact [email protected]',
589608
].join('\n')
590609
onError({ message })
591-
resolve({
592-
sessionState: initialSessionState,
593-
output: { type: 'error', message },
594-
})
610+
resolve(failure(new Error(message)))
595611
return
596612
}
597613
const { sessionState, output } = action
@@ -603,18 +619,14 @@ async function handlePromptResponse({
603619
message: 'No output from agent',
604620
},
605621
}
606-
resolve(state)
622+
resolve(success(state))
607623
} else {
608624
action satisfies never
609625
onError({
610626
message: 'Internal error: prompt response type not handled',
611627
})
612-
resolve({
613-
sessionState: initialSessionState,
614-
output: {
615-
type: 'error',
616-
message: 'Internal error: prompt response type not handled',
617-
},
618-
})
628+
resolve(
629+
failure(new Error('Internal error: prompt response type not handled')),
630+
)
619631
}
620632
}

0 commit comments

Comments
 (0)