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.
Files changed (44) hide show
  1. package/README.md +156 -14
  2. package/dist/client/assets/_dashboard-I7m6D7BE.js +1 -0
  3. package/dist/client/assets/_sessionId-DEliIff6.js +12 -0
  4. package/dist/client/assets/app-D7yorIIh.css +1 -0
  5. package/dist/client/assets/{createServerFn-Le0d8Pjz.js → createServerFn-Bn6_ISOt.js} +1 -1
  6. package/dist/client/assets/format-Bsprb3az.js +1 -0
  7. package/dist/client/assets/index-BkqRvnEf.js +1 -0
  8. package/dist/client/assets/{main-CzD8HjLq.js → main-CfJIADCp.js} +7 -7
  9. package/dist/client/assets/sessions.queries-CrJg4dYU.js +1 -0
  10. package/dist/client/assets/settings-C4_lsEzl.js +1 -0
  11. package/dist/client/assets/{settings.types-B4841OLF.js → settings.types-9Qf5WcRY.js} +1 -1
  12. package/dist/client/assets/stats-_r1gmaTe.js +4 -0
  13. package/dist/client/assets/useSessionCost-DPZ-ubM1.js +65 -0
  14. package/dist/client/favicon.svg +3 -0
  15. package/dist/server/assets/_dashboard-TUzgwLqB.js +112 -0
  16. package/dist/server/assets/{_sessionId-BwZK4Ezz.js → _sessionId-C-XZIPqn.js} +57 -35
  17. package/dist/server/assets/_tanstack-start-manifest_v-B51mSkGz.js +4 -0
  18. package/dist/server/assets/{claude-path-CkuljM34.js → claude-path-BdwflgZ1.js} +9 -3
  19. package/dist/server/assets/{format-CGmJnuhZ.js → format-DIZHV7IJ.js} +3 -3
  20. package/dist/server/assets/{index-D4VWrt2z.js → index-CKfH7HpA.js} +28 -60
  21. package/dist/server/assets/project-analytics.server-BkWSd6a8.js +61 -0
  22. package/dist/server/assets/{router-xTSe9UH_.js → router-Cb_hBXHI.js} +62 -31
  23. package/dist/server/assets/{session-detail.server-azkRfON2.js → session-detail.server-DLXl-Pn-.js} +1 -1
  24. package/dist/server/assets/session-scanner-CLfls9u-.js +93 -0
  25. package/dist/server/assets/sessions.queries-B5ZBiVJy.js +42 -0
  26. package/dist/server/assets/{sessions.server-B8zbmvSM.js → sessions.server-CUhasKW2.js} +5 -89
  27. package/dist/server/assets/{settings-ko61yfVs.js → settings-C0_KyVQQ.js} +66 -20
  28. package/dist/server/assets/stats-BtgVene-.js +886 -0
  29. package/dist/server/assets/{stats.server-BZWxV-mC.js → stats.server-qTOvID9-.js} +62 -3
  30. package/dist/server/assets/useSessionCost-CYs5UOX-.js +209 -0
  31. package/dist/server/server.js +13 -10
  32. package/package.json +11 -1
  33. package/dist/client/assets/_dashboard-CYwTENkn.js +0 -1
  34. package/dist/client/assets/_sessionId-Bwfhm_El.js +0 -12
  35. package/dist/client/assets/app-DhZyFob1.css +0 -1
  36. package/dist/client/assets/format-Bf-cSf6L.js +0 -1
  37. package/dist/client/assets/index-DXhX1hdS.js +0 -1
  38. package/dist/client/assets/settings-BSPc79zZ.js +0 -1
  39. package/dist/client/assets/stats-CDIvpOt9.js +0 -4
  40. package/dist/client/assets/useSessionCost-9NP6uhla.js +0 -61
  41. package/dist/server/assets/_dashboard--ukhquwO.js +0 -97
  42. package/dist/server/assets/_tanstack-start-manifest_v-gtQY7f-T.js +0 -4
  43. package/dist/server/assets/stats-DItsFPp5.js +0 -266
  44. 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, useEffect, useCallback, createContext, useContext } from "react";
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(false);
24
+ const [privacyMode, setPrivacyMode] = useState(readStoredPrivacyMode);
14
25
  const projectNameMapRef = useRef(/* @__PURE__ */ new Map());
15
26
  const nextIndexRef = useRef(1);
16
- useEffect(() => {
17
- try {
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: anonymizePath$1,
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-DhZyFob1.css";
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--ukhquwO.js");
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-DItsFPp5.js");
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-ko61yfVs.js");
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-D4VWrt2z.js");
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-BwZK4Ezz.js");
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$1 as R,
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
  };
@@ -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, g as getProjectsDir, d as decodeProjectDirName } from "./claude-path-CkuljM34.js";
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 * as fs from "node:fs";
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, useEffect } from "react";
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-blue-500 bg-blue-500/10 text-white" : "border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-700 hover:text-gray-300"}`,
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-blue-500/50 bg-blue-500/10 text-blue-400" : "border-gray-700 bg-gray-800 text-gray-300"} focus:border-blue-500 focus:outline-none`
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 shown in blue." })
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 [tier, setTier] = useState("pro");
108
- const [overrides, setOverrides] = useState({});
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-blue-600 text-white hover:bg-blue-500" : "cursor-not-allowed bg-gray-800 text-gray-500"}`,
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
  )