@gajae-code/coding-agent 0.18.11 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -27,7 +27,12 @@ export interface BuildReportInput {
27
27
 
28
28
  export function buildReport(input: BuildReportInput): MigrateReport {
29
29
  const total = emptyStatusCounts();
30
- const byType = { mcp: emptyStatusCounts(), skill: emptyStatusCounts(), source: emptyStatusCounts() };
30
+ const byType = {
31
+ mcp: emptyStatusCounts(),
32
+ skill: emptyStatusCounts(),
33
+ instruction: emptyStatusCounts(),
34
+ source: emptyStatusCounts(),
35
+ };
31
36
  const bySource = {} as Record<MigrateSource, StatusCounts>;
32
37
  for (const source of input.sources) bySource[source] = emptyStatusCounts();
33
38
 
@@ -75,10 +80,22 @@ export function renderHuman(report: MigrateReport): string {
75
80
  const mode = report.dryRun ? " (dry-run)" : "";
76
81
  lines.push(`gjc migrate${mode}: ${report.ok ? "ok" : "completed with failures"}`);
77
82
  lines.push(`Sources: ${report.sources.join(", ") || "none"}`);
78
- lines.push(`Destination: mcp=${report.destinations.mcpConfigPath} skills=${report.destinations.skillsDir}`);
83
+ lines.push(
84
+ `Destination: mcp=${report.destinations.mcpConfigPath} skills=${report.destinations.skillsDir} instructions=${report.destinations.instructionsPath}`,
85
+ );
79
86
  lines.push("");
80
87
  lines.push(`MCP: ${summarizeCounts(report.summary.byType.mcp)}`);
81
88
  lines.push(`Skills: ${summarizeCounts(report.summary.byType.skill)}`);
89
+ lines.push(`Instructions: ${summarizeCounts(report.summary.byType.instruction)}`);
90
+ const instructionActions = report.actions.filter(action => action.type === "instruction");
91
+ if (instructionActions.length > 0) {
92
+ lines.push("Instruction files:");
93
+ for (const action of instructionActions) {
94
+ lines.push(
95
+ ` - [${action.source}] ${action.name ?? "AGENTS.md"}: ${action.status}${action.reason ? ` (${action.reason})` : ""}`,
96
+ );
97
+ }
98
+ }
82
99
  if (Object.values(report.summary.byType.source).some(n => n > 0)) {
83
100
  lines.push(`Source: ${summarizeCounts(report.summary.byType.source)}`);
84
101
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Shared types for `gjc migrate`.
3
3
  *
4
- * Imports MCP servers and skills from other coding agents (Claude Code, Codex,
5
- * OpenCode) into native GJC config. See the consensus plan under
4
+ * Imports MCP servers, skills, and instruction files from other coding agents
5
+ * (Claude Code, Codex, OpenCode) into native GJC config. See the consensus plan under
6
6
  * `.gjc/plans/ralplan/` for the full taxonomy and force/collision semantics.
7
7
  */
8
8
  import type { MCPServerConfig } from "../runtime-mcp/types";
@@ -16,7 +16,7 @@ export const MIGRATE_SOURCES: readonly MigrateSource[] = ["claude-code", "codex"
16
16
  export const CANONICAL_SOURCE_ORDER: readonly MigrateSource[] = MIGRATE_SOURCES;
17
17
 
18
18
  /** What kind of thing an action/coverage row is about. */
19
- export type MigrateItemType = "mcp" | "skill" | "source";
19
+ export type MigrateItemType = "mcp" | "skill" | "instruction" | "source";
20
20
 
21
21
  /**
22
22
  * Per-item outcome taxonomy.
@@ -77,6 +77,13 @@ export interface SkillCandidate {
77
77
  warnings: string[];
78
78
  }
79
79
 
80
+ /** An instructions file imported as the active GJC AGENTS.md for its scope. */
81
+ export interface InstructionCandidate {
82
+ source: MigrateSource;
83
+ name: string;
84
+ content: string;
85
+ }
86
+
80
87
  /**
81
88
  * Source-level diagnostic for a single source/type pair (e.g. "codex mcp config
82
89
  * was malformed"). Distinct from per-item actions so absent/unreadable sources
@@ -93,6 +100,7 @@ export interface SourceDiagnostic {
93
100
  export interface AdapterResult {
94
101
  mcpCandidates: McpCandidate[];
95
102
  skillCandidates: SkillCandidate[];
103
+ instructionCandidates: InstructionCandidate[];
96
104
  diagnostics: SourceDiagnostic[];
97
105
  }
98
106
 
@@ -112,6 +120,7 @@ export interface MigrateAction {
112
120
  /** Resolved payload the executor needs; never serialized to the report. */
113
121
  mcp?: { config: MCPServerConfig; force: boolean };
114
122
  skill?: { content: string };
123
+ instruction?: { content: string };
115
124
  }
116
125
 
117
126
  export interface MigrateWarning {
@@ -126,6 +135,7 @@ export type StatusCounts = Record<MigrationStatus, number>;
126
135
  export interface MigrateDestinations {
127
136
  mcpConfigPath: string;
128
137
  skillsDir: string;
138
+ instructionsPath: string;
129
139
  }
130
140
 
131
141
  /** The full machine-readable report emitted with `--json`. */
@@ -138,7 +148,7 @@ export interface MigrateReport {
138
148
  destinations: MigrateDestinations;
139
149
  summary: {
140
150
  total: StatusCounts;
141
- byType: { mcp: StatusCounts; skill: StatusCounts; source: StatusCounts };
151
+ byType: { mcp: StatusCounts; skill: StatusCounts; instruction: StatusCounts; source: StatusCounts };
142
152
  bySource: Record<MigrateSource, StatusCounts>;
143
153
  };
144
154
  actions: Array<{
@@ -112,6 +112,11 @@ condition in prose. Error guidance states the safe recovery action without
112
112
  showing credentials. Confirmations are explicit focused choices; destructive
113
113
  remove/disable actions are never the default side effect of navigation.
114
114
 
115
+ Guidance the user must act on (for example the subagent await-timeout
116
+ "inspect before cancelling" instruction) wraps to the terminal width instead of
117
+ being clipped to one preview line, so the action survives narrow terminals. It
118
+ stays bounded: at most 12 wrapped rows, the last ending in `…` if cut.
119
+
115
120
  A non-cancellable action visibly locks navigation and names the reason. A
116
121
  cancellable action names cancellation while it is pending, aborts on exit, and
117
122
  must not render a late completion after disposal. This follows the selector's
@@ -469,3 +474,49 @@ differently named bundles, and a stored `uri#ref` differs from a bare `uri`, so
469
474
  locator-based refusal would refuse installs that should proceed. Refusing on a
470
475
  guess is worse than reporting the real failure, so identity must be readable for
471
476
  refusal to apply.
477
+
478
+ ## Subagent compact await panel
479
+
480
+ Source: `tools/subagent-render.ts` (the `subagent` await panel), behind the
481
+ `task.compactSubagentPanel` setting (default off). This section extends the
482
+ existing tool-panel grammar; it adds no tokens.
483
+
484
+ - **Scope:** live await progress only. A final result (timeout guidance,
485
+ failure and completion receipts, status/steer/resume replies) always keeps the
486
+ full per-subagent rendering, so nothing the user must act on is summarized.
487
+ - **Anatomy (collapsed, setting on):** a height independent of the number of
488
+ subagents — the existing status header, one `dim` summary row of group counts
489
+ joined by ` · `, at most one `warning` row naming the most-degraded provider and
490
+ how many more providers degrade (the expanded view keeps one row per provider), and one `dim` hint row in parentheses
491
+ naming the effective `app.tools.expand` key and, while anything runs, the
492
+ existing `ctrl+s to observe sessions` hint.
493
+ - **Count order:** `N subagents running`, then `failed`, `retrying`,
494
+ `idle >30s`, `queued`, `paused`, `done`, `cancelled`. Zero counts are omitted.
495
+ Attention states come first because a narrow terminal truncates the row end;
496
+ truncation shows `…` so a dropped count is visible.
497
+ - **States:** collapsed → summary; expanded (the existing tool-output toggle)
498
+ → the unchanged per-subagent list. With the setting off the panel is the
499
+ unchanged per-subagent list in both states. Status is carried by words and
500
+ counts, never by color or the spinner alone; ASCII/no-color keeps the same
501
+ text.
502
+ - **Motion:** no new animation. With the setting on, await progress re-emits
503
+ only when the summary counts or per-provider retry counts change (plus the
504
+ existing liveness interval).
505
+ - **International text:** the summary and hint are fixed English labels and
506
+ numbers; subagent descriptions (which may be CJK) appear only in the expanded
507
+ list, whose layout is unchanged.
508
+
509
+ Capture command (fixture-backed, real renderer, fixed clock):
510
+
511
+ ```sh
512
+ bun packages/coding-agent/scripts/capture-subagent-compact-panel-showcase.ts --out .gjc/qa/subagent-compact-panel-<run>
513
+ ```
514
+
515
+ The matrix is every state at `80x24`, `120x36`, and `160x48` in
516
+ `unicode-color` (including `compact-final-timeout`, a final timeout receipt
517
+ with the setting on), plus `compact-collapsed-15-live` and `compact-collapsed-mixed`
518
+ at `80x24/ascii-no-color`, `compact-collapsed-mixed/48x36/unicode-color`, and
519
+ `narrow-cjk/48x36` in both render modes: 26 entries. An entry whose panel fits
520
+ the viewport is captured once (`top`); an overflowing entry is captured at
521
+ `top`, `middle`, and `bottom` so no tail is hidden. Every capture has
522
+ `terminal.txt`, `terminal-ansi.txt`, `terminal.html`, and `metadata.json`.
@@ -1,4 +1,6 @@
1
1
  import type { ClientCapabilities } from "@agentclientprotocol/sdk";
2
+ import { startupProjectEnvSnapshot } from "@gajae-code/utils/env";
3
+ import { canonicalEnvKey } from "@gajae-code/utils/env-file";
2
4
 
3
5
  export type AcpPermissionMode = "auto" | "prompt" | "always-allow";
4
6
 
@@ -9,6 +11,21 @@ function parseAcpPermissionMode(value: unknown): AcpPermissionMode {
9
11
  return "prompt";
10
12
  }
11
13
 
14
+ /**
15
+ * The default process environment includes the cwd `.env` merge. A live value
16
+ * equal to that startup declaration is the overlay and fails safe. A different
17
+ * live value is the launching environment, including when the declaration is
18
+ * dynamic (`$` or backticks) and is not that value. An explicit env map is
19
+ * the caller's value. The raw string is parsed so padding still fails safe.
20
+ */
21
+ function permissionModeFromEnv(env: NodeJS.ProcessEnv): unknown {
22
+ const raw = env[ACP_PERMISSION_MODE_ENV];
23
+ if (env !== process.env || typeof raw !== "string") return raw;
24
+ const declared = startupProjectEnvSnapshot().values[canonicalEnvKey(ACP_PERMISSION_MODE_ENV)];
25
+ if (declared !== undefined && raw.trim() === declared.trim()) return undefined;
26
+ return raw;
27
+ }
28
+
12
29
  /** Client metadata is authoritative; the process environment is only a fallback when that field is absent. */
13
30
  export function resolveAcpPermissionMode(
14
31
  clientCapabilities: ClientCapabilities | undefined,
@@ -21,5 +38,5 @@ export function resolveAcpPermissionMode(
21
38
  return parseAcpPermissionMode((gjc as { permissionHandling?: unknown }).permissionHandling);
22
39
  }
23
40
  }
24
- return parseAcpPermissionMode(env[ACP_PERMISSION_MODE_ENV]);
41
+ return parseAcpPermissionMode(permissionModeFromEnv(env));
25
42
  }
@@ -4,13 +4,17 @@ import {
4
4
  Container,
5
5
  Image,
6
6
  ImageProtocol,
7
+ isCursorNeutralImagePermittedInFallback,
8
+ isTerminalGraphicsFallbackActive,
7
9
  isViewportAnchorSourceRenderer,
8
10
  Markdown,
11
+ type MarkdownTheme,
9
12
  Spacer,
10
13
  TERMINAL,
11
14
  Text,
12
15
  type ViewportAnchorSource,
13
16
  } from "@gajae-code/tui";
17
+ import { SvgFigure } from "@gajae-code/tui/chat/svg-figure";
14
18
  import { formatNumber } from "@gajae-code/utils";
15
19
  import { settings } from "../../config/settings";
16
20
  import { renderDeepInterviewAssistantText } from "../../deep-interview/render-middleware";
@@ -27,6 +31,44 @@ const THINKING_REPETITION_ELIDE_MIN_RUN = 24;
27
31
  const THINKING_REPETITION_VISIBLE_TOKENS = 3;
28
32
  const THINKING_REPETITION_TOKEN_PATTERN = /[\p{L}\p{N}_'-]{1,32}/gu;
29
33
 
34
+ let assistantMarkdownThemeBase: MarkdownTheme | undefined;
35
+ let cachedAssistantMarkdownTheme: MarkdownTheme | undefined;
36
+
37
+ function getAssistantMarkdownTheme(): MarkdownTheme {
38
+ const base = getMarkdownTheme();
39
+ if (base === assistantMarkdownThemeBase && cachedAssistantMarkdownTheme) return cachedAssistantMarkdownTheme;
40
+ assistantMarkdownThemeBase = base;
41
+ cachedAssistantMarkdownTheme = {
42
+ ...base,
43
+ resolveSvgFigure: (source, context) => {
44
+ if (!settings.get("terminal.showImages")) return null;
45
+ const graphicsSuppressed =
46
+ isTerminalGraphicsFallbackActive() &&
47
+ !(TERMINAL.imageProtocol === ImageProtocol.Kitty && isCursorNeutralImagePermittedInFallback());
48
+ if (!TERMINAL.imageProtocol) return null; // Stable environment: cache the code fallback.
49
+ if (graphicsSuppressed) return undefined; // The suppression scope can end; retry on repaint.
50
+ const palette = theme.getSvgPalette();
51
+ const previous = context.previous instanceof SvgFigure ? context.previous : undefined;
52
+ const figure =
53
+ previous ??
54
+ new SvgFigure({
55
+ theme: { fallbackColor: (text: string) => theme.fg("toolOutput", text) },
56
+ palette,
57
+ onChange: context.onChange,
58
+ });
59
+ figure.update(source, context.closed || !context.streaming, context.closed, palette);
60
+ if (figure.retryable) {
61
+ // A failed preview remains a code block, but must observe layout changes
62
+ // so a smaller terminal canvas can retry the same source.
63
+ figure.render(context.width);
64
+ return undefined;
65
+ }
66
+ return figure.failed ? null : figure;
67
+ },
68
+ };
69
+ return cachedAssistantMarkdownTheme;
70
+ }
71
+
30
72
  interface ThinkingRepetitionToken {
31
73
  text: string;
32
74
  normalized: string;
@@ -92,6 +134,7 @@ export class AssistantMessageComponent extends Container {
92
134
  #convertedKittyImages = new Map<string, ImageContent & { source: string }>();
93
135
  #kittyConversionsInFlight = new Map<string, string>();
94
136
  #toolImageGenerations = new Map<string, number>();
137
+ #unsubscribeSettings?: () => void;
95
138
 
96
139
  #responseHeader = new Text(theme.bold(theme.fg("statusLineModel", "gajae")), 1, 0);
97
140
  #contentBlocksCache = new WeakMap<object, { source: string; component: Component }>();
@@ -133,6 +176,15 @@ export class AssistantMessageComponent extends Container {
133
176
  }
134
177
  this.#visibleSignature = this.#visibleContentSignature();
135
178
  this.#visibleSignatureInitialized = true;
179
+ this.#unsubscribeSettings = settings.onChanged(path => {
180
+ if (path !== "terminal.showImages") return;
181
+ for (const block of this.#lastMessage?.content ?? []) {
182
+ if (block.type !== "text") continue;
183
+ const cached = this.#contentBlocksCache.get(block);
184
+ if (cached?.component instanceof Markdown) cached.component.invalidate();
185
+ }
186
+ this.#requestRepaint?.();
187
+ });
136
188
  }
137
189
 
138
190
  #visibleContentSignature(): string {
@@ -339,7 +391,7 @@ export class AssistantMessageComponent extends Container {
339
391
  cached.source = content.text;
340
392
  return cached.component;
341
393
  }
342
- const component = deepInterview ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
394
+ const component = deepInterview ?? new Markdown(trimmed, 1, 0, getAssistantMarkdownTheme());
343
395
  if (component instanceof Markdown) {
344
396
  component.setOnStaleThrottle(this.#requestRepaint);
345
397
  component.setStreaming(streaming);
@@ -626,6 +678,8 @@ export class AssistantMessageComponent extends Container {
626
678
  override dispose(): void {
627
679
  if (this.#disposed) return;
628
680
  this.#disposed = true;
681
+ this.#unsubscribeSettings?.();
682
+ this.#unsubscribeSettings = undefined;
629
683
  this.#toolImageGenerations.clear();
630
684
  this.#kittyConversionsInFlight.clear();
631
685
  this.#convertedKittyImages.clear();
@@ -108,9 +108,9 @@ class SelectSubmenu extends Container {
108
108
  description: string,
109
109
  options: ReadonlyArray<SelectItem>,
110
110
  currentValue: string,
111
- onSelect: (value: string) => void,
112
- onCancel: () => void,
113
- onSelectionChange?: (value: string) => void | Promise<void>,
111
+ private readonly onSelect: (value: string) => void,
112
+ private readonly onCancel: () => void,
113
+ private readonly onSelectionChange?: (value: string) => void | Promise<void>,
114
114
  private readonly getPreview?: () => string,
115
115
  ) {
116
116
  super();
@@ -137,46 +137,54 @@ class SelectSubmenu extends Container {
137
137
  // Spacer
138
138
  this.addChild(new Spacer(1));
139
139
 
140
- // Select list
141
- this.#selectList = new SelectList(options, Math.min(options.length, 10), () => getSelectListTheme());
142
-
143
- // Pre-select current value
144
- const currentIndex = options.findIndex(o => o.value === currentValue);
145
- if (currentIndex !== -1) {
146
- this.#selectList.setSelectedIndex(currentIndex);
147
- }
148
-
149
- this.#selectList.onSelect = item => {
150
- onSelect(item.value);
151
- };
140
+ this.#selectList = this.#createSelectList(options, currentValue);
141
+ this.addChild(this.#selectList);
152
142
 
153
- this.#selectList.onCancel = onCancel;
143
+ // Hint
144
+ this.addChild(new Spacer(1));
145
+ this.addChild(
146
+ new DynamicThemeText(() => theme.fg("dim", uiString(settings.get("ui.language"), "settings.selectHint"))),
147
+ );
148
+ }
154
149
 
155
- if (onSelectionChange) {
156
- this.#selectList.onSelectionChange = item => {
150
+ #createSelectList(options: ReadonlyArray<SelectItem>, selectedValue?: string): SelectList {
151
+ const selectList = new SelectList(options, Math.min(options.length, 10), () => getSelectListTheme());
152
+ const selectedIndex = options.findIndex(option => option.value === selectedValue);
153
+ if (selectedIndex !== -1) selectList.setSelectedIndex(selectedIndex);
154
+ selectList.onSelect = item => this.onSelect(item.value);
155
+ selectList.onCancel = this.onCancel;
156
+ if (this.onSelectionChange) {
157
+ selectList.onSelectionChange = item => {
157
158
  const requestId = ++this.#previewUpdateRequestId;
158
- const result = onSelectionChange(item.value);
159
+ const result = this.onSelectionChange?.(item.value);
159
160
  if (result && typeof (result as Promise<void>).then === "function") {
160
161
  void (result as Promise<void>).finally(() => {
161
- if (requestId === this.#previewUpdateRequestId) {
162
- this.#updatePreview();
163
- }
162
+ if (requestId === this.#previewUpdateRequestId) this.#updatePreview();
164
163
  });
165
164
  return;
166
165
  }
167
- if (requestId === this.#previewUpdateRequestId) {
168
- this.#updatePreview();
169
- }
166
+ if (requestId === this.#previewUpdateRequestId) this.#updatePreview();
170
167
  };
171
168
  }
169
+ return selectList;
170
+ }
172
171
 
173
- this.addChild(this.#selectList);
172
+ refreshOptions(options: ReadonlyArray<SelectItem>, retainMissingSelection = false): void {
173
+ const previous = this.#selectList;
174
+ const selectedItem = previous.getSelectedItem();
175
+ if (retainMissingSelection && selectedItem && !options.some(option => option.value === selectedItem.value)) {
176
+ const label = selectedItem.label.endsWith(" (unavailable)")
177
+ ? selectedItem.label
178
+ : `${selectedItem.label} (unavailable)`;
179
+ options = [...options, { ...selectedItem, label }];
180
+ }
181
+ const selectedValue = selectedItem?.value;
182
+ this.#selectList = this.#createSelectList(options, selectedValue);
183
+ this.replaceChildren(this.children.map(child => (child === previous ? this.#selectList : child)));
184
+ }
174
185
 
175
- // Hint
176
- this.addChild(new Spacer(1));
177
- this.addChild(
178
- new DynamicThemeText(() => theme.fg("dim", uiString(settings.get("ui.language"), "settings.selectHint"))),
179
- );
186
+ refreshPreview(): void {
187
+ this.#updatePreview();
180
188
  }
181
189
 
182
190
  #updatePreview(): void {
@@ -1528,6 +1536,10 @@ export interface SettingsCallbacks {
1528
1536
  onPetCommit?: (mode: string) => boolean;
1529
1537
  /** Called for status line preview while configuring */
1530
1538
  onStatusLinePreview?: (settings: StatusLinePreviewSettings) => void;
1539
+ /** Called when a status-line submenu begins an uncommitted preview. */
1540
+ onStatusLinePreviewStart?: () => void;
1541
+ /** Called when a status-line preview is committed, cancelled, or disposed. */
1542
+ onStatusLinePreviewEnd?: () => void;
1531
1543
  /** Get current rendered status line for inline preview */
1532
1544
  getStatusLinePreview?: (width?: number) => string;
1533
1545
  /** Render a status-line preview for supplied draft settings without mutating the live status line. */
@@ -1582,6 +1594,8 @@ export class SettingsSelectorComponent extends Container {
1582
1594
  #currentTabId: SettingTab | "plugins" | "gjc-bundles" = "appearance";
1583
1595
  #textInputActive = false;
1584
1596
  #activeProviderOrderEditor: Container | null = null;
1597
+ #activeSelectSubmenu: SelectSubmenu | null = null;
1598
+ #activeModelProfileSubmenu: SelectSubmenu | null = null;
1585
1599
 
1586
1600
  constructor(
1587
1601
  private readonly context: SettingsRuntimeContext,
@@ -1634,9 +1648,15 @@ export class SettingsSelectorComponent extends Container {
1634
1648
  }
1635
1649
 
1636
1650
  #switchToTab(tabId: SettingTab | "plugins" | "gjc-bundles"): void {
1651
+ if (this.#currentTabId === "appearance" && tabId !== "appearance") {
1652
+ this.callbacks.onStatusLinePreviewEnd?.();
1653
+ }
1637
1654
  if (this.#currentTabId === "notifications" && tabId !== "notifications" && !this.#disposeNotificationsEditor()) {
1638
1655
  return;
1639
1656
  }
1657
+ this.#activeSelectSubmenu?.dispose();
1658
+ this.#activeSelectSubmenu = null;
1659
+ this.#activeModelProfileSubmenu = null;
1640
1660
  // Release an open provider-order editor (and its context subscriptions)
1641
1661
  // before switching tabs; the submenu's done() never runs on tab change,
1642
1662
  // so without this the abandoned editor's listeners would survive.
@@ -1833,6 +1853,18 @@ export class SettingsSelectorComponent extends Container {
1833
1853
  // Preview handlers
1834
1854
  let onPreview: ((value: string) => void | Promise<void>) | undefined;
1835
1855
  let onPreviewCancel: (() => void) | undefined;
1856
+ let onPreviewEnd: (() => void) | undefined;
1857
+ let statusLinePreviewStarted = false;
1858
+ const startStatusLinePreview = (): void => {
1859
+ if (statusLinePreviewStarted) return;
1860
+ statusLinePreviewStarted = true;
1861
+ this.callbacks.onStatusLinePreviewStart?.();
1862
+ };
1863
+ const endStatusLinePreview = (): void => {
1864
+ if (!statusLinePreviewStarted) return;
1865
+ statusLinePreviewStarted = false;
1866
+ this.callbacks.onStatusLinePreviewEnd?.();
1867
+ };
1836
1868
 
1837
1869
  if (def.path === "theme.dark" || def.path === "theme.light") {
1838
1870
  const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
@@ -1844,6 +1876,7 @@ export class SettingsSelectorComponent extends Container {
1844
1876
  };
1845
1877
  } else if (def.path === "statusLine.preset") {
1846
1878
  onPreview = value => {
1879
+ startStatusLinePreview();
1847
1880
  const presetDef = getPreset(value as StatusLinePreset);
1848
1881
  this.callbacks.onStatusLinePreview?.({
1849
1882
  preset: value as StatusLinePreset,
@@ -1854,6 +1887,7 @@ export class SettingsSelectorComponent extends Container {
1854
1887
  });
1855
1888
  this.#updateStatusPreview();
1856
1889
  };
1890
+ onPreviewEnd = endStatusLinePreview;
1857
1891
  onPreviewCancel = () => {
1858
1892
  const currentPreset = settings.get("statusLine.preset");
1859
1893
  const presetDef = getPreset(currentPreset);
@@ -1880,9 +1914,11 @@ export class SettingsSelectorComponent extends Container {
1880
1914
  };
1881
1915
  } else if (def.path === "statusLine.separator") {
1882
1916
  onPreview = value => {
1917
+ startStatusLinePreview();
1883
1918
  this.callbacks.onStatusLinePreview?.({ separator: value as StatusLineSeparatorStyle });
1884
1919
  this.#updateStatusPreview();
1885
1920
  };
1921
+ onPreviewEnd = endStatusLinePreview;
1886
1922
  onPreviewCancel = () => {
1887
1923
  const separator = settings.get("statusLine.separator");
1888
1924
  this.callbacks.onStatusLinePreview?.({ separator, previewHighlightSegment: undefined });
@@ -1890,9 +1926,11 @@ export class SettingsSelectorComponent extends Container {
1890
1926
  };
1891
1927
  } else if (def.path === "statusLine.maxRows") {
1892
1928
  onPreview = value => {
1929
+ startStatusLinePreview();
1893
1930
  this.callbacks.onStatusLinePreview?.({ maxRows: Number(value) });
1894
1931
  this.#updateStatusPreview();
1895
1932
  };
1933
+ onPreviewEnd = endStatusLinePreview;
1896
1934
  onPreviewCancel = () => {
1897
1935
  this.callbacks.onStatusLinePreview?.({
1898
1936
  maxRows: settings.get("statusLine.maxRows"),
@@ -1914,7 +1952,14 @@ export class SettingsSelectorComponent extends Container {
1914
1952
  const isThemeSetting = def.path === "theme.dark" || def.path === "theme.light";
1915
1953
  const getPreview = isThemeSetting ? this.callbacks.getStatusLinePreview : undefined;
1916
1954
 
1917
- return new SelectSubmenu(
1955
+ let submenu: SelectSubmenu;
1956
+ const finish = (value?: string): void => {
1957
+ if (this.#activeSelectSubmenu === submenu) this.#activeSelectSubmenu = null;
1958
+ if (this.#activeModelProfileSubmenu === submenu) this.#activeModelProfileSubmenu = null;
1959
+ submenu.dispose();
1960
+ done(value);
1961
+ };
1962
+ submenu = new SelectSubmenu(
1918
1963
  title,
1919
1964
  description,
1920
1965
  options,
@@ -1922,7 +1967,7 @@ export class SettingsSelectorComponent extends Container {
1922
1967
  value => {
1923
1968
  if (def.path === "modelProfile.default") {
1924
1969
  this.callbacks.onChange(def.path, value);
1925
- done(value);
1970
+ finish(value);
1926
1971
  return;
1927
1972
  }
1928
1973
  if (def.path === "theme.dark" || def.path === "theme.light") {
@@ -1937,7 +1982,7 @@ export class SettingsSelectorComponent extends Container {
1937
1982
  }
1938
1983
  if (!this.callbacks.onThemeCommit) return;
1939
1984
  void this.callbacks.onThemeCommit(def.path, value, currentValue).then(accepted => {
1940
- if (accepted) done(value);
1985
+ if (accepted) finish(value);
1941
1986
  });
1942
1987
  return;
1943
1988
  }
@@ -1953,21 +1998,26 @@ export class SettingsSelectorComponent extends Container {
1953
1998
  ) {
1954
1999
  return;
1955
2000
  }
1956
- done(accepted ? value : undefined);
2001
+ finish(accepted ? value : undefined);
1957
2002
  return;
1958
2003
  }
1959
2004
  if (!commitInteractiveSettings(this.callbacks, () => this.#setSettingValue(def.path, value))) return;
1960
2005
  this.callbacks.onChange(def.path, value);
1961
2006
  if (def.path === "ui.language") this.#refreshTabBarLanguage();
1962
- done(value);
2007
+ onPreviewEnd?.();
2008
+ finish(value);
1963
2009
  },
1964
2010
  () => {
1965
2011
  onPreviewCancel?.();
1966
- done();
2012
+ onPreviewEnd?.();
2013
+ finish();
1967
2014
  },
1968
2015
  onPreview,
1969
2016
  getPreview,
1970
2017
  );
2018
+ this.#activeSelectSubmenu = submenu;
2019
+ if (def.path === "modelProfile.default") this.#activeModelProfileSubmenu = submenu;
2020
+ return submenu;
1971
2021
  }
1972
2022
 
1973
2023
  /**
@@ -2277,8 +2327,24 @@ export class SettingsSelectorComponent extends Container {
2277
2327
  return (this.#currentList || this.#pluginComponent || this.#gjcBundleComponent || this.#notificationsEditor)!;
2278
2328
  }
2279
2329
 
2330
+ /** Refresh saved rows and model-profile choices without replacing active editors. */
2331
+ refreshFromConfiguration(availableModelProfiles: readonly string[]): void {
2332
+ this.context.availableModelProfiles = [...availableModelProfiles];
2333
+ this.#refreshCurrentTabItems();
2334
+ this.#activeModelProfileSubmenu?.refreshOptions(
2335
+ this.context.availableModelProfiles.map(profile => ({ value: profile, label: profile })),
2336
+ true,
2337
+ );
2338
+ this.#activeSelectSubmenu?.refreshPreview();
2339
+ this.#updateStatusPreview();
2340
+ }
2341
+
2280
2342
  override dispose(): void {
2281
2343
  if (this.#notificationsEditor?.navigationLocked) return;
2344
+ this.callbacks.onStatusLinePreviewEnd?.();
2345
+ this.#activeSelectSubmenu?.dispose();
2346
+ this.#activeSelectSubmenu = null;
2347
+ this.#activeModelProfileSubmenu = null;
2282
2348
  // Release an open provider-order editor (and its context subscriptions)
2283
2349
  // when the whole selector is torn down without a normal close.
2284
2350
  this.#activeProviderOrderEditor?.dispose();
@@ -6,6 +6,7 @@ import { getEnvApiKey, type ToolCall, type UsageLimit, type UsageReport } from "
6
6
  import type { ProviderDetails } from "@gajae-code/ai/provider-details";
7
7
  import { type Keybinding, Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
8
8
  import { formatBytes, formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
9
+ import { canonicalEnvKey, projectEnvSnapshot } from "@gajae-code/utils/env-file";
9
10
  import { resolveAppendOnlyMode } from "../../append-only-mode";
10
11
  import { jobElapsedMs } from "../../async";
11
12
  import { reset as resetCapabilities } from "../../capability";
@@ -107,6 +108,40 @@ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAcce
107
108
  ].join("\n");
108
109
  }
109
110
 
111
+ const GH_SHARE_DESTINATION_KEYS = ["GH_HOST", "GH_CONFIG_DIR"] as const;
112
+
113
+ /**
114
+ * Project dotenv at import, before `/move` changes cwd. Those declarations are
115
+ * already in the process environment, and a later `setProjectDir` does not
116
+ * remove them.
117
+ */
118
+ const ghShareProjectSnapshot = projectEnvSnapshot();
119
+
120
+ /**
121
+ * Environment for the default `/share` `gh` children. A `GH_HOST` or
122
+ * `GH_CONFIG_DIR` that matches the project dotenv is omitted so the repository
123
+ * cannot retarget gist upload or point `gh` at a config directory it shipped.
124
+ * An operator value the project does not declare is kept.
125
+ */
126
+ function ghShareChildEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
127
+ const child: NodeJS.ProcessEnv = { ...env };
128
+ for (const name of GH_SHARE_DESTINATION_KEYS) {
129
+ const raw = child[name];
130
+ if (typeof raw !== "string") continue;
131
+ const key = canonicalEnvKey(name);
132
+ const declared = ghShareProjectSnapshot.values[key];
133
+ if (declared === undefined) continue;
134
+ if (!(ghShareProjectSnapshot.dynamic.has(key) || declared === raw || declared === raw.trim())) continue;
135
+ delete child[name];
136
+ if (process.platform === "win32") {
137
+ for (const present of Object.keys(child)) {
138
+ if (canonicalEnvKey(present) === key) delete child[present];
139
+ }
140
+ }
141
+ }
142
+ return child;
143
+ }
144
+
110
145
  export class CommandController {
111
146
  constructor(private readonly ctx: InteractiveModeContext) {}
112
147
 
@@ -242,9 +277,10 @@ export class CommandController {
242
277
  return;
243
278
  }
244
279
 
280
+ const ghEnv = ghShareChildEnv();
245
281
  try {
246
282
  const authProcess = Bun.spawn(["gh", "auth", "status"], {
247
- env: process.env,
283
+ env: ghEnv,
248
284
  stdin: "ignore",
249
285
  stdout: "ignore",
250
286
  stderr: "ignore",
@@ -278,7 +314,7 @@ export class CommandController {
278
314
  let cancellationRequested = false;
279
315
  try {
280
316
  const gistProcess = Bun.spawn(["gh", "gist", "create", "--public=false", tmpFile], {
281
- env: process.env,
317
+ env: ghEnv,
282
318
  stdin: "ignore",
283
319
  stdout: "pipe",
284
320
  stderr: "pipe",