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/package.json +1 -1
- package/src/bin/opencode-manager.ts +1 -1
- package/src/cli/commands/tui.ts +8 -1
- package/src/cli/index.ts +1 -1
- package/src/lib/opencode-data-sqlite.ts +646 -149
- package/src/tui/app.tsx +129 -63
- package/src/tui/args.ts +48 -3
- package/src/tui/index.tsx +15 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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(
|
|
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
|
-
[
|
|
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(
|
|
347
|
+
provider.loadSessionRecords().then((sessions) => {
|
|
334
348
|
if (!cancelled) {
|
|
335
349
|
setAllSessions(sessions)
|
|
336
350
|
}
|
|
337
351
|
})
|
|
338
352
|
return () => { cancelled = true }
|
|
339
|
-
}, [
|
|
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
|
|
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,
|
|
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
|
-
{
|
|
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({
|
|
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
|
-
[
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
818
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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 = ({
|
|
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(
|
|
1624
|
+
provider.loadSessionRecords().then((sessions) => {
|
|
1568
1625
|
if (cancelled) return
|
|
1569
|
-
return computeGlobalTokenSummary(sessions
|
|
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
|
-
}, [
|
|
1633
|
+
}, [provider, tokenRefreshKey])
|
|
1577
1634
|
|
|
1578
1635
|
// Load all sessions for chat search
|
|
1579
1636
|
useEffect(() => {
|
|
1580
1637
|
let cancelled = false
|
|
1581
|
-
loadSessionRecords(
|
|
1638
|
+
provider.loadSessionRecords().then((sessions) => {
|
|
1582
1639
|
if (!cancelled) {
|
|
1583
1640
|
setAllSessions(sessions)
|
|
1584
1641
|
}
|
|
1585
1642
|
})
|
|
1586
1643
|
return () => { cancelled = true }
|
|
1587
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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,
|
|
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,
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
121
|
+
console.log("0.4.5")
|
|
87
122
|
process.exit(0)
|
|
88
123
|
}
|
|
89
124
|
}
|
|
90
125
|
|
|
91
|
-
|
|
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(
|
|
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
|
/**
|