@gajae-code/coding-agent 0.18.7 → 0.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { Container, matchesKey, type SelectItem, SelectList, Text } from "@gajae-code/tui";
22
22
  import { sanitizeText } from "@gajae-code/utils";
23
+ import { resolveGlobalUserSkillLinkTrust } from "../../../config/skill-settings-defaults";
23
24
  import { type CustomizationInventory, loadCustomizationInventory } from "../../../customization/inventory";
24
25
  import {
25
26
  removeHookFile,
@@ -47,6 +48,7 @@ import { ImportWizard } from "./import-wizard";
47
48
  /** Minimal settings slice the dashboard reads and writes. */
48
49
  export interface CustomizationSettingsSlice {
49
50
  get(key: string): unknown;
51
+ getGlobal?(key: string): unknown;
50
52
  set?(key: string, value: unknown): void;
51
53
  }
52
54
 
@@ -150,6 +152,16 @@ export class CustomizationDashboard extends Container {
150
152
  cwd: this.#cwd,
151
153
  home: this.#homeDir,
152
154
  policy: this.#skillPolicy(),
155
+ allowExternalUserSkillSymlinks: resolveGlobalUserSkillLinkTrust({
156
+ trustUserSkills:
157
+ typeof this.#settings?.getGlobal === "function"
158
+ ? (this.#settings.getGlobal("skills.trustUserSkills") as boolean | undefined)
159
+ : undefined,
160
+ enablePiUser:
161
+ typeof this.#settings?.getGlobal === "function"
162
+ ? (this.#settings.getGlobal("skills.enablePiUser") as boolean | undefined)
163
+ : undefined,
164
+ }),
153
165
  disabledExtensions: this.#getStringArray("disabledExtensions"),
154
166
  });
155
167
  }
@@ -59,7 +59,7 @@ export class ExtensionDashboard extends Container {
59
59
  async #init(): Promise<void> {
60
60
  const sm = this.settings ?? (await Settings.init());
61
61
  const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
62
- this.#state = await createInitialState(this.cwd, disabledIds);
62
+ this.#state = await createInitialState(this.cwd, disabledIds, sm);
63
63
 
64
64
  // Calculate max visible items based on terminal height
65
65
  // Reserve ~10 lines for header, tabs, help text, borders
@@ -193,7 +193,7 @@ export class ExtensionDashboard extends Container {
193
193
 
194
194
  const sm = this.settings ?? Settings.instance;
195
195
  const disabledIds = sm ? ((sm.get("disabledExtensions") as string[]) ?? []) : [];
196
- const nextState = await refreshState(this.#state, this.cwd, disabledIds);
196
+ const nextState = await refreshState(this.#state, this.cwd, disabledIds, sm);
197
197
  if (refreshToken !== this.#refreshToken) return;
198
198
  this.#state = nextState;
199
199
 
@@ -14,6 +14,7 @@ import type { Skill } from "../../../capability/skill";
14
14
  import type { SlashCommand } from "../../../capability/slash-command";
15
15
  import type { CustomTool } from "../../../capability/tool";
16
16
  import type { SourceMeta } from "../../../capability/types";
17
+ import type { Settings } from "../../../config/settings";
17
18
  import {
18
19
  disableProvider,
19
20
  enableProvider,
@@ -43,7 +44,11 @@ export interface ExtensionSettingsManager {
43
44
  /**
44
45
  * Load all extensions from all capabilities.
45
46
  */
46
- export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): Promise<Extension[]> {
47
+ export async function loadAllExtensions(
48
+ cwd?: string,
49
+ disabledIds?: string[],
50
+ settings?: Settings,
51
+ ): Promise<Extension[]> {
47
52
  const extensions: Extension[] = [];
48
53
  const disabledExtensions = new Set<string>(disabledIds ?? []);
49
54
 
@@ -97,7 +102,7 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P
97
102
  }
98
103
  }
99
104
 
100
- const loadOpts = cwd ? { cwd, includeDisabled: true } : { includeDisabled: true };
105
+ const loadOpts = cwd ? { cwd, includeDisabled: true, settings } : { includeDisabled: true, settings };
101
106
 
102
107
  // Load skills
103
108
  try {
@@ -553,8 +558,12 @@ export function applyDisabledExtensionsToState(state: DashboardState, disabledId
553
558
  /**
554
559
  * Create initial dashboard state.
555
560
  */
556
- export async function createInitialState(cwd?: string, disabledIds?: string[]): Promise<DashboardState> {
557
- const extensions = await loadAllExtensions(cwd, disabledIds);
561
+ export async function createInitialState(
562
+ cwd?: string,
563
+ disabledIds?: string[],
564
+ settings?: Settings,
565
+ ): Promise<DashboardState> {
566
+ const extensions = await loadAllExtensions(cwd, disabledIds, settings);
558
567
  const tabs = buildProviderTabs(extensions);
559
568
  const tabFiltered = extensions; // "all" tab by default
560
569
  const searchFiltered = tabFiltered;
@@ -592,8 +601,9 @@ export async function refreshState(
592
601
  state: DashboardState,
593
602
  cwd?: string,
594
603
  disabledIds?: string[],
604
+ settings?: Settings,
595
605
  ): Promise<DashboardState> {
596
- const extensions = await loadAllExtensions(cwd, disabledIds);
606
+ const extensions = await loadAllExtensions(cwd, disabledIds, settings);
597
607
  const tabs = buildProviderTabs(extensions);
598
608
 
599
609
  // Get current provider from tabs
@@ -1369,10 +1369,7 @@ export class ModelSelectorComponent extends Container {
1369
1369
  }
1370
1370
 
1371
1371
  #getProfileAvailableModels(): Model[] {
1372
- const getAvailableForProfileActivation = this.#modelRegistry.getAvailableForProfileActivation;
1373
- return typeof getAvailableForProfileActivation === "function"
1374
- ? getAvailableForProfileActivation.call(this.#modelRegistry)
1375
- : this.#modelRegistry.getAvailable();
1372
+ return this.#modelRegistry.getAvailable();
1376
1373
  }
1377
1374
 
1378
1375
  #createProfileResolutionRegistry(availableModels: readonly Model[]) {
@@ -30,7 +30,7 @@ const PROVIDER_ONBOARDING_OPTIONS: ProviderOnboardingOption[] = [
30
30
  },
31
31
  {
32
32
  label: "Import existing credentials",
33
- description: "Detect and import Claude Code / Codex CLI logins already on this machine.",
33
+ description: "Detect and import Claude Code / Codex CLI / Kiro CLI logins already on this machine.",
34
34
  action: "import-credentials",
35
35
  },
36
36
  ];
@@ -109,6 +109,10 @@ const modelSegment: StatusLineSegment = {
109
109
  const modelName = shortenModelId(state.model?.id ?? state.model?.name);
110
110
 
111
111
  let content = withIcon(theme.icon.model, modelName);
112
+ const unavailableProfile = ctx.session.getUnavailableModelProfile?.();
113
+ if (unavailableProfile) {
114
+ content = `${theme.fg("warning", `profile unavailable: ${sanitizeStatusText(unavailableProfile)}`)} ${content}`;
115
+ }
112
116
 
113
117
  if (ctx.session.isFastModeActive() && theme.icon.fast) {
114
118
  content += ` ${theme.icon.fast}`;
@@ -60,6 +60,8 @@ export const __eventControllerPerfCounters = {
60
60
  const COMPLETION_NOTIFY_COMMAND_TIMEOUT_MS = 10_000;
61
61
  const TEXTLESS_LENGTH_WARNING =
62
62
  'Response ended with stopReason "length" before producing visible text or a tool call. Check the model\'s output and context token limits (maxTokens in models.yml) or lower the reasoning level (/reasoning), then retry.';
63
+ const TEXTLESS_INCOMPLETE_WARNING =
64
+ "Response ended before producing visible text or a tool call. See the provider diagnostic for the reason.";
63
65
 
64
66
  function isTextlessLengthStop(message: AssistantMessage | undefined): boolean {
65
67
  return (
@@ -1090,7 +1092,7 @@ export class EventController {
1090
1092
  event.stopReason !== "maintenance" &&
1091
1093
  isTextlessLengthStop(lastAssistant)
1092
1094
  ) {
1093
- this.ctx.showWarning(TEXTLESS_LENGTH_WARNING);
1095
+ this.ctx.showWarning(lastAssistant?.errorMessage ? TEXTLESS_INCOMPLETE_WARNING : TEXTLESS_LENGTH_WARNING);
1094
1096
  }
1095
1097
  this.sendCompletionNotification();
1096
1098
  this.ctx.promptSuggestion?.onAgentEnd();
@@ -1372,15 +1374,20 @@ export class EventController {
1372
1374
 
1373
1375
  const sessionName = this.ctx.sessionManager.getSessionName();
1374
1376
  const textlessLengthStop = isTextlessLengthStop(last);
1377
+ const incompleteTitle = last?.errorMessage ? "Response incomplete" : "Output limit reached";
1375
1378
  const title = textlessLengthStop
1376
1379
  ? sessionName
1377
- ? `${sessionName}: Output limit reached`
1378
- : "Output limit reached"
1380
+ ? `${sessionName}: ${incompleteTitle}`
1381
+ : incompleteTitle
1379
1382
  : sessionName
1380
1383
  ? `${sessionName}: Complete`
1381
1384
  : "Complete";
1382
1385
  const summary = summaryFromMessage(last, 1000);
1383
- const body = textlessLengthStop ? TEXTLESS_LENGTH_WARNING : (summary ?? "Complete");
1386
+ const body = textlessLengthStop
1387
+ ? last?.errorMessage
1388
+ ? TEXTLESS_INCOMPLETE_WARNING
1389
+ : TEXTLESS_LENGTH_WARNING
1390
+ : (summary ?? "Complete");
1384
1391
  const sessionManager = this.ctx.sessionManager as { getCwd?: () => string; getSessionId?: () => string };
1385
1392
  const payload: CompletionNotifyPayload = {
1386
1393
  type: "agent-turn-complete",
@@ -40,7 +40,9 @@ import { CommandPalette, type CommandPaletteAction, type CommandPaletteEntry } f
40
40
  import type { PasteTextContext } from "../components/custom-editor";
41
41
  import { QueuePaneComponent } from "../components/queue-pane";
42
42
  import { type QueuedMessageMoveDirection, QueuedMessageSelectorComponent } from "../components/queued-message-selector";
43
+ import { focusedUiOwnsInterrupt } from "../utils/interrupt-ownership";
43
44
  import { matchesAppInterrupt } from "../utils/keybinding-matchers";
45
+ import { createUnforwardedOptionChordWarner } from "../utils/macos-option-forwarding";
44
46
 
45
47
  const QUEUE_SELECTOR_NAVIGATION_ACTIONS = [
46
48
  "tui.select.up",
@@ -95,6 +97,8 @@ function isExpandable(obj: unknown): obj is Expandable {
95
97
  export class InputController {
96
98
  readonly actionRegistry: ActionRegistry<void>;
97
99
  readonly #loadPastedImageBatch: typeof loadPastedImageBatch;
100
+ /** Session id with an automatic-title request pending, if any. */
101
+ #autoTitleInFlightSessionId: string | undefined;
98
102
  #deferredSubmission?: {
99
103
  text: string;
100
104
  images?: InteractiveModeContext["pendingImages"];
@@ -546,15 +550,23 @@ export class InputController {
546
550
  if (!isInterruptKey && !isClearKey) {
547
551
  return undefined;
548
552
  }
549
- if (isClearKey && !isInterruptKey && this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
553
+ // Listeners run before focused handleInput. Yield interrupt/back to the
554
+ // focused UI before touching BTW, maintenance, retries or workflow state.
555
+ // Clear (Ctrl+C by default) remains a global abort, even if the same
556
+ // key is also configured as interrupt.
557
+ if (isInterruptKey && !isClearKey && focusedUiOwnsInterrupt(this.ctx)) {
558
+ this.#resetEscapeGestures();
559
+ return undefined;
560
+ }
561
+ if (isClearKey && this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
550
562
  this.#resetEscapeGestures();
551
563
  return { consume: true };
552
564
  }
553
- if (isClearKey && !isInterruptKey && this.ctx.hookSelector?.hasActiveInlineInput?.() === true) {
565
+ if (isClearKey && this.ctx.hookSelector?.hasActiveInlineInput?.() === true) {
554
566
  this.#resetEscapeGestures();
555
567
  return undefined;
556
568
  }
557
- if (isClearKey && !isInterruptKey) {
569
+ if (isClearKey) {
558
570
  if (this.#handlePendingSteerInterrupt()) return { consume: true };
559
571
  if (
560
572
  this.#handleCancellableWorkEscape({
@@ -807,6 +819,12 @@ export class InputController {
807
819
  this.ctx.editor.setActionKeys("app.message.queue", this.ctx.keybindings.getKeys("app.message.queue"));
808
820
  this.ctx.editor.onQueue = () => this.#executeAction("app.message.queue");
809
821
  this.#registerCommandPaletteAction("app.message.queue", queue, true);
822
+ this.ctx.editor.onUnforwardedOptionChord ??= createUnforwardedOptionChordWarner({
823
+ platform: process.platform,
824
+ terminalProgram: Bun.env.TERM_PROGRAM,
825
+ formatKeyHint: key => this.ctx.keybindings.formatKeyHint(key),
826
+ showWarning: message => this.ctx.showWarning(message),
827
+ });
810
828
 
811
829
  this.ctx.editor.onViewportPageScroll = direction => this.ctx.ui.scrollViewportPages(direction);
812
830
  this.ctx.editor.onViewportFollowLive = () => {
@@ -1163,6 +1181,7 @@ export class InputController {
1163
1181
  return;
1164
1182
  }
1165
1183
  this.ctx.queueCompactionMessage(text, "steer", composer);
1184
+ this.#titleQueuedCompactionText(text);
1166
1185
  return;
1167
1186
  }
1168
1187
 
@@ -1172,6 +1191,9 @@ export class InputController {
1172
1191
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
1173
1192
  if (this.ctx.session.isStreaming) {
1174
1193
  invalidateSessionTitleGeneration(this.ctx.sessionManager);
1194
+ // A turn started by a skill command has no user message yet, so the
1195
+ // first typed message can arrive here instead of the idle path.
1196
+ this.maybeGenerateSessionTitle(text);
1175
1197
  if (this.#canModifyComposer(composer)) {
1176
1198
  this.ctx.editor.addToHistory(text);
1177
1199
  this.ctx.editor.setText("");
@@ -1200,32 +1222,7 @@ export class InputController {
1200
1222
  // First, move any pending bash components to chat
1201
1223
  this.ctx.flushPendingBashComponents();
1202
1224
 
1203
- // Generate session title on first message
1204
- const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
1205
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$pickenv("GJC_NO_TITLE", "PI_NO_TITLE")) {
1206
- const registry = this.ctx.session.modelRegistry;
1207
- generateSessionTitle(
1208
- text,
1209
- registry,
1210
- this.ctx.settings,
1211
- this.ctx.session.credentialSessionId,
1212
- this.ctx.session.model,
1213
- provider => this.ctx.session.agent.metadataForProvider(provider),
1214
- )
1215
- .then(async title => {
1216
- if (title) {
1217
- const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
1218
- if (applied) {
1219
- setSessionTerminalTitle(
1220
- this.ctx.sessionManager.getSessionName()!,
1221
- this.ctx.sessionManager.getCwd(),
1222
- );
1223
- this.ctx.updateEditorBorderColor();
1224
- }
1225
- }
1226
- })
1227
- .catch(() => {});
1228
- }
1225
+ this.maybeGenerateSessionTitle(text);
1229
1226
 
1230
1227
  if (this.ctx.onInputCallback) {
1231
1228
  // Include any pending images from clipboard paste
@@ -1628,6 +1625,51 @@ export class InputController {
1628
1625
  return this.ctx.settings.get("busyPromptMode") === "steer" ? "steer" : "followUp";
1629
1626
  }
1630
1627
 
1628
+ /**
1629
+ * Generate the automatic session title from the first user message. Called
1630
+ * from the editor submit and queue-shortcut paths (idle and streaming) and
1631
+ * from interactive startup messages; it is a no-op once the session has a
1632
+ * user message, a name, or a title request already in flight. The result is
1633
+ * bound to the session that requested it: a completion that lands after the
1634
+ * session was replaced (New Session, switch, resume) is discarded.
1635
+ */
1636
+ maybeGenerateSessionTitle(text: string): void {
1637
+ if (this.ctx.sessionManager.getSessionName()) return;
1638
+ const sessionId = this.ctx.sessionManager.getSessionId();
1639
+ if (this.#autoTitleInFlightSessionId === sessionId) return;
1640
+ if ($pickenv("GJC_NO_TITLE", "PI_NO_TITLE")) return;
1641
+ if (this.ctx.session.messages.some((m: AgentMessage) => m.role === "user")) return;
1642
+ // Keep locally intercepted command arguments away from the title model.
1643
+ if (this.ctx.session.isLocallyHandledSlashCommand(text)) return;
1644
+ this.#autoTitleInFlightSessionId = sessionId;
1645
+ generateSessionTitle(
1646
+ text,
1647
+ this.ctx.session.modelRegistry,
1648
+ this.ctx.settings,
1649
+ this.ctx.session.credentialSessionId,
1650
+ this.ctx.session.model,
1651
+ provider => this.ctx.session.agent.metadataForProvider(provider),
1652
+ )
1653
+ .then(async title => {
1654
+ if (!title || this.ctx.sessionManager.getSessionId() !== sessionId) return;
1655
+ const applied = await this.ctx.sessionManager.setSessionName(title, "auto");
1656
+ if (applied) {
1657
+ setSessionTerminalTitle(this.ctx.sessionManager.getSessionName()!, this.ctx.sessionManager.getCwd());
1658
+ this.ctx.updateEditorBorderColor();
1659
+ }
1660
+ })
1661
+ .catch(() => {})
1662
+ .finally(() => {
1663
+ if (this.#autoTitleInFlightSessionId === sessionId) this.#autoTitleInFlightSessionId = undefined;
1664
+ });
1665
+ }
1666
+
1667
+ /** Title text accepted into the compaction queue; skill commands replay through the skill path and are not titled. */
1668
+ #titleQueuedCompactionText(text: string): void {
1669
+ if (parseSkillInvocations(text, this.ctx.skillCommands ?? new Map()).length > 0) return;
1670
+ this.maybeGenerateSessionTitle(text);
1671
+ }
1672
+
1631
1673
  /**
1632
1674
  * Dispatch skill slash invocation(s) (`/skill:<name>`) through custom messages
1633
1675
  * using the supplied `streamingBehavior`. Returns true if the text contains a
@@ -1736,6 +1778,7 @@ export class InputController {
1736
1778
  return;
1737
1779
  }
1738
1780
  this.ctx.queueCompactionMessage(text, "followUp");
1781
+ this.#titleQueuedCompactionText(text);
1739
1782
  return;
1740
1783
  }
1741
1784
 
@@ -1750,6 +1793,7 @@ export class InputController {
1750
1793
  followUpQueuePolicy: "sequential",
1751
1794
  }),
1752
1795
  );
1796
+ this.maybeGenerateSessionTitle(text);
1753
1797
  this.ctx.updatePendingMessagesDisplay();
1754
1798
  this.ctx.ui.requestRender();
1755
1799
  return;
@@ -1761,6 +1805,7 @@ export class InputController {
1761
1805
  return;
1762
1806
  }
1763
1807
 
1808
+ this.maybeGenerateSessionTitle(text);
1764
1809
  if (this.ctx.session.isStreaming) {
1765
1810
  this.ctx.editor.addToHistory(text);
1766
1811
  this.ctx.editor.setText("");
@@ -44,7 +44,8 @@ import { parseCommandArgs } from "../shared";
44
44
  import { buildOAuthLoginAnchor, createOAuthUrlCopyLease } from "../shared/oauth-url-copy";
45
45
  import { theme } from "../theme/theme";
46
46
  import type { InteractiveModeContext } from "../types";
47
- import { matchesAppInterrupt } from "../utils/keybinding-matchers";
47
+ import { focusedUiOwnsInterrupt } from "../utils/interrupt-ownership";
48
+ import { matchesAppClear, matchesAppInterrupt } from "../utils/keybinding-matchers";
48
49
  import { groupBySource, parseRemoveArgs, readScopeFlag, showCommandMessage } from "./command-controller-shared";
49
50
 
50
51
  function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
@@ -645,7 +646,9 @@ export class MCPCommandController {
645
646
  const cancellationUnsubscribe =
646
647
  signal === undefined && typeof this.ctx.ui.addInputListener === "function"
647
648
  ? this.ctx.ui.addInputListener(data => {
648
- if (data !== "\x03" && !matchesAppInterrupt(data)) return;
649
+ const isClearKey = data === "\x03" || matchesAppClear(data);
650
+ if (!isClearKey && !matchesAppInterrupt(data)) return;
651
+ if (!isClearKey && focusedUiOwnsInterrupt(this.ctx)) return;
649
652
  userController.abort(new Error("OAuth flow cancelled"));
650
653
  return { consume: true };
651
654
  })
@@ -1839,7 +1842,9 @@ export class MCPCommandController {
1839
1842
  const oauthUrlCopyLease = createOAuthUrlCopyLease(this.ctx);
1840
1843
  const abortController = new AbortController();
1841
1844
  const interruptUnsubscribe = this.ctx.ui.addInputListener?.(data => {
1842
- if (data !== "\x03" && !matchesAppInterrupt(data)) return;
1845
+ const isClearKey = data === "\x03" || matchesAppClear(data);
1846
+ if (!isClearKey && !matchesAppInterrupt(data)) return;
1847
+ if (!isClearKey && focusedUiOwnsInterrupt(this.ctx)) return;
1843
1848
  abortController.abort(new Error("Smithery authorization cancelled."));
1844
1849
  return { consume: true };
1845
1850
  });
@@ -1612,7 +1612,7 @@ export class SelectorController {
1612
1612
  }
1613
1613
 
1614
1614
  async #handleCredentialImport(): Promise<void> {
1615
- this.ctx.showStatus("Scanning for existing Claude Code / Codex CLI credentials…");
1615
+ this.ctx.showStatus("Scanning for existing Claude Code / Codex CLI / Kiro CLI credentials…");
1616
1616
  const preview = await runExternalCredentialAutoImport({
1617
1617
  authStorage: {
1618
1618
  importCredentialIfAbsent: async () => ({
@@ -2494,17 +2494,20 @@ export class SelectorController {
2494
2494
  * default) then refresh the status surfaces. Rethrows so callers surface errors.
2495
2495
  */
2496
2496
  async #applyModelProfile(profileName: string, persistDefault: boolean): Promise<void> {
2497
+ this.ctx.session.markUserModelSelection();
2497
2498
  const profileLabel = formatModelProfileDisplayLabel(
2498
2499
  this.ctx.session.modelRegistry.getModelProfile(profileName) ?? { name: profileName },
2499
2500
  );
2500
- await activateModelProfile(
2501
- {
2502
- session: this.ctx.session,
2503
- modelRegistry: this.ctx.session.modelRegistry,
2504
- settings: this.ctx.settings,
2505
- profileName,
2506
- },
2507
- { persistDefault },
2501
+ await this.ctx.session.withSdkControlMutation(() =>
2502
+ activateModelProfile(
2503
+ {
2504
+ session: this.ctx.session,
2505
+ modelRegistry: this.ctx.session.modelRegistry,
2506
+ settings: this.ctx.settings,
2507
+ profileName,
2508
+ },
2509
+ { persistDefault },
2510
+ ),
2508
2511
  );
2509
2512
  this.ctx.statusLine.invalidate();
2510
2513
  this.ctx.updateEditorBorderColor();
@@ -2774,8 +2777,15 @@ export class SelectorController {
2774
2777
  return;
2775
2778
  }
2776
2779
  const { model, role, thinkingLevel, selector: selectedSelector } = selection;
2780
+ if (
2781
+ (role !== null && role !== "default") ||
2782
+ selection.roles?.some(targetRole => targetRole !== "default")
2783
+ ) {
2784
+ this.ctx.session.markUserModelSelection();
2785
+ }
2777
2786
  if (role === null) {
2778
2787
  // Temporary: update agent state but don't persist to settings
2788
+ this.ctx.session.markUserModelSelection();
2779
2789
  await this.ctx.session.setModelTemporary(model, thinkingLevel, {
2780
2790
  cause: "temporary-operation",
2781
2791
  reason: "other",
@@ -3677,12 +3687,15 @@ export class SelectorController {
3677
3687
  return;
3678
3688
  }
3679
3689
 
3680
- await activateModelProfile({
3681
- session: this.ctx.session,
3682
- modelRegistry: this.ctx.session.modelRegistry,
3683
- settings: this.ctx.settings,
3684
- profileName: recommendedProfile.name,
3685
- });
3690
+ this.ctx.session.markUserModelSelection();
3691
+ await this.ctx.session.withSdkControlMutation(() =>
3692
+ activateModelProfile({
3693
+ session: this.ctx.session,
3694
+ modelRegistry: this.ctx.session.modelRegistry,
3695
+ settings: this.ctx.settings,
3696
+ profileName: recommendedProfile.name,
3697
+ }),
3698
+ );
3686
3699
  }
3687
3700
 
3688
3701
  async #handleOAuthLogin(providerId: string, options?: OAuthSelectorOptions): Promise<void> {
@@ -2642,6 +2642,10 @@ export class InteractiveMode implements InteractiveModeContext {
2642
2642
  this.#inputController.handleBackgroundCommand();
2643
2643
  }
2644
2644
 
2645
+ maybeGenerateSessionTitle(text: string): void {
2646
+ this.#inputController.maybeGenerateSessionTitle(text);
2647
+ }
2648
+
2645
2649
  handleImagePaste(): Promise<boolean> {
2646
2650
  return this.#inputController.handleImagePaste();
2647
2651
  }
@@ -11,6 +11,8 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
11
11
  import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
12
12
  import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
13
13
  import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
14
+ import { buildSessionProjectProgress } from "../progress/collect-project-progress";
15
+ import { toProjectProgressSnapshot } from "../progress/progress-snapshot";
14
16
  import {
15
17
  parseSyntheticModelId,
16
18
  resolveSyntheticModelSelection,
@@ -201,6 +203,8 @@ export async function initializeExtensions(session: AgentSession, options: Initi
201
203
  return { bytes: new Uint8Array(await file.slice(start, end).arrayBuffer()), totalBytes: file.size };
202
204
  },
203
205
  getJobs: () => session.getAsyncJobSnapshot(),
206
+ getProjectProgress: async () =>
207
+ toProjectProgressSnapshot(await buildSessionProjectProgress(session, session.sessionManager)),
204
208
  onJobFold: listener => session.onJobFold(listener),
205
209
  onSessionEvent: listener => session.subscribe(listener),
206
210
  setSdkPermissionProvider: provider => session.setSdkPermissionProvider(provider),
@@ -1,3 +1,5 @@
1
+ import { getStreamIdleTimeoutRemediationMs } from "@gajae-code/ai/utils/idle-iterator";
2
+
1
3
  const STREAM_IDLE_TIMEOUT_PATTERN = /\bstream stalled while waiting for the next event\b/i;
2
4
  const GENERIC_ABORT_PATTERN = /^Request was aborted\.?$/i;
3
5
  const ABORT_DISPLAY_LABEL_PATTERN = /^(?:Operation aborted|Aborted after \d+ retry attempts?)(?::|$)/;
@@ -37,5 +39,5 @@ function normalizeAbortCause(errorMessage: string | undefined): string {
37
39
  function streamIdleTimeoutHint(cause: string): string {
38
40
  if (!STREAM_IDLE_TIMEOUT_PATTERN.test(cause)) return "";
39
41
  const separator = /[.!?]$/.test(cause) ? " " : ". ";
40
- return `${separator}Hint: set PI_STREAM_IDLE_TIMEOUT_MS=300000 for slow reasoning/proxy streams, or PI_STREAM_IDLE_TIMEOUT_MS=0 to disable the watchdog.`;
42
+ return `${separator}Hint: set PI_STREAM_IDLE_TIMEOUT_MS=${getStreamIdleTimeoutRemediationMs()} for slow reasoning/proxy streams, or PI_STREAM_IDLE_TIMEOUT_MS=0 to disable the watchdog.`;
41
43
  }
@@ -0,0 +1,12 @@
1
+ import type { InteractiveModeContext } from "../types";
2
+
3
+ /** Ordinary focused UI owns interrupt/back before background work does. */
4
+ export function focusedUiOwnsInterrupt(
5
+ ctx: Pick<InteractiveModeContext, "ui" | "editor" | "hookSelector" | "hookInput" | "hookEditor">,
6
+ ): boolean {
7
+ const focused = ctx.ui.getFocusedComponent?.();
8
+ if (!focused?.handleInput || focused === ctx.editor) return false;
9
+ // Non-inline hook prompts deliberately retain workflow-level interruption.
10
+ if (focused === ctx.hookSelector) return ctx.hookSelector.hasActiveInlineInput();
11
+ return focused !== ctx.hookInput && focused !== ctx.hookEditor;
12
+ }
@@ -16,6 +16,10 @@ export function matchesAppInterrupt(data: string): boolean {
16
16
  return matchesKey(data, "escape") || matchesKey(data, "esc");
17
17
  }
18
18
 
19
+ export function matchesAppClear(data: string): boolean {
20
+ return getKeybindings().matches(data, "app.clear");
21
+ }
22
+
19
23
  export function matchesSelectCancel(data: string): boolean {
20
24
  return getKeybindings().matches(data, "tui.select.cancel");
21
25
  }
@@ -0,0 +1,80 @@
1
+ import type { KeyId } from "@gajae-code/tui";
2
+
3
+ /**
4
+ * Characters macOS composes for Option+<letter> on the US/ABC layout when the
5
+ * terminal sends Option as text instead of Alt/Meta. Dead-key letters (e, i, n,
6
+ * u) compose nothing on their own and are absent.
7
+ */
8
+ const MACOS_OPTION_COMPOSED_KEYS: ReadonlyMap<string, KeyId> = new Map<string, KeyId>([
9
+ ["å", "alt+a"],
10
+ ["∫", "alt+b"],
11
+ ["ç", "alt+c"],
12
+ ["∂", "alt+d"],
13
+ ["ƒ", "alt+f"],
14
+ ["©", "alt+g"],
15
+ ["˙", "alt+h"],
16
+ ["∆", "alt+j"],
17
+ ["˚", "alt+k"],
18
+ ["¬", "alt+l"],
19
+ ["µ", "alt+m"],
20
+ ["ø", "alt+o"],
21
+ ["π", "alt+p"],
22
+ ["œ", "alt+q"],
23
+ ["®", "alt+r"],
24
+ ["ß", "alt+s"],
25
+ ["†", "alt+t"],
26
+ ["√", "alt+v"],
27
+ ["∑", "alt+w"],
28
+ ["≈", "alt+x"],
29
+ ["¥", "alt+y"],
30
+ ["Ω", "alt+z"],
31
+ ]);
32
+
33
+ /** The Option chord that composes `text` on the macOS US/ABC layout, if any. */
34
+ export function macosOptionChordForComposedText(text: string): KeyId | undefined {
35
+ return MACOS_OPTION_COMPOSED_KEYS.get(text);
36
+ }
37
+
38
+ /** Terminal-specific instruction for sending Option as Alt/Meta, when the terminal is known. */
39
+ export function macosOptionForwardingInstruction(terminalProgram: string | undefined): string | undefined {
40
+ const program = terminalProgram?.toLowerCase() ?? "";
41
+ if (program.includes("ghostty")) {
42
+ return "Ghostty: set macos-option-as-alt = true in its config, then reload the config or restart Ghostty.";
43
+ }
44
+ if (program === "apple_terminal") {
45
+ return "Terminal.app: enable Settings > Profiles > Keyboard > Use Option as Meta key.";
46
+ }
47
+ if (program === "iterm.app") {
48
+ return "iTerm2: set Settings > Profiles > Keys > Left and Right Option key to Esc+.";
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ export interface UnforwardedOptionChordWarnerOptions {
54
+ platform: NodeJS.Platform;
55
+ terminalProgram: string | undefined;
56
+ formatKeyHint: (key: KeyId) => string;
57
+ showWarning: (message: string) => void;
58
+ }
59
+
60
+ /**
61
+ * Builds the composer callback that explains, once, why a bound Option shortcut
62
+ * produced a character instead of firing. Returns undefined off macOS, where the
63
+ * composition table does not apply.
64
+ */
65
+ export function createUnforwardedOptionChordWarner(
66
+ options: UnforwardedOptionChordWarnerOptions,
67
+ ): ((key: KeyId, text: string) => void) | undefined {
68
+ if (options.platform !== "darwin") return undefined;
69
+ let warned = false;
70
+ return (key, text) => {
71
+ if (warned) return;
72
+ warned = true;
73
+ const instruction =
74
+ macosOptionForwardingInstruction(options.terminalProgram) ??
75
+ "Configure the terminal to send Option as Alt/Meta (Esc+).";
76
+ options.showWarning(
77
+ `${options.formatKeyHint(key)} arrived as "${text}": the terminal is sending Option as text, so Option shortcuts cannot reach GJC. ${instruction} Or remap the shortcut to a Control chord in keybindings.json.`,
78
+ );
79
+ };
80
+ }