@gajae-code/coding-agent 0.11.7 → 0.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +73 -1
  2. package/dist/types/cli/args.d.ts +0 -2
  3. package/dist/types/cli.d.ts +1 -0
  4. package/dist/types/config/keybindings.d.ts +33 -3
  5. package/dist/types/config/model-profile-activation.d.ts +4 -3
  6. package/dist/types/config/model-profile-contract.d.ts +38 -0
  7. package/dist/types/config/model-profiles.d.ts +2 -3
  8. package/dist/types/config/settings-schema.d.ts +15 -2
  9. package/dist/types/edit/modes/patch.d.ts +13 -0
  10. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  11. package/dist/types/eval/py/executor.d.ts +1 -1
  12. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
  14. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
  15. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
  16. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
  17. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
  18. package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
  19. package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
  20. package/dist/types/gjc-runtime/state-writer.d.ts +44 -0
  21. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +1 -0
  22. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +24 -0
  23. package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
  24. package/dist/types/main.d.ts +8 -0
  25. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  26. package/dist/types/modes/components/tool-status-header.d.ts +3 -2
  27. package/dist/types/modes/components/welcome.d.ts +4 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  30. package/dist/types/modes/interactive-mode.d.ts +5 -2
  31. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  32. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  33. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  34. package/dist/types/modes/types.d.ts +3 -0
  35. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  36. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  37. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  38. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  39. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  40. package/dist/types/sdk/broker/broker.d.ts +2 -0
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
  43. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  44. package/dist/types/sdk/bus/config.d.ts +2 -0
  45. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  46. package/dist/types/sdk/bus/index.d.ts +25 -0
  47. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +15 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +128 -1
  52. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  53. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  54. package/dist/types/sdk/host/host.d.ts +1 -1
  55. package/dist/types/sdk/host/query/handlers.d.ts +9 -0
  56. package/dist/types/sdk/index.d.ts +2 -0
  57. package/dist/types/sdk/prompt-status.d.ts +88 -0
  58. package/dist/types/sdk/session-directory.d.ts +1 -1
  59. package/dist/types/sdk/startup-capability.d.ts +3 -0
  60. package/dist/types/session/agent-session.d.ts +2 -0
  61. package/dist/types/session/internal/managed-session-scope.d.ts +22 -3
  62. package/dist/types/session/internal/managed-session-storage.d.ts +24 -4
  63. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  64. package/dist/types/session/session-manager.d.ts +5 -1
  65. package/dist/types/session/session-storage.d.ts +31 -0
  66. package/dist/types/skill-state/active-state.d.ts +4 -0
  67. package/dist/types/task/index.d.ts +3 -1
  68. package/dist/types/task/receipt.d.ts +2 -0
  69. package/dist/types/task/types.d.ts +94 -0
  70. package/dist/types/tools/ask.d.ts +454 -174
  71. package/dist/types/tools/image-gen.d.ts +47 -1
  72. package/dist/types/tools/index.d.ts +4 -0
  73. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  74. package/package.json +8 -8
  75. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  76. package/scripts/dogfood-repository-binding.ts +209 -0
  77. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  78. package/scripts/run-test-manifest.ts +4 -4
  79. package/src/cli/args.ts +2 -92
  80. package/src/cli/mcp-cli.ts +4 -21
  81. package/src/cli/notify-cli.ts +5 -38
  82. package/src/cli.ts +50 -0
  83. package/src/commands/deep-interview.ts +9 -4
  84. package/src/commands/sdk.ts +2 -2
  85. package/src/commit/agentic/index.ts +3 -3
  86. package/src/commit/map-reduce/index.ts +2 -2
  87. package/src/config/keybindings.ts +157 -24
  88. package/src/config/model-profile-activation.ts +7 -18
  89. package/src/config/model-profile-contract.ts +161 -0
  90. package/src/config/model-profiles.ts +2 -3
  91. package/src/config/model-registry.ts +11 -1
  92. package/src/config/settings-schema.ts +17 -2
  93. package/src/coordinator-mcp/model-preset.ts +21 -74
  94. package/src/defaults/gjc/skills/deep-interview/SKILL.md +80 -165
  95. package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
  96. package/src/edit/index.ts +3 -6
  97. package/src/edit/modes/patch.ts +38 -1
  98. package/src/edit/modes/replace.ts +42 -0
  99. package/src/edit/path-mutation-lock.ts +112 -0
  100. package/src/eval/py/executor.ts +30 -11
  101. package/src/eval/py/prelude.py +2 -2
  102. package/src/eval/py/tool-bridge.ts +34 -18
  103. package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
  104. package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
  105. package/src/gjc-runtime/deep-interview-draft.ts +842 -0
  106. package/src/gjc-runtime/deep-interview-payload.ts +262 -0
  107. package/src/gjc-runtime/deep-interview-recorder.ts +294 -226
  108. package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
  109. package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
  110. package/src/gjc-runtime/deep-interview-state.ts +916 -4
  111. package/src/gjc-runtime/launch-tmux.ts +1 -1
  112. package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
  113. package/src/gjc-runtime/ralplan-runtime.ts +78 -4
  114. package/src/gjc-runtime/repository-binding.ts +267 -0
  115. package/src/gjc-runtime/session-state-sidecar.ts +25 -9
  116. package/src/gjc-runtime/state-writer.ts +166 -2
  117. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
  118. package/src/gjc-runtime/tmux-sessions.ts +45 -6
  119. package/src/gjc-runtime/ultragoal-guard.ts +5 -5
  120. package/src/gjc-runtime/ultragoal-runtime.ts +181 -19
  121. package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
  122. package/src/gjc-runtime/workflow-manifest.ts +191 -5
  123. package/src/internal-urls/docs-index.generated.ts +8 -6
  124. package/src/internal-urls/local-protocol.ts +33 -16
  125. package/src/internal-urls/mcp-protocol.ts +73 -11
  126. package/src/main.ts +25 -10
  127. package/src/modes/DESIGN.md +5 -0
  128. package/src/modes/acp/acp-agent.ts +50 -16
  129. package/src/modes/components/model-selector.ts +42 -8
  130. package/src/modes/components/plan-preview-overlay.ts +1 -1
  131. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  132. package/src/modes/components/tool-status-header.ts +20 -4
  133. package/src/modes/components/welcome.ts +32 -20
  134. package/src/modes/controllers/command-controller.ts +97 -54
  135. package/src/modes/controllers/event-controller.ts +10 -0
  136. package/src/modes/controllers/input-controller.ts +7 -2
  137. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  138. package/src/modes/controllers/selector-controller.ts +46 -54
  139. package/src/modes/interactive-mode.ts +70 -36
  140. package/src/modes/prompt-action-autocomplete.ts +32 -13
  141. package/src/modes/prompt-suggestion-controller.ts +96 -0
  142. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  143. package/src/modes/types.ts +3 -0
  144. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  145. package/src/modes/utils/ui-helpers.ts +5 -3
  146. package/src/prompts/agents/architect.md +0 -1
  147. package/src/prompts/agents/critic.md +0 -1
  148. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  149. package/src/prompts/system/system-prompt.md +7 -4
  150. package/src/prompts/tools/cron.md +3 -1
  151. package/src/runtime-mcp/json-rpc.ts +7 -4
  152. package/src/runtime-mcp/manager.ts +51 -53
  153. package/src/runtime-mcp/oauth-flow.ts +0 -72
  154. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  155. package/src/runtime-mcp/redaction.ts +30 -0
  156. package/src/runtime-mcp/transports/http.ts +13 -6
  157. package/src/sdk/broker/broker.ts +34 -20
  158. package/src/sdk/broker/lifecycle.ts +185 -36
  159. package/src/sdk/broker/session-index.ts +11 -1
  160. package/src/sdk/bus/chat-daemon-control.ts +20 -5
  161. package/src/sdk/bus/config-commands.ts +2 -2
  162. package/src/sdk/bus/config.ts +9 -1
  163. package/src/sdk/bus/conversation-store.ts +40 -6
  164. package/src/sdk/bus/index.ts +240 -65
  165. package/src/sdk/bus/lifecycle-commands.ts +47 -3
  166. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  167. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  168. package/src/sdk/bus/notification-orchestration.ts +12 -0
  169. package/src/sdk/bus/notification-service.ts +27 -2
  170. package/src/sdk/bus/prompt-reconciliation.ts +239 -0
  171. package/src/sdk/bus/recent-activity.ts +90 -26
  172. package/src/sdk/bus/telegram-daemon-contract.ts +15 -3
  173. package/src/sdk/bus/telegram-daemon-control.ts +145 -33
  174. package/src/sdk/bus/telegram-daemon.ts +853 -112
  175. package/src/sdk/bus/telegram-reference.ts +20 -2
  176. package/src/sdk/cli/session-cli.ts +15 -2
  177. package/src/sdk/host/control/dispatch.ts +1 -1
  178. package/src/sdk/host/control/operations.ts +1 -1
  179. package/src/sdk/host/host.ts +2 -6
  180. package/src/sdk/host/query/handlers.ts +70 -5
  181. package/src/sdk/index.ts +12 -0
  182. package/src/sdk/prompt-status.ts +97 -0
  183. package/src/sdk/protocol/operation-inventory.generated.json +47 -0
  184. package/src/sdk/protocol/operation-registry.ts +18 -4
  185. package/src/sdk/session-directory.ts +5 -1
  186. package/src/sdk/session.ts +2 -0
  187. package/src/sdk/startup-capability.ts +9 -1
  188. package/src/session/agent-session.ts +138 -5
  189. package/src/session/blob-store.ts +7 -2
  190. package/src/session/internal/managed-session-scope.ts +998 -133
  191. package/src/session/internal/managed-session-storage.ts +268 -203
  192. package/src/session/internal/native-publish-outcome.ts +266 -0
  193. package/src/session/session-manager.ts +406 -360
  194. package/src/session/session-storage.ts +151 -11
  195. package/src/skill-state/active-state.ts +10 -0
  196. package/src/task/executor.ts +13 -5
  197. package/src/task/gjc-command.ts +2 -2
  198. package/src/task/index.ts +121 -15
  199. package/src/task/receipt.ts +3 -0
  200. package/src/task/types.ts +34 -3
  201. package/src/tools/ask.ts +199 -88
  202. package/src/tools/ast-edit.ts +2 -2
  203. package/src/tools/bash-pty-selection.ts +2 -2
  204. package/src/tools/browser/screenshot-format.ts +6 -1
  205. package/src/tools/cron.ts +1 -1
  206. package/src/tools/image-gen.ts +178 -6
  207. package/src/tools/index.ts +4 -0
  208. package/src/tools/render-utils.ts +7 -1
  209. package/src/utils/edit-mode.ts +2 -2
  210. package/src/utils/prompt-suggestion.ts +265 -0
  211. package/src/utils/sixel.ts +3 -3
  212. package/src/web/insane/url-guard.ts +3 -1
  213. package/src/web/scrapers/docs-rs.ts +30 -5
