@@ -2,12 +2,14 @@ import { redirect } from 'next/navigation'
22import Link from 'next/link'
33import { getSessionToken , validateSession } from '@/lib/auth'
44import { listSessions , decodeB64 , encodeB64 , resolveProjectPath , detectContinuationChains } from '@/lib/claude-fs'
5+ import { listOpenCodeSessions , openCodeDirectory , isOpenCodePath } from '@/lib/opencode-fs'
56import { getProjectMeta } from '@/lib/project-meta'
67import { loadSessionTags } from '@/lib/session-tags'
78import Nav from '@/components/Nav'
89import ProcessControls from '@/components/ProcessControls'
910import NewSessionForm from '@/components/NewSessionForm'
1011import SessionTagsButton from '@/components/SessionTagsButton'
12+ import type { SessionInfo } from '@/lib/types'
1113
1214export const dynamic = 'force-dynamic'
1315
@@ -24,20 +26,69 @@ export default async function ProjectPage({ searchParams }: Props) {
2426 if ( ! encoded ) redirect ( '/projects' )
2527
2628 const dirName = decodeB64 ( encoded )
27- const sessions = listSessions ( dirName )
29+
30+ // ── OpenCode project ────────────────────────────────────────────────────
31+ if ( isOpenCodePath ( dirName ) ) {
32+ const directory = openCodeDirectory ( dirName )
33+ const sessions = listOpenCodeSessions ( directory )
34+ const title = directory . split ( '/' ) . pop ( ) || directory
35+
36+ return (
37+ < >
38+ < Nav />
39+ < main style = { { padding : '32px 28px' , maxWidth : 1100 , margin : '0 auto' , width : '100%' } } >
40+ < div style = { { marginBottom : 28 } } >
41+ < Link href = "/projects" style = { { color : 'var(--text2)' , fontSize : 13 , textDecoration : 'none' } } >
42+ ← Projects
43+ </ Link >
44+ < div style = { { marginTop : 10 } } >
45+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
46+ < h1 style = { { margin : '0 0 3px' , fontSize : 22 , fontWeight : 700 , letterSpacing : '-0.02em' } } >
47+ { title }
48+ </ h1 >
49+ < span className = "chip" style = { { fontSize : 11 , background : 'rgba(99,102,241,0.15)' , color : 'var(--text2)' } } >
50+ opencode
51+ </ span >
52+ </ div >
53+ < p style = { { margin : 0 , color : 'var(--text3)' , fontSize : 12 , fontFamily : 'ui-monospace, monospace' } } >
54+ { directory }
55+ </ p >
56+ </ div >
57+ </ div >
58+
59+ < section >
60+ < h2 style = { { fontSize : 12 , fontWeight : 600 , color : 'var(--text2)' , margin : '0 0 12px' , textTransform : 'uppercase' , letterSpacing : '0.06em' } } >
61+ Sessions · { sessions . length }
62+ </ h2 >
63+ { sessions . length === 0 ? (
64+ < p style = { { color : 'var(--text2)' , fontSize : 14 } } > No sessions found.</ p >
65+ ) : (
66+ < div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
67+ { sessions . map ( s => (
68+ < OpenCodeSessionRow key = { s . sessionId } session = { s } />
69+ ) ) }
70+ </ div >
71+ ) }
72+ </ section >
73+ </ main >
74+ </ >
75+ )
76+ }
77+
78+ // ── Claude project ───────────────────────────────────────────────────────
79+ const sessions = listSessions ( dirName )
2880 const projectPath = resolveProjectPath ( dirName )
29- const meta = getProjectMeta ( projectPath )
30- const title = meta ?. displayName || projectPath . split ( '/' ) . pop ( ) || projectPath
31- const chains = detectContinuationChains ( dirName )
32- const tagStore = loadSessionTags ( )
81+ const meta = getProjectMeta ( projectPath )
82+ const title = meta ?. displayName || projectPath . split ( '/' ) . pop ( ) || projectPath
83+ const chains = detectContinuationChains ( dirName )
84+ const tagStore = loadSessionTags ( )
3385
34- // Build reverse map: parentId → childId
3586 const childOf = new Map < string , string > ( )
3687 for ( const [ child , parent ] of chains . entries ( ) ) {
3788 childOf . set ( parent , child )
3889 }
3990
40- const active = sessions . filter ( s => s . processState === 'running' || s . processState === 'paused' )
91+ const active = sessions . filter ( s => s . processState === 'running' || s . processState === 'paused' )
4192 const history = sessions . filter ( s => s . processState !== 'running' && s . processState !== 'paused' )
4293
4394 return (
@@ -60,7 +111,6 @@ export default async function ProjectPage({ searchParams }: Props) {
60111 </ div >
61112 </ div >
62113
63- { /* New session — full-width block below header */ }
64114 < div style = { { marginTop : 16 } } >
65115 < NewSessionForm projectPath = { projectPath } />
66116 </ div >
@@ -77,7 +127,7 @@ export default async function ProjectPage({ searchParams }: Props) {
77127 </ div >
78128 < div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
79129 { active . map ( s => (
80- < SessionRow
130+ < ClaudeSessionRow
81131 key = { s . sessionId }
82132 session = { s }
83133 parentId = { chains . get ( s . sessionId ) }
@@ -101,7 +151,7 @@ export default async function ProjectPage({ searchParams }: Props) {
101151 ) : (
102152 < div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
103153 { history . map ( s => (
104- < SessionRow
154+ < ClaudeSessionRow
105155 key = { s . sessionId }
106156 session = { s }
107157 parentId = { chains . get ( s . sessionId ) }
@@ -119,6 +169,43 @@ export default async function ProjectPage({ searchParams }: Props) {
119169 )
120170}
121171
172+ // ── OpenCode session row (read-only, no process controls) ─────────────────
173+
174+ function OpenCodeSessionRow ( { session : s } : { session : SessionInfo } ) {
175+ return (
176+ < div className = "glass" style = { { borderRadius : 12 , padding : '14px 18px' , display : 'flex' , alignItems : 'center' , gap : 14 } } >
177+ < div style = { { flex : 1 , minWidth : 0 } } >
178+ < Link
179+ href = { `/session?f=${ encodeB64 ( s . filepath ) } ` }
180+ style = { {
181+ color : 'var(--text)' , fontWeight : 500 , fontSize : 14 , textDecoration : 'none' ,
182+ display : 'block' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' , marginBottom : 7 ,
183+ } }
184+ >
185+ { s . firstPrompt }
186+ </ Link >
187+ < div style = { { display : 'flex' , gap : 6 , alignItems : 'center' , flexWrap : 'wrap' } } >
188+ < span className = "chip" > Finished</ span >
189+ { s . estimatedCostUsd != null && s . estimatedCostUsd > 0 && (
190+ < span className = "chip" title = "Cost" > { formatCost ( s . estimatedCostUsd ) } </ span >
191+ ) }
192+ < span className = "chip" > { formatRelative ( s . mtime ) } </ span >
193+ < span className = "chip" style = { { fontFamily : 'ui-monospace, monospace' } } > { s . sessionId . slice ( 0 , 8 ) } </ span >
194+ </ div >
195+ </ div >
196+ < Link
197+ href = { `/session?f=${ encodeB64 ( s . filepath ) } ` }
198+ className = "glass-btn"
199+ style = { { fontSize : 13 , padding : '6px 14px' , flexShrink : 0 } }
200+ >
201+ Open →
202+ </ Link >
203+ </ div >
204+ )
205+ }
206+
207+ // ── Claude session row (full-featured) ────────────────────────────────────
208+
122209function activityLabel ( processState : string , currentActivity ?: string | null ) : string {
123210 if ( processState === 'paused' ) return 'Paused'
124211 if ( processState !== 'running' ) return 'Finished'
@@ -134,7 +221,7 @@ function formatCost(usd: number): string {
134221 return `$${ usd . toFixed ( 2 ) } `
135222}
136223
137- function SessionRow ( {
224+ function ClaudeSessionRow ( {
138225 session : s ,
139226 parentId,
140227 childId,
@@ -158,26 +245,15 @@ function SessionRow({
158245
159246 return (
160247 < div className = "glass" style = { {
161- borderRadius : 12 ,
162- padding : '14px 18px' ,
163- display : 'flex' ,
164- alignItems : 'center' ,
165- gap : 14 ,
248+ borderRadius : 12 , padding : '14px 18px' , display : 'flex' , alignItems : 'center' , gap : 14 ,
166249 borderColor : s . processState === 'running' ? 'rgba(61,214,140,0.20)' : undefined ,
167250 } } >
168251 < div style = { { flex : 1 , minWidth : 0 } } >
169252 < Link
170253 href = { `/session?f=${ encodeB64 ( s . filepath ) } ` }
171254 style = { {
172- color : 'var(--text)' ,
173- fontWeight : 500 ,
174- fontSize : 14 ,
175- textDecoration : 'none' ,
176- display : 'block' ,
177- overflow : 'hidden' ,
178- textOverflow : 'ellipsis' ,
179- whiteSpace : 'nowrap' ,
180- marginBottom : 7 ,
255+ color : 'var(--text)' , fontWeight : 500 , fontSize : 14 , textDecoration : 'none' ,
256+ display : 'block' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' , marginBottom : 7 ,
181257 } }
182258 >
183259 { s . firstPrompt }
@@ -231,8 +307,8 @@ function SessionRow({
231307
232308function formatRelative ( ms : number ) : string {
233309 const diff = Date . now ( ) - ms
234- if ( diff < 60_000 ) return 'just now'
235- if ( diff < 3_600_000 ) return `${ Math . floor ( diff / 60_000 ) } m ago`
236- if ( diff < 86_400_000 ) return `${ Math . floor ( diff / 3_600_000 ) } h ago`
310+ if ( diff < 60_000 ) return 'just now'
311+ if ( diff < 3_600_000 ) return `${ Math . floor ( diff / 60_000 ) } m ago`
312+ if ( diff < 86_400_000 ) return `${ Math . floor ( diff / 3_600_000 ) } h ago`
237313 return `${ Math . floor ( diff / 86_400_000 ) } d ago`
238314}
0 commit comments