opencode-manager 0.4.2 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tui/app.tsx CHANGED
@@ -13,34 +13,21 @@ import { copyToClipboardSync } from "../lib/clipboard"
13
13
  import {
14
14
  ProjectRecord,
15
15
  SessionRecord,
16
- deleteProjectMetadata,
17
- deleteSessionMetadata,
18
16
  describeProject,
19
17
  describeSession,
20
18
  formatDate,
21
19
  formatDisplayPath,
22
- loadProjectRecords,
23
- loadSessionRecords,
24
- updateSessionTitle,
25
- copySession,
26
- moveSession,
27
- copySessions,
28
- moveSessions,
29
20
  BatchOperationResult,
30
21
  TokenSummary,
31
22
  TokenBreakdown,
32
23
  AggregateTokenSummary,
33
- computeSessionTokenSummary,
34
- computeProjectTokenSummary,
35
- computeGlobalTokenSummary,
36
24
  clearTokenCache,
37
25
  ChatMessage,
38
26
  ChatPart,
39
- loadSessionChatIndex,
40
- hydrateChatMessageParts,
41
27
  ChatSearchResult,
42
- searchSessionsChat,
43
28
  } from "../lib/opencode-data"
29
+ import { DEFAULT_SQLITE_PATH } from "../lib/opencode-data-sqlite"
30
+ import { createProvider, type DataProvider, type StorageBackend } from "../lib/opencode-data-provider"
44
31
  import { createSearcher, type SearchCandidate } from "../lib/search"
45
32
 
46
33
  type TabKey = "projects" | "sessions"
@@ -60,7 +47,7 @@ type ConfirmState = {
60
47
  }
61
48
 
