@gajae-code/coding-agent 0.7.8 → 0.7.10

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 (92) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +8 -0
  3. package/dist/types/config/keybindings.d.ts +1 -1
  4. package/dist/types/config/model-profile-activation.d.ts +19 -2
  5. package/dist/types/config/model-profiles.d.ts +1 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/settings-schema.d.ts +12 -3
  8. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  9. package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
  10. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  12. package/dist/types/modes/components/hook-input.d.ts +1 -0
  13. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  14. package/dist/types/modes/components/model-selector.d.ts +9 -0
  15. package/dist/types/modes/components/status-line.d.ts +3 -0
  16. package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +3 -1
  18. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  19. package/dist/types/modes/types.d.ts +3 -1
  20. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  21. package/dist/types/notifications/html-format.d.ts +2 -0
  22. package/dist/types/notifications/recent-activity.d.ts +5 -1
  23. package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
  24. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  25. package/dist/types/notifications/telegram-daemon.d.ts +1 -10
  26. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  27. package/dist/types/notifications/threaded-render.d.ts +1 -0
  28. package/dist/types/tools/ask.d.ts +26 -0
  29. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  30. package/dist/types/utils/cmux-workspace.d.ts +23 -0
  31. package/dist/types/web/search/index.d.ts +1 -1
  32. package/dist/types/web/search/provider.d.ts +5 -0
  33. package/dist/types/web/search/render.d.ts +1 -0
  34. package/package.json +7 -7
  35. package/src/cli/notify-cli.ts +1 -1
  36. package/src/cli/web-search-cli.ts +3 -2
  37. package/src/cli.ts +1 -0
  38. package/src/config/keybindings.ts +1 -1
  39. package/src/config/model-profile-activation.ts +92 -11
  40. package/src/config/model-profiles.ts +15 -2
  41. package/src/config/model-registry.ts +81 -5
  42. package/src/config/settings-schema.ts +14 -3
  43. package/src/coordinator-mcp/server.ts +4 -2
  44. package/src/deep-interview/render-middleware.ts +1 -0
  45. package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
  46. package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
  47. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  48. package/src/extensibility/extensions/types.ts +11 -0
  49. package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
  50. package/src/gjc-runtime/launch-tmux.ts +366 -48
  51. package/src/gjc-runtime/team-runtime.ts +69 -4
  52. package/src/gjc-runtime/ultragoal-guard.ts +150 -24
  53. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  54. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  55. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  56. package/src/hooks/skill-state.ts +33 -73
  57. package/src/internal-urls/docs-index.generated.ts +8 -7
  58. package/src/modes/components/custom-model-preset-wizard.ts +2 -4
  59. package/src/modes/components/hook-editor.ts +7 -4
  60. package/src/modes/components/hook-input.ts +4 -0
  61. package/src/modes/components/hook-selector.ts +21 -3
  62. package/src/modes/components/model-selector.ts +84 -13
  63. package/src/modes/components/settings-selector.ts +83 -24
  64. package/src/modes/components/status-line/presets.ts +11 -0
  65. package/src/modes/components/status-line/segments.ts +10 -2
  66. package/src/modes/components/status-line.ts +1 -0
  67. package/src/modes/controllers/extension-ui-controller.ts +20 -2
  68. package/src/modes/controllers/selector-controller.ts +130 -7
  69. package/src/modes/interactive-mode.ts +7 -2
  70. package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
  71. package/src/modes/types.ts +6 -1
  72. package/src/notifications/daemon-paths.ts +22 -0
  73. package/src/notifications/html-format.ts +75 -1
  74. package/src/notifications/index.ts +12 -1
  75. package/src/notifications/recent-activity.ts +32 -8
  76. package/src/notifications/telegram-daemon-cli.ts +13 -4
  77. package/src/notifications/telegram-daemon-control.ts +9 -0
  78. package/src/notifications/telegram-daemon.ts +69 -45
  79. package/src/notifications/telegram-reference.ts +36 -14
  80. package/src/notifications/threaded-render.ts +14 -6
  81. package/src/prompts/system/system-prompt.md +10 -1
  82. package/src/sdk.ts +13 -6
  83. package/src/session/agent-session.ts +10 -1
  84. package/src/tools/ask.ts +147 -17
  85. package/src/tools/eval.ts +2 -2
  86. package/src/tools/render-utils.ts +2 -0
  87. package/src/tools/ultragoal-ask-guard.ts +39 -5
  88. package/src/utils/cmux-workspace.ts +106 -0
  89. package/src/utils/title-generator.ts +2 -0
  90. package/src/web/search/index.ts +22 -10
  91. package/src/web/search/provider.ts +15 -0
  92. package/src/web/search/render.ts +3 -0
