@oh-my-pi/pi-coding-agent 3.8.1337 → 3.13.1337

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
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.13.1337] - 2026-01-04
6
+
7
+ ## [3.9.1337] - 2026-01-04
8
+
9
+ ### Changed
10
+
11
+ - Changed default for `lsp.formatOnWrite` setting from `true` to `false`
12
+ - Updated status line thinking level display to use emoji icons instead of abbreviated text
13
+ - Changed auto-compact indicator from "(auto)" text to icon
14
+
15
+ ### Fixed
16
+
17
+ - Fixed status line not updating token counts and cost after starting a new session
18
+ - Fixed stale diagnostics persisting after file content changes in LSP client
19
+
5
20
  ## [3.8.1337] - 2026-01-04
6
21
  ### Added
7
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "3.8.1337",
3
+ "version": "3.13.1337",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-agent-core": "3.8.1337",
43
- "@oh-my-pi/pi-ai": "3.8.1337",
44
- "@oh-my-pi/pi-tui": "3.8.1337",
42
+ "@oh-my-pi/pi-agent-core": "3.13.1337",
43
+ "@oh-my-pi/pi-ai": "3.13.1337",
44
+ "@oh-my-pi/pi-tui": "3.13.1337",
45
45
  "@sinclair/typebox": "^0.34.46",
46
46
  "ajv": "^8.17.1",
47
47
  "chalk": "^5.5.0",
@@ -59,7 +59,7 @@ export interface MCPSettings {
59
59
  }
60
60
 
61
61
  export interface LspSettings {
62
- formatOnWrite?: boolean; // default: true (format files using LSP after write tool writes code files)
62
+ formatOnWrite?: boolean; // default: false (format files using LSP after write tool writes code files)
63
63
  diagnosticsOnWrite?: boolean; // default: true (return LSP diagnostics after write tool writes code files)
64
64
  diagnosticsOnEdit?: boolean; // default: false (return LSP diagnostics after edit tool edits code files)
65
65
  }
@@ -539,7 +539,7 @@ export class SettingsManager {
539
539
  }
540
540
 
541
541
  getLspFormatOnWrite(): boolean {
542
- return this.settings.lsp?.formatOnWrite ?? true;
542
+ return this.settings.lsp?.formatOnWrite ?? false;
543
543
  }
544
544
 
