fleetlens 0.10.0 → 0.10.2

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 (154) 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 +3 -4
  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 +3 -4
  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 +3 -4
  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]__0o43ejh._.js +3 -0
  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]__08agijy._.js +3 -0
  95. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_8zh-i._.js +9 -0
  96. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ej_p3s._.js +9 -0
  97. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0i06ito._.js +3 -0
  98. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +9 -0
  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]__0r4tks8._.js +1 -1
  105. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0s4dg1y._.js +1 -1
  106. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +1 -1
  107. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
  108. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv0m0q._.js +3 -0
  109. package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
  110. package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
  111. package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0m67-xi._.js +1 -1
  112. package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
  113. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_12.endt._.js +3 -0
  114. package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
  115. package/app/apps/web/.next/server/pages/500.html +1 -1
  116. package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
  117. package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
  118. package/app/apps/web/.next/static/chunks/063r4x2.9csh8.js +14 -0
  119. package/app/apps/web/.next/static/chunks/{09--oejbzw3q-.js → 0rtba0saz74e1.js} +2 -2
  120. package/app/apps/web/app/api/ask/route.ts +124 -111
  121. package/app/apps/web/app/api/events/route.ts +3 -4
  122. package/app/apps/web/app/api/runs/[runId]/route.ts +2 -2
  123. package/app/apps/web/app/api/runs/[runId]/stream/route.ts +3 -2
  124. package/app/apps/web/app/api/runs/route.ts +8 -4
  125. package/app/apps/web/app/sessions/[id]/session-view.tsx +164 -49
  126. package/app/apps/web/components/job-queue-widget.tsx +11 -8
  127. package/app/apps/web/lib/calibration-data.ts +2 -4
  128. package/app/apps/web/lib/changelog-data.generated.ts +1 -1
  129. package/app/apps/web/lib/entries-index.ts +2 -2
  130. package/app/apps/web/lib/jobs.ts +3 -5
  131. package/app/apps/web/lib/usage-data.ts +3 -4
  132. package/app/apps/web/package.json +1 -1
  133. package/app/apps/web/tsconfig.tsbuildinfo +1 -1
  134. package/dist/daemon-worker.js +481 -81
  135. package/dist/index.js +4373 -3961
  136. package/package.json +1 -1
  137. package/app/apps/web/.next/server/chunks/[root-of-the-server]__07qxzx4._.js +0 -9
  138. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0fu2xfy._.js +0 -5
  139. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gfd42k._.js +0 -9
  140. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0i.m8y0._.js +0 -3
  141. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0iy5aqy._.js +0 -9
  142. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh7-nf._.js +0 -3
  143. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nvdffv._.js +0 -3
  144. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +0 -3
  145. package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s2~c3g._.js +0 -3
  146. package/app/apps/web/.next/server/chunks/packages_09wmnv.._.js +0 -3
  147. package/app/apps/web/.next/server/chunks/packages_0o6.gpk._.js +0 -3
  148. package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +0 -3
  149. package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +0 -3
  150. package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +0 -3
  151. package/app/apps/web/.next/static/chunks/0qcu452dr~edz.js +0 -14
  152. /package/app/apps/web/.next/static/{L0Dy1hOQrZjV9l0TNulvk → XTns3Y6jHWmb7EYKzQi55}/_buildManifest.js +0 -0
  153. /package/app/apps/web/.next/static/{L0Dy1hOQrZjV9l0TNulvk → XTns3Y6jHWmb7EYKzQi55}/_clientMiddlewareManifest.js +0 -0
  154. /package/app/apps/web/.next/static/{L0Dy1hOQrZjV9l0TNulvk → XTns3Y6jHWmb7EYKzQi55}/_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;