62
49
  type ProjectsPanelProps = {
63
- root: string
50
+ provider: DataProvider
64
51
  active: boolean
65
52
  locked: boolean
66
53
  searchQuery: string
@@ -70,7 +57,7 @@ type ProjectsPanelProps = {
70
57
  }
71
58
 
72
59
  type SessionsPanelProps = {
73
- root: string
60
+ provider: DataProvider
74
61
  active: boolean
75
62
  locked: boolean
76
63
  projectFilter: string | null
@@ -135,6 +122,33 @@ function formatAggregateSummaryShort(summary: AggregateTokenSummary): string {
135
122
  return base
136
123
  }
137
124
 
125
+ async function runBatchSessionOperation(
126
+ provider: DataProvider,
127
+ sessions: SessionRecord[],
128
+ targetProjectId: string,
129
+ mode: "move" | "copy"
130
+ ): Promise<BatchOperationResult> {
131
+ const succeeded: BatchOperationResult["succeeded"] = []
132
+ const failed: BatchOperationResult["failed"] = []
133
+
134
+ for (const session of sessions) {
135
+ try {
136
+ const newRecord =
137
+ mode === "move"
138
+ ? await provider.moveSession(session, targetProjectId)
139
+ : await provider.copySession(session, targetProjectId)
140
+ succeeded.push({ session, newRecord })
141
+ } catch (error) {
142
+ failed.push({
143
+ session,
144
+ error: error instanceof Error ? error.message : String(error),
145
+ })
146
+ }
147
+ }
148
+
149
+ return { succeeded, failed }
150
+ }
151
+
138
152
  // Clipboard functionality moved to ../lib/clipboard.ts
139
153
  // Use copyToClipboardSyncSync for fire-and-forget clipboard operations
140
154
 
@@ -245,7 +259,7 @@ const ProjectSelector = ({
245
259
  }
246
260
 
247
261
  const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function ProjectsPanel(
248
- { root, active, locked, searchQuery, onNotify, requestConfirm, onNavigateToSessions },
262
+ { provider, active, locked, searchQuery, onNotify, requestConfirm, onNavigateToSessions },
249
263
  ref,
250
264
  ) {
251
265
  const [records, setRecords] = useState<ProjectRecord[]>([])
@@ -279,7 +293,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
279
293
  setLoading(true)
280
294
  setError(null)
281
295
  try {
282
- const data = await loadProjectRecords({ root })
296
+ const data = await provider.loadProjectRecords()
283
297
  setRecords(data)
284
298
  if (!silent) {
285
299
  onNotify(`Loaded ${data.length} project(s).`)
@@ -292,7 +306,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
292
306
  setLoading(false)
293
307
  }
294
308
  },
295
- [root, onNotify],
309
+ [provider, onNotify],
296
310
  )
297
311
 
298
312
  useEffect(() => {
@@ -330,13 +344,13 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
330
344
  // Load all sessions once for token computation
331
345
  useEffect(() => {
332
346
  let cancelled = false
333
- loadSessionRecords({ root }).then((sessions) => {
347
+ provider.loadSessionRecords().then((sessions) => {
334
348
  if (!cancelled) {
335
349
  setAllSessions(sessions)
336
350
  }
337
351
  })
338
352
  return () => { cancelled = true }
339
- }, [root, records]) // Re-fetch when projects change (implies sessions may have changed)
353
+ }, [provider, records]) // Re-fetch when projects change (implies sessions may have changed)
340
354
 
341
355
  // Compute token summary for current project
342
356
  useEffect(() => {
@@ -345,7 +359,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
345
359
  return
346
360
  }
347
361
  let cancelled = false
348
- computeProjectTokenSummary(currentRecord.projectId, allSessions, root).then((summary) => {
362
+ provider.computeProjectTokenSummary(currentRecord.projectId, allSessions).then((summary) => {
349
363
  if (!cancelled) {
350
364
  setCurrentProjectTokens(summary)
351
365
  }
@@ -353,7 +367,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
353
367
  return () => {
354
368
  cancelled = true
355
369
  }
356
- }, [currentRecord, allSessions, root])
370
+ }, [currentRecord, allSessions, provider])
357
371
 
358
372
  const toggleSelection = useCallback((record: ProjectRecord | undefined) => {
359
373
  if (!record) {
@@ -401,7 +415,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
401
415
  .slice(0, MAX_CONFIRM_PREVIEW)
402
416
  .map((record) => describeProject(record, { fullPath: true })),
403
417
  onConfirm: async () => {
404
- const { removed, failed } = await deleteProjectMetadata(selectedRecords)
418
+ const { removed, failed } = await provider.deleteProjectMetadata(selectedRecords)
405
419
  setSelectedIndexes(new Set())
406
420
  const msg = failed.length
407
421
  ? `Removed ${removed.length} project file(s). Failed: ${failed.length}`
@@ -410,7 +424,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
410
424
  await refreshRecords(true)
411
425
  },
412
426
  })
413
- }, [selectedRecords, onNotify, requestConfirm, refreshRecords])
427
+ }, [selectedRecords, onNotify, requestConfirm, refreshRecords, provider])
414
428
 
415
429
  const handleKey = useCallback(
416
430
  (key: KeyEvent) => {
@@ -549,7 +563,7 @@ const ProjectsPanel = forwardRef<PanelHandle, ProjectsPanelProps>(function Proje
549
563
  })
550
564
 
551
565
  const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function SessionsPanel(
552
- { root, active, locked, projectFilter, searchQuery, globalTokenSummary, onNotify, requestConfirm, onClearFilter, onOpenChatViewer },
566
+ { provider, active, locked, projectFilter, searchQuery, globalTokenSummary, onNotify, requestConfirm, onClearFilter, onOpenChatViewer },
553
567
  ref,
554
568
  ) {
555
569
  const [records, setRecords] = useState<SessionRecord[]>([])
@@ -637,7 +651,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
637
651
  setLoading(true)
638
652
  setError(null)
639
653
  try {
640
- const data = await loadSessionRecords({ root, projectId: projectFilter || undefined })
654
+ const data = await provider.loadSessionRecords({ projectId: projectFilter || undefined })
641
655
  setRecords(data)
642
656
  if (!silent) {
643
657
  onNotify(`Loaded ${data.length} session(s).`)
@@ -650,7 +664,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
650
664
  setLoading(false)
651
665
  }
652
666
  },
653
- [root, projectFilter, onNotify],
667
+ [provider, projectFilter, onNotify],
654
668
  )
655
669
 
656
670
  useEffect(() => {
@@ -692,7 +706,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
692
706
  return
693
707
  }
694
708
  let cancelled = false
695
- computeSessionTokenSummary(currentSession, root).then((summary) => {
709
+ provider.computeSessionTokenSummary(currentSession).then((summary) => {
696
710
  if (!cancelled) {
697
711
  setCurrentTokenSummary(summary)
698
712
  }
@@ -700,7 +714,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
700
714
  return () => {
701
715
  cancelled = true
702
716
  }
703
- }, [currentSession, root])
717
+ }, [currentSession, provider])
704
718
 
705
719
  // Compute filtered token summary (deferred to avoid UI freeze)
706
720
  useEffect(() => {
@@ -713,7 +727,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
713
727
 
714
728
  // Compute filtered (project-only) if filter is active.
715
729
  if (projectFilter) {
716
- computeProjectTokenSummary(projectFilter, records, root).then((summary) => {
730
+ provider.computeProjectTokenSummary(projectFilter, records).then((summary) => {
717
731
  if (!cancelled) {
718
732
  setFilteredTokenSummary(summary)
719
733
  }
@@ -723,7 +737,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
723
737
  return () => {
724
738
  cancelled = true
725
739
  }
726
- }, [records, projectFilter, root])
740
+ }, [records, projectFilter, provider])
727
741
 
728
742
  const toggleSelection = useCallback((session: SessionRecord | undefined) => {
729
743
  if (!session) {
@@ -774,7 +788,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
774
788
  .slice(0, MAX_CONFIRM_PREVIEW)
775
789
  .map((session) => describeSession(session, { fullPath: true })),
776
790
  onConfirm: async () => {
777
- const { removed, failed } = await deleteSessionMetadata(selectedSessions)
791
+ const { removed, failed } = await provider.deleteSessionMetadata(selectedSessions)
778
792
  setSelectedIndexes(new Set())
779
793
  const msg = failed.length
780
794
  ? `Removed ${removed.length} session file(s). Failed: ${failed.length}`
@@ -783,7 +797,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
783
797
  await refreshRecords(true)
784
798
  },
785
799
  })
786
- }, [selectedSessions, onNotify, requestConfirm, refreshRecords])
800
+ }, [selectedSessions, onNotify, requestConfirm, refreshRecords, provider])
787
801
 
788
802
  const executeRename = useCallback(async () => {
789
803
  if (!currentSession || !renameValue.trim()) {
@@ -796,7 +810,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
796
810
  return
797
811
  }
798
812
  try {
799
- await updateSessionTitle(currentSession.filePath, renameValue.trim())
813
+ await provider.updateSessionTitle(currentSession, renameValue.trim())
800
814
  onNotify(`Renamed to "${renameValue.trim()}"`)
801
815
  setIsRenaming(false)
802
816
  setRenameValue('')
@@ -805,7 +819,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
805
819
  const msg = error instanceof Error ? error.message : String(error)
806
820
  onNotify(`Rename failed: ${msg}`, 'error')
807
821
  }
808
- }, [currentSession, renameValue, onNotify, refreshRecords])
822
+ }, [currentSession, renameValue, onNotify, refreshRecords, provider])
809
823
 
810
824
  const executeTransfer = useCallback(async (
811
825
  targetProject: ProjectRecord,
@@ -814,8 +828,12 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
814
828
  setIsSelectingProject(false)
815
829
  setOperationMode(null)
816
830
 
817
- const operationFn = mode === 'move' ? moveSessions : copySessions
818
- const result = await operationFn(selectedSessions, targetProject.projectId, root)
831
+ const result = await runBatchSessionOperation(
832
+ provider,
833
+ selectedSessions,
834
+ targetProject.projectId,
835
+ mode
836
+ )
819
837
 
820
838
  setSelectedIndexes(new Set())
821
839
 
@@ -833,7 +851,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
833
851
  }
834
852
 
835
853
  await refreshRecords(true)
836
- }, [selectedSessions, root, onNotify, refreshRecords])
854
+ }, [selectedSessions, provider, onNotify, refreshRecords])
837
855
 
