Skip to content

Commit 004154b

Browse files
authored
V4.0.7-r1 (#107)
### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._
1 parent 4717456 commit 004154b

157 files changed

Lines changed: 23396 additions & 1901 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/app/src/components/dialog-fork.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { extractPromptFromParts } from "@/utils/prompt"
1111
import type { TextPart as SDKTextPart } from "@deepagent-code/sdk/v2/client"
1212
import { base64Encode } from "@deepagent-code/core/util/encode"
1313
import { useLanguage } from "@/context/language"
14+
import { Identifier } from "@/utils/id"
1415

1516
interface ForkableMessage {
1617
id: string
@@ -30,6 +31,8 @@ export const DialogFork: Component = () => {
3031
const prompt = usePrompt()
3132
const dialog = useDialog()
3233
const language = useLanguage()
34+
const pendingIntents = new Map<string, string>()
35+
const pendingRequests = new Set<string>()
3336

3437
const messages = createMemo((): ForkableMessage[] => {
3538
const sessionID = params.id
@@ -68,13 +71,19 @@ export const DialogFork: Component = () => {
6871
})
6972
const dir = base64Encode(sdk.directory)
7073

74+
const intentKey = `${sessionID}:${item.id}`
75+
if (pendingRequests.has(intentKey)) return
76+
const intentID = pendingIntents.get(intentKey) ?? Identifier.ascending("fork")
77+
pendingIntents.set(intentKey, intentID)
78+
pendingRequests.add(intentKey)
7179
sdk.client.session
72-
.fork({ sessionID, messageID: item.id })
80+
.fork({ sessionID, messageID: item.id, intentID })
7381
.then((forked) => {
7482
if (!forked.data) {
7583
showToast({ title: language.t("common.requestFailed") })
7684
return
7785
}
86+
pendingIntents.delete(intentKey)
7887
dialog.close()
7988
prompt.set(restored, undefined, { dir, id: forked.data.id })
8089
navigate(`/${dir}/session/${forked.data.id}`)
@@ -83,6 +92,7 @@ export const DialogFork: Component = () => {
8392
const message = err instanceof Error ? err.message : String(err)
8493
showToast({ title: language.t("common.requestFailed"), description: message })
8594
})
95+
.finally(() => pendingRequests.delete(intentKey))
8696
}
8797

8898
return (

packages/app/src/components/prompt-input/submit.test.ts

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"
2-
import type { Prompt } from "@/context/prompt"
2+
import type { ContextItem, Prompt } from "@/context/prompt"
33

44
let createPromptSubmit: typeof import("./submit").createPromptSubmit
55

@@ -16,6 +16,7 @@ const optimistic: Array<{
1616
}
1717
}> = []
1818
const optimisticSeeded: boolean[] = []
19+
const optimisticRemoved: string[] = []
1920
const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
2021
const promoted: Array<{ directory: string; sessionID: string }> = []
2122
const sentShell: string[] = []
@@ -32,6 +33,8 @@ const sentPromptAsync: Array<{
3233
directory: string
3334
metadata?: unknown
3435
text?: string
36+
parts?: Array<{ id?: string; type: string; text?: string }>
37+
messageID?: string
3538
intentID?: string
3639
intentSource?: string
3740
intentVariant?: string
@@ -47,8 +50,10 @@ let variant: string | undefined
4750
let promptMode: "direct" | "intelligence" | "wish" = "direct"
4851
let appLocale = "en"
4952
let releaseDelayedPrompt: (() => void) | undefined
53+
const rejectedAdmissionReceipts = new Set<string>()
5054

5155
const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
56+
const promptContextItems: Array<ContextItem & { key: string }> = []
5257
const flushAsyncSubmit = () => new Promise((resolve) => setTimeout(resolve, 0))
5358

5459
const clientFor = (directory: string) => {
@@ -72,6 +77,7 @@ const clientFor = (directory: string) => {
7277
promptAsync: async (payload?: {
7378
metadata?: unknown
7479
parts?: Array<{ type: string; text?: string }>
80+
messageID?: string
7581
intentID?: string
7682
intentSource?: string
7783
intentVariant?: string
@@ -80,17 +86,26 @@ const clientFor = (directory: string) => {
8086
directory,
8187
metadata: payload?.metadata,
8288
text: payload?.parts?.find((part) => part.type === "text")?.text,
89+
parts: payload?.parts,
90+
messageID: payload?.messageID,
8391
intentID: payload?.intentID,
8492
intentSource: payload?.intentSource,
8593
intentVariant: payload?.intentVariant,
8694
}
8795
sentPromptAsync.push(sent)
96+
if (
97+
(sent.text === "receipt lost" || sent.text === "Edited retry goal") &&
98+
!rejectedAdmissionReceipts.has(sent.text)
99+
) {
100+
rejectedAdmissionReceipts.add(sent.text)
101+
throw new Error("connection closed after durable admission")
102+
}
88103
if (sent.text === "prompt waits after admission") {
89104
await new Promise<void>((resolve) => {
90105
releaseDelayedPrompt = resolve
91106
})
92107
}
93-
return { data: undefined }
108+
return { data: { messageID: "msg_server_admitted", delivery: "steer" } }
94109
},
95110
command: async () => ({ data: undefined }),
96111
abort: async () => ({ data: undefined }),
@@ -139,8 +154,8 @@ const clientFor = (directory: string) => {
139154
}
140155
}
141156
const result = {
142-
prompt_draft_id: "prompt_draft:test:1",
143-
context_plan_id: "context_plan:test:1",
157+
prompt_draft_id: `prompt_draft:test:${preparedDrafts.length}`,
158+
context_plan_id: `context_plan:test:${preparedDrafts.length}`,
144159
state: "draft_ready",
145160
mode: payload.body?.mode ?? "intelligence",
146161
route: text === "hello" ? "general" : "code",
@@ -227,9 +242,14 @@ beforeAll(async () => {
227242
reset: () => undefined,
228243
set: () => undefined,
229244
context: {
230-
add: () => undefined,
231-
remove: () => undefined,
232-
items: () => [],
245+
add: (item: ContextItem) => {
246+
promptContextItems.push({ ...item, key: `restored:${promptContextItems.length}:${item.path}` })
247+
},
248+
remove: (key: string) => {
249+
const index = promptContextItems.findIndex((item) => item.key === key)
250+
if (index >= 0) promptContextItems.splice(index, 1)
251+
},
252+
items: () => promptContextItems,
233253
},
234254
}),
235255
}))
@@ -274,7 +294,9 @@ beforeAll(async () => {
274294
!!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title,
275295
)
276296
},
277-
remove: () => undefined,
297+
remove: (value: { messageID: string }) => {
298+
optimisticRemoved.push(value.messageID)
299+
},
278300
},
279301
},
280302
set: () => undefined,
@@ -338,6 +360,7 @@ beforeEach(() => {
338360
enabledAutoAccept.length = 0
339361
optimistic.length = 0
340362
optimisticSeeded.length = 0
363+
optimisticRemoved.length = 0
341364
promoted.length = 0
342365
params = {}
343366
sentShell.length = 0
@@ -347,13 +370,15 @@ beforeEach(() => {
347370
sentPromptAsync.length = 0
348371
promptPrepareEvents.length = 0
349372
promptPrepareProgress.length = 0
373+
promptContextItems.length = 0
350374
promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
351375
selected = "/repo/worktree-a"
352376
variant = undefined
353377
promptMode = "direct"
354378
appLocale = "en"
355379
releaseDelayedPrompt?.()
356380
releaseDelayedPrompt = undefined
381+
rejectedAdmissionReceipts.clear()
357382
for (const key of Object.keys(storedSessions)) delete storedSessions[key]
358383
})
359384

@@ -455,6 +480,7 @@ describe("prompt submit worktree selection", () => {
455480
model: { providerID: "provider", modelID: "model", variant: "high" },
456481
},
457482
})
483+
expect(optimisticRemoved).toHaveLength(1)
458484
})
459485

460486
test("seeds new sessions before optimistic prompts are added", async () => {
@@ -784,4 +810,113 @@ describe("prompt submit worktree selection", () => {
784810
expect(sentPromptAsync).toHaveLength(1)
785811
promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
786812
})
813+
814+
test("reuses submission identity when the durable admission receipt is lost", async () => {
815+
params = { id: "session-1" }
816+
promptValue[0] = { type: "text", content: "receipt lost", start: 0, end: 12 }
817+
const context = {
818+
type: "file" as const,
819+
path: "src/retry.ts",
820+
comment: "keep this review context",
821+
commentID: "comment-1",
822+
key: "original-ui-key",
823+
}
824+
promptContextItems.push(context, {
825+
type: "file",
826+
path: "src/reference.ts",
827+
key: "reference-ui-key",
828+
})
829+
830+
const submit = createPromptSubmit({
831+
info: () => ({ id: "session-1" }),
832+
imageAttachments: () => [],
833+
commentCount: () => 0,
834+
autoAccept: () => false,
835+
mode: () => "normal",
836+
working: () => false,
837+
editor: () => undefined,
838+
queueScroll: () => undefined,
839+
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
840+
addToHistory: () => undefined,
841+
resetHistoryNavigation: () => undefined,
842+
setMode: () => undefined,
843+
setPopover: () => undefined,
844+
onSubmit: () => undefined,
845+
})
846+
847+
const event = { preventDefault: () => undefined } as unknown as Event
848+
await submit.handleSubmit(event)
849+
await flushAsyncSubmit()
850+
851+
expect(promptContextItems.find((item) => item.commentID === context.commentID)?.key).not.toBe(context.key)
852+
853+
await submit.handleSubmit(event)
854+
await flushAsyncSubmit()
855+
856+
expect(sentPromptAsync).toHaveLength(2)
857+
expect(sentPromptAsync[0]?.messageID).toBeDefined()
858+
expect(sentPromptAsync[1]?.messageID).toBe(sentPromptAsync[0]?.messageID)
859+
expect(sentPromptAsync[0]?.intentID).toBeDefined()
860+
expect(sentPromptAsync[1]?.intentID).toBe(sentPromptAsync[0]?.intentID)
861+
expect(sentPromptAsync[1]?.parts).toEqual(sentPromptAsync[0]?.parts)
862+
863+
promptContextItems.push({ ...context, key: "new-ui-key-after-success" })
864+
await submit.handleSubmit(event)
865+
await flushAsyncSubmit()
866+
867+
expect(sentPromptAsync).toHaveLength(3)
868+
expect(sentPromptAsync[2]?.messageID).not.toBe(sentPromptAsync[0]?.messageID)
869+
expect(sentPromptAsync[2]?.intentID).not.toBe(sentPromptAsync[0]?.intentID)
870+
promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
871+
})
872+
873+
test("reuses the final prepared payload when an intelligence admission receipt is lost", async () => {
874+
params = { id: "session-1" }
875+
promptMode = "intelligence"
876+
promptValue[0] = { type: "text", content: "intelligence receipt lost", start: 0, end: 25 }
877+
let confirms = 0
878+
879+
const submit = createPromptSubmit({
880+
info: () => ({ id: "session-1" }),
881+
imageAttachments: () => [],
882+
commentCount: () => 0,
883+
autoAccept: () => false,
884+
mode: () => "normal",
885+
working: () => false,
886+
editor: () => undefined,
887+
queueScroll: () => undefined,
888+
promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
889+
addToHistory: () => undefined,
890+
resetHistoryNavigation: () => undefined,
891+
setMode: () => undefined,
892+
setPopover: () => undefined,
893+
confirmPromptDraft: async () => {
894+
confirms += 1
895+
return { editedGoal: "Edited retry goal" }
896+
},
897+
onSubmit: () => undefined,
898+
})
899+
900+
const event = { preventDefault: () => undefined } as unknown as Event
901+
await submit.handleSubmit(event)
902+
await flushAsyncSubmit()
903+
await submit.handleSubmit(event)
904+
await flushAsyncSubmit()
905+
906+
expect(preparedDrafts).toHaveLength(1)
907+
expect(confirms).toBe(1)
908+
expect(sentPromptAsync).toHaveLength(2)
909+
expect(sentPromptAsync[1]?.messageID).toBe(sentPromptAsync[0]?.messageID)
910+
expect(sentPromptAsync[1]?.intentID).toBe(sentPromptAsync[0]?.intentID)
911+
expect(sentPromptAsync[1]?.parts).toEqual(sentPromptAsync[0]?.parts)
912+
expect(sentPromptAsync[1]?.metadata).toEqual(sentPromptAsync[0]?.metadata)
913+
expect(sentPromptAsync[1]?.metadata).toMatchObject({
914+
deepagent: {
915+
prompt_pipeline: {
916+
confirmed_draft_id: "prompt_draft:test:1",
917+
},
918+
},
919+
})
920+
promptValue[0] = { type: "text", content: "ls", start: 0, end: 2 }
921+
})
787922
})

0 commit comments

Comments
 (0)