consensus-cli 0.1.0 → 0.1.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/CHANGELOG.md +11 -0
- package/README.md +3 -2
- package/dist/activity.js +20 -3
- package/dist/codexLogs.js +73 -1
- package/dist/scan.js +79 -8
- package/package.json +3 -2
- package/public/app.js +85 -5
- package/public/style.css +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ This project follows Semantic Versioning.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.1.2 - 2026-01-24
|
|
9
|
+
- Lower CPU threshold for active detection.
|
|
10
|
+
- Increase activity window defaults for long-running turns.
|
|
11
|
+
- Skip vendor codex helper processes to avoid duplicate tiles.
|
|
12
|
+
- Improve session mapping for active-state detection.
|
|
13
|
+
|
|
14
|
+
## 0.1.1 - 2026-01-24
|
|
15
|
+
- Smooth active state to prevent animation flicker.
|
|
16
|
+
- Add `consensus-cli` binary alias so `npx consensus-cli` works.
|
|
17
|
+
- Extend active window to match Codex event cadence.
|
|
18
|
+
|
|
8
19
|
## 0.1.0 - 2026-01-24
|
|
9
20
|
- Initial public release.
|
|
10
21
|
- Improve work summaries and recent events (latest-first, event-only fallback).
|
package/README.md
CHANGED
|
@@ -64,8 +64,9 @@ consensus dev server running on http://127.0.0.1:8787
|
|
|
64
64
|
- `CONSENSUS_CODEX_HOME`: override Codex home (default `~/.codex`).
|
|
65
65
|
- `CONSENSUS_PROCESS_MATCH`: regex to match codex processes.
|
|
66
66
|
- `CONSENSUS_REDACT_PII`: set to `0` to disable redaction (default enabled).
|
|
67
|
-
- `CONSENSUS_EVENT_ACTIVE_MS`: active window after last event in ms (default `
|
|
68
|
-
- `CONSENSUS_CPU_ACTIVE`: CPU threshold for active state (default `
|
|
67
|
+
- `CONSENSUS_EVENT_ACTIVE_MS`: active window after last event in ms (default `300000`).
|
|
68
|
+
- `CONSENSUS_CPU_ACTIVE`: CPU threshold for active state (default `1`).
|
|
69
|
+
- `CONSENSUS_ACTIVE_HOLD_MS`: keep active state this long after activity (default `600000`).
|
|
69
70
|
|
|
70
71
|
Full config details: `docs/configuration.md`
|
|
71
72
|
|
package/dist/activity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const DEFAULT_CPU_THRESHOLD =
|
|
2
|
-
const DEFAULT_EVENT_WINDOW_MS =
|
|
1
|
+
const DEFAULT_CPU_THRESHOLD = 1;
|
|
2
|
+
const DEFAULT_EVENT_WINDOW_MS = 300000;
|
|
3
|
+
const DEFAULT_ACTIVE_HOLD_MS = 600000;
|
|
3
4
|
export function deriveState(input) {
|
|
4
5
|
if (input.hasError)
|
|
5
6
|
return "error";
|
|
@@ -10,5 +11,21 @@ export function deriveState(input) {
|
|
|
10
11
|
const cpuActive = input.cpu > cpuThreshold;
|
|
11
12
|
const eventActive = typeof input.lastEventAt === "number" &&
|
|
12
13
|
now - input.lastEventAt <= eventWindowMs;
|
|
13
|
-
|
|
14
|
+
const inFlight = !!input.inFlight;
|
|
15
|
+
return cpuActive || eventActive || inFlight ? "active" : "idle";
|
|
16
|
+
}
|
|
17
|
+
export function deriveStateWithHold(input) {
|
|
18
|
+
const now = input.now ?? Date.now();
|
|
19
|
+
const holdMs = input.holdMs ?? Number(process.env.CONSENSUS_ACTIVE_HOLD_MS || DEFAULT_ACTIVE_HOLD_MS);
|
|
20
|
+
const baseState = deriveState({ ...input, now });
|
|
21
|
+
let lastActiveAt = input.previousActiveAt;
|
|
22
|
+
if (baseState === "active") {
|
|
23
|
+
lastActiveAt = now;
|
|
24
|
+
}
|
|
25
|
+
if (baseState === "idle" &&
|
|
26
|
+
typeof lastActiveAt === "number" &&
|
|
27
|
+
now - lastActiveAt <= holdMs) {
|
|
28
|
+
return { state: "active", lastActiveAt };
|
|
29
|
+
}
|
|
30
|
+
return { state: baseState, lastActiveAt };
|
|
14
31
|
}
|
package/dist/codexLogs.js
CHANGED
|
@@ -4,11 +4,14 @@ import path from "path";
|
|
|
4
4
|
import { redactText } from "./redact.js";
|
|
5
5
|
const SESSION_WINDOW_MS = 30 * 60 * 1000;
|
|
6
6
|
const SESSION_SCAN_INTERVAL_MS = 5000;
|
|
7
|
+
const SESSION_ID_SCAN_INTERVAL_MS = 60000;
|
|
7
8
|
const MAX_READ_BYTES = 512 * 1024;
|
|
8
9
|
const MAX_EVENTS = 50;
|
|
9
10
|
let cachedSessions = [];
|
|
10
11
|
let lastSessionScan = 0;
|
|
11
12
|
const tailStates = new Map();
|
|
13
|
+
const sessionIdCache = new Map();
|
|
14
|
+
const sessionIdLastScan = new Map();
|
|
12
15
|
export function resolveCodexHome(env = process.env) {
|
|
13
16
|
const override = env.CONSENSUS_CODEX_HOME || env.CODEX_HOME;
|
|
14
17
|
return override ? path.resolve(override) : path.join(os.homedir(), ".codex");
|
|
@@ -41,6 +44,36 @@ async function walk(dir, out, windowMs) {
|
|
|
41
44
|
}
|
|
42
45
|
}));
|
|
43
46
|
}
|
|
47
|
+
async function findSessionFile(dir, sessionId) {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const fullPath = path.join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) {
|
|
58
|
+
const found = await findSessionFile(fullPath, sessionId);
|
|
59
|
+
if (found)
|
|
60
|
+
return found;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
64
|
+
continue;
|
|
65
|
+
if (!entry.name.includes(sessionId))
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const stat = await fsp.stat(fullPath);
|
|
69
|
+
return { path: fullPath, mtimeMs: stat.mtimeMs };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
44
77
|
export async function listRecentSessions(codexHome, windowMs = SESSION_WINDOW_MS) {
|
|
45
78
|
const now = Date.now();
|
|
46
79
|
if (now - lastSessionScan < SESSION_SCAN_INTERVAL_MS) {
|
|
@@ -54,6 +87,28 @@ export async function listRecentSessions(codexHome, windowMs = SESSION_WINDOW_MS
|
|
|
54
87
|
cachedSessions = results;
|
|
55
88
|
return results;
|
|
56
89
|
}
|
|
90
|
+
export async function findSessionById(codexHome, sessionId) {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const lastScan = sessionIdLastScan.get(sessionId) || 0;
|
|
93
|
+
if (now - lastScan < SESSION_ID_SCAN_INTERVAL_MS) {
|
|
94
|
+
const cached = sessionIdCache.get(sessionId);
|
|
95
|
+
if (cached) {
|
|
96
|
+
try {
|
|
97
|
+
const stat = await fsp.stat(cached);
|
|
98
|
+
return { path: cached, mtimeMs: stat.mtimeMs };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
sessionIdLastScan.set(sessionId, now);
|
|
107
|
+
const sessionsDir = path.join(codexHome, "sessions");
|
|
108
|
+
const found = await findSessionFile(sessionsDir, sessionId);
|
|
109
|
+
sessionIdCache.set(sessionId, found ? found.path : null);
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
57
112
|
export function pickSessionForProcess(sessions, startTimeMs) {
|
|
58
113
|
if (sessions.length === 0)
|
|
59
114
|
return undefined;
|
|
@@ -256,6 +311,8 @@ export async function updateTail(sessionPath) {
|
|
|
256
311
|
const combined = state.partial + text;
|
|
257
312
|
const lines = combined.split(/\r?\n/);
|
|
258
313
|
state.partial = lines.pop() || "";
|
|
314
|
+
const startRe = /(turn|item|response)\.started/i;
|
|
315
|
+
const endRe = /(turn|item|response)\.(completed|failed|errored)/i;
|
|
259
316
|
for (const line of lines) {
|
|
260
317
|
if (!line.trim())
|
|
261
318
|
continue;
|
|
@@ -270,6 +327,12 @@ export async function updateTail(sessionPath) {
|
|
|
270
327
|
const { summary, kind, isError, model, type } = summarizeEvent(ev);
|
|
271
328
|
if (model)
|
|
272
329
|
state.model = model;
|
|
330
|
+
if (typeof type === "string") {
|
|
331
|
+
if (startRe.test(type))
|
|
332
|
+
state.inFlight = true;
|
|
333
|
+
if (endRe.test(type))
|
|
334
|
+
state.inFlight = false;
|
|
335
|
+
}
|
|
273
336
|
if (summary) {
|
|
274
337
|
const entry = {
|
|
275
338
|
ts,
|
|
@@ -328,5 +391,14 @@ export function summarizeTail(state) {
|
|
|
328
391
|
lastTool: state.lastTool?.summary,
|
|
329
392
|
lastPrompt: state.lastPrompt?.summary,
|
|
330
393
|
};
|
|
331
|
-
return {
|
|
394
|
+
return {
|
|
395
|
+
doing,
|
|
396
|
+
title,
|
|
397
|
+
events,
|
|
398
|
+
model: state.model,
|
|
399
|
+
hasError,
|
|
400
|
+
summary,
|
|
401
|
+
lastEventAt,
|
|
402
|
+
inFlight: state.inFlight,
|
|
403
|
+
};
|
|
332
404
|
}
|
package/dist/scan.js
CHANGED
|
@@ -4,11 +4,12 @@ import fs from "fs";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { execFile } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import {
|
|
8
|
-
import { listRecentSessions, pickSessionForProcess, resolveCodexHome, summarizeTail, updateTail, } from "./codexLogs.js";
|
|
7
|
+
import { deriveStateWithHold } from "./activity.js";
|
|
8
|
+
import { listRecentSessions, findSessionById, pickSessionForProcess, resolveCodexHome, summarizeTail, updateTail, } from "./codexLogs.js";
|
|
9
9
|
import { redactText } from "./redact.js";
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
11
|
const repoCache = new Map();
|
|
12
|
+
const activityCache = new Map();
|
|
12
13
|
function isCodexProcess(cmd, name, matchRe) {
|
|
13
14
|
if (!cmd && !name)
|
|
14
15
|
return false;
|
|
@@ -16,6 +17,8 @@ function isCodexProcess(cmd, name, matchRe) {
|
|
|
16
17
|
return matchRe.test(cmd || "") || matchRe.test(name || "");
|
|
17
18
|
}
|
|
18
19
|
const cmdLine = cmd || "";
|
|
20
|
+
if (cmdLine.includes("/codex/vendor/"))
|
|
21
|
+
return false;
|
|
19
22
|
if (name === "codex")
|
|
20
23
|
return true;
|
|
21
24
|
if (cmdLine === "codex" || cmdLine.startsWith("codex "))
|
|
@@ -70,6 +73,22 @@ function parseDoingFromCmd(cmd) {
|
|
|
70
73
|
return "codex";
|
|
71
74
|
return undefined;
|
|
72
75
|
}
|
|
76
|
+
function extractSessionId(cmd) {
|
|
77
|
+
const parts = cmd.split(/\s+/g);
|
|
78
|
+
const resumeIndex = parts.indexOf("resume");
|
|
79
|
+
if (resumeIndex !== -1) {
|
|
80
|
+
const token = parts[resumeIndex + 1];
|
|
81
|
+
if (token && /^[0-9a-fA-F-]{16,}$/.test(token))
|
|
82
|
+
return token;
|
|
83
|
+
}
|
|
84
|
+
const sessionFlag = parts.findIndex((part) => part === "--session" || part === "--session-id");
|
|
85
|
+
if (sessionFlag !== -1) {
|
|
86
|
+
const token = parts[sessionFlag + 1];
|
|
87
|
+
if (token && /^[0-9a-fA-F-]{16,}$/.test(token))
|
|
88
|
+
return token;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
73
92
|
function normalizeTitle(value) {
|
|
74
93
|
if (!value)
|
|
75
94
|
return undefined;
|
|
@@ -133,6 +152,32 @@ async function getCwdsForPids(pids) {
|
|
|
133
152
|
}
|
|
134
153
|
return result;
|
|
135
154
|
}
|
|
155
|
+
async function getStartTimesForPids(pids) {
|
|
156
|
+
const result = new Map();
|
|
157
|
+
if (pids.length === 0)
|
|
158
|
+
return result;
|
|
159
|
+
if (process.platform === "win32")
|
|
160
|
+
return result;
|
|
161
|
+
try {
|
|
162
|
+
const { stdout } = await execFileAsync("ps", ["-o", "pid=,lstart=", "-p", pids.join(",")]);
|
|
163
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
166
|
+
if (!match)
|
|
167
|
+
continue;
|
|
168
|
+
const pid = Number(match[1]);
|
|
169
|
+
const dateStr = match[2].trim();
|
|
170
|
+
const parsed = Date.parse(dateStr);
|
|
171
|
+
if (!Number.isNaN(parsed)) {
|
|
172
|
+
result.set(pid, parsed);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
136
181
|
function findRepoRoot(cwd) {
|
|
137
182
|
if (repoCache.has(cwd))
|
|
138
183
|
return repoCache.get(cwd) || null;
|
|
@@ -152,6 +197,7 @@ function findRepoRoot(cwd) {
|
|
|
152
197
|
return null;
|
|
153
198
|
}
|
|
154
199
|
export async function scanCodexProcesses() {
|
|
200
|
+
const now = Date.now();
|
|
155
201
|
const matchEnv = process.env.CONSENSUS_PROCESS_MATCH;
|
|
156
202
|
let matchRe;
|
|
157
203
|
if (matchEnv) {
|
|
@@ -173,16 +219,24 @@ export async function scanCodexProcesses() {
|
|
|
173
219
|
usage = {};
|
|
174
220
|
}
|
|
175
221
|
const cwds = await getCwdsForPids(pids);
|
|
222
|
+
const startTimes = await getStartTimesForPids(pids);
|
|
176
223
|
const codexHome = resolveCodexHome();
|
|
177
224
|
const sessions = await listRecentSessions(codexHome);
|
|
178
225
|
const agents = [];
|
|
226
|
+
const seenIds = new Set();
|
|
179
227
|
for (const proc of codexProcs) {
|
|
180
228
|
const stats = usage[proc.pid] || {};
|
|
181
229
|
const cpu = typeof stats.cpu === "number" ? stats.cpu : 0;
|
|
182
230
|
const mem = typeof stats.memory === "number" ? stats.memory : 0;
|
|
183
231
|
const elapsed = stats.elapsed;
|
|
184
|
-
const startMs = typeof elapsed === "number"
|
|
185
|
-
|
|
232
|
+
const startMs = typeof elapsed === "number"
|
|
233
|
+
? Date.now() - elapsed
|
|
234
|
+
: startTimes.get(proc.pid);
|
|
235
|
+
const cmdRaw = proc.cmd || proc.name || "";
|
|
236
|
+
const sessionId = extractSessionId(cmdRaw);
|
|
237
|
+
const session = (sessionId && sessions.find((item) => item.path.includes(sessionId))) ||
|
|
238
|
+
(sessionId ? await findSessionById(codexHome, sessionId) : undefined) ||
|
|
239
|
+
pickSessionForProcess(sessions, startMs);
|
|
186
240
|
let doing;
|
|
187
241
|
let events;
|
|
188
242
|
let model;
|
|
@@ -190,6 +244,7 @@ export async function scanCodexProcesses() {
|
|
|
190
244
|
let title;
|
|
191
245
|
let summary;
|
|
192
246
|
let lastEventAt;
|
|
247
|
+
let inFlight = false;
|
|
193
248
|
if (session) {
|
|
194
249
|
const tail = await updateTail(session.path);
|
|
195
250
|
if (tail) {
|
|
@@ -201,6 +256,7 @@ export async function scanCodexProcesses() {
|
|
|
201
256
|
title = normalizeTitle(tailSummary.title);
|
|
202
257
|
summary = tailSummary.summary;
|
|
203
258
|
lastEventAt = tailSummary.lastEventAt;
|
|
259
|
+
inFlight = !!tailSummary.inFlight;
|
|
204
260
|
}
|
|
205
261
|
}
|
|
206
262
|
if (!doing) {
|
|
@@ -214,12 +270,22 @@ export async function scanCodexProcesses() {
|
|
|
214
270
|
const cwd = redactText(cwdRaw) || cwdRaw;
|
|
215
271
|
const repoRoot = cwdRaw ? findRepoRoot(cwdRaw) : null;
|
|
216
272
|
const repoName = repoRoot ? path.basename(repoRoot) : undefined;
|
|
217
|
-
const
|
|
218
|
-
const
|
|
273
|
+
const id = `${proc.pid}`;
|
|
274
|
+
const cached = activityCache.get(id);
|
|
275
|
+
const activity = deriveStateWithHold({
|
|
276
|
+
cpu,
|
|
277
|
+
hasError,
|
|
278
|
+
lastEventAt,
|
|
279
|
+
inFlight,
|
|
280
|
+
previousActiveAt: cached?.lastActiveAt,
|
|
281
|
+
now,
|
|
282
|
+
});
|
|
283
|
+
const state = activity.state;
|
|
284
|
+
activityCache.set(id, { lastActiveAt: activity.lastActiveAt, lastSeenAt: now });
|
|
285
|
+
seenIds.add(id);
|
|
219
286
|
const cmd = redactText(cmdRaw) || cmdRaw;
|
|
220
287
|
const cmdShort = shortenCmd(cmd);
|
|
221
288
|
const kind = inferKind(cmd);
|
|
222
|
-
const id = `${proc.pid}`;
|
|
223
289
|
const startedAt = startMs ? Math.floor(startMs / 1000) : undefined;
|
|
224
290
|
const computedTitle = title || deriveTitle(doing, repoName, proc.pid);
|
|
225
291
|
const safeSummary = sanitizeSummary(summary);
|
|
@@ -244,7 +310,12 @@ export async function scanCodexProcesses() {
|
|
|
244
310
|
events,
|
|
245
311
|
});
|
|
246
312
|
}
|
|
247
|
-
|
|
313
|
+
for (const id of activityCache.keys()) {
|
|
314
|
+
if (!seenIds.has(id)) {
|
|
315
|
+
activityCache.delete(id);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { ts: now, agents };
|
|
248
319
|
}
|
|
249
320
|
const isDirectRun = process.argv[1] && process.argv[1].endsWith("scan.js");
|
|
250
321
|
if (isDirectRun) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "consensus-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "dist/server.js",
|
|
16
16
|
"bin": {
|
|
17
|
-
"consensus": "dist/cli.js"
|
|
17
|
+
"consensus": "dist/cli.js",
|
|
18
|
+
"consensus-cli": "dist/cli.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist",
|
package/public/app.js
CHANGED
|
@@ -49,6 +49,53 @@ let searchMatches = new Set();
|
|
|
49
49
|
const layout = new Map();
|
|
50
50
|
const occupied = new Map();
|
|
51
51
|
|
|
52
|
+
function ensureSelectedVisible(agent) {
|
|
53
|
+
if (!agent || !panel.classList.contains("open")) return;
|
|
54
|
+
if (view.dragging) return;
|
|
55
|
+
const panelRect = panel.getBoundingClientRect();
|
|
56
|
+
if (panelRect.width >= window.innerWidth * 0.8) return;
|
|
57
|
+
const key = keyForAgent(agent);
|
|
58
|
+
const coord = layout.get(key);
|
|
59
|
+
if (!coord) return;
|
|
60
|
+
|
|
61
|
+
const screen = isoToScreen(coord.x, coord.y, tileW, tileH);
|
|
62
|
+
const memMB = (agent.mem || 0) / (1024 * 1024);
|
|
63
|
+
const heightBase = Math.min(120, Math.max(18, memMB * 0.4));
|
|
64
|
+
const idleScale = agent.state === "idle" ? 0.6 : 1;
|
|
65
|
+
const height = heightBase * idleScale;
|
|
66
|
+
|
|
67
|
+
const targetX = view.x + screen.x * view.scale;
|
|
68
|
+
const targetY = view.y + screen.y * view.scale;
|
|
69
|
+
const halfW = (tileW / 2) * view.scale;
|
|
70
|
+
const halfH = (tileH / 2) * view.scale;
|
|
71
|
+
const padding = 36;
|
|
72
|
+
const viewportWidth = window.innerWidth - panelRect.width;
|
|
73
|
+
const viewportHeight = window.innerHeight;
|
|
74
|
+
|
|
75
|
+
const left = targetX - halfW;
|
|
76
|
+
const right = targetX + halfW;
|
|
77
|
+
const top = targetY - (height + tileH * 0.6) * view.scale;
|
|
78
|
+
const bottom = targetY + (halfH + tileH * 0.6) * view.scale;
|
|
79
|
+
|
|
80
|
+
let dx = 0;
|
|
81
|
+
let dy = 0;
|
|
82
|
+
if (right > viewportWidth - padding) {
|
|
83
|
+
dx = viewportWidth - padding - right;
|
|
84
|
+
} else if (left < padding) {
|
|
85
|
+
dx = padding - left;
|
|
86
|
+
}
|
|
87
|
+
if (top < padding) {
|
|
88
|
+
dy = padding - top;
|
|
89
|
+
} else if (bottom > viewportHeight - padding) {
|
|
90
|
+
dy = viewportHeight - padding - bottom;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (dx !== 0 || dy !== 0) {
|
|
94
|
+
view.x += dx;
|
|
95
|
+
view.y += dy;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
function resize() {
|
|
53
100
|
deviceScale = window.devicePixelRatio || 1;
|
|
54
101
|
canvas.width = window.innerWidth * deviceScale;
|
|
@@ -369,13 +416,16 @@ function draw() {
|
|
|
369
416
|
ctx.fillStyle = "rgba(228, 230, 235, 0.6)";
|
|
370
417
|
ctx.font = "16px Space Grotesk";
|
|
371
418
|
ctx.textAlign = "center";
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
419
|
+
ctx.fillText("No codex processes found", 0, 0);
|
|
420
|
+
ctx.restore();
|
|
421
|
+
requestAnimationFrame(draw);
|
|
422
|
+
return;
|
|
376
423
|
}
|
|
377
424
|
|
|
378
425
|
updateLayout(agents);
|
|
426
|
+
if (selected) {
|
|
427
|
+
ensureSelectedVisible(selected);
|
|
428
|
+
}
|
|
379
429
|
|
|
380
430
|
const drawList = agents
|
|
381
431
|
.map((agent) => {
|
|
@@ -399,10 +449,15 @@ function draw() {
|
|
|
399
449
|
const palette = statePalette[item.agent.state] || statePalette.idle;
|
|
400
450
|
const memMB = item.agent.mem / (1024 * 1024);
|
|
401
451
|
const heightBase = Math.min(120, Math.max(18, memMB * 0.4));
|
|
452
|
+
const isActive = item.agent.state === "active";
|
|
402
453
|
const pulse =
|
|
403
|
-
|
|
454
|
+
isActive && !reducedMotion
|
|
404
455
|
? 4 + Math.sin(time / 200) * 3
|
|
405
456
|
: 0;
|
|
457
|
+
const pulsePhase =
|
|
458
|
+
isActive && !reducedMotion
|
|
459
|
+
? (Math.sin(time / 240) + 1) / 2
|
|
460
|
+
: 0;
|
|
406
461
|
const idleScale = item.agent.state === "idle" ? 0.6 : 1;
|
|
407
462
|
const height = heightBase * idleScale + pulse;
|
|
408
463
|
|
|
@@ -425,6 +480,31 @@ function draw() {
|
|
|
425
480
|
null
|
|
426
481
|
);
|
|
427
482
|
|
|
483
|
+
if (isActive) {
|
|
484
|
+
const glowAlpha = 0.12 + pulsePhase * 0.22;
|
|
485
|
+
const capAlpha = 0.16 + pulsePhase * 0.28;
|
|
486
|
+
ctx.save();
|
|
487
|
+
drawDiamond(
|
|
488
|
+
ctx,
|
|
489
|
+
x,
|
|
490
|
+
y + tileH * 0.02,
|
|
491
|
+
tileW * 0.92,
|
|
492
|
+
tileH * 0.46,
|
|
493
|
+
`rgba(87, 242, 198, ${glowAlpha})`,
|
|
494
|
+
null
|
|
495
|
+
);
|
|
496
|
+
drawDiamond(
|
|
497
|
+
ctx,
|
|
498
|
+
x,
|
|
499
|
+
y - height - tileH * 0.18,
|
|
500
|
+
roofSize * 0.82,
|
|
501
|
+
roofSize * 0.42,
|
|
502
|
+
`rgba(87, 242, 198, ${capAlpha})`,
|
|
503
|
+
null
|
|
504
|
+
);
|
|
505
|
+
ctx.restore();
|
|
506
|
+
}
|
|
507
|
+
|
|
428
508
|
if (selected && selected.id === item.agent.id) {
|
|
429
509
|
drawDiamond(ctx, x, y, tileW + 10, tileH + 6, "rgba(0,0,0,0)", "#57f2c6");
|
|
430
510
|
}
|
package/public/style.css
CHANGED
|
@@ -320,11 +320,13 @@ body {
|
|
|
320
320
|
margin-top: 6px;
|
|
321
321
|
background: var(--idle);
|
|
322
322
|
box-shadow: 0 0 10px transparent;
|
|
323
|
+
transform-origin: center;
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
.lane-pill.active {
|
|
326
327
|
background: var(--active);
|
|
327
328
|
box-shadow: 0 0 12px rgba(81, 195, 165, 0.5);
|
|
329
|
+
animation: lanePulse 1.3s ease-in-out infinite;
|
|
328
330
|
}
|
|
329
331
|
|
|
330
332
|
.lane-pill.error {
|
|
@@ -332,6 +334,21 @@ body {
|
|
|
332
334
|
box-shadow: 0 0 12px rgba(209, 88, 75, 0.5);
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
@keyframes lanePulse {
|
|
338
|
+
0% {
|
|
339
|
+
transform: scale(1);
|
|
340
|
+
box-shadow: 0 0 10px rgba(81, 195, 165, 0.35);
|
|
341
|
+
}
|
|
342
|
+
50% {
|
|
343
|
+
transform: scale(1.35);
|
|
344
|
+
box-shadow: 0 0 16px rgba(81, 195, 165, 0.6);
|
|
345
|
+
}
|
|
346
|
+
100% {
|
|
347
|
+
transform: scale(1);
|
|
348
|
+
box-shadow: 0 0 10px rgba(81, 195, 165, 0.35);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
335
352
|
.lane-copy {
|
|
336
353
|
display: flex;
|
|
337
354
|
flex-direction: column;
|