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 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 `8000`).
68
- - `CONSENSUS_CPU_ACTIVE`: CPU threshold for active state (default `5`).
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 = 5;
2
- const DEFAULT_EVENT_WINDOW_MS = 8000;
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
- return cpuActive || eventActive ? "active" : "idle";
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 { doing, title, events, model: state.model, hasError, summary, lastEventAt };
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 { deriveState } from "./activity.js";
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" ? Date.now() - elapsed : undefined;
185
- const session = pickSessionForProcess(sessions, startMs);
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 state = deriveState({ cpu, hasError, lastEventAt });
218
- const cmdRaw = proc.cmd || proc.name || "";
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
- return { ts: Date.now(), agents };
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.0",
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
- ctx.fillText("No codex processes found", 0, 0);
373
- ctx.restore();
374
- requestAnimationFrame(draw);
375
- return;
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
- item.agent.state === "active" && !reducedMotion
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;