codeblog-app 1.6.1 → 1.6.3
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/storage/chat.ts +92 -0
- package/src/storage/db.ts +17 -0
- package/src/tui/commands.ts +11 -0
- package/src/tui/routes/home.tsx +36 -1
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Database as BunDatabase } from "bun:sqlite"
|
|
2
|
+
import { Global } from "../global"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
function db() {
|
|
6
|
+
const dbpath = path.join(Global.Path.data, "codeblog.db")
|
|
7
|
+
const sqlite = new BunDatabase(dbpath, { create: true })
|
|
8
|
+
sqlite.run("PRAGMA journal_mode = WAL")
|
|
9
|
+
sqlite.run("PRAGMA foreign_keys = ON")
|
|
10
|
+
|
|
11
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
title TEXT,
|
|
14
|
+
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
15
|
+
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
16
|
+
)`)
|
|
17
|
+
|
|
18
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_messages (
|
|
19
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
+
session_id TEXT NOT NULL,
|
|
21
|
+
role TEXT NOT NULL,
|
|
22
|
+
content TEXT NOT NULL,
|
|
23
|
+
tool_name TEXT,
|
|
24
|
+
tool_status TEXT,
|
|
25
|
+
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
26
|
+
)`)
|
|
27
|
+
|
|
28
|
+
return sqlite
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChatMsg {
|
|
32
|
+
role: "user" | "assistant" | "tool"
|
|
33
|
+
content: string
|
|
34
|
+
toolName?: string
|
|
35
|
+
toolStatus?: "running" | "done" | "error"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export namespace ChatHistory {
|
|
39
|
+
export function create(id: string, title?: string) {
|
|
40
|
+
const d = db()
|
|
41
|
+
d.run("INSERT OR REPLACE INTO chat_sessions (id, title, time_created, time_updated) VALUES (?, ?, ?, ?)", [id, title || null, Date.now(), Date.now()])
|
|
42
|
+
d.close()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function save(sessionId: string, messages: ChatMsg[]) {
|
|
46
|
+
const d = db()
|
|
47
|
+
d.run("DELETE FROM chat_messages WHERE session_id = ?", [sessionId])
|
|
48
|
+
const stmt = d.prepare("INSERT INTO chat_messages (session_id, role, content, tool_name, tool_status, time_created) VALUES (?, ?, ?, ?, ?, ?)")
|
|
49
|
+
for (const m of messages) {
|
|
50
|
+
stmt.run(sessionId, m.role, m.content, m.toolName || null, m.toolStatus || null, Date.now())
|
|
51
|
+
}
|
|
52
|
+
// Update session title from first user message
|
|
53
|
+
const first = messages.find((m) => m.role === "user")
|
|
54
|
+
if (first) {
|
|
55
|
+
const title = first.content.slice(0, 80)
|
|
56
|
+
d.run("UPDATE chat_sessions SET title = ?, time_updated = ? WHERE id = ?", [title, Date.now(), sessionId])
|
|
57
|
+
}
|
|
58
|
+
d.close()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function load(sessionId: string): ChatMsg[] {
|
|
62
|
+
const d = db()
|
|
63
|
+
const rows = d.query("SELECT role, content, tool_name, tool_status FROM chat_messages WHERE session_id = ? ORDER BY id ASC").all(sessionId) as any[]
|
|
64
|
+
d.close()
|
|
65
|
+
return rows.map((r) => ({
|
|
66
|
+
role: r.role,
|
|
67
|
+
content: r.content,
|
|
68
|
+
...(r.tool_name ? { toolName: r.tool_name } : {}),
|
|
69
|
+
...(r.tool_status ? { toolStatus: r.tool_status } : {}),
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function list(limit = 20): Array<{ id: string; title: string | null; time: number; count: number }> {
|
|
74
|
+
const d = db()
|
|
75
|
+
const rows = d.query(`
|
|
76
|
+
SELECT s.id, s.title, s.time_updated as time,
|
|
77
|
+
(SELECT COUNT(*) FROM chat_messages WHERE session_id = s.id) as count
|
|
78
|
+
FROM chat_sessions s
|
|
79
|
+
ORDER BY s.time_updated DESC
|
|
80
|
+
LIMIT ?
|
|
81
|
+
`).all(limit) as any[]
|
|
82
|
+
d.close()
|
|
83
|
+
return rows.map((r) => ({ id: r.id, title: r.title, time: r.time, count: r.count }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function remove(sessionId: string) {
|
|
87
|
+
const d = db()
|
|
88
|
+
d.run("DELETE FROM chat_messages WHERE session_id = ?", [sessionId])
|
|
89
|
+
d.run("DELETE FROM chat_sessions WHERE id = ?", [sessionId])
|
|
90
|
+
d.close()
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/storage/db.ts
CHANGED
|
@@ -47,6 +47,23 @@ export namespace Database {
|
|
|
47
47
|
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
48
48
|
)`)
|
|
49
49
|
|
|
50
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
title TEXT,
|
|
53
|
+
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
54
|
+
time_updated INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
55
|
+
)`)
|
|
56
|
+
|
|
57
|
+
sqlite.run(`CREATE TABLE IF NOT EXISTS chat_messages (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
session_id TEXT NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE,
|
|
60
|
+
role TEXT NOT NULL,
|
|
61
|
+
content TEXT NOT NULL,
|
|
62
|
+
tool_name TEXT,
|
|
63
|
+
tool_status TEXT,
|
|
64
|
+
time_created INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
65
|
+
)`)
|
|
66
|
+
|
|
50
67
|
sqlite.run(`CREATE TABLE IF NOT EXISTS notifications_cache (
|
|
51
68
|
id TEXT PRIMARY KEY,
|
|
52
69
|
type TEXT NOT NULL,
|
package/src/tui/commands.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface CommandDeps {
|
|
|
16
16
|
startAIConfig: () => void
|
|
17
17
|
setMode: (mode: "dark" | "light") => void
|
|
18
18
|
send: (prompt: string) => void
|
|
19
|
+
resume: (id?: string) => void
|
|
20
|
+
listSessions: () => Array<{ id: string; title: string | null; time: number; count: number }>
|
|
19
21
|
colors: {
|
|
20
22
|
primary: string
|
|
21
23
|
success: string
|
|
@@ -49,6 +51,15 @@ export function createCommands(deps: CommandDeps): CmdDef[] {
|
|
|
49
51
|
{ name: "/dark", description: "Switch to dark mode", action: () => { deps.setMode("dark"); deps.showMsg("Dark mode", deps.colors.text) } },
|
|
50
52
|
{ name: "/light", description: "Switch to light mode", action: () => { deps.setMode("light"); deps.showMsg("Light mode", deps.colors.text) } },
|
|
51
53
|
{ name: "/exit", description: "Exit CodeBlog", action: () => deps.exit() },
|
|
54
|
+
{ name: "/resume", description: "Resume last chat session", action: (parts) => deps.resume(parts[1]) },
|
|
55
|
+
{ name: "/history", description: "Show recent chat sessions", action: () => {
|
|
56
|
+
try {
|
|
57
|
+
const sessions = deps.listSessions()
|
|
58
|
+
if (sessions.length === 0) { deps.showMsg("No chat history yet", deps.colors.warning); return }
|
|
59
|
+
const lines = sessions.map((s, i) => `${i + 1}. ${s.title || "(untitled)"} (${s.count} msgs, ${new Date(s.time).toLocaleDateString()})`)
|
|
60
|
+
deps.showMsg(lines.join(" | "), deps.colors.text)
|
|
61
|
+
} catch { deps.showMsg("Failed to load history", deps.colors.error) }
|
|
62
|
+
}},
|
|
52
63
|
|
|
53
64
|
// === Session tools (scan_sessions, read_session, analyze_session) ===
|
|
54
65
|
{ name: "/scan", description: "Scan IDE coding sessions", action: () => deps.send("Scan my local IDE coding sessions and tell me what you found. Show sources, projects, and session counts.") },
|
package/src/tui/routes/home.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { useTheme } from "../context/theme"
|
|
|
6
6
|
import { createCommands, LOGO, TIPS } from "../commands"
|
|
7
7
|
import { TOOL_LABELS } from "../../ai/tools"
|
|
8
8
|
import { mask, saveProvider } from "../../ai/configure"
|
|
9
|
+
import { ChatHistory } from "../../storage/chat"
|
|
9
10
|
|
|
10
11
|
interface ChatMsg {
|
|
11
12
|
role: "user" | "assistant" | "tool"
|
|
@@ -37,8 +38,36 @@ export function Home(props: {
|
|
|
37
38
|
const [streamText, setStreamText] = createSignal("")
|
|
38
39
|
let abortCtrl: AbortController | undefined
|
|
39
40
|
let escCooldown = 0
|
|
41
|
+
let sessionId = ""
|
|
40
42
|
const chatting = createMemo(() => messages().length > 0 || streaming())
|
|
41
43
|
|
|
44
|
+
function ensureSession() {
|
|
45
|
+
if (!sessionId) {
|
|
46
|
+
sessionId = crypto.randomUUID()
|
|
47
|
+
try { ChatHistory.create(sessionId) } catch {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveChat() {
|
|
52
|
+
if (!sessionId) return
|
|
53
|
+
try { ChatHistory.save(sessionId, messages()) } catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resumeSession(sid?: string) {
|
|
57
|
+
try {
|
|
58
|
+
if (!sid) {
|
|
59
|
+
const sessions = ChatHistory.list(1)
|
|
60
|
+
if (sessions.length === 0) { showMsg("No previous sessions", theme.colors.warning); return }
|
|
61
|
+
sid = sessions[0].id
|
|
62
|
+
}
|
|
63
|
+
const msgs = ChatHistory.load(sid)
|
|
64
|
+
if (msgs.length === 0) { showMsg("Session is empty", theme.colors.warning); return }
|
|
65
|
+
sessionId = sid
|
|
66
|
+
setMessages(msgs as ChatMsg[])
|
|
67
|
+
showMsg("Resumed session", theme.colors.success)
|
|
68
|
+
} catch { showMsg("Failed to resume", theme.colors.error) }
|
|
69
|
+
}
|
|
70
|
+
|
|
42
71
|
// Shimmer animation for thinking state (like Claude Code)
|
|
43
72
|
const SHIMMER_WORDS = ["Thinking", "Reasoning", "Composing", "Reflecting", "Analyzing", "Processing"]
|
|
44
73
|
const [shimmerIdx, setShimmerIdx] = createSignal(0)
|
|
@@ -65,6 +94,7 @@ export function Home(props: {
|
|
|
65
94
|
|
|
66
95
|
function clearChat() {
|
|
67
96
|
setMessages([]); setStreamText(""); setStreaming(false); setMessage("")
|
|
97
|
+
sessionId = ""
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
const commands = createCommands({
|
|
@@ -80,6 +110,8 @@ export function Home(props: {
|
|
|
80
110
|
},
|
|
81
111
|
setMode: theme.setMode,
|
|
82
112
|
send,
|
|
113
|
+
resume: resumeSession,
|
|
114
|
+
listSessions: () => { try { return ChatHistory.list(10) } catch { return [] } },
|
|
83
115
|
colors: theme.colors,
|
|
84
116
|
})
|
|
85
117
|
|
|
@@ -110,6 +142,7 @@ export function Home(props: {
|
|
|
110
142
|
|
|
111
143
|
async function send(text: string) {
|
|
112
144
|
if (!text.trim() || streaming()) return
|
|
145
|
+
ensureSession()
|
|
113
146
|
const userMsg: ChatMsg = { role: "user", content: text.trim() }
|
|
114
147
|
const prev = messages()
|
|
115
148
|
setMessages([...prev, userMsg])
|
|
@@ -161,6 +194,7 @@ export function Home(props: {
|
|
|
161
194
|
return [...updated, { role: "assistant" as const, content: `Error: ${err.message}` }]
|
|
162
195
|
})
|
|
163
196
|
setStreamText(""); setStreaming(false)
|
|
197
|
+
saveChat()
|
|
164
198
|
},
|
|
165
199
|
}, mid, abortCtrl.signal)
|
|
166
200
|
abortCtrl = undefined
|
|
@@ -169,6 +203,7 @@ export function Home(props: {
|
|
|
169
203
|
setMessages((p) => [...p, { role: "assistant", content: `Error: ${msg}` }])
|
|
170
204
|
setStreamText("")
|
|
171
205
|
setStreaming(false)
|
|
206
|
+
saveChat()
|
|
172
207
|
}
|
|
173
208
|
}
|
|
174
209
|
|
|
@@ -329,7 +364,7 @@ export function Home(props: {
|
|
|
329
364
|
|
|
330
365
|
{/* When chatting: messages fill the space */}
|
|
331
366
|
<Show when={chatting()}>
|
|
332
|
-
<box flexDirection="column" flexGrow={1} paddingTop={1}>
|
|
367
|
+
<box flexDirection="column" flexGrow={1} paddingTop={1} overflow="scroll">
|
|
333
368
|
<For each={messages()}>
|
|
334
369
|
{(msg) => (
|
|
335
370
|
<box flexShrink={0}>
|