@gajae-code/coding-agent 0.4.5 → 0.5.1

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 (185) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/types/async/job-manager.d.ts +26 -0
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli/list-models.d.ts +6 -0
  5. package/dist/types/commands/gc.d.ts +26 -0
  6. package/dist/types/commands/harness.d.ts +3 -0
  7. package/dist/types/config/file-lock-gc.d.ts +5 -0
  8. package/dist/types/config/file-lock.d.ts +7 -0
  9. package/dist/types/config/model-profile-activation.d.ts +11 -2
  10. package/dist/types/config/model-profiles.d.ts +7 -0
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/model-resolver.d.ts +2 -0
  13. package/dist/types/config/models-config-schema.d.ts +30 -0
  14. package/dist/types/config/settings-schema.d.ts +4 -3
  15. package/dist/types/coordinator/contract.d.ts +1 -1
  16. package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
  17. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
  18. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
  24. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
  25. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
  26. package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
  27. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  28. package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
  29. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
  30. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
  31. package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
  32. package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
  33. package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
  34. package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
  35. package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
  36. package/dist/types/gjc-runtime/team-runtime.d.ts +5 -1
  37. package/dist/types/gjc-runtime/tmux-common.d.ts +14 -0
  38. package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
  39. package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
  40. package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
  41. package/dist/types/harness-control-plane/owner.d.ts +8 -1
  42. package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
  43. package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
  44. package/dist/types/harness-control-plane/storage.d.ts +20 -0
  45. package/dist/types/harness-control-plane/types.d.ts +4 -0
  46. package/dist/types/hindsight/mental-models.d.ts +5 -5
  47. package/dist/types/modes/components/hook-selector.d.ts +7 -1
  48. package/dist/types/modes/components/model-selector.d.ts +1 -12
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
  51. package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
  52. package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
  53. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
  54. package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
  56. package/dist/types/sdk.d.ts +5 -0
  57. package/dist/types/session/agent-session.d.ts +3 -1
  58. package/dist/types/session/blob-store.d.ts +59 -4
  59. package/dist/types/session/session-manager.d.ts +24 -6
  60. package/dist/types/session/streaming-output.d.ts +3 -2
  61. package/dist/types/session/tool-choice-queue.d.ts +6 -0
  62. package/dist/types/skill-state/workflow-hud.d.ts +14 -0
  63. package/dist/types/task/receipt.d.ts +1 -0
  64. package/dist/types/task/types.d.ts +7 -0
  65. package/dist/types/thinking-metadata.d.ts +16 -0
  66. package/dist/types/thinking.d.ts +3 -12
  67. package/dist/types/tools/ask.d.ts +15 -1
  68. package/dist/types/tools/index.d.ts +2 -0
  69. package/dist/types/tools/resolve.d.ts +0 -10
  70. package/dist/types/tools/subagent.d.ts +6 -0
  71. package/dist/types/utils/tool-choice.d.ts +14 -1
  72. package/package.json +7 -7
  73. package/src/async/job-manager.ts +52 -0
  74. package/src/cli/args.ts +3 -0
  75. package/src/cli/auth-broker-cli.ts +1 -0
  76. package/src/cli/list-models.ts +13 -1
  77. package/src/cli.ts +9 -4
  78. package/src/commands/gc.ts +22 -0
  79. package/src/commands/harness.ts +43 -5
  80. package/src/commands/launch.ts +2 -2
  81. package/src/commands/session.ts +3 -1
  82. package/src/config/file-lock-gc.ts +181 -0
  83. package/src/config/file-lock.ts +14 -0
  84. package/src/config/model-profile-activation.ts +15 -3
  85. package/src/config/model-profiles.ts +264 -56
  86. package/src/config/model-resolver.ts +9 -6
  87. package/src/config/models-config-schema.ts +1 -0
  88. package/src/config/settings-schema.ts +6 -3
  89. package/src/coordinator/contract.ts +1 -0
  90. package/src/coordinator-mcp/server.ts +513 -26
  91. package/src/cursor.ts +16 -2
  92. package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
  93. package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
  94. package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
  95. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
  96. package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
  97. package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
  98. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
  99. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
  100. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
  101. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
  102. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
  103. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
  104. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
  105. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
  106. package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
  107. package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
  108. package/src/defaults/gjc/skills/team/SKILL.md +3 -2
  109. package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
  110. package/src/defaults/gjc-defaults.ts +7 -0
  111. package/src/defaults/gjc-grok-cli.ts +22 -0
  112. package/src/export/html/index.ts +13 -9
  113. package/src/extensibility/extensions/index.ts +1 -0
  114. package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
  115. package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
  116. package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
  117. package/src/gjc-runtime/deep-interview-state.ts +324 -0
  118. package/src/gjc-runtime/gc-render.ts +70 -0
  119. package/src/gjc-runtime/gc-runtime.ts +403 -0
  120. package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
  121. package/src/gjc-runtime/ralplan-runtime.ts +58 -7
  122. package/src/gjc-runtime/state-renderer.ts +12 -3
  123. package/src/gjc-runtime/state-runtime.ts +46 -29
  124. package/src/gjc-runtime/team-gc.ts +49 -0
  125. package/src/gjc-runtime/team-runtime.ts +211 -8
  126. package/src/gjc-runtime/tmux-common.ts +29 -0
  127. package/src/gjc-runtime/tmux-gc.ts +176 -0
  128. package/src/gjc-runtime/tmux-sessions.ts +68 -12
  129. package/src/gjc-runtime/ultragoal-runtime.ts +517 -41
  130. package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
  131. package/src/gjc-runtime/workflow-manifest.ts +16 -1
  132. package/src/harness-control-plane/gc-adapter.ts +184 -0
  133. package/src/harness-control-plane/owner.ts +89 -27
  134. package/src/harness-control-plane/receipt-spool.ts +128 -0
  135. package/src/harness-control-plane/state-machine.ts +27 -6
  136. package/src/harness-control-plane/storage.ts +93 -0
  137. package/src/harness-control-plane/types.ts +4 -0
  138. package/src/hindsight/mental-models.ts +17 -16
  139. package/src/internal-urls/docs-index.generated.ts +14 -8
  140. package/src/main.ts +7 -2
  141. package/src/modes/components/assistant-message.ts +26 -14
  142. package/src/modes/components/diff.ts +97 -0
  143. package/src/modes/components/hook-selector.ts +19 -0
  144. package/src/modes/components/model-selector.ts +370 -181
  145. package/src/modes/components/status-line/segments.ts +1 -1
  146. package/src/modes/components/tool-execution.ts +30 -13
  147. package/src/modes/controllers/command-controller.ts +25 -6
  148. package/src/modes/controllers/extension-ui-controller.ts +3 -0
  149. package/src/modes/controllers/selector-controller.ts +34 -42
  150. package/src/modes/rpc/rpc-client.ts +3 -2
  151. package/src/modes/rpc/rpc-mode.ts +187 -39
  152. package/src/modes/rpc/rpc-types.ts +5 -2
  153. package/src/modes/shared/agent-wire/command-dispatch.ts +279 -257
  154. package/src/modes/shared/agent-wire/command-validation.ts +11 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
  156. package/src/modes/shared/agent-wire/session-registry.ts +109 -0
  157. package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
  158. package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
  159. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  160. package/src/sdk.ts +46 -5
  161. package/src/secrets/obfuscator.ts +102 -27
  162. package/src/session/agent-session.ts +179 -25
  163. package/src/session/blob-store.ts +148 -6
  164. package/src/session/session-manager.ts +311 -60
  165. package/src/session/streaming-output.ts +185 -122
  166. package/src/session/tool-choice-queue.ts +23 -0
  167. package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
  168. package/src/skill-state/workflow-hud.ts +106 -10
  169. package/src/slash-commands/builtin-registry.ts +3 -2
  170. package/src/task/executor.ts +78 -6
  171. package/src/task/receipt.ts +5 -0
  172. package/src/task/render.ts +21 -1
  173. package/src/task/types.ts +8 -0
  174. package/src/thinking-metadata.ts +51 -0
  175. package/src/thinking.ts +26 -46
  176. package/src/tools/ask.ts +56 -1
  177. package/src/tools/bash.ts +1 -1
  178. package/src/tools/index.ts +2 -0
  179. package/src/tools/job.ts +3 -2
  180. package/src/tools/monitor.ts +36 -1
  181. package/src/tools/resolve.ts +93 -18
  182. package/src/tools/subagent-render.ts +9 -0
  183. package/src/tools/subagent.ts +26 -2
  184. package/src/utils/edit-mode.ts +1 -1
  185. package/src/utils/tool-choice.ts +45 -16
