fleetlens 0.9.0 → 0.10.1

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 (167) hide show
  1. package/app/apps/web/.next/BUILD_ID +1 -1
  2. package/app/apps/web/.next/build-manifest.json +3 -3
  3. package/app/apps/web/.next/prerender-manifest.json +3 -3
  4. package/app/apps/web/.next/server/app/_global-error.html +1 -1
  5. package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
  6. package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/app/apps/web/.next/server/app/_not-found/page.js +2 -2
  12. package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/app/apps/web/.next/server/app/api/ask/route.js +4 -3
  15. package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
  16. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +2 -3
  17. package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
  18. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +2 -3
  19. package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
  20. package/app/apps/web/.next/server/app/api/digest/month-index/route.js +3 -3
  21. package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
  22. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +2 -3
  23. package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
  24. package/app/apps/web/.next/server/app/api/digest/week-index/route.js +3 -3
  25. package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
  26. package/app/apps/web/.next/server/app/api/events/route.js +2 -3
  27. package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
  28. package/app/apps/web/.next/server/app/api/jobs/route.js +1 -1
  29. package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
  30. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js +1 -1
  31. package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
  32. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js +1 -1
  33. package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
  34. package/app/apps/web/.next/server/app/api/runs/route.js +1 -1
  35. package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
  36. package/app/apps/web/.next/server/app/api/settings/route.js +3 -4
  37. package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
  38. package/app/apps/web/.next/server/app/changelog/page.js +2 -2
  39. package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
  40. package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  41. package/app/apps/web/.next/server/app/day/[date]/page.js +2 -2
  42. package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
  43. package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
  44. package/app/apps/web/.next/server/app/day/page.js +2 -2
  45. package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
  46. package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
  47. package/app/apps/web/.next/server/app/digest/[date]/page.js +2 -2
  48. package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
  49. package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
  50. package/app/apps/web/.next/server/app/digest/page.js +2 -2
  51. package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
  52. package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
  53. package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -2
  54. package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
  55. package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
  56. package/app/apps/web/.next/server/app/insights/page.js +2 -2
  57. package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
  58. package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
  59. package/app/apps/web/.next/server/app/page.js +2 -2
  60. package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
  61. package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/app/apps/web/.next/server/app/parallelism/page.js +2 -2
  63. package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
  64. package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
  65. package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -2
  66. package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
  67. package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
  68. package/app/apps/web/.next/server/app/projects/page.js +2 -2
  69. package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
  70. package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  71. package/app/apps/web/.next/server/app/runs/page.js +2 -2
  72. package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
  73. package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
  74. package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -2
  75. package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  76. package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  77. package/app/apps/web/.next/server/app/sessions/page.js +2 -2
  78. package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
  79. package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  80. package/app/apps/web/.next/server/app/settings/page.js +2 -2
  81. package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
  82. package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  83. package/app/apps/web/.next/server/app/usage/page.js +2 -2
  84. package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
  85. package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  86. package/app/apps/web/.next/server/chunks/[externals]__09v47rg._.js +3 -0
  87. package/app/apps/web/.next/server/chunks/{[externals]__0fpbr8-._.js → [externals]__0o43ejh._.js} +2 -2
  88. package/app/apps/web/.next/server/chunks/[root-of-the-server]__00n3f9y._.js +1 -1
  89. package/app/apps/web/.next/server/chunks/[root-of-the-server]__02px2mk._.js +3 -0
  90. package/app/apps/web/.next/server/chunks/[root-of-the-server]__02zukjw._.js +1 -1
  91. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +5 -0
  92. package/app/apps/web/.next/server/chunks/[root-of-the-server]__067~im1._.js +2 -2
  93. package/app/apps/web/.next/server/chunks/[root-of-the-server]__06b4cj-._.js +1 -1
  94. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0e8n_x4._.js → [root-of-the-server]__08agijy._.js} +2 -2
  95. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_8zh-i._.js +3 -3
  96. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ej_p3s._.js +2 -2
  97. package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0-vgrh1._.js → [root-of-the-server]__0i06ito._.js} +2 -2
  98. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +2 -2
  99. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0vm_41a._.js +3 -0
  100. package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +4 -4
  101. package/app/apps/web/.next/server/chunks/packages_0c-sz9n._.js +3 -0
  102. package/app/apps/web/.next/server/chunks/packages_0wywuu.._.js +3 -0
  103. package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +27 -13
  104. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0d~2~le._.js +1 -1
  105. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
  106. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0s4dg1y._.js +3 -0
  107. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +1 -1
  108. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +3 -3
  109. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv0m0q._.js +3 -0
  110. package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +1 -1
  111. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  112. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +1 -1
  113. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +2 -2
  114. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_usage-charts-dashboard_tsx_0bgr_uv._.js +1 -1
  115. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0m67-xi._.js +1 -1
  116. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  117. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_12.endt._.js +3 -0
  118. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  119. package/app/apps/web/.next/server/pages/500.html +1 -1
  120. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  121. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  122. package/app/apps/web/.next/static/chunks/04a36uhu~p_z-.css +1 -0
  123. package/app/apps/web/.next/static/chunks/{0q2vd~aj.1hpz.js → 0g8ujtb0lp._9.js} +3 -3
  124. package/app/apps/web/.next/static/chunks/0l3ck.awv~4t7.js +1 -0
  125. package/app/apps/web/.next/static/chunks/{171za22fv684..js → 0lxuac2v0_gdl.js} +2 -2
  126. package/app/apps/web/.next/static/chunks/0n0n.0279ot6c.js +1 -0
  127. package/app/apps/web/.next/static/chunks/0uev~g~qvbjv9.js +4 -0
  128. package/app/apps/web/.next/static/chunks/0~n2a9f52wju9.js +14 -0
  129. package/app/apps/web/app/api/ask/route.ts +124 -111
  130. package/app/apps/web/app/api/events/route.ts +3 -4
  131. package/app/apps/web/app/api/runs/[runId]/route.ts +2 -2
  132. package/app/apps/web/app/api/runs/[runId]/stream/route.ts +3 -2
  133. package/app/apps/web/app/api/runs/route.ts +8 -4
  134. package/app/apps/web/app/sessions/[id]/session-view.tsx +31 -0
  135. package/app/apps/web/components/job-queue-widget.tsx +11 -8
  136. package/app/apps/web/components/previous-cycles-trend.tsx +26 -11
  137. package/app/apps/web/components/usage-chart.tsx +35 -37
  138. package/app/apps/web/components/usage-gauges.tsx +18 -12
  139. package/app/apps/web/components/usage-sidebar.tsx +23 -12
  140. package/app/apps/web/lib/calibration-data.ts +2 -4
  141. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  142. package/app/apps/web/lib/entries-index.ts +2 -2
  143. package/app/apps/web/lib/jobs.ts +3 -5
  144. package/app/apps/web/lib/usage-data.ts +3 -4
  145. package/app/apps/web/lib/utilization-tone.ts +74 -0
  146. package/app/apps/web/package.json +1 -1
  147. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  148. package/dist/daemon-worker.js +609 -110
  149. package/dist/index.js +5312 -4802
  150. package/package.json +1 -1
  151. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.z48ue._.js +0 -3
  152. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0dctov1._.js +0 -5
  153. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +0 -3
  154. package/app/apps/web/.next/server/chunks/packages_09hmrqx._.js +0 -3
  155. package/app/apps/web/.next/server/chunks/packages_118y1lp._.js +0 -3
  156. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +0 -3
  157. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +0 -3
  158. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0ytdqxt._.js +0 -3
  159. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +0 -3
  160. package/app/apps/web/.next/static/chunks/05m.oz3kmf1p7.js +0 -1
  161. package/app/apps/web/.next/static/chunks/07-fq4im3pmjp.js +0 -4
  162. package/app/apps/web/.next/static/chunks/0cxyt4-24..b7.js +0 -14
  163. package/app/apps/web/.next/static/chunks/0o9ayt8sknubd.js +0 -1
  164. package/app/apps/web/.next/static/chunks/0wfbj5zr~az.z.css +0 -1
  165. /package/app/apps/web/.next/static/{u6-8f8u1tspYCacGAz53R → sxcMbKfZ5XgVXYN4QHOHd}/_buildManifest.js +0 -0
  166. /package/app/apps/web/.next/static/{u6-8f8u1tspYCacGAz53R → sxcMbKfZ5XgVXYN4QHOHd}/_clientMiddlewareManifest.js +0 -0
  167. /package/app/apps/web/.next/static/{u6-8f8u1tspYCacGAz53R → sxcMbKfZ5XgVXYN4QHOHd}/_ssgManifest.js +0 -0
