@gajae-code/coding-agent 0.7.6 → 0.7.8

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 (111) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +8 -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/model-profiles.d.ts +2 -2
  8. package/dist/types/config/model-registry.d.ts +8 -3
  9. package/dist/types/config/models-config-schema.d.ts +5 -5
  10. package/dist/types/config/settings-schema.d.ts +123 -9
  11. package/dist/types/config/settings.d.ts +7 -0
  12. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  13. package/dist/types/export/html/template.generated.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
  15. package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
  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 +37 -1
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
  20. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  21. package/dist/types/modes/components/model-selector.d.ts +2 -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 +4 -3
  25. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  26. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  27. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  28. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  29. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  30. package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
  31. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  32. package/dist/types/tools/computer-gc.d.ts +23 -0
  33. package/dist/types/tools/cron.d.ts +36 -61
  34. package/dist/types/tools/index.d.ts +0 -1
  35. package/dist/types/tools/resource-gc.d.ts +54 -0
  36. package/dist/types/web/search/index.d.ts +1 -0
  37. package/dist/types/web/search/providers/utils.d.ts +11 -4
  38. package/package.json +7 -7
  39. package/src/cli/args.ts +0 -1
  40. package/src/cli/fast-help.ts +0 -1
  41. package/src/cli/local-provider-smoke.ts +632 -0
  42. package/src/cli/notify-cli.ts +4 -2
  43. package/src/cli/update-cli.ts +155 -13
  44. package/src/cli/web-search-cli.ts +5 -0
  45. package/src/cli.ts +18 -0
  46. package/src/commands/launch.ts +46 -1
  47. package/src/commands/local-provider.ts +62 -0
  48. package/src/commands/notify.ts +1 -1
  49. package/src/config/model-profiles.ts +3 -4
  50. package/src/config/model-registry.ts +58 -7
  51. package/src/config/models-config-schema.ts +10 -1
  52. package/src/config/settings-schema.ts +127 -10
  53. package/src/config/settings.ts +11 -0
  54. package/src/coordinator-mcp/server.ts +276 -34
  55. package/src/export/html/template.generated.ts +1 -1
  56. package/src/export/html/template.js +0 -12
  57. package/src/gjc-runtime/launch-tmux.ts +34 -8
  58. package/src/gjc-runtime/launch-worktree.ts +16 -2
  59. package/src/gjc-runtime/state-renderer.ts +13 -0
  60. package/src/gjc-runtime/team-runtime.ts +21 -6
  61. package/src/gjc-runtime/tmux-common.ts +16 -0
  62. package/src/gjc-runtime/tmux-sessions.ts +13 -9
  63. package/src/gjc-runtime/ultragoal-guard.ts +167 -0
  64. package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
  65. package/src/goals/tools/goal-tool.ts +14 -1
  66. package/src/internal-urls/docs-index.generated.ts +7 -7
  67. package/src/modes/components/agent-dashboard.ts +2 -1
  68. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  69. package/src/modes/components/model-selector.ts +182 -5
  70. package/src/modes/components/session-observer-overlay.ts +3 -3
  71. package/src/modes/components/settings-selector.ts +491 -5
  72. package/src/modes/components/status-line/segments.ts +7 -18
  73. package/src/modes/components/status-line/types.ts +6 -2
  74. package/src/modes/components/status-line.ts +69 -35
  75. package/src/modes/controllers/event-controller.ts +98 -4
  76. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  77. package/src/modes/controllers/input-controller.ts +15 -0
  78. package/src/modes/controllers/selector-controller.ts +18 -4
  79. package/src/modes/prompt-action-autocomplete.ts +8 -0
  80. package/src/modes/tmux-scroll.ts +92 -0
  81. package/src/modes/utils/terminal-bell.ts +48 -0
  82. package/src/notifications/telegram-daemon-cli.ts +112 -9
  83. package/src/notifications/telegram-daemon.ts +4 -0
  84. package/src/prompts/system/system-prompt.md +2 -2
  85. package/src/prompts/tools/cron.md +5 -3
  86. package/src/prompts/tools/read.md +1 -1
  87. package/src/sdk.ts +5 -0
  88. package/src/session/agent-session.ts +118 -5
  89. package/src/slash-commands/builtin-registry.ts +106 -13
  90. package/src/tools/browser/tab-supervisor.ts +86 -2
  91. package/src/tools/computer-gc.ts +66 -0
  92. package/src/tools/computer.ts +2 -0
  93. package/src/tools/cron.ts +75 -112
  94. package/src/tools/index.ts +2 -8
  95. package/src/tools/read.ts +25 -55
  96. package/src/tools/renderers.ts +0 -2
  97. package/src/tools/resource-gc.ts +291 -0
  98. package/src/tools/ultragoal-ask-guard.ts +7 -1
  99. package/src/web/search/index.ts +1 -0
  100. package/src/web/search/providers/utils.ts +22 -5
  101. package/vendor/insane-search/MANIFEST.json +3 -1
  102. package/vendor/insane-search/engine/__init__.py +14 -0
  103. package/vendor/insane-search/engine/content_safety.py +151 -0
  104. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  105. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  106. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  107. package/dist/types/tools/inspect-image.d.ts +0 -31
  108. package/src/prompts/tools/inspect-image-system.md +0 -20
  109. package/src/prompts/tools/inspect-image.md +0 -32
  110. package/src/tools/inspect-image-renderer.ts +0 -103
  111. package/src/tools/inspect-image.ts +0 -172
