@gajae-code/coding-agent 0.7.7 → 0.7.9

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 (96) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +14 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/update-cli.d.ts +24 -1
  6. package/dist/types/commands/local-provider.d.ts +34 -0
  7. package/dist/types/config/keybindings.d.ts +1 -1
  8. package/dist/types/config/model-profile-activation.d.ts +19 -2
  9. package/dist/types/config/model-profiles.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +9 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +45 -0
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  15. package/dist/types/gjc-runtime/launch-tmux.d.ts +14 -5
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
  17. package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
  18. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +3 -1
  19. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  20. package/dist/types/modes/components/hook-input.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +11 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +4 -1
  23. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  24. package/dist/types/modes/components/status-line.d.ts +7 -3
  25. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  26. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  27. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  28. package/dist/types/modes/interactive-mode.d.ts +3 -1
  29. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  30. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +21 -1
  31. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  32. package/dist/types/modes/types.d.ts +3 -1
  33. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  34. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  35. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  36. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  37. package/dist/types/tools/ask.d.ts +24 -0
  38. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  39. package/package.json +7 -7
  40. package/src/cli/local-provider-smoke.ts +632 -0
  41. package/src/cli/notify-cli.ts +5 -3
  42. package/src/cli/update-cli.ts +155 -13
  43. package/src/cli.ts +19 -0
  44. package/src/commands/launch.ts +46 -1
  45. package/src/commands/local-provider.ts +62 -0
  46. package/src/commands/notify.ts +1 -1
  47. package/src/config/keybindings.ts +1 -1
  48. package/src/config/model-profile-activation.ts +92 -11
  49. package/src/config/model-profiles.ts +15 -2
  50. package/src/config/model-registry.ts +136 -6
  51. package/src/config/models-config-schema.ts +9 -0
  52. package/src/config/settings-schema.ts +48 -0
  53. package/src/config/settings.ts +11 -0
  54. package/src/coordinator-mcp/server.ts +280 -36
  55. package/src/deep-interview/render-middleware.ts +1 -0
  56. package/src/defaults/gjc/skills/deep-interview/SKILL.md +14 -6
  57. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  58. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  59. package/src/gjc-runtime/launch-tmux.ts +321 -38
  60. package/src/gjc-runtime/launch-worktree.ts +16 -2
  61. package/src/gjc-runtime/team-runtime.ts +87 -7
  62. package/src/gjc-runtime/tmux-common.ts +16 -0
  63. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  64. package/src/gjc-runtime/ultragoal-guard.ts +11 -5
  65. package/src/internal-urls/docs-index.generated.ts +10 -9
  66. package/src/modes/components/agent-dashboard.ts +2 -1
  67. package/src/modes/components/custom-model-preset-wizard.ts +31 -192
  68. package/src/modes/components/hook-editor.ts +7 -4
  69. package/src/modes/components/hook-input.ts +4 -0
  70. package/src/modes/components/model-selector.ts +260 -12
  71. package/src/modes/components/session-observer-overlay.ts +3 -3
  72. package/src/modes/components/settings-selector.ts +506 -5
  73. package/src/modes/components/status-line/segments.ts +16 -19
  74. package/src/modes/components/status-line/types.ts +6 -2
  75. package/src/modes/components/status-line.ts +70 -35
  76. package/src/modes/controllers/event-controller.ts +98 -4
  77. package/src/modes/controllers/extension-ui-controller.ts +5 -0
  78. package/src/modes/controllers/input-controller.ts +15 -0
  79. package/src/modes/controllers/selector-controller.ts +142 -11
  80. package/src/modes/interactive-mode.ts +7 -2
  81. package/src/modes/prompt-action-autocomplete.ts +8 -0
  82. package/src/modes/shared/agent-wire/deep-interview-gate.ts +11 -4
  83. package/src/modes/tmux-scroll.ts +92 -0
  84. package/src/modes/types.ts +6 -1
  85. package/src/modes/utils/terminal-bell.ts +48 -0
  86. package/src/notifications/daemon-paths.ts +22 -0
  87. package/src/notifications/telegram-daemon-cli.ts +125 -13
  88. package/src/notifications/telegram-daemon.ts +6 -21
  89. package/src/prompts/system/system-prompt.md +5 -0
  90. package/src/sdk.ts +10 -1
  91. package/src/session/agent-session.ts +84 -6
  92. package/src/slash-commands/builtin-registry.ts +106 -13
  93. package/src/tools/ask.ts +11 -1
  94. package/src/tools/eval.ts +2 -2
  95. package/src/tools/render-utils.ts +2 -0
  96. package/src/tools/ultragoal-ask-guard.ts +39 -5