@@ -18,6 +18,12 @@
18
18
  import { getSession } from "@claude-lens/parser/fs";
19
19
  import { summarizeSessionForAI } from "@/lib/ai/session-summary";
20
20
  import { spawn } from "node:child_process";
21
+ import {
22
+ runClaudeUnderTmux,
23
+ tmuxRunnerAvailable,
24
+ TmuxRunError,
25
+ TmuxUnavailableError,
26
+ } from "@claude-lens/entries/node";
21
27
 
22
28
  export const dynamic = "force-dynamic";
23
29
  export const runtime = "nodejs";
@@ -56,6 +62,11 @@ export async function POST(request: Request) {
56
62
  const fullPrompt = `Here is the session transcript summary:\n\n${context}\n\n---\n\nUser's question: ${question}`;
57
63
 
58
64
  const encoder = new TextEncoder();
65
+ // Resolve at request time so an admin toggling tmux on/off doesn't
66
+ // require a server restart.
67
+ const useTmux =
68
+ process.env.FLEETLENS_FORCE_PRINT_MODE !== "1" && tmuxRunnerAvailable().ok;
69
+
59
70
  const stream = new ReadableStream({
60
71
  start(controller) {
61
72
  let closed = false;
@@ -69,127 +80,129 @@ export async function POST(request: Request) {
69
80
  }
70
81
  }
71
82
 
72
- // Resolve the claude binary — prefer the well-known location if it
73
- // exists, otherwise fall back to PATH resolution. The user must have
74
- // Claude Code installed and logged in.
75
- const claudeBin = "claude";
76
-
77
- const args = [
78
- "-p", // print mode (non-interactive)
79
- "--output-format",
80
- "stream-json",
81
- "--verbose",
82
- "--model",
83
- model,
84
- "--tools",
85
- "", // no tools — pure analysis
86
- "--disable-slash-commands",
87
- "--no-session-persistence",
88
- "--setting-sources",
89
- "", // skip user settings for speed
90
- "--append-system-prompt",
91
- SYSTEM_PROMPT,
92
- ];
93
-
94
- const proc = spawn(claudeBin, args, {
95
- stdio: ["pipe", "pipe", "pipe"],
96
- // Inherit PATH so the claude binary is found.
97
- env: { ...process.env },
98
- });
99
-
100
- // Write the prompt to stdin and close.
101
- proc.stdin.write(fullPrompt);
102
- proc.stdin.end();
103
-
104
- let stderr = "";
105
-
106
- proc.stdout.on("data", (chunk: Buffer) => {
107
- const text = chunk.toString("utf8");
108
- // stream-json emits one JSON object per line.
109
- for (const line of text.split("\n")) {
110
- const trimmed = line.trim();
111
- if (!trimmed) continue;
83
+ function finish() {
84
+ if (!closed) {
85
+ try { controller.close(); } catch {}
86
+ closed = true;
87
+ }
88
+ }
89
+
90
+ // Kick off the LLM work without awaiting — Next.js waits for start()
91
+ // to return before opening the response, so any blocking here would
92
+ // stall the SSE headers.
93
+ void (async () => {
94
+ if (useTmux) {
112
95
  try {
113
- const obj = JSON.parse(trimmed) as Record<string, unknown>;
114
-
115
- // We care about assistant message content blocks of type "text".
116
- if (obj.type === "assistant") {
117
- const msg = obj.message as Record<string, unknown> | undefined;
118
- const content = msg?.content as Array<Record<string, unknown>> | undefined;
119
- if (Array.isArray(content)) {
120
- for (const block of content) {
121
- if (block.type === "text" && typeof block.text === "string") {
122
- send({ type: "delta", text: block.text });
96
+ const res = await runClaudeUnderTmux({
97
+ systemPrompt: SYSTEM_PROMPT,
98
+ model,
99
+ userPrompt: fullPrompt,
100
+ onDelta: (chunk) => send({ type: "delta", text: chunk }),
101
+ signal: request.signal,
102
+ });
103
+ send({ type: "done", totalTokens: res.input_tokens + res.output_tokens });
104
+ finish();
105
+ return;
106
+ } catch (err) {
107
+ const isExpected = err instanceof TmuxUnavailableError || err instanceof TmuxRunError;
108
+ if (!isExpected) {
109
+ console.warn("[ask] tmux runner failed, falling back to -p:", err);
110
+ }
111
+ // fall through to print mode
112
+ }
113
+ }
114
+ runPrintModeFallback();
115
+ })();
116
+
117
+ function runPrintModeFallback() {
118
+ const claudeBin = "claude";
119
+
120
+ const args = [
121
+ "-p",
122
+ "--output-format",
123
+ "stream-json",
124
+ "--verbose",
125
+ "--model",
126
+ model,
127
+ "--tools",
128
+ "",
129
+ "--disable-slash-commands",
130
+ "--no-session-persistence",
131
+ "--setting-sources",
132
+ "",
133
+ "--append-system-prompt",
134
+ SYSTEM_PROMPT,
135
+ ];
136
+
137
+ const proc = spawn(claudeBin, args, {
138
+ stdio: ["pipe", "pipe", "pipe"],
139
+ env: { ...process.env },
140
+ });
141
+
142
+ proc.stdin.write(fullPrompt);
143
+ proc.stdin.end();
144
+
145
+ let stderr = "";
146
+
147
+ proc.stdout.on("data", (chunk: Buffer) => {
148
+ const text = chunk.toString("utf8");
149
+ for (const line of text.split("\n")) {
150
+ const trimmed = line.trim();
151
+ if (!trimmed) continue;
152
+ try {
153
+ const obj = JSON.parse(trimmed) as Record<string, unknown>;
154
+ if (obj.type === "assistant") {
155
+ const msg = obj.message as Record<string, unknown> | undefined;
156
+ const content = msg?.content as Array<Record<string, unknown>> | undefined;
157
+ if (Array.isArray(content)) {
158
+ for (const block of content) {
159
+ if (block.type === "text" && typeof block.text === "string") {
160
+ send({ type: "delta", text: block.text });
161
+ }
123
162
  }
124
163
  }
125
164
  }
165
+ if (obj.type === "result") {
166
+ const usage = obj.usage as Record<string, unknown> | undefined;
167
+ const totalTokens =
168
+ typeof usage?.input_tokens === "number" &&
169
+ typeof usage?.output_tokens === "number"
170
+ ? usage.input_tokens + usage.output_tokens
171
+ : undefined;
172
+ send({ type: "done", totalTokens: totalTokens ?? undefined });
173
+ }
174
+ } catch {
175
+ // Skip non-JSON lines (verbose debug output, etc.)
126
176
  }
127
-
128
- // Result event — extract total tokens for display.
129
- if (obj.type === "result") {
130
- const usage = obj.usage as Record<string, unknown> | undefined;
131
- const totalTokens =
132
- typeof usage?.input_tokens === "number" &&
133
- typeof usage?.output_tokens === "number"
134
- ? usage.input_tokens + usage.output_tokens
135
- : undefined;
136
- send({ type: "done", totalTokens: totalTokens ?? undefined });
137
- }
138
- } catch {
139
- // Skip non-JSON lines (verbose debug output, etc.)
140
177
  }
141
- }
142
- });
143
-
144
- proc.stderr.on("data", (chunk: Buffer) => {
145
- stderr += chunk.toString("utf8");
146
- });
147
-
148
- proc.on("close", (code) => {
149
- if (code !== 0 && !closed) {
150
- send({
151
- type: "error",
152
- message: stderr.trim().slice(0, 300) || `claude exited with code ${code}`,
153
- });
154
- }
155
- if (!closed) {
156
- try {
157
- controller.close();
158
- } catch {
159
- // already closed
178
+ });
179
+
180
+ proc.stderr.on("data", (chunk: Buffer) => {
181
+ stderr += chunk.toString("utf8");
182
+ });
183
+
184
+ proc.on("close", (code) => {
185
+ if (code !== 0 && !closed) {
186
+ send({
187
+ type: "error",
188
+ message: stderr.trim().slice(0, 300) || `claude exited with code ${code}`,
189
+ });
160
190
  }
161
- closed = true;
162
- }
163
- });
191
+ finish();
192
+ });
164
193
 
165
- proc.on("error", (err) => {
166
- if (!closed) {
167
- send({ type: "error", message: `Failed to spawn claude: ${err.message}` });
168
- try {
169
- controller.close();
170
- } catch {
171
- // already closed
194
+ proc.on("error", (err) => {
195
+ if (!closed) {
196
+ send({ type: "error", message: `Failed to spawn claude: ${err.message}` });
197
+ finish();
172
198
  }
173
- closed = true;
174
- }
175
- });
199
+ });
176
200
 
177
- // If the client aborts, kill the subprocess.
178
- request.signal.addEventListener("abort", () => {
179
- try {
180
- proc.kill("SIGTERM");
181
- } catch {
182
- // ignore
183
- }
184
- if (!closed) {
185
- try {
186
- controller.close();
187
- } catch {
188
- // already closed
189
- }
190
- closed = true;
191
- }
192
- });
201
+ request.signal.addEventListener("abort", () => {
202
+ try { proc.kill("SIGTERM"); } catch { /* ignore */ }
203
+ finish();
204
+ });
205
+ }
193
206
  },
194
207
  });
195
208
 
@@ -14,17 +14,16 @@
14
14
  * changed file so the next RSC render re-parses fresh data.
15
15
  */
16
16
 
17
- import { invalidateFile, DEFAULT_ROOT } from "@claude-lens/parser/fs";
17
+ import { invalidateFile, DEFAULT_ROOT, cclensHome } from "@claude-lens/parser/fs";
18
18
  import { watch, promises as fs, existsSync } from "node:fs";
19
19
  import path from "node:path";
20
- import { homedir } from "node:os";
21
20
 
22
21
  export const dynamic = "force-dynamic";
23
22
  // Edge runtime can't do fs.watch
24
23
  export const runtime = "nodejs";
25
24
 
26
- /** ~/.cclens/usage.jsonl — written by the cclens daemon every 5 minutes. */
27
- const USAGE_LOG_DIR = path.join(homedir(), ".cclens");
25
+ /** cclensHome()/usage.jsonl — written by the cclens daemon every 5 minutes. */
26
+ const USAGE_LOG_DIR = cclensHome();
28
27
  const USAGE_LOG_FILE = "usage.jsonl";
29
28
 
30
29
  type LiveEvent =
@@ -1,11 +1,11 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
3
+ import { cclensPath } from "@claude-lens/parser/fs";
4
4
 
5
5
  export const runtime = "nodejs";
6
6
  export const dynamic = "force-dynamic";
7
7
 
8
- const RUNS_DIR = join(homedir(), ".cclens", "llm-runs");
8
+ const RUNS_DIR = cclensPath("llm-runs");
9
9
 
10
10
  /**
11
11
  * GET /api/runs/<runId> dump the full event trace
@@ -1,11 +1,12 @@
1
1
  import { existsSync, readdirSync, statSync, watch, openSync, readSync, closeSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { cclensPath } from "@claude-lens/parser/fs";
4
5
 
5
6
  export const runtime = "nodejs";
6
7
  export const dynamic = "force-dynamic";
7
8
 
8
- const RUNS_DIR = join(homedir(), ".cclens", "llm-runs");
9
+ const RUNS_DIR = cclensPath("llm-runs");
9
10
 
10
11
  /**
11
12
  * GET /api/runs/<runId>/stream → SSE tail of a per-run trace file
@@ -1,7 +1,7 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { existsSync, readFileSync, readdirSync, statSync, openSync, readSync, closeSync } from "node:fs";
3
- import { homedir } from "node:os";
4
3
  import { join } from "node:path";
4
+ import { cclensPath } from "@claude-lens/parser/fs";
5
5
 
6
6
  export const runtime = "nodejs";
7
7
  export const dynamic = "force-dynamic";
@@ -57,8 +57,12 @@ function listActiveRuns(): ActiveRun[] {
57
57
  }
58
58
  const runs: ActiveRun[] = [];
59
59
  for (const line of out.split("\n")) {
60
- if (!line.includes("claude -p")) continue;
60
+ // Match both `claude -p` (print mode) and tmux-driven interactive
61
+ // (no `-p`). The shared marker is `--append-system-prompt`, which
62
+ // the LLM-runner pipeline always sets — interactive user sessions
63
+ // launched from a terminal don't carry it.
61
64
  if (!line.includes("--append-system-prompt")) continue;
65
+ if (!/\bclaude\b/.test(line)) continue;
62
66
  if (line.includes("grep")) continue;
63
67
  const m = line.trim().match(/^(\d+)\s+(\S+)\s+([\d:.]+)\s+(.+)$/);
64
68
  if (!m) continue;
@@ -74,7 +78,7 @@ function listActiveRuns(): ActiveRun[] {
74
78
  return runs.sort((a, b) => a.elapsed_s - b.elapsed_s);
75
79
  }
76
80
 
77
- const SPEND_PATH = join(homedir(), ".cclens", "llm-spend.jsonl");
81
+ const SPEND_PATH = cclensPath("llm-spend.jsonl");
78
82
 
79
83
  type CompletedRun = {
80
84
  ts: string;
@@ -123,7 +127,7 @@ function parseSince(s: string | null): number {
123
127
  return Date.now() - 24 * 3_600_000;
124
128
  }
125
129
 
126
- const RUNS_DIR = join(homedir(), ".cclens", "llm-runs");
130
+ const RUNS_DIR = cclensPath("llm-runs");
127
131
 
128
132
  type TraceSummary = {
129
133
  run_id: string;
@@ -621,6 +621,12 @@ export function SessionView({
621
621
  </span>
622
622
  <InlineStatDivider />
623
623
  {model && <InlineStat icon={<Cpu size={12} />} value={model} mono />}
624
+ {session.entrypoint && (
625
+ <>
626
+ <InlineStatDivider />
627
+ <EntrypointBadge entrypoint={session.entrypoint} />
628
+ </>
629
+ )}
624
630
  <InlineStatDivider />
625
631
  <InlineStat icon={<Folder size={12} />} value={projectName} truncate />
626
632
  <InlineStatDivider />
@@ -1143,6 +1149,31 @@ function InlineStatDivider() {
1143
1149
  );
1144
1150
  }
1145
1151
 
1152
+ function EntrypointBadge({ entrypoint }: { entrypoint: string }) {
1153
+ // cli + claude-desktop are human-driven; sdk-* are programmatic.
1154
+ const isSdk = entrypoint.startsWith("sdk-");
1155
+ const tone = isSdk
1156
+ ? { bg: "rgba(245, 158, 11, 0.16)", fg: "#b45309" }
1157
+ : { bg: "rgba(16, 185, 129, 0.16)", fg: "#047857" };
1158
+ return (
1159
+ <span
1160
+ title={`entrypoint: ${entrypoint}`}
1161
+ style={{
1162
+ fontSize: 10.5,
1163
+ padding: "2px 7px",
1164
+ borderRadius: 4,
1165
+ background: tone.bg,
1166
+ color: tone.fg,
1167
+ fontWeight: 600,
1168
+ fontFamily: "var(--font-mono)",
1169
+ letterSpacing: 0.2,
1170
+ }}
1171
+ >
1172
+ {entrypoint}
1173
+ </span>
1174
+ );
1175
+ }
1176
+
1146
1177
  function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
1147
1178
  const { ref, anchor, open, close } = useAnchoredTooltip();
1148
1179
  const u = usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
@@ -40,34 +40,37 @@ export function JobQueueWidget() {
40
40
  });
41
41
  };
42
42
 
43
- // Poll /api/jobs. Faster cadence when there's anything active.
43
+ // Poll /api/jobs. Faster cadence when there's anything active. Empty
44
+ // deps + closure-local `hasActive` — listing `jobs` here would re-run
45
+ // the effect on every successful tick (because setJobs changes the
46
+ // ref), collapsing the throttle into a tight 1–4 ms refetch loop.
44
47
  useEffect(() => {
45
48
  let mounted = true;
46
49
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
50
+ let hasActive = false;
47
51
 
48
52
  const tick = async () => {
49
53
  try {
50
54
  const res = await fetch("/api/jobs", { cache: "no-store" });
51
55
  if (!res.ok) return;
52
56
  const data = await res.json() as { jobs: Job[] };
53
- if (mounted) setJobs(data.jobs);
57
+ if (!mounted) return;
58
+ setJobs(data.jobs);
59
+ hasActive = data.jobs.some(j => j.status === "running" || j.status === "queued");
54
60
  } catch { /* ignore */ }
55
61
  };
56
62
 
57
63
  const schedule = () => {
58
64
  if (!mounted) return;
59
- const hasActive = jobs.some(j => j.status === "running" || j.status === "queued");
60
- const delay = hasActive ? 1500 : 6000;
61
- timeoutId = setTimeout(async () => { await tick(); schedule(); }, delay);
65
+ timeoutId = setTimeout(async () => { await tick(); schedule(); }, hasActive ? 1500 : 6000);
62
66
  };
63
67
 
64
- void tick();
65
- schedule();
68
+ void tick().then(schedule);
66
69
  return () => {
67
70
  mounted = false;
68
71
  if (timeoutId) clearTimeout(timeoutId);
69
72
  };
70
- }, [jobs]);
73
+ }, []);
71
74
 
