@gajae-code/coding-agent 0.16.1 → 0.16.4

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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
@@ -1,7 +1,9 @@
1
1
  import * as fs from "node:fs";
2
2
 
3
+ import { resolveOAuthStorageProvider } from "@gajae-code/ai/core";
3
4
  import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
4
- import { formatCount, getProjectDir } from "@gajae-code/utils";
5
+ import { formatCount, getProjectDir, logger } from "@gajae-code/utils";
6
+ import { getShellConfig } from "@gajae-code/utils/shell-config";
5
7
  import {
6
8
  type AppKeybinding,
7
9
  KEYBINDINGS,
@@ -19,6 +21,11 @@ import type { ActionRegistry, FocusDomain } from "../action-registry";
19
21
  import { EMPTY_JOBS_SNAPSHOT, type JobsSnapshot } from "../jobs-observer";
20
22
  import { sanitizeStatusText } from "../shared";
21
23
  import { renderSkillHudBar } from "./skill-hud/render";
24
+ import {
25
+ normalizeStatusLineCommandOptions,
26
+ runStatusLineCommand,
27
+ type StatusLineCommandOptions,
28
+ } from "./status-line/command";
22
29
  import { lookupCurrentPrCached } from "./status-line/gh";
23
30
  import {
24
31
  canReuseCachedPr,
@@ -40,6 +47,7 @@ export interface StatusLineSegmentOptions {
40
47
  git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
41
48
  time?: { format?: "12h" | "24h"; showSeconds?: boolean };
42
49
  usage?: { mode?: "used" | "remaining" };
50
+ command?: StatusLineCommandOptions;
43
51
  }
44
52
 
45
53
  export interface StatusLineSettings {
@@ -49,6 +57,7 @@ export interface StatusLineSettings {
49
57
  separator?: StatusLineSeparatorStyle;
50
58
  segmentOptions?: StatusLineSegmentOptions;
51
59
  previewHighlightSegment?: StatusLineSegmentId;
60
+ previewHighlightStyle?: "focus" | "selected";
52
61
  showHookStatus?: boolean;
53
62
  showSkillHud?: boolean;
54
63
  showActionHints?: boolean;
@@ -62,6 +71,7 @@ export interface StatusLineComponentOptions {
62
71
  getKeybindings?: () => KeybindingsManager;
63
72
  focusDomain?: FocusDomain;
64
73
  keyDisplayContext?: KeyDisplayContext;
74
+ onUpdate?: () => void;
65
75
  }
66
76
 
67
77
  export interface StatusLineActionHint {
@@ -69,6 +79,14 @@ export interface StatusLineActionHint {
69
79
  content: string;
70
80
  }
71
81
 
82
+ export interface StatusLinePreviewParts {
83
+ left: string[];
84
+ leftIds: StatusLineSegmentId[];
85
+ right: string[];
86
+ rightIds: (StatusLineSegmentId | null)[];
87
+ separator: SeparatorDef;
88
+ }
89
+
72
90
  const ACTION_HINT_PRIORITY: readonly AppKeybinding[] = [
73
91
  "app.message.sendNow",
74
92
  "app.message.followUp",
@@ -136,6 +154,7 @@ interface CollectedStatusSegments {
136
154
  /** Parallel to `right`; null for action hints, job counts and the version tag. */
137
155
  rightSegIds: (StatusLineSegmentId | null)[];
138
156
  previewHighlightSegment: StatusLineSegmentId | undefined;
157
+ previewHighlightStyle: "focus" | "selected";
139
158
  sessionAccent: boolean | undefined;
140
159
  leftSepWidth: number;
141
160
  rightSepWidth: number;
@@ -155,6 +174,8 @@ export class StatusLineComponent implements Component {
155
174
  #branchInFlight = false;
156
175
  #gitWatcher: fs.FSWatcher | null = null;
157
176
  #onBranchChange: (() => void) | null = null;
177
+ #onUpdate: (() => void) | null = null;
178
+ #disposed = false;
158
179
  #autoCompactEnabled: boolean = true;
159
180
  #hookStatuses: Map<string, string> = new Map();
160
181
  #subagentCount: number = 0;
@@ -195,9 +216,22 @@ export class StatusLineComponent implements Component {
195
216
 
196
217
  // Provider usage caching (5-min TTL, OAuth/sub only)
197
218
  #cachedUsage: SegmentContext["usage"] = null;
219
+ #cachedUsageReports: unknown = null;
220
+ #cachedUsageProvider: string | undefined;
198
221
  #usageFetchedAt = 0;
199
222
  #usageInFlight = false;
200
223
 
224
+ // Configured command segment cache. Command execution is always backgrounded;
225
+ // rendering only reads these bounded, last-known values.
226
+ #commandConfigKey: string | undefined;
227
+ #commandOutput: string | null = null;
228
+ #commandFailed = false;
229
+ #commandFetchedAt = 0;
230
+ #commandInFlightKey: string | undefined;
231
+ #commandAbortController: AbortController | undefined;
232
+ #commandRefreshTimer: ReturnType<typeof setTimeout> | undefined;
233
+ #commandDiagnosticKey: string | undefined;
234
+
201
235
  constructor(
202
236
  private readonly session: AgentSession,
203
237
  options: StatusLineComponentOptions = {},
@@ -218,6 +252,7 @@ export class StatusLineComponent implements Component {
218
252
  this.#getKeybindings = options.getKeybindings;
219
253
  this.#focusDomain = options.focusDomain ?? "composer";
220
254
  this.#keyDisplayContext = options.keyDisplayContext;
255
+ this.#onUpdate = options.onUpdate ?? null;
221
256
  }
222
257
 
223
258
  updateSettings(settings: StatusLineSettings): void {
@@ -300,12 +335,175 @@ export class StatusLineComponent implements Component {
300
335
  }
301
336
 
302
337
  dispose(): void {
338
+ this.#disposed = true;
339
+ this.#onBranchChange = null;
340
+ this.#onUpdate = null;
341
+ this.#cancelCommandExecution();
342
+ this.#clearCommandRefreshTimer();
303
343
  if (this.#gitWatcher) {
304
344
  this.#gitWatcher.close();
305
345
  this.#gitWatcher = null;
306
346
  }
307
347
  }
308
348
 
349
+ #notifyUpdate(): void {
350
+ if (this.#onUpdate) this.#onUpdate();
351
+ else this.#onBranchChange?.();
352
+ }
353
+
354
+ #logCommandDiagnostic(key: string, message: string, details: Record<string, unknown> = {}): void {
355
+ if (this.#commandDiagnosticKey === key) return;
356
+ this.#commandDiagnosticKey = key;
357
+ logger.warn(message, details);
358
+ }
359
+
360
+ #clearCommandRefreshTimer(): void {
361
+ if (this.#commandRefreshTimer) {
362
+ clearTimeout(this.#commandRefreshTimer);
363
+ this.#commandRefreshTimer = undefined;
364
+ }
365
+ }
366
+
367
+ #cancelCommandExecution(): void {
368
+ this.#commandAbortController?.abort();
369
+ this.#commandAbortController = undefined;
370
+ this.#commandInFlightKey = undefined;
371
+ }
372
+
373
+ #disableCommandSegment(): void {
374
+ this.#clearCommandRefreshTimer();
375
+ this.#cancelCommandExecution();
376
+ this.#commandConfigKey = undefined;
377
+ this.#commandOutput = null;
378
+ this.#commandFailed = false;
379
+ this.#commandFetchedAt = 0;
380
+ this.#commandDiagnosticKey = undefined;
381
+ }
382
+
383
+ #armCommandRefreshTimer(key: string, refreshMs: number): void {
384
+ this.#clearCommandRefreshTimer();
385
+ if (this.#disposed || this.#commandConfigKey !== key) return;
386
+ this.#commandRefreshTimer = setTimeout(
387
+ () => {
388
+ this.#commandRefreshTimer = undefined;
389
+ if (this.#disposed || this.#commandConfigKey !== key) return;
390
+ this.#notifyUpdate();
391
+ },
392
+ Math.max(0, refreshMs),
393
+ );
394
+ this.#commandRefreshTimer.unref?.();
395
+ }
396
+
397
+ #trustedCommandOptions(): StatusLineCommandOptions | undefined {
398
+ const globalLeft = this.session.settings.getGlobal("statusLine.leftSegments");
399
+ const globalRight = this.session.settings.getGlobal("statusLine.rightSegments");
400
+ const includesCommand = (value: unknown): boolean => Array.isArray(value) && value.includes("command");
401
+ if (!includesCommand(globalLeft) && !includesCommand(globalRight)) return undefined;
402
+
403
+ const globalSegmentOptions = this.session.settings.getGlobal("statusLine.segmentOptions");
404
+ if (!globalSegmentOptions || typeof globalSegmentOptions !== "object" || Array.isArray(globalSegmentOptions)) {
405
+ return undefined;
406
+ }
407
+ const commandOptions = (globalSegmentOptions as Record<string, unknown>).command;
408
+ const resolved = normalizeStatusLineCommandOptions(commandOptions);
409
+ return resolved.command ? resolved : undefined;
410
+ }
411
+
412
+ #refreshCommandInBackground(options: StatusLineCommandOptions | undefined): string {
413
+ const resolved = normalizeStatusLineCommandOptions(options);
414
+ const key = JSON.stringify(resolved);
415
+ if (this.#commandConfigKey !== key) {
416
+ this.#clearCommandRefreshTimer();
417
+ this.#cancelCommandExecution();
418
+ this.#commandConfigKey = key;
419
+ this.#commandOutput = null;
420
+ this.#commandFailed = false;
421
+ this.#commandFetchedAt = 0;
422
+ this.#commandDiagnosticKey = undefined;
423
+ this.#renderedRowsCache = undefined;
424
+ }
425
+
426
+ if (!resolved.command) {
427
+ this.#commandFailed = true;
428
+ this.#logCommandDiagnostic(key, "Status line command segment is configured without a command");
429
+ return key;
430
+ }
431
+
432
+ const now = Date.now();
433
+ if (this.#commandInFlightKey === key) return key;
434
+ const elapsed = now - this.#commandFetchedAt;
435
+ if (this.#commandFetchedAt > 0 && elapsed < resolved.refreshMs) {
436
+ this.#armCommandRefreshTimer(key, resolved.refreshMs - elapsed);
437
+ return key;
438
+ }
439
+ this.#clearCommandRefreshTimer();
440
+
441
+ this.#commandInFlightKey = key;
442
+ const abortController = new AbortController();
443
+ this.#commandAbortController = abortController;
444
+ let shellConfig: { shell: string; args: string[]; env: Record<string, string> };
445
+ try {
446
+ shellConfig = getShellConfig(this.session.settings.getGlobal("shellPath"));
447
+ } catch (error) {
448
+ if (this.#commandInFlightKey === key && this.#commandAbortController === abortController) {
449
+ this.#commandInFlightKey = undefined;
450
+ this.#commandAbortController = undefined;
451
+ }
452
+ this.#commandFailed = true;
453
+ this.#commandFetchedAt = Date.now();
454
+ this.#logCommandDiagnostic(key, "Status line command shell configuration failed", {
455
+ error: sanitizeStatusText(error instanceof Error ? error.message : String(error)).slice(0, 256),
456
+ });
457
+ this.#armCommandRefreshTimer(key, resolved.refreshMs);
458
+ return key;
459
+ }
460
+
461
+ void runStatusLineCommand(resolved.command, {
462
+ cwd: getProjectDir(),
463
+ shell: shellConfig.shell,
464
+ shellArgs: shellConfig.args,
465
+ env: shellConfig.env,
466
+ timeoutMs: resolved.timeoutMs,
467
+ signal: abortController.signal,
468
+ })
469
+ .then(result => {
470
+ if (this.#disposed || this.#commandConfigKey !== key) return;
471
+ this.#commandFetchedAt = Date.now();
472
+ if (result.timedOut || result.exitCode !== 0) {
473
+ this.#commandFailed = true;
474
+ this.#logCommandDiagnostic(key, "Status line command failed", {
475
+ exitCode: result.exitCode,
476
+ timedOut: result.timedOut,
477
+ stderr: result.stderr,
478
+ outputTruncated: result.outputTruncated,
479
+ });
480
+ } else {
481
+ this.#commandOutput = result.stdout;
482
+ this.#commandFailed = false;
483
+ this.#commandDiagnosticKey = undefined;
484
+ }
485
+ this.#renderedRowsCache = undefined;
486
+ this.#notifyUpdate();
487
+ })
488
+ .catch(error => {
489
+ if (this.#disposed || this.#commandConfigKey !== key) return;
490
+ this.#commandFetchedAt = Date.now();
491
+ this.#commandFailed = true;
492
+ this.#logCommandDiagnostic(key, "Status line command could not start", {
493
+ error: sanitizeStatusText(error instanceof Error ? error.message : String(error)).slice(0, 256),
494
+ });
495
+ this.#renderedRowsCache = undefined;
496
+ this.#notifyUpdate();
497
+ })
498
+ .finally(() => {
499
+ if (this.#commandInFlightKey !== key || this.#commandAbortController !== abortController) return;
500
+ this.#commandInFlightKey = undefined;
501
+ this.#commandAbortController = undefined;
502
+ this.#armCommandRefreshTimer(key, resolved.refreshMs);
503
+ });
504
+ return key;
505
+ }
506
+
309
507
  invalidate(): void {
310
508
  this.#invalidateGitCaches();
311
509
  }
@@ -502,6 +700,8 @@ export class StatusLineComponent implements Component {
502
700
  void fetcher
503
701
  .call(this.session)
504
702
  .then(reports => {
703
+ this.#cachedUsageReports = reports;
704
+ this.#cachedUsageProvider = this.#activeUsageProvider();
505
705
  this.#cachedUsage = this.#normalizeUsageReports(reports);
506
706
  this.#usageFetchedAt = Date.now();
507
707
  if (this.#onBranchChange) {
@@ -521,7 +721,9 @@ export class StatusLineComponent implements Component {
521
721
  }
522
722
 
523
723
  #normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
524
- if (!Array.isArray(reports)) return null;
724
+ const activeProvider = this.#activeUsageProvider();
725
+
726
+ if (!activeProvider || !Array.isArray(reports)) return null;
525
727
  const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
526
728
  const seen = new Set<string>();
527
729
  const now = Date.now();
@@ -555,20 +757,24 @@ export class StatusLineComponent implements Component {
555
757
  for (const report of reports) {
556
758
  if (!report || typeof report !== "object") continue;
557
759
  const provider = (report as { provider?: unknown }).provider;
558
- const providerId = typeof provider === "string" ? provider : undefined;
760
+ const providerId = typeof provider === "string" ? resolveOAuthStorageProvider(provider) : undefined;
761
+ if (providerId !== activeProvider) continue;
559
762
  const limits = (report as { limits?: unknown }).limits;
560
763
  if (!Array.isArray(limits)) continue;
561
764
  for (const limit of limits) {
562
765
  if (!limit || typeof limit !== "object") continue;
563
766
  const l = limit as {
564
767
  id?: unknown;
565
- scope?: { windowId?: string; tier?: string; modelId?: string };
768
+ scope?: { provider?: unknown; windowId?: string; tier?: string; modelId?: string };
566
769
  window?: { id?: string; resetsAt?: number };
567
770
  amount?: { usedFraction?: number };
568
771
  };
569
772
  const fraction = l.amount?.usedFraction;
570
773
  if (typeof fraction !== "number") continue;
571
774
  const id = typeof l.id === "string" ? l.id : "";
775
+ const scopeProvider =
776
+ typeof l.scope?.provider === "string" ? resolveOAuthStorageProvider(l.scope.provider) : undefined;
777
+ if (scopeProvider !== undefined && scopeProvider !== providerId) continue;
572
778
  const windowId = l.scope?.windowId ?? l.window?.id;
573
779
  const tier = l.scope?.tier;
574
780
  const modelId = l.scope?.modelId;
@@ -584,9 +790,9 @@ export class StatusLineComponent implements Component {
584
790
  }
585
791
  } else if (windowId === "5h" && !tier) {
586
792
  pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
587
- } else if (windowId === "7d" && !tier) {
793
+ } else if (windowId === "7d" && !tier && providerId !== "grok-build" && providerId !== "xai") {
588
794
  pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
589
- } else if (providerId === "grok-build" && windowId === "weekly" && !tier) {
795
+ } else if (providerId === "grok-build" && id === "grok-build:weekly" && windowId === "weekly" && !tier) {
590
796
  pushWindow("grok-build:weekly", "weekly", fraction, resetsAt, "h");
591
797
  }
592
798
  }
@@ -595,17 +801,32 @@ export class StatusLineComponent implements Component {
595
801
  return windows.length > 0 ? { windows } : null;
596
802
  }
597
803
 
804
+ #activeUsageProvider(): string | undefined {
805
+ const model = this.session.state.model ?? this.session.model;
806
+ if (!model || typeof model !== "object") return undefined;
807
+ const provider = (model as { provider?: unknown }).provider;
808
+ return typeof provider === "string" && provider.length > 0 ? resolveOAuthStorageProvider(provider) : undefined;
809
+ }
810
+
811
+ #syncCachedUsageForActiveProvider(): void {
812
+ const activeProvider = this.#activeUsageProvider();
813
+ if (this.#cachedUsageProvider === activeProvider) return;
814
+ this.#cachedUsageProvider = activeProvider;
815
+ this.#cachedUsage = this.#normalizeUsageReports(this.#cachedUsageReports);
816
+ }
817
+
598
818
  #buildSegmentContext(
599
819
  width: number,
600
820
  effectiveSettings: Required<
601
821
  Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
602
822
  > &
603
823
  StatusLineSettings,
824
+ previewOnly = false,
604
825
  ): SegmentContext {
605
826
  const state = this.session.state;
606
827
 
607
- // Trigger background fetch (5-min TTL); render uses cached value
608
- this.refreshUsageInBackground();
828
+ this.#syncCachedUsageForActiveProvider();
829
+ if (!previewOnly) this.refreshUsageInBackground();
609
830
 
610
831
  // Get usage statistics
611
832
  const aggregateUsageStats = this.session.sessionManager?.getUsageStatistics() ?? {
@@ -633,6 +854,18 @@ export class StatusLineComponent implements Component {
633
854
  // treats a null value as hidden, so gating here is behavior-identical.
634
855
  const prSegmentActive =
635
856
  effectiveSettings.leftSegments.includes("pr") || effectiveSettings.rightSegments.includes("pr");
857
+ const commandSegmentActive =
858
+ effectiveSettings.leftSegments.includes("command") || effectiveSettings.rightSegments.includes("command");
859
+ const commandOptions = commandSegmentActive ? this.#trustedCommandOptions() : undefined;
860
+ let commandKey: string | undefined = this.#commandConfigKey;
861
+ if (!previewOnly) {
862
+ if (commandOptions) {
863
+ commandKey = this.#refreshCommandInBackground(commandOptions);
864
+ } else {
865
+ this.#disableCommandSegment();
866
+ commandKey = undefined;
867
+ }
868
+ }
636
869
 
637
870
  return {
638
871
  session: this.session,
@@ -651,9 +884,19 @@ export class StatusLineComponent implements Component {
651
884
  git: {
652
885
  branch: this.#getCurrentBranch(),
653
886
  status: this.#getGitStatus(),
654
- pr: prSegmentActive ? this.#lookupPr() : null,
887
+ pr: prSegmentActive ? (previewOnly ? (this.#cachedPr ?? null) : this.#lookupPr()) : null,
655
888
  },
656
889
  usage: this.#cachedUsage,
890
+ command:
891
+ commandSegmentActive && (previewOnly || commandKey !== undefined)
892
+ ? {
893
+ output: this.#commandOutput,
894
+ failed: this.#commandFailed,
895
+ pending: previewOnly
896
+ ? this.#commandOutput === null && !this.#commandFailed
897
+ : this.#commandInFlightKey === commandKey,
898
+ }
899
+ : undefined,
657
900
  };
658
901
  }
659
902
 
@@ -730,7 +973,14 @@ export class StatusLineComponent implements Component {
730
973
  if (available <= 0 || count <= 0) return "";
731
974
  const withCount = `…+${count}`;
732
975
  const text = visibleWidth(withCount) <= available ? withCount : "…";
733
- return theme.fg("dim", text);
976
+ return truncateToWidth(theme.fg("dim", text), available);
977
+ }
978
+
979
+ #renderPreviewHighlight(content: string, style: "focus" | "selected"): string {
980
+ const bgColor = style === "selected" ? "warning" : "text";
981
+ const bgAnsi = theme.getFgAnsi(bgColor).replace("\x1b[38;", "\x1b[48;");
982
+ const fgAnsi = theme.getBgAnsi("selectedBg").replace("\x1b[48;", "\x1b[38;");
983
+ return `${bgAnsi}${fgAnsi}> ${Bun.stripANSI(content)} <\x1b[0m`;
734
984
  }
735
985
 
736
986
  #renderStatusGroup(
@@ -795,8 +1045,9 @@ export class StatusLineComponent implements Component {
795
1045
  Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
796
1046
  > &
797
1047
  StatusLineSettings,
1048
+ previewOnly = false,
798
1049
  ): CollectedStatusSegments {
799
- const ctx = this.#buildSegmentContext(width, effectiveSettings);
1050
+ const ctx = this.#buildSegmentContext(width, effectiveSettings, previewOnly);
800
1051
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
801
1052
 
802
1053
  // Use the subtle surface tone (the same elevated background as user-message
@@ -808,12 +1059,14 @@ export class StatusLineComponent implements Component {
808
1059
  const sepAnsi = theme.getFgAnsi("statusLineSep");
809
1060
 
810
1061
  const previewHighlightSegment = effectiveSettings.previewHighlightSegment;
1062
+ const previewHighlightStyle = effectiveSettings.previewHighlightStyle ?? "focus";
811
1063
  const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
812
- previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
1064
+ previewHighlightSegment === segId ? this.#renderPreviewHighlight(content, previewHighlightStyle) : content;
813
1065
 
814
1066
  const left: string[] = [];
815
1067
  const leftSegIds: StatusLineSegmentId[] = [];
816
1068
  for (const segId of effectiveSettings.leftSegments) {
1069
+ if (segId === "command" && !ctx.command) continue;
817
1070
  const rendered = renderSegment(segId, ctx);
818
1071
  if (rendered.visible && rendered.content) {
819
1072
  left.push(highlightSegment(segId, rendered.content));
@@ -834,6 +1087,7 @@ export class StatusLineComponent implements Component {
834
1087
  this.#keyDisplayContext,
835
1088
  );
836
1089
  for (const segId of effectiveSettings.rightSegments) {
1090
+ if (segId === "command" && !ctx.command) continue;
837
1091
  const rendered = renderSegment(segId, ctx);
838
1092
  if (rendered.visible && rendered.content) {
839
1093
  right.push(highlightSegment(segId, rendered.content));
@@ -869,6 +1123,7 @@ export class StatusLineComponent implements Component {
869
1123
  right,
870
1124
  rightSegIds,
871
1125
  previewHighlightSegment,
1126
+ previewHighlightStyle,
872
1127
  sessionAccent: effectiveSettings.sessionAccent,
873
1128
  leftSepWidth: visibleWidth(separatorDef.left),
874
1129
  rightSepWidth: visibleWidth(separatorDef.right),
@@ -965,7 +1220,7 @@ export class StatusLineComponent implements Component {
965
1220
 
966
1221
  #buildStatusLine(width: number, precollected?: CollectedStatusSegments): string {
967
1222
  const seg = precollected ?? this.#collectStatusSegments(width, this.#resolveSettings());
968
- const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment } = seg;
1223
+ const { ctx, separatorDef, bgAnsi, fgAnsi, sepAnsi, previewHighlightSegment, previewHighlightStyle } = seg;
969
1224
  const { leftSepWidth, rightSepWidth, leftCapWidth, rightCapWidth } = seg;
970
1225
  const topFillWidth = Math.max(0, width);
971
1226
 
@@ -998,7 +1253,10 @@ export class StatusLineComponent implements Component {
998
1253
  const overflow = totalWidth() - budget;
999
1254
  const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, overflow);
1000
1255
  if (shrunk !== null) {
1001
- left[pathIdx] = previewHighlightSegment === "path" ? `\x1b[7m${shrunk}\x1b[27m` : shrunk;
1256
+ left[pathIdx] =
1257
+ previewHighlightSegment === "path"
1258
+ ? this.#renderPreviewHighlight(shrunk, previewHighlightStyle)
1259
+ : shrunk;
1002
1260
  leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
1003
1261
  }
1004
1262
  }
@@ -1087,9 +1345,9 @@ export class StatusLineComponent implements Component {
1087
1345
  * being dropped. Falls back to the polished justified single row whenever
1088
1346
  * everything fits on one line.
1089
1347
  */
1090
- #buildStatusRows(width: number, maxRows: number): string[] {
1348
+ #buildStatusRows(width: number, maxRows: number, previewOnly = false): string[] {
1091
1349
  const effectiveSettings = this.#resolveSettings();
1092
- const seg = this.#collectStatusSegments(width, effectiveSettings);
1350
+ const seg = this.#collectStatusSegments(width, effectiveSettings, previewOnly);
1093
1351
  const cacheKey = JSON.stringify({
1094
1352
  width,
1095
1353
  maxRows,
@@ -1124,14 +1382,14 @@ export class StatusLineComponent implements Component {
1124
1382
  .join(","),
1125
1383
  version: this.#version,
1126
1384
  });
1127
- if (this.#renderedRowsCache?.key === cacheKey) {
1385
+ if (!previewOnly && this.#renderedRowsCache?.key === cacheKey) {
1128
1386
  this.#renderedRowsCacheHits++;
1129
1387
  return [...this.#renderedRowsCache.rows];
1130
1388
  }
1131
- this.#renderedRowsCacheMisses++;
1389
+ if (!previewOnly) this.#renderedRowsCacheMisses++;
1132
1390
 
1133
1391
  if (seg.left.length === 0 && seg.right.length === 0) {
1134
- this.#renderedRowsCache = { key: cacheKey, rows: [] };
1392
+ if (!previewOnly) this.#renderedRowsCache = { key: cacheKey, rows: [] };
1135
1393
  return [];
1136
1394
  }
1137
1395
 
@@ -1209,7 +1467,7 @@ export class StatusLineComponent implements Component {
1209
1467
  if (dropped > 0) {
1210
1468
  const priorityRow = this.#priorityRowFor(seg, [], [], topFillWidth);
1211
1469
  if (priorityRow !== null) {
1212
- this.#renderedRowsCache = { key: cacheKey, rows: [priorityRow] };
1470
+ if (!previewOnly) this.#renderedRowsCache = { key: cacheKey, rows: [priorityRow] };
1213
1471
  return [priorityRow];
1214
1472
  }
1215
1473
  }
@@ -1249,7 +1507,7 @@ export class StatusLineComponent implements Component {
1249
1507
  if (packedRows.length === 0 && marker) rows = [marker];
1250
1508
  }
1251
1509
 
1252
- this.#renderedRowsCache = { key: cacheKey, rows };
1510
+ if (!previewOnly) this.#renderedRowsCache = { key: cacheKey, rows };
1253
1511
  return [...rows];
1254
1512
  }
1255
1513
 
@@ -1271,6 +1529,120 @@ export class StatusLineComponent implements Component {
1271
1529
  return this.#buildStatusRows(width, this.#resolveMaxRows()).join("\n");
1272
1530
  }
1273
1531
 
1532
+ getPreviewContentForSettings(width: number, previewSettings: StatusLineSettings): string {
1533
+ const previousSettings = this.#settings;
1534
+ this.#settings = { ...previousSettings, previewHighlightSegment: undefined, ...previewSettings };
1535
+ try {
1536
+ return this.#buildStatusRows(width, this.#resolveMaxRows(), true).join("\n");
1537
+ } finally {
1538
+ this.#settings = previousSettings;
1539
+ }
1540
+ }
1541
+
1542
+ #placePreviewSegments(
1543
+ seg: CollectedStatusSegments,
1544
+ width: number,
1545
+ ): {
1546
+ left: string[];
1547
+ leftIds: StatusLineSegmentId[];
1548
+ right: string[];
1549
+ rightIds: (StatusLineSegmentId | null)[];
1550
+ } {
1551
+ const {
1552
+ ctx,
1553
+ previewHighlightSegment,
1554
+ previewHighlightStyle,
1555
+ leftSepWidth,
1556
+ rightSepWidth,
1557
+ leftCapWidth,
1558
+ rightCapWidth,
1559
+ } = seg;
1560
+ const rank = this.#priorityRanker(seg);
1561
+ const layout = (budget: number) => {
1562
+ const left = [...seg.left];
1563
+ const leftIds = [...seg.leftSegIds];
1564
+ const right = [...seg.right];
1565
+ const rightIds = [...seg.rightSegIds];
1566
+ let leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
1567
+ let rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
1568
+ const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
1569
+ let dropped = 0;
1570
+
1571
+ const pathIdx = leftIds.indexOf("path");
1572
+ if (pathIdx >= 0 && totalWidth() > budget) {
1573
+ const shrunk = this.#shrinkPathToWidth(left[pathIdx], ctx, totalWidth() - budget);
1574
+ if (shrunk !== null) {
1575
+ left[pathIdx] =
1576
+ previewHighlightSegment === "path"
1577
+ ? this.#renderPreviewHighlight(shrunk, previewHighlightStyle)
1578
+ : shrunk;
1579
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
1580
+ }
1581
+ }
1582
+ while (totalWidth() > budget && (left.length > 0 || right.length > 0)) {
1583
+ let victimSide: "left" | "right" | null = null;
1584
+ let victimIndex = -1;
1585
+ let victimRank = Number.POSITIVE_INFINITY;
1586
+ for (let index = right.length - 1; index >= 0; index -= 1) {
1587
+ const candidate = rank(rightIds[index]);
1588
+ if (candidate < victimRank) {
1589
+ victimRank = candidate;
1590
+ victimSide = "right";
1591
+ victimIndex = index;
1592
+ }
1593
+ }
1594
+ for (let index = left.length - 1; index >= 0; index -= 1) {
1595
+ const candidate = rank(leftIds[index]);
1596
+ if (candidate < victimRank) {
1597
+ victimRank = candidate;
1598
+ victimSide = "left";
1599
+ victimIndex = index;
1600
+ }
1601
+ }
1602
+ if (victimSide === null) break;
1603
+ if (victimSide === "right") {
1604
+ right.splice(victimIndex, 1);
1605
+ rightIds.splice(victimIndex, 1);
1606
+ rightWidth = this.#groupWidth(right, rightCapWidth, rightSepWidth);
1607
+ } else {
1608
+ left.splice(victimIndex, 1);
1609
+ leftIds.splice(victimIndex, 1);
1610
+ leftWidth = this.#groupWidth(left, leftCapWidth, leftSepWidth);
1611
+ }
1612
+ dropped += 1;
1613
+ }
1614
+ return { left, leftIds, right, rightIds, dropped };
1615
+ };
1616
+
1617
+ let placed = layout(Math.max(0, width));
1618
+ let reserved = 0;
1619
+ if (placed.dropped > 0 && width > 0) {
1620
+ for (let pass = 0; pass < 4; pass += 1) {
1621
+ const needed = Math.min(width, visibleWidth(`…+${placed.dropped}`));
1622
+ if (needed <= reserved) break;
1623
+ reserved = needed;
1624
+ placed = layout(Math.max(0, width - reserved));
1625
+ }
1626
+ }
1627
+ return placed;
1628
+ }
1629
+
1630
+ getPreviewPartsForSettings(width: number, previewSettings: StatusLineSettings): StatusLinePreviewParts {
1631
+ const previousSettings = this.#settings;
1632
+ this.#settings = { ...previousSettings, ...previewSettings };
1633
+ this.#resolvedSettingsCache = undefined;
1634
+ this.#resolvedSettingsFingerprint = undefined;
1635
+ try {
1636
+ const seg = this.#collectStatusSegments(width, this.#resolveSettings(), true);
1637
+ const placed = this.#placePreviewSegments(seg, width);
1638
+ return { ...placed, separator: seg.separatorDef };
1639
+ } finally {
1640
+ this.#settings = previousSettings;
1641
+ this.#resolvedSettingsCache = undefined;
1642
+ this.#resolvedSettingsFingerprint = undefined;
1643
+ }
1644
+ }
1645
+
1274
1646
  getCacheStatsForTest(): { rowHits: number; rowMisses: number } {
1275
1647
  return { rowHits: this.#renderedRowsCacheHits, rowMisses: this.#renderedRowsCacheMisses };
1276
1648
  }
@@ -1043,6 +1043,7 @@ export class CommandController {
1043
1043
  this.ctx.ui.requestRender();
1044
1044
 
1045
1045
  prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
1046
+ this.ctx.resetAssistantTextPresentation();
1046
1047
  this.ctx.chatContainer.clear();
1047
1048
  this.ctx.pendingMessagesContainer.clear();
1048
1049
  this.ctx.compactionQueuedMessages = [];
@@ -1083,6 +1084,7 @@ export class CommandController {
1083
1084
  this.ctx.ui.requestRender();
1084
1085
 
1085
1086
  prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
1087
+ this.ctx.resetAssistantTextPresentation();
1086
1088
  this.ctx.chatContainer.clear();
1087
1089
  this.ctx.pendingMessagesContainer.clear();
1088
1090
  this.ctx.compactionQueuedMessages = [];