@@ -15,7 +15,7 @@ import {
1515import { createStore , produce } from "solid-js/store"
1616import { createFocusSignal } from "@solid-primitives/active-element"
1717import { useLocal } from "@/context/local"
18- import { selectionFromLines , useFile , type FileSelection } from "@/context/file"
18+ import { useFile , type FileSelection } from "@/context/file"
1919import {
2020 ContentPart ,
2121 DEFAULT_PROMPT ,
@@ -161,18 +161,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
161161 const sessionKey = createMemo ( ( ) => `${ params . dir } ${ params . id ? "/" + params . id : "" } ` )
162162 const tabs = createMemo ( ( ) => layout . tabs ( sessionKey ( ) ) )
163163 const view = createMemo ( ( ) => layout . view ( sessionKey ( ) ) )
164- const activeFile = createMemo ( ( ) => {
165- const tab = tabs ( ) . active ( )
166- if ( ! tab ) return
167- return files . pathFromTab ( tab )
168- } )
164+ const recent = createMemo ( ( ) => {
165+ const all = tabs ( ) . all ( )
166+ const active = tabs ( ) . active ( )
167+ const order = active ? [ active , ...all . filter ( ( x ) => x !== active ) ] : all
168+ const seen = new Set < string > ( )
169+ const paths : string [ ] = [ ]
170+
171+ for ( const tab of order ) {
172+ const path = files . pathFromTab ( tab )
173+ if ( ! path ) continue
174+ if ( seen . has ( path ) ) continue
175+ seen . add ( path )
176+ paths . push ( path )
177+ }
169178
170- const activeFileSelection = createMemo ( ( ) => {
171- const path = activeFile ( )
172- if ( ! path ) return
173- const range = files . selectedLines ( path )
174- if ( ! range ) return
175- return selectionFromLines ( range )
179+ return paths
176180 } )
177181 const info = createMemo ( ( ) => ( params . id ? sync . session . get ( params . id ) : undefined ) )
178182 const status = createMemo (
@@ -393,7 +397,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
393397 if ( ! isFocused ( ) ) setComposing ( false )
394398 } )
395399
396- type AtOption = { type : "agent" ; name : string ; display : string } | { type : "file" ; path : string ; display : string }
400+ type AtOption =
401+ | { type : "agent" ; name : string ; display : string }
402+ | { type : "file" ; path : string ; display : string ; recent ?: boolean }
397403
398404 const agentList = createMemo ( ( ) =>
399405 sync . data . agent
@@ -424,12 +430,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
424430 } = useFilteredList < AtOption > ( {
425431 items : async ( query ) => {
426432 const agents = agentList ( )
433+ const open = recent ( )
434+ const seen = new Set ( open )
435+ const pinned : AtOption [ ] = open . map ( ( path ) => ( { type : "file" , path, display : path , recent : true } ) )
427436 const paths = await files . searchFilesAndDirectories ( query )
428- const fileOptions : AtOption [ ] = paths . map ( ( path ) => ( { type : "file" , path, display : path } ) )
429- return [ ...agents , ...fileOptions ]
437+ const fileOptions : AtOption [ ] = paths
438+ . filter ( ( path ) => ! seen . has ( path ) )
439+ . map ( ( path ) => ( { type : "file" , path, display : path } ) )
440+ return [ ...agents , ...pinned , ...fileOptions ]
430441 } ,
431442 key : atKey ,
432443 filterKeys : [ "display" ] ,
444+ groupBy : ( item ) => {
445+ if ( item . type === "agent" ) return "agent"
446+ if ( item . recent ) return "recent"
447+ return "file"
448+ } ,
449+ sortGroupsBy : ( a , b ) => {
450+ const rank = ( category : string ) => {
451+ if ( category === "agent" ) return 0
452+ if ( category === "recent" ) return 1
453+ return 2
454+ }
455+ return rank ( a . category ) - rank ( b . category )
456+ } ,
433457 onSelect : handleAtSelect ,
434458 } )
435459
@@ -1242,37 +1266,67 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
12421266
12431267 const usedUrls = new Set ( fileAttachmentParts . map ( ( part ) => part . url ) )
12441268
1245- const contextFileParts : Array < {
1246- id : string
1247- type : "file"
1248- mime : string
1249- url : string
1250- filename ?: string
1251- } > = [ ]
1252-
1253- const addContextFile = ( path : string , selection ?: FileSelection ) => {
1254- const absolute = toAbsolutePath ( path )
1255- const query = selection ? `?start=${ selection . startLine } &end=${ selection . endLine } ` : ""
1269+ const context = prompt . context . items ( ) . slice ( )
1270+
1271+ const commentItems = context . filter ( ( item ) => item . type === "file" && ! ! item . comment ?. trim ( ) )
1272+
1273+ const contextParts : Array <
1274+ | {
1275+ id : string
1276+ type : "text"
1277+ text : string
1278+ }
1279+ | {
1280+ id : string
1281+ type : "file"
1282+ mime : string
1283+ url : string
1284+ filename ?: string
1285+ }
1286+ > = [ ]
1287+
1288+ const commentNote = ( path : string , selection : FileSelection | undefined , comment : string ) => {
1289+ const start = selection ? Math . min ( selection . startLine , selection . endLine ) : undefined
1290+ const end = selection ? Math . max ( selection . startLine , selection . endLine ) : undefined
1291+ const range =
1292+ start === undefined || end === undefined
1293+ ? "this file"
1294+ : start === end
1295+ ? `line ${ start } `
1296+ : `lines ${ start } through ${ end } `
1297+
1298+ return `The user made the following comment regarding ${ range } of ${ path } : ${ comment } `
1299+ }
1300+
1301+ const addContextFile = ( input : { path : string ; selection ?: FileSelection ; comment ?: string } ) => {
1302+ const absolute = toAbsolutePath ( input . path )
1303+ const query = input . selection ? `?start=${ input . selection . startLine } &end=${ input . selection . endLine } ` : ""
12561304 const url = `file://${ absolute } ${ query } `
1257- if ( usedUrls . has ( url ) ) return
1305+
1306+ const comment = input . comment ?. trim ( )
1307+ if ( ! comment && usedUrls . has ( url ) ) return
12581308 usedUrls . add ( url )
1259- contextFileParts . push ( {
1309+
1310+ if ( comment ) {
1311+ contextParts . push ( {
1312+ id : Identifier . ascending ( "part" ) ,
1313+ type : "text" ,
1314+ text : commentNote ( input . path , input . selection , comment ) ,
1315+ } )
1316+ }
1317+
1318+ contextParts . push ( {
12601319 id : Identifier . ascending ( "part" ) ,
12611320 type : "file" ,
12621321 mime : "text/plain" ,
12631322 url,
1264- filename : getFilename ( path ) ,
1323+ filename : getFilename ( input . path ) ,
12651324 } )
12661325 }
12671326
1268- const activePath = activeFile ( )
1269- if ( activePath && prompt . context . activeTab ( ) ) {
1270- addContextFile ( activePath , activeFileSelection ( ) )
1271- }
1272-
1273- for ( const item of prompt . context . items ( ) ) {
1327+ for ( const item of context ) {
12741328 if ( item . type !== "file" ) continue
1275- addContextFile ( item . path , item . selection )
1329+ addContextFile ( { path : item . path , selection : item . selection , comment : item . comment } )
12761330 }
12771331
12781332 const imageAttachmentParts = images . map ( ( attachment ) => ( {
@@ -1292,7 +1346,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
12921346 const requestParts = [
12931347 textPart ,
12941348 ...fileAttachmentParts ,
1295- ...contextFileParts ,
1349+ ...contextParts ,
12961350 ...agentAttachmentParts ,
12971351 ...imageAttachmentParts ,
12981352 ]
@@ -1345,6 +1399,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13451399 )
13461400 }
13471401
1402+ for ( const item of commentItems ) {
1403+ prompt . context . remove ( item . key )
1404+ }
1405+
13481406 clearInput ( )
13491407 addOptimisticMessage ( )
13501408
@@ -1363,6 +1421,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13631421 description : errorMessage ( err ) ,
13641422 } )
13651423 removeOptimisticMessage ( )
1424+ for ( const item of commentItems ) {
1425+ prompt . context . add ( {
1426+ type : "file" ,
1427+ path : item . path ,
1428+ selection : item . selection ,
1429+ comment : item . comment ,
1430+ commentID : item . commentID ,
1431+ preview : item . preview ,
1432+ } )
1433+ }
13661434 restoreInput ( )
13671435 } )
13681436 }
@@ -1487,49 +1555,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
14871555 </ div >
14881556 </ div >
14891557 </ Show >
1490- < Show when = { prompt . context . items ( ) . length > 0 || ! ! activeFile ( ) } >
1558+ < Show when = { prompt . context . items ( ) . length > 0 } >
14911559 < div class = "flex flex-nowrap items-start gap-1.5 px-3 pt-3 overflow-x-auto no-scrollbar" >
1492- < Show when = { prompt . context . activeTab ( ) ? activeFile ( ) : undefined } >
1493- { ( path ) => (
1494- < div class = "shrink-0 flex flex-col gap-1 rounded-md bg-surface-base border border-border-base px-2 py-1 max-w-[320px]" >
1495- < div class = "flex items-center gap-1.5" >
1496- < FileIcon node = { { path : path ( ) , type : "file" } } class = "shrink-0 size-3.5" />
1497- < div class = "flex items-center text-11-regular min-w-0" >
1498- < span class = "text-text-weak whitespace-nowrap truncate min-w-0" > { getDirectory ( path ( ) ) } </ span >
1499- < span class = "text-text-strong whitespace-nowrap" > { getFilename ( path ( ) ) } </ span >
1500- < Show when = { activeFileSelection ( ) } >
1501- { ( sel ) => (
1502- < span class = "text-text-weak whitespace-nowrap ml-1" >
1503- { sel ( ) . startLine === sel ( ) . endLine
1504- ? `:${ sel ( ) . startLine } `
1505- : `:${ sel ( ) . startLine } -${ sel ( ) . endLine } ` }
1506- </ span >
1507- ) }
1508- </ Show >
1509- < span class = "text-text-weak whitespace-nowrap ml-1" > { language . t ( "prompt.context.active" ) } </ span >
1510- </ div >
1511- < IconButton
1512- type = "button"
1513- icon = "close"
1514- variant = "ghost"
1515- class = "h-5 w-5"
1516- onClick = { ( ) => prompt . context . removeActive ( ) }
1517- aria-label = { language . t ( "prompt.context.removeActiveFile" ) }
1518- />
1519- </ div >
1520- </ div >
1521- ) }
1522- </ Show >
1523- < Show when = { ! prompt . context . activeTab ( ) && ! ! activeFile ( ) } >
1524- < button
1525- type = "button"
1526- class = "shrink-0 flex items-center gap-1.5 px-1.5 py-0.5 rounded-md bg-surface-base border border-border-base text-11-regular text-text-weak hover:bg-surface-raised-base-hover"
1527- onClick = { ( ) => prompt . context . addActive ( ) }
1528- >
1529- < Icon name = "plus-small" size = "small" />
1530- < span > { language . t ( "prompt.context.includeActiveFile" ) } </ span >
1531- </ button >
1532- </ Show >
15331560 < For each = { prompt . context . items ( ) } >
15341561 { ( item ) => {
15351562 const preview = createMemo ( ( ) => selectionPreview ( item . path , item . selection , item . preview ) )
0 commit comments