72
75
  const active = jobs.filter(j => j.status === "running" || j.status === "queued");
73
76
  const recent = jobs.filter(j => j.status === "done" || j.status === "error" || j.status === "cancelled").slice(0, 8);
@@ -1,10 +1,19 @@
1
1
  import type { CyclePeak } from "@/lib/calibration-data";
2
+ import {
3
+ paceToneForCycle,
4
+ toneVar,
5
+ utilizationTone,
6
+ } from "@/lib/utilization-tone";
2
7
 
3
- // Horizontal bar chart of the last few cycles' peak utilization. One bar
4
- // per cycle, height proportional to peak %, color follows the burndown
5
- // danger thresholds (green / amber / red). The right-most bar is "current"
6
- // (in-progress) and gets a distinct styling so it isn't read as a finished
7
- // trend point. Designed for direct reuse on the team-edition member card.
8
+ const HOUR = 3_600_000;
9
+ const WINDOW_MS = { "5h": 5 * HOUR, "7d": 7 * 24 * HOUR } as const;
10
+
11
+ // Horizontal bar chart of recent cycle peaks. Color logic differs by
12
+ // cycle state: completed cycles are colored by their final peak (≥70%
13
+ // green = full plan use, <40% red = wasted headroom), in-progress cycles
14
+ // are colored by PACE (utilization vs elapsed cycle fraction) so a 50%
15
+ // peak halfway through a 7-day cycle reads on-pace green, not amber.
16
+ // Right-most bar is the in-progress cycle, dashed border + transparency.
8
17
  export function PreviousCyclesTrend({
9
18
  windowLabel,
10
19
  cycles,
@@ -51,19 +60,25 @@ export function PreviousCyclesTrend({
51
60
  }}
52
61
  >
53
62
  {cycles.map((c, i) => (
54
- <CycleBar key={i} cycle={c} />
63
+ <CycleBar key={i} cycle={c} windowLabel={windowLabel} />
55
64
  ))}
56
65
  </div>
57
66
  </section>
58
67
  );
59
68
  }