545
545
  setLspFormatOnWrite(enabled: boolean): void {
@@ -526,6 +526,9 @@ export async function syncContent(client: LspClient, filePath: string, content:
526
526
  }
527
527
 
528
528
  const syncPromise = (async () => {
529
+ // Clear stale diagnostics before syncing new content
530
+ client.diagnostics.delete(uri);
531
+
529
532
  const info = client.openFiles.get(uri);
530
533
 
531
534
  if (!info) {
@@ -6,15 +6,43 @@ import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui"
6
6
  import type { AgentSession } from "../../../core/agent-session";
7
7
  import { theme } from "../theme/theme";
8
8
 
9
- // Nerd Font icons (matching Claude/statusline-nerd.sh)
9
+ // Thinking level icons (Nerd Font)
10
+ const THINKING_ICONS: Record<string, string> = {
11
+ minimal: "🤨 min",
12
+ low: "🤔 low",
13
+ medium: "🤓 mid",
14
+ high: "🤯 high",
15
+ xhigh: "🧠 xhi",
16
+ };
17
+
18
+ // Nerd Font icons
10
19
  const ICONS = {
11
20
  model: "\uf4bc", // robot/model
12
21
  folder: "\uf115", // folder
13
- branch: "\uf126", // git branch
22
+ branch: "\ue725", // git branch
14
23
  sep: "\ue0b1", // powerline thin chevron
15
- tokens: "\uf0ce", // table/tokens
24
+ tokens: "\ue26b", // coins
25
+ context: "\ue70f", // window
26
+ auto: "\udb80\udc68", // auto
16
27
  } as const;
17
28
 
29
+ /** Create a colored text segment with background */
30
+ function plSegment(content: string, fgAnsi: string, bgAnsi: string): string {
31
+ return `${bgAnsi}${fgAnsi} ${content} \x1b[0m`;
32
+ }
33
+
34
+ /** Create separator with background */
35
+ function plSep(sepAnsi: string, bgAnsi: string): string {
36
+ return `${bgAnsi}${sepAnsi}${ICONS.sep}\x1b[0m`;
37
+ }
38
+
39
+ /** Create end cap - solid arrow transitioning bg to terminal default */
40
+ function plEnd(bgAnsi: string): string {
41
+ // Use the bg color as fg for the arrow (creates the triangle effect)
42
+ const fgFromBg = bgAnsi.replace("\x1b[48;", "\x1b[38;");
43
+ return `${fgFromBg}\ue0b0\x1b[0m`;
44
+ }
45
+
18
46
  /**
19
47
  * Sanitize text for display in a single-line status.
20
48
  * Removes newlines, tabs, carriage returns, and other control characters.
@@ -50,7 +78,7 @@ function findGitHeadPath(): string | null {
50
78
  /**
51
79
  * Footer component that shows pwd, token stats, and context usage
52
80
  */
53
- export class FooterComponent implements Component {
81
+ export class StatusLineComponent implements Component {
54
82
  private session: AgentSession;
55
83
  private cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
56
84
  private gitWatcher: FSWatcher | null = null;
@@ -220,7 +248,7 @@ export class FooterComponent implements Component {
220
248
  }
221
249
  }
222
250
 
223
- render(width: number): string[] {
251
+ private buildStatusLine(): string {
224
252
  const state = this.session.state;
225
253
 
226
254
  // Calculate cumulative usage from ALL session entries
@@ -264,132 +292,157 @@ export class FooterComponent implements Component {
264
292
  return `${Math.round(n / 1000000)}M`;
265
293
  };
266
294
 
267
- // Powerline separator (very dim)
268
- const sep = theme.fg("footerSep", ` ${ICONS.sep} `);
269
-
270
295
  // ═══════════════════════════════════════════════════════════════════════
271
- // SEGMENT 1: Model (Gold/White)
296
+ // SEGMENT 1: Model
272
297
  // ═══════════════════════════════════════════════════════════════════════
273
- const modelName = state.model?.id || "no-model";
274
- let modelSegment = theme.fg("footerModel", `${ICONS.model} ${modelName}`);
298
+ let modelName = state.model?.name || state.model?.id || "no-model";
299
+ // Strip "Claude " prefix for brevity
300
+ if (modelName.startsWith("Claude ")) {
301
+ modelName = modelName.slice(7);
302
+ }
303
+ let modelContent = `${ICONS.model} ${modelName}`;
275
304
  if (state.model?.reasoning) {
276
305
  const level = state.thinkingLevel || "off";
277
306
  if (level !== "off") {
278
- modelSegment += theme.fg("footerSep", " · ") + theme.fg("footerModel", level);
307
+ modelContent += ` · ${THINKING_ICONS[level] ?? level}`;
279
308
  }
280
309
  }
281
310
 
282
311
  // ═══════════════════════════════════════════════════════════════════════
283
- // SEGMENT 2: Path (Cyan with dim separators)
284
- // Replace home with ~, strip /work/, color separators
312
+ // SEGMENT 2: Path
285
313
  // ═══════════════════════════════════════════════════════════════════════
286
314
  let pwd = process.cwd();
287
315
  const home = process.env.HOME || process.env.USERPROFILE;
288
316
  if (home && pwd.startsWith(home)) {
289
317
  pwd = `~${pwd.slice(home.length)}`;
290
318
  }
291
- // Strip /work/ prefix
292
319
  if (pwd.startsWith("/work/")) {
293
320
  pwd = pwd.slice(6);
294
321
  }
295
- // Color path with dim separators: ~/foo/bar -> ~/foo/bar (separators dim)
296
- const pathColored = pwd
297
- .split("/")
298
- .map((part) => theme.fg("footerPath", part))
299
- .join(theme.fg("footerSep", "/"));
300
- const pathSegment = theme.fg("footerIcon", `${ICONS.folder} `) + pathColored;
322
+ const pathContent = `${ICONS.folder} ${pwd}`;
301
323
 
302
324
  // ═══════════════════════════════════════════════════════════════════════
303
- // SEGMENT 3: Git Branch + Status (Green/Yellow)
325
+ // SEGMENT 3: Git Branch + Status
304
326
  // ═══════════════════════════════════════════════════════════════════════
305
327
  const branch = this.getCurrentBranch();
306
- let gitSegment = "";
328
+ let gitContent = "";
329
+ let gitColorName: "statusLineGitClean" | "statusLineGitDirty" = "statusLineGitClean";
307
330
  if (branch) {
308
331
  const gitStatus = this.getGitStatus();
309
332
  const isDirty = gitStatus && (gitStatus.staged > 0 || gitStatus.unstaged > 0 || gitStatus.untracked > 0);
333
+ gitColorName = isDirty ? "statusLineGitDirty" : "statusLineGitClean";
310
334
 
311
- // Branch name - green if clean, yellow if dirty
312
- const branchColor = isDirty ? "footerDirty" : "footerBranch";
313
- gitSegment = theme.fg("footerIcon", `${ICONS.branch} `) + theme.fg(branchColor, branch);
335
+ gitContent = `${ICONS.branch} ${branch}`;
314
336
 
315
- // Add status indicators
316
337
  if (gitStatus) {
317
338
  const indicators: string[] = [];
318
339
  if (gitStatus.unstaged > 0) {
319
- indicators.push(theme.fg("footerDirty", `*${gitStatus.unstaged}`));
340
+ indicators.push(theme.fg("statusLineDirty", `*${gitStatus.unstaged}`));
320
341
  }
321
342
  if (gitStatus.staged > 0) {
322
- indicators.push(theme.fg("footerStaged", `+${gitStatus.staged}`));
343
+ indicators.push(theme.fg("statusLineStaged", `+${gitStatus.staged}`));
323
344
  }
324
345
  if (gitStatus.untracked > 0) {
325
- indicators.push(theme.fg("footerUntracked", `!${gitStatus.untracked}`));
346
+ indicators.push(theme.fg("statusLineUntracked", `?${gitStatus.untracked}`));
326
347
  }
327
348
  if (indicators.length > 0) {
328
- gitSegment += ` ${indicators.join(" ")}`;
349
+ gitContent += ` ${indicators.join(" ")}`;
329
350
  }
330
351
  }
331
352
  }
332
353
 
333
354
  // ═══════════════════════════════════════════════════════════════════════
334
- // SEGMENT 4: Stats (Pink/Magenta tones)
335
- // Concise: total tokens, cost, context%
355
+ // SEGMENT 4: Context (window usage)
336
356
  // ═══════════════════════════════════════════════════════════════════════
337
- const statParts: string[] = [];
357
+ const autoIndicator = this.autoCompactEnabled ? ` ${ICONS.auto}` : "";
358
+ const contextText = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
359
+ let contextContent: string;
360
+ if (contextPercentValue > 90) {
361
+ contextContent = `${ICONS.context} ${theme.fg("error", contextText)}`;
362
+ } else if (contextPercentValue > 70) {
363
+ contextContent = `${ICONS.context} ${theme.fg("warning", contextText)}`;
364
+ } else {
365
+ contextContent = `${ICONS.context} ${contextText}`;
366
+ }
367
+
368
+ // ═══════════════════════════════════════════════════════════════════════
369
+ // SEGMENT 5: Spend (tokens + cost)
370
+ // ═══════════════════════════════════════════════════════════════════════
371
+ const spendParts: string[] = [];
338
372
 
339
- // Total tokens (input + output + cache)
340
373
  const totalTokens = totalInput + totalOutput + totalCacheRead + totalCacheWrite;
341
374
  if (totalTokens) {
342
- statParts.push(theme.fg("footerOutput", `${ICONS.tokens} ${formatTokens(totalTokens)}`));
375
+ spendParts.push(`${ICONS.tokens} ${formatTokens(totalTokens)}`);
343
376
  }
344
377
 
345
- // Cost (pink)
346
378
  const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
347
379
  if (totalCost || usingSubscription) {
348
- const costDisplay = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
349
- statParts.push(theme.fg("footerCost", costDisplay));
380
+ const costDisplay = `$${totalCost.toFixed(2)}${usingSubscription ? " (sub)" : ""}`;
381
+ spendParts.push(costDisplay);
350
382
  }
351
383
 
352
- // Context percentage with severity coloring
353
- const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
354
- const contextDisplay = `${contextPercentValue.toFixed(1)}%/${formatTokens(contextWindow)}${autoIndicator}`;
355
- let contextColored: string;
356
- if (contextPercentValue > 90) {
357
- contextColored = theme.fg("error", contextDisplay);
358
- } else if (contextPercentValue > 70) {
359
- contextColored = theme.fg("warning", contextDisplay);
360
- } else {
361
- contextColored = theme.fg("footerSep", contextDisplay);
362
- }
363
- statParts.push(contextColored);
364
-
365
- const statsSegment = statParts.join(" ");
384
+ const spendContent = theme.fg("statusLineCost", spendParts.join(" · "));
366
385
 
367
386
  // ═══════════════════════════════════════════════════════════════════════
368
- // Assemble single powerline-style line
369
- // [Model] > [Path] > [Git] > [Stats]
387
+ // Assemble: [Model] > [Path] > [Git?] > [Context] > [Spend] >
370
388
  // ═══════════════════════════════════════════════════════════════════════
371
- const segments = [modelSegment, pathSegment];
372
- if (gitSegment) segments.push(gitSegment);
373
- segments.push(statsSegment);
389
+ const bgAnsi = theme.getBgAnsi("statusLineBg");
390
+ const sepAnsi = theme.getFgAnsi("statusLineSep");
391
+
392
+ let statusLine = "";
393
+
394
+ // Model segment
395
+ statusLine += plSegment(modelContent, theme.getFgAnsi("statusLineModel"), bgAnsi);
396
+ statusLine += plSep(sepAnsi, bgAnsi);
374
397
 
375
- let statusLine = segments.join(sep);
398
+ // Path segment
399
+ statusLine += plSegment(pathContent, theme.getFgAnsi("statusLinePath"), bgAnsi);
376
400
 
377
- // Truncate if needed
378
- if (visibleWidth(statusLine) > width) {
379
- statusLine = truncateToWidth(statusLine, width, theme.fg("footerSep", "…"));
401
+ if (gitContent) {
402
+ statusLine += plSep(sepAnsi, bgAnsi);
403
+ statusLine += plSegment(gitContent, theme.getFgAnsi(gitColorName), bgAnsi);
380
404
  }
381
405
 
382
- const lines = [statusLine];
406
+ // Context segment
407
+ statusLine += plSep(sepAnsi, bgAnsi);
408
+ statusLine += plSegment(contextContent, theme.getFgAnsi("statusLineContext"), bgAnsi);
409
+
410
+ // Spend segment
411
+ statusLine += plSep(sepAnsi, bgAnsi);
412
+ statusLine += plSegment(spendContent, theme.getFgAnsi("statusLineSpend"), bgAnsi);
413
+
414
+ // End cap (solid arrow to terminal bg)
415
+ statusLine += plEnd(bgAnsi);
416
+
417
+ return statusLine;
418
+ }
383
419
 
384
- // Hook statuses (optional second line)
385
- if (this.hookStatuses.size > 0) {
386
- const sortedStatuses = Array.from(this.hookStatuses.entries())
387
- .sort(([a], [b]) => a.localeCompare(b))
388
- .map(([, text]) => sanitizeStatusText(text));
389
- const hookLine = sortedStatuses.join(" ");
390
- lines.push(truncateToWidth(hookLine, width, theme.fg("footerSep", "…")));
420
+ /**
421
+ * Get the status line content for use as editor top border.
422
+ * Returns the content string and its visible width.
423
+ */
424
+ getTopBorder(_width: number): { content: string; width: number } {
425
+ const content = this.buildStatusLine();
426
+ return {
427
+ content,
428
+ width: visibleWidth(content),
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Render only hook statuses (if any).
434
+ * Used when footer is integrated into editor border.
435
+ */
436
+ render(width: number): string[] {
437
+ // Only render hook statuses - main status is in editor's top border
438
+ if (this.hookStatuses.size === 0) {
439
+ return [];
391
440
  }
392
441
 
393
- return lines;
442
+ const sortedStatuses = Array.from(this.hookStatuses.entries())
443
+ .sort(([a], [b]) => a.localeCompare(b))
444
+ .map(([, text]) => sanitizeStatusText(text));
445
+ const hookLine = sortedStatuses.join(" ");
446
+ return [truncateToWidth(hookLine, width, theme.fg("statusLineSep", "…"))];
394
447
  }
395
448
  }
@@ -43,7 +43,6 @@ import { CompactionSummaryMessageComponent } from "./components/compaction-summa
43
43
  import { CustomEditor } from "./components/custom-editor";
44
44
  import { DynamicBorder } from "./components/dynamic-border";
45
45
  import { ExtensionDashboard } from "./components/extensions";
46
- import { FooterComponent } from "./components/footer";
47
46
  import { HookEditorComponent } from "./components/hook-editor";
48
47
  import { HookInputComponent } from "./components/hook-input";
49
48
  import { HookMessageComponent } from "./components/hook-message";
@@ -52,6 +51,7 @@ import { ModelSelectorComponent } from "./components/model-selector";
52
51
  import { OAuthSelectorComponent } from "./components/oauth-selector";
53
52
  import { SessionSelectorComponent } from "./components/session-selector";
54
53
  import { SettingsSelectorComponent } from "./components/settings-selector";
54
+ import { StatusLineComponent } from "./components/status-line";
55
55
  import { ToolExecutionComponent } from "./components/tool-execution";
56
56
  import { TreeSelectorComponent } from "./components/tree-selector";
57
57
  import { TtsrNotificationComponent } from "./components/ttsr-notification";
@@ -85,7 +85,7 @@ export class InteractiveMode {
85
85
  private statusContainer: Container;
86
86
  private editor: CustomEditor;
87
87
  private editorContainer: Container;
88
- private footer: FooterComponent;
88
+ private statusLine: StatusLineComponent;
89
89
  private version: string;
90
90
  private isInitialized = false;
91
91
  private onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
@@ -176,8 +176,8 @@ export class InteractiveMode {
176
176
  this.editor = new CustomEditor(getEditorTheme());
177
177
  this.editorContainer = new Container();
178
178
  this.editorContainer.addChild(this.editor);
179
- this.footer = new FooterComponent(session);
180
- this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
179
+ this.statusLine = new StatusLineComponent(session);
180
+ this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
181
181
 
182
182
  // Define slash commands for autocomplete
183
183
  const slashCommands: SlashCommand[] = [
@@ -287,7 +287,7 @@ export class InteractiveMode {
287
287
  this.ui.addChild(this.statusContainer);
288
288
  this.ui.addChild(new Spacer(1));
289
289
  this.ui.addChild(this.editorContainer);
290
- this.ui.addChild(this.footer);
290
+ this.ui.addChild(this.statusLine); // Only renders hook statuses (main status in editor border)
291
291
  this.ui.setFocus(this.editor);
292
292
 
293
293
  this.setupKeyHandlers();
@@ -311,9 +311,13 @@ export class InteractiveMode {
311
311
  });
312
312
 
313
313
  // Set up git branch watcher
314
- this.footer.watchBranch(() => {
314
+ this.statusLine.watchBranch(() => {
315
+ this.updateEditorTopBorder();
315
316
  this.ui.requestRender();
316
317
  });
318
+
319
+ // Initial top border update
320
+ this.updateEditorTopBorder();
317
321
  }
318
322
 
319
323
  // =========================================================================
@@ -492,7 +496,7 @@ export class InteractiveMode {
492
496
  * Set hook status text in the footer.
493
497
  */
494
498
  private setHookStatus(key: string, text: string | undefined): void {
495
- this.footer.setHookStatus(key, text);
499
+ this.statusLine.setHookStatus(key, text);
496
500
  this.ui.requestRender();
497
501
  }
498
502
 
@@ -934,7 +938,8 @@ export class InteractiveMode {
934
938
  await this.init();
935
939
  }
936
940
 
937
- this.footer.invalidate();
941
+ this.statusLine.invalidate();
942
+ this.updateEditorTopBorder();
938
943
 
939
944
  switch (event.type) {
940
945
  case "agent_start":
@@ -1039,7 +1044,8 @@ export class InteractiveMode {
1039
1044
  }
1040
1045
  this.streamingComponent = undefined;
1041
1046
  this.streamingMessage = undefined;
1042
- this.footer.invalidate();
1047
+ this.statusLine.invalidate();
1048
+ this.updateEditorTopBorder();
1043
1049
  }
1044
1050
  this.ui.requestRender();
1045
1051
  break;
@@ -1147,7 +1153,8 @@ export class InteractiveMode {
1147
1153
  summary: event.result.summary,
1148
1154
  timestamp: Date.now(),
1149
1155
  });
1150
- this.footer.invalidate();
1156
+ this.statusLine.invalidate();
1157
+ this.updateEditorTopBorder();
1151
1158
  }
1152
1159
  this.ui.requestRender();
1153
1160
  break;
@@ -1324,7 +1331,7 @@ export class InteractiveMode {
1324
1331
  this.pendingTools.clear();
1325
1332
 
1326
1333
  if (options.updateFooter) {
1327
- this.footer.invalidate();
1334
+ this.statusLine.invalidate();
1328
1335
  this.updateEditorBorderColor();
1329
1336
  }
1330
1337
 
@@ -1492,17 +1499,24 @@ export class InteractiveMode {
1492
1499
  const level = this.session.thinkingLevel || "off";
1493
1500
  this.editor.borderColor = theme.getThinkingBorderColor(level);
1494
1501
  }
1502
+ // Update footer content in editor's top border
1503
+ this.updateEditorTopBorder();
1495
1504
  this.ui.requestRender();
1496
1505
  }
1497
1506
 
1507
+ private updateEditorTopBorder(): void {
1508
+ const width = this.ui.getWidth();
1509
+ const topBorder = this.statusLine.getTopBorder(width);
1510
+ this.editor.setTopBorder(topBorder);
1511
+ }
1512
+
1498
1513
  private cycleThinkingLevel(): void {
1499
1514
  const newLevel = this.session.cycleThinkingLevel();
1500
1515
  if (newLevel === undefined) {
1501
1516
  this.showStatus("Current model does not support thinking");
1502
1517
  } else {
1503
- this.footer.invalidate();
1518
+ this.statusLine.invalidate();
1504
1519
  this.updateEditorBorderColor();
1505
- this.showStatus(`Thinking level: ${newLevel}`);
1506
1520
  }
1507
1521
  }
1508
1522
 
@@ -1513,7 +1527,7 @@ export class InteractiveMode {
1513
1527
  const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
1514
1528
  this.showStatus(msg);
1515
1529
  } else {
1516
- this.footer.invalidate();
1530
+ this.statusLine.invalidate();
1517
1531
  this.updateEditorBorderColor();
1518
1532
  const thinkingStr =
1519
1533
  result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
@@ -1749,7 +1763,7 @@ export class InteractiveMode {
1749
1763
  // Session-managed settings (not in SettingsManager)
1750
1764
  case "autoCompact":
1751
1765
  this.session.setAutoCompactionEnabled(value as boolean);
1752
- this.footer.setAutoCompactEnabled(value as boolean);
1766
+ this.statusLine.setAutoCompactEnabled(value as boolean);
1753
1767
  break;
1754
1768
  case "queueMode":
1755
1769
  this.session.setQueueMode(value as "all" | "one-at-a-time");
@@ -1759,7 +1773,7 @@ export class InteractiveMode {
1759
1773
  break;
1760
1774
  case "thinkingLevel":
1761
1775
  this.session.setThinkingLevel(value as ThinkingLevel);
1762
- this.footer.invalidate();
1776
+ this.statusLine.invalidate();
1763
1777
  this.updateEditorBorderColor();
1764
1778
  break;
1765
1779
 
@@ -1808,7 +1822,7 @@ export class InteractiveMode {
1808
1822
  // Only update agent state for default role
1809
1823
  if (role === "default") {
1810
1824
  await this.session.setModel(model, role);
1811
- this.footer.invalidate();
1825
+ this.statusLine.invalidate();
1812
1826
  this.updateEditorBorderColor();
1813
1827
  }
1814
1828
  // For other roles (small), just show status - settings already updated by selector
@@ -2394,6 +2408,10 @@ export class InteractiveMode {
2394
2408
  // New session via session (emits hook and tool session events)
2395
2409
  await this.session.newSession();
2396
2410
 
2411
+ // Update status line (token counts, cost reset)
2412
+ this.statusLine.invalidate();
2413
+ this.updateEditorTopBorder();
2414
+
2397
2415
  // Clear UI state
2398
2416
  this.chatContainer.clear();
2399
2417
  this.pendingMessagesContainer.clear();
@@ -2533,7 +2551,8 @@ export class InteractiveMode {
2533
2551
  const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
2534
2552
  this.addMessageToChat(msg);
2535
2553
 
2536
- this.footer.invalidate();
2554
+ this.statusLine.invalidate();
2555
+ this.updateEditorTopBorder();
2537
2556
  } catch (error) {
2538
2557
  const message = error instanceof Error ? error.message : String(error);
2539
2558
  if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
@@ -2553,7 +2572,7 @@ export class InteractiveMode {
2553
2572
  this.loadingAnimation.stop();
2554
2573
  this.loadingAnimation = undefined;
2555
2574
  }
2556
- this.footer.dispose();
2575
+ this.statusLine.dispose();
2557
2576
  if (this.unsubscribe) {
2558
2577
  this.unsubscribe();
2559
2578
  }
@@ -79,19 +79,19 @@
79
79
 
80
80
  "bashMode": "cyan",
81
81
 
82
- "footerIcon": 39,
83
- "footerSep": 244,
84
- "footerModel": 255,
85
- "footerPath": 39,
86
- "footerBranch": 76,
87
- "footerStaged": 70,
88
- "footerDirty": 178,
89
- "footerUntracked": 39,
90
- "footerInput": 76,
91
- "footerOutput": 205,
92
- "footerCacheRead": 76,
93
- "footerCacheWrite": 76,
94
- "footerCost": 205
82
+ "statusLineBg": "#121212",
83
+ "statusLineSep": 244,
84
+ "statusLineModel": "#d787af",
85
+ "statusLinePath": "#00afaf",
86
+ "statusLineGitClean": "#5faf5f",
87
+ "statusLineGitDirty": "#d7af5f",
88
+ "statusLineContext": "#8787af",
89
+ "statusLineSpend": "#5fafaf",
90
+ "statusLineStaged": 70,
91
+ "statusLineDirty": 178,
92
+ "statusLineUntracked": 39,
93
+ "statusLineOutput": 205,
94
+ "statusLineCost": 205
95
95
  },
96
96
  "export": {
97
97
  "pageBg": "#18181e",
@@ -76,19 +76,19 @@
76
76
 
77
77
  "bashMode": "green",
78
78
 
79
- "footerIcon": 31,
80
- "footerSep": 246,
81
- "footerModel": 236,
82
- "footerPath": 31,
83
- "footerBranch": 28,
84
- "footerStaged": 28,
85
- "footerDirty": 136,
86
- "footerUntracked": 31,
87
- "footerInput": 28,
88
- "footerOutput": 133,
89
- "footerCacheRead": 28,
90
- "footerCacheWrite": 28,
91
- "footerCost": 133
79
+ "statusLineBg": "#e0e0e0",
80
+ "statusLineSep": "#808080",
81
+ "statusLineModel": "#875f87",
82
+ "statusLinePath": "#005f87",
83
+ "statusLineGitClean": "#005f00",
84
+ "statusLineGitDirty": "#af5f00",
85
+ "statusLineContext": "#5f5f87",
86
+ "statusLineSpend": "#005f5f",
87
+ "statusLineStaged": 28,
88
+ "statusLineDirty": 136,
89
+ "statusLineUntracked": 31,
90
+ "statusLineOutput": 133,
91
+ "statusLineCost": 133
92
92
  },
93
93
  "export": {
94
94
  "pageBg": "#f8f8f8",
@@ -85,20 +85,20 @@ const ThemeJsonSchema = Type.Object({
85
85
  thinkingXhigh: ColorValueSchema,
86
86
  // Bash Mode (1 color)
87
87
  bashMode: ColorValueSchema,
88
- // Footer Status Line (10 colors)
89
- footerIcon: ColorValueSchema,
90
- footerSep: ColorValueSchema,
91
- footerModel: ColorValueSchema,
92
- footerPath: ColorValueSchema,
93
- footerBranch: ColorValueSchema,
94
- footerStaged: ColorValueSchema,
95
- footerDirty: ColorValueSchema,
96
- footerUntracked: ColorValueSchema,
97
- footerInput: ColorValueSchema,
98
- footerOutput: ColorValueSchema,
99
- footerCacheRead: ColorValueSchema,
100
- footerCacheWrite: ColorValueSchema,
101
- footerCost: ColorValueSchema,
88
+ // Footer Status Line
89
+ statusLineBg: ColorValueSchema,
90
+ statusLineSep: ColorValueSchema,
91
+ statusLineModel: ColorValueSchema,
92
+ statusLinePath: ColorValueSchema,
93
+ statusLineGitClean: ColorValueSchema,
94
+ statusLineGitDirty: ColorValueSchema,
95
+ statusLineContext: ColorValueSchema,
96
+ statusLineSpend: ColorValueSchema,
97
+ statusLineStaged: ColorValueSchema,
98
+ statusLineDirty: ColorValueSchema,
99
+ statusLineUntracked: ColorValueSchema,
100
+ statusLineOutput: ColorValueSchema,
101
+ statusLineCost: ColorValueSchema,
102
102
  }),
103
103
  export: Type.Optional(
104
104
  Type.Object({
@@ -160,19 +160,18 @@ export type ThemeColor =
160
160
  | "thinkingHigh"
161
161
  | "thinkingXhigh"
162
162
  | "bashMode"
163
- | "footerIcon"
164
- | "footerSep"
165
- | "footerModel"
166
- | "footerPath"
167
- | "footerBranch"
168
- | "footerStaged"
169
- | "footerDirty"
170
- | "footerUntracked"
171
- | "footerInput"
172
- | "footerOutput"
173
- | "footerCacheRead"
174
- | "footerCacheWrite"
175
- | "footerCost";
163
+ | "statusLineSep"
164
+ | "statusLineModel"
165
+ | "statusLinePath"
166
+ | "statusLineGitClean"
167
+ | "statusLineGitDirty"
168
+ | "statusLineContext"
169
+ | "statusLineSpend"
170
+ | "statusLineStaged"
171
+ | "statusLineDirty"
172
+ | "statusLineUntracked"
173
+ | "statusLineOutput"
174
+ | "statusLineCost";
176
175
 
177
176
  export type ThemeBg =
178
177
  | "selectedBg"
@@ -180,7 +179,8 @@ export type ThemeBg =
180
179
  | "customMessageBg"
181
180
  | "toolPendingBg"
182
181
  | "toolSuccessBg"
183
- | "toolErrorBg";
182
+ | "toolErrorBg"
183
+ | "statusLineBg";
184
184
 
185
185
  type ColorMode = "truecolor" | "256color";
186
186
 
@@ -536,6 +536,7 @@ function createTheme(themeJson: ThemeJson, mode?: ColorMode): Theme {
536
536
  "toolPendingBg",
537
537
  "toolSuccessBg",
538
538
  "toolErrorBg",
539
+ "statusLineBg",
539
540
  ]);
540
541
  for (const [key, value] of Object.entries(resolvedColors)) {
541
542
  if (bgColorKeys.has(key)) {