@@ -40,6 +40,7 @@ export interface StatusLineSettings {
40
40
  rightSegments?: StatusLineSegmentId[];
41
41
  separator?: StatusLineSeparatorStyle;
42
42
  segmentOptions?: StatusLineSegmentOptions;
43
+ previewHighlightSegment?: StatusLineSegmentId;
43
44
  showHookStatus?: boolean;
44
45
  showSkillHud?: boolean;
45
46
  sessionAccent?: boolean;
@@ -181,11 +182,8 @@ export class StatusLineComponent implements Component {
181
182
  #lastTokensPerSecond: number | null = null;
182
183
  #lastTokensPerSecondTimestamp: number | null = null;
183
184
 
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;
185
+ // Provider usage caching (5-min TTL, OAuth/sub only)
186
+ #cachedUsage: SegmentContext["usage"] = null;
189
187
  #usageFetchedAt = 0;
190
188
  #usageInFlight = false;
191
189
  // Context breakdown — incremental cache. Replaces the previous 2-second
@@ -221,7 +219,7 @@ export class StatusLineComponent implements Component {
221
219
  }
222
220
 
223
221
  updateSettings(settings: StatusLineSettings): void {
224
- this.#settings = { ...this.#settings, ...settings };
222
+ this.#settings = { ...this.#settings, previewHighlightSegment: undefined, ...settings };
225
223
  }
226
224
 
227
225
  setAutoCompactEnabled(enabled: boolean): void {
@@ -462,9 +460,9 @@ export class StatusLineComponent implements Component {
462
460
  }
463
461
 
464
462
  /**
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.
463
+ * Background-refresh the OAuth quota report. Guarded by a 5-min TTL on both
464
+ * success (cache lifetime) and error (backoff). Exposed (non-private) so
465
+ * unit tests can verify the backoff invariant.
468
466
  */
469
467
  refreshUsageInBackground(): void {
470
468
  const now = Date.now();
@@ -478,6 +476,9 @@ export class StatusLineComponent implements Component {
478
476
  .then(reports => {
479
477
  this.#cachedUsage = this.#normalizeUsageReports(reports);
480
478
  this.#usageFetchedAt = Date.now();
479
+ if (this.#onBranchChange) {
480
+ this.#onBranchChange();
481
+ }
481
482
  })
482
483
  .catch(() => {
483
484
  // Backoff on error: stamp the fetch time so the 5-min TTL guard
@@ -491,47 +492,77 @@ export class StatusLineComponent implements Component {
491
492
  });
492
493
  }
493
494
 
494
- #normalizeUsageReports(reports: unknown): {
495
- fiveHour?: { percent: number; resetMinutes?: number };
496
- sevenDay?: { percent: number; resetHours?: number };
497
- } | null {
495
+ #normalizeUsageReports(reports: unknown): SegmentContext["usage"] {
498
496
  if (!Array.isArray(reports)) return null;
499
- let fiveHour: { percent: number; resetMinutes?: number } | undefined;
500
- let sevenDay: { percent: number; resetHours?: number } | undefined;
497
+ const windows: NonNullable<SegmentContext["usage"]>["windows"] = [];
498
+ const seen = new Set<string>();
501
499
  const now = Date.now();
500
+
501
+ const codexWindowLabel = (windowId: string | undefined, fallback: string): string => {
502
+ if (windowId && /^\d+[hd]$/.test(windowId)) return windowId;
503
+ return fallback;
504
+ };
505
+ const codexResetUnit = (label: string): "m" | "h" => (label.endsWith("d") ? "h" : "m");
506
+
507
+ const pushWindow = (
508
+ key: string,
509
+ label: string,
510
+ fraction: number,
511
+ resetsAt: number | undefined,
512
+ resetUnit: "m" | "h",
513
+ ) => {
514
+ if (seen.has(key)) return;
515
+ seen.add(key);
516
+ windows.push({
517
+ label,
518
+ percent: fraction * 100,
519
+ resetValue:
520
+ typeof resetsAt === "number"
521
+ ? Math.max(0, Math.round((resetsAt - now) / (resetUnit === "m" ? 60_000 : 3_600_000)))
522
+ : undefined,
523
+ resetUnit,
524
+ });
525
+ };
526
+
502
527
  for (const report of reports) {
503
528
  if (!report || typeof report !== "object") continue;
529
+ const provider = (report as { provider?: unknown }).provider;
530
+ const providerId = typeof provider === "string" ? provider : undefined;
504
531
  const limits = (report as { limits?: unknown }).limits;
505
532
  if (!Array.isArray(limits)) continue;
506
533
  for (const limit of limits) {
507
534
  if (!limit || typeof limit !== "object") continue;
508
535
  const l = limit as {
509
- scope?: { windowId?: string; tier?: string };
510
- window?: { resetsAt?: number };
536
+ id?: unknown;
537
+ scope?: { windowId?: string; tier?: string; modelId?: string };
538
+ window?: { id?: string; resetsAt?: number };
511
539
  amount?: { usedFraction?: number };
512
540
  };
513
541
  const fraction = l.amount?.usedFraction;
514
542
  if (typeof fraction !== "number") continue;
515
- const windowId = l.scope?.windowId;
543
+ const id = typeof l.id === "string" ? l.id : "";
544
+ const windowId = l.scope?.windowId ?? l.window?.id;
516
545
  const tier = l.scope?.tier;
546
+ const modelId = l.scope?.modelId;
517
547
  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
- };
548
+
549
+ if (providerId === "openai-codex") {
550
+ if (id === "openai-codex:primary" || (!id && !!windowId && windowId !== "7d" && !modelId)) {
551
+ const label = codexWindowLabel(windowId, "primary");
552
+ pushWindow("codex:primary", label, fraction, resetsAt, codexResetUnit(label));
553
+ } else if (id === "openai-codex:secondary" || (!id && windowId === "7d" && !modelId)) {
554
+ const label = codexWindowLabel(windowId, "secondary");
555
+ pushWindow("codex:secondary", label, fraction, resetsAt, codexResetUnit(label));
556
+ }
557
+ } else if (windowId === "5h" && !tier) {
558
+ pushWindow(`${providerId ?? "provider"}:5h`, "5h", fraction, resetsAt, "m");
559
+ } else if (windowId === "7d" && !tier) {
560
+ pushWindow(`${providerId ?? "provider"}:7d`, "7d", fraction, resetsAt, "h");
530
561
  }
531
562
  }
532
563
  }
533
- if (!fiveHour && !sevenDay) return null;
534
- return { fiveHour, sevenDay };
564
+
565
+ return windows.length > 0 ? { windows } : null;
535
566
  }
536
567
 
537
568
  /**
@@ -684,12 +715,15 @@ export class StatusLineComponent implements Component {
684
715
  const sepAnsi = theme.getFgAnsi("statusLineSep");
685
716
 
686
717
  // Collect visible segment contents
718
+ const highlightSegment = (segId: StatusLineSegmentId, content: string): string =>
719
+ effectiveSettings.previewHighlightSegment === segId ? `\x1b[7m${content}\x1b[27m` : content;
720
+
687
721
  const leftParts: string[] = [];
688
722
  const leftSegIds: StatusLineSegmentId[] = [];
689
723
  for (const segId of effectiveSettings.leftSegments) {
690
724
  const rendered = renderSegment(segId, ctx);
691
725
  if (rendered.visible && rendered.content) {
692
- leftParts.push(rendered.content);
726
+ leftParts.push(highlightSegment(segId, rendered.content));
693
727
  leftSegIds.push(segId);
694
728
  }
695
729
  }
@@ -698,7 +732,7 @@ export class StatusLineComponent implements Component {
698
732
  for (const segId of effectiveSettings.rightSegments) {
699
733
  const rendered = renderSegment(segId, ctx);
700
734
  if (rendered.visible && rendered.content) {
701
- rightParts.push(rendered.content);
735
+ rightParts.push(highlightSegment(segId, rendered.content));
702
736
  }
703
737
  }
704
738
 
@@ -765,7 +799,7 @@ export class StatusLineComponent implements Component {
765
799
  if (!adjusted.visible || !adjusted.content) break;
766
800
  reRendered = adjusted;
767
801
  }
768
- left[pathIdx] = reRendered.content;
802
+ left[pathIdx] = highlightSegment("path", reRendered.content);
769
803
  leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
770
804
  }
771
805
  }
@@ -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,
@@ -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) {
@@ -8,6 +8,7 @@ import { activateModelProfile, materializeActiveModelProfileAssignment } from ".
8
8
  import { recommendModelProfileForProvider } from "../../config/model-profiles";
9
9
  import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
10
10
  import { formatModelSelectorValue } from "../../config/model-resolver";
11
+ import type { ModelProfileConfig } from "../../config/models-config-schema";
11
12
  import { settings } from "../../config/settings";
12
13
  import { DebugSelectorComponent } from "../../debug";
13
14
  import { disableProvider, enableProvider } from "../../discovery";
@@ -51,6 +52,7 @@ import {
51
52
  setPreferredImageProvider,
52
53
  setPreferredSearchProvider,
53
54
  setSearchFallbackProviders,
55
+ setSearchHardTimeoutMs,
54
56
  } from "../../tools";
55
57
  import { setSessionTerminalTitle } from "../../utils/title-generator";
56
58
  import { AgentDashboard } from "../components/agent-dashboard";
@@ -238,7 +240,7 @@ export class SelectorController {
238
240
  this.ctx.ui.requestRender();
239
241
  }
240
242
 
241
- showCustomModelPresetWizard(): void {
243
+ showCustomModelPresetWizard(snapshot: ModelProfileConfig): void {
242
244
  this.showSelector(done => {
243
245
  let wizard: CustomModelPresetWizardComponent;
244
246
  const submit = async (input: CustomModelPresetWizardSubmit): Promise<void> => {
@@ -255,6 +257,7 @@ export class SelectorController {
255
257
  }
256
258
  };
257
259
  wizard = new CustomModelPresetWizardComponent(
260
+ snapshot,
258
261
  input => {
259
262
  void submit(input);
260
263
  },
@@ -335,14 +338,16 @@ export class SelectorController {
335
338
  separator: settings.get("statusLine.separator"),
336
339
  showHookStatus: settings.get("statusLine.showHookStatus"),
337
340
  sessionAccent: settings.get("statusLine.sessionAccent"),
341
+ segmentOptions: settings.get("statusLine.segmentOptions"),
338
342
  ...previewSettings,
339
343
  });
340
344
  this.ctx.updateEditorTopBorder();
341
345
  this.ctx.ui.requestRender();
342
346
  },
343
- getStatusLinePreview: () => {
347
+ getStatusLinePreview: (width?: number) => {
344
348
  // Return the rendered status line for inline preview
345
- const availableWidth = this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
349
+ const availableWidth =
350
+ width ?? this.ctx.editor.getTopBorderAvailableWidth(this.ctx.ui.terminal.columns);
346
351
  return this.ctx.statusLine.getTopBorder(availableWidth).content;
347
352
  },
348
353
  onPluginsChanged: () => {
@@ -358,6 +363,7 @@ export class SelectorController {
358
363
  separator: settings.get("statusLine.separator"),
359
364
  showHookStatus: settings.get("statusLine.showHookStatus"),
360
365
  sessionAccent: settings.get("statusLine.sessionAccent"),
366
+ segmentOptions: settings.get("statusLine.segmentOptions"),
361
367
  });
362
368
  this.ctx.updateEditorTopBorder();
363
369
  this.ctx.ui.requestRender();
@@ -602,6 +608,9 @@ export class SelectorController {
602
608
  case "statusLineShowHooks":
603
609
  case "statusLine.showHookStatus":
604
610
  case "statusLine.sessionAccent":
611
+ case "statusLine.leftSegments":
612
+ case "statusLine.rightSegments":
613
+ case "statusLine.segmentOptions":
605
614
  case "statusLineSegments":
606
615
  case "statusLineModelThinking":
607
616
  case "statusLinePathAbbreviate":
@@ -641,6 +650,11 @@ export class SelectorController {
641
650
  );
642
651
  }
643
652
  break;
653
+ case "web_search.timeout":
654
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
655
+ setSearchHardTimeoutMs(value * 1000);
656
+ }
657
+ break;
644
658
  case "providers.image":
645
659
  if (
646
660
  value === "auto" ||
@@ -676,7 +690,7 @@ export class SelectorController {
676
690
  try {
677
691
  if (selection.kind === "createProfile") {
678
692
  done();
679
- this.showCustomModelPresetWizard();
693
+ this.showCustomModelPresetWizard(selection.profile);
680
694
  return;
681
695
  }
682
696
  if (selection.kind === "profile") {
@@ -29,6 +29,7 @@ interface PromptActionAutocompleteOptions {
29
29
  copyCurrentLine: () => void;
30
30
  copyPrompt: () => void;
31
31
  pasteImage: () => void;
32
+ scrollTmuxToPreviousUserInput: () => void;
32
33
  undo: (prefix: string) => void;
33
34
  moveCursorToMessageEnd: () => void;
34
35
  moveCursorToMessageStart: () => void;
@@ -378,6 +379,13 @@ export function createPromptActionAutocompleteProvider(
378
379
  keywords: ["paste", "image", "clipboard", "screenshot", "attach", "vision"],
379
380
  execute: options.pasteImage,
380
381
  },
382
+ {
383
+ id: "tmux-previous-user-input",
384
+ label: "Scroll to previous user input",
385
+ description: "tmux copy-mode",
386
+ keywords: ["scroll", "tmux", "previous", "user", "input", "prompt", "history"],
387
+ execute: options.scrollTmuxToPreviousUserInput,
388
+ },
381
389
  {
382
390
  id: "undo",
383
391
  label: "Undo",
@@ -0,0 +1,92 @@
1
+ import { resolveGjcTmuxCommand } from "../gjc-runtime/tmux-common";
2
+
3
+ // tmux copy-mode pads lines to the pane width, so allow trailing spaces before
4
+ // the line end while keeping the match scoped to the standalone `user` label.
5
+ export const TMUX_PREVIOUS_USER_INPUT_SEARCH_PATTERN = "^ *user *$";
6
+
7
+ type TmuxScrollFailureReason =
8
+ | "not_inside_tmux"
9
+ | "tmux_command_failed"
10
+ | "copy_mode_failed"
11
+ | "history_bottom_failed"
12
+ | "search_failed";
13
+
14
+ export type TmuxScrollToPreviousUserInputResult =
15
+ | { ok: true }
16
+ | { ok: false; reason: TmuxScrollFailureReason; error?: string };
17
+
18
+ interface TmuxSpawnResult {
19
+ exitCode: number | null;
20
+ stderr?: { toString(): string };
21
+ }
22
+
23
+ type TmuxSpawnSync = (command: string, args: string[], env: NodeJS.ProcessEnv) => TmuxSpawnResult;
24
+
25
+ function defaultSpawnSync(command: string, args: string[], env: NodeJS.ProcessEnv): TmuxSpawnResult {
26
+ return Bun.spawnSync([command, ...args], {
27
+ env,
28
+ stdout: "ignore",
29
+ stderr: "pipe",
30
+ });
31
+ }
32
+
33
+ function getErrorText(result: TmuxSpawnResult): string | undefined {
34
+ const text = result.stderr?.toString().trim();
35
+ return text ? text : undefined;
36
+ }
37
+
38
+ function runTmuxStep(
39
+ command: string,
40
+ args: string[],
41
+ env: NodeJS.ProcessEnv,
42
+ spawnSync: TmuxSpawnSync,
43
+ reason: TmuxScrollFailureReason,
44
+ ): TmuxScrollToPreviousUserInputResult {
45
+ try {
46
+ const result = spawnSync(command, args, env);
47
+ if (result.exitCode === 0) return { ok: true };
48
+ return { ok: false, reason, error: getErrorText(result) };
49
+ } catch (error) {
50
+ return { ok: false, reason, error: error instanceof Error ? error.message : String(error) };
51
+ }
52
+ }
53
+
54
+ export function scrollTmuxToPreviousUserInput(
55
+ env: NodeJS.ProcessEnv = process.env,
56
+ spawnSync: TmuxSpawnSync = defaultSpawnSync,
57
+ ): TmuxScrollToPreviousUserInputResult {
58
+ if (!env.TMUX) return { ok: false, reason: "not_inside_tmux" };
59
+
60
+ let tmuxCommand: string;
61
+ try {
62
+ tmuxCommand = resolveGjcTmuxCommand(env);
63
+ } catch (error) {
64
+ return {
65
+ ok: false,
66
+ reason: "tmux_command_failed",
67
+ error: error instanceof Error ? error.message : String(error),
68
+ };
69
+ }
70
+
71
+ const targetPane = env.TMUX_PANE?.trim();
72
+ const targetArgs = targetPane ? ["-t", targetPane] : [];
73
+ const copyModeResult = runTmuxStep(tmuxCommand, ["copy-mode", ...targetArgs], env, spawnSync, "copy_mode_failed");
74
+ if (!copyModeResult.ok) return copyModeResult;
75
+
76
+ const historyBottomResult = runTmuxStep(
77
+ tmuxCommand,
78
+ ["send-keys", ...targetArgs, "-X", "history-bottom"],
79
+ env,
80
+ spawnSync,
81
+ "history_bottom_failed",
82
+ );
83
+ if (!historyBottomResult.ok) return historyBottomResult;
84
+
85
+ return runTmuxStep(
86
+ tmuxCommand,
87
+ ["send-keys", ...targetArgs, "-X", "search-backward", TMUX_PREVIOUS_USER_INPUT_SEARCH_PATTERN],
88
+ env,
89
+ spawnSync,
90
+ "search_failed",
91
+ );
92
+ }
@@ -0,0 +1,48 @@
1
+ import { settings } from "../../config/settings";
2
+
3
+ export type TerminalBellEvent = "complete" | "approval" | "ask";
4
+
5
+ const BEL = "\x07";
6
+
7
+ function getBellSetting(key: Parameters<typeof settings.get>[0]): boolean {
8
+ try {
9
+ return Boolean(settings.get(key));
10
+ } catch {
11
+ // Some component-level tests exercise UI helpers before Settings.init().
12
+ // Terminal bells are purely best-effort, so an unavailable settings store
13
+ // must not make the UI path throw.
14
+ return false;
15
+ }
16
+ }
17
+
18
+ function enabledForEvent(event: TerminalBellEvent): boolean {
19
+ if (!getBellSetting("notifications.terminalBell")) return false;
20
+ switch (event) {
21
+ case "complete":
22
+ return getBellSetting("notifications.bellOnComplete");
23
+ case "approval":
24
+ return getBellSetting("notifications.bellOnApproval");
25
+ case "ask":
26
+ return getBellSetting("notifications.bellOnAsk");
27
+ }
28
+ }
29
+
30
+ export function ringTerminalBell(
31
+ event: TerminalBellEvent,
32
+ output: Pick<NodeJS.WriteStream, "write"> = process.stdout,
33
+ ): void {
34
+ if (!enabledForEvent(event)) return;
35
+ try {
36
+ output.write(BEL);
37
+ } catch {
38
+ // Best-effort local notification only.
39
+ }
40
+ }
41
+
42
+ export function classifyHookSelectorBellEvent(title: string): TerminalBellEvent {
43
+ const normalized = title.toLowerCase();
44
+ if (normalized.includes("approval") || normalized.includes("approve") || normalized.includes("plan ready")) {
45
+ return "approval";
46
+ }
47
+ return "ask";
48
+ }