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.
- 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 +2 -3
- 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 +2 -3
- 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 +2 -3
- 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]__0fpbr8-._.js → [externals]__0o43ejh._.js} +2 -2
- 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]__0e8n_x4._.js → [root-of-the-server]__08agijy._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0_8zh-i._.js +3 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0ej_p3s._.js +2 -2
- package/app/apps/web/.next/server/chunks/{[root-of-the-server]__0-vgrh1._.js → [root-of-the-server]__0i06ito._.js} +2 -2
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0jprdks._.js +2 -2
- 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]__0d~2~le._.js +1 -1
- 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 +3 -0
- 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 +3 -3
- package/app/apps/web/.next/server/chunks/ssr/[root-of-the-server]__0yv0m0q._.js +3 -0
- package/app/apps/web/.next/server/chunks/ssr/_0jw2sm7._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/_0xu2pvc._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0ly1xes._.js +1 -1
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_0txtsv5._.js +2 -2
- package/app/apps/web/.next/server/chunks/ssr/apps_web_components_usage-charts-dashboard_tsx_0bgr_uv._.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/04a36uhu~p_z-.css +1 -0
- package/app/apps/web/.next/static/chunks/{0q2vd~aj.1hpz.js → 0g8ujtb0lp._9.js} +3 -3
- package/app/apps/web/.next/static/chunks/0l3ck.awv~4t7.js +1 -0
- package/app/apps/web/.next/static/chunks/{171za22fv684..js → 0lxuac2v0_gdl.js} +2 -2
- package/app/apps/web/.next/static/chunks/0n0n.0279ot6c.js +1 -0
- package/app/apps/web/.next/static/chunks/0uev~g~qvbjv9.js +4 -0
- package/app/apps/web/.next/static/chunks/0~n2a9f52wju9.js +14 -0
- 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 +31 -0
- package/app/apps/web/components/job-queue-widget.tsx +11 -8
- package/app/apps/web/components/previous-cycles-trend.tsx +26 -11
- package/app/apps/web/components/usage-chart.tsx +35 -37
- package/app/apps/web/components/usage-gauges.tsx +18 -12
- package/app/apps/web/components/usage-sidebar.tsx +23 -12
- 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/lib/utilization-tone.ts +74 -0
- package/app/apps/web/package.json +1 -1
- package/app/apps/web/tsconfig.tsbuildinfo +1 -1
- package/dist/daemon-worker.js +609 -110
- package/dist/index.js +5312 -4802
- package/package.json +1 -1
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0.z48ue._.js +0 -3
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0dctov1._.js +0 -5
- package/app/apps/web/.next/server/chunks/[root-of-the-server]__0op9af~._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_09hmrqx._.js +0 -3
- package/app/apps/web/.next/server/chunks/packages_118y1lp._.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/[root-of-the-server]__0ytdqxt._.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/05m.oz3kmf1p7.js +0 -1
- package/app/apps/web/.next/static/chunks/07-fq4im3pmjp.js +0 -4
- package/app/apps/web/.next/static/chunks/0cxyt4-24..b7.js +0 -14
- package/app/apps/web/.next/static/chunks/0o9ayt8sknubd.js +0 -1
- package/app/apps/web/.next/static/chunks/0wfbj5zr~az.z.css +0 -1
- /package/app/apps/web/.next/static/{u6-8f8u1tspYCacGAz53R → sxcMbKfZ5XgVXYN4QHOHd}/_buildManifest.js +0 -0
- /package/app/apps/web/.next/static/{u6-8f8u1tspYCacGAz53R → sxcMbKfZ5XgVXYN4QHOHd}/_clientMiddlewareManifest.js +0 -0
- /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
|
-
|
|
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;
|
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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({
|
|
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
|
|
64
|
-
pct
|
|
65
|
-
pct
|
|
66
|
-
|
|
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
|
-
*
|
|
11
|
+
* Burndown-style chart for a single usage window.
|
|
11
12
|
*
|
|
12
|
-
* Y-axis: remaining budget (100% at start, 0% when quota
|
|
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
|
-
* —
|
|
17
|
-
* Actual line: solid, plots (100 - utilization) at each snapshot's captured_at
|
|
17
|
+
* — full-utilization trajectory.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
230
|
-
|
|
231
|
-
{
|
|
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
|
-
{/*
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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)}
|