838
856
  const handleKey = useCallback(
839
857
  (key: KeyEvent) => {
@@ -944,7 +962,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
944
962
  return
945
963
  }
946
964
  // Load projects for selection
947
- loadProjectRecords({ root }).then(projects => {
965
+ provider.loadProjectRecords().then(projects => {
948
966
  // Filter out current project if filtering by project
949
967
  const filtered = projectFilter
950
968
  ? projects.filter(p => p.projectId !== projectFilter)
@@ -964,7 +982,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
964
982
  onNotify('No sessions selected for copy', 'error')
965
983
  return
966
984
  }
967
- loadProjectRecords({ root }).then(projects => {
985
+ provider.loadProjectRecords().then(projects => {
968
986
  setAvailableProjects(projects)
969
987
  setProjectCursor(0)
970
988
  setOperationMode('copy')
@@ -989,7 +1007,7 @@ const SessionsPanel = forwardRef<PanelHandle, SessionsPanelProps>(function Sessi
989
1007
  return
990
1008
  }
991
1009
  },
992
- [active, locked, currentSession, projectFilter, onClearFilter, onNotify, requestDeletion, toggleSelection, isRenaming, executeRename, isSelectingProject, availableProjects, projectCursor, operationMode, executeTransfer, selectedSessions, root, onOpenChatViewer],
1010
+ [active, locked, currentSession, projectFilter, onClearFilter, onNotify, requestDeletion, toggleSelection, isRenaming, executeRename, isSelectingProject, availableProjects, projectCursor, operationMode, executeTransfer, selectedSessions, provider, onOpenChatViewer],
993
1011
  )
994
1012
 
995
1013
  useImperativeHandle(
@@ -1526,7 +1544,19 @@ const ChatViewer = ({
1526
1544
  )
1527
1545
  }
1528
1546
 
1529
- export const App = ({ root }: { root: string }) => {
1547
+ export const App = ({
1548
+ root,
1549
+ backend,
1550
+ dbPath,
1551
+ sqliteStrict,
1552
+ forceWrite,
1553
+ }: {
1554
+ root: string
1555
+ backend: StorageBackend
1556
+ dbPath?: string
1557
+ sqliteStrict: boolean
1558
+ forceWrite: boolean
1559
+ }) => {
1530
1560
  const renderer = useRenderer()
1531
1561
  const projectsRef = useRef<PanelHandle>(null)
1532
1562
  const sessionsRef = useRef<PanelHandle>(null)
@@ -1537,6 +1567,7 @@ export const App = ({ root }: { root: string }) => {
1537
1567
  const [searchQuery, setSearchQuery] = useState("")
1538
1568
  const [status, setStatus] = useState("Ready")
1539
1569
  const [statusLevel, setStatusLevel] = useState<NotificationLevel>("info")
1570
+ const [sqliteWarning, setSqliteWarning] = useState<string | null>(null)
1540
1571
  const [confirmState, setConfirmState] = useState<ConfirmState | null>(null)
1541
1572
  const [showHelp, setShowHelp] = useState(true)
1542
1573
  const [confirmBusy, setConfirmBusy] = useState(false)
@@ -1561,35 +1592,56 @@ export const App = ({ root }: { root: string }) => {
1561
1592
  const [chatSearching, setChatSearching] = useState(false)
1562
1593
  const [allSessions, setAllSessions] = useState<SessionRecord[]>([])
1563
1594
 
1595
+ const resolvedDbPath = useMemo(() => {
1596
+ if (backend !== "sqlite") {
1597
+ return undefined
1598
+ }
1599
+ return dbPath ?? DEFAULT_SQLITE_PATH
1600
+ }, [backend, dbPath])
1601
+
1602
+ const notify = useCallback((message: string, level: NotificationLevel = "info") => {
1603
+ setStatus(message)
1604
+ setStatusLevel(level)
1605
+ }, [])
1606
+
1607
+ const provider = useMemo(() => {
1608
+ return createProvider({
1609
+ backend,
1610
+ root,
1611
+ dbPath: resolvedDbPath,
1612
+ sqliteStrict,
1613
+ forceWrite,
1614
+ onWarning: (message) => {
1615
+ setSqliteWarning(message)
1616
+ notify(message, "error")
1617
+ },
1618
+ })
1619
+ }, [backend, root, resolvedDbPath, sqliteStrict, forceWrite, notify, setSqliteWarning])
1620
+
1564
1621
  // Load global tokens
1565
1622
  useEffect(() => {
1566
1623
  let cancelled = false
1567
- loadSessionRecords({ root }).then((sessions) => {
1624
+ provider.loadSessionRecords().then((sessions) => {
1568
1625
  if (cancelled) return
1569
- return computeGlobalTokenSummary(sessions, root)
1626
+ return provider.computeGlobalTokenSummary(sessions)
1570
1627
  }).then((summary) => {
1571
1628
  if (!cancelled && summary) {
1572
1629
  setGlobalTokens(summary)
1573
1630
  }
1574
1631
  })
1575
1632
  return () => { cancelled = true }
1576
- }, [root, tokenRefreshKey])
1633
+ }, [provider, tokenRefreshKey])
1577
1634
 
1578
1635
  // Load all sessions for chat search
1579
1636
  useEffect(() => {
1580
1637
  let cancelled = false
1581
- loadSessionRecords({ root }).then((sessions) => {
1638
+ provider.loadSessionRecords().then((sessions) => {
1582
1639
  if (!cancelled) {
1583
1640
  setAllSessions(sessions)
1584
1641
  }
1585
1642
  })
1586
1643
  return () => { cancelled = true }
1587
- }, [root, tokenRefreshKey])
1588
-
1589
- const notify = useCallback((message: string, level: NotificationLevel = "info") => {
1590
- setStatus(message)
1591
- setStatusLevel(level)
1592
- }, [])
1644
+ }, [provider, tokenRefreshKey])
1593
1645
 
1594
1646
  const requestConfirm = useCallback((state: ConfirmState) => {
1595
1647
  setConfirmState(state)
@@ -1637,7 +1689,7 @@ export const App = ({ root }: { root: string }) => {
1637
1689
  setChatPartsCache(new Map())
1638
1690
 
1639
1691
  try {
1640
- const messages = await loadSessionChatIndex(session.sessionId, root)
1692
+ const messages = await provider.loadSessionChatIndex(session.sessionId)
1641
1693
  setChatMessages(messages)
1642
1694
  if (messages.length > 0) {
1643
1695
  setChatCursor(0)
@@ -1648,7 +1700,7 @@ export const App = ({ root }: { root: string }) => {
1648
1700
  } finally {
1649
1701
  setChatLoading(false)
1650
1702
  }
1651
- }, [root])
1703
+ }, [provider])
1652
1704
 
1653
1705
  const closeChatViewer = useCallback(() => {
1654
1706
  setChatViewerOpen(false)
@@ -1671,7 +1723,7 @@ export const App = ({ root }: { root: string }) => {
1671
1723
  }
1672
1724
 
1673
1725
  try {
1674
- const hydrated = await hydrateChatMessageParts(message, root)
1726
+ const hydrated = await provider.hydrateChatMessageParts(message)
1675
1727
  setChatPartsCache(prev => new Map(prev).set(message.messageId, hydrated))
1676
1728
  setChatMessages(prev => prev.map(m =>
1677
1729
  m.messageId === message.messageId ? hydrated : m
@@ -1688,7 +1740,7 @@ export const App = ({ root }: { root: string }) => {
1688
1740
  m.messageId === message.messageId ? errorMsg : m
1689
1741
  ))
1690
1742
  }
1691
- }, [root, chatPartsCache])
1743
+ }, [provider, chatPartsCache])
1692
1744
 
1693
1745
  const copyChatMessage = useCallback((message: ChatMessage) => {
1694
1746
  if (!message.parts || message.parts.length === 0) {
@@ -1731,7 +1783,7 @@ export const App = ({ root }: { root: string }) => {
1731
1783
  ? allSessions.filter(s => s.projectId === sessionFilter)
1732
1784
  : allSessions
1733
1785
 
1734
- const results = await searchSessionsChat(sessionsToSearch, chatSearchQuery, root, { maxResults: 100 })
1786
+ const results = await provider.searchSessionsChat(sessionsToSearch, chatSearchQuery, { maxResults: 100 })
1735
1787
  setChatSearchResults(results)
1736
1788
  setChatSearchCursor(0)
1737
1789
  } catch (err) {
@@ -1741,7 +1793,7 @@ export const App = ({ root }: { root: string }) => {
1741
1793
  } finally {
1742
1794
  setChatSearching(false)
1743
1795
  }
1744
- }, [chatSearchQuery, sessionFilter, allSessions, root, notify])
1796
+ }, [chatSearchQuery, sessionFilter, allSessions, provider, notify])
1745
1797
 
1746
1798
  const handleChatSearchResult = useCallback(async (result: ChatSearchResult) => {
1747
1799
  // Find the session and open chat viewer at the matching message
@@ -1991,11 +2043,25 @@ export const App = ({ root }: { root: string }) => {
1991
2043
  <text fg={PALETTE.muted}>{globalTokens ? '?' : 'loading...'}</text>
1992
2044
  )}
1993
2045
  </box>
1994
- <text>Root: {root}</text>
2046
+ <box style={{ flexDirection: "row", gap: 1 }}>
2047
+ <text fg={PALETTE.accent}>Storage:</text>
2048
+ <text fg={backend === "sqlite" ? PALETTE.info : PALETTE.muted}>
2049
+ {backend === "sqlite" ? "SQLite" : "JSONL"}
2050
+ </text>
2051
+ <text fg={PALETTE.muted}>|</text>
2052
+ <text>
2053
+ {backend === "sqlite"
2054
+ ? `DB: ${formatDisplayPath(resolvedDbPath ?? "(default)")}`
2055
+ : `Root: ${formatDisplayPath(root)}`}
2056
+ </text>
2057
+ </box>
1995
2058
  <text>
1996
2059
  Tabs: [1] Projects [2] Sessions | Active: {activeTab} | Global: Tab switch, / search, X clear, R reload, Q quit, ? help
1997
2060
  </text>
1998
2061
  {sessionFilter ? <text fg="#a3e635">Session filter: {sessionFilter}</text> : null}
2062
+ {backend === "sqlite" && sqliteWarning ? (
2063
+ <text fg={PALETTE.danger}>SQLite warning: {sqliteWarning}</text>
2064
+ ) : null}
1999
2065
  </box>
2000
2066
 
2001
2067
  {showHelp
@@ -2010,7 +2076,7 @@ export const App = ({ root }: { root: string }) => {
2010
2076
  <box style={{ flexDirection: "row", gap: 1, flexGrow: 1 }}>
2011
2077
  <ProjectsPanel
2012
2078
  ref={projectsRef}
2013
- root={root}
2079
+ provider={provider}
2014
2080
  active={activeTab === "projects"}
2015
2081
  locked={Boolean(confirmState) || showHelp}
2016
2082
  searchQuery={activeTab === "projects" ? searchQuery : ""}
@@ -2020,7 +2086,7 @@ export const App = ({ root }: { root: string }) => {
2020
2086
  />
2021
2087
  <SessionsPanel
2022
2088
  ref={sessionsRef}
2023
- root={root}
2089
+ provider={provider}
2024
2090
  active={activeTab === "sessions"}
2025
2091
  locked={Boolean(confirmState) || showHelp || chatViewerOpen || chatSearchOpen}
2026
2092
  projectFilter={sessionFilter}
package/src/tui/args.ts CHANGED
@@ -7,9 +7,15 @@
7
7
 
8
8
  import { resolve } from "node:path"
9
9
  import { DEFAULT_ROOT } from "../lib/opencode-data"
10
+ import { DEFAULT_SQLITE_PATH } from "../lib/opencode-data-sqlite"
11
+ import type { StorageBackend } from "../lib/opencode-data-provider"
10
12
 
11
13
  export interface TUIOptions {
12
14
  root: string
15
+ backend: StorageBackend
16
+ dbPath?: string
17
+ sqliteStrict: boolean
18
+ forceWrite: boolean
13
19
  }
14
20
 
15
21
  /**
@@ -17,7 +23,14 @@ export interface TUIOptions {
17
23
  */
18
24
  export function printUsage(): void {
19
25
  console.log(`OpenCode Metadata TUI
20
- Usage: bun run tui [-- --root /path/to/storage]
26
+ Usage: bun run tui [-- --root /path/to/storage] [-- --experimental-sqlite] [-- --db /path/to/opencode.db]
27
+
28
+ Storage options:
29
+ --root <path> Root path to JSONL storage (default: ~/.local/share/opencode)
30
+ --experimental-sqlite Use SQLite backend instead of JSONL files
31
+ --db <path> Path to SQLite database (implies --experimental-sqlite)
32
+ --sqlite-strict Fail on SQLite warnings or malformed data
33
+ --force-write Wait for SQLite write locks before failing
21
34
 
22
35
  Key bindings:
23
36
  Tab / 1 / 2 Switch between projects and sessions
@@ -70,6 +83,10 @@ Chat viewer (when open):
70
83
  */
71
84
  export function parseArgs(argv: string[] = process.argv.slice(2)): TUIOptions {
72
85
  let root = DEFAULT_ROOT
86
+ let backend: StorageBackend = "jsonl"
87
+ let dbPath: string | undefined
88
+ let sqliteStrict = false
89
+ let forceWrite = false
73
90
 
74
91
  for (let idx = 0; idx < argv.length; idx += 1) {
75
92
  const token = argv[idx]
@@ -78,15 +95,43 @@ export function parseArgs(argv: string[] = process.argv.slice(2)): TUIOptions {
78
95
  idx += 1
79
96
  continue
80
97
  }
98
+ if (token === "--db" && argv[idx + 1]) {
99
+ dbPath = resolve(argv[idx + 1])
100
+ backend = "sqlite"
101
+ idx += 1
102
+ continue
103
+ }
104
+ if (token === "--experimental-sqlite") {
105
+ backend = "sqlite"
106
+ continue
107
+ }
108
+ if (token === "--sqlite-strict") {
109
+ sqliteStrict = true
110
+ continue
111
+ }
112
+ if (token === "--force-write") {
113
+ forceWrite = true
114
+ continue
115
+ }
81
116
  if (token === "--help" || token === "-h") {
82
117
  printUsage()
83
118
  process.exit(0)
84
119
  }
85
120
  if (token === "--version" || token === "-V") {
86
- console.log("0.4.0")
121
+ console.log("0.4.5")
87
122
  process.exit(0)
88
123
  }
89
124
  }
90
125
 
91
- return { root }
126
+ if (backend === "sqlite" && !dbPath) {
127
+ dbPath = resolve(DEFAULT_SQLITE_PATH)
128
+ }
129
+
130
+ return {
131
+ root: resolve(root),
132
+ backend,
133
+ dbPath,
134
+ sqliteStrict,
135
+ forceWrite,
136
+ }
92
137
  }
package/src/tui/index.tsx CHANGED
@@ -13,6 +13,7 @@ import { createCliRenderer } from "@opentui/core"
13
13
 
14
14
  import { App } from "./app"
15
15
  import { DEFAULT_ROOT } from "../lib/opencode-data"
16
+ import { DEFAULT_SQLITE_PATH } from "../lib/opencode-data-sqlite"
16
17
  import { parseArgs, printUsage, type TUIOptions } from "./args"
17
18
 
18
19
  // Re-export args module for external consumers
@@ -24,8 +25,21 @@ export { parseArgs, printUsage, type TUIOptions }
24
25
  */
25
26
  export async function launchTUI(options?: Partial<TUIOptions>): Promise<void> {
26
27
  const root = options?.root ?? DEFAULT_ROOT
28
+ const backend = options?.backend ?? (options?.dbPath ? "sqlite" : "jsonl")
29
+ const sqliteStrict = options?.sqliteStrict ?? false
30
+ const forceWrite = options?.forceWrite ?? false
31
+ const dbPath = backend === "sqlite" ? (options?.dbPath ?? DEFAULT_SQLITE_PATH) : undefined
32
+
27
33
  const renderer = await createCliRenderer()
28
- createRoot(renderer).render(<App root={root} />)
34
+ createRoot(renderer).render(
35
+ <App
36
+ root={root}
37
+ backend={backend}
38
+ dbPath={dbPath}
39
+ sqliteStrict={sqliteStrict}
40
+ forceWrite={forceWrite}
41
+ />
42
+ )
29
43
  }
30
44
 
31
45
  /**