@@ -32,6 +32,7 @@ export interface StatusLineSegmentOptions {
32
32
  path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
33
33
  git?: { showBranch?: boolean; showStaged?: boolean; showUnstaged?: boolean; showUntracked?: boolean };
34
34
  time?: { format?: "12h" | "24h"; showSeconds?: boolean };
35
+ usage?: { mode?: "used" | "remaining" };
35
36
  }
36
37
 
37
38
  export interface StatusLineSettings {
@@ -40,6 +41,7 @@ export interface StatusLineSettings {
40
41
  rightSegments?: StatusLineSegmentId[];
41
42
  separator?: StatusLineSeparatorStyle;
42
43
  segmentOptions?: StatusLineSegmentOptions;
44
+ previewHighlightSegment?: StatusLineSegmentId;
43
45
  showHookStatus?: boolean;
44
46
  showSkillHud?: boolean;
45
47
  sessionAccent?: boolean;
@@ -181,11 +183,8 @@ export class StatusLineComponent implements Component {
181
183
  #lastTokensPerSecond: number | null = null;
182
184
  #lastTokensPerSecondTimestamp: number | null = null;
183
185
 
184
- // Anthropic usage caching (5-min TTL, OAuth/sub only)
185
- #cachedUsage: {
186
- fiveHour?: { percent: number; resetMinutes?: number };
187
- sevenDay?: { percent: number; resetHours?: number };
188
- } | null = null;
186
+ // Provider usage caching (5-min TTL, OAuth/sub only)
187
+ #cachedUsage: SegmentContext["usage"] = null;
189
188
  #usageFetchedAt = 0;
190
189
  #usageInFlight = false;
191
190
  // Context breakdown — incremental cache. Replaces the previous 2-second
@@ -221,7 +220,7 @@ export class StatusLineComponent implements Component {
221
220
  }
222
221
 
223
222
  updateSettings(settings: StatusLineSettings): void {
224
- this.#settings = { ...this.#settings, ...settings };
223
+ this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
225
224
  }
226
225
 
227
226
  setAutoCompactEnabled(enabled: boolean): void {
@@ -462,9 +461,9 @@ export class StatusLineComponent implements Component {
462
461
  }
463
462
 
464
463
  /**
465
- * Background-refresh the Anthropic OAuth quota report. Guarded by a 5-min
466
- * TTL on both success (cache lifetime) and error (backoff). Exposed
467
- * (non-private) so unit tests can verify the backoff invariant.
464
+ * Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
465
+ * success (cache lifetime) and error (backoff). Exposed (non-private) so
466
+ * unit tests can verify the backoff invariant.
468
467
  */
469
468
  refreshUsageInBackground(): void {
470
469
  const now = Date.now();
@@ -478,6 +477,9 @@ export class StatusLineComponent implements Component {
478
477
  .then(reports => {
479
478
  this.#cachedUsage = this.#normalizeUsageReports(reports);
480
479
  this.#usageFetchedAt = Date.now();
480
+ if (this.#onBranchChange) {
481
+ this.#onBranchChange();
482
+ }
481
483
  })
482
484
  .catch(() => {
483
485
  // Backoff on error: stamp the fetch time so the 5-min TTL guard
@@ -491,47 +493,77 @@ export class StatusLineComponent implements Component {
491
493
  });
492
494
  }
493
495
 
494
- #normalizeUsageReports(reports: unknown): {
495
- fiveHour?: { percent: number; resetMinutes?: number };
496
- sevenDay?: { percent: number; resetHours?: number };
497
- } | null {
496
+ #normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
498
497
  if (!Array.isArray(reports)) return null;
499
- let fiveHour: { percent: number; resetMinutes?: number } | undefined;
500
- let sevenDay: { percent: number; resetHours?: number } | undefined;
498
+ const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
499
+ const seen = new Set<string>();
501
500
  const now = Date.now();
501
+
502
+ const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
503
+ if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
504
+ return fallback;
505
+ };
506
+ const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
507
+
508
+ const pushWindow = (
509
+ key: string,
510
+ label: string,
511
+ fraction: number,
512
+ resetsAt: number | undefined,
513
+ resetUnit: "m" | "h",
514
+ ) => {
515
+ if (seen.has(key)) return;
516
+ seen.add(key);
517
+ windows.push({
518
+ label,
519
+ percent: fraction * 100,
520
+ resetValue:
521
+ typeof resetsAt === "number"
522
+ ? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
523
+ : undefined,
524
+ resetUnit,
525
+ });
526
+ };
527
+
502
528
  for (const report of reports) {
503
529
  if (!report || typeof report !== "object") continue;
530
+ const provider = (report as { provider?: unknown }).provider;
531
+ const providerId = typeof provider === "string" ? provider : undefined;
504
532
  const limits = (report as { limits?: unknown }).limits;
505
533
  if (!Array.isArray(limits)) continue;
506
534
  for (const limit of limits) {
507
535
  if (!limit || typeof limit !== "object") continue;
508
536
  const l = limit as {
509
- scope?: { windowId?: string; tier?: string };
510
- window?: { resetsAt?: number };
537
+ id?: unknown;
538
+ scope?: { windowId?: string; tier?: string; modelId?: string };
539
+ window?: { id?: string; resetsAt?: number };
511
540
  amount?: { usedFraction?: number };
512
541
  };
513
542
  const fraction = l.amount?.usedFraction;
514
543
  if (typeof fraction !== "number") continue;
515
- const windowId = l.scope?.windowId;
544
+ const id = typeof l.id === "string" ? l.id : "";
545
+ const windowId = l.scope?.windowId ?? l.window?.id;
516
546
  const tier = l.scope?.tier;
547
+ const modelId = l.scope?.modelId;
517
548
  const resetsAt = l.window?.resetsAt;
518
- if (windowId === "5h" && !tier && !fiveHour) {
519
- fiveHour = {
520
- percent: fraction * 100,
521
- resetMinutes:
522
- typeof resetsAt === "number" ? Math.max(0, Math.round((resetsAt - now) / 60_000)) : undefined,
523
- };
524
- } else if (windowId === "7d" && !tier && !sevenDay) {
525
- sevenDay = {
526
- percent: fraction * 100,
527
- resetHours:
528
- typeof resetsAt === "number" ? Math.max(0, Math.round((resetsAt - now) / 3_600_000)) : undefined,
529
- };
549
+
550
+ if (providerId === "openai-codex") {
551
+ if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
552
+ const label = codexWindowLabel(windowId, "primary");
553
+ pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
554
+ } else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
555
+ const label = codexWindowLabel(windowId, "secondary");
556
+ pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
557
+ }
558
+ } else if (windowId === "5h" && !tier) {
559
+ pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
560
+ } else if (windowId === "7d" && !tier) {
561
+ pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
530
562
  }
531
563
  }
532
564
  }
533
- if (!fiveHour && !sevenDay) return null;
534
- return { fiveHour, sevenDay };
565
+
566
+ return windows.length > 0 ? { windows } : null;
535
567
  }
536
568
 
537
569
  /**
@@ -684,12 +716,15 @@ export class StatusLineComponent implements Component {
684
716
  const sepAnsi = theme.getFgAnsi("statusLineSep");
685
717
 
686
718
  // Collect visible segment contents
719
+ const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
720
+ effectiveSettings.previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
721
+
687
722
  const leftParts: string[] = [];
688
723
  const leftSegIds: StatusLineSegmentId[] = [];
689
724
  for (const segId of effectiveSettings.leftSegments) {
690
725
  const rendered = renderSegment(segId, ctx);
691
726
  if (rendered.visible && rendered.content) {
692
- leftParts.push(rendered.content);
727
+ leftParts.push(highlightSegment(segId, rendered.content));
693
728
  leftSegIds.push(segId);
694
729
  }
695
730
  }
@@ -698,7 +733,7 @@ export class StatusLineComponent implements Component {
698
733
  for (const segId of effectiveSettings.rightSegments) {
699
734
  const rendered = renderSegment(segId, ctx);
700
735
  if (rendered.visible && rendered.content) {
701
- rightParts.push(rendered.content);
736
+ rightParts.push(highlightSegment(segId, rendered.content));
702
737
  }
703
738
  }
704
739
 
@@ -765,7 +800,7 @@ export class StatusLineComponent implements Component {
765
800
  if (!adjusted.visible || !adjusted.content) break;
766
801
  reRendered = adjusted;
767
802
  }
768
- left[pathIdx] = reRendered.content;
803
+ left[pathIdx] = highlightSegment("path", reRendered.content);
769
804
  leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
770
805
  }
771
806
  }
@@ -3,6 +3,7 @@ import { calculatePromptTokens } from "@gajae-code/agent-core/compaction/compact
3
3
  import type { AssistantMessage, ImageContent } from "@gajae-code/ai";
4
4
  import { parseRateLimitReason } from "@gajae-code/ai";
5
5
  import { type Component, Loader, TERMINAL, Text } from "@gajae-code/tui";
6
+ import { logger } from "@gajae-code/utils";
6
7
  import { settings } from "../../config/settings";
7
8
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
8
9
  import {
@@ -15,16 +16,56 @@ import { ToolExecutionComponent } from "../../modes/components/tool-execution";
15
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
16
17
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
17
18
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
19
+ import { summaryFromMessage } from "../../notifications/helpers";
18
20
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
19
21
  import type { AgentSessionEvent } from "../../session/agent-session";
20
22
  import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
21
23
  import type { ResolveToolDetails } from "../../tools/resolve";
22
24
  import { interruptHint } from "../shared";
23
25
  import { buildAbortDisplayMessage } from "../utils/abort-message";
26
+ import { ringTerminalBell } from "../utils/terminal-bell";
24
27
 
25
28
  type AgentSessionEventKind = AgentSessionEvent["type"];
26
29
 
27
30
  const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
31
+ const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
32
+
33
+ interface CompletionNotifyPayload {
34
+ type: "agent-turn-complete";
35
+ title: string;
36
+ body: string;
37
+ cwd: string;
38
+ sessionId?: string;
39
+ sessionName?: string;
40
+ lastAssistantMessage?: string;
41
+ stopReason?: string;
42
+ }
43
+
44
+ function completionNotifyShellCommand(command: string): string[] {
45
+ if (process.platform === "win32") {
46
+ return ["cmd.exe", "/d", "/s", "/c", command];
47
+ }
48
+ return ["/bin/sh", "-c", command];
49
+ }
50
+
51
+ function cleanNotificationEnvValue(value: string | undefined, max = 4000): string {
52
+ if (!value) return "";
53
+ return value.replaceAll("\0", "").slice(0, max);
54
+ }
55
+
56
+ function buildCompletionNotifyEnv(payload: CompletionNotifyPayload): Record<string, string> {
57
+ return {
58
+ GJC_NOTIFICATION_TYPE: payload.type,
59
+ GJC_NOTIFICATION_TITLE: cleanNotificationEnvValue(payload.title, 500),
60
+ GJC_NOTIFICATION_BODY: cleanNotificationEnvValue(payload.body),
61
+ GJC_NOTIFICATION_CWD: cleanNotificationEnvValue(payload.cwd, 2000),
62
+ GJC_NOTIFICATION_SESSION_ID: cleanNotificationEnvValue(payload.sessionId, 500),
63
+ GJC_NOTIFICATION_SESSION_NAME: cleanNotificationEnvValue(payload.sessionName, 500),
64
+ GJC_NOTIFICATION_LAST_ASSISTANT_MESSAGE: cleanNotificationEnvValue(payload.lastAssistantMessage),
65
+ GJC_NOTIFICATION_STOP_REASON: cleanNotificationEnvValue(payload.stopReason, 100),
66
+ GJC_NOTIFICATION_JSON: cleanNotificationEnvValue(JSON.stringify(payload), 8000),
67
+ };
68
+ }
28
69
 
29
70
  function friendlyRetryReason(errorMessage: string | undefined): string {
30
71
  if (!errorMessage) return "";
@@ -857,8 +898,46 @@ export class EventController {
857
898
  return lastAssistant?.usage ? calculatePromptTokens(lastAssistant.usage) : 0;
858
899
  }
859
900
 
901
+ #runCompletionNotifyCommand(payload: CompletionNotifyPayload): void {
902
+ const command = settings.getGlobal("completion.notifyCommand")?.trim();
903
+ if (!command) return;
904
+
905
+ try {
906
+ const proc = Bun.spawn(completionNotifyShellCommand(command), {
907
+ cwd: payload.cwd,
908
+ env: {
909
+ ...process.env,
910
+ ...buildCompletionNotifyEnv(payload),
911
+ },
912
+ stdin: "ignore",
913
+ stdout: "ignore",
914
+ stderr: "ignore",
915
+ });
916
+ proc.unref();
917
+ const timer = setTimeout(() => {
918
+ try {
919
+ proc.kill();
920
+ } catch {}
921
+ }, COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS);
922
+ timer.unref?.();
923
+ void proc.exited
924
+ .then(exitCode => {
925
+ clearTimeout(timer);
926
+ if (exitCode !== 0) {
927
+ logger.warn("completion notify command exited non-zero", { exitCode });
928
+ }
929
+ })
930
+ .catch(error => {
931
+ clearTimeout(timer);
932
+ logger.warn("completion notify command failed", { error: String(error) });
933
+ });
934
+ } catch (error) {
935
+ logger.warn("completion notify command failed to start", { error: String(error) });
936
+ }
937
+ }
938
+
860
939
  sendCompletionNotification(): void {
861
- if (this.ctx.isBackgrounded === false) return;
940
+ const isBackgrounded = this.ctx.isBackgrounded !== false;
862
941
  const notify = settings.get("completion.notify");
863
942
  if (notify === "off") return;
864
943
 
@@ -869,9 +948,24 @@ export class EventController {
869
948
  const last = this.ctx.session.getLastAssistantMessage?.();
870
949
  if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
871
950
 
872
- const title = this.ctx.sessionManager.getSessionName();
873
- const message = title ? `${title}: Complete` : "Complete";
874
- TERMINAL.sendNotification(message);
951
+ const sessionName = this.ctx.sessionManager.getSessionName();
952
+ const title = sessionName ? `${sessionName}: Complete` : "Complete";
953
+ const summary = summaryFromMessage(last, 1000);
954
+ const body = summary ?? "Complete";
955
+ const sessionManager = this.ctx.sessionManager as { getCwd?: () => string; getSessionId?: () => string };
956
+ const payload: CompletionNotifyPayload = {
957
+ type: "agent-turn-complete",
958
+ title,
959
+ body,
960
+ cwd: sessionManager.getCwd?.() ?? settings.getCwd(),
961
+ sessionId: sessionManager.getSessionId?.(),
962
+ sessionName: sessionName || undefined,
963
+ lastAssistantMessage: summary,
964
+ stopReason: last?.stopReason,
965
+ };
966
+ ringTerminalBell("complete");
967
+ if (isBackgrounded) TERMINAL.sendNotification(title);
968
+ this.#runCompletionNotifyCommand(payload);
875
969
  }
876
970
 
877
971
  async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
@@ -23,6 +23,7 @@ import { HookSelectorComponent } from "../../modes/components/hook-selector";
23
23
  import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
24
24
  import type { InteractiveModeContext } from "../../modes/types";
25
25
  import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
26
+ import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
26
27
 
27
28
  const MAX_WIDGET_LINES = 10;
28
29
  const HOOK_SELECTOR_CHROME_ROWS = 7;
@@ -622,6 +623,8 @@ export class ExtensionUiController {
622
623
  const scrollTitleRows =
623
624
  requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
624
625
 
626
+ ringTerminalBell(classifyHookSelectorBellEvent(title));
627
+
625
628
  this.ctx.hookSelector = new HookSelectorComponent(
626
629
  title,
627
630
  options,
@@ -712,6 +715,7 @@ export class ExtensionUiController {
712
715
  title: string,
713
716
  placeholder?: string,
714
717
  dialogOptions?: ExtensionUIDialogOptions,
718
+ inputOptions?: { readonly initialValue?: string },
715
719
  ): Promise<string | undefined> {
716
720
  const { promise, finish, attachAbort } = this.#createHookDialogState(
717
721
  () => this.hideHookInput(),
@@ -729,6 +733,7 @@ export class ExtensionUiController {
729
733
  finish(undefined);
730
734
  },
731
735
  {
736
+ initialValue: inputOptions?.initialValue,
732
737
  timeout: dialogOptions?.timeout,
733
738
  onTimeout: dialogOptions?.onTimeout,
734
739
  tui: this.ctx.ui,
@@ -9,6 +9,7 @@ import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibil
9
9
  import { expandEmoticons } from "../../modes/emoji-autocomplete";
10
10
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
11
11
  import { theme } from "../../modes/theme/theme";
12
+ import { scrollTmuxToPreviousUserInput as scrollTmuxPaneToPreviousUserInput } from "../../modes/tmux-scroll";
12
13
  import type { InteractiveModeContext } from "../../modes/types";
13
14
  import type { AgentSessionEvent } from "../../session/agent-session";
14
15
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
@@ -858,6 +859,7 @@ export class InputController {
858
859
  copyCurrentLine: () => this.handleCopyCurrentLine(),
859
860
  copyPrompt: () => this.handleCopyPrompt(),
860
861
  pasteImage: () => void this.handleImagePaste(),
862
+ scrollTmuxToPreviousUserInput: () => this.scrollTmuxToPreviousUserInput(),
861
863
  undo: prefix => this.ctx.editor.undoPastTransientText(prefix),
862
864
  moveCursorToMessageEnd: () => this.ctx.editor.moveToMessageEnd(),
863
865
  moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
@@ -952,6 +954,19 @@ export class InputController {
952
954
  this.setToolsExpanded(!this.ctx.toolOutputExpanded);
953
955
  }
954
956
 
957
+ scrollTmuxToPreviousUserInput(): void {
958
+ const result = scrollTmuxPaneToPreviousUserInput();
959
+ if (result.ok) return;
960
+
961
+ if (result.reason === "not_inside_tmux") {
962
+ this.ctx.showWarning("Previous-input scroll works only inside tmux.");
963
+ return;
964
+ }
965
+
966
+ const detail = result.error ? `: ${result.error}` : ".";
967
+ this.ctx.showWarning(`Failed to scroll tmux to previous user input${detail}`);
968
+ }
969
+
955
970
  setToolsExpanded(expanded: boolean): void {
956
971
  this.ctx.toolOutputExpanded = expanded;
957
972
  for (const child of this.ctx.chatContainer.children) {
@@ -4,10 +4,17 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
4
4
  import type { Component, OverlayHandle } from "@gajae-code/tui";
5
5
  import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
6
6
  import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
7
- import { activateModelProfile, materializeActiveModelProfileAssignment } from "../../config/model-profile-activation";
8
- import { recommendModelProfileForProvider } from "../../config/model-profiles";
7
+ import {
8
+ activateModelProfile,
9
+ type MaterializeModelProfileForDeletionResult,
10
+ materializeActiveModelProfileAssignment,
11
+ materializeModelProfileForDeletion,
12
+ restoreMaterializedModelProfileForDeletion,
13
+ } from "../../config/model-profile-activation";
14
+ import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
9
15
  import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
10
16
  import { formatModelSelectorValue } from "../../config/model-resolver";
17
+ import type { ModelProfileConfig } from "../../config/models-config-schema";
11
18
  import { settings } from "../../config/settings";
12
19
  import { DebugSelectorComponent } from "../../debug";
13
20
  import { disableProvider, enableProvider } from "../../discovery";
@@ -38,7 +45,11 @@ import {
38
45
  CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING,
39
46
  runExternalCredentialAutoImport,
40
47
  } from "../../setup/credential-auto-import";
41
- import { filterAutoImportOAuthCredentials, formatDiscoverySummary } from "../../setup/credential-import";
48
+ import {
49
+ filterAutoImportOAuthCredentials,
50
+ formatDiscoverySummary,
51
+ type ImportableCredential,
52
+ } from "../../setup/credential-import";
42
53
  import {
43
54
  MODEL_ONBOARDING_API_PROVIDER_COMMAND,
44
55
  MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
@@ -239,7 +250,7 @@ export class SelectorController {
239
250
  this.ctx.ui.requestRender();
240
251
  }
241
252
 
242
- showCustomModelPresetWizard(): void {
253
+ showCustomModelPresetWizard(snapshot: ModelProfileConfig): void {
243
254
  this.showSelector(done => {
244
255
  let wizard: CustomModelPresetWizardComponent;
245
256
  const submit = async (input: CustomModelPresetWizardSubmit): Promise<void> => {
@@ -247,7 +258,7 @@ export class SelectorController {
247
258
  const profile = await this.ctx.session.modelRegistry.saveCustomModelProfile(input.name, input.profile);
248
259
  await this.ctx.session.modelRegistry.refresh("offline");
249
260
  await this.ctx.notifyConfigChanged?.();
250
- this.ctx.showStatus(`Custom model preset created: ${profile.displayName ?? profile.name}`);
261
+ this.ctx.showStatus(`Custom model preset created: ${formatModelProfileDisplayLabel(profile)}`);
251
262
  done();
252
263
  this.ctx.ui.requestRender();
253
264
  } catch (err) {
@@ -256,6 +267,7 @@ export class SelectorController {
256
267
  }
257
268
  };
258
269
  wizard = new CustomModelPresetWizardComponent(
270
+ snapshot,
259
271
  input => {
260
272
  void submit(input);
261
273
  },
@@ -269,6 +281,106 @@ export class SelectorController {
269
281
  });
270
282
  }
271
283
 
284
+ async #renameCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
285
+ const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
286
+ const currentName = profile ? formatModelProfileDisplayLabel(profile) : profileName;
287
+ const input = await this.ctx.showHookInput(`Rename custom model preset: ${currentName}`, undefined, undefined, {
288
+ initialValue: currentName,
289
+ });
290
+ if (input === undefined) {
291
+ this.ctx.showStatus("Preset rename cancelled.");
292
+ this.ctx.ui.requestRender();
293
+ return;
294
+ }
295
+ try {
296
+ const renamed = await this.ctx.session.modelRegistry.renameCustomModelProfile(profileName, input);
297
+ await this.ctx.session.modelRegistry.refresh("offline");
298
+ await this.ctx.notifyConfigChanged?.();
299
+ modelSelector.refreshPresetProfiles(renamed.name);
300
+ this.ctx.showStatus(`Custom model preset renamed: ${formatModelProfileDisplayLabel(renamed)}`);
301
+ this.ctx.ui.requestRender();
302
+ } catch (err) {
303
+ this.ctx.showError(`Preset rename failed: ${err instanceof Error ? err.message : String(err)}`);
304
+ }
305
+ }
306
+
307
+ async #deleteCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
308
+ const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
309
+ const profileLabel = profile ? formatModelProfileDisplayLabel(profile) : profileName;
310
+ const confirmed = await this.ctx.showHookConfirm(
311
+ `Delete custom model preset: ${profileLabel}`,
312
+ "This removes the preset entry after preserving current role model settings when this preset is active/default.",
313
+ );
314
+ if (!confirmed) {
315
+ this.ctx.showStatus("Preset delete cancelled.");
316
+ this.ctx.ui.requestRender();
317
+ return;
318
+ }
319
+
320
+ const activeProfile = this.ctx.session.getActiveModelProfile?.();
321
+ const defaultProfile = this.ctx.settings.get("modelProfile.default");
322
+ let snapshot: MaterializeModelProfileForDeletionResult | undefined;
323
+ let deletedProfile: ModelProfileConfig | undefined;
324
+ const refreshSelectorState = (refreshedProfileName?: string): void => {
325
+ modelSelector.refreshRoleAssignments({
326
+ currentModel: this.ctx.session.model,
327
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
328
+ activeModelProfile:
329
+ this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
330
+ });
331
+ modelSelector.refreshPresetProfiles(refreshedProfileName);
332
+ };
333
+ try {
334
+ if (activeProfile === profileName || defaultProfile === profileName) {
335
+ snapshot = await materializeModelProfileForDeletion({
336
+ session: this.ctx.session,
337
+ modelRegistry: this.ctx.session.modelRegistry,
338
+ settings: this.ctx.settings,
339
+ profileName,
340
+ });
341
+ }
342
+ deletedProfile = await this.ctx.session.modelRegistry.deleteCustomModelProfile(profileName);
343
+ await this.ctx.session.modelRegistry.refresh("offline");
344
+ await this.ctx.notifyConfigChanged?.();
345
+ refreshSelectorState();
346
+ this.ctx.showStatus(`Custom model preset deleted: ${profileLabel}`);
347
+ this.ctx.ui.requestRender();
348
+ } catch (err) {
349
+ let presetRestoreError: unknown;
350
+ if (deletedProfile) {
351
+ try {
352
+ await this.ctx.session.modelRegistry.saveCustomModelProfile(profileName, deletedProfile);
353
+ await this.ctx.session.modelRegistry.refresh("offline");
354
+ } catch (restoreErr) {
355
+ presetRestoreError = restoreErr;
356
+ }
357
+ }
358
+ if (snapshot) {
359
+ try {
360
+ await restoreMaterializedModelProfileForDeletion({
361
+ settings: this.ctx.settings,
362
+ session: this.ctx.session,
363
+ snapshot,
364
+ });
365
+ } catch (restoreErr) {
366
+ refreshSelectorState(deletedProfile ? profileName : undefined);
367
+ this.ctx.showError(
368
+ `Preset delete failed and settings rollback failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`,
369
+ );
370
+ return;
371
+ }
372
+ }
373
+ if (deletedProfile) refreshSelectorState(profileName);
374
+ if (presetRestoreError) {
375
+ this.ctx.showError(
376
+ `Preset delete failed and preset restore failed: ${presetRestoreError instanceof Error ? presetRestoreError.message : String(presetRestoreError)}`,
377
+ );
378
+ return;
379
+ }
380
+ this.ctx.showError(`Preset delete failed: ${err instanceof Error ? err.message : String(err)}`);
381
+ }
382
+ }
383
+
272
384
  showCustomProviderWizard(): void {
273
385
  this.showSelector(done => {
274
386
  let wizard: CustomProviderWizardComponent;
@@ -336,14 +448,16 @@ export class SelectorController {
336
448
  separator: settings.get("statusLine.separator"),
337
449
  showHookStatus: settings.get("statusLine.showHookStatus"),
338
450
  sessionAccent: settings.get("statusLine.sessionAccent"),
451
+ segmentOptions: settings.get("statusLine.segmentOptions"),
339
452
  ...previewSettings,
340
453
  });
341
454
  this.ctx.updateEditorTopBorder();
342
455
  this.ctx.ui.requestRender();
343
456
  },
344
- getStatusLinePreview: () => {
457
+ getStatusLinePreview: (width?: number) => {
345
458
  // Return the rendered status line for inline preview
346
- const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
459
+ const availableWidth =
460
+ width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
347
461
  return this.ctx.statusLine.getTopBorder(availableWidth).content;
348
462
  },
349
463
  onPluginsChanged: () => {
@@ -359,6 +473,7 @@ export class SelectorController {
359
473
  separator: settings.get("statusLine.separator"),
360
474
  showHookStatus: settings.get("statusLine.showHookStatus"),
361
475
  sessionAccent: settings.get("statusLine.sessionAccent"),
476
+ segmentOptions: settings.get("statusLine.segmentOptions"),
362
477
  });
363
478
  this.ctx.updateEditorTopBorder();
364
479
  this.ctx.ui.requestRender();
@@ -603,6 +718,9 @@ export class SelectorController {
603
718
  case "statusLineShowHooks":
604
719
  case "statusLine.showHookStatus":
605
720
  case "statusLine.sessionAccent":
721
+ case "statusLine.leftSegments":
722
+ case "statusLine.rightSegments":
723
+ case "statusLine.segmentOptions":
606
724
  case "statusLineSegments":
607
725
  case "statusLineModelThinking":
608
726
  case "statusLinePathAbbreviate":
@@ -682,10 +800,23 @@ export class SelectorController {
682
800
  try {
683
801
  if (selection.kind === "createProfile") {
684
802
  done();
685
- this.showCustomModelPresetWizard();
803
+ this.showCustomModelPresetWizard(selection.profile);
804
+ return;
805
+ }
806
+ if (selection.kind === "renameProfile") {
807
+ await this.#renameCustomModelPreset(selection.profileName, modelSelector);
808
+ return;
809
+ }
810
+ if (selection.kind === "deleteProfile") {
811
+ await this.#deleteCustomModelPreset(selection.profileName, modelSelector);
686
812
  return;
687
813
  }
688
814
  if (selection.kind === "profile") {
815
+ const profileLabel = formatModelProfileDisplayLabel(
816
+ this.ctx.session.modelRegistry.getModelProfile(selection.profileName) ?? {
817
+ name: selection.profileName,
818
+ },
819
+ );
689
820
  await activateModelProfile(
690
821
  {
691
822
  session: this.ctx.session,
@@ -699,8 +830,8 @@ export class SelectorController {
699
830
  this.ctx.updateEditorBorderColor();
700
831
  this.ctx.showStatus(
701
832
  selection.setDefault
702
- ? `Default model profile: ${selection.profileName}`
703
- : `Model profile: ${selection.profileName}`,
833
+ ? `Default model profile: ${profileLabel}`
834
+ : `Model profile: ${profileLabel}`,
704
835
  );
705
836
  done();
706
837
  this.ctx.ui.requestRender();
@@ -1330,7 +1461,7 @@ export class SelectorController {
1330
1461
  }
1331
1462
  }
1332
1463
 
1333
- let externalCredentialCandidates: ReturnType<typeof filterAutoImportOAuthCredentials> = [];
1464
+ let externalCredentialCandidates: ImportableCredential[] = [];
1334
1465
  if (
1335
1466
  mode === "login" &&
1336
1467
  providerId === undefined &&
@@ -2684,8 +2684,13 @@ export class InteractiveMode implements InteractiveModeContext {
2684
2684
  this.#extensionUiController.hideHookSelector();
2685
2685
  }
2686
2686
 
2687
- showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
2688
- return this.#extensionUiController.showHookInput(title, placeholder);
2687
+ showHookInput(
2688
+ title: string,
2689
+ placeholder?: string,
2690
+ dialogOptions?: ExtensionUIDialogOptions,
2691
+ inputOptions?: { readonly initialValue?: string },
2692
+ ): Promise<string | undefined> {
2693
+ return this.#extensionUiController.showHookInput(title, placeholder, dialogOptions, inputOptions);
2689
2694
  }
2690
2695
 
2691
2696
  hideHookInput(): void {