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.
- package/app/apps/web/.next/BUILD_ID +1 -1
- package/app/apps/web/.next/build-manifest.json +3 -3
- package/app/apps/web/.next/prerender-manifest.json +3 -3
- package/app/apps/web/.next/server/app/_global-error.html +1 -1
- package/app/apps/web/.next/server/app/_global-error.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/apps/web/.next/server/app/_not-found/page.js +2 -2
- package/app/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/api/ask/route.js +4 -3
- package/app/apps/web/.next/server/app/api/ask/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js +3 -4
- package/app/apps/web/.next/server/app/api/digest/day/[date]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js +3 -4
- package/app/apps/web/.next/server/app/api/digest/month/[yearMonth]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/month-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js +3 -4
- package/app/apps/web/.next/server/app/api/digest/week/[startDate]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js +3 -3
- package/app/apps/web/.next/server/app/api/digest/week-index/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/events/route.js +2 -3
- package/app/apps/web/.next/server/app/api/events/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js +1 -1
- package/app/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js +1 -1
- package/app/apps/web/.next/server/app/api/runs/[runId]/stream/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/runs/route.js +1 -1
- package/app/apps/web/.next/server/app/api/runs/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/api/settings/route.js +3 -4
- package/app/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page.js +2 -2
- package/app/apps/web/.next/server/app/changelog/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page.js +2 -2
- package/app/apps/web/.next/server/app/day/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/day/page.js +2 -2
- package/app/apps/web/.next/server/app/day/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/day/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page.js +2 -2
- package/app/apps/web/.next/server/app/digest/[date]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/[date]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/digest/page.js +2 -2
- package/app/apps/web/.next/server/app/digest/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/digest/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page.js +2 -2
- package/app/apps/web/.next/server/app/insights/[key]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/[key]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/insights/page.js +2 -2
- package/app/apps/web/.next/server/app/insights/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/insights/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/page.js +2 -2
- package/app/apps/web/.next/server/app/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/parallelism/page.js +2 -2
- package/app/apps/web/.next/server/app/parallelism/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/parallelism/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page.js +2 -2
- package/app/apps/web/.next/server/app/projects/[slug]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/projects/page.js +2 -2
- package/app/apps/web/.next/server/app/projects/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/runs/page.js +2 -2
- package/app/apps/web/.next/server/app/runs/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page.js +2 -2
- package/app/apps/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/sessions/page.js +2 -2
- package/app/apps/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/settings/page.js +2 -2
- package/app/apps/web/.next/server/app/settings/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/app/usage/page.js +2 -2
- package/app/apps/web/.next/server/app/usage/page.js.nft.json +1 -1
- package/app/apps/web/.next/server/app/usage/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/chunks/[externals]__09v47rg._.js +3 -0
- package/app/apps/web/.next/server/chunks/[externals]__0o43ejh._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__00n3f9y._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__02px2mk._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__02zukjw._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0455vu3._.js +5 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__067~im1._.js +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__06b4cj-._.js +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__08agijy._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_8zh-i._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ej_p3s._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0i06ito._.js +3 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +9 -0
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0vm_41a._.js +3 -0
- package/app/apps/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0ck.78s.js +4 -4
- package/app/apps/web/.next/server/chunks/packages_0c-sz9n._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_0wywuu.._.js +3 -0
- package/app/apps/web/.next/server/chunks/packages_entries_dist_node_0bckr7-.js +27 -13
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0r4tks8._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0s4dg1y._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0tejjy_._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0w52fbc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv0m0q._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_lib_0m67-xi._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_entries_dist_node_0_271yr.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_12.endt._.js +3 -0
- package/app/apps/web/.next/server/middleware-build-manifest.js +3 -3
- package/app/apps/web/.next/server/pages/500.html +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.js +1 -1
- package/app/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next/static/chunks/063r4x2.9csh8.js +14 -0
- package/app/apps/web/.next/static/chunks/{09--oejbzw3q-.js → 0rtba0saz74e1.js} +2 -2
- package/app/apps/web/app/api/ask/route.ts +124 -111
- package/app/apps/web/app/api/events/route.ts +3 -4
- package/app/apps/web/app/api/runs/[runId]/route.ts +2 -2
- package/app/apps/web/app/api/runs/[runId]/stream/route.ts +3 -2
- package/app/apps/web/app/api/runs/route.ts +8 -4
- package/app/apps/web/app/sessions/[id]/session-view.tsx +164 -49
- package/app/apps/web/components/job-queue-widget.tsx +11 -8
- package/app/apps/web/lib/calibration-data.ts +2 -4
- package/app/apps/web/lib/changelog-data.generated.ts +1 -1
- package/app/apps/web/lib/entries-index.ts +2 -2
- package/app/apps/web/lib/jobs.ts +3 -5
- package/app/apps/web/lib/usage-data.ts +3 -4
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +481 -81
- package/dist/index.js +4373 -3961
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__07qxzx4._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0fu2xfy._.js +0 -5
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0gfd42k._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0i.m8y0._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0iy5aqy._.js +0 -9
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nh7-nf._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0nvdffv._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0s2~c3g._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_09wmnv.._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_0o6.gpk._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_parser_dist_fs_122oe0-.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0sp6.jt._.js +0 -3
- package/app/apps/web/.next/server/chunks/ssr/packages_parser_dist_10_oegc._.js +0 -3
- package/app/apps/web/.next/static/chunks/0qcu452dr~edz.js +0 -14
- /package/app/apps/web/.next/static/{L0Dy1hOQrZjV9l0TNulvk → XTns3Y6jHWmb7EYKzQi55}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{L0Dy1hOQrZjV9l0TNulvk → XTns3Y6jHWmb7EYKzQi55}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
162
|
-
}
|
|
163
|
-
});
|
|
191
|
+
finish();
|
|
192
|
+
});
|
|
164
193
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
174
|
-
}
|
|
175
|
-
});
|
|
199
|
+
});
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
/**
|
|
27
|
-
const USAGE_LOG_DIR =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
343
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
2641
|
-
* the given row's tOffset
|
|
2642
|
-
*
|
|
2643
|
-
|
|
2644
|
-
|
|
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
|
|
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
|
|
2667
|
-
if (
|
|
2668
|
-
|
|
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
|
|