@@ -1,6 +1,7 @@
1
1
  import { type Component, padding, TERMINAL, truncateToWidth, visibleWidth } from "@gajae-code/tui";
2
2
  import { APP_NAME } from "@gajae-code/utils";
3
3
  import { formatBuildLabel } from "../../build-metadata";
4
+ import { formatKeyHint, type KeyDisplayContext } from "../../config/keybindings";
4
5
  import { type ThemeColor, theme } from "../../modes/theme/theme";
5
6
 
6
7
  export interface RecentSession {
@@ -21,26 +22,28 @@ export interface WelcomeComponentOptions {
21
22
  changelogMarkdown?: string;
22
23
  collapseChangelog?: boolean;
23
24
  buildLabel?: string;
25
+ keyDisplayContext?: KeyDisplayContext;
24
26
  }
25
27
 
26
28
  const WELCOME_STATIC_RIGHT_ROWS_EXCLUDING_DYNAMIC_SECTIONS = 9;
27
29
  const DEFAULT_WHATS_NEW_ROWS = 3;
28
30
  const MAX_WHATS_NEW_ROWS = 12;
29
- const NEWLINE_FLOW_KEY =
30
- process.platform === "win32" ? { key: "alt+enter", label: "newline" } : { key: "ctrl+j", label: "newline" };
31
-
32
- const FLOW_KEY_ITEMS: ReadonlyArray<{ key: string; label: string }> = [
33
- { key: "/", label: "commands" },
34
- { key: "#", label: "actions" },
35
- { key: "!", label: "shell" },
36
- { key: "$", label: "python" },
37
- { key: "?", label: "keymap" },
38
- { key: "ctrl+l", label: "model" },
39
- { key: "shift+tab", label: "reasoning" },
40
- { key: "tab", label: "complete" },
41
- NEWLINE_FLOW_KEY,
42
- { key: "ctrl+c", label: "clear" },
43
- ];
31
+
32
+ function flowKeyItems(context: KeyDisplayContext): ReadonlyArray<{ key: string; label: string }> {
33
+ const newlineKey = context.platform === "win32" ? "alt+enter" : "ctrl+j";
34
+ return [
35
+ { key: "/", label: "commands" },
36
+ { key: "#", label: "actions" },
37
+ { key: "!", label: "shell" },
38
+ { key: "$", label: "python" },
39
+ { key: "?", label: "keymap" },
40
+ { key: "ctrl+l", label: "model" },
41
+ { key: "shift+tab", label: "reasoning" },
42
+ { key: "tab", label: "complete" },
43
+ { key: newlineKey, label: "newline" },
44
+ { key: "ctrl+c", label: "clear" },
45
+ ];
46
+ }
44
47
 
45
48
  /**
46
49
  * GJC-native launch surface with compact command affordances, project
@@ -325,7 +328,8 @@ export class WelcomeComponent implements Component {
325
328
  }
326
329
 
327
330
  #flowKeyItemText(item: { key: string; label: string }): string {
328
- return `${theme.fg("dim", item.key)}${theme.fg("muted", ` ${item.label}`)}`;
331
+ const context = this.options.keyDisplayContext ?? { platform: process.platform };
332
+ return `${theme.fg("dim", formatKeyHint(item.key, context))}${theme.fg("muted", ` ${item.label}`)}`;
329
333
  }
330
334
 
331
335
  #flowKeyRows(width: number): string[] {
@@ -333,7 +337,7 @@ export class WelcomeComponent implements Component {
333
337
  const separator = ` ${theme.fg("dim", "·")} `;
334
338
  const rows: string[] = [];
335
339
  let current = "";
336
- for (const item of FLOW_KEY_ITEMS) {
340
+ for (const item of flowKeyItems(this.options.keyDisplayContext ?? { platform: process.platform })) {
337
341
  const segment = this.#flowKeyItemText(item);
338
342
  const next = current ? `${current}${separator}${segment}` : segment;
339
343
  if (current && visibleWidth(next) > contentWidth) {
@@ -352,7 +356,7 @@ export class WelcomeComponent implements Component {
352
356
  const rowLimit = Math.max(1, Math.floor(maxRows));
353
357
  if (rows.length <= rowLimit) return rows;
354
358
  if (rowLimit === 1) {
355
- const firstItem = FLOW_KEY_ITEMS[0];
359
+ const firstItem = flowKeyItems(this.options.keyDisplayContext ?? { platform: process.platform })[0];
356
360
  const firstSegment = firstItem ? this.#flowKeyItemText(firstItem) : theme.fg("dim", "keys");
357
361
  return [this.#fitToWidth(` ${firstSegment} ${theme.fg("dim", "· … ")}${theme.bold("/help")}`, width)];
358
362
  }
@@ -661,8 +665,16 @@ function gradientLogo(lines: readonly string[], phase = 0, shine?: ShineConfig):
661
665
 
662
666
  /** Total length of the intro animation. */
663
667
  const INTRO_MS = 3000;
664
- /** Render cadence during the intro (~30fps). */
665
- const INTRO_TICK_MS = 33;
668
+ /** Resolve the intro cadence without making tests mutate global process state. */
669
+ export function resolveWelcomeIntroTickMs(
670
+ platform: NodeJS.Platform = process.platform,
671
+ tmux = process.env.TMUX,
672
+ ): number {
673
+ return platform === "win32" && tmux ? 100 : 33;
674
+ }
675
+
676
+ /** Render at 30fps directly, but cap native Windows multiplexers at 10fps to avoid ConPTY output backpressure. */
677
+ const INTRO_TICK_MS = resolveWelcomeIntroTickMs();
666
678
  /** Number of full gradient rotations the sweep performs before settling. */
667
679
  const INTRO_SWEEPS = 2.5;
668
680
  /** Number of times the shine highlight crosses the diagonal across the intro. */
@@ -10,12 +10,12 @@ import {
10
10
  type UsageLimit,
11
11
  type UsageReport,
12
12
  } from "@gajae-code/ai";
13
- import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
13
+ import { type Keybinding, Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
14
14
  import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
15
- import { $ } from "bun";
16
15
  import { resolveAppendOnlyMode } from "../../append-only-mode";
17
16
  import { jobElapsedMs } from "../../async";
18
17
  import { reset as resetCapabilities } from "../../capability";
18
+ import type { KeybindingsManager } from "../../config/keybindings";
19
19
  import { clearClaudePluginRootsCache } from "../../discovery/helpers";
20
20
  import { loadCustomShare } from "../../export/custom-share";
21
21
  import type { CompactOptions } from "../../extensibility/extensions/types";
@@ -36,7 +36,7 @@ import { EvalExecutionComponent } from "../../modes/components/eval-execution";
36
36
  import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
37
37
  import type { InteractiveModeContext } from "../../modes/types";
38
38
  import { computeContextBreakdown, renderContextUsage } from "../../modes/utils/context-usage";
39
- import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
39
+ import { buildHotkeysMarkdown, formatHotkeyMarkdownCode } from "../../modes/utils/hotkeys-markdown";
40
40
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
41
41
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
42
42
  import type { AuthStorage } from "../../session/auth-storage";
@@ -61,6 +61,39 @@ function showMarkdownPanel(ctx: InteractiveModeContext, title: string, markdown:
61
61
  ctx.ui.requestRender();
62
62
  }
63
63
 
64
+ export function buildHelpMarkdown(keybindings: Pick<KeybindingsManager, "getAccessibleDisplayString">): string {
65
+ const displayKey = (action: Keybinding): string => keybindings.getAccessibleDisplayString(action) || "Disabled";
66
+ const sessionNewKey = formatHotkeyMarkdownCode(displayKey("app.session.new"));
67
+ const selectModelKey = formatHotkeyMarkdownCode(displayKey("app.model.select"));
68
+ const autocompleteNavigationKeys = formatHotkeyMarkdownCode(
69
+ `${displayKey("tui.select.up")}/${displayKey("tui.select.down")}`,
70
+ false,
71
+ );
72
+ const autocompleteConfirmKeys = formatHotkeyMarkdownCode(
73
+ `${displayKey("tui.input.submit")}/${displayKey("tui.input.tab")}`,
74
+ false,
75
+ );
76
+
77
+ return [
78
+ "**Beginner actions**",
79
+ "",
80
+ "| Action | How |",
81
+ "|---|---|",
82
+ `| Start a fresh session | ${sessionNewKey} or \`/new\` |`,
83
+ "| Resume another session | `/resume` |",
84
+ "| Show session details | `/session info` |",
85
+ "| Delete current session transcript/artifacts | `/session delete` |",
86
+ `| Select a model | \`/model\` or ${selectModelKey} |`,
87
+ "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
88
+ "",
89
+ "**Finding commands**",
90
+ "",
91
+ `- Type \`/\` to browse slash commands, then use ${autocompleteNavigationKeys} and ${autocompleteConfirmKeys}.`,
92
+ "- Type `#` to browse prompt actions like starting a session, pasting an image, or moving the cursor.",
93
+ "- Type `!` for shell commands and `$` for Python snippets.",
94
+ ].join("\n");
95
+ }
96
+
64
97
  export class CommandController {
65
98
  constructor(private readonly ctx: InteractiveModeContext) {}
66
99
 
@@ -120,21 +153,34 @@ export class CommandController {
120
153
  }
121
154
 
122
155
  async handleShareCommand(): Promise<void> {
123
- const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
124
- const cleanupTempFile = async () => {
156
+ let tempDir: string | undefined;
157
+ try {
158
+ let tmpFile: string;
125
159
  try {
126
- await fs.rm(tmpFile, { force: true });
127
- } catch {
128
- // Ignore cleanup errors
160
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-share-"));
161
+ if (process.platform !== "win32") await fs.chmod(tempDir, 0o700);
162
+ tmpFile = path.join(tempDir, "session.html");
163
+ const file = await fs.open(tmpFile, "wx", 0o600);
164
+ await file.close();
165
+ await this.ctx.session.exportToHtml(tmpFile);
166
+ if (process.platform !== "win32") await fs.chmod(tmpFile, 0o600);
167
+ } catch (error: unknown) {
168
+ this.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
169
+ return;
170
+ }
171
+ await this.#shareExport(tmpFile);
172
+ } finally {
173
+ if (tempDir) {
174
+ try {
175
+ await fs.rm(tempDir, { recursive: true, force: true });
176
+ } catch {
177
+ // Ignore cleanup errors
178
+ }
129
179
  }
130
- };
131
- try {
132
- await this.ctx.session.exportToHtml(tmpFile);
133
- } catch (error: unknown) {
134
- this.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
135
- return;
136
180
  }
181
+ }
137
182
 
183
+ async #shareExport(tmpFile: string): Promise<void> {
138
184
  try {
139
185
  const customShare = await loadCustomShare();
140
186
  if (customShare) {
@@ -149,7 +195,6 @@ export class CommandController {
149
195
  this.ctx.editorContainer.clear();
150
196
  this.ctx.editorContainer.addChild(this.ctx.editor);
151
197
  this.ctx.ui.setFocus(this.ctx.editor);
152
- await cleanupTempFile();
153
198
  };
154
199
 
155
200
  try {
@@ -176,20 +221,22 @@ export class CommandController {
176
221
  }
177
222
  }
178
223
  } catch (err) {
179
- await cleanupTempFile();
180
224
  this.ctx.showError(err instanceof Error ? err.message : String(err));
181
225
  return;
182
226
  }
183
227
 
184
228
  try {
185
- const authResult = await $`gh auth status`.quiet().nothrow();
186
- if (authResult.exitCode !== 0) {
187
- await cleanupTempFile();
229
+ const authProcess = Bun.spawn(["gh", "auth", "status"], {
230
+ env: process.env,
231
+ stdin: "ignore",
232
+ stdout: "ignore",
233
+ stderr: "ignore",
234
+ });
235
+ if ((await authProcess.exited) !== 0) {
188
236
  this.ctx.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
189
237
  return;
190
238
  }
191
239
  } catch {
192
- await cleanupTempFile();
193
240
  this.ctx.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
194
241
  return;
195
242
  }
@@ -205,27 +252,40 @@ export class CommandController {
205
252
  this.ctx.editorContainer.clear();
206
253
  this.ctx.editorContainer.addChild(this.ctx.editor);
207
254
  this.ctx.ui.setFocus(this.ctx.editor);
208
- await cleanupTempFile();
209
- };
210
-
211
- loader.onAbort = () => {
212
- void restoreEditor();
213
- this.ctx.showStatus("Share cancelled");
214
255
  };
215
256
 
257
+ let cancellationRequested = false;
216
258
  try {
217
- const result = await $`gh gist create --public=false ${tmpFile}`.quiet().nothrow();
218
- if (loader.signal.aborted) return;
259
+ const gistProcess = Bun.spawn(["gh", "gist", "create", "--public=false", tmpFile], {
260
+ env: process.env,
261
+ stdin: "ignore",
262
+ stdout: "pipe",
263
+ stderr: "pipe",
264
+ signal: loader.signal,
265
+ });
266
+ loader.onAbort = () => {
267
+ cancellationRequested = gistProcess.exitCode === null;
268
+ };
269
+ const stdoutPromise = new Response(gistProcess.stdout).text();
270
+ const stderrPromise = new Response(gistProcess.stderr).text();
271
+ const exitCode = await gistProcess.exited;
272
+ const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
273
+
274
+ if (cancellationRequested) {
275
+ await restoreEditor();
276
+ this.ctx.showStatus("Share cancelled");
277
+ return;
278
+ }
219
279
 
220
280
  await restoreEditor();
221
281
 
222
- if (result.exitCode !== 0) {
223
- const errorMsg = result.stderr.toString("utf-8").trim() || "Unknown error";
282
+ if (exitCode !== 0) {
283
+ const errorMsg = stderr.trim() || "Unknown error";
224
284
  this.ctx.showError(`Failed to create gist: ${errorMsg}`);
225
285
  return;
226
286
  }
227
287
 
228
- const gistUrl = result.stdout.toString("utf-8").trim();
288
+ const gistUrl = stdout.trim();
229
289
  const gistId = gistUrl.split("/").pop();
230
290
  if (!gistId) {
231
291
  this.ctx.showError("Failed to parse gist ID from gh output");
@@ -236,10 +296,12 @@ export class CommandController {
236
296
  this.ctx.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
237
297
  this.openInBrowser(previewUrl);
238
298
  } catch (error: unknown) {
239
- if (!loader.signal.aborted) {
240
- await restoreEditor();
241
- this.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
299
+ await restoreEditor();
300
+ if (cancellationRequested) {
301
+ this.ctx.showStatus("Share cancelled");
302
+ return;
242
303
  }
304
+ this.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
243
305
  }
244
306
  }
245
307
 
@@ -577,26 +639,7 @@ export class CommandController {
577
639
  }
578
640
 
579
641
  handleHelpCommand(): void {
580
- const sessionNewKey = this.ctx.keybindings.getDisplayString("app.session.new") || "/new";
581
- const markdown = [
582
- "**Beginner actions**",
583
- "",
584
- "| Action | How |",
585
- "|---|---|",
586
- `| Start a fresh session | \`${sessionNewKey}\` or \`/new\` |`,
587
- "| Resume another session | `/resume` |",
588
- "| Show session details | `/session info` |",
589
- "| Delete current session transcript/artifacts | `/session delete` |",
590
- "| Select a model | `/model` or `Ctrl+L` |",
591
- "| Show all shortcuts | `?` on an empty prompt or `/hotkeys` |",
592
- "",
593
- "**Finding commands**",
594
- "",
595
- "- Type `/` to browse slash commands, then use arrows and Enter/Tab.",
596
- "- Type `#` to browse prompt actions like starting a session, pasting an image, or moving the cursor.",
597
- "- Type `!` for shell commands and `$` for Python snippets.",
598
- ].join("\n");
599
- showMarkdownPanel(this.ctx, "GJC Help", markdown);
642
+ showMarkdownPanel(this.ctx, "GJC Help", buildHelpMarkdown(this.ctx.keybindings));
600
643
  }
601
644
 
602
645
  handleToolsCommand(): void {
@@ -242,6 +242,7 @@ export class EventController {
242
242
  }
243
243
 
244
244
  async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
245
+ this.ctx.promptSuggestion?.onAgentStart();
245
246
  this.#lastIntent = undefined;
246
247
  this.#readToolCallArgs.clear();
247
248
  this.#readToolCallAssistantComponents.clear();
@@ -836,6 +837,7 @@ export class EventController {
836
837
  this.ctx.ui.requestRender();
837
838
  this.#scheduleIdleCompaction();
838
839
  this.sendCompletionNotification();
840
+ this.ctx.promptSuggestion?.onAgentEnd();
839
841
  }
840
842
 
841
843
  async #handleAutoCompactionStart(
@@ -847,6 +849,10 @@ export class EventController {
847
849
  this.ctx.editor.onEscape = () => {
848
850
  this.ctx.session.abortCompaction();
849
851
  };
852
+ if (this.ctx.loadingAnimation) {
853
+ this.ctx.loadingAnimation.stop();
854
+ this.ctx.loadingAnimation = undefined;
855
+ }
850
856
  this.ctx.statusContainer.clear();
851
857
  const reasonText =
852
858
  event.reason === "overflow" ? "Context overflow detected, " : event.reason === "idle" ? "Idle " : "";
@@ -939,6 +945,10 @@ export class EventController {
939
945
  this.ctx.session.abortRetry();
940
946
  }
941
947
  };
948
+ if (this.ctx.loadingAnimation) {
949
+ this.ctx.loadingAnimation.stop();
950
+ this.ctx.loadingAnimation = undefined;
951
+ }
942
952
  this.ctx.statusContainer.clear();
943
953
  // Stop any prior retry loader/timer before installing a new one.
944
954
  this.ctx.retryLoader?.stop();
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@gajae-code/tui";
5
- import { $env, logger, sanitizeText } from "@gajae-code/utils";
5
+ import { $pickenv, logger, sanitizeText } from "@gajae-code/utils";
6
6
  import { type AppKeybinding, KEYBINDINGS } from "../../config/keybindings";
7
7
  import { isSettingsInitialized, settings } from "../../config/settings";
8
8
  import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
@@ -725,8 +725,12 @@ export class InputController {
725
725
  });
726
726
  }
727
727
 
728
+ // Tab on an empty composer accepts the pending ghost-text prompt suggestion.
729
+ this.ctx.editor.onTab = (text: string) => this.ctx.promptSuggestion?.tryAcceptOnTab(text) === true;
730
+
728
731
  this.ctx.editor.onChange = (text: string) => {
729
732
  this.#resetEscapeGestures();
733
+ this.ctx.promptSuggestion?.notifyEditorChanged(text);
730
734
  const wasBashMode = this.ctx.isBashMode;
731
735
  const wasBashNoContext = this.ctx.isBashNoContext;
732
736
  const wasPythonMode = this.ctx.isPythonMode;
@@ -930,7 +934,7 @@ export class InputController {
930
934
 
931
935
  // Generate session title on first message
932
936
  const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
933
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
937
+ if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$pickenv("GJC_NO_TITLE", "PI_NO_TITLE")) {
934
938
  const registry = this.ctx.session.modelRegistry;
935
939
  generateSessionTitle(
936
940
  text,
@@ -1789,6 +1793,7 @@ export class InputController {
1789
1793
  moveCursorToMessageStart: () => this.ctx.editor.moveToMessageStart(),
1790
1794
  moveCursorToLineStart: () => this.ctx.editor.moveToLineStart(),
1791
1795
  moveCursorToLineEnd: () => this.ctx.editor.moveToLineEnd(),
1796
+ getPromptSuggestion: () => this.ctx.promptSuggestion?.current ?? null,
1792
1797
  });
1793
1798
  }
1794
1799
 
@@ -4,6 +4,7 @@
4
4
  * Handles /mcp subcommands for managing MCP servers.
5
5
  */
6
6
  import * as path from "node:path";
7
+ import { resolveMCPOAuthResourceOrigin, resolveMCPOAuthTokenEndpoint } from "@gajae-code/ai";
7
8
  import { Spacer, Text } from "@gajae-code/tui";
8
9
  import { getMCPConfigPath, getProjectDir } from "@gajae-code/utils";
9
10
  import type { SourceMeta } from "../../capability/types";
@@ -61,6 +62,7 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string)
61
62
  */
62
63
  interface OAuthFlowResult {
63
64
  credentialId: string;
65
+ tokenEndpoint: string;
64
66
  clientId?: string;
65
67
  clientSecret?: string;
66
68
  }
@@ -423,6 +425,7 @@ export class MCPCommandController {
423
425
  try {
424
426
  const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
425
427
  const oauthResult = await this.#handleOAuthFlow(
428
+ finalConfig.url,
426
429
  oauth.authorizationUrl,
427
430
  oauth.tokenUrl,
428
431
  oauth.clientId ?? finalConfig.oauth?.clientId ?? "",
@@ -439,7 +442,7 @@ export class MCPCommandController {
439
442
  auth: {
440
443
  type: "oauth",
441
444
  credentialId: oauthResult.credentialId,
442
- tokenUrl: oauth.tokenUrl,
445
+ tokenUrl: oauthResult.tokenEndpoint,
443
446
  clientId: persistedClientId,
444
447
  clientSecret: persistedClientSecret,
445
448
  },
@@ -480,8 +483,15 @@ export class MCPCommandController {
480
483
  done();
481
484
  this.#handleWizardCancel();
482
485
  },
483
- async (authUrl: string, tokenUrl: string, clientId: string, clientSecret: string, scopes: string) => {
484
- return await this.#handleOAuthFlow(authUrl, tokenUrl, clientId, clientSecret, scopes);
486
+ async (
487
+ endpointUrl: string,
488
+ authUrl: string,
489
+ tokenUrl: string,
490
+ clientId: string,
491
+ clientSecret: string,
492
+ scopes: string,
493
+ ) => {
494
+ return await this.#handleOAuthFlow(endpointUrl, authUrl, tokenUrl, clientId, clientSecret, scopes);
485
495
  },
486
496
  async (config: MCPServerConfig) => {
487
497
  return await this.#handleTestConnection(config);
@@ -503,6 +513,7 @@ export class MCPCommandController {
503
513
  * Handle OAuth authentication flow for MCP server
504
514
  */
505
515
  async #handleOAuthFlow(
516
+ endpointUrl: string,
506
517
  authUrl: string,
507
518
  tokenUrl: string,
508
519
  clientId: string,
@@ -514,11 +525,13 @@ export class MCPCommandController {
514
525
  ): Promise<OAuthFlowResult> {
515
526
  const authStorage = this.ctx.session.modelRegistry.authStorage;
516
527
  let parsedAuthUrl: URL;
528
+ const resourceOrigin = resolveMCPOAuthResourceOrigin(endpointUrl);
529
+ const tokenEndpoint = resolveMCPOAuthTokenEndpoint(tokenUrl);
530
+ if (!resourceOrigin || !tokenEndpoint) throw new Error("Invalid MCP OAuth configuration.");
517
531
 
518
532
  // Validate OAuth URLs
519
533
  try {
520
534
  parsedAuthUrl = new URL(authUrl);
521
- new URL(tokenUrl);
522
535
  } catch (_error) {
523
536
  throw new Error(
524
537
  `Invalid OAuth URLs. Please check:\n Authorization URL: ${authUrl}\n Token URL: ${tokenUrl}`,
@@ -533,7 +546,7 @@ export class MCPCommandController {
533
546
  const flow = new MCPOAuthFlow(
534
547
  {
535
548
  authorizationUrl: authUrl,
536
- tokenUrl: tokenUrl,
549
+ tokenUrl: tokenEndpoint,
537
550
  clientId: resolvedClientId,
538
551
  clientSecret: resolvedClientSecret,
539
552
  scopes: scopes || undefined,
@@ -618,6 +631,10 @@ export class MCPCommandController {
618
631
  const oauthCredential: OAuthCredential = {
619
632
  type: "oauth",
620
633
  ...credentials,
634
+ mcpBinding: {
635
+ resourceOrigin,
636
+ tokenEndpoint,
637
+ },
621
638
  };
622
639
 
623
640
  // Store under a synthetic provider name
@@ -625,6 +642,7 @@ export class MCPCommandController {
625
642
 
626
643
  return {
627
644
  credentialId,
645
+ tokenEndpoint,
628
646
  clientId: flow.resolvedClientId,
629
647
  clientSecret: flow.registeredClientSecret,
630
648
  };
@@ -1328,6 +1346,10 @@ export class MCPCommandController {
1328
1346
  this.ctx.showError(`Server "${name}" is disabled. Run /mcp enable ${name} first.`);
1329
1347
  return;
1330
1348
  }
1349
+ if (found.config.type !== "http" && found.config.type !== "sse") {
1350
+ this.ctx.showError(`Server "${name}" does not use a remote MCP transport.`);
1351
+ return;
1352
+ }
1331
1353
 
1332
1354
  const currentAuth = (found.config as MCPServerConfig & { auth?: MCPAuthConfig }).auth;
1333
1355
  if (currentAuth?.type === "oauth") {
@@ -1341,6 +1363,7 @@ export class MCPCommandController {
1341
1363
  this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
1342
1364
 
1343
1365
  const oauthResult = await this.#handleOAuthFlow(
1366
+ found.config.url,
1344
1367
  oauth.authorizationUrl,
1345
1368
  oauth.tokenUrl,
1346
1369
  oauth.clientId ?? found.config.oauth?.clientId ?? "",
@@ -1359,7 +1382,7 @@ export class MCPCommandController {
1359
1382
  auth: {
1360
1383
  type: "oauth",
1361
1384
  credentialId: oauthResult.credentialId,
1362
- tokenUrl: oauth.tokenUrl,
1385
+ tokenUrl: oauthResult.tokenEndpoint,
1363
1386
  clientId: persistedClientId,
1364
1387
  clientSecret: persistedClientSecret,
1365
1388
  },