@@ -36,6 +36,18 @@ export declare const askSchema: z.ZodObject<{
36
36
  dimension: z.ZodString;
37
37
  ambiguity: z.ZodNumber;
38
38
  }, z.core.$strip>>;
39
+ workflowGate: z.ZodOptional<z.ZodObject<{
40
+ stage: z.ZodEnum<{
41
+ "deep-interview": "deep-interview";
42
+ ralplan: "ralplan";
43
+ ultragoal: "ultragoal";
44
+ }>;
45
+ kind: z.ZodEnum<{
46
+ approval: "approval";
47
+ execution: "execution";
48
+ question: "question";
49
+ }>;
50
+ }, z.core.$strip>>;
39
51
  }, z.core.$strip>>;
40
52
  }, z.core.$strip>;
41
53
  export type AskToolInput = z.infer<typeof askSchema>;
@@ -47,6 +59,7 @@ export interface QuestionResult {
47
59
  multi: boolean;
48
60
  selectedOptions: string[];
49
61
  customInput?: string;
62
+ clarificationQuestion?: string;
50
63
  }
51
64
  export interface AskToolDetails {
52
65
  question?: string;
@@ -54,6 +67,7 @@ export interface AskToolDetails {
54
67
  multi?: boolean;
55
68
  selectedOptions?: string[];
56
69
  customInput?: string;
70
+ clarificationQuestion?: string;
57
71
  /** Multi-part question mode */
58
72
  results?: QuestionResult[];
59
73
  }
@@ -87,6 +101,18 @@ export declare class AskTool implements AgentTool<typeof askSchema, AskToolDetai
87
101
  dimension: z.ZodString;
88
102
  ambiguity: z.ZodNumber;
89
103
  }, z.core.$strip>>;
104
+ workflowGate: z.ZodOptional<z.ZodObject<{
105
+ stage: z.ZodEnum<{
106
+ "deep-interview": "deep-interview";
107
+ ralplan: "ralplan";
108
+ ultragoal: "ultragoal";
109
+ }>;
110
+ kind: z.ZodEnum<{
111
+ approval: "approval";
112
+ execution: "execution";
113
+ question: "question";
114
+ }>;
115
+ }, z.core.$strip>>;
90
116
  }, z.core.$strip>>;
91
117
  }, z.core.$strip>;
92
118
  readonly strict = true;
@@ -1,5 +1,12 @@
1
1
  import type { AgentTool } from "@gajae-code/agent-core";
2
2
  import { type UltragoalAskBlockDiagnostic } from "../gjc-runtime/ultragoal-guard";
3
+ export interface UltragoalAskGuardContext {
4
+ activeSkillState?: {
5
+ skill?: string;
6
+ session_id?: string;
7
+ } | null;
8
+ sessionId?: string | null;
9
+ }
3
10
  export declare function formatUltragoalAskBlockMessage(diagnostic: UltragoalAskBlockDiagnostic): string;
4
- export declare function assertUltragoalAskAllowed(cwd: string): Promise<void>;
5
- export declare function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string): T;
11
+ export declare function assertUltragoalAskAllowed(cwd: string, context?: UltragoalAskGuardContext): Promise<void>;
12
+ export declare function guardToolForUltragoalAsk<T extends AgentTool>(tool: T, getCwd: () => string, getContext?: () => UltragoalAskGuardContext): T;
@@ -0,0 +1,23 @@
1
+ export interface CmuxWorkspaceRenameCommand {
2
+ command: string;
3
+ args: string[];
4
+ }
5
+ export interface CmuxWorkspaceRenameProcess {
6
+ exited: Promise<number>;
7
+ kill(): void;
8
+ unref(): void;
9
+ }
10
+ export interface CmuxWorkspaceTitleSyncOptions {
11
+ env?: NodeJS.ProcessEnv;
12
+ isTty?: boolean;
13
+ which?: (command: string) => string | null;
14
+ spawn?: (command: string[], options: {
15
+ env: NodeJS.ProcessEnv;
16
+ stdin: "ignore";
17
+ stdout: "ignore";
18
+ stderr: "ignore";
19
+ }) => CmuxWorkspaceRenameProcess;
20
+ }
21
+ export declare function sanitizeCmuxWorkspaceTitle(title: string | undefined): string | undefined;
22
+ export declare function buildCmuxWorkspaceRenameCommand(sessionName: string | undefined, env?: NodeJS.ProcessEnv): CmuxWorkspaceRenameCommand | null;
23
+ export declare function syncCmuxWorkspaceTitle(sessionName: string | undefined, options?: CmuxWorkspaceTitleSyncOptions): void;
@@ -110,7 +110,7 @@ export declare class WebSearchTool implements AgentTool<typeof webSearchSchema,
110
110
  /** Web search tool as CustomTool (for TUI rendering support) */
