@@ -7,6 +7,7 @@ import { Locale } from "@/util/locale"
77import { useProject } from "@tui/context/project"
88import { useTheme } from "../context/theme"
99import { useSDK } from "../context/sdk"
10+ import { useLocal } from "../context/local"
1011import { Flag } from "@opencode-ai/core/flag/flag"
1112import { DialogSessionRename } from "./dialog-session-rename"
1213import { createDebouncedSignal } from "../util/signal"
@@ -25,6 +26,7 @@ export function DialogSessionList() {
2526 const project = useProject ( )
2627 const { theme } = useTheme ( )
2728 const sdk = useSDK ( )
29+ const local = useLocal ( )
2830 const toast = useToast ( )
2931 const [ toDelete , setToDelete ] = createSignal < string > ( )
3032 const [ search , setSearch ] = createDebouncedSignal ( "" , 150 )
@@ -128,6 +130,8 @@ export function DialogSessionList() {
128130
129131 const [ browseOrder ] = createSignal < string [ ] > ( orderByRecency ( sync . data . session ) )
130132
133+ const RECENT_LIMIT = 5
134+
131135 const options = createMemo ( ( ) => {
132136 const today = new Date ( ) . toDateString ( )
133137 const sessionMap = new Map (
@@ -139,46 +143,72 @@ export function DialogSessionList() {
139143 const searchResult = searchResults ( )
140144 const displayOrder = searchResult ? orderByRecency ( searchResult ) : browseOrder ( )
141145
142- return displayOrder
143- . map ( ( id ) => sessionMap . get ( id ) )
144- . filter ( ( x ) => x !== undefined )
145- . map ( ( x ) => {
146- const workspace = x . workspaceID ? project . workspace . get ( x . workspaceID ) : undefined
146+ const dismissed = new Set ( local . session . dismissedRecent ( ) )
147+ const pinned = local . session . pinned ( ) . filter ( ( id ) => sessionMap . has ( id ) )
148+ const pinnedSet = new Set ( pinned )
149+ const slotByID = new Map ( local . session . slots ( ) . map ( ( id , i ) => [ id , i + 1 ] ) )
147150
148- let footer : JSX . Element | string = ""
149- if ( Flag . OPENCODE_EXPERIMENTAL_WORKSPACES ) {
150- if ( x . workspaceID ) {
151- footer = workspace ? (
152- < WorkspaceLabel
153- type = { workspace . type }
154- name = { workspace . name }
155- status = { project . workspace . status ( x . workspaceID ) ?? "error" }
156- />
157- ) : (
158- < WorkspaceLabel type = "unknown" name = { x . workspaceID } status = "error" />
159- )
160- }
161- } else {
162- footer = Locale . time ( x . time . updated )
163- }
151+ const recent = displayOrder
152+ . filter ( ( id ) => ! pinnedSet . has ( id ) && ! dismissed . has ( id ) )
153+ . slice ( 0 , RECENT_LIMIT )
154+ const recentSet = new Set ( recent )
164155
165- const date = new Date ( x . time . updated )
166- let category = date . toDateString ( )
167- if ( category === today ) {
168- category = "Today"
169- }
170- const isDeleting = toDelete ( ) === x . id
171- const status = sync . data . session_status ?. [ x . id ]
172- const isWorking = status ?. type === "busy" || status ?. type === "retry"
173- return {
174- title : isDeleting ? `Press ${ deleteHint ( ) } again to confirm` : x . title ,
175- bg : isDeleting ? theme . error : undefined ,
176- value : x . id ,
177- category,
178- footer,
179- gutter : isWorking ? ( ) => < Spinner /> : undefined ,
156+ function buildOption ( id : string , category : string ) {
157+ const x = sessionMap . get ( id )
158+ if ( ! x ) return undefined
159+ const workspace = x . workspaceID ? project . workspace . get ( x . workspaceID ) : undefined
160+
161+ let footer : JSX . Element | string = ""
162+ if ( Flag . OPENCODE_EXPERIMENTAL_WORKSPACES ) {
163+ if ( x . workspaceID ) {
164+ footer = workspace ? (
165+ < WorkspaceLabel
166+ type = { workspace . type }
167+ name = { workspace . name }
168+ status = { project . workspace . status ( x . workspaceID ) ?? "error" }
169+ />
170+ ) : (
171+ < WorkspaceLabel type = "unknown" name = { x . workspaceID } status = "error" />
172+ )
180173 }
174+ } else {
175+ footer = Locale . time ( x . time . updated )
176+ }
177+
178+ const isDeleting = toDelete ( ) === x . id
179+ const status = sync . data . session_status ?. [ x . id ]
180+ const isWorking = status ?. type === "busy" || status ?. type === "retry"
181+ const slot = slotByID . get ( x . id )
182+ const gutter = isWorking
183+ ? ( ) => < Spinner />
184+ : slot !== undefined
185+ ? ( ) => < text fg = { theme . accent } > { slot } </ text >
186+ : undefined
187+ return {
188+ title : isDeleting ? `Press ${ deleteHint ( ) } again to confirm` : x . title ,
189+ bg : isDeleting ? theme . error : undefined ,
190+ value : x . id ,
191+ category,
192+ footer,
193+ gutter,
194+ }
195+ }
196+
197+ const remaining = displayOrder
198+ . filter ( ( id ) => ! pinnedSet . has ( id ) && ! recentSet . has ( id ) )
199+ . map ( ( id ) => {
200+ const x = sessionMap . get ( id )
201+ if ( ! x ) return undefined
202+ const label = new Date ( x . time . updated ) . toDateString ( )
203+ return buildOption ( id , label === today ? "Today" : label )
181204 } )
205+ . filter ( ( x ) => x !== undefined )
206+
207+ return [
208+ ...pinned . map ( ( id ) => buildOption ( id , "Pinned" ) ) . filter ( ( x ) => x !== undefined ) ,
209+ ...recent . map ( ( id ) => buildOption ( id , "Recent" ) ) . filter ( ( x ) => x !== undefined ) ,
210+ ...remaining ,
211+ ]
182212 } )
183213
184214 onMount ( ( ) => {
@@ -203,6 +233,28 @@ export function DialogSessionList() {
203233 dialog . clear ( )
204234 } }
205235 actions = { [
236+ {
237+ command : "session.pin.toggle" ,
238+ title : "pin/unpin" ,
239+ onTrigger : ( option ) => {
240+ local . session . togglePin ( option . value )
241+ } ,
242+ } ,
243+ {
244+ command : "session.toggle.recent" ,
245+ title : "toggle recent" ,
246+ onTrigger : ( option ) => {
247+ if ( local . session . isPinned ( option . value ) ) {
248+ toast . show ( {
249+ variant : "info" ,
250+ message : "Unpin the session first to toggle it in Recent" ,
251+ duration : 3000 ,
252+ } )
253+ return
254+ }
255+ local . session . toggleRecent ( option . value )
256+ } ,
257+ } ,
206258 {
207259 command : "session.delete" ,
208260 title : "delete" ,
0 commit comments