@@ -66,7 +66,7 @@ function classifyProjectDir(pwd: string): { scratch: boolean; relative: string |
66
66
  const gajaeSegment: StatusLineSegment = {
67
67
  id: "gajae",
68
68
  render(_ctx) {
69
- return { content: theme.fg("accent", "GJC"), visible: true };
69
+ return { content: theme.fg("accent", "🦞"), visible: true };
70
70
  },
71
71
  };
72
72
 
@@ -152,6 +152,7 @@ export class ToolExecutionComponent extends Container {
152
152
  isError?: boolean;
153
153
  details?: any;
154
154
  };
155
+ #textOutputCache?: { content: unknown; showImages: boolean; terminalImageProtocol: unknown; output: string };
155
156
  // Edit preview state
156
157
  #editMode?: EditMode;
157
158
  #editDiffPreview?: PerFileDiffPreview[];
@@ -197,9 +198,11 @@ export class ToolExecutionComponent extends Container {
197
198
 
198
199
  this.addChild(new Spacer(1));
199
200
 
200
- // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
201
- this.#contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
202
- this.#contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
201
+ // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
202
+ // Vertical padding is 0: block separation comes solely from the leading Spacer
203
+ // (1 blank line above each block), matching reference TUIs (083.2).
204
+ this.#contentBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
205
+ this.#contentText = new Text("", 1, 0, (text: string) => theme.bg("toolPendingBg", text));
203
206
 
204
207
  // Use Box for custom tools or built-in tools that have renderers
205
208
  const hasRenderer = toolName in toolRenderers;
@@ -295,6 +298,7 @@ export class ToolExecutionComponent extends Container {
295
298
  isPartial = false,
296
299
  _toolCallId?: string,
297
300
  ): void {
301
+ this.#textOutputCache = undefined;
298
302
  this.#result = result;
299
303
  this.#isPartial = isPartial;
300
304
  // When tool is complete, ensure args are marked complete so spinner stops
@@ -397,6 +401,7 @@ export class ToolExecutionComponent extends Container {
397
401
 
398
402
  setShowImages(show: boolean): void {
399
403
  this.#showImages = show;
404
+ this.#textOutputCache = undefined;
400
405
  this.#updateDisplay();
401
406
  }
402
407
 
@@ -514,7 +519,7 @@ export class ToolExecutionComponent extends Container {
514
519
  const fileBgFn = fileResult.isError
515
520
  ? (text: string) => theme.bg("toolErrorBg", text)
516
521
  : (text: string) => theme.bg("toolSuccessBg", text);
517
- const fileBox = new Box(1, 1, fileBgFn);
522
+ const fileBox = new Box(1, 0, fileBgFn);
518
523
  try {
519
524
  const resultComponent = renderer.renderResult(
520
525
  { content: [], details: fileResult, isError: fileResult.isError },
@@ -540,7 +545,7 @@ export class ToolExecutionComponent extends Container {
540
545
  const pendingSpacer = new Spacer(1);
541
546
  this.#multiFileBoxes.push(pendingSpacer);
542
547
  this.addChild(pendingSpacer);
543
- const pendingBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
548
+ const pendingBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
544
549
  const pendingText = renderStatusLine(
545
550
  {
546
551
  icon: "pending",
@@ -723,16 +728,27 @@ export class ToolExecutionComponent extends Container {
723
728
  #getTextOutput(): string {
724
729
  if (!this.#result) return "";
725
730
 
726
- const textBlocks = this.#result.content?.filter((c: any) => c.type === "text") || [];
727
- const imageBlocks = this.#getAllImageBlocks();
731
+ const content = this.#result.content;
732
+ const terminalImageProtocol = TERMINAL.imageProtocol;
733
+ const cached = this.#textOutputCache;
734
+ if (
735
+ cached?.content === content &&
736
+ cached.showImages === this.#showImages &&
737
+ cached.terminalImageProtocol === terminalImageProtocol
738
+ ) {
739
+ return cached.output;
740
+ }
728
741
 
729
- let output = textBlocks
730
- .map((c: any) => {
731
- return sanitizeWithOptionalSixelPassthrough(c.text || "", sanitizeText);
732
- })
733
- .join("\n");
742
+ const textParts: string[] = [];
743
+ for (const block of content ?? []) {
744
+ if (block.type !== "text") continue;
745
+ const text = block.text || "";
746
+ textParts.push(sanitizeWithOptionalSixelPassthrough(text, sanitizeText));
747
+ }
748
+ let output = textParts.join("\n");
734
749
 
735
- if (imageBlocks.length > 0 && (!TERMINAL.imageProtocol || !this.#showImages)) {
750
+ const imageBlocks = this.#getAllImageBlocks();
751
+ if (imageBlocks.length > 0 && (!terminalImageProtocol || !this.#showImages)) {
736
752
  const imageIndicators = imageBlocks
737
753
  .map((img: any) => {
738
754
  const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
@@ -742,6 +758,7 @@ export class ToolExecutionComponent extends Container {
742
758
  output = output ? `${output}\n${imageIndicators}` : imageIndicators;
743
759
  }
744
760
 
761
+ this.#textOutputCache = { content, showImages: this.#showImages, terminalImageProtocol, output };
745
762
  return output;
746
763
  }
747
764
 
@@ -13,6 +13,7 @@ import {
13
13
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
14
14
  import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
15
15
  import { $ } from "bun";
16
+ import { jobElapsedMs } from "../../async";
16
17
  import { reset as resetCapabilities } from "../../capability";
17
18
  import { clearClaudePluginRootsCache } from "../../discovery/helpers";
18
19
  import { loadCustomShare } from "../../export/custom-share";
@@ -1279,7 +1280,7 @@ const BAR_WIDTH_MAX = 24;
1279
1280
  const BAR_WIDTH_MIN = 4;
1280
1281
 
1281
1282
  function renderJobLine(job: AsyncJobSnapshotItem, now: number): string {
1282
- const duration = formatDuration(Math.max(0, now - job.startTime));
1283
+ const duration = formatDuration(jobElapsedMs(job, now));
1283
1284
  const status = formatJobStatus(job.status);
1284
1285
  return `${theme.fg("dim", job.id)} ${theme.fg("dim", `[${job.type}]`)} ${status} ${theme.fg("dim", `(${duration})`)}`;
1285
1286
  }
@@ -1538,7 +1539,7 @@ function resolveColumnWidth(count: number, available: number, trailing: number):
1538
1539
  return ideal;
1539
1540
  }
1540
1541
 
1541
- function renderUsageReports(
1542
+ export function renderUsageReports(
1542
1543
  reports: UsageReport[],
1543
1544
  uiTheme: typeof theme,
1544
1545
  nowMs: number,
@@ -1592,17 +1593,35 @@ function renderUsageReports(
1592
1593
 
1593
1594
  lines.push(uiTheme.bold(uiTheme.fg("accent", providerName)));
1594
1595
 
1596
+ // Stable account column order shared across every window group, so each
1597
+ // account keeps the same column in the 5h / 7d / ... rows. Rank accounts
1598
+ // by total usage across their windows (a per-account value identical for
1599
+ // every group), tie-broken by label for determinism.
1600
+ const rankedAccounts = providerReports
1601
+ .map(report => {
1602
+ const [firstLimit] = report.limits;
1603
+ return {
1604
+ report,
1605
+ total: report.limits.reduce((sum, limit) => sum + (resolveFraction(limit) ?? 0), 0),
1606
+ label: firstLimit ? formatAccountLabel(firstLimit, report, 0) : formatUnlimitedReportLabel(report, 0),
1607
+ };
1608
+ })
1609
+ .sort((a, b) => (a.total !== b.total ? b.total - a.total : a.label.localeCompare(b.label)));
1610
+ const accountRank = new Map<UsageReport, number>();
1611
+ rankedAccounts.forEach((entry, rank) => {
1612
+ accountRank.set(entry.report, rank);
1613
+ });
1614
+
1595
1615
  const renderableGroups = Array.from(limitGroups.values()).map(group => {
1596
1616
  const entries = group.limits.map((limit, index) => ({
1597
1617
  limit,
1598
1618
  report: group.reports[index],
1599
- fraction: resolveFraction(limit),
1600
1619
  index,
1601
1620
  }));
1602
1621
  entries.sort((a, b) => {
1603
- const aFraction = a.fraction ?? -1;
1604
- const bFraction = b.fraction ?? -1;
1605
- if (aFraction !== bFraction) return bFraction - aFraction;
1622
+ const aRank = accountRank.get(a.report) ?? Number.MAX_SAFE_INTEGER;
1623
+ const bRank = accountRank.get(b.report) ?? Number.MAX_SAFE_INTEGER;
1624
+ if (aRank !== bRank) return aRank - bRank;
1606
1625
  return a.index - b.index;
1607
1626
  });
1608
1627
  const sortedLimits = entries.map(entry => entry.limit);
@@ -645,6 +645,9 @@ export class ExtensionUiController {
645
645
  timeout: dialogOptions?.timeout,
646
646
  onTimeout: dialogOptions?.onTimeout,
647
647
  tui: this.ctx.ui,
648
+ // Share the main prompt editor's autocomplete provider so the
649
+ // inline "Other (type your own)" editor supports `@` file links.
650
+ autocompleteProvider: dialogOptions?.customInput ? this.ctx.editor.getAutocompleteProvider() : undefined,
648
651
  outline: dialogOptions?.outline,
649
652
  wrapFocused: dialogOptions?.wrapFocused,
650
653
  scrollTitleRows,
@@ -5,6 +5,7 @@ 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
7
  import { activateModelProfile } from "../../config/model-profile-activation";
8
+ import { recommendModelProfileForProvider } from "../../config/model-profiles";
8
9
  import { settings } from "../../config/settings";
9
10
  import { DebugSelectorComponent } from "../../debug";
10
11
  import { disableProvider, enableProvider } from "../../discovery";
@@ -45,7 +46,7 @@ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "
45
46
  import { ExtensionDashboard } from "../components/extensions";
46
47
  import { HistorySearchComponent } from "../components/history-search";
47
48
  import { JobsOverlayComponent } from "../components/jobs-overlay";
48
- import { ModelSelectorComponent, type ModelSelectorSelection } from "../components/model-selector";
49
+ import { ModelSelectorComponent } from "../components/model-selector";
49
50
  import { OAuthSelectorComponent } from "../components/oauth-selector";
50
51
  import { PluginSelectorComponent } from "../components/plugin-selector";
51
52
  import {
@@ -69,6 +70,7 @@ const CALLBACK_SERVER_PROVIDERS = new Set<string>([
69
70
  "google-gemini-cli",
70
71
  "google-antigravity",
71
72
  "xai",
73
+ "grok-build",
72
74
  ]);
73
75
 
74
76
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
@@ -537,12 +539,6 @@ export class SelectorController {
537
539
  this.ctx.session.scopedModels,
538
540
  async selection => {
539
541
  try {
540
- if (selection.kind === "preset") {
541
- await this.#applyModelAssignmentPreset(selection);
542
- done();
543
- this.ctx.ui.requestRender();
544
- return;
545
- }
546
542
  if (selection.kind === "profile") {
547
543
  await activateModelProfile(
548
544
  {
@@ -609,45 +605,12 @@ export class SelectorController {
609
605
  done();
610
606
  this.ctx.ui.requestRender();
611
607
  },
612
- options,
608
+ { ...options, sessionId: this.ctx.session.sessionId },
613
609
  );
614
610
  return { component: selector, focus: selector };
615
611
  });
616
612
  }
617
613
 
618
- async #applyModelAssignmentPreset(selection: Extract<ModelSelectorSelection, { kind: "preset" }>): Promise<void> {
619
- const { assignments, model, preset, selector } = selection;
620
- const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
621
- if (!apiKey) {
622
- throw new Error(`No API key for ${model.provider}/${model.id}`);
623
- }
624
-
625
- const defaultThinkingLevel = assignments.default;
626
- await this.ctx.session.setModel(model, "default", {
627
- selector,
628
- thinkingLevel: defaultThinkingLevel,
629
- });
630
- if (defaultThinkingLevel && defaultThinkingLevel !== ThinkingLevel.Inherit) {
631
- this.ctx.session.setThinkingLevel(defaultThinkingLevel);
632
- }
633
-
634
- const overrides = this.ctx.settings.get("task.agentModelOverrides");
635
- const nextOverrides = { ...overrides };
636
- for (const [targetRole, presetThinkingLevel] of Object.entries(assignments) as [
637
- keyof Extract<ModelSelectorSelection, { kind: "preset" }>["assignments"],
638
- ThinkingLevel,
639
- ][]) {
640
- if (!targetRole || targetRole === "default") continue;
641
- nextOverrides[targetRole] =
642
- presetThinkingLevel === ThinkingLevel.Inherit ? selector : `${selector}:${presetThinkingLevel}`;
643
- }
644
- this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
645
- this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
646
- this.ctx.statusLine.invalidate();
647
- this.ctx.updateEditorBorderColor();
648
- this.ctx.showStatus(`${preset.label}: ${selector}`);
649
- }
650
-
651
614
  async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
652
615
  const mgr = new MarketplaceManager({
653
616
  marketplacesRegistryPath: getMarketplacesRegistryPath(),
@@ -1034,6 +997,34 @@ export class SelectorController {
1034
997
  await this.showSessionSelector();
1035
998
  }
1036
999
 
1000
+ async #handlePostLoginModelProfileRecommendation(providerId: string): Promise<void> {
1001
+ const recommendedProfile = recommendModelProfileForProvider(
1002
+ providerId,
1003
+ this.ctx.session.modelRegistry.getModelProfiles(),
1004
+ );
1005
+ if (!recommendedProfile) {
1006
+ return;
1007
+ }
1008
+
1009
+ const activeProfile = this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default");
1010
+ if (activeProfile) {
1011
+ this.ctx.showStatus(`Preset ${recommendedProfile.name} is available in /model.`);
1012
+ return;
1013
+ }
1014
+
1015
+ const confirmed = await this.ctx.showHookConfirm(`Apply ${recommendedProfile.name} now?`, "");
1016
+ if (!confirmed) {
1017
+ return;
1018
+ }
1019
+
1020
+ await activateModelProfile({
1021
+ session: this.ctx.session,
1022
+ modelRegistry: this.ctx.session.modelRegistry,
1023
+ settings: this.ctx.settings,
1024
+ profileName: recommendedProfile.name,
1025
+ });
1026
+ }
1027
+
1037
1028
  async #handleOAuthLogin(providerId: string): Promise<void> {
1038
1029
  this.ctx.showStatus(`Logging in to ${providerId}…`);
1039
1030
  const manualInput = this.ctx.oauthManualInput;
@@ -1090,6 +1081,7 @@ export class SelectorController {
1090
1081
  new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
1091
1082
  );
1092
1083
  this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
1084
+ await this.#handlePostLoginModelProfileRecommendation(providerId);
1093
1085
  this.ctx.ui.requestRender();
1094
1086
  } catch (error: unknown) {
1095
1087
  this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1120,7 +1112,7 @@ export class SelectorController {
1120
1112
  async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
1121
1113
  if (providerId) {
1122
1114
  const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
1123
- if (!oauthProvider) {
1115
+ if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
1124
1116
  this.ctx.showError(`Unknown OAuth provider: ${providerId}`);
1125
1117
  return;
1126
1118
  }
@@ -12,6 +12,7 @@ import type { SessionStats } from "../../session/agent-session";
12
12
  import type {
13
13
  RpcCommand,
14
14
  RpcExtensionUIRequest,
15
+ RpcGetStateInclude,
15
16
  RpcHandoffResult,
16
17
  RpcHostToolCallRequest,
17
18
  RpcHostToolCancelRequest,
@@ -442,8 +443,8 @@ export class RpcClient {
442
443
  /**
443
444
  * Get current session state.
444
445
  */
445
- async getState(): Promise<RpcSessionState> {
446
- const response = await this.#send({ type: "get_state" });
446
+ async getState(include?: RpcGetStateInclude[]): Promise<RpcSessionState> {
447
+ const response = await this.#send(include ? { type: "get_state", include } : { type: "get_state" });
447
448
  return this.#getData(response);
448
449
  }
449
450
 
@@ -10,8 +10,10 @@
10
10
  * - Events: AgentSessionEvent objects streamed as they occur
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
+
14
+ import * as fs from "node:fs/promises";
13
15
  import * as path from "node:path";
14
- import { $env, readJsonl, Snowflake } from "@gajae-code/utils";
16
+ import { $pickenv, readLines, Snowflake } from "@gajae-code/utils";
15
17
  import type {
16
18
  ExtensionUIContext,
17
19
  ExtensionUIDialogOptions,
@@ -23,6 +25,7 @@ import { initializeExtensions } from "../runtime-init";
23
25
  import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
24
26
  import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
25
27
  import { rpcError as error } from "../shared/agent-wire/responses";
28
+ import { registerRpcSession, unregisterRpcSession } from "../shared/agent-wire/session-registry";
26
29
  import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
27
30
  import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
28
31
  import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
@@ -70,13 +73,15 @@ function parseValueDialogResponse(
70
73
  return undefined;
71
74
  }
72
75
 
73
- function shouldEmitRpcTitles(): boolean {
74
- const raw = $env.PI_RPC_EMIT_TITLE;
76
+ export function shouldEmitRpcTitlesForTest(): boolean {
77
+ const raw = $pickenv("GJC_RPC_EMIT_TITLE", "PI_RPC_EMIT_TITLE");
75
78
  if (!raw) return false;
76
79
  const normalized = raw.trim().toLowerCase();
77
80
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
78
81
  }
79
82
 
83
+ const shouldEmitRpcTitles = shouldEmitRpcTitlesForTest;
84
+
80
85
  function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
81
86
  if (event.includes("denied")) return "denied";
82
87
  if (event.includes("exceeded")) return "exceeded";
@@ -156,6 +161,7 @@ export function requestRpcEditor(
156
161
  export async function runRpcMode(
157
162
  session: AgentSession,
158
163
  setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
164
+ options?: { listen?: string },
159
165
  ): Promise<never> {
160
166
  // Signal to RPC clients that the server is ready to accept commands
161
167
  // Suppress terminal notifications: they write \x07 (BEL) or OSC sequences directly to
@@ -164,11 +170,20 @@ export async function runRpcMode(
164
170
  // may write there.
165
171
  process.env.PI_NOTIFICATIONS = "off";
166
172
 
167
- process.stdout.write(`${JSON.stringify({ type: "ready" })}\n`);
173
+ // Frames go to a swappable sink: stdout for stdio, the active client socket for a
174
+ // persistent --listen (UDS) server. Defaults to stdout, so the stdio path is unchanged.
175
+ let frameSink = (line: string): void => {
176
+ process.stdout.write(line);
177
+ };
168
178
  const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
169
- process.stdout.write(`${JSON.stringify(obj)}\n`);
179
+ frameSink(`${JSON.stringify(obj)}\n`);
170
180
  };
181
+ // stdio announces readiness immediately; the UDS server announces it per client connection.
182
+ if (!options?.listen) {
183
+ output({ type: "ready" });
184
+ }
171
185
  const emitRpcTitles = shouldEmitRpcTitles();
186
+ const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
172
187
 
173
188
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
174
189
  const hostToolBridge = new RpcHostToolBridge(output);
@@ -229,6 +244,37 @@ export async function runRpcMode(
229
244
 
230
245
  // Shutdown request flag (wrapped in object to allow mutation with const)
231
246
  const shutdownState = { requested: false };
247
+ let shutdownStarted = false;
248
+ // Tracks in-flight non-blocking command handlers so shutdown can drain them.
249
+ const inFlightCommands = new Set<Promise<void>>();
250
+ async function shutdown(exitCode: number, reason: string): Promise<never> {
251
+ if (shutdownStarted) {
252
+ process.exit(exitCode);
253
+ }
254
+ shutdownStarted = true;
255
+ // Let in-flight non-blocking commands (bash/compact/handoff) finish and emit
256
+ // their responses before teardown, bounded so a never-resolving login cannot
257
+ // wedge shutdown (issue 13).
258
+ if (inFlightCommands.size > 0) {
259
+ await Promise.race([Promise.allSettled([...inFlightCommands]), Bun.sleep(5000)]);
260
+ }
261
+ await unregisterRpcSession(session.sessionId).catch(() => {});
262
+ hostToolBridge.rejectAllPending(`${reason} before host tool execution completed`);
263
+ hostUriBridge.clear(`${reason} before host URI request completed`);
264
+ try {
265
+ await session.sessionManager.ensureOnDisk();
266
+ } catch (err) {
267
+ output(error(undefined, "shutdown", decodeError(err)));
268
+ process.exit(1);
269
+ }
270
+ try {
271
+ await session.dispose();
272
+ } catch (err) {
273
+ output(error(undefined, "shutdown", decodeError(err)));
274
+ process.exit(1);
275
+ }
276
+ process.exit(exitCode);
277
+ }
232
278
 
233
279
  /**
234
280
  * Extension UI context that uses the RPC protocol.
@@ -376,7 +422,7 @@ export async function runRpcMode(
376
422
  }
377
423
 
378
424
  setTitle(title: string): void {
379
- // Title updates are low-value noise for most RPC hosts; opt in via PI_RPC_EMIT_TITLE=1.
425
+ // Title updates are low-value noise for most RPC hosts; opt in via GJC_RPC_EMIT_TITLE=1.
380
426
  if (!emitRpcTitles) return;
381
427
  this.output({
382
428
  type: "extension_ui_request",
@@ -491,62 +537,164 @@ export async function runRpcMode(
491
537
  unattendedControlPlane,
492
538
  });
493
539
 
540
+ // Cancellation commands must interrupt in-flight work, so they bypass the ordered
541
+ // queue and run immediately. Everything else runs through a serial chain so causal
542
+ // order is preserved (e.g. `get_state` after `bash` still observes the bash result)
543
+ // while the read loop itself never blocks — that is what lets a cancellation command
544
+ // reach a long-running `bash`/`compact`/`handoff`/`login` instead of being
545
+ // head-of-line-blocked behind it (issue 13).
546
+ const CANCELLATION_COMMANDS = new Set<RpcCommand["type"]>(["abort", "abort_bash", "abort_retry"]);
547
+ let orderedChain: Promise<void> = Promise.resolve();
548
+ const runCommand = async (command: RpcCommand): Promise<void> => {
549
+ try {
550
+ output(await handleCommand(command));
551
+ } catch (err) {
552
+ output(error(command.id, command.type, decodeError(err)));
553
+ }
554
+ };
555
+ const trackCommand = (task: Promise<void>): void => {
556
+ inFlightCommands.add(task);
557
+ void task.finally(() => inFlightCommands.delete(task));
558
+ };
559
+ const dispatchCommand = (command: RpcCommand): void => {
560
+ if (CANCELLATION_COMMANDS.has(command.type)) {
561
+ trackCommand(runCommand(command));
562
+ return;
563
+ }
564
+ orderedChain = orderedChain.then(() => runCommand(command));
565
+ trackCommand(orderedChain);
566
+ };
567
+
494
568
  /**
495
569
  * Check if shutdown was requested and perform shutdown if so.
496
570
  * Called after handling each command when waiting for the next command.
497
571
  */
498
572
  async function checkShutdownRequested(): Promise<void> {
499
573
  if (!shutdownState.requested) return;
500
-
501
- if (session.extensionRunner?.hasHandlers("session_shutdown")) {
502
- await session.extensionRunner.emit({ type: "session_shutdown" });
503
- }
504
-
505
- process.exit(0);
574
+ await shutdown(0, "RPC shutdown requested");
506
575
  }
507
576
 
508
- // Listen for JSON input using Bun's stdin
509
- for await (const parsed of readJsonl(Bun.stdin.stream())) {
577
+ // Parse + route a single inbound JSONL frame. Shared by the stdio reader and the
578
+ // persistent UDS server so both transports use the same command surface.
579
+ const inputDecoder = new TextDecoder("utf-8", { fatal: false });
580
+ async function handleInboundLine(text: string): Promise<void> {
581
+ let parsed: unknown;
582
+ try {
583
+ parsed = JSON.parse(text);
584
+ } catch (err) {
585
+ output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
586
+ return;
587
+ }
510
588
  try {
511
- // Handle extension UI responses
512
589
  if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
513
590
  const response = parsed as RpcExtensionUIResponse;
514
- const pending = pendingExtensionRequests.get(response.id);
515
- if (pending) {
516
- pending.resolve(response);
517
- }
518
- continue;
591
+ pendingExtensionRequests.get(response.id)?.resolve(response);
592
+ return;
519
593
  }
520
-
521
594
  if (isRpcHostToolResult(parsed)) {
522
595
  hostToolBridge.handleResult(parsed);
523
- continue;
596
+ return;
524
597
  }
525
-
526
598
  if (isRpcHostToolUpdate(parsed)) {
527
599
  hostToolBridge.handleUpdate(parsed);
528
- continue;
600
+ return;
529
601
  }
530
-
531
602
  if (isRpcHostUriResult(parsed)) {
532
603
  hostUriBridge.handleResult(parsed);
533
- continue;
604
+ return;
534
605
  }
535
-
536
- // Handle regular commands
537
- const command = parsed as RpcCommand;
538
- const response = await handleCommand(command);
539
- output(response);
540
-
541
- // Check for deferred shutdown request (idle between commands)
606
+ // Ordered commands run through a serial chain to preserve causal order; the
607
+ // reader never blocks, so cancellation commands stay responsive even while a
608
+ // long command is in flight (issue 13).
609
+ dispatchCommand(parsed as RpcCommand);
542
610
  await checkShutdownRequested();
543
- } catch (e: any) {
544
- output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
611
+ } catch (err) {
612
+ output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
545
613
  }
546
614
  }
547
615
 
548
- // stdin closed RPC client is gone, exit cleanly
549
- hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
550
- hostUriBridge.clear("RPC client disconnected before host URI request completed");
551
- process.exit(0);
616
+ // Persistent UDS server (issue 09): keep the AgentSession alive across client
617
+ // reconnects instead of exiting on stdin EOF. Frames route to the active client
618
+ // socket; while no client is connected they are dropped (clients resync via
619
+ // get_state/get_messages on reconnect).
620
+ if (options?.listen) {
621
+ const socketPath = options.listen;
622
+ await fs.mkdir(path.dirname(socketPath), { recursive: true }).catch(() => {});
623
+ await fs.rm(socketPath, { force: true }).catch(() => {});
624
+ await registerRpcSession({
625
+ sessionId: session.sessionId,
626
+ pid: process.pid,
627
+ transport: "socket",
628
+ cwd: session.sessionManager.getCwd(),
629
+ model: session.model?.id,
630
+ startedAt: new Date().toISOString(),
631
+ endpoint: socketPath,
632
+ }).catch(() => {});
633
+
634
+ const noopSink = (_line: string): void => {};
635
+ let currentSocket: object | undefined;
636
+ let buf = "";
637
+ Bun.listen({
638
+ unix: socketPath,
639
+ socket: {
640
+ open(socket) {
641
+ currentSocket = socket;
642
+ buf = "";
643
+ frameSink = (line: string) => {
644
+ socket.write(line);
645
+ };
646
+ output({ type: "ready" });
647
+ },
648
+ data(socket, data) {
649
+ if (socket !== currentSocket) return;
650
+ buf += inputDecoder.decode(data);
651
+ while (true) {
652
+ const nl = buf.indexOf("\n");
653
+ if (nl < 0) break;
654
+ const text = buf.slice(0, nl).trim();
655
+ buf = buf.slice(nl + 1);
656
+ if (text) void handleInboundLine(text);
657
+ }
658
+ },
659
+ close(socket) {
660
+ if (socket === currentSocket) {
661
+ currentSocket = undefined;
662
+ frameSink = noopSink;
663
+ }
664
+ },
665
+ error() {},
666
+ },
667
+ });
668
+
669
+ const onSignal = (): void => {
670
+ void shutdown(0, "RPC socket server signal");
671
+ };
672
+ process.on("SIGINT", onSignal);
673
+ process.on("SIGTERM", onSignal);
674
+ // Block until an explicit shutdown (signal/extension) calls process.exit.
675
+ await new Promise<never>(() => {});
676
+ throw new Error("RPC socket server returned unexpectedly");
677
+ }
678
+
679
+ // Register this stdio RPC session so other processes can discover it (issue 10).
680
+ await registerRpcSession({
681
+ sessionId: session.sessionId,
682
+ pid: process.pid,
683
+ transport: "stdio",
684
+ cwd: session.sessionManager.getCwd(),
685
+ model: session.model?.id,
686
+ startedAt: new Date().toISOString(),
687
+ }).catch(() => {});
688
+
689
+ // Listen for JSONL input using Bun's stdin. Parse frame-by-frame so a malformed
690
+ // command reports a parse error without poisoning the whole long-lived RPC session.
691
+ for await (const line of readLines(Bun.stdin.stream())) {
692
+ const text = inputDecoder.decode(line).trim();
693
+ if (!text) continue;
694
+ await handleInboundLine(text);
695
+ }
696
+
697
+ // stdin closed — RPC client is gone, flush durable state and exit cleanly
698
+ await shutdown(0, "RPC client disconnected");
699
+ throw new Error("RPC shutdown returned unexpectedly");
552
700
  }