@@ -2,14 +2,15 @@ import type React from "react";
22import { useCallback , useEffect , useState } from "react" ;
33import { createRoot } from "react-dom/client" ;
44import faviconUrl from "../../favicon.svg" ;
5- import type { Project , SessionSummary } from "../shared/types.ts" ;
5+ import type { GlobalSessionResult , Project , SessionSummary } from "../shared/types.ts" ;
66import { DashboardStats } from "./components/dashboard/DashboardStats.tsx" ;
77import { Header } from "./components/layout/Header.tsx" ;
88import { Layout } from "./components/layout/Layout.tsx" ;
99import { SubAgentView } from "./components/message/SubAgentView.tsx" ;
1010import { HiddenProjectList } from "./components/project/HiddenProjectList.tsx" ;
1111import { ProjectList } from "./components/project/ProjectList.tsx" ;
1212import { SessionList } from "./components/project/SessionList.tsx" ;
13+ import { SearchModal } from "./components/search/SearchModal.tsx" ;
1314import { SessionPresentation } from "./components/session/SessionPresentation.tsx" ;
1415import { SessionView } from "./components/session/SessionView.tsx" ;
1516import { SubAgentPresentation } from "./components/session/SubAgentPresentation.tsx" ;
@@ -156,6 +157,8 @@ function App() {
156157 const { hiddenIds, hide, unhide } = useHiddenProjects ( ) ;
157158 const [ view , setView ] = useState < ViewState > ( { kind : "home" } ) ;
158159 const [ ready , setReady ] = useState ( false ) ;
160+ const [ searchOpen , setSearchOpen ] = useState ( false ) ;
161+ const [ searchSessions , setSearchSessions ] = useState < GlobalSessionResult [ ] > ( [ ] ) ;
159162
160163 // Restore view from URL hash on mount
161164 useEffect ( ( ) => {
@@ -208,12 +211,56 @@ function App() {
208211 }
209212 } , [ view ] ) ;
210213
214+ const fetchSearchSessions = useCallback ( ( ) => {
215+ fetch ( "/api/search/sessions" )
216+ . then ( ( res ) => res . json ( ) )
217+ . then ( ( data : { sessions : GlobalSessionResult [ ] } ) => setSearchSessions ( data . sessions ) )
218+ . catch ( ( ) => { } ) ;
219+ } , [ ] ) ;
220+
221+ const openSearch = useCallback ( ( ) => {
222+ setSearchOpen ( true ) ;
223+ fetchSearchSessions ( ) ;
224+ } , [ fetchSearchSessions ] ) ;
225+
226+ const handleSearchSelect = useCallback ( async ( encodedPath : string , sessionId : string ) => {
227+ setSearchOpen ( false ) ;
228+ try {
229+ const [ projectsRes , sessionsRes ] = await Promise . all ( [
230+ fetch ( "/api/projects" ) ,
231+ fetch ( `/api/projects/${ encodedPath } /sessions` ) ,
232+ ] ) ;
233+ const projectsData = await projectsRes . json ( ) ;
234+ const sessionsData = await sessionsRes . json ( ) ;
235+ const project = projectsData . projects . find ( ( p : Project ) => p . encodedPath === encodedPath ) ;
236+ const session = sessionsData . sessions . find ( ( s : SessionSummary ) => s . sessionId === sessionId ) ;
237+ if ( project && session ) {
238+ setView ( { kind : "session" , project, session, presenting : false } ) ;
239+ }
240+ } catch {
241+ // ignore
242+ }
243+ } , [ ] ) ;
244+
245+ // Cmd+K / Ctrl+K toggles search
246+ useEffect ( ( ) => {
247+ function handleCmdK ( e : KeyboardEvent ) {
248+ if ( ( e . metaKey || e . ctrlKey ) && e . key === "k" ) {
249+ e . preventDefault ( ) ;
250+ setSearchOpen ( ( prev ) => {
251+ if ( ! prev ) fetchSearchSessions ( ) ;
252+ return ! prev ;
253+ } ) ;
254+ }
255+ }
256+ window . addEventListener ( "keydown" , handleCmdK ) ;
257+ return ( ) => window . removeEventListener ( "keydown" , handleCmdK ) ;
258+ } , [ fetchSearchSessions ] ) ;
259+
211260 // Global keyboard shortcuts: p = toggle presentation, +/- = font size
212261 useEffect ( ( ) => {
213262 function handleKeyDown ( e : KeyboardEvent ) {
214- if ( e . target instanceof HTMLInputElement || e . target instanceof HTMLTextAreaElement ) {
215- return ;
216- }
263+ if ( e . target instanceof HTMLInputElement || e . target instanceof HTMLTextAreaElement ) return ;
217264 if ( e . ctrlKey || e . metaKey || e . altKey ) return ;
218265
219266 switch ( e . key ) {
@@ -257,71 +304,80 @@ function App() {
257304 }
258305
259306 return (
260- < Layout sidebar = { sidebarContent } hideSidebar = { isPresenting } >
261- < Header
262- title = { headerTitle }
263- breadcrumb = { breadcrumb }
264- copyCommand = {
265- view . kind === "session" ? `claude --resume ${ view . session . sessionId } ` : undefined
266- }
267- backHref = {
268- view . kind === "subagent" ? `#/${ view . project . encodedPath } /${ view . sessionId } ` : undefined
269- }
270- sessionType = { view . kind === "session" ? view . session . sessionType : undefined }
271- themeSetting = { themeSetting }
272- onCycleTheme = { cycleTheme }
273- fontSize = { fontSize }
274- onIncreaseFontSize = { increase }
275- onDecreaseFontSize = { decrease }
276- presentationActive = { isPresenting }
277- onTogglePresentation = { togglePresentation }
278- showPresentationToggle = { canPresent }
279- />
280- { view . kind === "home" && (
281- < >
307+ < >
308+ { searchOpen && (
309+ < SearchModal
310+ sessions = { searchSessions }
311+ onSelect = { handleSearchSelect }
312+ onClose = { ( ) => setSearchOpen ( false ) }
313+ />
314+ ) }
315+ < Layout sidebar = { sidebarContent } hideSidebar = { isPresenting } onSearchClick = { openSearch } >
316+ < Header
317+ title = { headerTitle }
318+ breadcrumb = { breadcrumb }
319+ copyCommand = {
320+ view . kind === "session" ? `claude --resume ${ view . session . sessionId } ` : undefined
321+ }
322+ backHref = {
323+ view . kind === "subagent" ? `#/${ view . project . encodedPath } /${ view . sessionId } ` : undefined
324+ }
325+ sessionType = { view . kind === "session" ? view . session . sessionType : undefined }
326+ themeSetting = { themeSetting }
327+ onCycleTheme = { cycleTheme }
328+ fontSize = { fontSize }
329+ onIncreaseFontSize = { increase }
330+ onDecreaseFontSize = { decrease }
331+ presentationActive = { isPresenting }
332+ onTogglePresentation = { togglePresentation }
333+ showPresentationToggle = { canPresent }
334+ />
335+ { view . kind === "home" && (
336+ < >
337+ < div className = "empty-state" >
338+ < img src = { faviconUrl } alt = "" width = "64" height = "64" className = "empty-state-logo" />
339+ < div className = "empty-state-title" > Welcome to Klovi</ div >
340+ < p > Select a project from the sidebar to browse your Claude Code sessions</ p >
341+ </ div >
342+ < DashboardStats />
343+ </ >
344+ ) }
345+ { view . kind === "hidden" && (
346+ < HiddenProjectList hiddenIds = { hiddenIds } onUnhide = { unhide } onBack = { goHome } />
347+ ) }
348+ { view . kind === "project" && (
282349 < div className = "empty-state" >
283- < img src = { faviconUrl } alt = "" width = "64" height = "64" className = "empty-state-logo" />
284- < div className = "empty-state-title" > Welcome to Klovi</ div >
285- < p > Select a project from the sidebar to browse your Claude Code sessions</ p >
350+ < div className = "empty-state-title" > Select a session</ div >
351+ < p > Choose a conversation from the sidebar</ p >
286352 </ div >
287- < DashboardStats />
288- </ >
289- ) }
290- { view . kind === "hidden" && (
291- < HiddenProjectList hiddenIds = { hiddenIds } onUnhide = { unhide } onBack = { goHome } />
292- ) }
293- { view . kind === "project" && (
294- < div className = "empty-state" >
295- < div className = "empty-state-title" > Select a session</ div >
296- < p > Choose a conversation from the sidebar</ p >
297- </ div >
298- ) }
299- { view . kind === "session" &&
300- ( view . presenting ? (
301- < SessionPresentation
302- sessionId = { view . session . sessionId }
303- project = { view . project . encodedPath }
304- onExit = { togglePresentation }
305- />
306- ) : (
307- < SessionView sessionId = { view . session . sessionId } project = { view . project . encodedPath } />
308- ) ) }
309- { view . kind === "subagent" &&
310- ( view . presenting ? (
311- < SubAgentPresentation
312- sessionId = { view . sessionId }
313- project = { view . project . encodedPath }
314- agentId = { view . agentId }
315- onExit = { togglePresentation }
316- />
317- ) : (
318- < SubAgentView
319- sessionId = { view . sessionId }
320- project = { view . project . encodedPath }
321- agentId = { view . agentId }
322- />
323- ) ) }
324- </ Layout >
353+ ) }
354+ { view . kind === "session" &&
355+ ( view . presenting ? (
356+ < SessionPresentation
357+ sessionId = { view . session . sessionId }
358+ project = { view . project . encodedPath }
359+ onExit = { togglePresentation }
360+ />
361+ ) : (
362+ < SessionView sessionId = { view . session . sessionId } project = { view . project . encodedPath } />
363+ ) ) }
364+ { view . kind === "subagent" &&
365+ ( view . presenting ? (
366+ < SubAgentPresentation
367+ sessionId = { view . sessionId }
368+ project = { view . project . encodedPath }
369+ agentId = { view . agentId }
370+ onExit = { togglePresentation }
371+ />
372+ ) : (
373+ < SubAgentView
374+ sessionId = { view . sessionId }
375+ project = { view . project . encodedPath }
376+ agentId = { view . agentId }
377+ />
378+ ) ) }
379+ </ Layout >
380+ </ >
325381 ) ;
326382}
327383
0 commit comments