claude-session-dashboard 0.1.3 → 0.3.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/README.md +156 -14
- package/dist/client/assets/_dashboard-I7m6D7BE.js +1 -0
- package/dist/client/assets/_sessionId-DEliIff6.js +12 -0
- package/dist/client/assets/app-D7yorIIh.css +1 -0
- package/dist/client/assets/{createServerFn-Le0d8Pjz.js → createServerFn-Bn6_ISOt.js} +1 -1
- package/dist/client/assets/format-Bsprb3az.js +1 -0
- package/dist/client/assets/index-BkqRvnEf.js +1 -0
- package/dist/client/assets/{main-CzD8HjLq.js → main-CfJIADCp.js} +7 -7
- package/dist/client/assets/sessions.queries-CrJg4dYU.js +1 -0
- package/dist/client/assets/settings-C4_lsEzl.js +1 -0
- package/dist/client/assets/{settings.types-B4841OLF.js → settings.types-9Qf5WcRY.js} +1 -1
- package/dist/client/assets/stats-_r1gmaTe.js +4 -0
- package/dist/client/assets/useSessionCost-DPZ-ubM1.js +65 -0
- package/dist/client/favicon.svg +3 -0
- package/dist/server/assets/_dashboard-TUzgwLqB.js +112 -0
- package/dist/server/assets/{_sessionId-BwZK4Ezz.js → _sessionId-C-XZIPqn.js} +57 -35
- package/dist/server/assets/_tanstack-start-manifest_v-B51mSkGz.js +4 -0
- package/dist/server/assets/{claude-path-CkuljM34.js → claude-path-BdwflgZ1.js} +9 -3
- package/dist/server/assets/{format-CGmJnuhZ.js → format-DIZHV7IJ.js} +3 -3
- package/dist/server/assets/{index-D4VWrt2z.js → index-CKfH7HpA.js} +28 -60
- package/dist/server/assets/project-analytics.server-BkWSd6a8.js +61 -0
- package/dist/server/assets/{router-xTSe9UH_.js → router-Cb_hBXHI.js} +62 -31
- package/dist/server/assets/{session-detail.server-azkRfON2.js → session-detail.server-DLXl-Pn-.js} +1 -1
- package/dist/server/assets/session-scanner-CLfls9u-.js +93 -0
- package/dist/server/assets/sessions.queries-B5ZBiVJy.js +42 -0
- package/dist/server/assets/{sessions.server-B8zbmvSM.js → sessions.server-CUhasKW2.js} +5 -89
- package/dist/server/assets/{settings-ko61yfVs.js → settings-C0_KyVQQ.js} +66 -20
- package/dist/server/assets/stats-BtgVene-.js +886 -0
- package/dist/server/assets/{stats.server-BZWxV-mC.js → stats.server-qTOvID9-.js} +62 -3
- package/dist/server/assets/useSessionCost-CYs5UOX-.js +209 -0
- package/dist/server/server.js +13 -10
- package/package.json +11 -1
- package/dist/client/assets/_dashboard-CYwTENkn.js +0 -1
- package/dist/client/assets/_sessionId-Bwfhm_El.js +0 -12
- package/dist/client/assets/app-DhZyFob1.css +0 -1
- package/dist/client/assets/format-Bf-cSf6L.js +0 -1
- package/dist/client/assets/index-DXhX1hdS.js +0 -1
- package/dist/client/assets/settings-BSPc79zZ.js +0 -1
- package/dist/client/assets/stats-CDIvpOt9.js +0 -4
- package/dist/client/assets/useSessionCost-9NP6uhla.js +0 -61
- package/dist/server/assets/_dashboard--ukhquwO.js +0 -97
- package/dist/server/assets/_tanstack-start-manifest_v-gtQY7f-T.js +0 -4
- package/dist/server/assets/stats-DItsFPp5.js +0 -266
- package/dist/server/assets/useSessionCost-EB0VxklP.js +0 -76
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
|
|
2
|
+
import { s as scanAllSessions } from "./session-scanner-CLfls9u-.js";
|
|
3
|
+
import { c as createServerFn } from "../server.js";
|
|
4
|
+
import "node:fs";
|
|
5
|
+
import "node:path";
|
|
6
|
+
import "./claude-path-BdwflgZ1.js";
|
|
7
|
+
import "node:os";
|
|
8
|
+
import "./session-parser-CAEXxF1D.js";
|
|
9
|
+
import "node:readline";
|
|
10
|
+
import "@tanstack/history";
|
|
11
|
+
import "@tanstack/router-core/ssr/client";
|
|
12
|
+
import "@tanstack/router-core";
|
|
13
|
+
import "node:async_hooks";
|
|
14
|
+
import "@tanstack/router-core/ssr/server";
|
|
15
|
+
import "h3-v2";
|
|
16
|
+
import "tiny-invariant";
|
|
17
|
+
import "seroval";
|
|
18
|
+
import "react/jsx-runtime";
|
|
19
|
+
import "@tanstack/react-router/ssr/server";
|
|
20
|
+
import "@tanstack/react-router";
|
|
21
|
+
function aggregateProjectAnalytics(allSessions) {
|
|
22
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const session of allSessions) {
|
|
24
|
+
const existing = projectMap.get(session.projectPath) ?? [];
|
|
25
|
+
existing.push(session);
|
|
26
|
+
projectMap.set(session.projectPath, existing);
|
|
27
|
+
}
|
|
28
|
+
const projects = [];
|
|
29
|
+
for (const [projectPath, sessions] of projectMap) {
|
|
30
|
+
if (sessions.length === 0) continue;
|
|
31
|
+
const firstSession = sessions[0];
|
|
32
|
+
projects.push({
|
|
33
|
+
projectPath,
|
|
34
|
+
projectName: firstSession.projectName ?? projectPath.split("/").pop() ?? "Unknown",
|
|
35
|
+
totalSessions: sessions.length,
|
|
36
|
+
activeSessions: sessions.filter((s) => s.isActive).length,
|
|
37
|
+
totalMessages: sessions.reduce((sum, s) => sum + s.messageCount, 0),
|
|
38
|
+
totalDurationMs: sessions.reduce((sum, s) => sum + s.durationMs, 0),
|
|
39
|
+
firstSessionAt: sessions.reduce((min, s) => s.startedAt < min ? s.startedAt : min, firstSession.startedAt),
|
|
40
|
+
lastSessionAt: sessions.reduce((max, s) => s.lastActiveAt > max ? s.lastActiveAt : max, firstSession.lastActiveAt)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
projects.sort((a, b) => b.lastSessionAt.localeCompare(a.lastSessionAt));
|
|
44
|
+
return {
|
|
45
|
+
projects
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const getProjectAnalytics_createServerFn_handler = createServerRpc({
|
|
49
|
+
id: "64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb",
|
|
50
|
+
name: "getProjectAnalytics",
|
|
51
|
+
filename: "src/features/project-analytics/project-analytics.server.ts"
|
|
52
|
+
}, (opts) => getProjectAnalytics.__executeServer(opts));
|
|
53
|
+
const getProjectAnalytics = createServerFn({
|
|
54
|
+
method: "GET"
|
|
55
|
+
}).handler(getProjectAnalytics_createServerFn_handler, async () => {
|
|
56
|
+
const allSessions = await scanAllSessions();
|
|
57
|
+
return aggregateProjectAnalytics(allSessions);
|
|
58
|
+
});
|
|
59
|
+
export {
|
|
60
|
+
getProjectAnalytics_createServerFn_handler
|
|
61
|
+
};
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { createRootRoute, Outlet, HeadContent, Scripts, createFileRoute, lazyRouteComponent, redirect, createRouter } from "@tanstack/react-router";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
|
4
|
-
import { useState, useRef,
|
|
4
|
+
import { useState, useRef, useCallback, createContext, useContext } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
const OS_USERNAME_PATTERN = /^(\/(?:Users|home))\/[^/]+/;
|
|
7
|
-
function anonymizePath(path) {
|
|
7
|
+
function anonymizePath(path, anonymizedProjectName) {
|
|
8
|
+
if (anonymizedProjectName) {
|
|
9
|
+
return `.../${anonymizedProjectName}`;
|
|
10
|
+
}
|
|
8
11
|
return path.replace(OS_USERNAME_PATTERN, "$1/user");
|
|
9
12
|
}
|
|
10
13
|
const STORAGE_KEY = "claude-dashboard:privacy-mode";
|
|
14
|
+
function readStoredPrivacyMode() {
|
|
15
|
+
if (typeof window === "undefined") return false;
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(STORAGE_KEY) === "true";
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
const PrivacyContext = createContext(null);
|
|
12
23
|
function PrivacyProvider({ children }) {
|
|
13
|
-
const [privacyMode, setPrivacyMode] = useState(
|
|
24
|
+
const [privacyMode, setPrivacyMode] = useState(readStoredPrivacyMode);
|
|
14
25
|
const projectNameMapRef = useRef(/* @__PURE__ */ new Map());
|
|
15
26
|
const nextIndexRef = useRef(1);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
19
|
-
if (stored === "true") {
|
|
20
|
-
setPrivacyMode(true);
|
|
21
|
-
}
|
|
22
|
-
} catch {
|
|
23
|
-
}
|
|
24
|
-
}, []);
|
|
27
|
+
const branchNameMapRef = useRef(/* @__PURE__ */ new Map());
|
|
28
|
+
const nextBranchIndexRef = useRef(1);
|
|
25
29
|
const togglePrivacyMode = useCallback(() => {
|
|
26
30
|
setPrivacyMode((prev) => {
|
|
27
31
|
const next = !prev;
|
|
@@ -31,16 +35,11 @@ function PrivacyProvider({ children }) {
|
|
|
31
35
|
}
|
|
32
36
|
projectNameMapRef.current = /* @__PURE__ */ new Map();
|
|
33
37
|
nextIndexRef.current = 1;
|
|
38
|
+
branchNameMapRef.current = /* @__PURE__ */ new Map();
|
|
39
|
+
nextBranchIndexRef.current = 1;
|
|
34
40
|
return next;
|
|
35
41
|
});
|
|
36
42
|
}, []);
|
|
37
|
-
const anonymizePath$1 = useCallback(
|
|
38
|
-
(path) => {
|
|
39
|
-
if (!privacyMode) return path;
|
|
40
|
-
return anonymizePath(path);
|
|
41
|
-
},
|
|
42
|
-
[privacyMode]
|
|
43
|
-
);
|
|
44
43
|
const anonymizeProjectName = useCallback(
|
|
45
44
|
(name) => {
|
|
46
45
|
if (!privacyMode) return name;
|
|
@@ -53,14 +52,38 @@ function PrivacyProvider({ children }) {
|
|
|
53
52
|
},
|
|
54
53
|
[privacyMode]
|
|
55
54
|
);
|
|
55
|
+
const anonymizePathFn = useCallback(
|
|
56
|
+
(path, projectName) => {
|
|
57
|
+
if (!privacyMode) return path;
|
|
58
|
+
if (projectName) {
|
|
59
|
+
const anonName = anonymizeProjectName(projectName);
|
|
60
|
+
return anonymizePath(path, anonName);
|
|
61
|
+
}
|
|
62
|
+
return anonymizePath(path);
|
|
63
|
+
},
|
|
64
|
+
[privacyMode, anonymizeProjectName]
|
|
65
|
+
);
|
|
66
|
+
const anonymizeBranch = useCallback(
|
|
67
|
+
(branch) => {
|
|
68
|
+
if (!privacyMode) return branch;
|
|
69
|
+
const existing = branchNameMapRef.current.get(branch);
|
|
70
|
+
if (existing) return existing;
|
|
71
|
+
const anonymized = `branch-${nextBranchIndexRef.current}`;
|
|
72
|
+
nextBranchIndexRef.current += 1;
|
|
73
|
+
branchNameMapRef.current.set(branch, anonymized);
|
|
74
|
+
return anonymized;
|
|
75
|
+
},
|
|
76
|
+
[privacyMode]
|
|
77
|
+
);
|
|
56
78
|
return /* @__PURE__ */ jsx(
|
|
57
79
|
PrivacyContext.Provider,
|
|
58
80
|
{
|
|
59
81
|
value: {
|
|
60
82
|
privacyMode,
|
|
61
83
|
togglePrivacyMode,
|
|
62
|
-
anonymizePath:
|
|
63
|
-
anonymizeProjectName
|
|
84
|
+
anonymizePath: anonymizePathFn,
|
|
85
|
+
anonymizeProjectName,
|
|
86
|
+
anonymizeBranch
|
|
64
87
|
},
|
|
65
88
|
children
|
|
66
89
|
}
|
|
@@ -73,7 +96,7 @@ function usePrivacy() {
|
|
|
73
96
|
}
|
|
74
97
|
return ctx;
|
|
75
98
|
}
|
|
76
|
-
const appCss = "/assets/app-
|
|
99
|
+
const appCss = "/assets/app-D7yorIIh.css";
|
|
77
100
|
const queryClient = new QueryClient({
|
|
78
101
|
defaultOptions: {
|
|
79
102
|
queries: {
|
|
@@ -87,13 +110,16 @@ const Route$6 = createRootRoute({
|
|
|
87
110
|
meta: [
|
|
88
111
|
{ charSet: "utf-8" },
|
|
89
112
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
90
|
-
{ title: "Claude Session Dashboard" }
|
|
113
|
+
{ title: "Claude Session Dashboard" },
|
|
114
|
+
{ name: "theme-color", content: "#141413" },
|
|
115
|
+
{ name: "description", content: "Local observability dashboard for Claude Code sessions" }
|
|
91
116
|
],
|
|
92
117
|
links: [
|
|
93
118
|
{
|
|
94
119
|
rel: "stylesheet",
|
|
95
120
|
href: appCss
|
|
96
|
-
}
|
|
121
|
+
},
|
|
122
|
+
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }
|
|
97
123
|
]
|
|
98
124
|
}),
|
|
99
125
|
component: RootComponent
|
|
@@ -110,7 +136,7 @@ function RootDocument({ children }) {
|
|
|
110
136
|
] })
|
|
111
137
|
] });
|
|
112
138
|
}
|
|
113
|
-
const $$splitComponentImporter$4 = () => import("./_dashboard
|
|
139
|
+
const $$splitComponentImporter$4 = () => import("./_dashboard-TUzgwLqB.js");
|
|
114
140
|
const Route$5 = createFileRoute("/_dashboard")({
|
|
115
141
|
component: lazyRouteComponent($$splitComponentImporter$4, "component")
|
|
116
142
|
});
|
|
@@ -119,15 +145,19 @@ const Route$4 = createFileRoute("/")({
|
|
|
119
145
|
throw redirect({ to: "/sessions" });
|
|
120
146
|
}
|
|
121
147
|
});
|
|
122
|
-
const $$splitComponentImporter$3 = () => import("./stats-
|
|
148
|
+
const $$splitComponentImporter$3 = () => import("./stats-BtgVene-.js");
|
|
149
|
+
const statsSearchSchema = z.object({
|
|
150
|
+
tab: z.enum(["overview", "projects"]).default("overview").catch("overview")
|
|
151
|
+
});
|
|
123
152
|
const Route$3 = createFileRoute("/_dashboard/stats")({
|
|
153
|
+
validateSearch: statsSearchSchema,
|
|
124
154
|
component: lazyRouteComponent($$splitComponentImporter$3, "component")
|
|
125
155
|
});
|
|
126
|
-
const $$splitComponentImporter$2 = () => import("./settings-
|
|
156
|
+
const $$splitComponentImporter$2 = () => import("./settings-C0_KyVQQ.js");
|
|
127
157
|
const Route$2 = createFileRoute("/_dashboard/settings")({
|
|
128
158
|
component: lazyRouteComponent($$splitComponentImporter$2, "component")
|
|
129
159
|
});
|
|
130
|
-
const $$splitComponentImporter$1 = () => import("./index-
|
|
160
|
+
const $$splitComponentImporter$1 = () => import("./index-CKfH7HpA.js");
|
|
131
161
|
const sessionsSearchSchema = z.object({
|
|
132
162
|
page: z.number().int().min(1).default(1).catch(1),
|
|
133
163
|
pageSize: z.number().int().min(5).max(100).default(5).catch(5),
|
|
@@ -139,7 +169,7 @@ const Route$1 = createFileRoute("/_dashboard/sessions/")({
|
|
|
139
169
|
validateSearch: sessionsSearchSchema,
|
|
140
170
|
component: lazyRouteComponent($$splitComponentImporter$1, "component")
|
|
141
171
|
});
|
|
142
|
-
const $$splitComponentImporter = () => import("./_sessionId-
|
|
172
|
+
const $$splitComponentImporter = () => import("./_sessionId-C-XZIPqn.js");
|
|
143
173
|
const searchSchema = z.object({
|
|
144
174
|
project: z.string().optional()
|
|
145
175
|
});
|
|
@@ -202,8 +232,9 @@ const router = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
|
|
|
202
232
|
getRouter
|
|
203
233
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
204
234
|
export {
|
|
205
|
-
Route$
|
|
206
|
-
Route as a,
|
|
235
|
+
Route$3 as R,
|
|
236
|
+
Route$1 as a,
|
|
237
|
+
Route as b,
|
|
207
238
|
router as r,
|
|
208
239
|
usePrivacy as u
|
|
209
240
|
};
|
package/dist/server/assets/{session-detail.server-azkRfON2.js → session-detail.server-DLXl-Pn-.js}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import { e as extractProjectName,
|
|
4
|
+
import { e as extractProjectName, a as getProjectsDir, d as decodeProjectDirName } from "./claude-path-BdwflgZ1.js";
|
|
5
5
|
import { p as parseDetail } from "./session-parser-CAEXxF1D.js";
|
|
6
6
|
import { c as createServerFn } from "../server.js";
|
|
7
7
|
import "node:os";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { a as getProjectsDir, d as decodeProjectDirName, e as extractProjectName, b as extractSessionId } from "./claude-path-BdwflgZ1.js";
|
|
4
|
+
import { a as parseSummary } from "./session-parser-CAEXxF1D.js";
|
|
5
|
+
async function scanProjects() {
|
|
6
|
+
const projectsDir = getProjectsDir();
|
|
7
|
+
let entries;
|
|
8
|
+
try {
|
|
9
|
+
entries = await fs.promises.readdir(projectsDir);
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const projects = [];
|
|
14
|
+
for (const dirName of entries) {
|
|
15
|
+
const dirPath = path.join(projectsDir, dirName);
|
|
16
|
+
const stat = await fs.promises.stat(dirPath).catch(() => null);
|
|
17
|
+
if (!stat?.isDirectory()) continue;
|
|
18
|
+
const files = await fs.promises.readdir(dirPath).catch(() => []);
|
|
19
|
+
const sessionFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
20
|
+
if (sessionFiles.length === 0) continue;
|
|
21
|
+
const decodedPath = decodeProjectDirName(dirName);
|
|
22
|
+
projects.push({
|
|
23
|
+
dirName,
|
|
24
|
+
decodedPath,
|
|
25
|
+
projectName: extractProjectName(decodedPath),
|
|
26
|
+
sessionFiles
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return projects;
|
|
30
|
+
}
|
|
31
|
+
const ACTIVE_THRESHOLD_MS = 12e4;
|
|
32
|
+
async function isSessionActive(projectDirName, sessionId) {
|
|
33
|
+
const projectsDir = getProjectsDir();
|
|
34
|
+
const jsonlPath = path.join(projectsDir, projectDirName, `${sessionId}.jsonl`);
|
|
35
|
+
const lockDirPath = path.join(projectsDir, projectDirName, sessionId);
|
|
36
|
+
const stat = await fs.promises.stat(jsonlPath).catch(() => null);
|
|
37
|
+
if (!stat) return false;
|
|
38
|
+
const age = Date.now() - stat.mtimeMs;
|
|
39
|
+
if (age > ACTIVE_THRESHOLD_MS) return false;
|
|
40
|
+
const lockStat = await fs.promises.stat(lockDirPath).catch(() => null);
|
|
41
|
+
return lockStat?.isDirectory() ?? false;
|
|
42
|
+
}
|
|
43
|
+
const summaryCache = /* @__PURE__ */ new Map();
|
|
44
|
+
async function scanAllSessions() {
|
|
45
|
+
const projects = await scanProjects();
|
|
46
|
+
const summaries = [];
|
|
47
|
+
for (const project of projects) {
|
|
48
|
+
for (const file of project.sessionFiles) {
|
|
49
|
+
const sessionId = extractSessionId(file);
|
|
50
|
+
const filePath = path.join(
|
|
51
|
+
getProjectsDir(),
|
|
52
|
+
project.dirName,
|
|
53
|
+
file
|
|
54
|
+
);
|
|
55
|
+
const stat = await fs.promises.stat(filePath).catch(() => null);
|
|
56
|
+
if (!stat) continue;
|
|
57
|
+
const cached = summaryCache.get(sessionId);
|
|
58
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
59
|
+
const active = await isSessionActive(project.dirName, sessionId);
|
|
60
|
+
summaries.push({ ...cached.summary, isActive: active });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const summary = await parseSummary(
|
|
64
|
+
filePath,
|
|
65
|
+
sessionId,
|
|
66
|
+
project.decodedPath,
|
|
67
|
+
project.projectName,
|
|
68
|
+
stat.size
|
|
69
|
+
);
|
|
70
|
+
if (summary) {
|
|
71
|
+
const active = await isSessionActive(project.dirName, sessionId);
|
|
72
|
+
summary.isActive = active;
|
|
73
|
+
summaryCache.set(sessionId, {
|
|
74
|
+
mtimeMs: stat.mtimeMs,
|
|
75
|
+
summary
|
|
76
|
+
});
|
|
77
|
+
summaries.push(summary);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
summaries.sort(
|
|
82
|
+
(a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime()
|
|
83
|
+
);
|
|
84
|
+
return summaries;
|
|
85
|
+
}
|
|
86
|
+
async function getActiveSessions() {
|
|
87
|
+
const all = await scanAllSessions();
|
|
88
|
+
return all.filter((s) => s.isActive);
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
getActiveSessions as g,
|
|
92
|
+
scanAllSessions as s
|
|
93
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { queryOptions, keepPreviousData } from "@tanstack/react-query";
|
|
2
|
+
import { c as createSsrRpc } from "./createSsrRpc-CVg2UDl0.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { c as createServerFn } from "../server.js";
|
|
5
|
+
const getSessionList = createServerFn({
|
|
6
|
+
method: "GET"
|
|
7
|
+
}).handler(createSsrRpc("bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d"));
|
|
8
|
+
const getActiveSessionList = createServerFn({
|
|
9
|
+
method: "GET"
|
|
10
|
+
}).handler(createSsrRpc("839d29fe93dfa2a6d506af7b48ca25197190a5ff4c796e970ddfdc6e8c98827f"));
|
|
11
|
+
const paginatedSessionsInputSchema = z.object({
|
|
12
|
+
page: z.number().int().min(1),
|
|
13
|
+
pageSize: z.number().int().min(5).max(100),
|
|
14
|
+
search: z.string(),
|
|
15
|
+
status: z.enum(["all", "active", "completed"]),
|
|
16
|
+
project: z.string()
|
|
17
|
+
});
|
|
18
|
+
const getPaginatedSessions = createServerFn({
|
|
19
|
+
method: "GET"
|
|
20
|
+
}).inputValidator((input) => paginatedSessionsInputSchema.parse(input)).handler(createSsrRpc("a3f42f9012fd83586787da8f7cb90649da739dd947d867eb67572f68735ff495"));
|
|
21
|
+
queryOptions({
|
|
22
|
+
queryKey: ["sessions", "list"],
|
|
23
|
+
queryFn: () => getSessionList(),
|
|
24
|
+
refetchInterval: 3e4
|
|
25
|
+
});
|
|
26
|
+
const activeSessionsQuery = queryOptions({
|
|
27
|
+
queryKey: ["sessions", "active"],
|
|
28
|
+
queryFn: () => getActiveSessionList(),
|
|
29
|
+
refetchInterval: 3e3
|
|
30
|
+
});
|
|
31
|
+
function paginatedSessionListQuery(params) {
|
|
32
|
+
return queryOptions({
|
|
33
|
+
queryKey: ["sessions", "paginated", params],
|
|
34
|
+
queryFn: () => getPaginatedSessions({ data: params }),
|
|
35
|
+
placeholderData: keepPreviousData,
|
|
36
|
+
refetchInterval: 3e4
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
activeSessionsQuery as a,
|
|
41
|
+
paginatedSessionListQuery as p
|
|
42
|
+
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { g as getProjectsDir, d as decodeProjectDirName, e as extractProjectName, b as extractSessionId } from "./claude-path-CkuljM34.js";
|
|
6
|
-
import { a as parseSummary } from "./session-parser-CAEXxF1D.js";
|
|
3
|
+
import { s as scanAllSessions, g as getActiveSessions } from "./session-scanner-CLfls9u-.js";
|
|
7
4
|
import { c as createServerFn } from "../server.js";
|
|
5
|
+
import "node:fs";
|
|
6
|
+
import "node:path";
|
|
7
|
+
import "./claude-path-BdwflgZ1.js";
|
|
8
8
|
import "node:os";
|
|
9
|
+
import "./session-parser-CAEXxF1D.js";
|
|
9
10
|
import "node:readline";
|
|
10
11
|
import "@tanstack/history";
|
|
11
12
|
import "@tanstack/router-core/ssr/client";
|
|
@@ -18,91 +19,6 @@ import "seroval";
|
|
|
18
19
|
import "react/jsx-runtime";
|
|
19
20
|
import "@tanstack/react-router/ssr/server";
|
|
20
21
|
import "@tanstack/react-router";
|
|
21
|
-
async function scanProjects() {
|
|
22
|
-
const projectsDir = getProjectsDir();
|
|
23
|
-
let entries;
|
|
24
|
-
try {
|
|
25
|
-
entries = await fs.promises.readdir(projectsDir);
|
|
26
|
-
} catch {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
const projects = [];
|
|
30
|
-
for (const dirName of entries) {
|
|
31
|
-
const dirPath = path.join(projectsDir, dirName);
|
|
32
|
-
const stat = await fs.promises.stat(dirPath).catch(() => null);
|
|
33
|
-
if (!stat?.isDirectory()) continue;
|
|
34
|
-
const files = await fs.promises.readdir(dirPath).catch(() => []);
|
|
35
|
-
const sessionFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
36
|
-
if (sessionFiles.length === 0) continue;
|
|
37
|
-
const decodedPath = decodeProjectDirName(dirName);
|
|
38
|
-
projects.push({
|
|
39
|
-
dirName,
|
|
40
|
-
decodedPath,
|
|
41
|
-
projectName: extractProjectName(decodedPath),
|
|
42
|
-
sessionFiles
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return projects;
|
|
46
|
-
}
|
|
47
|
-
const ACTIVE_THRESHOLD_MS = 12e4;
|
|
48
|
-
async function isSessionActive(projectDirName, sessionId) {
|
|
49
|
-
const projectsDir = getProjectsDir();
|
|
50
|
-
const jsonlPath = path.join(projectsDir, projectDirName, `${sessionId}.jsonl`);
|
|
51
|
-
const lockDirPath = path.join(projectsDir, projectDirName, sessionId);
|
|
52
|
-
const stat = await fs.promises.stat(jsonlPath).catch(() => null);
|
|
53
|
-
if (!stat) return false;
|
|
54
|
-
const age = Date.now() - stat.mtimeMs;
|
|
55
|
-
if (age > ACTIVE_THRESHOLD_MS) return false;
|
|
56
|
-
const lockStat = await fs.promises.stat(lockDirPath).catch(() => null);
|
|
57
|
-
return lockStat?.isDirectory() ?? false;
|
|
58
|
-
}
|
|
59
|
-
const summaryCache = /* @__PURE__ */ new Map();
|
|
60
|
-
async function scanAllSessions() {
|
|
61
|
-
const projects = await scanProjects();
|
|
62
|
-
const summaries = [];
|
|
63
|
-
for (const project of projects) {
|
|
64
|
-
for (const file of project.sessionFiles) {
|
|
65
|
-
const sessionId = extractSessionId(file);
|
|
66
|
-
const filePath = path.join(
|
|
67
|
-
getProjectsDir(),
|
|
68
|
-
project.dirName,
|
|
69
|
-
file
|
|
70
|
-
);
|
|
71
|
-
const stat = await fs.promises.stat(filePath).catch(() => null);
|
|
72
|
-
if (!stat) continue;
|
|
73
|
-
const cached = summaryCache.get(sessionId);
|
|
74
|
-
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
75
|
-
const active = await isSessionActive(project.dirName, sessionId);
|
|
76
|
-
summaries.push({ ...cached.summary, isActive: active });
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const summary = await parseSummary(
|
|
80
|
-
filePath,
|
|
81
|
-
sessionId,
|
|
82
|
-
project.decodedPath,
|
|
83
|
-
project.projectName,
|
|
84
|
-
stat.size
|
|
85
|
-
);
|
|
86
|
-
if (summary) {
|
|
87
|
-
const active = await isSessionActive(project.dirName, sessionId);
|
|
88
|
-
summary.isActive = active;
|
|
89
|
-
summaryCache.set(sessionId, {
|
|
90
|
-
mtimeMs: stat.mtimeMs,
|
|
91
|
-
summary
|
|
92
|
-
});
|
|
93
|
-
summaries.push(summary);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
summaries.sort(
|
|
98
|
-
(a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime()
|
|
99
|
-
);
|
|
100
|
-
return summaries;
|
|
101
|
-
}
|
|
102
|
-
async function getActiveSessions() {
|
|
103
|
-
const all = await scanAllSessions();
|
|
104
|
-
return all.filter((s) => s.isActive);
|
|
105
|
-
}
|
|
106
22
|
const getSessionList_createServerFn_handler = createServerRpc({
|
|
107
23
|
id: "bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d",
|
|
108
24
|
name: "getSessionList",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState
|
|
2
|
+
import { useState } from "react";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { s as settingsQuery, u as useSettingsMutation } from "./settings.queries-DSQd324O.js";
|
|
5
5
|
import { a as SUBSCRIPTION_TIERS, b as DEFAULT_PRICING, D as DEFAULT_SETTINGS } from "./settings.types-DntadCHo.js";
|
|
6
|
+
import { u as usePrivacy } from "./router-Cb_hBXHI.js";
|
|
6
7
|
import "./createSsrRpc-CVg2UDl0.js";
|
|
7
8
|
import "../server.js";
|
|
8
9
|
import "@tanstack/history";
|
|
@@ -24,7 +25,7 @@ function TierSelector({ value, onChange }) {
|
|
|
24
25
|
{
|
|
25
26
|
type: "button",
|
|
26
27
|
onClick: () => onChange(tier.id),
|
|
27
|
-
className: `rounded-lg border px-3 py-2 text-left transition-colors ${isSelected ? "border-
|
|
28
|
+
className: `rounded-lg border px-3 py-2 text-left transition-colors ${isSelected ? "border-brand-500 bg-brand-500/10 text-white" : "border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-700 hover:text-gray-300"}`,
|
|
28
29
|
children: [
|
|
29
30
|
/* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: tier.displayName }),
|
|
30
31
|
/* @__PURE__ */ jsx("div", { className: "mt-0.5 font-mono text-[10px] text-gray-500", children: tier.monthlyUSD !== null ? `$${tier.monthlyUSD}/mo` : "Custom" })
|
|
@@ -92,28 +93,31 @@ function PricingTableEditor({ overrides, onChange }) {
|
|
|
92
93
|
min: "0",
|
|
93
94
|
value,
|
|
94
95
|
onChange: (e) => handleCellChange(model.modelId, f.key, e.target.value),
|
|
95
|
-
className: `w-20 rounded border px-2 py-1 text-right font-mono text-xs ${changed ? "border-
|
|
96
|
+
className: `w-20 rounded border px-2 py-1 text-right font-mono text-xs ${changed ? "border-brand-500/50 bg-brand-500/10 text-brand-400" : "border-gray-700 bg-gray-800 text-gray-300"} focus:border-brand-500 focus:outline-none`
|
|
96
97
|
}
|
|
97
98
|
) }, f.key);
|
|
98
99
|
})
|
|
99
100
|
] }, model.modelId)) })
|
|
100
101
|
] }),
|
|
101
|
-
/* @__PURE__ */ jsx("p", { className: "mt-2 text-[10px] text-gray-600", children: "Prices in USD per million tokens. Overridden values
|
|
102
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-[10px] text-gray-600", children: "Prices in USD per million tokens. Overridden values are highlighted." })
|
|
102
103
|
] });
|
|
103
104
|
}
|
|
104
105
|
function SettingsPage() {
|
|
105
106
|
const { data: settings, isLoading } = useQuery(settingsQuery);
|
|
107
|
+
if (isLoading || !settings) {
|
|
108
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
109
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-gray-800" }),
|
|
110
|
+
/* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
|
|
111
|
+
] });
|
|
112
|
+
}
|
|
113
|
+
return /* @__PURE__ */ jsx(SettingsForm, { settings });
|
|
114
|
+
}
|
|
115
|
+
function SettingsForm({ settings }) {
|
|
106
116
|
const mutation = useSettingsMutation();
|
|
107
|
-
const
|
|
108
|
-
const [
|
|
117
|
+
const { privacyMode, togglePrivacyMode } = usePrivacy();
|
|
118
|
+
const [tier, setTier] = useState(settings.subscriptionTier);
|
|
119
|
+
const [overrides, setOverrides] = useState(settings.pricingOverrides);
|
|
109
120
|
const [isDirty, setIsDirty] = useState(false);
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
if (settings) {
|
|
112
|
-
setTier(settings.subscriptionTier);
|
|
113
|
-
setOverrides(settings.pricingOverrides);
|
|
114
|
-
setIsDirty(false);
|
|
115
|
-
}
|
|
116
|
-
}, [settings]);
|
|
117
121
|
function handleTierChange(newTier) {
|
|
118
122
|
setTier(newTier);
|
|
119
123
|
setIsDirty(true);
|
|
@@ -139,15 +143,57 @@ function SettingsPage() {
|
|
|
139
143
|
}
|
|
140
144
|
});
|
|
141
145
|
}
|
|
142
|
-
if (isLoading) {
|
|
143
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
144
|
-
/* @__PURE__ */ jsx("div", { className: "h-8 w-48 animate-pulse rounded bg-gray-800" }),
|
|
145
|
-
/* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" })
|
|
146
|
-
] });
|
|
147
|
-
}
|
|
148
146
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
149
147
|
/* @__PURE__ */ jsx("h1", { className: "text-xl font-bold text-white", children: "Settings" }),
|
|
150
148
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Configure your subscription tier and API pricing for cost estimation." }),
|
|
149
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
150
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-gray-300", children: "Privacy Mode" }),
|
|
151
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-gray-500", children: "Hide project names, file paths, and branch names across the dashboard. Useful when screen-sharing or recording demos." }),
|
|
152
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
153
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
154
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-300", children: "Enable privacy mode" }),
|
|
155
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
156
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: privacyMode ? "On" : "Off" }),
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
"button",
|
|
159
|
+
{
|
|
160
|
+
type: "button",
|
|
161
|
+
role: "switch",
|
|
162
|
+
"aria-checked": privacyMode,
|
|
163
|
+
onClick: togglePrivacyMode,
|
|
164
|
+
className: `relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full transition-colors ${privacyMode ? "bg-brand-600" : "bg-gray-800"}`,
|
|
165
|
+
children: /* @__PURE__ */ jsx(
|
|
166
|
+
"span",
|
|
167
|
+
{
|
|
168
|
+
className: `inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform ${privacyMode ? "translate-x-[18px]" : "translate-x-[3px]"}`
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
] })
|
|
174
|
+
] }),
|
|
175
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-gray-800 pt-3", children: [
|
|
176
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-gray-400", children: "What gets hidden:" }),
|
|
177
|
+
/* @__PURE__ */ jsxs("ul", { className: "mt-1.5 space-y-1 text-[10px] text-gray-500", children: [
|
|
178
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
179
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Project names" }),
|
|
180
|
+
" ",
|
|
181
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-600", children: "→ project-1, project-2, ..." })
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
184
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "File paths" }),
|
|
185
|
+
" ",
|
|
186
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-600", children: "→ .../project-1" })
|
|
187
|
+
] }),
|
|
188
|
+
/* @__PURE__ */ jsxs("li", { children: [
|
|
189
|
+
/* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "Branch names" }),
|
|
190
|
+
" ",
|
|
191
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-gray-600", children: "→ branch-1, branch-2, ..." })
|
|
192
|
+
] })
|
|
193
|
+
] })
|
|
194
|
+
] })
|
|
195
|
+
] })
|
|
196
|
+
] }),
|
|
151
197
|
/* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
|
|
152
198
|
/* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-gray-300", children: "Subscription Tier" }),
|
|
153
199
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-[10px] text-gray-500", children: "Select your Claude subscription plan. This is informational only and does not affect cost calculations." }),
|
|
@@ -186,7 +232,7 @@ function SettingsPage() {
|
|
|
186
232
|
type: "button",
|
|
187
233
|
onClick: handleSave,
|
|
188
234
|
disabled: !isDirty || mutation.isPending,
|
|
189
|
-
className: `rounded-lg px-4 py-1.5 text-xs font-medium transition-colors ${isDirty && !mutation.isPending ? "bg-
|
|
235
|
+
className: `rounded-lg px-4 py-1.5 text-xs font-medium transition-colors ${isDirty && !mutation.isPending ? "bg-brand-600 text-white hover:bg-brand-500" : "cursor-not-allowed bg-gray-800 text-gray-500"}`,
|
|
190
236
|
children: mutation.isPending ? "Saving..." : "Save"
|
|
191
237
|
}
|
|
192
238
|
)
|