@@ -292,34 +292,42 @@ export function SessionView({
292
292
  /** Detect PR creations in this session. */
293
293
  const prMarkers = useMemo(() => detectPrMarkers(session), [session]);
294
294
 
295
- /** Idle bands derived from raw event timestamps, classified per gap.
295
+ /** Idle bands derived from raw event timestamps. Anchors include every
296
+ * timestamped event (agent-thinking and meta are activity too), and
297
+ * each emitted band's `start..end` is a literal time range so the
298
+ * minimap stripe width tracks actual idle duration — no across-gap
299
+ * merging.
296
300
  *
297
- * Two flavors:
298
- * - In-turn idle (gap between agent activities within the same user
299
- * turn) — multiple of these inside one turn collapse into ONE merged
300
- * band whose `start..end` spans the whole period and `durationMs`
301
- * sums the actual idle.
302
- * - Between-turn idle (gap whose terminating anchor IS a user message)
303
- * — emitted standalone. This is "user paused, walked away, came
304
- * back" time and is conceptually distinct from agent thinking.
305
- *
306
- * Anchors only count user / agent / tool-call / tool-result events —
307
- * meta + thinking events are skipped so sub-second event-to-event gaps
308
- * don't crowd the strip.
301
+ * Two exclusions keep "delegated work" and "response latency" out of
302
+ * idle:
303
+ * - Subagent run spans are carved out of every candidate gap. When a
304
+ * parent dispatches an Agent tool call the gap between tool_use and
305
+ * tool_result is the subagent's runtime — not idle. Background
306
+ * runs that overlap a user-away gap likewise get clipped out so
307
+ * only the genuinely-unwatched portion remains. The carve can
308
+ * produce multiple surviving slices per gap; each becomes its own
309
+ * band so the visual extent matches reality.
310
+ * - "Awaiting first response" — the period between a user message
311
+ * and the first agent/tool-call anchor — is dropped wholesale,
312
+ * even if intermediate thinking/meta anchors split it into
313
+ * multiple sub-gaps. Long model reasoning before the first reply
314
+ * is composing, not idle. (User→user gaps still register as
315
+ * between-turn idle because no agent activity occurred between.)
309
316
  *
310
317
  * Side effect: the leading warm-up gap before the first agent response
311
318
  * drops out unless it crosses MIN_GAP_MS — which the first 6s in a
312
319
  * typical Codex session does not. */
313
320
  const rawIdleBands = useMemo(() => {
314
- const isAnchor = (role: string) =>
315
- role === "user" ||
316
- role === "agent" ||
317
- role === "tool-call" ||
318
- role === "tool-result";
319
- const MIN_GAP_MS = 10_000;
321
+ // Anchor on ANY timestamped event so agent-thinking, meta, and other
322
+ // intra-turn signals prove the parent is doing work. Only what we
323
+ // need to distinguish is "user" (turn boundary) vs everything else.
324
+ const isUserRole = (role: string) => role === "user";
325
+ // 30s threshold: keeps ordinary tool latency (Read big file, slow Bash,
326
+ // model thinking between anchors) out of the idle stripes while still
327
+ // catching anything that reads as a real pause.
328
+ const MIN_GAP_MS = 30_000;
320
329
  const anchors: { ms: number; role: string }[] = [];
321
330
  for (const e of events) {
322
- if (!isAnchor(e.role)) continue;
323
331
  if (!e.timestamp) continue;
324
332
  const ms = Date.parse(e.timestamp);
325
333
  if (!Number.isFinite(ms)) continue;
@@ -329,38 +337,103 @@ export function SessionView({
329
337
  anchors.sort((a, b) => a.ms - b.ms);
330
338
  const sessionStartMs = anchors[0]!.ms - (events.find((e) => e.timestamp)?.tOffsetMs ?? 0);
331
339
 
340
+ const subagentSpans = (session.subagents ?? [])
341
+ .filter(
342
+ (s): s is SubagentRun & { startTOffsetMs: number; endTOffsetMs: number } =>
343
+ s.startTOffsetMs !== undefined && s.endTOffsetMs !== undefined,
344
+ )
345
+ .map((s) => ({ start: s.startTOffsetMs, end: s.endTOffsetMs }))
346
+ .sort((a, b) => a.start - b.start);
347
+
348
+ /** Subtract subagent spans from [start, end] in offset coordinates,
349
+ * returning 0+ remaining slices in time order. */
350
+ const carveOutSubagents = (
351
+ start: number,
352
+ end: number,
353
+ ): { start: number; end: number }[] => {
354
+ let pieces: { start: number; end: number }[] = [{ start, end }];
355
+ for (const span of subagentSpans) {
356
+ if (span.end <= start) continue;
357
+ if (span.start >= end) break;
358
+ const next: { start: number; end: number }[] = [];
359
+ for (const p of pieces) {
360
+ if (span.end <= p.start || span.start >= p.end) {
361
+ next.push(p);
362
+ continue;
363
+ }
364
+ if (span.start > p.start) next.push({ start: p.start, end: span.start });
365
+ if (span.end < p.end) next.push({ start: span.end, end: p.end });
366
+ }
367
+ pieces = next;
368
+ }
369
+ return pieces;
370
+ };
371
+
372
+ const overlapsSubagent = (start: number, end: number): boolean => {
373
+ for (const span of subagentSpans) {
374
+ if (span.end <= start) continue;
375
+ // subagentSpans is sorted by start, so once span.start >= end no
376
+ // later span can overlap [start, end] either — bail out cleanly.
377
+ if (span.start >= end) return false;
378
+ return true;
379
+ }
380
+ return false;
381
+ };
382
+
332
383
  type Band = { start: number; end: number; durationMs: number };
333
384
  const bands: Band[] = [];
334
- let inTurn: Band | null = null;
335
385
 
336
- const flushInTurn = () => {
337
- if (inTurn && inTurn.durationMs > MIN_GAP_MS) bands.push(inTurn);
338
- inTurn = null;
339
- };
386
+ // "Awaiting first response" — true after a user message, false once an
387
+ // agent or tool-call anchor proves the model started replying.
388
+ // Thinking and meta events don't clear it, so a user→thinking→agent
389
+ // sequence where thinking→agent crosses MIN_GAP_MS still reads as
390
+ // response latency rather than idle.
391
+ let awaitingFirstResponse = false;
392
+ if (anchors[0]) {
393
+ if (isUserRole(anchors[0].role)) awaitingFirstResponse = true;
394
+ }
340
395
 
341
396
  for (let i = 1; i < anchors.length; i++) {
342
- const gap = anchors[i]!.ms - anchors[i - 1]!.ms;
343
- if (gap <= MIN_GAP_MS) continue;
397
+ const rawGap = anchors[i]!.ms - anchors[i - 1]!.ms;
398
+ const curRole = anchors[i]!.role;
399
+ // Snapshot the phase BEFORE updating, so the gap is classified by
400
+ // the state at its start (anchors[i-1]).
401
+ const skipAsLatency = awaitingFirstResponse && !isUserRole(curRole);
402
+
403
+ if (isUserRole(curRole)) awaitingFirstResponse = true;
404
+ else if (curRole === "agent" || curRole === "tool-call") awaitingFirstResponse = false;
405
+
406
+ if (rawGap <= MIN_GAP_MS) continue;
407
+ if (skipAsLatency) continue;
408
+
344
409
  const gStart = anchors[i - 1]!.ms - sessionStartMs;
345
410
  const gEnd = anchors[i]!.ms - sessionStartMs;
411
+ const betweenTurn = isUserRole(curRole);
412
+
413
+ if (!betweenTurn) {
414
+ // In-turn gap: the parent is mid-turn. If any subagent run overlaps,
415
+ // the parent is waiting on delegated work — not idle. Drop the
416
+ // whole gap rather than carving out startup/teardown slivers that
417
+ // would clutter the minimap with thin stripes. Gaps with no
418
+ // subagent overlap (long Bash runs, model thinking between tool
419
+ // calls) still emit as a single band.
420
+ if (overlapsSubagent(gStart, gEnd)) continue;
421
+ bands.push({ start: gStart, end: gEnd, durationMs: gEnd - gStart });
422
+ continue;
423
+ }
346
424
 
347
- if (anchors[i]!.role === "user") {
348
- // Between-turn idle — close any in-progress in-turn merge first,
349
- // then push the between-turn band on its own.
350
- flushInTurn();
351
- bands.push({ start: gStart, end: gEnd, durationMs: gap });
352
- } else {
353
- // In-turn idle — accumulate into the running merged band.
354
- if (!inTurn) inTurn = { start: gStart, end: gEnd, durationMs: gap };
355
- else {
356
- inTurn.end = gEnd;
357
- inTurn.durationMs += gap;
425
+ // Between-turn gap: user walked away. Carve out background subagent
426
+ // activity so only the genuinely-unwatched portion counts as idle.
427
+ // Each surviving slice becomes its own band.
428
+ for (const slice of carveOutSubagents(gStart, gEnd)) {
429
+ const dur = slice.end - slice.start;
430
+ if (dur > MIN_GAP_MS) {
431
+ bands.push({ start: slice.start, end: slice.end, durationMs: dur });
358
432
  }
359
433
  }
360
434
  }
361
- flushInTurn();
362
435
  return bands;
363
- }, [events]);
436
+ }, [events, session.subagents]);
364
437
 
365
438
  const coldResumeMarkers = useMemo(() => {
366
439
  const seen = new Set<string>();
@@ -621,6 +694,12 @@ export function SessionView({
621
694
  </span>
622
695
  <InlineStatDivider />
623
696
  {model && <InlineStat icon={<Cpu size={12} />} value={model} mono />}
697
+ {session.entrypoint && (
698
+ <>
699
+ <InlineStatDivider />
700
+ <EntrypointBadge entrypoint={session.entrypoint} />
701
+ </>
702
+ )}
624
703
  <InlineStatDivider />
625
704
  <InlineStat icon={<Folder size={12} />} value={projectName} truncate />
626
705
  <InlineStatDivider />
@@ -1143,6 +1222,31 @@ function InlineStatDivider() {
1143
1222
  );
1144
1223
  }
1145
1224
 
1225
+ function EntrypointBadge({ entrypoint }: { entrypoint: string }) {
1226
+ // cli + claude-desktop are human-driven; sdk-* are programmatic.
1227
+ const isSdk = entrypoint.startsWith("sdk-");
1228
+ const tone = isSdk
1229
+ ? { bg: "rgba(245, 158, 11, 0.16)", fg: "#b45309" }
1230
+ : { bg: "rgba(16, 185, 129, 0.16)", fg: "#047857" };
1231
+ return (
1232
+ <span
1233
+ title={`entrypoint: ${entrypoint}`}
1234
+ style={{
1235
+ fontSize: 10.5,
1236
+ padding: "2px 7px",
1237
+ borderRadius: 4,
1238
+ background: tone.bg,
1239
+ color: tone.fg,
1240
+ fontWeight: 600,
1241
+ fontFamily: "var(--font-mono)",
1242
+ letterSpacing: 0.2,
1243
+ }}
1244
+ >
1245
+ {entrypoint}
1246
+ </span>
1247
+ );
1248
+ }
1249
+
1146
1250
  function InlineTokenStat({ usage }: { usage: SessionEvent["usage"] }) {
1147
1251
  const { ref, anchor, open, close } = useAnchoredTooltip();
1148
1252
  const u = usage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
@@ -2637,14 +2741,15 @@ function TranscriptList({
2637
2741
  }
2638
2742
 
2639
2743
  const idleBandList = (rawIdleBands ?? []).slice().sort((a, b) => a.start - b.start);
2640
- /** Returns the durationMs of the idle band that ends at (or just before)
2641
- * the given row's tOffset, or null if no band matches. Tolerance is 1s
2642
- * to absorb Date.parse rounding differences between anchor.ms and the
2643
- * presentation row's tOffsetMs. */
2644
- const idleBeforeOffset = (tOff: number | undefined): number | null => {
2744
+ /** Returns the first matching idle band (or null) whose end falls within
2745
+ * 1s of the given row's tOffset. Tolerance absorbs Date.parse rounding
2746
+ * between anchor.ms and the presentation row's tOffsetMs. */
2747
+ const idleBandBeforeOffset = (
2748
+ tOff: number | undefined,
2749
+ ): { start: number; durationMs: number } | null => {
2645
2750
  if (tOff === undefined) return null;
2646
2751
  for (const b of idleBandList) {
2647
- if (Math.abs(b.end - tOff) < 1000) return b.durationMs;
2752
+ if (Math.abs(b.end - tOff) < 1000) return b;
2648
2753
  }
2649
2754
  return null;
2650
2755
  };
@@ -2656,6 +2761,11 @@ function TranscriptList({
2656
2761
  };
2657
2762
 
2658
2763
  const out: React.ReactNode[] = [];
2764
+ // A user row and the turn-collapsed row that follows it share a
2765
+ // tOffsetMs (turn anchors at the user message — see buildMegaRows), so
2766
+ // both would match the same between-turn band without this guard. Track
2767
+ // emitted band starts and skip duplicates.
2768
+ const emittedBandStarts = new Set<number>();
2659
2769
  for (let i = 0; i < displayRows.length; i++) {
2660
2770
  const d = displayRows[i];
2661
2771
  // Emit a "Session idle" divider before any row that starts right after
@@ -2663,9 +2773,14 @@ function TranscriptList({
2663
2773
  // expanded turn doesn't render dividers for its inner steps.
2664
2774
  const indented = d.kind === "presentation" ? d.indented : false;
2665
2775
  if (!indented) {
2666
- const idleMs = idleBeforeOffset(rowTOffset(d));
2667
- if (idleMs !== null && idleMs > IDLE_THRESHOLD_MS) {
2668
- out.push(<IdleDivider key={`idle-before-${i}`} gapMs={idleMs} />);
2776
+ const band = idleBandBeforeOffset(rowTOffset(d));
2777
+ if (
2778
+ band !== null &&
2779
+ band.durationMs > IDLE_THRESHOLD_MS &&
2780
+ !emittedBandStarts.has(band.start)
2781
+ ) {
2782
+ emittedBandStarts.add(band.start);
2783
+ out.push(<IdleDivider key={`idle-before-${i}`} gapMs={band.durationMs} />);
2669
2784
  }
2670
2785
  }
2671
2786