60
69
 
61
- function CycleBar({ cycle }: { cycle: CyclePeak }) {
70
+ function CycleBar({
71
+ cycle,
72
+ windowLabel,
73
+ }: {
74
+ cycle: CyclePeak;
75
+ windowLabel: "5h" | "7d";
76
+ }) {
62
77
  const pct = Math.max(0, Math.min(100, cycle.peakPct));
63
- const color =
64
- pct >= 90 ? "var(--af-danger)" :
65
- pct >= 70 ? "#b58400" :
66
- "var(--af-success)";
78
+ const tone = cycle.current
79
+ ? paceToneForCycle(pct, new Date(cycle.endsAt).getTime(), WINDOW_MS[windowLabel])
80
+ : utilizationTone(pct);
81
+ const color = toneVar(tone);
67
82
  const date = new Date(cycle.endsAt);
68
83
  // Compact "Apr 23" date label; "current" cycle gets "ends Apr 30" instead
69
84
  const label = cycle.current
@@ -1,23 +1,24 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useState } from "react";
3
+ import { useEffect, useMemo, useState } from "react";
4
4
  import { Maximize2 } from "lucide-react";
5
5
  import type { UsageSnapshot, UsageWindow } from "@/lib/usage-data";
6
+ import { paceLabel, toneVar } from "@/lib/utilization-tone";
6
7
 
7
8
  type SeriesKey = "five_hour" | "seven_day" | "seven_day_sonnet";
8
9
 
9
10
  /**
10
- * Sprint-burndown-style chart for a single usage window.
11
+ * Burndown-style chart for a single usage window.
11
12
  *
12
- * Y-axis: remaining budget (100% at start, 0% when quota is exhausted)
13
+ * Y-axis: remaining budget (100% at start, 0% when quota exhausts)
13
14
  * X-axis: time from window start to reset
14
15
  *
15
16
  * Ideal line: dashed diagonal from (windowStart, 100%) to (resetsAt, 0%)
16
- * — the "sustainable burn" trajectory
17
- * Actual line: solid, plots (100 - utilization) at each snapshot's captured_at
17
+ * — full-utilization trajectory.
18
18
  *
19
- * ABOVE the ideal line = saving budget (on track)
20
- * BELOW the ideal line = burning faster than sustainable (behind schedule)
19
+ * BELOW the ideal line = burning at or above pace (getting plan value)
20
+ * ABOVE the ideal line = under-burning (paying for unused headroom)
21
+ * Far below ideal = will exhaust before reset (throttling risk)
21
22
  */
22
23
  export function UsageChart({
23
24
  snapshots,
@@ -90,6 +91,14 @@ export function UsageChart({
90
91
  predicted: number | null;
91
92
  } | null>(null);
92
93
 
94
+ // SSR can't agree with the client on `Date.now()` — the server renders
95
+ // at request time, the client hydrates milliseconds-to-minutes later.
96
+ // Both depended on `now`, so the "now" cursor + pace label produced
97
+ // a hydration mismatch warning. Render time-dependent UI only after
98
+ // mount; the SSR fragment matches the client's pre-mount fragment.
99
+ const [mounted, setMounted] = useState(false);
100
+ useEffect(() => setMounted(true), []);
101
+
93
102
  if (!computed) {
94
103
  return (
95
104
  <div
@@ -121,14 +130,9 @@ export function UsageChart({
121
130
 
122
131
  const nowIdeal = idealAt(now);
123
132
  const delta = currentRemaining - nowIdeal;
124
- const toneColor =
125
- delta < -10
126
- ? "var(--af-danger)"
127
- : delta < 0
128
- ? "var(--af-warning)"
129
- : "var(--af-success)";
130
- const toneLabel =
131
- delta < -10 ? "behind schedule" : delta < 0 ? "slightly behind" : "on track";
133
+ const pace = paceLabel(delta);
134
+ const toneColor = toneVar(pace.tone);
135
+ const toneLabel = pace.label;
132
136
 
133
137
  const linePath =
134
138
  points.length > 0
@@ -226,9 +230,16 @@ export function UsageChart({
226
230
  remaining
227
231
  </span>
228
232
  </div>
229
- <div style={{ fontSize: 11, color: toneColor, fontWeight: 500 }}>
230
- {toneLabel} ({delta >= 0 ? "+" : ""}
231
- {delta.toFixed(1)}%)
233
+ <div
234
+ suppressHydrationWarning
235
+ style={{ fontSize: 11, color: toneColor, fontWeight: 500 }}
236
+ >
237
+ {mounted && (
238
+ <>
239
+ {toneLabel} ({delta >= 0 ? "+" : ""}
240
+ {delta.toFixed(1)}pp)
241
+ </>
242
+ )}
232
243
  </div>
233
244
  <div
234
245
  suppressHydrationWarning
@@ -338,23 +349,9 @@ export function UsageChart({
338
349
  {formatAxisDatetime(windowEnd)}
339
350
  </text>
340
351
 
341
- {/* Danger (<10% remaining) and caution (10–30%) bands at the bottom */}
342
- <rect
343
- x={padding.left}
344
- y={yScale(10)}
345
- width={plotW}
346
- height={yScale(0) - yScale(10)}
347
- fill="var(--af-danger)"
348
- fillOpacity="0.07"
349
- />
350
- <rect
351
- x={padding.left}
352
- y={yScale(30)}
353
- width={plotW}
354
- height={yScale(10) - yScale(30)}
355
- fill="var(--af-warning)"
356
- fillOpacity="0.05"
357
- />
352
+ {/* No static danger bands. Low-remaining used to be flagged red,
353
+ but in the "high use = good" framing that band is the goal
354
+ zone. Pace label + ideal line carry the signal. */}
358
355
 
359
356
  {/* Area fill under actual line */}
360
357
  <path d={areaPath} fill={colorVar} fillOpacity="0.1" />
@@ -371,8 +368,9 @@ export function UsageChart({
371
368
  strokeDasharray="4 3"
372
369
  />
373
370
 
374
- {/* Current-time vertical marker */}
375
- {now >= windowStart && now <= windowEnd && (
371
+ {/* Current-time vertical marker — only after mount so the SSR
372
+ and client renders agree (Date.now() differs between them). */}
373
+ {mounted && now >= windowStart && now <= windowEnd && (
376
374
  <g>
377
375
  <line
378
376
  x1={xScale(now)}