opencode-session 0.1.0
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/LICENSE +21 -0
- package/package.json +46 -0
- package/src/config.ts +29 -0
- package/src/data/loader.ts +495 -0
- package/src/data/manager.ts +312 -0
- package/src/index.ts +56 -0
- package/src/types.ts +173 -0
- package/src/ui/app.ts +245 -0
- package/src/ui/components/confirm-dialog.ts +117 -0
- package/src/ui/components/detail-viewer.ts +307 -0
- package/src/ui/components/header.ts +62 -0
- package/src/ui/components/index.ts +8 -0
- package/src/ui/components/list-container.ts +97 -0
- package/src/ui/components/log-viewer.ts +217 -0
- package/src/ui/components/status-bar.ts +99 -0
- package/src/ui/components/tabbar.ts +79 -0
- package/src/ui/controllers/confirm-controller.ts +57 -0
- package/src/ui/controllers/index.ts +92 -0
- package/src/ui/controllers/log-controller.ts +173 -0
- package/src/ui/controllers/log-viewer-controller.ts +52 -0
- package/src/ui/controllers/main-controller.ts +142 -0
- package/src/ui/controllers/message-viewer-controller.ts +176 -0
- package/src/ui/controllers/orphan-controller.ts +125 -0
- package/src/ui/controllers/project-viewer-controller.ts +113 -0
- package/src/ui/controllers/session-viewer-controller.ts +181 -0
- package/src/ui/keybindings.ts +158 -0
- package/src/ui/state.ts +299 -0
- package/src/ui/views/base-view.ts +92 -0
- package/src/ui/views/index.ts +45 -0
- package/src/ui/views/log-list.ts +81 -0
- package/src/ui/views/main-view.ts +242 -0
- package/src/ui/views/orphan-list.ts +241 -0
- package/src/utils.ts +118 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { readdir, rm, readFile, writeFile } from "fs/promises"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import {
|
|
4
|
+
SESSION_DIR,
|
|
5
|
+
MESSAGE_DIR,
|
|
6
|
+
PART_DIR,
|
|
7
|
+
PROJECT_DIR,
|
|
8
|
+
SESSION_DIFF_DIR,
|
|
9
|
+
TODO_DIR,
|
|
10
|
+
SNAPSHOT_DIR,
|
|
11
|
+
FRECENCY_FILE,
|
|
12
|
+
} from "../config"
|
|
13
|
+
import type { SessionInfo, ProjectInfo, LogFile, Message, FrecencyEntry } from "../types"
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Utility Functions
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
async function safeRm(path: string, options?: { recursive?: boolean }): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await rm(path, { force: true, ...options })
|
|
22
|
+
return true
|
|
23
|
+
} catch {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readJsonFile<T>(path: string): Promise<T | null> {
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(path, "utf-8")
|
|
31
|
+
return JSON.parse(content) as T
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Session Deletion
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
export interface DeleteSessionResult {
|
|
42
|
+
success: boolean
|
|
43
|
+
sessionID: string
|
|
44
|
+
filesDeleted: number
|
|
45
|
+
bytesFreed: number
|
|
46
|
+
error?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function deleteSession(session: SessionInfo): Promise<DeleteSessionResult> {
|
|
50
|
+
let filesDeleted = 0
|
|
51
|
+
const bytesFreed = session.sizeBytes
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// 1. Get all message IDs for this session
|
|
55
|
+
const messageDir = join(MESSAGE_DIR, session.id)
|
|
56
|
+
let messageIDs: string[] = []
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const messageFiles = await readdir(messageDir)
|
|
60
|
+
for (const file of messageFiles) {
|
|
61
|
+
if (!file.endsWith(".json")) continue
|
|
62
|
+
const message = await readJsonFile<Message>(join(messageDir, file))
|
|
63
|
+
if (message) {
|
|
64
|
+
messageIDs.push(message.id)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// No messages directory
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Delete all parts for each message
|
|
72
|
+
for (const messageID of messageIDs) {
|
|
73
|
+
const partDir = join(PART_DIR, messageID)
|
|
74
|
+
if (await safeRm(partDir, { recursive: true })) {
|
|
75
|
+
filesDeleted++
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Delete messages directory
|
|
80
|
+
if (await safeRm(messageDir, { recursive: true })) {
|
|
81
|
+
filesDeleted++
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 4. Delete session diff
|
|
85
|
+
const diffPath = join(SESSION_DIFF_DIR, `${session.id}.json`)
|
|
86
|
+
if (await safeRm(diffPath)) {
|
|
87
|
+
filesDeleted++
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 5. Delete session todo
|
|
91
|
+
const todoPath = join(TODO_DIR, `${session.id}.json`)
|
|
92
|
+
if (await safeRm(todoPath)) {
|
|
93
|
+
filesDeleted++
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 6. Delete session file itself
|
|
97
|
+
const sessionPath = join(SESSION_DIR, session.projectID, `${session.id}.json`)
|
|
98
|
+
if (await safeRm(sessionPath)) {
|
|
99
|
+
filesDeleted++
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
sessionID: session.id,
|
|
105
|
+
filesDeleted,
|
|
106
|
+
bytesFreed,
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
sessionID: session.id,
|
|
112
|
+
filesDeleted,
|
|
113
|
+
bytesFreed: 0,
|
|
114
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function deleteSessions(
|
|
120
|
+
sessions: SessionInfo[]
|
|
121
|
+
): Promise<DeleteSessionResult[]> {
|
|
122
|
+
const results: DeleteSessionResult[] = []
|
|
123
|
+
for (const session of sessions) {
|
|
124
|
+
results.push(await deleteSession(session))
|
|
125
|
+
}
|
|
126
|
+
return results
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Project Deletion
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
export interface DeleteProjectResult {
|
|
134
|
+
success: boolean
|
|
135
|
+
projectID: string
|
|
136
|
+
sessionsDeleted: number
|
|
137
|
+
filesDeleted: number
|
|
138
|
+
bytesFreed: number
|
|
139
|
+
frecencyEntriesRemoved: number
|
|
140
|
+
error?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function deleteProject(project: ProjectInfo): Promise<DeleteProjectResult> {
|
|
144
|
+
let sessionsDeleted = 0
|
|
145
|
+
let totalFilesDeleted = 0
|
|
146
|
+
let totalBytesFreed = 0
|
|
147
|
+
let frecencyEntriesRemoved = 0
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
// 1. Delete all sessions for this project
|
|
151
|
+
for (const session of project.sessions) {
|
|
152
|
+
const result = await deleteSession(session)
|
|
153
|
+
if (result.success) {
|
|
154
|
+
sessionsDeleted++
|
|
155
|
+
totalFilesDeleted += result.filesDeleted
|
|
156
|
+
totalBytesFreed += result.bytesFreed
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 2. Delete project file
|
|
161
|
+
const projectPath = join(PROJECT_DIR, `${project.id}.json`)
|
|
162
|
+
if (await safeRm(projectPath)) {
|
|
163
|
+
totalFilesDeleted++
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3. Delete session directory for this project (should be empty now)
|
|
167
|
+
const projectSessionDir = join(SESSION_DIR, project.id)
|
|
168
|
+
await safeRm(projectSessionDir, { recursive: true })
|
|
169
|
+
|
|
170
|
+
// 4. Delete snapshot directory
|
|
171
|
+
const snapshotDir = join(SNAPSHOT_DIR, project.id)
|
|
172
|
+
if (await safeRm(snapshotDir, { recursive: true })) {
|
|
173
|
+
totalFilesDeleted++
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 5. Clean up frecency entries
|
|
177
|
+
frecencyEntriesRemoved = await cleanFrecencyForDirectory(project.worktree)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
projectID: project.id,
|
|
182
|
+
sessionsDeleted,
|
|
183
|
+
filesDeleted: totalFilesDeleted,
|
|
184
|
+
bytesFreed: totalBytesFreed,
|
|
185
|
+
frecencyEntriesRemoved,
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
projectID: project.id,
|
|
191
|
+
sessionsDeleted,
|
|
192
|
+
filesDeleted: totalFilesDeleted,
|
|
193
|
+
bytesFreed: totalBytesFreed,
|
|
194
|
+
frecencyEntriesRemoved,
|
|
195
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Frecency Cleanup
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
export async function cleanFrecencyForDirectory(directory: string): Promise<number> {
|
|
205
|
+
try {
|
|
206
|
+
const content = await readFile(FRECENCY_FILE, "utf-8")
|
|
207
|
+
const lines = content.trim().split("\n")
|
|
208
|
+
const filteredLines: string[] = []
|
|
209
|
+
let removedCount = 0
|
|
210
|
+
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
if (!line) continue
|
|
213
|
+
try {
|
|
214
|
+
const entry = JSON.parse(line) as FrecencyEntry
|
|
215
|
+
// Keep entries that don't start with the deleted directory
|
|
216
|
+
if (!entry.path.startsWith(directory)) {
|
|
217
|
+
filteredLines.push(line)
|
|
218
|
+
} else {
|
|
219
|
+
removedCount++
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
// Keep invalid lines as-is
|
|
223
|
+
filteredLines.push(line)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Write back the filtered content
|
|
228
|
+
await writeFile(FRECENCY_FILE, filteredLines.join("\n") + "\n")
|
|
229
|
+
|
|
230
|
+
return removedCount
|
|
231
|
+
} catch {
|
|
232
|
+
return 0
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Log Deletion
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
export interface DeleteLogResult {
|
|
241
|
+
success: boolean
|
|
242
|
+
filename: string
|
|
243
|
+
bytesFreed: number
|
|
244
|
+
error?: string
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function deleteLog(log: LogFile): Promise<DeleteLogResult> {
|
|
248
|
+
try {
|
|
249
|
+
await rm(log.path, { force: true })
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
filename: log.filename,
|
|
253
|
+
bytesFreed: log.sizeBytes,
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
filename: log.filename,
|
|
259
|
+
bytesFreed: 0,
|
|
260
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function deleteLogs(logs: LogFile[]): Promise<DeleteLogResult[]> {
|
|
266
|
+
const results: DeleteLogResult[] = []
|
|
267
|
+
for (const log of logs) {
|
|
268
|
+
results.push(await deleteLog(log))
|
|
269
|
+
}
|
|
270
|
+
return results
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// Check if project has remaining sessions
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
export async function getProjectSessionCount(projectID: string): Promise<number> {
|
|
278
|
+
try {
|
|
279
|
+
const projectSessionDir = join(SESSION_DIR, projectID)
|
|
280
|
+
const files = await readdir(projectSessionDir)
|
|
281
|
+
return files.filter((f) => f.endsWith(".json")).length
|
|
282
|
+
} catch {
|
|
283
|
+
return 0
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// After deleting sessions, check if we should clean up the project too
|
|
288
|
+
export async function cleanupEmptyProject(
|
|
289
|
+
projectID: string,
|
|
290
|
+
worktree: string
|
|
291
|
+
): Promise<boolean> {
|
|
292
|
+
const remainingCount = await getProjectSessionCount(projectID)
|
|
293
|
+
if (remainingCount === 0) {
|
|
294
|
+
// Delete project file
|
|
295
|
+
const projectPath = join(PROJECT_DIR, `${projectID}.json`)
|
|
296
|
+
await safeRm(projectPath)
|
|
297
|
+
|
|
298
|
+
// Delete session directory
|
|
299
|
+
const projectSessionDir = join(SESSION_DIR, projectID)
|
|
300
|
+
await safeRm(projectSessionDir, { recursive: true })
|
|
301
|
+
|
|
302
|
+
// Delete snapshots
|
|
303
|
+
const snapshotDir = join(SNAPSHOT_DIR, projectID)
|
|
304
|
+
await safeRm(snapshotDir, { recursive: true })
|
|
305
|
+
|
|
306
|
+
// Clean frecency
|
|
307
|
+
await cleanFrecencyForDirectory(worktree)
|
|
308
|
+
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
return false
|
|
312
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { App } from "./ui/app"
|
|
4
|
+
|
|
5
|
+
async function main(): Promise<void> {
|
|
6
|
+
const args = process.argv.slice(2)
|
|
7
|
+
|
|
8
|
+
// Handle --help and --version flags
|
|
9
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
10
|
+
console.log(`
|
|
11
|
+
opencode-session - Manage OpenCode sessions
|
|
12
|
+
|
|
13
|
+
Usage: opencode-session [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
-h, --help Show this help message
|
|
17
|
+
-v, --version Show version number
|
|
18
|
+
|
|
19
|
+
Navigation:
|
|
20
|
+
j / ↓ Move down
|
|
21
|
+
k / ↑ Move up
|
|
22
|
+
Space Toggle selection
|
|
23
|
+
a Select all / Deselect all
|
|
24
|
+
Enter Confirm action
|
|
25
|
+
|
|
26
|
+
Actions:
|
|
27
|
+
d Delete selected item(s)
|
|
28
|
+
D Delete ALL (in orphans/logs view)
|
|
29
|
+
o View orphan sessions
|
|
30
|
+
p View projects
|
|
31
|
+
l View log files
|
|
32
|
+
r Reload data
|
|
33
|
+
|
|
34
|
+
General:
|
|
35
|
+
Esc / q Go back / Quit
|
|
36
|
+
? Show help
|
|
37
|
+
`)
|
|
38
|
+
process.exit(0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
42
|
+
const pkg = await import("../package.json")
|
|
43
|
+
console.log(pkg.version)
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Start the app
|
|
48
|
+
const app = new App()
|
|
49
|
+
await app.init()
|
|
50
|
+
app.run()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch((error) => {
|
|
54
|
+
console.error("Fatal error:", error)
|
|
55
|
+
process.exit(1)
|
|
56
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Session Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export interface SessionTime {
|
|
6
|
+
created: number
|
|
7
|
+
updated: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SessionSummary {
|
|
11
|
+
additions: number
|
|
12
|
+
deletions: number
|
|
13
|
+
files: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Session {
|
|
17
|
+
id: string
|
|
18
|
+
slug: string
|
|
19
|
+
version: string
|
|
20
|
+
projectID: string
|
|
21
|
+
directory: string
|
|
22
|
+
parentID?: string
|
|
23
|
+
title?: string
|
|
24
|
+
time: SessionTime
|
|
25
|
+
summary?: SessionSummary
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SessionInfo extends Session {
|
|
29
|
+
sizeBytes: number
|
|
30
|
+
messageCount: number
|
|
31
|
+
isOrphan: boolean
|
|
32
|
+
projectWorktree: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Message & Part Types
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
export interface MessageTime {
|
|
40
|
+
created: number
|
|
41
|
+
completed?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MessageTokens {
|
|
45
|
+
input: number
|
|
46
|
+
output: number
|
|
47
|
+
reasoning: number
|
|
48
|
+
cache: { read: number; write: number }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Message {
|
|
52
|
+
id: string
|
|
53
|
+
sessionID: string
|
|
54
|
+
role: "user" | "assistant"
|
|
55
|
+
time: MessageTime
|
|
56
|
+
parentID?: string
|
|
57
|
+
modelID?: string
|
|
58
|
+
providerID?: string
|
|
59
|
+
mode?: string
|
|
60
|
+
agent?: string
|
|
61
|
+
cost?: number
|
|
62
|
+
tokens?: MessageTokens
|
|
63
|
+
finish?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Part {
|
|
67
|
+
id: string
|
|
68
|
+
sessionID: string
|
|
69
|
+
messageID: string
|
|
70
|
+
type: "text" | "tool" | "step-start" | "step-finish"
|
|
71
|
+
text?: string
|
|
72
|
+
callID?: string
|
|
73
|
+
tool?: string
|
|
74
|
+
state?: {
|
|
75
|
+
status?: string
|
|
76
|
+
input?: Record<string, unknown>
|
|
77
|
+
output?: string
|
|
78
|
+
title?: string
|
|
79
|
+
metadata?: Record<string, unknown>
|
|
80
|
+
time?: { start: number; end: number }
|
|
81
|
+
}
|
|
82
|
+
time?: { start: number; end: number }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Todo Types
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
export interface Todo {
|
|
90
|
+
id: string
|
|
91
|
+
content: string
|
|
92
|
+
status: "pending" | "in_progress" | "completed" | "cancelled"
|
|
93
|
+
priority: "high" | "medium" | "low"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Project Storage Info
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export interface ProjectStorageInfo {
|
|
101
|
+
sessionFiles: number
|
|
102
|
+
messageFiles: number
|
|
103
|
+
partFiles: number
|
|
104
|
+
diffSize: number
|
|
105
|
+
todoSize: number
|
|
106
|
+
totalMessages: number
|
|
107
|
+
totalParts: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Project Types
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
export interface ProjectTime {
|
|
115
|
+
created: number
|
|
116
|
+
updated: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface Project {
|
|
120
|
+
id: string
|
|
121
|
+
worktree: string
|
|
122
|
+
vcs?: string
|
|
123
|
+
sandboxes?: string[]
|
|
124
|
+
time: ProjectTime
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ProjectInfo extends Project {
|
|
128
|
+
sessionCount: number
|
|
129
|
+
totalSizeBytes: number
|
|
130
|
+
isOrphan: boolean
|
|
131
|
+
sessions: SessionInfo[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Frecency & Log Types
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
export interface FrecencyEntry {
|
|
139
|
+
path: string
|
|
140
|
+
frequency: number
|
|
141
|
+
lastOpen: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface LogFile {
|
|
145
|
+
path: string
|
|
146
|
+
filename: string
|
|
147
|
+
date: Date
|
|
148
|
+
sizeBytes: number
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// UI Types
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
export type ViewType = "main" | "orphans" | "logs"
|
|
156
|
+
|
|
157
|
+
// View order for Tab navigation
|
|
158
|
+
export const VIEW_ORDER: ViewType[] = ["main", "orphans", "logs"]
|
|
159
|
+
|
|
160
|
+
export interface AppState {
|
|
161
|
+
view: ViewType
|
|
162
|
+
sessions: SessionInfo[]
|
|
163
|
+
projects: ProjectInfo[]
|
|
164
|
+
logs: LogFile[]
|
|
165
|
+
selectedIndices: Set<number>
|
|
166
|
+
currentIndex: number
|
|
167
|
+
expandedProjects: Set<string>
|
|
168
|
+
showConfirm: boolean
|
|
169
|
+
confirmAction: (() => Promise<void>) | null
|
|
170
|
+
confirmMessage: string
|
|
171
|
+
isLoading: boolean
|
|
172
|
+
statusMessage: string
|
|
173
|
+
}
|