111
111
  export declare const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails>;
112
112
  export declare function getSearchTools(): CustomTool<any, any>[];
113
- export { getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders } from "./provider";
113
+ export { getConfiguredSearchProviderPreference, getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders, } from "./provider";
114
114
  export { setSearchHardTimeoutMs } from "./providers/utils";
115
115
  export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
116
116
  export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
@@ -8,6 +8,11 @@ export declare function getSearchProvider(id: SearchProviderId): Promise<SearchP
8
8
  export declare const SEARCH_PROVIDER_ORDER: SearchProviderId[];
9
9
  export declare function setPreferredSearchProvider(provider: SearchProviderId | "auto"): void;
10
10
  export declare function setSearchFallbackProviders(ids: readonly string[]): void;
11
+ export interface SearchProviderPreferenceSource {
12
+ get(path: "providers.webSearch" | "web_search.provider"): SearchProviderId | "auto";
13
+ has(path: "providers.webSearch" | "web_search.provider"): boolean;
14
+ }
15
+ export declare function getConfiguredSearchProviderPreference(settings: SearchProviderPreferenceSource): SearchProviderId | "auto";
11
16
  export interface ResolveProviderChainOptions {
12
17
  authStorage: AuthStorage;
13
18
  sessionId?: string;
@@ -10,6 +10,7 @@ import type { SearchResponse } from "./types";
10
10
  export interface SearchRenderDetails {
11
11
  response: SearchResponse;
12
12
  error?: string;
13
+ warning?: string;
13
14
  }
14
15
  /** Render web search result with tree-based layout */
15
16
  export declare function renderSearchResult(result: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.7.8",
4
+ "version": "0.7.10",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -52,12 +52,12 @@
52
52
  "@agentclientprotocol/sdk": "0.21.0",
53
53
  "@babel/parser": "^7.29.3",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@gajae-code/stats": "0.7.8",
56
- "@gajae-code/agent-core": "0.7.8",
57
- "@gajae-code/ai": "0.7.8",
58
- "@gajae-code/natives": "0.7.8",
59
- "@gajae-code/tui": "0.7.8",
60
- "@gajae-code/utils": "0.7.8",
55
+ "@gajae-code/stats": "0.7.10",
56
+ "@gajae-code/agent-core": "0.7.10",
57
+ "@gajae-code/ai": "0.7.10",
58
+ "@gajae-code/natives": "0.7.10",
59
+ "@gajae-code/tui": "0.7.10",
60
+ "@gajae-code/utils": "0.7.10",
61
61
  "@puppeteer/browsers": "^2.13.0",
62
62
  "@types/turndown": "5.0.6",
63
63
  "@xterm/headless": "^6.0.0",
@@ -4,7 +4,7 @@
4
4
  * Handles `gjc notify` setup/status and the hidden daemon entrypoint.
5
5
  */
6
6
  import { createInterface } from "node:readline/promises";
7
- import { APP_NAME } from "@gajae-code/utils";
7
+ import { APP_NAME } from "@gajae-code/utils/dirs";
8
8
  import chalk from "chalk";
9
9
  import type { Settings } from "../config/settings";
10
10
  import { getNotificationConfig, maskToken } from "../notifications/config";
@@ -9,6 +9,7 @@ import chalk from "chalk";
9
9
  import { Settings } from "../config/settings";
10
10
  import { initTheme, theme } from "../modes/theme/theme";
11
11
  import {
12
+ getConfiguredSearchProviderPreference,
12
13
  isConfigurableSearchProviderId,
13
14
  isSearchProviderPreference,
14
15
  runSearchQuery,
@@ -170,8 +171,8 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
170
171
 
171
172
  await initTheme();
172
173
  const settings = await Settings.init();
173
- const configuredProvider = settings.get("providers.webSearch");
174
- if (typeof configuredProvider === "string" && isSearchProviderPreference(configuredProvider)) {
174
+ const configuredProvider = getConfiguredSearchProviderPreference(settings);
175
+ if (isSearchProviderPreference(configuredProvider)) {
175
176
  setPreferredSearchProvider(configuredProvider);
176
177
  }
177
178
  const configuredFallback = settings.get("web_search.fallback");
package/src/cli.ts CHANGED
@@ -26,6 +26,7 @@ export const commands: CommandEntry[] = [
26
26
  { name: "codex-native-hook", load: () => import("./commands/codex-native-hook").then(m => m.default) },
27
27
  { name: "state", load: () => import("./commands/state").then(m => m.default) },
28
28
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
29
+ { name: "acp", load: () => import("./commands/acp").then(m => m.default) },
29
30
  { name: "skills", load: () => import("./commands/skills").then(m => m.default) },
30
31
  { name: "session", load: () => import("./commands/session").then(m => m.default) },
31
32
  { name: "harness", load: () => import("./commands/harness").then(m => m.default) },
@@ -118,7 +118,7 @@ export const KEYBINDINGS = {
118
118
  },
119
119
  "app.message.followUp": {
120
120
  defaultKeys: [],
121
- description: "Send follow-up message (no default; Ctrl+Enter inserts a newline)",
121
+ description: "Send follow-up message (no default; Ctrl+Enter submits)",
122
122
  },
123
123
  "app.message.queue": {
124
124
  defaultKeys: "alt+enter",
@@ -5,6 +5,7 @@ import { formatClampedModelSelector } from "../thinking";
5
5
  import {
6
6
  aggregateModelProfileRequiredProviders,
7
7
  formatAvailableProfileNames,
8
+ formatModelProfileDisplayLabel,
8
9
  resolveProfileBindings,
9
10
  } from "./model-profiles";
10
11
  import {
@@ -46,7 +47,7 @@ export interface PrepareModelProfileActivationOptions {
46
47
  export interface PreparedModelProfileActivation {
47
48
  profileName: string;
48
49
  session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
49
- settings: Pick<Settings, "get" | "override" | "set" | "flush">;
50
+ settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "override" | "set" | "flush">;
50
51
  previousModel: Model<Api> | undefined;
51
52
  previousThinkingLevel: ThinkingLevel | undefined;
52
53
  previousAgentModelOverrides: Record<string, string>;
@@ -108,8 +109,8 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
108
109
  return true;
109
110
  }
110
111
 
111
- export function formatModelProfileCredentialError(profileName: string, providers: readonly string[]): string {
112
- return `Model profile "${profileName}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
112
+ export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
113
+ return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
113
114
  }
114
115
 
115
116
  function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
@@ -183,6 +184,7 @@ export async function prepareModelProfileActivation(
183
184
  const available = formatAvailableProfileNames(profiles);
184
185
  throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
185
186
  }
187
+ const profileLabel = formatModelProfileDisplayLabel(profile);
186
188
 
187
189
  const allProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
188
190
  const alternativeGroups = profile.alternativeProviderGroups ?? [];
@@ -202,19 +204,19 @@ export async function prepareModelProfileActivation(
202
204
  // Check strict (non-alternative) providers — all must be authenticated.
203
205
  const strictMissing = missingProviders.filter(p => !alternativeSet.has(p));
204
206
  if (strictMissing.length > 0) {
205
- throw new Error(formatModelProfileCredentialError(options.profileName, strictMissing));
207
+ throw new Error(formatModelProfileCredentialError(profileLabel, strictMissing));
206
208
  }
207
209
 
208
210
  // Check alternative groups — at least one provider per group must be authenticated.
209
211
  for (const group of alternativeGroups) {
210
212
  const groupAuthenticated = group.some(p => authenticatedProviders.includes(p));
211
213
  if (!groupAuthenticated) {
212
- throw new Error(formatModelProfileCredentialError(options.profileName, [...group]));
214
+ throw new Error(formatModelProfileCredentialError(profileLabel, [...group]));
213
215
  }
214
216
  }
215
217
 
216
218
  if (authenticatedProviders.length === 0) {
217
- throw new Error(formatModelProfileCredentialError(options.profileName, missingProviders));
219
+ throw new Error(formatModelProfileCredentialError(profileLabel, missingProviders));
218
220
  }
219
221
 
220
222
  const availableModels = options.modelRegistry.getAll();
@@ -229,9 +231,7 @@ export async function prepareModelProfileActivation(
229
231
  })
230
232
  : undefined;
231
233
  if (bindings.defaultSelector && !resolvedDefault?.model) {
232
- throw new Error(
233
- `Model profile "${options.profileName}" default selector did not resolve: ${bindings.defaultSelector}`,
234
- );
234
+ throw new Error(`Model profile "${profileLabel}" default selector did not resolve: ${bindings.defaultSelector}`);
235
235
  }
236
236
 
237
237
  const modelRoles: Record<string, string> = {};
@@ -241,7 +241,7 @@ export async function prepareModelProfileActivation(
241
241
  modelRegistry: options.modelRegistry,
242
242
  });
243
243
  if (!resolved.model) {
244
- throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
244
+ throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selector}`);
245
245
  }
246
246
  modelRoles[role] = formatClampedModelSelector(selector, resolved.model);
247
247
  }
@@ -256,7 +256,7 @@ export async function prepareModelProfileActivation(
256
256
  modelRegistry: options.modelRegistry,
257
257
  });
258
258
  if (!resolved.model) {
259
- throw new Error(`Model profile "${options.profileName}" ${role} selector did not resolve: ${selector}`);
259
+ throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selector}`);
260
260
  }
261
261
  agentModelOverrides[role] = formatClampedModelSelector(selector, resolved.model);
262
262
  }
@@ -357,6 +357,87 @@ export async function applyPreparedModelProfileActivation(
357
357
  }
358
358
  }
359
359
 
360
+ export interface MaterializeModelProfileForDeletionResult {
361
+ modelRoles: Record<string, string>;
362
+ agentModelOverrides: Record<string, string>;
363
+ previousModelRoles: Record<string, string>;
364
+ previousAgentModelOverrides: Record<string, string>;
365
+ previousDefaultProfile: string | undefined;
366
+ previousPersistedDefaultProfile: string | undefined;
367
+ previousActiveModelProfile: string | undefined;
368
+ }
369
+
370
+ export async function materializeModelProfileForDeletion(
371
+ options: PrepareModelProfileActivationOptions & {
372
+ settings: Pick<Settings, "clearOverride" | "flush" | "get" | "getGlobal" | "override" | "set">;
373
+ },
374
+ ): Promise<MaterializeModelProfileForDeletionResult> {
375
+ const prepared = await prepareModelProfileActivation(options);
376
+ const previousDefaultProfile = prepared.settings.get("modelProfile.default");
377
+ const previousPersistedDefaultProfile = prepared.settings.getGlobal("modelProfile.default");
378
+ const nextModelRoles = {
379
+ ...prepared.previousModelRoles,
380
+ ...(prepared.defaultModel
381
+ ? {
382
+ default: formatModelSelectorValue(
383
+ `${prepared.defaultModel.provider}/${prepared.defaultModel.id}`,
384
+ prepared.defaultThinkingLevel,
385
+ ),
386
+ }
387
+ : {}),
388
+ ...prepared.modelRoles,
389
+ };
390
+ const nextAgentModelOverrides = {
391
+ ...prepared.previousAgentModelOverrides,
392
+ ...prepared.agentModelOverrides,
393
+ };
394
+
395
+ try {
396
+ prepared.settings.set("modelRoles", nextModelRoles);
397
+ prepared.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
398
+ prepared.settings.set("modelProfile.default", undefined);
399
+ prepared.settings.clearOverride("modelProfile.default");
400
+ prepared.settings.override("modelRoles", nextModelRoles);
401
+ prepared.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
402
+ prepared.session.setActiveModelProfile?.(undefined);
403
+ await prepared.settings.flush();
404
+ } catch (error) {
405
+ prepared.settings.set("modelRoles", prepared.previousModelRoles);
406
+ prepared.settings.set("task.agentModelOverrides", prepared.previousAgentModelOverrides);
407
+ prepared.settings.set("modelProfile.default", previousPersistedDefaultProfile);
408
+ prepared.settings.override("modelRoles", prepared.previousModelRoles);
409
+ prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverrides);
410
+ prepared.settings.override("modelProfile.default", previousDefaultProfile);
411
+ prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile);
412
+ throw error;
413
+ }
414
+
415
+ return {
416
+ modelRoles: nextModelRoles,
417
+ agentModelOverrides: nextAgentModelOverrides,
418
+ previousModelRoles: prepared.previousModelRoles,
419
+ previousAgentModelOverrides: prepared.previousAgentModelOverrides,
420
+ previousDefaultProfile,
421
+ previousPersistedDefaultProfile,
422
+ previousActiveModelProfile: prepared.previousActiveModelProfile,
423
+ };
424
+ }
425
+
426
+ export async function restoreMaterializedModelProfileForDeletion(options: {
427
+ settings: Pick<Settings, "flush" | "override" | "set">;
428
+ session: Pick<ModelProfileActivationSession, "setActiveModelProfile">;
429
+ snapshot: MaterializeModelProfileForDeletionResult;
430
+ }): Promise<void> {
431
+ options.settings.set("modelRoles", options.snapshot.previousModelRoles);
432
+ options.settings.set("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
433
+ options.settings.set("modelProfile.default", options.snapshot.previousPersistedDefaultProfile);
434
+ options.settings.override("modelRoles", options.snapshot.previousModelRoles);
435
+ options.settings.override("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
436
+ options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfile);
437
+ options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile);
438
+ await options.settings.flush();
439
+ }
440
+
360
441
  export async function activateModelProfile(
361
442
  options: PrepareModelProfileActivationOptions,
362
443
  applyOptions: { persistDefault?: boolean } = {},
@@ -1,3 +1,4 @@
1
+ import { sanitizeText } from "@gajae-code/utils";
1
2
  import type { GjcModelAssignmentTargetId } from "./model-registry";
2
3
  import type { ModelsConfig } from "./models-config-schema";
3
4
 
@@ -98,7 +99,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
98
99
  }),
99
100
  profile("claude-opus", ["anthropic"], {
100
101
  default: "anthropic/claude-opus-4-8:xhigh",
101
- executor: "anthropic/claude-sonnet-4-6",
102
+ executor: "anthropic/claude-sonnet-5",
102
103
  planner: "anthropic/claude-opus-4-8:low",
103
104
  critic: "anthropic/claude-opus-4-8:high",
104
105
  architect: "anthropic/claude-opus-4-8:xhigh",
@@ -267,6 +268,10 @@ export interface ModelProfilePresentation {
267
268
  providerGroup: string;
268
269
  }
269
270
 
271
+ function sanitizeModelProfileLabel(value: string): string {
272
+ return sanitizeText(value).replace(/\s+/g, " ").trim();
273
+ }
274
+
270
275
  const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
271
276
  "codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
272
277
  "codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
@@ -332,7 +337,15 @@ export function getModelProfilePresentation(
332
337
  const displayName = typeof profile === "string" ? undefined : profile.displayName;
333
338
  const presentation = PROFILE_PRESENTATION[name];
334
339
  if (presentation) return presentation;
335
- return { displayName: displayName ?? name, providerGroup: "CUSTOM" };
340
+ return { displayName: formatModelProfileDisplayLabel({ name, displayName }), providerGroup: "CUSTOM" };
341
+ }
342
+
343
+ export function formatModelProfileDisplayLabel(profile: Pick<ModelProfileDefinition, "name" | "displayName">): string {
344
+ return (
345
+ sanitizeModelProfileLabel(profile.displayName ?? profile.name) ||
346
+ sanitizeModelProfileLabel(profile.name) ||
347
+ "Unnamed profile"
348
+ );
336
349
  }
337
350
 
338
351
  export function groupModelProfilesForPresetLanding(
@@ -83,7 +83,8 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
83
83
  };
84
84
 
85
85
  export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
86
-
86
+ export const MODEL_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
87
+ export const MODEL_PROFILE_NAME_PATTERN_DESCRIPTION = "lowercase letters, numbers, dots, underscores, or hyphens";
87
88
  export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
88
89
 
89
90
  export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
@@ -1590,10 +1591,7 @@ export class ModelRegistry {
1590
1591
  const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1591
1592
  throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1592
1593
  }
1593
- await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
1594
- await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(checkedConfig.data, null, 2));
1595
- this.#modelsConfigFile.invalidate();
1596
- this.#reloadStaticModels();
1594
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
1597
1595
  const modelMapping = { ...definition.model_mapping };
1598
1596
  const profile: ModelProfileDefinition = {
1599
1597
  name: normalizedName,
@@ -1604,6 +1602,84 @@ export class ModelRegistry {
1604
1602
  };
1605
1603
  return profile;
1606
1604
  }
1605
+
1606
+ async renameCustomModelProfile(name: string, displayName: string): Promise<ModelProfileDefinition> {
1607
+ const normalizedName = name.trim();
1608
+ const nextDisplayName = displayName.trim();
1609
+ if (!normalizedName) throw new Error("Profile name is required.");
1610
+ if (!nextDisplayName) throw new Error("Profile display name is required.");
1611
+ const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "rename");
1612
+ const nextProfiles = {
1613
+ ...(current.profiles ?? {}),
1614
+ [normalizedName]: {
1615
+ ...profile,
1616
+ display_name: nextDisplayName,
1617
+ },
1618
+ };
1619
+ const checkedConfig = ModelsConfigSchema.safeParse({ ...current, profiles: nextProfiles });
1620
+ if (!checkedConfig.success) {
1621
+ const first = checkedConfig.error.issues[0];
1622
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1623
+ throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1624
+ }
1625
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
1626
+ const modelMapping = { ...profile.model_mapping };
1627
+ return {
1628
+ name: normalizedName,
1629
+ displayName: nextDisplayName,
1630
+ requiredProviders: aggregateModelProfileRequiredProviders(profile.required_providers, { modelMapping }),
1631
+ modelMapping,
1632
+ source: "user",
1633
+ };
1634
+ }
1635
+
1636
+ async deleteCustomModelProfile(name: string): Promise<ModelProfileConfig> {
1637
+ const normalizedName = name.trim();
1638
+ if (!normalizedName) throw new Error("Profile name is required.");
1639
+ const { current, profile } = this.#loadCustomProfileForMutation(normalizedName, "delete");
1640
+ const nextProfiles = { ...(current.profiles ?? {}) };
1641
+ delete nextProfiles[normalizedName];
1642
+ const checkedConfig = ModelsConfigSchema.safeParse({
1643
+ ...current,
1644
+ profiles: Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined,
1645
+ });
1646
+ if (!checkedConfig.success) {
1647
+ const first = checkedConfig.error.issues[0];
1648
+ const where = first?.path.length ? `/${first.path.map(String).join("/")}` : "root";
1649
+ throw new Error(`Generated models config is invalid at ${where}: ${first?.message ?? "unknown schema error"}`);
1650
+ }
1651
+ await this.#writeCheckedModelsConfig(checkedConfig.data);
1652
+ return profile;
1653
+ }
1654
+
1655
+ #loadCustomProfileForMutation(
1656
+ normalizedName: string,
1657
+ action: "rename" | "delete",
1658
+ ): { current: ModelsConfig; profile: ModelProfileConfig } {
1659
+ const loaded = this.#modelsConfigFile.tryLoad();
1660
+ if (loaded.status === "error") {
1661
+ throw new Error(
1662
+ `Cannot ${action} custom model profile because ${this.#modelsConfigFile.path()} is invalid. Fix the existing config before modifying presets.`,
1663
+ );
1664
+ }
1665
+ const current = loaded.value ?? this.#modelsConfigFile.createDefault();
1666
+ const profile = current.profiles?.[normalizedName];
1667
+ if (!profile) {
1668
+ const existing = this.#modelProfiles.get(normalizedName);
1669
+ if (existing && existing.source !== "user") {
1670
+ throw new Error(`Cannot ${action} bundled model profile: ${normalizedName}.`);
1671
+ }
1672
+ throw new Error(`Custom model profile does not exist: ${normalizedName}.`);
1673
+ }
1674
+ return { current, profile };
1675
+ }
1676
+
1677
+ async #writeCheckedModelsConfig(config: ModelsConfig): Promise<void> {
1678
+ await fs.mkdir(path.dirname(this.#modelsConfigFile.path()), { recursive: true });
1679
+ await Bun.write(this.#modelsConfigFile.path(), Bun.YAML.stringify(config, null, 2));
1680
+ this.#modelsConfigFile.invalidate();
1681
+ this.#reloadStaticModels();
1682
+ }
1607
1683
  applyConfiguredModelBindings(targetSettings: Settings): void {
1608
1684
  this.#modelBindingsTargetSettings = targetSettings;
1609
1685
  this.#applyConfiguredModelBindingsToTarget();
@@ -280,7 +280,8 @@ export const SETTINGS_SCHEMA = {
280
280
  ui: {
281
281
  tab: "interaction",
282
282
  label: "Terminal Bell",
283
- description: "Emit a BEL character for local terminal notifications",
283
+ description:
284
+ "Emit a BEL character for local terminal notifications. Windows Terminal may keep BEL silent depending on profile/system sound settings; use completion.notifyCommand for a PowerShell Console.Beep workaround.",
284
285
  },
285
286
  },
286
287
  "notifications.bellOnComplete": {
@@ -454,7 +455,7 @@ export const SETTINGS_SCHEMA = {
454
455
  // Status line
455
456
  "statusLine.preset": {
456
457
  type: "enum",
457
- values: ["default", "minimal", "compact", "full", "nerd", "ascii", "custom"] as const,
458
+ values: ["default", "default-usage", "minimal", "compact", "full", "nerd", "ascii", "custom"] as const,
458
459
  default: "default",
459
460
  ui: {
460
461
  tab: "appearance",
@@ -462,6 +463,11 @@ export const SETTINGS_SCHEMA = {
462
463
  description: "Pre-built status line configurations",
463
464
  options: [
464
465
  { value: "default", label: "Default", description: "Model, path, git, context, tokens, cost" },
466
+ {
467
+ value: "default-usage",
468
+ label: "Default + Usage",
469
+ description: "Default layout with provider usage quota",
470
+ },
465
471
  { value: "minimal", label: "Minimal", description: "Path and git only" },
466
472
  { value: "compact", label: "Compact", description: "Model, git, cost, context" },
467
473
  { value: "full", label: "Full", description: "All segments including time" },
@@ -1179,7 +1185,7 @@ export const SETTINGS_SCHEMA = {
1179
1185
  tab: "interaction",
1180
1186
  label: "Completion Notification Command",
1181
1187
  description:
1182
- "Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables.",
1188
+ "Optional user-level shell command to run when an agent turn completes; receives GJC_NOTIFICATION_* environment variables. On Windows, this can call PowerShell [Console]::Beep when terminal BEL is silent.",
1183
1189
  },
1184
1190
  },
1185
1191
 
@@ -2204,6 +2210,11 @@ export const SETTINGS_SCHEMA = {
2204
2210
  ui: { tab: "tools", label: "Web Search", description: "Enable the web_search tool for web searching" },
2205
2211
  },
2206
2212
 
2213
+ "web_search.provider": {
2214
+ type: "enum",
2215
+ values: ["auto", ...CONFIGURABLE_SEARCH_PROVIDER_IDS] as const,
2216
+ default: "auto",
2217
+ },
2207
2218
  "web_search.fallback": {
2208
2219
  type: "array",
2209
2220
  default: EMPTY_STRING_ARRAY,
@@ -1273,8 +1273,10 @@ async function sendTmuxPromptKeys(
1273
1273
  prompt: string,
1274
1274
  runner: CommandRunner = runCommand,
1275
1275
  ): Promise<boolean> {
1276
- const sent = await runner(["tmux", "send-keys", "-t", target, prompt, "C-m", "C-m"]);
1277
- return sent.exitCode === 0;
1276
+ const typed = await runner(["tmux", "send-keys", "-t", target, "-l", prompt]);
1277
+ if (typed.exitCode !== 0) return false;
1278
+ const submitted = await runner(["tmux", "send-keys", "-t", target, "C-m"]);
1279
+ return submitted.exitCode === 0;
1278
1280
  }
1279
1281
 
1280
1282
  function boundedLineCount(value: unknown): number {
@@ -49,6 +49,7 @@ interface ThresholdModel {
49
49
  type DeepInterviewModel = RoundQuestionModel | TopologyQuestionModel | ProgressModel | ThresholdModel;
50
50
 
51
51
  function normalizeText(text: string): string {
52
+ if (typeof text !== "string") return "";
52
53
  return text.trim().replaceAll("\r\n", "\n");
53
54
  }
54
55