-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathlogs.py
More file actions
155 lines (138 loc) · 6.64 KB
/
Copy pathlogs.py
File metadata and controls
155 lines (138 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""
API route for logs — mirrors src/app/api/logs/route.ts
GET /api/logs
"""
import json
import logging
import os
import re
import sqlite3
from contextlib import closing
from datetime import datetime
from flask import Blueprint, jsonify
from utils.workspace_path import resolve_workspace_path
from utils.path_helpers import to_epoch_ms, warn_workspace_json_read
bp = Blueprint("logs", __name__)
_logger = logging.getLogger(__name__)
def _extract_chat_id_from_bubble_key(key: str) -> str | None:
m = re.match(r"^bubbleId:([^:]+):", key)
return m.group(1) if m else None
@bp.route("/api/logs")
def get_logs():
try:
workspace_path = resolve_workspace_path()
logs = []
# Global storage (new Cursor format)
global_db_path = os.path.normpath(os.path.join(workspace_path, "..", "globalStorage", "state.vscdb"))
if os.path.isfile(global_db_path):
try:
# closing() guarantees .close() on scope exit (issue #17).
with closing(sqlite3.connect(f"file:{global_db_path}?mode=ro", uri=True)) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'").fetchall()
chat_map: dict[str, list] = {}
for row in rows:
chat_id = _extract_chat_id_from_bubble_key(row["key"])
if not chat_id:
continue
try:
bubble = json.loads(row["value"])
chat_map.setdefault(chat_id, []).append(bubble)
except Exception as e:
_logger.warning(
"Failed to decode bubble row %s: %s",
row["key"],
e,
)
for chat_id, bubbles in chat_map.items():
bubbles = [b for b in bubbles if isinstance(b, dict)]
if not bubbles:
continue
bubbles.sort(key=lambda b: to_epoch_ms(b.get("createdAt") or b.get("timestamp")))
first = bubbles[0]
last = bubbles[-1]
if not first or not last:
continue
title_text = first.get("text", "") or ""
title = title_text.split("\n")[0] if title_text else f"Chat {chat_id[:8]}"
logs.append({
"id": chat_id,
"workspaceId": "global",
"workspaceFolder": None,
"title": title,
"timestamp": to_epoch_ms(last.get("createdAt") or last.get("timestamp")) or int(datetime.now().timestamp() * 1000),
"type": "chat",
"messageCount": len(bubbles),
})
except Exception:
_logger.exception("Error reading global storage")
# Per-workspace (legacy)
try:
for name in os.listdir(workspace_path):
full = os.path.join(workspace_path, name)
if not os.path.isdir(full):
continue
db_path = os.path.join(full, "state.vscdb")
wj_path = os.path.join(full, "workspace.json")
if not os.path.isfile(db_path):
continue
workspace_folder = None
try:
with open(wj_path, "r", encoding="utf-8") as f:
wd = json.load(f)
workspace_folder = wd.get("folder")
except Exception as e:
warn_workspace_json_read(_logger, name, e)
try:
# closing() guarantees .close() on scope exit (issue #17).
with closing(sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)) as conn:
# Chat logs
chat_row = conn.execute(
"SELECT value FROM ItemTable WHERE [key] = 'workbench.panel.aichat.view.aichat.chatdata'"
).fetchone()
if chat_row and chat_row[0]:
data = json.loads(chat_row[0])
tabs = data.get("tabs") or []
for tab in tabs:
logs.append({
"id": tab.get("id", ""),
"workspaceId": name,
"workspaceFolder": workspace_folder,
"title": tab.get("title") or f"Chat {(tab.get('id') or '')[:8]}",
"timestamp": tab.get("timestamp", 0),
"type": "chat",
"messageCount": len(tab.get("bubbles") or []),
})
# Composer logs
comp_row = conn.execute(
"SELECT value FROM ItemTable WHERE [key] = 'composer.composerData'"
).fetchone()
if comp_row and comp_row[0]:
data = json.loads(comp_row[0])
for c in (data.get("allComposers") or []):
logs.append({
"id": c.get("composerId", ""),
"workspaceId": name,
"workspaceFolder": workspace_folder,
"title": c.get("text") or f"Composer {(c.get('composerId') or '')[:8]}",
"timestamp": to_epoch_ms(c.get("lastUpdatedAt")) or to_epoch_ms(c.get("createdAt")) or 0,
"type": "composer",
"messageCount": len(c.get("conversation") or []),
})
except Exception as e:
_logger.warning(
"Failed to read logs from workspace %s: %s",
name,
e,
)
except Exception as e:
_logger.warning(
"Failed to iterate workspaces under %s: %s",
workspace_path,
e,
)
logs.sort(key=lambda log: log.get("timestamp") or 0, reverse=True)
return jsonify({"logs": logs})
except Exception:
_logger.exception("Failed to get logs")
return jsonify({"error": "Failed to get logs", "logs": []}), 500