@oh-my-pi/pi-coding-agent 14.1.0 → 14.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +8 -8
  3. package/src/async/job-manager.ts +43 -10
  4. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  5. package/src/config/mcp-schema.json +1 -1
  6. package/src/config/model-equivalence.ts +1 -0
  7. package/src/config/model-registry.ts +63 -34
  8. package/src/config/model-resolver.ts +111 -15
  9. package/src/config/settings-schema.ts +4 -3
  10. package/src/config/settings.ts +1 -1
  11. package/src/cursor.ts +64 -23
  12. package/src/edit/index.ts +254 -89
  13. package/src/edit/modes/chunk.ts +336 -57
  14. package/src/edit/modes/hashline.ts +51 -26
  15. package/src/edit/modes/patch.ts +16 -10
  16. package/src/edit/modes/replace.ts +15 -7
  17. package/src/edit/renderer.ts +248 -94
  18. package/src/export/html/template.generated.ts +1 -1
  19. package/src/export/html/template.js +6 -4
  20. package/src/extensibility/custom-tools/types.ts +0 -3
  21. package/src/extensibility/extensions/loader.ts +16 -0
  22. package/src/extensibility/extensions/runner.ts +2 -7
  23. package/src/extensibility/extensions/types.ts +8 -4
  24. package/src/internal-urls/docs-index.generated.ts +3 -3
  25. package/src/ipy/executor.ts +447 -52
  26. package/src/ipy/kernel.ts +39 -13
  27. package/src/lsp/client.ts +54 -0
  28. package/src/lsp/index.ts +8 -0
  29. package/src/lsp/types.ts +6 -0
  30. package/src/main.ts +0 -1
  31. package/src/modes/acp/acp-agent.ts +4 -1
  32. package/src/modes/components/bash-execution.ts +16 -4
  33. package/src/modes/components/status-line/presets.ts +17 -6
  34. package/src/modes/components/status-line/segments.ts +15 -0
  35. package/src/modes/components/status-line-segment-editor.ts +1 -0
  36. package/src/modes/components/status-line.ts +7 -1
  37. package/src/modes/components/tool-execution.ts +145 -75
  38. package/src/modes/controllers/command-controller.ts +24 -1
  39. package/src/modes/controllers/event-controller.ts +4 -1
  40. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  41. package/src/modes/controllers/input-controller.ts +9 -3
  42. package/src/modes/controllers/selector-controller.ts +4 -1
  43. package/src/modes/interactive-mode.ts +19 -3
  44. package/src/modes/print-mode.ts +13 -4
  45. package/src/modes/prompt-action-autocomplete.ts +3 -5
  46. package/src/modes/rpc/rpc-mode.ts +8 -2
  47. package/src/modes/shared.ts +2 -2
  48. package/src/modes/types.ts +1 -0
  49. package/src/modes/utils/ui-helpers.ts +1 -0
  50. package/src/prompts/tools/bash.md +2 -2
  51. package/src/prompts/tools/chunk-edit.md +191 -163
  52. package/src/prompts/tools/hashline.md +11 -11
  53. package/src/prompts/tools/patch.md +10 -5
  54. package/src/prompts/tools/{await.md → poll.md} +1 -1
  55. package/src/prompts/tools/read-chunk.md +3 -3
  56. package/src/prompts/tools/task.md +2 -2
  57. package/src/prompts/tools/vim.md +98 -0
  58. package/src/sdk.ts +754 -724
  59. package/src/session/agent-session.ts +164 -34
  60. package/src/session/session-manager.ts +50 -4
  61. package/src/slash-commands/builtin-registry.ts +17 -0
  62. package/src/task/executor.ts +4 -4
  63. package/src/task/index.ts +3 -5
  64. package/src/task/types.ts +2 -2
  65. package/src/tools/bash.ts +26 -8
  66. package/src/tools/find.ts +5 -2
  67. package/src/tools/grep.ts +77 -8
  68. package/src/tools/index.ts +48 -19
  69. package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
  70. package/src/tools/python.ts +293 -278
  71. package/src/tools/submit-result.ts +5 -2
  72. package/src/tools/todo-write.ts +8 -2
  73. package/src/tools/vim.ts +966 -0
  74. package/src/utils/edit-mode.ts +2 -1
  75. package/src/utils/session-color.ts +55 -0
  76. package/src/utils/title-generator.ts +15 -6
  77. package/src/vim/buffer.ts +309 -0
  78. package/src/vim/commands.ts +382 -0
  79. package/src/vim/engine.ts +2426 -0
  80. package/src/vim/parser.ts +151 -0
  81. package/src/vim/render.ts +252 -0
  82. package/src/vim/types.ts +197 -0
