takomi 2.1.21 → 2.1.25

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.
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
- import { platform } from "node:os";
4
+ import { homedir, platform } from "node:os";
5
5
  import { spawn } from "node:child_process";
6
6
 
7
7
  type NotifySoundConfig = {
@@ -10,10 +10,11 @@ type NotifySoundConfig = {
10
10
 
11
11
  type NotifyMethod = "auto" | "wav";
12
12
 
13
- const CONFIG_PATH = join(process.cwd(), ".pi", "notify-sound.json");
14
- const WAV_PATH = join(process.cwd(), ".pi", "notify-sound.wav");
15
- const PS1_PATH = join(process.cwd(), ".pi", "notify-sound.ps1");
16
- const VBS_PATH = join(process.cwd(), ".pi", "notify-sound.vbs");
13
+ const GLOBAL_NOTIFY_DIR = join(homedir(), ".pi", "agent", "notify-sound");
14
+ const CONFIG_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.json");
15
+ const WAV_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.wav");
16
+ const PS1_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.ps1");
17
+ const VBS_PATH = join(GLOBAL_NOTIFY_DIR, "notify-sound.vbs");
17
18
  const STALE_AGENT_START_MS = 24 * 60 * 60 * 1000;
18
19
 
19
20
  let config: NotifySoundConfig = { enabled: true };
@@ -44,7 +44,11 @@ const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
44
44
  { value: "daily", label: "daily", description: "Show daily usage rows" },
45
45
  { value: "models", label: "models", description: "Show model usage leaderboard" },
46
46
  { value: "projects", label: "projects", description: "Show project usage leaderboard" },
47
- { value: "sessions", label: "sessions", description: "Show longest/busiest main sessions" },
47
+ { value: "projects-full", label: "projects-full", description: "Show full project names" },
48
+ { value: "sessions", label: "sessions", description: "Show longest active main sessions" },
49
+ { value: "sessions-full", label: "sessions-full", description: "Show full session names and files" },
50
+ { value: "tasks", label: "tasks", description: "Show longest tasks by active turn span" },
51
+ { value: "tasks-full", label: "tasks-full", description: "Show full task prompts" },
48
52
  { value: "tools", label: "tools", description: "Show most used tools" },
49
53
  { value: "agents", label: "agents", description: "Show main agent role leaderboard" },
50
54
  { value: "subagents", label: "subagents", description: "Show subagent run leaderboard" },
@@ -95,7 +99,7 @@ export function commandHelp(): string {
95
99
  "/takomi mode <direct|orchestrate|review>",
96
100
  "/takomi gate <auto|review>",
97
101
  "/takomi subagents <on|off|status|expand|collapse|toggle>",
98
- "/takomi stats [overview|daily|models|projects|sessions|tools|subagents|sources] [since 7d]",
102
+ "/takomi stats [overview|daily|models|projects|projects-full|sessions|sessions-full|tasks|tasks-full|tools|subagents|sources] [since 7d]",
99
103
  "/takomi routing [show|where]",
100
104
  "/takomi routing <policy text> # updates global policy",
101
105
  "/takomi routing local <policy text> # project override",
@@ -306,8 +306,8 @@ function center(text, width) {
306
306
  return ' '.repeat(pad) + text;
307
307
  }
308
308
 
309
- function statCard(value, label) {
310
- return { value: String(value), label };
309
+ function statCard(value, label, color = pc.white) {
310
+ return { value: String(value), label, color };
311
311
  }
312
312
 
313
313
  // ── Table Helper ────────────────────────────────────────────────────────────
@@ -348,11 +348,42 @@ function runLabel(run, width = 28) {
348
348
  const label = run ? `${run.agent || 'unknown'}: ${run.task || ''}`.trim() : '-';
349
349
  return label.length > width ? label.slice(0, width - 1) + '…' : label;
350
350
  }
351
+ function indentWrap(text, width = 76, indent = ' ') {
352
+ const words = String(text || '').split(/\s+/).filter(Boolean);
353
+ const lines = [];
354
+ let line = '';
355
+ for (const word of words) {
356
+ if ((line + ' ' + word).trim().length > width && line) { lines.push(line); line = word; }
357
+ else line = (line + ' ' + word).trim();
358
+ }
359
+ if (line) lines.push(line);
360
+ return lines.map((l, i) => (i ? indent : '') + l).join('\n');
361
+ }
362
+ function renderFullList(title, rows, renderRow, limit = 20) {
363
+ const lines = ['\n' + pc.bold(pc.magenta('Takomi Stats')), renderTable(title, [], [{ width: 74 }])];
364
+ rows.slice(0, limit).forEach((row, i) => lines.push(renderRow(row, i)));
365
+ lines.push('\n' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
366
+ return lines.join('\n');
367
+ }
351
368
 
352
369
  function renderFocusedView(stats, opts = {}) {
353
370
  const view = opts.view;
354
371
  const limit = opts.limit || 20;
355
372
  if (!view || view === 'overview') return null;
373
+ if (view === 'projects-full' || view === 'project-full') return renderFullList('Top Projects — Full Names', stats.byProject, (r, i) => [
374
+ ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.key)}`,
375
+ ` ${pc.cyan(fmtTokens(r.total))} ${pc.dim(fmtMoney(r.cost))} ${pc.dim(r.events + ' calls')}`,
376
+ ].join('\n'), limit);
377
+ if (view === 'sessions-full' || view === 'session-full') return renderFullList('Longest Active Sessions — Full Names', stats.topSessions, (r, i) => [
378
+ ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.project || r.cwd || r.key || 'unknown')}`,
379
+ ` ${pc.cyan(ms(sessionDuration(r)))} ${pc.dim(r.turns + ' turns')} ${pc.dim(r.toolCalls + ' tools')}`,
380
+ ` ${pc.dim(r.file || '')}`,
381
+ ].join('\n'), limit);
382
+ if (view === 'tasks-full' || view === 'task-full') return renderFullList('Longest Tasks — Full Prompts', stats.topTasks || [], (r, i) => [
383
+ ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.cyan(ms(taskDuration(r)))} ${pc.magenta(r.toolCalls + ' tools')} ${pc.dim(dayOf(r.start))}`,
384
+ ` ${pc.white(indentWrap(r.title || r.project || r.session || 'unknown'))}`,
385
+ ` ${pc.dim(r.project || '')}`,
386
+ ].join('\n'), limit);
356
387
  const tables = {
357
388
  models: ['Top Models', stats.byModel, [
358
389
  { width: 26, align: 'left', get: r => pc.white(r.key) },
@@ -398,7 +429,7 @@ function renderFocusedView(stats, opts = {}) {
398
429
  tasks: ['Longest Tasks', stats.topTasks || [], [
399
430
  { width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
400
431
  { width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
401
- { width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
432
+ { width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
402
433
  { width: 36, align: 'left', get: r => pc.white(taskLabel(r, 36)) },
403
434
  ]],
404
435
  daily: ['Daily Usage', [...stats.byDay].reverse(), [
@@ -455,8 +486,9 @@ export function renderTakomiStats(stats, opts = {}) {
455
486
  for (const c of cards) {
456
487
  const vPad = Math.max(0, Math.floor((cardW - c.value.length) / 2));
457
488
  const lPad = Math.max(0, Math.floor((cardW - c.label.length) / 2));
458
- const vContent = ' '.repeat(vPad) + pc.bold(pc.white(c.value));
459
- const lContent = ' '.repeat(lPad) + pc.dim(c.label);
489
+ const color = c.color || pc.white;
490
+ const vContent = ' '.repeat(vPad) + pc.bold(color(c.value));
491
+ const lContent = ' '.repeat(lPad) + pc.dim(color(c.label));
460
492
  // Pad to cardW visible chars
461
493
  vStr += ansiPadEnd(vContent, cardW);
462
494
  lStr += ansiPadEnd(lContent, cardW);
@@ -486,11 +518,11 @@ export function renderTakomiStats(stats, opts = {}) {
486
518
  // ── Duration Cards ────────────────────────────────────────────────────
487
519
  lines.push('');
488
520
  const cards3 = [
489
- statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Longest Session'),
490
- statCard(longestSession ? String(longestSession.turns) : '-', 'Session Turns'),
491
- statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Task'),
492
- statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Task Tools'),
493
- statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Most Subagents'),
521
+ statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Active Session', pc.cyan),
522
+ statCard(longestSession ? String(longestSession.turns) : '-', 'Turns in Session', pc.cyan),
523
+ statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Task', pc.magenta),
524
+ statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Tools in Task', pc.magenta),
525
+ statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Max Subagents', pc.blue),
494
526
  ];
495
527
  const [v3, l3] = buildCardLines(cards3);
496
528
  lines.push(v3);
@@ -560,7 +592,7 @@ export function renderTakomiStats(stats, opts = {}) {
560
592
  lines.push(renderTable('Longest Tasks', stats.topTasks.slice(0, 5), [
561
593
  { width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
562
594
  { width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
563
- { width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
595
+ { width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
564
596
  { width: 34, align: 'left', get: r => pc.white(taskLabel(r, 34)) },
565
597
  ]));
566
598
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.21",
3
+ "version": "2.1.25",
4
4
  "description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -88,10 +88,10 @@ async function files(root, suffix = '.jsonl') {
88
88
  await walk(root); return out;
89
89
  }
90
90
 
91
- async function scanPiSessions(root, source, events, sessionRows = []) {
91
+ async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
92
92
  for (const file of await files(root)) {
93
- let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '';
94
- const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map() };
93
+ let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
94
+ const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0 };
95
95
  const text = await fs.readFile(file, 'utf8').catch(() => '');
96
96
  for (const line of text.split(/\r?\n/)) {
97
97
  const obj = safeJson(line); if (!obj) continue;
@@ -110,11 +110,20 @@ async function scanPiSessions(root, source, events, sessionRows = []) {
110
110
  const msg = obj.type === 'message' && obj.message ? obj.message : null;
111
111
  if (msg) {
112
112
  row.messages += 1;
113
- if (msg.role === 'user') row.turns += 1;
113
+ const ts = obj.timestamp || msg.timestamp || '';
114
+ if (msg.role === 'user') {
115
+ if (currentTask?.end && currentTask.end !== currentTask.start) taskRows.push(currentTask);
116
+ row.turns += 1;
117
+ const textPart = (msg.content || []).find(p => p?.type === 'text')?.text || '';
118
+ currentTask = { source, file, session, project: projectKey(file), cwd, start: ts, end: ts, provider, model, turns: 1, toolCalls: 0, title: String(textPart).replace(/\s+/g, ' ').trim() };
119
+ } else if (currentTask && ts) {
120
+ currentTask.end = ts;
121
+ }
114
122
  for (const part of msg.content || []) {
115
123
  if (!part || part.type !== 'toolCall') continue;
116
124
  const name = part.name || 'unknown';
117
125
  row.toolCalls += 1;
126
+ if (currentTask) currentTask.toolCalls += 1;
118
127
  if (name === 'takomi_subagent') {
119
128
  const args = part.arguments || {};
120
129
  const count = Array.isArray(args.tasks) ? args.tasks.length : Array.isArray(args.chain) ? args.chain.length : 1;
@@ -126,6 +135,8 @@ async function scanPiSessions(root, source, events, sessionRows = []) {
126
135
  const u = msg && msg.usage;
127
136
  if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, true) });
128
137
  }
138
+ if (currentTask?.end && currentTask.end !== currentTask.start) taskRows.push(currentTask);
139
+ row.activeMs = taskRows.filter(t => t.file === file).reduce((sum, t) => sum + taskDuration(t), 0);
129
140
  if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
130
141
  }
131
142
  }
@@ -141,13 +152,16 @@ async function scanRunHistory(file) {
141
152
  export async function collectTakomiStats(opts = {}) {
142
153
  const home = opts.home || os.homedir();
143
154
  const cwd = opts.cwd || process.cwd();
144
- const rawEvents = [], rawSessions = [];
145
- await scanPiSessions(path.join(home, '.pi', 'agent', 'sessions'), 'pi-global', rawEvents, rawSessions);
146
- await scanPiSessions(path.join(cwd, '.pi', 'agent', 'sessions'), 'pi-project', rawEvents, rawSessions);
147
- await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions);
155
+ const rawEvents = [], rawSessions = [], rawTasks = [];
156
+ const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
157
+ const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
158
+ await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
159
+ if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
160
+ await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
148
161
  const sinceDay = parseSince(opts.since);
149
162
  const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
150
163
  const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
164
+ const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
151
165
  const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
152
166
  const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map(), byTool = new Map(), byRole = new Map(), byStage = new Map(), byWorkflow = new Map();
153
167
  let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
@@ -165,9 +179,10 @@ export async function collectTakomiStats(opts = {}) {
165
179
  }
166
180
  const byAgent = new Map(); let longestRun = null;
167
181
  for (const r of runs) { add(byAgent, r.agent || 'unknown', { total: 0, events: 1 }); if (!longestRun || (+r.duration||0) > (+longestRun.duration||0)) longestRun = r; }
168
- const topSessions = [...sessionRows].sort((a,b)=>b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
182
+ const topSessions = [...sessionRows].sort((a,b)=>(b.activeMs||0)-(a.activeMs||0) || b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
183
+ const topTasks = [...taskRows].sort((a,b)=>taskDuration(b)-taskDuration(a) || b.toolCalls-a.toolCalls).slice(0, 20);
169
184
  const mostSubagentsSession = [...sessionRows].sort((a,b)=>b.subagentCalls-a.subagentCalls)[0] || null;
170
- return { generatedAt: new Date().toISOString(), cwd, since: sinceDay, totals, sessions: new Set([...events.map(e => e.session), ...sessionRows.map(s => s.session)]).size, byDay: [...byDay.values()].sort((a,b)=>a.key.localeCompare(b.key)), byModel: [...byModel.values()].sort((a,b)=>b.total-a.total), bySource: [...bySource.values()].sort((a,b)=>b.total-a.total), byProject: [...byProject.values()].sort((a,b)=>b.total-a.total), byTool: [...byTool.values()].sort((a,b)=>b.events-a.events), byRole: [...byRole.values()].sort((a,b)=>b.events-a.events), byStage: [...byStage.values()].sort((a,b)=>b.events-a.events), byWorkflow: [...byWorkflow.values()].sort((a,b)=>b.events-a.events), byAgent: [...byAgent.values()].sort((a,b)=>b.events-a.events), sessionRows, topSessions, mostSubagentsSession, runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
185
+ return { generatedAt: new Date().toISOString(), cwd, since: sinceDay, totals, sessions: new Set([...events.map(e => e.session), ...sessionRows.map(s => s.session)]).size, byDay: [...byDay.values()].sort((a,b)=>a.key.localeCompare(b.key)), byModel: [...byModel.values()].sort((a,b)=>b.total-a.total), bySource: [...bySource.values()].sort((a,b)=>b.total-a.total), byProject: [...byProject.values()].sort((a,b)=>b.total-a.total), byTool: [...byTool.values()].sort((a,b)=>b.events-a.events), byRole: [...byRole.values()].sort((a,b)=>b.events-a.events), byStage: [...byStage.values()].sort((a,b)=>b.events-a.events), byWorkflow: [...byWorkflow.values()].sort((a,b)=>b.events-a.events), byAgent: [...byAgent.values()].sort((a,b)=>b.events-a.events), sessionRows, taskRows, topSessions, topTasks, mostSubagentsSession, runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
171
186
  }
172
187
 
173
188
  // ── Streak Calculation ──────────────────────────────────────────────────────
@@ -291,8 +306,8 @@ function center(text, width) {
291
306
  return ' '.repeat(pad) + text;
292
307
  }
293
308
 
294
- function statCard(value, label) {
295
- return { value: String(value), label };
309
+ function statCard(value, label, color = pc.white) {
310
+ return { value: String(value), label, color };
296
311
  }
297
312
 
298
313
  // ── Table Helper ────────────────────────────────────────────────────────────
@@ -317,11 +332,58 @@ function sessionLabel(row, width = 36) {
317
332
  return project.length > width ? '…' + project.slice(-(width - 1)) : project;
318
333
  }
319
334
  function sessionDay(row) { return dayOf(row.start || row.end).slice(5) || '??-??'; }
335
+ function sessionDuration(row) {
336
+ return row?.activeMs || 0;
337
+ }
338
+ function taskDuration(row) {
339
+ const start = Date.parse(row?.start || '');
340
+ const end = Date.parse(row?.end || '');
341
+ return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : 0;
342
+ }
343
+ function taskLabel(row, width = 34) {
344
+ const label = row?.title || row?.project || row?.session || 'unknown';
345
+ return label.length > width ? label.slice(0, width - 1) + '…' : label;
346
+ }
347
+ function runLabel(run, width = 28) {
348
+ const label = run ? `${run.agent || 'unknown'}: ${run.task || ''}`.trim() : '-';
349
+ return label.length > width ? label.slice(0, width - 1) + '…' : label;
350
+ }
351
+ function indentWrap(text, width = 76, indent = ' ') {
352
+ const words = String(text || '').split(/\s+/).filter(Boolean);
353
+ const lines = [];
354
+ let line = '';
355
+ for (const word of words) {
356
+ if ((line + ' ' + word).trim().length > width && line) { lines.push(line); line = word; }
357
+ else line = (line + ' ' + word).trim();
358
+ }
359
+ if (line) lines.push(line);
360
+ return lines.map((l, i) => (i ? indent : '') + l).join('\n');
361
+ }
362
+ function renderFullList(title, rows, renderRow, limit = 20) {
363
+ const lines = ['\n' + pc.bold(pc.magenta('Takomi Stats')), renderTable(title, [], [{ width: 74 }])];
364
+ rows.slice(0, limit).forEach((row, i) => lines.push(renderRow(row, i)));
365
+ lines.push('\n' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
366
+ return lines.join('\n');
367
+ }
320
368
 
321
369
  function renderFocusedView(stats, opts = {}) {
322
370
  const view = opts.view;
323
371
  const limit = opts.limit || 20;
324
372
  if (!view || view === 'overview') return null;
373
+ if (view === 'projects-full' || view === 'project-full') return renderFullList('Top Projects — Full Names', stats.byProject, (r, i) => [
374
+ ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.key)}`,
375
+ ` ${pc.cyan(fmtTokens(r.total))} ${pc.dim(fmtMoney(r.cost))} ${pc.dim(r.events + ' calls')}`,
376
+ ].join('\n'), limit);
377
+ if (view === 'sessions-full' || view === 'session-full') return renderFullList('Longest Active Sessions — Full Names', stats.topSessions, (r, i) => [
378
+ ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.project || r.cwd || r.key || 'unknown')}`,
379
+ ` ${pc.cyan(ms(sessionDuration(r)))} ${pc.dim(r.turns + ' turns')} ${pc.dim(r.toolCalls + ' tools')}`,
380
+ ` ${pc.dim(r.file || '')}`,
381
+ ].join('\n'), limit);
382
+ if (view === 'tasks-full' || view === 'task-full') return renderFullList('Longest Tasks — Full Prompts', stats.topTasks || [], (r, i) => [
383
+ ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.cyan(ms(taskDuration(r)))} ${pc.magenta(r.toolCalls + ' tools')} ${pc.dim(dayOf(r.start))}`,
384
+ ` ${pc.white(indentWrap(r.title || r.project || r.session || 'unknown'))}`,
385
+ ` ${pc.dim(r.project || '')}`,
386
+ ].join('\n'), limit);
325
387
  const tables = {
326
388
  models: ['Top Models', stats.byModel, [
327
389
  { width: 26, align: 'left', get: r => pc.white(r.key) },
@@ -355,14 +417,21 @@ function renderFocusedView(stats, opts = {}) {
355
417
  { width: 10, align: 'right', get: r => pc.cyan(String(r.events)) },
356
418
  { width: 8, align: 'left', get: () => pc.dim('calls') },
357
419
  ]],
358
- sessions: ['Longest Main Sessions', stats.topSessions, [
420
+ sessions: ['Longest Active Sessions', stats.topSessions, [
359
421
  { width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
360
- { width: 34, align: 'left', get: r => pc.white(sessionLabel(r, 34)) },
422
+ { width: 30, align: 'left', get: r => pc.white(sessionLabel(r, 30)) },
423
+ { width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
361
424
  { width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
362
425
  { width: 8, align: 'left', get: () => pc.dim('turns') },
363
426
  { width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
364
427
  { width: 8, align: 'left', get: () => pc.dim('tools') },
365
428
  ]],
429
+ tasks: ['Longest Tasks', stats.topTasks || [], [
430
+ { width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
431
+ { width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
432
+ { width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
433
+ { width: 36, align: 'left', get: r => pc.white(taskLabel(r, 36)) },
434
+ ]],
366
435
  daily: ['Daily Usage', [...stats.byDay].reverse(), [
367
436
  { width: 12, align: 'left', get: r => pc.white(r.key) },
368
437
  { width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
@@ -385,6 +454,8 @@ export function renderTakomiStats(stats, opts = {}) {
385
454
  const topModel = stats.byModel[0]?.key || 'unknown';
386
455
  const peak = stats.byDay.reduce((a,b) => b.total > (a?.total||0) ? b : a, null);
387
456
  const streaks = calcStreaks(stats.byDay);
457
+ const longestSession = stats.topSessions[0] || null;
458
+ const longestTask = stats.topTasks?.[0] || null;
388
459
  const lines = [];
389
460
 
390
461
  // ── Header ────────────────────────────────────────────────────────────
@@ -415,8 +486,9 @@ export function renderTakomiStats(stats, opts = {}) {
415
486
  for (const c of cards) {
416
487
  const vPad = Math.max(0, Math.floor((cardW - c.value.length) / 2));
417
488
  const lPad = Math.max(0, Math.floor((cardW - c.label.length) / 2));
418
- const vContent = ' '.repeat(vPad) + pc.bold(pc.white(c.value));
419
- const lContent = ' '.repeat(lPad) + pc.dim(c.label);
489
+ const color = c.color || pc.white;
490
+ const vContent = ' '.repeat(vPad) + pc.bold(color(c.value));
491
+ const lContent = ' '.repeat(lPad) + pc.dim(color(c.label));
420
492
  // Pad to cardW visible chars
421
493
  vStr += ansiPadEnd(vContent, cardW);
422
494
  lStr += ansiPadEnd(lContent, cardW);
@@ -443,6 +515,19 @@ export function renderTakomiStats(stats, opts = {}) {
443
515
  lines.push(v2);
444
516
  lines.push(l2);
445
517
 
518
+ // ── Duration Cards ────────────────────────────────────────────────────
519
+ lines.push('');
520
+ const cards3 = [
521
+ statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Active Session', pc.cyan),
522
+ statCard(longestSession ? String(longestSession.turns) : '-', 'Turns in Session', pc.cyan),
523
+ statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Task', pc.magenta),
524
+ statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Tools in Task', pc.magenta),
525
+ statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Max Subagents', pc.blue),
526
+ ];
527
+ const [v3, l3] = buildCardLines(cards3);
528
+ lines.push(v3);
529
+ lines.push(l3);
530
+
446
531
  // ── Info line ─────────────────────────────────────────────────────────
447
532
  lines.push('');
448
533
  const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events${stats.since ? ` · since ${stats.since}` : ''}`;
@@ -490,9 +575,10 @@ export function renderTakomiStats(stats, opts = {}) {
490
575
  // ── Main Session Table ──────────────────────────────────────────────────
491
576
  if (stats.topSessions.length) {
492
577
  lines.push('');
493
- lines.push(renderTable('Longest Main Sessions', stats.topSessions.slice(0, 5), [
578
+ lines.push(renderTable('Longest Active Sessions', stats.topSessions.slice(0, 5), [
494
579
  { width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
495
- { width: 32, align: 'left', get: r => pc.white(sessionLabel(r, 32)) },
580
+ { width: 28, align: 'left', get: r => pc.white(sessionLabel(r, 28)) },
581
+ { width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
496
582
  { width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
497
583
  { width: 8, align: 'left', get: r => pc.dim('turns') },
498
584
  { width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
@@ -500,6 +586,27 @@ export function renderTakomiStats(stats, opts = {}) {
500
586
  ]));
501
587
  }
502
588
 
589
+ // ── Longest Tasks ──────────────────────────────────────────────────────
590
+ if (stats.topTasks?.length) {
591
+ lines.push('');
592
+ lines.push(renderTable('Longest Tasks', stats.topTasks.slice(0, 5), [
593
+ { width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
594
+ { width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
595
+ { width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
596
+ { width: 34, align: 'left', get: r => pc.white(taskLabel(r, 34)) },
597
+ ]));
598
+ }
599
+
600
+ // ── Longest Subagent Run ───────────────────────────────────────────────
601
+ if (stats.longestRun) {
602
+ lines.push('');
603
+ lines.push(renderTable('Longest Subagent Run', [stats.longestRun], [
604
+ { width: 22, align: 'left', get: r => pc.white(r.agent || 'unknown') },
605
+ { width: 10, align: 'right', get: r => pc.cyan(ms(+r.duration || 0)) },
606
+ { width: 30, align: 'left', get: r => pc.dim(runLabel(r, 30)) },
607
+ ]));
608
+ }
609
+
503
610
  // ── Tools Table ─────────────────────────────────────────────────────────
504
611
  if (stats.byTool.length) {
505
612
  lines.push('');