@@ -9,7 +9,7 @@ import { useLanguage } from "@/context/language"
99import { useLayout } from "@/context/layout"
1010import { useLocal } from "@/context/local"
1111import { usePermission } from "@/context/permission"
12- import { type ImageAttachmentPart , type Prompt , usePrompt } from "@/context/prompt"
12+ import { type ContextItem , type ImageAttachmentPart , type Prompt , usePrompt } from "@/context/prompt"
1313import { useSDK } from "@/context/sdk"
1414import { useSync } from "@/context/sync"
1515import { Identifier } from "@/utils/id"
@@ -25,6 +25,145 @@ type PendingPrompt = {
2525
2626const pending = new Map < string , PendingPrompt > ( )
2727
28+ export type FollowupDraft = {
29+ sessionID : string
30+ sessionDirectory : string
31+ prompt : Prompt
32+ context : ( ContextItem & { key : string } ) [ ]
33+ agent : string
34+ model : { providerID : string ; modelID : string }
35+ variant ?: string
36+ }
37+
38+ type FollowupSendInput = {
39+ client : ReturnType < typeof useSDK > [ "client" ]
40+ globalSync : ReturnType < typeof useGlobalSync >
41+ sync : ReturnType < typeof useSync >
42+ draft : FollowupDraft
43+ messageID ?: string
44+ optimisticBusy ?: boolean
45+ before ?: ( ) => Promise < boolean > | boolean
46+ }
47+
48+ const draftText = ( prompt : Prompt ) => prompt . map ( ( part ) => ( "content" in part ? part . content : "" ) ) . join ( "" )
49+
50+ const draftImages = ( prompt : Prompt ) => prompt . filter ( ( part ) : part is ImageAttachmentPart => part . type === "image" )
51+
52+ export async function sendFollowupDraft ( input : FollowupSendInput ) {
53+ const text = draftText ( input . draft . prompt )
54+ const images = draftImages ( input . draft . prompt )
55+ const [ , setStore ] = input . globalSync . child ( input . draft . sessionDirectory )
56+
57+ const setBusy = ( ) => {
58+ if ( ! input . optimisticBusy ) return
59+ setStore ( "session_status" , input . draft . sessionID , { type : "busy" } )
60+ }
61+
62+ const setIdle = ( ) => {
63+ if ( ! input . optimisticBusy ) return
64+ setStore ( "session_status" , input . draft . sessionID , { type : "idle" } )
65+ }
66+
67+ const wait = async ( ) => {
68+ const ok = await input . before ?.( )
69+ if ( ok === false ) return false
70+ return true
71+ }
72+
73+ const [ head , ...tail ] = text . split ( " " )
74+ const cmd = head ?. startsWith ( "/" ) ? head . slice ( 1 ) : undefined
75+ if ( cmd && input . sync . data . command . find ( ( item ) => item . name === cmd ) ) {
76+ setBusy ( )
77+ try {
78+ if ( ! ( await wait ( ) ) ) {
79+ setIdle ( )
80+ return false
81+ }
82+
83+ await input . client . session . command ( {
84+ sessionID : input . draft . sessionID ,
85+ command : cmd ,
86+ arguments : tail . join ( " " ) ,
87+ agent : input . draft . agent ,
88+ model : `${ input . draft . model . providerID } /${ input . draft . model . modelID } ` ,
89+ variant : input . draft . variant ,
90+ parts : images . map ( ( attachment ) => ( {
91+ id : Identifier . ascending ( "part" ) ,
92+ type : "file" as const ,
93+ mime : attachment . mime ,
94+ url : attachment . dataUrl ,
95+ filename : attachment . filename ,
96+ } ) ) ,
97+ } )
98+ return true
99+ } catch ( err ) {
100+ setIdle ( )
101+ throw err
102+ }
103+ }
104+
105+ const messageID = input . messageID ?? Identifier . ascending ( "message" )
106+ const { requestParts, optimisticParts } = buildRequestParts ( {
107+ prompt : input . draft . prompt ,
108+ context : input . draft . context ,
109+ images,
110+ text,
111+ sessionID : input . draft . sessionID ,
112+ messageID,
113+ sessionDirectory : input . draft . sessionDirectory ,
114+ } )
115+
116+ const message : Message = {
117+ id : messageID ,
118+ sessionID : input . draft . sessionID ,
119+ role : "user" ,
120+ time : { created : Date . now ( ) } ,
121+ agent : input . draft . agent ,
122+ model : input . draft . model ,
123+ variant : input . draft . variant ,
124+ }
125+
126+ const add = ( ) =>
127+ input . sync . session . optimistic . add ( {
128+ directory : input . draft . sessionDirectory ,
129+ sessionID : input . draft . sessionID ,
130+ message,
131+ parts : optimisticParts ,
132+ } )
133+
134+ const remove = ( ) =>
135+ input . sync . session . optimistic . remove ( {
136+ directory : input . draft . sessionDirectory ,
137+ sessionID : input . draft . sessionID ,
138+ messageID,
139+ } )
140+
141+ setBusy ( )
142+ add ( )
143+
144+ try {
145+ if ( ! ( await wait ( ) ) ) {
146+ setIdle ( )
147+ remove ( )
148+ return false
149+ }
150+
151+ await input . client . session . promptAsync ( {
152+ sessionID : input . draft . sessionID ,
153+ agent : input . draft . agent ,
154+ model : input . draft . model ,
155+ messageID,
156+ parts : requestParts ,
157+ variant : input . draft . variant ,
158+ } )
159+ return true
160+ } catch ( err ) {
161+ setIdle ( )
162+ remove ( )
163+ throw err
164+ }
165+ }
166+
28167type PromptSubmitInput = {
29168 info : Accessor < { id : string } | undefined >
30169 imageAttachments : Accessor < ImageAttachmentPart [ ] >
@@ -41,6 +180,9 @@ type PromptSubmitInput = {
41180 setPopover : ( popover : "at" | "slash" | null ) => void
42181 newSessionWorktree ?: Accessor < string | undefined >
43182 onNewSessionWorktreeReset ?: ( ) => void
183+ shouldQueue ?: Accessor < boolean >
184+ onQueue ?: ( draft : FollowupDraft ) => void
185+ onAbort ?: ( ) => void
44186 onSubmit ?: ( ) => void
45187}
46188
@@ -82,6 +224,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
82224 const [ , setStore ] = globalSync . child ( sdk . directory )
83225 setStore ( "todo" , sessionID , [ ] )
84226
227+ input . onAbort ?.( )
228+
85229 const queued = pending . get ( sessionID )
86230 if ( queued ) {
87231 queued . abort . abort ( )
@@ -116,6 +260,12 @@ export function createPromptSubmit(input: PromptSubmitInput) {
116260 }
117261 }
118262
263+ const clearContext = ( ) => {
264+ for ( const item of prompt . context . items ( ) ) {
265+ prompt . context . remove ( item . key )
266+ }
267+ }
268+
119269 const handleSubmit = async ( event : Event ) => {
120270 event . preventDefault ( )
121271
@@ -215,14 +365,22 @@ export function createPromptSubmit(input: PromptSubmitInput) {
215365 return
216366 }
217367
218- input . onSubmit ?.( )
219-
220368 const model = {
221369 modelID : currentModel . id ,
222370 providerID : currentModel . provider . id ,
223371 }
224372 const agent = currentAgent . name
225373 const variant = local . model . variant . current ( )
374+ const context = prompt . context . items ( ) . slice ( )
375+ const draft : FollowupDraft = {
376+ sessionID : session . id ,
377+ sessionDirectory,
378+ prompt : currentPrompt ,
379+ context,
380+ agent,
381+ model,
382+ variant,
383+ }
226384
227385 const clearInput = ( ) => {
228386 prompt . reset ( )
@@ -243,6 +401,15 @@ export function createPromptSubmit(input: PromptSubmitInput) {
243401 } )
244402 }
245403
404+ if ( ! isNewSession && mode === "normal" && input . shouldQueue ?.( ) ) {
405+ input . onQueue ?.( draft )
406+ clearContext ( )
407+ clearInput ( )
408+ return
409+ }
410+
411+ input . onSubmit ?.( )
412+
246413 if ( mode === "shell" ) {
247414 clearInput ( )
248415 client . session
@@ -295,48 +462,19 @@ export function createPromptSubmit(input: PromptSubmitInput) {
295462 }
296463 }
297464
298- const context = prompt . context . items ( ) . slice ( )
299465 const commentItems = context . filter ( ( item ) => item . type === "file" && ! ! item . comment ?. trim ( ) )
300-
301466 const messageID = Identifier . ascending ( "message" )
302- const { requestParts, optimisticParts } = buildRequestParts ( {
303- prompt : currentPrompt ,
304- context,
305- images,
306- text,
307- sessionID : session . id ,
308- messageID,
309- sessionDirectory,
310- } )
311-
312- const optimisticMessage : Message = {
313- id : messageID ,
314- sessionID : session . id ,
315- role : "user" ,
316- time : { created : Date . now ( ) } ,
317- agent,
318- model,
319- variant,
320- }
321-
322- const addOptimisticMessage = ( ) =>
323- sync . session . optimistic . add ( {
324- directory : sessionDirectory ,
325- sessionID : session . id ,
326- message : optimisticMessage ,
327- parts : optimisticParts ,
328- } )
329467
330- const removeOptimisticMessage = ( ) =>
468+ const removeOptimisticMessage = ( ) => {
331469 sync . session . optimistic . remove ( {
332470 directory : sessionDirectory ,
333471 sessionID : session . id ,
334472 messageID,
335473 } )
474+ }
336475
337476 removeCommentItems ( commentItems )
338477 clearInput ( )
339- addOptimisticMessage ( )
340478
341479 const waitForWorktree = async ( ) => {
342480 const worktree = WorktreeState . get ( sessionDirectory )
@@ -393,20 +531,15 @@ export function createPromptSubmit(input: PromptSubmitInput) {
393531 return true
394532 }
395533
396- const send = async ( ) => {
397- const ok = await waitForWorktree ( )
398- if ( ! ok ) return
399- await client . session . promptAsync ( {
400- sessionID : session . id ,
401- agent,
402- model,
403- messageID,
404- parts : requestParts ,
405- variant,
406- } )
407- }
408-
409- void send ( ) . catch ( ( err ) => {
534+ void sendFollowupDraft ( {
535+ client,
536+ sync,
537+ globalSync,
538+ draft,
539+ messageID,
540+ optimisticBusy : sessionDirectory === projectDirectory ,
541+ before : waitForWorktree ,
542+ } ) . catch ( ( err ) => {
410543 pending . delete ( session . id )
411544 if ( sessionDirectory === projectDirectory ) {
412545 sync . set ( "session_status" , session . id , { type : "idle" } )
0 commit comments