package/src/ipy/kernel.ts CHANGED
@@ -49,6 +49,10 @@ interface KernelShutdownOptions {
49
49
  timeoutMs?: number;
50
50
  }
51
51
 
52
+ export interface KernelShutdownResult {
53
+ confirmed: boolean;
54
+ }
55
+
52
56
  function getRemainingTimeMs(deadlineMs?: number): number | undefined {
53
57
  if (deadlineMs === undefined) return undefined;
54
58
  return Math.max(0, deadlineMs - Date.now());
@@ -399,10 +403,11 @@ export class PythonKernel {
399
403
  #ws: WebSocket | null = null;
400
404
  #disposed = false;
401
405
  #alive = true;
406
+ #shutdownStarted = false;
407
+ #shutdownConfirmed = false;
402
408
  #messageHandlers = new Map<string, (msg: JupyterMessage) => void>();
403
409
  #channelHandlers = new Map<string, Set<(msg: JupyterMessage) => void>>();
404
410
  #pendingExecutions = new Map<string, (reason: string) => void>();
405
-
406
411
  private constructor(
407
412
  readonly id: string,
408
413
  readonly kernelId: string,
@@ -1004,11 +1009,18 @@ export class PythonKernel {
1004
1009
  }
1005
1010
  }
1006
1011
 
1007
- async shutdown(options?: KernelShutdownOptions): Promise<void> {
1008
- if (this.#disposed) return;
1009
- this.#disposed = true;
1010
- this.#alive = false;
1011
- this.#abortPendingExecutions("Kernel shutdown");
1012
+ async shutdown(options?: KernelShutdownOptions): Promise<KernelShutdownResult> {
1013
+ if (this.#shutdownConfirmed) return { confirmed: true };
1014
+ if (!this.#shutdownStarted) {
1015
+ this.#shutdownStarted = true;
1016
+ this.#alive = false;
1017
+ this.#abortPendingExecutions("Kernel shutdown");
1018
+
1019
+ if (this.#ws) {
1020
+ this.#ws.close();
1021
+ this.#ws = null;
1022
+ }
1023
+ }
1012
1024
 
1013
1025
  const shutdownSignal = combineAbortSignal(
1014
1026
  { signal: options?.signal },
@@ -1016,24 +1028,38 @@ export class PythonKernel {
1016
1028
  "Python kernel shutdown timed out",
1017
1029
  );
1018
1030
 
1031
+ let confirmed = false;
1019
1032
  try {
1020
- await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
1033
+ const response = await fetch(`${this.gatewayUrl}/api/kernels/${this.kernelId}`, {
1021
1034
  method: "DELETE",
1022
1035
  headers: this.#authHeaders(),
1023
1036
  signal: shutdownSignal,
1024
1037
  });
1038
+ const deleteConfirmed = response.status === 404 || response.status === 410;
1039
+ confirmed = response.ok || deleteConfirmed;
1040
+ if (!confirmed) {
1041
+ logger.warn("Kernel delete request was not confirmed", {
1042
+ status: response.status,
1043
+ statusText: response.statusText,
1044
+ });
1045
+ }
1025
1046
  } catch (err: unknown) {
1026
1047
  logger.warn("Failed to delete kernel via API", { error: err instanceof Error ? err.message : String(err) });
1027
1048
  }
1028
-
1029
- if (this.#ws) {
1030
- this.#ws.close();
1031
- this.#ws = null;
1032
- }
1049
+ this.#shutdownConfirmed = confirmed;
1050
+ this.#disposed = confirmed;
1033
1051
 
1034
1052
  if (this.isSharedGateway) {
1035
- await releaseSharedGateway();
1053
+ try {
1054
+ await releaseSharedGateway();
1055
+ } catch (err: unknown) {
1056
+ logger.warn("Failed to release shared gateway after kernel shutdown", {
1057
+ error: err instanceof Error ? err.message : String(err),
1058
+ });
1059
+ }
1036
1060
  }
1061
+
1062
+ return { confirmed };
1037
1063
  }
1038
1064
 
1039
1065
  #sendMessage(msg: JupyterMessage): void {
package/src/lsp/client.ts CHANGED
@@ -136,6 +136,9 @@ const CLIENT_CAPABILITIES = {
136
136
  dataSupport: true,
137
137
  },
138
138
  },
139
+ window: {
140
+ workDoneProgress: true,
141
+ },
139
142
  workspace: {
140
143
  applyEdit: true,
141
144
  workspaceEdit: {
@@ -267,6 +270,16 @@ async function startMessageReader(client: LspClient): Promise<void> {
267
270
  version: params.version ?? null,
268
271
  });
269
272
  client.diagnosticsVersion += 1;
273
+ } else if (message.method === "$/progress" && message.params) {
274
+ const params = message.params as { token: string | number; value?: { kind?: string } };
275
+ if (params.value?.kind === "begin") {
276
+ client.activeProgressTokens.add(params.token);
277
+ } else if (params.value?.kind === "end") {
278
+ client.activeProgressTokens.delete(params.token);
279
+ if (client.activeProgressTokens.size === 0) {
280
+ client.resolveProjectLoaded();
281
+ }
282
+ }
270
283
  }
271
284
  }
272
285
 
@@ -338,6 +351,13 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
338
351
  await handleApplyEditRequest(client, message);
339
352
  return;
340
353
  }
354
+ if (message.method === "window/workDoneProgress/create") {
355
+ // Accept progress token registration from the server
356
+ if (typeof message.id === "number") {
357
+ await sendResponse(client, message.id, null, message.method);
358
+ }
359
+ return;
360
+ }
341
361
  if (typeof message.id !== "number") return;
342
362
  await sendResponse(client, message.id, null, message.method, {
343
363
  code: -32601,
@@ -375,6 +395,9 @@ async function sendResponse(
375
395
  /** Timeout for warmup initialize requests (5 seconds) */
376
396
  export const WARMUP_TIMEOUT_MS = 5000;
377
397
 
398
+ /** Max time to wait for the server to report project loading completion via $/progress */
399
+ const PROJECT_LOAD_TIMEOUT_MS = 15_000;
400
+
378
401
  /**
379
402
  * Get or create an LSP client for the given server configuration and working directory.
380
403
  * @param config - Server configuration
@@ -413,6 +436,18 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
413
436
  env: env ? { ...Bun.env, ...env } : undefined,
414
437
  });
415
438
 
439
+ let resolveProjectLoaded!: () => void;
440
+ const projectLoaded = new Promise<void>(resolve => {
441
+ resolveProjectLoaded = resolve;
442
+ });
443
+ // Auto-resolve after timeout in case server doesn't use progress tokens
444
+ const projectLoadTimeout = setTimeout(resolveProjectLoaded, PROJECT_LOAD_TIMEOUT_MS);
445
+ const originalResolve = resolveProjectLoaded;
446
+ resolveProjectLoaded = () => {
447
+ clearTimeout(projectLoadTimeout);
448
+ originalResolve();
449
+ };
450
+
416
451
  const client: LspClient = {
417
452
  name: key,
418
453
  cwd,
@@ -426,6 +461,9 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
426
461
  messageBuffer: new Uint8Array(0),
427
462
  isReading: false,
428
463
  lastActivity: Date.now(),
464
+ activeProgressTokens: new Set(),
465
+ projectLoaded,
466
+ resolveProjectLoaded,
429
467
  };
430
468
  clients.set(key, client);
431
469
 
@@ -433,6 +471,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
433
471
  proc.exited.then(() => {
434
472
  clients.delete(key);
435
473
  clientLocks.delete(key);
474
+ client.resolveProjectLoaded();
436
475
 
437
476
  // Reject any pending requests — the server is gone, they will never complete.
438
477
  if (client.pendingRequests.size > 0) {
@@ -554,6 +593,21 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
554
593
  }
555
594
  }
556
595
 
596
+ /**
597
+ * Wait for the server's initial project loading to complete.
598
+ * Races the server's $/progress tracking against the abort signal.
599
+ * Returns immediately if loading already completed or timed out.
600
+ */
601
+ export async function waitForProjectLoaded(client: LspClient, signal?: AbortSignal): Promise<void> {
602
+ if (signal?.aborted) return;
603
+ await Promise.race([
604
+ client.projectLoaded,
605
+ ...(signal
606
+ ? [new Promise<void>(resolve => signal.addEventListener("abort", () => resolve(), { once: true }))]
607
+ : []),
608
+ ]);
609
+ }
610
+
557
611
  /**
558
612
  * Sync in-memory content to the LSP client without reading from disk.
559
613
  * Use this to provide instant feedback during edits before the file is saved.
package/src/lsp/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  setIdleTimeout,
21
21
  syncContent,
22
22
  WARMUP_TIMEOUT_MS,
23
+ waitForProjectLoaded,
23
24
  } from "./client";
24
25
  import { getLinterClient } from "./clients";
25
26
  import { getServersForFile, type LspConfig, loadConfig } from "./config";
@@ -1427,6 +1428,13 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1427
1428
 
1428
1429
  let output: string;
1429
1430
 
1431
+ // Wait for project loading to complete before cross-file operations
1432
+ // to ensure the server has indexed all project files.
1433
+ const crossFileActions = new Set(["definition", "type_definition", "implementation", "references", "rename"]);
1434
+ if (crossFileActions.has(action)) {
1435
+ await waitForProjectLoaded(client, signal);
1436
+ }
1437
+
1430
1438
  switch (action) {
1431
1439
  // =====================================================================
1432
1440
  // Standard LSP Operations
package/src/lsp/types.ts CHANGED
@@ -411,6 +411,12 @@ export interface LspClient {
411
411
  isReading: boolean;
412
412
  serverCapabilities?: LspServerCapabilities;
413
413
  lastActivity: number;
414
+ /** Tracks active work-done progress tokens from the server */
415
+ activeProgressTokens: Set<string | number>;
416
+ /** Resolves when the server's initial project loading completes (or after timeout) */
417
+ projectLoaded: Promise<void>;
418
+ /** Call to signal that project loading has completed */
419
+ resolveProjectLoaded: () => void;
414
420
  }
415
421
 
416
422
  // =============================================================================
package/src/main.ts CHANGED
@@ -839,7 +839,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
839
839
  settings: nextSettings,
840
840
  authStorage,
841
841
  modelRegistry,
842
- searchDb: session.searchDb,
843
842
  hasUI: false,
844
843
  });
845
844
  if (nextSession.extensionRunner) {
@@ -1148,10 +1148,13 @@ export class AcpAgent implements Agent {
1148
1148
  },
1149
1149
  getThinkingLevel: () => record.session.thinkingLevel,
1150
1150
  setThinkingLevel: level => record.session.setThinkingLevel(level),
1151
+ getSessionName: () => record.session.sessionManager.getSessionName(),
1152
+ setSessionName: async name => {
1153
+ await record.session.sessionManager.setSessionName(name, "user");
1154
+ },
1151
1155
  },
1152
1156
  {
1153
1157
  getModel: () => record.session.model,
1154
- getSearchDb: () => record.session.searchDb,
1155
1158
  isIdle: () => !record.session.isStreaming,
1156
1159
  abort: () => {
1157
1160
  void record.session.abort();
@@ -3,7 +3,18 @@
3
3
  */
4
4
 
5
5
  import { sanitizeText } from "@oh-my-pi/pi-natives";
6
- import { Container, ImageProtocol, Loader, Spacer, TERMINAL, Text, type TUI } from "@oh-my-pi/pi-tui";
6
+ import {
7
+ Container,
8
+ Ellipsis,
9
+ ImageProtocol,
10
+ Loader,
11
+ Spacer,
12
+ TERMINAL,
13
+ Text,
14
+ type TUI,
15
+ truncateToWidth,
16
+ visibleWidth,
17
+ } from "@oh-my-pi/pi-tui";
7
18
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
8
19
  import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
9
20
  import { getSixelLineMask, isSixelPassthroughEnabled, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
@@ -210,11 +221,12 @@ export class BashExecutionComponent extends Container {
210
221
  }
211
222
 
212
223
  #clampDisplayLine(line: string): string {
213
- if (line.length <= MAX_DISPLAY_LINE_CHARS) {
224
+ const visible = visibleWidth(line);
225
+ if (visible <= MAX_DISPLAY_LINE_CHARS) {
214
226
  return line;
215
227
  }
216
- const omitted = line.length - MAX_DISPLAY_LINE_CHARS;
217
- return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
228
+ const omitted = visible - MAX_DISPLAY_LINE_CHARS;
229
+ return `${truncateToWidth(line, MAX_DISPLAY_LINE_CHARS, Ellipsis.Omit)}… [${omitted} visible columns omitted]`;
218
230
  }
219
231
 
220
232
  #clampLinesPreservingSixel(lines: string[]): string[] {
@@ -3,7 +3,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
3
3
  export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
4
4
  default: {
5
5
  leftSegments: ["pi", "model", "plan_mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
6
- rightSegments: [],
6
+ rightSegments: ["session_name"],
7
7
  separator: "powerline-thin",
8
8
  segmentOptions: {
9
9
  model: { showThinkingLevel: true },
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
14
14
 
15
15
  minimal: {
16
16
  leftSegments: ["path", "git"],
17
- rightSegments: ["plan_mode", "context_pct"],
17
+ rightSegments: ["session_name", "plan_mode", "context_pct"],
18
18
  separator: "slash",
19
19
  segmentOptions: {
20
20
  path: { abbreviate: true, maxLength: 30 },
@@ -24,7 +24,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
24
24
 
25
25
  compact: {
26
26
  leftSegments: ["model", "plan_mode", "git", "pr"],
27
- rightSegments: ["cost", "context_pct"],
27
+ rightSegments: ["session_name", "cost", "context_pct"],
28
28
  separator: "powerline-thin",
29
29
  segmentOptions: {
30
30
  model: { showThinkingLevel: false },
@@ -34,7 +34,17 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
34
34
 
35
35
  full: {
36
36
  leftSegments: ["pi", "hostname", "model", "plan_mode", "path", "git", "pr", "subagents"],
37
- rightSegments: ["token_in", "token_out", "token_rate", "cache_read", "cost", "context_pct", "time_spent", "time"],
37
+ rightSegments: [
38
+ "session_name",
39
+ "token_in",
40
+ "token_out",
41
+ "token_rate",
42
+ "cache_read",
43
+ "cost",
44
+ "context_pct",
45
+ "time_spent",
46
+ "time",
47
+ ],
38
48
  separator: "powerline",
39
49
  segmentOptions: {
40
50
  model: { showThinkingLevel: true },
@@ -48,6 +58,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
48
58
  // Full preset with all Nerd Font icons
49
59
  leftSegments: ["pi", "hostname", "model", "plan_mode", "path", "git", "pr", "session", "subagents"],
50
60
  rightSegments: [
61
+ "session_name",
51
62
  "token_in",
52
63
  "token_out",
53
64
  "cache_read",
@@ -71,7 +82,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
71
82
  ascii: {
72
83
  // No Nerd Font dependencies
73
84
  leftSegments: ["model", "plan_mode", "path", "git", "pr"],
74
- rightSegments: ["token_total", "cost", "context_pct"],
85
+ rightSegments: ["session_name", "token_total", "cost", "context_pct"],
75
86
  separator: "ascii",
76
87
  segmentOptions: {
77
88
  model: { showThinkingLevel: true },
@@ -83,7 +94,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
83
94
  custom: {
84
95
  // User-defined - these are just defaults that get overridden
85
96
  leftSegments: ["model", "plan_mode", "path", "git", "pr"],
86
- rightSegments: ["token_total", "cost", "context_pct"],
97
+ rightSegments: ["session_name", "token_total", "cost", "context_pct"],
87
98
  separator: "powerline-thin",
88
99
  segmentOptions: {},
89
100
  },
@@ -5,6 +5,8 @@ import { TERMINAL } from "@oh-my-pi/pi-tui";
5
5
  import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
6
6
  import { theme } from "../../../modes/theme/theme";
7
7
  import { shortenPath } from "../../../tools/render-utils";
8
+ import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
9
+ import { sanitizeStatusText } from "../../shared";
8
10
  import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
9
11
  import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
10
12
 
@@ -354,6 +356,18 @@ const cacheWriteSegment: StatusLineSegment = {
354
356
  },
355
357
  };
356
358
 
359
+ const sessionNameSegment: StatusLineSegment = {
360
+ id: "session_name",
361
+ render(ctx) {
362
+ const sessionManager = ctx.session.sessionManager;
363
+ const name = sessionManager?.titleSource === "auto" ? undefined : sessionManager?.getSessionName();
364
+ if (!name) return { content: "", visible: false };
365
+
366
+ const ansi = getSessionAccentAnsi(getSessionAccentHex(name)) ?? theme.getFgAnsi("accent");
367
+ return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
368
+ },
369
+ };
370
+
357
371
  // ═══════════════════════════════════════════════════════════════════════════
358
372
  // Segment Registry
359
373
  // ═══════════════════════════════════════════════════════════════════════════
@@ -379,6 +393,7 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
379
393
  hostname: hostnameSegment,
380
394
  cache_read: cacheReadSegment,
381
395
  cache_write: cacheWriteSegment,
396
+ session_name: sessionNameSegment,
382
397
  };
383
398
 
384
399
  export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
@@ -36,6 +36,7 @@ const SEGMENT_INFO: Record<StatusLineSegmentId, { label: string; short: string }
36
36
  hostname: { label: "Host", short: "hostname" },
37
37
  cache_read: { label: "Cache ↓", short: "cache read" },
38
38
  cache_write: { label: "Cache ↑", short: "cache write" },
39
+ session_name: { label: "Session Name", short: "named session" },
39
40
  };
40
41
 
41
42
  type Column = "left" | "right" | "disabled";
@@ -9,6 +9,7 @@ import { theme } from "../../modes/theme/theme";
9
9
  import type { AgentSession } from "../../session/agent-session";
10
10
  import { calculatePromptTokens } from "../../session/compaction/compaction";
11
11
  import * as git from "../../utils/git";
12
+ import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../../utils/session-color";
12
13
  import { sanitizeStatusText } from "../shared";
13
14
  import {
14
15
  canReuseCachedPr,
@@ -471,7 +472,12 @@ export class StatusLineComponent implements Component {
471
472
  leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
472
473
  rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
473
474
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
474
- const gapFill = theme.fg("border", theme.boxRound.horizontal.repeat(gapWidth));
475
+ const accentHex = getSessionAccentHexForTitle(
476
+ this.session.sessionManager?.getSessionName(),
477
+ this.session.sessionManager?.titleSource,
478
+ );
479
+ const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
480
+ const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
475
481
  return leftGroup + gapFill + rightGroup;
476
482
  }
477
483