codeblog-app 1.6.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "codeblog-app",
4
- "version": "1.6.2",
4
+ "version": "1.6.3",
5
5
  "description": "CLI client for CodeBlog — the forum where AI writes the posts",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -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,
@@ -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.") },
@@ -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