@proxysoul/soulforge 2.15.6 → 2.15.7

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.
package/dist/index.js CHANGED
@@ -3056,6 +3056,12 @@ import { spawnSync } from "child_process";
3056
3056
  import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3057
3057
  import { homedir as homedir3 } from "os";
3058
3058
  import { join as join3 } from "path";
3059
+ function _invalidateKeychainCache(key) {
3060
+ if (key)
3061
+ _keychainHasCache.delete(key);
3062
+ else
3063
+ _keychainHasCache.clear();
3064
+ }
3059
3065
  function setDefaultKeyPriority(p) {
3060
3066
  _defaultPriority = p;
3061
3067
  }
@@ -3083,7 +3089,7 @@ function keychainAvailable() {
3083
3089
  function keychainGet(key) {
3084
3090
  try {
3085
3091
  if (process.platform === "darwin") {
3086
- const result = spawnSync("security", ["find-generic-password", "-a", KEYCHAIN_SERVICE, "-s", key, "-w"], { timeout: 5000, encoding: "utf-8" });
3092
+ const result = spawnSync("security", ["find-generic-password", "-a", KEYCHAIN_SERVICE, "-s", key, "-w"], { timeout: 5000, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
3087
3093
  if (result.status === 0 && result.stdout) {
3088
3094
  return result.stdout.trim();
3089
3095
  }
@@ -3092,7 +3098,8 @@ function keychainGet(key) {
3092
3098
  if (process.platform === "linux") {
3093
3099
  const result = spawnSync("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "key", key], {
3094
3100
  timeout: 5000,
3095
- encoding: "utf-8"
3101
+ encoding: "utf-8",
3102
+ stdio: ["ignore", "pipe", "ignore"]
3096
3103
  });
3097
3104
  if (result.status === 0 && result.stdout) {
3098
3105
  return result.stdout.trim();
@@ -3104,16 +3111,23 @@ function keychainGet(key) {
3104
3111
  }
3105
3112
  function keychainSet(key, value) {
3106
3113
  try {
3114
+ if (!value)
3115
+ return false;
3107
3116
  if (process.platform === "darwin") {
3108
- spawnSync("security", ["delete-generic-password", "-a", KEYCHAIN_SERVICE, "-s", key], {
3109
- timeout: 5000
3117
+ const result = spawnSync("security", ["add-generic-password", "-U", "-a", KEYCHAIN_SERVICE, "-s", key, "-w", value], {
3118
+ timeout: 5000,
3119
+ encoding: "utf-8",
3120
+ stdio: ["ignore", "ignore", "ignore"]
3110
3121
  });
3111
- const result = spawnSync("security", ["add-generic-password", "-a", KEYCHAIN_SERVICE, "-s", key, "-w"], { input: `${value}
3112
- `, timeout: 5000, encoding: "utf-8" });
3113
3122
  return result.status === 0;
3114
3123
  }
3115
3124
  if (process.platform === "linux") {
3116
- const result = spawnSync("secret-tool", ["store", "--label", `SoulForge ${key}`, "service", KEYCHAIN_SERVICE, "key", key], { input: value, timeout: 5000, encoding: "utf-8" });
3125
+ const result = spawnSync("secret-tool", ["store", "--label", `SoulForge ${key}`, "service", KEYCHAIN_SERVICE, "key", key], {
3126
+ input: value,
3127
+ timeout: 5000,
3128
+ encoding: "utf-8",
3129
+ stdio: ["pipe", "ignore", "ignore"]
3130
+ });
3117
3131
  return result.status === 0;
3118
3132
  }
3119
3133
  } catch {}
@@ -3122,12 +3136,13 @@ function keychainSet(key, value) {
3122
3136
  function keychainDelete(key) {
3123
3137
  try {
3124
3138
  if (process.platform === "darwin") {
3125
- const result = spawnSync("security", ["delete-generic-password", "-a", KEYCHAIN_SERVICE, "-s", key], { timeout: 5000 });
3139
+ const result = spawnSync("security", ["delete-generic-password", "-a", KEYCHAIN_SERVICE, "-s", key], { timeout: 5000, stdio: ["ignore", "ignore", "ignore"] });
3126
3140
  return result.status === 0;
3127
3141
  }
3128
3142
  if (process.platform === "linux") {
3129
3143
  const result = spawnSync("secret-tool", ["clear", "service", KEYCHAIN_SERVICE, "key", key], {
3130
- timeout: 5000
3144
+ timeout: 5000,
3145
+ stdio: ["ignore", "ignore", "ignore"]
3131
3146
  });
3132
3147
  return result.status === 0;
3133
3148
  }
@@ -3152,7 +3167,16 @@ function fileWrite(data) {
3152
3167
  function getSecretSources(key, priority = _defaultPriority) {
3153
3168
  const envVar = ENV_MAP[key];
3154
3169
  const hasEnv = !!(envVar && process.env[envVar]);
3155
- const hasKeychain = keychainAvailable() && !!keychainGet(key);
3170
+ let hasKeychain = false;
3171
+ if (keychainAvailable()) {
3172
+ const cached = _keychainHasCache.get(key);
3173
+ if (cached !== undefined) {
3174
+ hasKeychain = cached;
3175
+ } else {
3176
+ hasKeychain = !!keychainGet(key);
3177
+ _keychainHasCache.set(key, hasKeychain);
3178
+ }
3179
+ }
3156
3180
  const hasFile = !!fileRead()[key];
3157
3181
  let active = "none";
3158
3182
  if (priority === "app") {
@@ -3191,6 +3215,7 @@ function getSecret(key, priority = _defaultPriority) {
3191
3215
  function setSecret(key, value) {
3192
3216
  if (keychainAvailable()) {
3193
3217
  if (keychainSet(key, value)) {
3218
+ _invalidateKeychainCache(key);
3194
3219
  const data2 = fileRead();
3195
3220
  if (data2[key]) {
3196
3221
  delete data2[key];
@@ -3214,6 +3239,7 @@ function deleteSecret(key) {
3214
3239
  deleted = keychainDelete(key);
3215
3240
  if (deleted)
3216
3241
  storage = "keychain";
3242
+ _invalidateKeychainCache(key);
3217
3243
  }
3218
3244
  const data = fileRead();
3219
3245
  if (data[key]) {
@@ -3248,8 +3274,9 @@ function getProviderApiKey(envVar, priority = _defaultPriority) {
3248
3274
  }
3249
3275
  return getEnv() ?? getApp();
3250
3276
  }
3251
- var SECRETS_DIR, SECRETS_FILE, KEYCHAIN_SERVICE = "soulforge", _defaultPriority = "env", STATIC_SECRETS, ENV_MAP, ENV_TO_SECRET;
3277
+ var _keychainHasCache, SECRETS_DIR, SECRETS_FILE, KEYCHAIN_SERVICE = "soulforge", _defaultPriority = "env", STATIC_SECRETS, ENV_MAP, ENV_TO_SECRET;
3252
3278
  var init_secrets = __esm(() => {
3279
+ _keychainHasCache = new Map;
3253
3280
  SECRETS_DIR = join3(homedir3(), ".soulforge");
3254
3281
  SECRETS_FILE = join3(SECRETS_DIR, "secrets.json");
3255
3282
  STATIC_SECRETS = {
@@ -56679,11 +56706,13 @@ var init_ui = __esm(() => {
56679
56706
  hearthSettings: false,
56680
56707
  tabNamePopup: false,
56681
56708
  memoryBrowser: false,
56709
+ modelEvents: false,
56682
56710
  uiDemo: false
56683
56711
  };
56684
56712
  useUIStore = create()(subscribeWithSelector((set2) => ({
56685
56713
  modals: { ...INITIAL_MODALS },
56686
56714
  routerSlotPicking: null,
56715
+ fallbackForModel: null,
56687
56716
  commandPickerConfig: null,
56688
56717
  infoPopupConfig: null,
56689
56718
  statusDashboardTab: "Context",
@@ -56702,6 +56731,7 @@ var init_ui = __esm(() => {
56702
56731
  modals: s.modals[name21] ? { ...s.modals, [name21]: false } : { ...INITIAL_MODALS, [name21]: true }
56703
56732
  })),
56704
56733
  setRouterSlotPicking: (slot) => set2({ routerSlotPicking: slot }),
56734
+ setFallbackForModel: (modelId) => set2({ fallbackForModel: modelId }),
56705
56735
  openCommandPicker: (config2) => set2(() => ({
56706
56736
  commandPickerConfig: config2,
56707
56737
  modals: { ...INITIAL_MODALS, commandPicker: true }
@@ -63352,7 +63382,7 @@ var package_default;
63352
63382
  var init_package = __esm(() => {
63353
63383
  package_default = {
63354
63384
  name: "@proxysoul/soulforge",
63355
- version: "2.15.6",
63385
+ version: "2.15.7",
63356
63386
  description: "Graph-powered code intelligence \u2014 multi-agent coding with codebase-aware AI",
63357
63387
  repository: {
63358
63388
  type: "git",
@@ -96262,32 +96292,19 @@ Senior engineer. Quiet at the keyboard. Reads code like prose. Finds the file, o
96262
96292
  </identity>
96263
96293
 
96264
96294
  <tool_loop>
96265
- A turn is tool calls followed by exactly one final answer. The final answer is mandatory \u2014 every turn ends with text, never on a tool result. Between tool calls: silence \u2014 no prefix, no label, no "reading\u2026", no "checking\u2026", no narration of what you just saw or will do next. After the last tool: speak.
96266
-
96267
- Speak only when (a) the task is complete, (b) a destructive/irreversible action needs confirmation, (c) genuine ambiguity blocks progress, or (d) an unrecoverable error (missing credentials, API unreachable, repeated permission denial) makes further tool calls pointless. In every case, the turn ends with a final answer \u2014 empty endings are a bug.
96295
+ A turn is tool calls followed by exactly one final answer. Between tool calls: zero text \u2014 no acknowledgements ("Got it", "Done"), no self-narration ("I'll\u2026", "Let me\u2026", "Going to\u2026"), no progress declarations ("Found it", "Root cause confirmed"), no meta-previews ("One more check", "Just to be sure"), no transition announcements ("Here's what I found"), no advisory reassurances, no findings prose, no visible self-correction ("Wait \u2014 actually"). Synonyms and paraphrases that perform the same function are equally forbidden \u2014 if a sentence performs the function, delete it and call the next tool.
96268
96296
 
96269
- When warning about a destructive action: the warning is the answer \u2014 full sentences, no tool chain first.
96297
+ After the last tool: speak. The final answer is mandatory \u2014 every turn ends with text, never on a tool result. Speak only when (a) the task is complete, (b) a destructive/irreversible action needs confirmation, (c) genuine ambiguity blocks progress, or (d) an unrecoverable error makes further tool calls pointless. Warning about a destructive action: the warning IS the answer \u2014 full sentences, no tool chain first.
96270
96298
  </tool_loop>
96271
96299
 
96272
- <forbidden_between_tool_calls>
96273
- These grammatical classes (and their synonyms/paraphrases) are equally forbidden \u2014 if a sentence performs the function, delete it and call the next tool:
96274
- - Acknowledgements ("Got it", "Done", "Noted", emotes, asterisk gestures)
96275
- - Self-narration ("I'll\u2026", "Let me\u2026", "Going to\u2026", "Next I'll\u2026")
96276
- - Progress declarations ("Root cause confirmed", "Found it", "Makes sense")
96277
- - Meta-previews ("One more check", "Just to be sure", "Quick verification")
96278
- - Transition announcements ("Here's what I found", "With that done")
96279
- - Advisory reassurances ("Cross-tab noted", "No conflict here")
96280
- - Mid-flow findings prose, visible self-correction ("Wait \u2014 actually"), or repetition of anything already said
96281
- </forbidden_between_tool_calls>
96282
-
96283
96300
  <answer_voice>
96284
- Confident, flat, direct. No excitement, theatrics, hedging, apology. Reports what happened. Self-corrects silently \u2014 the answer reflects the corrected understanding, not the path to it. First word is a noun, verb, or file path \u2014 never "I", "we", "the", "so", "well", "ok", or any discourse marker.
96301
+ Confident, flat, direct. No excitement, theatrics, hedging, apology. Self-corrects silently \u2014 the answer reflects the corrected understanding, not the path to it. First word is a noun, verb, or file path \u2014 never "I", "we", "the", "so", "well", "ok", or any discourse marker. No closing pleasantries, no "let me know", no follow-up offers.
96285
96302
 
96286
- Shape: length matches work. One-file change \u2192 one line stating path and what changed (zero lines is a bug). Diagnostic \u2192 2-5 bullets of \`path:line \u2014 finding. fix.\`. Explanation \u2192 as long as needed, zero filler. One format per answer \u2014 bullets or prose, not both describing the same thing. No section headers unless the answer has \u22652 genuinely independent parts. No closing pleasantries, no "let me know", no follow-up offers.
96303
+ Shape: length matches work. One-file change \u2192 one line stating path and what changed (zero lines is a bug). Diagnostic \u2192 2-5 bullets of \`path:line \u2014 finding. fix.\`. Explanation \u2192 as long as needed, zero filler. One format per answer \u2014 bullets or prose, not both. No section headers unless the answer has \u22652 genuinely independent parts.
96287
96304
 
96288
- Compression: drop articles when unambiguous. Drop copula when predicate is adjective/participle. Replace causal prose with arrows (A \u2192 B \u2192 C). Prefer fragments. Use shortest verb (use not utilize, fix not "implement a solution for"). Strip hedging (might/probably/I think), strip filler (just/really/basically/actually/simply). Abbreviate domain terms when repeated (DB, auth, config, fn, ref). Code identifiers, file paths, type names, flags: verbatim.
96305
+ Compression: drop articles when unambiguous, drop copula when predicate is adjective/participle, replace causal prose with arrows (A \u2192 B \u2192 C), prefer fragments, shortest verb (use not utilize), strip hedging (might/probably/I think) and filler (just/really/basically/actually). Abbreviate domain terms when repeated (DB, auth, config, fn). Code identifiers, file paths, type names, flags: verbatim.
96289
96306
 
96290
- Suspend compression \u2014 write full sentences \u2014 for destructive actions, security warnings, multi-step instructions where fragment ambiguity risks misread, or when the user is confused. Resume terse after.
96307
+ Suspend compression \u2014 write full sentences \u2014 for destructive actions, security warnings, multi-step instructions where fragment ambiguity risks misread, or when the user is confused.
96291
96308
  </answer_voice>`, SHARED_RULES = `
96292
96309
  <task_discipline>
96293
96310
  - Surgical Read code before modifying. Stay focused on what was asked.
@@ -96303,9 +96320,11 @@ Suspend compression \u2014 write full sentences \u2014 for destructive actions,
96303
96320
  function resolveRetrySettings(raw, opts = {}) {
96304
96321
  const defaultBase = opts.agent ? DEFAULT_AGENT_BASE_DELAY_MS : DEFAULT_CHAT_BASE_DELAY_MS;
96305
96322
  const obj = raw && typeof raw === "object" ? raw : null;
96306
- const maxRetries = clampIntMin(obj?.maxAttempts, MIN_MAX_ATTEMPTS, DEFAULT_MAX_RETRIES2, "retry.maxAttempts");
96323
+ const legacyMaxAttempts = clampIntMin(obj?.maxAttempts, MIN_MAX_ATTEMPTS, DEFAULT_MAX_RETRIES2, "retry.maxAttempts");
96324
+ const maxTransientRetries = clampIntMin(obj?.maxTransientRetries, MIN_MAX_ATTEMPTS, legacyMaxAttempts, "retry.maxTransientRetries");
96325
+ const maxStallRetries = clampIntMin(obj?.maxStallRetries, MIN_MAX_ATTEMPTS, legacyMaxAttempts, "retry.maxStallRetries");
96307
96326
  const baseDelayMs = clampInt(obj?.baseDelayMs, MIN_BASE_DELAY_MS, MAX_BASE_DELAY_MS, defaultBase, "retry.baseDelayMs");
96308
- return { maxRetries, baseDelayMs };
96327
+ return { maxTransientRetries, maxStallRetries, baseDelayMs };
96309
96328
  }
96310
96329
  function clampIntMin(value, min, fallback, key) {
96311
96330
  if (value === undefined)
@@ -362206,6 +362225,7 @@ async function formatFile(filePath, cwd) {
362206
362225
  try {
362207
362226
  const proc = Bun.spawn(["sh", "-c", command], {
362208
362227
  cwd: effectiveCwd,
362228
+ stdin: "ignore",
362209
362229
  stdout: "pipe",
362210
362230
  stderr: "pipe",
362211
362231
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
@@ -362353,6 +362373,7 @@ var init_project = __esm(() => {
362353
362373
  const runCommand = async (cmd) => {
362354
362374
  const proc = Bun.spawn(["sh", "-c", cmd], {
362355
362375
  cwd,
362376
+ stdin: "ignore",
362356
362377
  stdout: "pipe",
362357
362378
  stderr: "pipe",
362358
362379
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", ...args2.env }
@@ -362512,6 +362533,7 @@ async function autoFormatAfterEdit(filePath, cwd) {
362512
362533
  try {
362513
362534
  const proc = Bun.spawn(["sh", "-c", command], {
362514
362535
  cwd: effectiveCwd,
362536
+ stdin: "ignore",
362515
362537
  stdout: "pipe",
362516
362538
  stderr: "pipe",
362517
362539
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
@@ -362924,7 +362946,7 @@ var init_edit_file = __esm(() => {
362924
362946
  init_ts_project_detect();
362925
362947
  editFileTool = {
362926
362948
  name: "edit_file",
362927
- description: "Edit a non-TS/JS file by replacing content (JSON, YAML, Markdown, config, raw text). For .ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs files use ast_edit \u2014 it's safer and won't drift. " + "Read first, then provide path, oldString, newString. " + "Provide lineStart (1-indexed from read output) for reliable line-anchored matching \u2014 " + "the range is derived from oldString line count. Without lineStart, falls back to string matching (fails if ambiguous). " + "Empty oldString creates a new file. Use multi_edit for multiple changes to the same file. " + "Edits are applied immediately.",
362949
+ description: "Edit a non-TS/JS file by replacing content (JSON, YAML, Markdown, config, raw text). For .ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs files use ast_edit \u2014 it's safer and won't drift. " + "Read first, then provide path, oldString, newString. " + "Provide lineStart (1-indexed from read output) for reliable line-anchored matching \u2014 the range is derived from oldString line count. Without lineStart, falls back to string matching (fails if ambiguous). " + "Keep oldString minimal and unique in the file \u2014 don't pad with large unchanged regions just to anchor a small change. " + "Empty oldString creates a new file. Use multi_edit for multiple changes to the same file. " + "Edits are applied immediately.",
362928
362950
  execute: async (args2) => {
362929
362951
  try {
362930
362952
  const filePath = resolve16(args2.path);
@@ -363099,7 +363121,7 @@ var init_ast_edit = __esm(() => {
363099
363121
  ]);
363100
363122
  astEditTool = {
363101
363123
  name: "ast_edit",
363102
- description: "AST edit for TS/JS (.ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs). Default editor for these files \u2014 ts-morph locates symbols by {target, name}, no oldString, no line drift. " + "Single op: {action, target, name, value?, newCode?, index?}. " + "Multi-op (atomic, same file): {operations:[{...}, ...]} \u2014 all-or-nothing rollback. " + "Create files: action='create_file', newCode=<full content>. " + "Targets: function|class|interface|type|enum|variable|method|property|constructor|arrow_function. " + "Class members: name='ClassName.memberName' or just 'memberName'. Arrow const: target='arrow_function', name='foo'. " + "Idempotent: add_import/add_named_import/add_named_reexport merge; add_constructor modifies in place. " + "replace_in_body is whitespace-tolerant (tab\u2194space, CRLF, indent drift auto-handled) and supports anchor-pair range mode: pass value=<short unique start anchor> + valueEnd=<short unique end anchor> to replace the span between them \u2014 rewrite a 100-line block with ~20 tokens. " + "Body shape \u2014 critical: set_body/add_statement/insert_statement take body CONTENTS ONLY (no surrounding {}, ts-morph wraps). " + "add_method/add_constructor/add_getter/add_setter take the FULL declaration INCLUDING braces (e.g. 'foo(x: number) { return x; }'). " + "replace takes the WHOLE symbol text including braces. " + "add_property on interface: 'name: type' or 'name?: type'; on class: 'name: type = value' or 'name = value'. " + "Import ops: value=module specifier (e.g. 'zod'), newCode=comma-separated names (e.g. 'z' or 'useState,useEffect'); remove_named_import uses value=module and name=single identifier. " + "Safe defaults: rename = declaration-only; use rename_global or rename_symbol for project-wide. " + "CANNOT target anonymous callbacks or union members inside a type alias \u2014 use replace on the whole symbol, or replace_in_body for AST-anchored text tweaks. " + "insert_text requires an anchor (index=0|-1 or value='after-imports'|'before-exports'). " + "See the <ast_edit> section of the system tool_usage block for the full operation taxonomy and examples.",
363124
+ description: "AST edit for TS/JS (.ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs). Default editor for these files \u2014 used BEFORE edit_file/multi_edit, not as fallback. ts-morph locates symbols by {target, name}: no oldString, no whitespace/escape failures, no line-offset drift. " + "Single op: {action, target, name, value?, newCode?, index?}. " + "Multi-op (atomic, same file): {operations:[{...}, ...]} \u2014 all-or-nothing rollback. Use for 'add import + use it' in one call. " + "Create files: action='create_file', newCode=<full content>. " + "Targets: function|class|interface|type|enum|variable|method|property|constructor|arrow_function. " + "Class members: name='ClassName.memberName' or just 'memberName'. Arrow const: target='arrow_function', name='foo'. " + "Idempotent: add_import/add_named_import/add_named_reexport merge; add_constructor modifies in place. " + "CAN DO (no fallback): any named symbol, JSX/TSX with Unicode/special chars/escape sequences/quotes (ts-morph wraps the TS compiler \u2014 no JSX limitation), large rewrites via replace or anchor-pair replace_in_body, whitespace drift (tab\u2194space, CRLF\u2194LF, indent stripping auto-handled). " + "CANNOT target: anonymous callbacks (inline arrows, IIFEs, object-literal methods without names) \u2192 use replace_in_body on the enclosing NAMED symbol. Union members inside a type alias \u2192 use replace on the whole type. Raw text inside comments/strings not bound to a symbol \u2192 replace_in_body on the enclosing symbol. " + "ONLY fall back to edit_file when: non-TS/JS file (JSON/YAML/MD/config), edit entirely outside any named symbol (e.g. top-of-file banner), or file has a parse error breaking ts-morph. 'Long needle' / 'JSX special chars' are NOT fallback reasons. " + "Tiers (pick smallest): MICRO (1-10 tok) set_type, set_return_type, set_async, set_export, rename, remove, set_initializer, add_parameter, set_optional. " + "BODY (10-100 tok) set_body, add_statement, add_property, add_method, add_constructor, add_decorator, set_extends, add_implements, replace_in_body. " + "FULL replace (whole symbol), create_file. " + "FILE-LEVEL add_import, add_named_import (idempotent merges), organize_imports, fix_missing_imports, add_function/class/interface/type_alias/enum, insert_text (REQUIRES anchor: index=0|-1 or value='after-imports'|'before-exports'). " + "ATOMIC operations:[{...},...] \u2014 all-or-nothing rollback. " + "Body shape \u2014 get this wrong and you corrupt the file: " + "set_body/add_statement/insert_statement take body CONTENTS ONLY \u2014 NO surrounding {} (ts-morph wraps; passing {\u2026} produces {{\u2026}}). " + "add_method/add_constructor/add_getter/add_setter take the FULL declaration INCLUDING braces (e.g. 'foo(x: number) { return x; }'). " + "replace takes the WHOLE symbol text including braces. " + "add_property on interface: 'name: type' or 'name?: type'; on class: 'name: type = value' or 'name = value'. " + "add_statement on expression-body arrow auto-wraps into a block \u2014 safe to call. " + "replace_in_body shapes (pick smallest): SHORT ANCHOR value=<1-2 unique lines> + newCode=<replacement>. ANCHOR PAIR (RANGE) value=<short start anchor> + valueEnd=<short end anchor> + newCode=<span replacement> \u2014 rewrites a 100-line block with ~20 tokens of anchors. Exact-match ambiguity (\u22652 identical hits) THROWS \u2014 add surrounding context or use anchor pair. " + "Import ops: value=module specifier (e.g. 'zod'), newCode=comma-separated names (e.g. 'z' or 'useState,useEffect'). remove_named_import: value=module, name=single identifier. " + "rename is declaration-only (safe default). Use rename_global / rename_symbol / move_symbol / rename_file for cross-file refactors. " + "Examples \u2014 " + "MICRO multi-op: ast_edit(path, operations:[{action:'set_async',target:'method',name:'UserStore.load',value:'true'},{action:'set_return_type',target:'method',name:'UserStore.load',value:'Promise<User>'}]). " + `BODY: ast_edit(path, action:'add_statement', target:'function', name:'loadConfig', newCode:"logger.info('config loaded');"). ` + "ANCHOR PAIR rewrite: ast_edit(path, action:'replace_in_body', target:'function', name:'ProviderSettings', value:'const caption = (', valueEnd:'</PremiumPopup>', newCode:'<new JSX>'). " + `ATOMIC import+method: ast_edit(path, operations:[{action:'add_named_import',value:'zod',newCode:'z'},{action:'add_method',target:'class',name:'Validator',newCode:"validate(input: unknown) { return z.string().parse(input); }"}]). ` + "CREATE: ast_edit('src/foo.ts', action:'create_file', newCode:'export function foo() { return 42; }\\\\n').",
363103
363125
  execute: async (args2) => {
363104
363126
  try {
363105
363127
  const filePath = resolve17(args2.path);
@@ -377122,7 +377144,8 @@ ${content2}` : content2,
377122
377144
  var exports_spawn = {};
377123
377145
  __export(exports_spawn, {
377124
377146
  buildSafeEnv: () => buildSafeEnv,
377125
- SAFE_STDIO: () => SAFE_STDIO
377147
+ SAFE_STDIO: () => SAFE_STDIO,
377148
+ SAFE_SPAWN_OPTS: () => SAFE_SPAWN_OPTS
377126
377149
  });
377127
377150
  function buildSafeEnv() {
377128
377151
  const env = {};
@@ -377136,7 +377159,7 @@ function buildSafeEnv() {
377136
377159
  env.GIT_TERMINAL_PROMPT = "0";
377137
377160
  return env;
377138
377161
  }
377139
- var SECRET_ENV_PATTERN, ENV_ALLOWLIST, SAFE_STDIO;
377162
+ var SECRET_ENV_PATTERN, ENV_ALLOWLIST, SAFE_STDIO, SAFE_SPAWN_OPTS;
377140
377163
  var init_spawn = __esm(() => {
377141
377164
  SECRET_ENV_PATTERN = /_API_KEY$|_SECRET$|_TOKEN$|_PASSWORD$|_CREDENTIAL$|_PRIVATE_KEY$/;
377142
377165
  ENV_ALLOWLIST = new Set([
@@ -377173,6 +377196,10 @@ var init_spawn = __esm(() => {
377173
377196
  "OTUI_TREE_SITTER_WORKER_PATH"
377174
377197
  ]);
377175
377198
  SAFE_STDIO = ["ignore", "pipe", "pipe"];
377199
+ SAFE_SPAWN_OPTS = {
377200
+ stdio: SAFE_STDIO,
377201
+ detached: true
377202
+ };
377176
377203
  });
377177
377204
 
377178
377205
  // src/core/git/status.ts
@@ -378114,7 +378141,7 @@ var init_grep = __esm(() => {
378114
378141
  init_install();
378115
378142
  grepTool = {
378116
378143
  name: "grep",
378117
- description: "[TIER-2] Raw ripgrep search \u2014 use soul_grep first, fall back to this for complex regex or non-code files. " + "Returns matching file paths sorted by modification time. " + "HOW TO USE: Provide a regex pattern. Optionally specify path to narrow scope, glob to filter file types. " + "LIMITATIONS: Results limited to 100 files (newest first). Hidden files are skipped.",
378144
+ description: "[TIER-2] Raw ripgrep search \u2014 use soul_grep first, fall back to this for complex regex or non-code files. " + "Returns matching lines with file paths and line numbers. Respects .gitignore. " + "HOW TO USE: Provide a regex pattern. Optionally specify path to narrow scope, glob to filter file types, maxCount to cap matches per file (default 50). " + "LIMITATIONS: Output capped at 32KB. Long lines truncated at 1000 chars. Hidden files skipped.",
378118
378145
  execute: async (args2) => {
378119
378146
  const pattern = args2.pattern;
378120
378147
  const searchPath = args2.path ?? ".";
@@ -378135,7 +378162,9 @@ var init_grep = __esm(() => {
378135
378162
  const rgBin = getVendoredPath("rg") ?? "rg";
378136
378163
  const proc = spawn9(rgBin, rgArgs, {
378137
378164
  cwd: process.cwd(),
378138
- timeout: 1e4
378165
+ timeout: 1e4,
378166
+ stdio: ["ignore", "pipe", "pipe"],
378167
+ detached: true
378139
378168
  });
378140
378169
  const chunks = [];
378141
378170
  let totalBytes = 0;
@@ -378154,7 +378183,7 @@ var init_grep = __esm(() => {
378154
378183
  if (lastNl > 0)
378155
378184
  output = output.slice(0, lastNl);
378156
378185
  output += `
378157
- [output capped \u2014 narrow with glob or path params]`;
378186
+ [Output capped at 32KB. Use maxCount=N (default 50) for fewer hits per file, or narrow with glob/path to refine.]`;
378158
378187
  }
378159
378188
  if (code === 0 || code === 1) {
378160
378189
  res(output || "No matches found.");
@@ -378164,7 +378193,9 @@ var init_grep = __esm(() => {
378164
378193
  fallbackArgs.push("--include", glob);
378165
378194
  const grepProc = spawn9("grep", fallbackArgs, {
378166
378195
  cwd: process.cwd(),
378167
- timeout: 1e4
378196
+ timeout: 1e4,
378197
+ stdio: ["ignore", "pipe", "pipe"],
378198
+ detached: true
378168
378199
  });
378169
378200
  const grepChunks = [];
378170
378201
  let grepBytes = 0;
@@ -378182,7 +378213,7 @@ var init_grep = __esm(() => {
378182
378213
  if (lastNl > 0)
378183
378214
  out2 = out2.slice(0, lastNl);
378184
378215
  out2 += `
378185
- [output capped \u2014 narrow with glob or path params]`;
378216
+ [Output capped at 32KB. Use maxCount=N (default 50) for fewer hits per file, or narrow with glob/path to refine.]`;
378186
378217
  }
378187
378218
  res(out2);
378188
378219
  });
@@ -378691,27 +378722,71 @@ function createMemoryTool(deps) {
378691
378722
  ], intelligence);
378692
378723
  return tool({
378693
378724
  description: [
378694
- "Persistent cross-session knowledge store. SQLite-backed, project + global scopes, FTS + semantic search.",
378725
+ "Persistent cross-session knowledge store. SQLite-backed, project + global scopes, FTS + semantic search. The across-session brain: Soul Map = what code IS; memory = WHY it got that way. Use it like a primary tool, not a last resort.",
378726
+ "",
378727
+ "Auto-recall fires before each user turn \u2014 top-3 relevant memories injected as <recalled_memories> stubs (summary + id + signals + '\u21B3 has details' marker), \u2264600ch typical, cached + deduped per session. When 'has details' matters, call action:'get' with the 8-char prefix to read the full body. Auto-recall is signal-driven and misses generic single-word prompts \u2014 fall back to action:'search' once when convention matters and nothing surfaced.",
378695
378728
  "",
378696
- "WRITE proactively when the user reveals durable knowledge a future session would need:",
378697
- " \u2022 pref \u2014 workflow/style ('use bun not npm', 'be terse')",
378698
- " \u2022 decision \u2014 choice with rationale ('migrated to zustand because redux boilerplate')",
378699
- " \u2022 gotcha \u2014 non-obvious bug/quirk ('JWT expiry uses container clock, drifts in prod')",
378700
- " \u2022 context \u2014 project fact not visible in code ('legacy/ deletes next sprint')",
378701
- "DON'T store what the Soul Map already shows: file structure, exports, signatures, deps.",
378729
+ "WRITE PROACTIVELY on these triggers (fire on ANY, not just user-prompted):",
378730
+ " 1. USER STATES A PREFERENCE OR DIRECTIVE \u2192 pref. Infer from cues \u2014 don't wait for the word 'remember'. Cues that mean 'this is durable': corrective tone about HOW you did something ('be terse', 'stop narrating', 'use bullets'); generalising language ('always', 'never', 'from now on', 'by default', 'prefer', 'in this repo we\u2026'); imperative meta-instructions about workflow/style/tooling/formatting/naming orthogonal to the current task; user repeats or rephrases the same correction (rule you missed last time, write it now); user asks 'why didn't you\u2026?' about a behavior (they had an expectation, capture it). Mid-instruction corrections ('commit it, and be concise') split into two acts: do the task, write the rule.",
378731
+ " 2. A CHOICE GETS MADE WITH A REASON \u2192 decision. Soul Map shows the WHAT; the WHY is what future-you needs. Capture rationale in details.",
378732
+ " 3. SHARP-EDGE DISCOVERED \u2192 gotcha. Bug that took >5min to diagnose, non-obvious quirk, 'don't touch X because Y', workaround for a flaky test. Include symptom + fix location.",
378702
378733
  "",
378703
- "RECALL is automatic \u2014 relevant memories are injected before each user turn based on prompt + edited files. Use action:'search' only when auto-recall missed something.",
378734
+ "SEARCH KEYWORDS \u2014 run action:'search' when about to do any of these and recall was empty: commit message shape, lint/format choice, test framework conventions, package manager (bun/npm/pnpm/yarn), import style, file naming, error-handling pattern, logger choice, state-management library, dispatch/agent setup, prompt-engineering rules. One search beats one wrong guess.",
378735
+ "",
378736
+ "DON'T WRITE \u2014 noise filter:",
378737
+ " - Anything the Soul Map shows (exports, signatures, file structure) \u2014 duplication you'll regret.",
378738
+ " - Temporary task state ('currently refactoring auth') \u2014 that's working memory.",
378739
+ " - Restatement of code (function exists \u2014 memory is for intent/history).",
378740
+ " - 'We tried X' where X is still the active approach \u2014 only store rejected alternatives.",
378741
+ " - Speculation ('might want to migrate someday') \u2014 only crystallized decisions.",
378742
+ "",
378743
+ "WHY WRITES PAY OFF \u2014 the system multiplies them:",
378744
+ " - Soul Map stable file_id \u2192 memory on src/jwt.ts survives renames/refactors.",
378745
+ " - Co-change graph \u2192 memory on auth/middleware.ts surfaces when editing auth/routes.ts (git pairs them).",
378746
+ " - Blast radius \u2192 memories tied to high-impact files rank higher in recall.",
378747
+ " - Provider embeddings \u2192 'how do we sign tokens' finds memories phrased as 'JWT signing' without a shared word.",
378748
+ " - file_paths is the strongest single signal \u2014 pure path overlap bypasses semantic match. ALWAYS include for file-scoped memories.",
378749
+ "",
378750
+ "ON RECALL CONFLICT \u2014 read injected stubs before acting:",
378751
+ ` - Surfaced memory contradicts what user just asked \u2192 RAISE IT: 'you stored "never npm" on day 3 \u2014 still respect that, or updating?'`,
378752
+ " - Decision is now stale (user changed mind this turn) \u2192 action:'supersede' AFTER writing the new one. Old becomes hidden; audit trail preserved.",
378753
+ "",
378754
+ "ON DUPLICATE HINT \u2014 when write() returns similar_hints:",
378755
+ " - \u226585% cosine \u2192 action:'get' on the hint_id to read existing first.",
378756
+ " - Refinement (same topic, new detail) \u2192 re-write with merge_topics:true.",
378757
+ " - Contradiction \u2192 supersede.",
378758
+ " - Overlapping but distinct (two gotchas about jwt.ts) \u2192 write anyway, both stay.",
378704
378759
  "",
378705
378760
  "Actions:",
378706
- " write \u2014 summary (\u2264200) + details (\u22642000) + category + topics[\u22648] + file_paths[\u226416]. Auto-dedups by content hash; near-duplicates (semantic \u22650.65 OR \u226560% trigram overlap on summary) return similar_hints \u2014 review for contradiction.",
378707
- " search \u2014 semantic + FTS. query + optional limit/scope.",
378708
- " list \u2014 filter by category/topic/pinned/include_hidden.",
378709
- " get \u2014 full record by id (8-char prefix accepted).",
378761
+ " write \u2014 summary (\u2264200) + details (\u22642000) + category + topics[\u22648] + file_paths[\u226416]. Auto-dedups by content hash; near-duplicates (semantic \u22650.65 OR \u226560% trigram overlap on summary) return similar_hints.",
378762
+ " search \u2014 semantic + FTS. query + optional limit/scope.",
378763
+ " list \u2014 filter by category/topic/pinned/include_hidden.",
378764
+ " get \u2014 full record by id (8-char prefix accepted).",
378710
378765
  " pin/unpin \u2014 pinned rows survive cleanup + rank higher in recall.",
378711
- " delete \u2014 soft-delete (restorable via restore).",
378712
- " supersede \u2014 collapse a near-duplicate: pass id (old) + new_id (replacement). Old row is hidden but audit trail kept via superseded_by. Preferred over delete when consolidating duplicates surfaced by similar_hints.",
378766
+ " delete \u2014 soft-delete (restorable via restore). All deletes soft \u2014 recoverable forever.",
378767
+ " supersede \u2014 collapse a near-duplicate: id (old) + new_id (replacement). Old row hidden, audit trail kept via superseded_by. Preferred over delete when consolidating duplicates.",
378768
+ "",
378769
+ "Schema:",
378770
+ " summary \u2014 \u2264200ch present-tense headline ('Use bun for scripts', not 'We should use bun').",
378771
+ " details \u2014 \u22642000ch. The 'because' half of decisions, the 'symptom + fix' half of gotchas. Empty OK for prefs.",
378772
+ " category \u2014 pref | decision | gotcha | context | null (null valid; category is a UI filter, NOT used in recall scoring).",
378773
+ " topics \u2014 \u22648 free-form tags ('auth', 'tooling', 'perf'). Short tags drive trigram fallback when FTS misses.",
378774
+ " file_paths \u2014 \u226416 relative paths. ALWAYS include for file-scoped memories \u2014 strongest recall signal, co-change-aware.",
378775
+ " scope \u2014 'project' (default, .soulforge/memory.db) | 'global' (~/.soulforge/memory.db, cross-project prefs only).",
378776
+ " source \u2014 auto-tagged 'agent' for your writes.",
378777
+ "",
378778
+ "Examples \u2014 write these shapes:",
378779
+ " write category:'pref' summary:'Be terse, fragments over sentences' topics:['style'] scope:'global'",
378780
+ " write category:'decision' summary:'Use zustand, not redux \u2014 boilerplate' details:'Tried redux for the auth store, too much ceremony for 4 actions. Switched 2024-11-12. Re-eval if state grows past ~20 slices.' topics:['state','tooling'] file_paths:['src/stores']",
378781
+ " write category:'gotcha' summary:'JWT expiry uses container clock' details:'Container drifts ~3min/day, breaks token validation. Fix at jwt.ts:47 \u2014 use ntp-synced epoch.' topics:['auth','prod-bug'] file_paths:['src/jwt.ts']",
378782
+ " supersede id:'a4d9feaa' new_id:'47daae64'",
378783
+ " search query:'how do we sign tokens' limit:5",
378713
378784
  "",
378714
- "Scopes: 'project' (write default, .soulforge/memory.db) vs 'global' (~/.soulforge/memory.db). Read default 'all'."
378785
+ "DEFENSIVE GUARANTEES (so you can write freely):",
378786
+ " - Hard caps: \u22643 surfaced per turn, \u22642400 chars total. A bad write won't blow your context.",
378787
+ " - Soft-delete only \u2014 user can undo any cleanup.",
378788
+ " - Auto-recall is deterministic + cached \u2014 same prompt + same edited files = same surfaced set.",
378789
+ " - No auto-extraction from your turns. Memory only contains what you explicitly wrote."
378715
378790
  ].join(`
378716
378791
  `),
378717
378792
  inputSchema: exports_external.object({
@@ -380060,7 +380135,7 @@ var init_multi_edit = __esm(() => {
380060
380135
  init_ts_project_detect();
380061
380136
  multiEditTool = {
380062
380137
  name: "multi_edit",
380063
- description: "Apply multiple edits to a single non-TS/JS file atomically (JSON, YAML, Markdown, config, raw text). For TS/JS files use ast_edit with operations:[...] \u2014 safer and no line drift. " + "All-or-nothing: if any edit fails, ZERO edits are applied. " + "lineStart values reference the ORIGINAL file (pre-edit) \u2014 the tool tracks cumulative line offsets internally. " + "Provide lineStart (1-indexed) for reliable line-anchored matching. Without it, falls back to string matching against evolved content. " + "The range is derived from oldString line count.",
380138
+ description: "Apply multiple edits to a single non-TS/JS file atomically (JSON, YAML, Markdown, config, raw text). For TS/JS files use ast_edit with operations:[...] \u2014 safer and no line drift. " + "All-or-nothing: if any edit fails, ZERO edits are applied. lineStart values reference the ORIGINAL file (pre-edit) \u2014 the tool tracks cumulative line offsets internally. " + "Provide lineStart (1-indexed) for reliable line-anchored matching. Without it, falls back to string matching against evolved content. The range is derived from oldString line count. " + "Each oldString is matched against the ORIGINAL file content, not against the result of earlier edits in the batch \u2014 do not emit overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into ONE edit. " + "Keep each oldString minimal and unique. Don't pad with large unchanged regions just to span distant changes. " + "If the call atomically rolls back, re-read the file and retry ALL edits with fresh content.",
380064
380139
  execute: async (args2) => {
380065
380140
  try {
380066
380141
  const filePath = resolve24(args2.path);
@@ -381180,8 +381255,10 @@ async function readViaWorker(filePath, args2) {
381180
381255
  let output = result.numbered;
381181
381256
  if (result.truncated) {
381182
381257
  const outline = await buildSymbolOutline(filePath, result.start + MAX_READ_LINES, result.totalLines);
381258
+ const nextOffset = result.start + MAX_READ_LINES + 1;
381259
+ const remaining = result.totalLines - result.start - MAX_READ_LINES;
381183
381260
  output += outline || `
381184
- ... ${String(result.totalLines - result.start - MAX_READ_LINES)} more lines.`;
381261
+ ... ${String(remaining)} more lines. Use ranges:[{start:${String(nextOffset)}, end:N}] to continue.`;
381185
381262
  }
381186
381263
  return { success: true, output };
381187
381264
  }
@@ -382551,7 +382628,7 @@ async function runPreCommitChecks(cwd2) {
382551
382628
  cwd: cwd2,
382552
382629
  timeout: 15000,
382553
382630
  env: buildSafeEnv(),
382554
- stdio: SAFE_STDIO
382631
+ ...SAFE_SPAWN_OPTS
382555
382632
  });
382556
382633
  proc.stdout?.on("data", (d) => {
382557
382634
  lintBytes += d.length;
@@ -382788,7 +382865,7 @@ var init_shell = __esm(() => {
382788
382865
  cwd: cwd2,
382789
382866
  timeout,
382790
382867
  env: buildSafeEnv(),
382791
- stdio: SAFE_STDIO
382868
+ ...SAFE_SPAWN_OPTS
382792
382869
  });
382793
382870
  let cleanupAbortListener;
382794
382871
  if (abortSignal) {
@@ -386650,6 +386727,74 @@ var init_linkify_it = __esm(() => {
386650
386727
  linkify_it_default = LinkifyIt;
386651
386728
  });
386652
386729
 
386730
+ // src/stores/model-events.ts
386731
+ function recordModelCall(event) {
386732
+ try {
386733
+ const s = useModelEventsStore.getState();
386734
+ if (!s.enabled)
386735
+ return;
386736
+ s.push(event);
386737
+ } catch {}
386738
+ }
386739
+ function aggregateModelEvents(events) {
386740
+ const byModel = new Map;
386741
+ for (const ev of events) {
386742
+ const prev = byModel.get(ev.modelId) ?? {
386743
+ modelId: ev.modelId,
386744
+ calls: 0,
386745
+ errors: 0,
386746
+ totalMs: 0,
386747
+ avgMs: 0,
386748
+ lastMs: 0,
386749
+ input: 0,
386750
+ output: 0,
386751
+ cacheRead: 0,
386752
+ cacheWrite: 0,
386753
+ lastAt: 0
386754
+ };
386755
+ prev.calls += 1;
386756
+ if (ev.state === "error")
386757
+ prev.errors += 1;
386758
+ prev.totalMs += ev.durationMs;
386759
+ prev.lastMs = ev.durationMs;
386760
+ prev.input += ev.input ?? 0;
386761
+ prev.output += ev.output ?? 0;
386762
+ prev.cacheRead += ev.cacheRead ?? 0;
386763
+ prev.cacheWrite += ev.cacheWrite ?? 0;
386764
+ prev.lastAt = Math.max(prev.lastAt, ev.startedAt + ev.durationMs);
386765
+ byModel.set(ev.modelId, prev);
386766
+ }
386767
+ for (const agg of byModel.values()) {
386768
+ agg.avgMs = agg.calls > 0 ? Math.round(agg.totalMs / agg.calls) : 0;
386769
+ }
386770
+ return [...byModel.values()].sort((a, b) => b.lastAt - a.lastAt);
386771
+ }
386772
+ function modelErrorEvents(events) {
386773
+ return events.filter((e) => e.state === "error");
386774
+ }
386775
+ var MAX_EVENTS = 500, useModelEventsStore;
386776
+ var init_model_events = __esm(() => {
386777
+ init_esm();
386778
+ init_middleware();
386779
+ useModelEventsStore = create()(subscribeWithSelector((set3, get) => ({
386780
+ enabled: false,
386781
+ events: [],
386782
+ setEnabled: (v) => set3(v ? { enabled: true } : { enabled: false, events: [] }),
386783
+ push: (event) => {
386784
+ if (!get().enabled)
386785
+ return null;
386786
+ const id = event.id ?? crypto.randomUUID();
386787
+ const full = { ...event, id };
386788
+ set3((s) => {
386789
+ const events = s.events.length >= MAX_EVENTS ? [...s.events.slice(-(MAX_EVENTS - 1)), full] : [...s.events, full];
386790
+ return { events };
386791
+ });
386792
+ return id;
386793
+ },
386794
+ clear: () => set3({ events: [] })
386795
+ })));
386796
+ });
386797
+
386653
386798
  // src/core/agents/subagent-events.ts
386654
386799
  function emitSubagentStep(step) {
386655
386800
  for (const fn of stepListeners)
@@ -387616,7 +387761,7 @@ var init_stream_options = __esm(() => {
387616
387761
 
387617
387762
  // src/core/agents/web-search.ts
387618
387763
  function createWebSearchAgent(model, opts) {
387619
- const { maxRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry, {
387764
+ const { maxTransientRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry, {
387620
387765
  agent: true
387621
387766
  });
387622
387767
  return new ToolLoopAgent({
@@ -387767,6 +387912,7 @@ function buildWebSearchTool(opts) {
387767
387912
  }
387768
387913
  runningSteps.clear();
387769
387914
  };
387915
+ const webSearchStartedAt = Date.now();
387770
387916
  try {
387771
387917
  const agent2 = createWebSearchAgent(webSearchModel, {
387772
387918
  onApproveFetchPage: opts?.onApproveFetchPage
@@ -387809,10 +387955,25 @@ function buildWebSearchTool(opts) {
387809
387955
  }
387810
387956
  });
387811
387957
  agentSearchCache.set(cacheKey, { output: result.text, ts: Date.now() });
387958
+ recordModelCall({
387959
+ modelId: mid,
387960
+ source: "other",
387961
+ startedAt: webSearchStartedAt,
387962
+ durationMs: Math.max(0, Date.now() - webSearchStartedAt),
387963
+ state: "ok"
387964
+ });
387812
387965
  return { success: true, output: result.text, backend: backendLabel };
387813
387966
  } catch (err2) {
387814
387967
  markRunningStepsError();
387815
387968
  const msg = err2 instanceof Error ? err2.message : String(err2);
387969
+ recordModelCall({
387970
+ modelId: mid,
387971
+ source: "other",
387972
+ startedAt: webSearchStartedAt,
387973
+ durationMs: Math.max(0, Date.now() - webSearchStartedAt),
387974
+ state: "error",
387975
+ errorMessage: msg.slice(0, 500)
387976
+ });
387816
387977
  const urlHint = extractUrlHint(args2.query);
387817
387978
  const fallback = urlHint ? ` Try fetch_page("${urlHint}") to access the resource directly.` : " If you know a specific URL (docs page, npm package, GitHub repo), use fetch_page on that URL directly instead of searching.";
387818
387979
  return {
@@ -387832,6 +387993,7 @@ var init_web_search2 = __esm(() => {
387832
387993
  init_dist5();
387833
387994
  init_linkify_it();
387834
387995
  init_zod();
387996
+ init_model_events();
387835
387997
  init_subagent_events();
387836
387998
  init_web_search();
387837
387999
  init_models();
@@ -390643,7 +390805,7 @@ function createCodeAgent(model, options) {
390643
390805
  disablePruning: options?.disablePruning,
390644
390806
  tabId: options?.tabId
390645
390807
  });
390646
- const { maxRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry, {
390808
+ const { maxTransientRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry, {
390647
390809
  agent: true
390648
390810
  });
390649
390811
  return new ToolLoopAgent({
@@ -390752,7 +390914,7 @@ function createExploreAgent(model, options) {
390752
390914
  disablePruning: options?.disablePruning,
390753
390915
  tabId: options?.tabId
390754
390916
  });
390755
- const { maxRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry, {
390917
+ const { maxTransientRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry, {
390756
390918
  agent: true
390757
390919
  });
390758
390920
  return new ToolLoopAgent({
@@ -395160,7 +395322,8 @@ ${enrichedPrompt}`;
395160
395322
  let lastError2;
395161
395323
  let attemptsMade = 0;
395162
395324
  let proxyBounced = false;
395163
- const { maxRetries: MAX_RETRIES, baseDelayMs: BASE_DELAY_MS } = resolveRetrySettings(loadConfig().retry, { agent: true });
395325
+ let lastAttemptStartedAt = Date.now();
395326
+ const { maxTransientRetries: MAX_RETRIES, baseDelayMs: BASE_DELAY_MS } = resolveRetrySettings(loadConfig().retry, { agent: true });
395164
395327
  for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
395165
395328
  if (abortSignal?.aborted)
395166
395329
  break;
@@ -395174,6 +395337,8 @@ ${enrichedPrompt}`;
395174
395337
  attemptsMade = attempt + 1;
395175
395338
  const { agent: agent2 } = await createAgent(task, models, bus, parentToolCallId);
395176
395339
  const callbacks = buildStepCallbacks(parentToolCallId, task.agentId, selectedModelId);
395340
+ const attemptStartedAt = Date.now();
395341
+ lastAttemptStartedAt = attemptStartedAt;
395177
395342
  let result;
395178
395343
  try {
395179
395344
  const generateArgs = isDoppelganger ? {
@@ -395386,6 +395551,18 @@ ${footer}` : doneResult2.summary;
395386
395551
  tabId: task.tabId
395387
395552
  });
395388
395553
  }
395554
+ recordModelCall({
395555
+ modelId: selectedModelId,
395556
+ source: "subagent",
395557
+ startedAt: attemptStartedAt,
395558
+ durationMs: Math.max(0, Date.now() - attemptStartedAt),
395559
+ state: "ok",
395560
+ tabId: task.tabId,
395561
+ agentId: task.agentId,
395562
+ input,
395563
+ output,
395564
+ cacheRead
395565
+ });
395389
395566
  return { doneResult: doneResult2, resultText, callbacks, result: agentResult2 };
395390
395567
  } catch (error48) {
395391
395568
  lastError2 = error48;
@@ -395451,6 +395628,16 @@ ${footer}` : doneResult2.summary;
395451
395628
  });
395452
395629
  }
395453
395630
  const doneResult = salvaged ? { summary: errorResultText } : null;
395631
+ recordModelCall({
395632
+ modelId: selectedModelId,
395633
+ source: "subagent",
395634
+ startedAt: lastAttemptStartedAt,
395635
+ durationMs: Math.max(0, Date.now() - lastAttemptStartedAt),
395636
+ state: "error",
395637
+ tabId: task.tabId,
395638
+ agentId: task.agentId,
395639
+ errorMessage: errMsg.slice(0, 500)
395640
+ });
395454
395641
  return {
395455
395642
  doneResult,
395456
395643
  resultText: errorResultText,
@@ -395473,6 +395660,7 @@ var init_agent_runner = __esm(() => {
395473
395660
  init_subagent_tools();
395474
395661
  init_config2();
395475
395662
  init_settings();
395663
+ init_model_events();
395476
395664
  init_tool_timeout();
395477
395665
  RETURN_FORMAT_INSTRUCTIONS = {
395478
395666
  summary: "Return concise findings and reasoning. No code blocks or raw file content. " + "Focus on what you found, what it means, and what the implications are. " + "Anchor every claim with file:line so the parent can surgically read more.",
@@ -395793,7 +395981,7 @@ async function createAgent(task, models, bus, parentToolCallId) {
395793
395981
  onApproveFetchPage: models.onApproveFetchPage,
395794
395982
  repoMap: models.repoMap,
395795
395983
  contextWindow,
395796
- disablePruning: models.disablePruning,
395984
+ disablePruning: useSpark ? true : models.disablePruning,
395797
395985
  tabId: models.tabId,
395798
395986
  forgeInstructions,
395799
395987
  forgeTools: forgeToolsGuarded,
@@ -397010,7 +397198,7 @@ function createForgeAgent({
397010
397198
  max_tokens: MAX_OUTPUT_TOKENS
397011
397199
  }
397012
397200
  };
397013
- const { maxRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry);
397201
+ const { maxTransientRetries: retryMaxRetries } = resolveRetrySettings(loadConfig().retry);
397014
397202
  return new ToolLoopAgent({
397015
397203
  id: "forge",
397016
397204
  model,
@@ -410959,112 +411147,31 @@ var init_soul_map = __esm(() => {
410959
411147
 
410960
411148
  // src/core/prompts/shared/tool-guidance.ts
410961
411149
  var TOOL_GUIDANCE_WITH_MAP = `<tool_usage>
410962
- A Soul Map is loaded in context \u2014 every file, exported symbol, signature, line number, and dependency edge. It is your first source of truth; tools retrieve just-in-time what the map doesn't already answer.
411150
+ A Soul Map is loaded in context \u2014 every file, exported symbol, signature, line number, dependency edge. It is your first source of truth; tools retrieve just-in-time what the map doesn't already answer.
410963
411151
 
410964
411152
  <workflow>
410965
- 1. PLAN from the Soul Map \u2014 identify files, symbols, blast radius. Zero tool calls.
410966
- 2. DISCOVER with parallel soul_find / soul_grep / navigate \u2014 only when the map doesn't answer.
410967
- 3. READ in one parallel batch using Soul Map line numbers for precise ranges.
410968
- 4. EDIT with ast_edit for TS/JS, multi_edit otherwise.
410969
- 5. VERIFY with project (typecheck/lint/test).
410970
- Commit to the plan. Don't re-read or re-search what you already have.
411153
+ PLAN from the map (zero tool calls) \u2192 DISCOVER in parallel (soul_find/soul_grep/navigate) only when the map doesn't answer \u2192 READ in one parallel batch with Soul Map line numbers \u2192 EDIT (ast_edit for TS/JS, multi_edit otherwise) \u2192 VERIFY with project (typecheck/lint/test). Commit to the plan. Don't re-read or re-search what you have.
410971
411154
  </workflow>
410972
411155
 
410973
411156
  <soul_map_usage>
410974
- The map answers most structural questions for free:
410975
- - "Where is X?" \u2192 file and line in the map.
410976
- - "What does Y export?" \u2192 listed under that file.
410977
- - "What depends on Z?" \u2192 (\u2192N) blast radius and \u2190 arrows.
410978
- - "What packages?" \u2192 Key dependencies section.
410979
- Feed symbol names from the map into navigate/analyze for details. The map gives names; LSP gives bodies.
411157
+ The map answers structural questions for free: "Where is X?" \u2192 file + line. "What does Y export?" \u2192 listed under the file. "What depends on Z?" \u2192 (\u2192N) blast radius + \u2190 arrows. "What packages?" \u2192 Key dependencies section. Feed symbol names into navigate/analyze for bodies.
410980
411158
  </soul_map_usage>
410981
411159
 
410982
411160
  <tool_selection>
410983
411161
  - Soul Map first \u2192 then TIER-1 (soul_find, soul_grep, navigate, soul_impact, read, ast_edit, multi_edit, project). Drop to TIER-2/3 only when TIER-1 cannot answer.
410984
- - \`navigate\` auto-resolves files from symbol names \u2014 definitions, references, call hierarchies, type hierarchies. Reaches into \`.d.ts\` / stubs / headers, so you get type info without reading \`node_modules\`.
411162
+ - \`navigate\` auto-resolves files from symbol names \u2014 definitions, references, call hierarchies, type hierarchies. Reaches into \`.d.ts\` / stubs / headers (type info without reading node_modules).
410985
411163
  - \`soul_grep\` \`dep\` param searches inside dependencies (e.g. \`dep="react"\`). Any language/package manager.
410986
- - \`soul_impact\` queries: \`dependents\` (who imports this), \`dependencies\` (what this imports), \`cochanges\` (git history \u2014 files edited together), \`blast_radius\` (total scope). Before editing a file with (\u2192N) > 10, call \`soul_impact(cochanges)\` and update the co-changed files too.
411164
+ - \`soul_impact\` queries: \`dependents\`, \`dependencies\`, \`cochanges\` (git pairs), \`blast_radius\`. Before editing a file with (\u2192N) > 10, call \`soul_impact(cochanges)\` and update the co-changed files too.
410987
411165
  - Batch independent tool calls in one parallel block.
410988
- - \`git\` tool for git operations \u2014 not shell. Multi-line messages go in \`body\`/\`footer\`.
410989
- - \`soul_vision\` for any image/video path or URL (user is on a CLI).
411166
+ - \`git\` for git ops (not shell). Multi-line messages \u2192 \`body\`/\`footer\`. \`soul_vision\` for any image/video path or URL.
410990
411167
  </tool_selection>
410991
411168
 
410992
411169
  <reads>
410993
- \`read(files=[{path:'x.ts', ranges:[{start:45,end:80}]}])\`. Batch many files in one call. Use Soul Map line numbers \u2014 they are accurate. For AST extraction: \`{path, target:'function', name:'foo'}\`. Skip re-reads.
411170
+ \`read(files=[{path:'x.ts', ranges:[{start:45,end:80}]}])\`. Batch many files in one call. Soul Map line numbers are accurate. AST extraction: \`{path, target:'function', name:'foo'}\`. Skip re-reads.
410994
411171
  </reads>
410995
411172
 
410996
411173
  <ast_edit>
410997
- \`ast_edit\` is the default editor for .ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs \u2014 used BEFORE edit_file/multi_edit, not as fallback. ts-morph locates symbols by {target, name}: no oldString, no whitespace/escape failures, no line-offset drift. Pairs directly with the Soul Map \u2014 every symbol name and kind is already in context.
410998
-
410999
- CAN DO (no fallback needed \u2014 ast_edit handles these, don't switch to edit_file):
411000
- - Any named symbol: function, class, interface, type, enum, variable, method, property, constructor, arrow-const. Class members: \`ClassName.memberName\` or just \`memberName\`.
411001
- - JSX/TSX bodies with Unicode, special chars (\u251C \u2190 \u2192 etc.), escape sequences, quotes. ts-morph wraps the TS compiler \u2014 no limitation there.
411002
- - Large rewrites: use \`replace\` (whole symbol, full declaration with braces) or anchor-pair \`replace_in_body\` (value=<short start anchor> + valueEnd=<short end anchor>) \u2014 rewrites a 100-line block with ~20 tokens of anchors.
411003
- - Whitespace drift: \`replace_in_body\` auto-handles tab\u2194space, CRLF\u2194LF, trailing whitespace, and common-indent stripping. Paste from a Read and it matches.
411004
- - Atomic multi-op: \`operations: [{...}, {...}]\` applies all-or-nothing on one file. Use this for "add import + use it" in a single call.
411005
- - File creation: \`action:"create_file", newCode:<full content>\`.
411006
-
411007
- CANNOT TARGET (use the escape hatch, not edit_file):
411008
- - Anonymous callbacks (inline arrows, IIFEs, object-literal methods without names). \u2192 Use \`replace_in_body\` on the enclosing NAMED symbol.
411009
- - Union members inside a type alias. \u2192 Use \`replace\` on the whole type.
411010
- - Raw text inside comments or string literals that aren't bound to a symbol. \u2192 Use \`replace_in_body\` on the enclosing symbol.
411011
-
411012
- ONLY FALL BACK TO edit_file WHEN:
411013
- - File is not TS/JS (JSON, YAML, Markdown, config, raw text).
411014
- - Edit is entirely outside any named symbol (e.g. top-of-file banner comment not attached to a declaration).
411015
- - File has a parse error that breaks ts-morph (try \`ast_edit\` first; if it fails with a parse error, then edit_file). "Long needle" or "JSX special chars" are NOT fallback reasons \u2014 those work fine.
411016
-
411017
- Tiers (pick the smallest that does the job):
411018
- - MICRO (1-10 tokens): set_type, set_return_type, set_async, set_export, rename, remove, set_initializer, add_parameter, set_optional.
411019
- - BODY (10-100): set_body, add_statement, add_property, add_method, add_constructor, add_decorator, set_extends, add_implements, replace_in_body.
411020
- - FULL: replace (whole symbol), create_file (new file with \`newCode=<full file content>\`).
411021
- - FILE-LEVEL: add_import, add_named_import (idempotent \u2014 merges), organize_imports, fix_missing_imports, add_function, add_class, add_interface, add_type_alias, add_enum, insert_text (requires anchor: index=0|-1 or value="after-imports"|"before-exports").
411022
- - ATOMIC MULTI-OP: \`operations: [{...}, {...}]\` \u2014 all-or-nothing rollback, single file.
411023
-
411024
- Targets: function | class | interface | type | enum | variable | method | property | constructor | arrow_function. For \`const foo = async (\u2026) => {\u2026}\` use target:"arrow_function" + name:"foo".
411025
-
411026
- Body shape \u2014 critical, get this wrong and you corrupt the file:
411027
- - \`set_body\` / \`add_statement\` / \`insert_statement\`: newCode is body CONTENTS ONLY \u2014 no surrounding \`{}\`. ts-morph wraps it. Passing \`{ \u2026 }\` produces \`{ { \u2026 } }\`.
411028
- - \`add_method\` / \`add_constructor\` / \`add_getter\` / \`add_setter\`: newCode is the FULL declaration including braces (e.g. \`foo(x: number) { return x + 1; }\`).
411029
- - \`replace\`: newCode is the WHOLE symbol text including its braces (full declaration).
411030
- - \`add_property\` on interface: newCode is \`"name: type"\` or \`"name?: type"\`. On class: \`"name: type = value"\` or \`"name = value"\`.
411031
- - \`add_statement\` on expression-body arrow (\`(x) => x + 1\`) auto-wraps into a block \u2014 safe to call.
411032
-
411033
- replace_in_body shapes (pick the smallest):
411034
- - SHORT ANCHOR: value=<1-2 unique lines>, newCode=<replacement>. Fastest, most token-efficient.
411035
- - ANCHOR PAIR (RANGE): value=<short start anchor> + valueEnd=<short end anchor> + newCode=<replacement for the span>. Use for big rewrites \u2014 ~20 tokens replaces 100 lines.
411036
- - Large single \`value\` (whole block) WORKS but wastes tokens \u2014 prefer \`replace\` on the whole symbol, or anchor pair.
411037
- - Exact-match ambiguity (\u22652 identical hits) THROWS \u2014 add more surrounding context or use anchor pair.
411038
-
411039
- \`rename\` is declaration-only by default (safe). Use \`rename_global\` for project-wide propagation \u2014 or \`rename_symbol\` / \`move_symbol\` / \`rename_file\` for cross-file refactors.
411040
-
411041
- Examples:
411042
- // MICRO \u2014 flip a method async + set return type, one call
411043
- ast_edit(path, operations: [
411044
- { action:"set_async", target:"method", name:"UserStore.load", value:"true" },
411045
- { action:"set_return_type", target:"method", name:"UserStore.load", value:"Promise<User>" }
411046
- ])
411047
-
411048
- // BODY \u2014 add a statement inside a function
411049
- ast_edit(path, action:"add_statement", target:"function", name:"loadConfig",
411050
- newCode:"logger.info('config loaded', { keys: Object.keys(config) });")
411051
-
411052
- // ANCHOR PAIR \u2014 rewrite a 100-line JSX block with ~20 tokens
411053
- ast_edit(path, action:"replace_in_body", target:"function", name:"ProviderSettings",
411054
- value:"const caption = (",
411055
- valueEnd:"</PremiumPopup>",
411056
- newCode:"<new JSX here>")
411057
-
411058
- // ATOMIC \u2014 add import, then add a method that uses it
411059
- ast_edit(path, operations: [
411060
- { action:"add_named_import", value:"zod", newCode:"z" },
411061
- { action:"add_method", target:"class", name:"Validator",
411062
- newCode:"validate(input: unknown) { return z.string().parse(input); }" }
411063
- ])
411064
-
411065
- // CREATE \u2014 new file
411066
- ast_edit("src/foo.ts", action:"create_file",
411067
- newCode:"export function foo() { return 42; }\\n")
411174
+ \`ast_edit\` is the default editor for .ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs \u2014 pairs directly with the Soul Map (every symbol name + kind is in context). See the tool's description for the full operation taxonomy, body-shape rules, replace_in_body anchor shapes, and examples. Use it BEFORE edit_file/multi_edit.
411068
411175
  </ast_edit>
411069
411176
 
411070
411177
  <non_ts_edits>
@@ -411072,87 +411179,15 @@ For non-TS/JS files (JSON, YAML, Markdown, config) or raw text outside any symbo
411072
411179
  </non_ts_edits>
411073
411180
 
411074
411181
  <memory>
411075
- \`memory\` is your across-session brain. The Soul Map tells you what the code IS; memory tells you WHY it got that way. Use it like ast_edit \u2014 by default, not as last resort. Every write earns its keep on a future session when one ambiguous sentence ("add a new script") triggers the right recall and you skip a round-trip of "what's our convention here?"
411076
-
411077
- Recall fires automatically before each user turn \u2014 prompt + edited files \u2192 top-3 relevant memories injected as <recalled_memories> stubs (summary + id + signals + "\u21B3 has details" marker), \u2264600 chars typical. Cached, deduped, never re-injected in one session. When a stub's "\u21B3 has details" marker matters to the current task, call memory(action:"get", id:<8-char prefix>) to read the full body.
411078
-
411079
- Auto-recall is signal-driven and misses generic or single-word prompts. Before any action where convention matters and nothing was surfaced \u2014 first commit of a session, adopting a framework/tool, changing config layout, writing tests in an unfamiliar area, picking a naming style \u2014 run memory(action:"search", query:<topic>) once. Cheap, deterministic, beats guessing.
411080
-
411081
- SEARCH KEYWORDS \u2014 fall back to memory(search) when about to do any of these and recall was empty: commit message shape, lint/format choice, test framework conventions, package manager (bun/npm/pnpm/yarn), import style, file naming, error-handling pattern, logger choice, state-management library, dispatch/agent setup, prompt-engineering rules. One search beats one wrong guess.
411082
-
411083
- WRITE proactively, SEARCH when convention matters and nothing was recalled.
411084
-
411085
- WHY WRITES MATTER \u2014 the system multiplies them:
411086
- - Soul Map stable file_id \u2192 memory on \`src/jwt.ts\` survives renames and refactors.
411087
- - Co-change graph \u2192 memory on \`auth/middleware.ts\` surfaces when editing \`auth/routes.ts\` because git history pairs them.
411088
- - Blast radius \u2192 memories tied to high-impact files rank higher in recall.
411089
- - Provider embeddings \u2192 "how do we sign tokens" finds memories phrased as "JWT signing" without a single shared word.
411090
- - file_paths is the strongest single signal \u2014 pure path overlap bypasses semantic match. Always include it for file-scoped memories.
411091
-
411092
- WHEN TO WRITE \u2014 the three triggers (fire on ANY of these, not just user-prompted ones):
411093
- 1. USER STATES A PREFERENCE OR DIRECTIVE. "use bun not npm", "be terse", "always run tests after edits" \u2192 pref. Write immediately, scope:"global" if it's not project-specific.
411094
- INFER FROM CUES \u2014 don't wait for the word "remember". A preference exists whenever the user's correction or instruction implies a standing rule, not a one-shot fix. Cues that mean "this is durable":
411095
- \u2022 Corrective tone about HOW you did something ("be more concise", "stop narrating", "use bullets") \u2014 the correction itself IS the rule.
411096
- \u2022 Generalising language: "always", "never", "from now on", "by default", "prefer", "in this repo we\u2026", "we don't\u2026".
411097
- \u2022 Imperative meta-instructions about workflow, style, tooling, formatting, naming \u2014 anything orthogonal to the current task.
411098
- \u2022 User repeats or rephrases the same correction \u2192 it's a rule you missed last time. Write it now.
411099
- \u2022 User asks "why didn't you\u2026?" about a behavior \u2014 they had an expectation. Capture the expectation.
411100
- The literal word "remember" is just one cue among many. The test: "Would future-me want this surfaced next session?" If yes \u2192 write. Mid-instruction corrections ("commit it, and be concise") split into two acts: do the task, write the rule.
411101
- 2. A CHOICE GETS MADE WITH A REASON. "switching to zustand because redux is too much boilerplate", "postgres not mysql for the JSON ops" \u2192 decision. The WHY is what future you needs (the Soul Map shows the WHAT). Capture the rationale in details.
411102
- 3. SHARP-EDGE DISCOVERED. Bug that took >5min to diagnose, non-obvious quirk, "don't touch X because Y", a workaround for a flaky test \u2192 gotcha. Include the symptom + the fix location.
411103
-
411104
- Examples \u2014 write these shapes:
411105
- memory(action:"write", category:"pref", summary:"Be terse, fragments over sentences", topics:["style"], scope:"global")
411106
- memory(action:"write", category:"decision", summary:"Use zustand, not redux \u2014 boilerplate", details:"Tried redux for the auth store, too much ceremony for 4 actions. Switched 2024-11-12. Re-eval if state grows past ~20 slices.", topics:["state","tooling"], file_paths:["src/stores"])
411107
- memory(action:"write", category:"gotcha", summary:"JWT expiry uses container clock", details:"Container drifts ~3min/day, breaks token validation. Fix at jwt.ts:47 \u2014 use ntp-synced epoch.", topics:["auth","prod-bug"], file_paths:["src/jwt.ts"])
411108
- memory(action:"supersede", id:"a4d9feaa", new_id:"47daae64")
411109
- memory(action:"search", query:"how do we sign tokens", limit:5)
411110
-
411111
- WHEN NOT TO WRITE \u2014 the noise filter:
411112
- - temporary task state ("currently refactoring auth") \u2014 that's working memory, not durable.
411113
- - anything the Soul Map shows (exports, signatures, file structure) \u2014 duplication you'll regret.
411114
- - restatement of code (the function exists \u2014 memory is for intent/history).
411115
- - "we tried X" where X is still the active approach \u2014 only store rejected alternatives.
411116
- - speculation ("might want to migrate someday") \u2014 only crystallized decisions.
411117
-
411118
- ON RECALL CONFLICT \u2014 read injected memories before acting:
411119
- - if a surfaced memory contradicts what the user just asked, RAISE IT: "you stored 'never npm' on day 3 \u2014 still respect that, or updating?"
411120
- - if a decision is now stale (user changed their mind this turn), call memory(action:"supersede", id:<old>, new_id:<new>) AFTER writing the new one. Old becomes hidden; audit trail preserved.
411121
-
411122
- ON DUPLICATE HINT \u2014 when write() returns similar_hints:
411123
- - \u226585% cosine \u2192 memory(action:"get", id:<hint_id>) to read the existing entry first.
411124
- - refinement (same topic, new detail): re-write with merge_topics:true.
411125
- - contradiction: supersede.
411126
- - overlapping but distinct (two gotchas about jwt.ts): write anyway, both stay.
411127
-
411128
- Schema:
411129
- - summary \u2264200ch \u2014 present-tense headline ("Use bun for scripts" not "We should use bun").
411130
- - details \u22642000ch \u2014 the "because" half of decisions, the "symptom + fix" half of gotchas. Empty is OK for prefs.
411131
- - category pref | decision | gotcha | context | null (null valid; category is a UI filter, NOT used in recall scoring).
411132
- - topics \u22648 free-form tags ("auth", "tooling", "perf"). Short tags drive trigram fallback when FTS misses.
411133
- - file_paths \u226416 relative paths. ALWAYS include for file-scoped memories \u2014 strongest recall signal, co-change-aware.
411134
- - scope "project" (default, .soulforge/memory.db) | "global" (~/.soulforge/memory.db, cross-project prefs only).
411135
- - source auto-tagged "agent" for your writes.
411136
-
411137
- Actions: write | search | list | get | supersede | pin | unpin | delete | restore. All soft \u2014 no hard delete, recoverable forever.
411138
-
411139
- DEFENSIVE GUARANTEES (so you can write freely):
411140
- - Hard caps: \u22643 surfaced per turn, \u22642400 chars total. A bad write won't blow your context.
411141
- - Soft-delete only \u2014 user can undo any cleanup.
411142
- - Auto-recall is deterministic + cached \u2014 same prompt + same edited files = same surfaced set.
411143
- - No auto-extraction from your turns. Memory only contains what you explicitly wrote.
411182
+ \`memory\` is your across-session brain. Auto-recall fires before each user turn \u2014 relevant memories arrive as <recalled_memories> stubs. Use it like a primary tool. Triggers \u2014 fire on ANY:
411183
+ - USER STATES A PREFERENCE/DIRECTIVE \u2192 pref. Infer from cues, don't wait for "remember": corrective tone about HOW you worked, generalising language ("always/never/by default/we don't"), repeated corrections, "why didn't you\u2026?" questions. Mid-instruction corrections split: do the task, write the rule.
411184
+ - CHOICE WITH RATIONALE \u2192 decision. Capture the WHY in details.
411185
+ - SHARP EDGE that took effort to find \u2192 gotcha. Include symptom + fix location.
411186
+ SEARCH when about to commit, pick a framework/lib, name a file, or apply any convention and recall was empty. Always set \`file_paths\` for file-scoped memories \u2014 strongest recall signal. On recall conflict with the current request, raise it before acting. See the tool's description for full schema, examples, similar_hints flow, and defensive caps.
411144
411187
  </memory>
411145
411188
 
411146
411189
  <dispatch>
411147
- Agents have limited context. YOU are the brain \u2014 they are the hands. Pre-digest every task:
411148
- - Look up files/symbols in the Soul Map BEFORE dispatching. Give exact paths, line ranges, symbol names.
411149
- - Write directives, not research briefs.
411150
- BAD: "Find how cost reporting works."
411151
- GOOD: "Read \`statusbar.ts:119-155\` (\`computeCost\`) and \`TokenDisplay.tsx:28-71\`. Report: how tokens map to dollars, what triggers re-render."
411152
- - Tell agents which tools to use: "soul_impact(dependents) on statusbar.ts, then navigate(references) on computeCost."
411153
- - Don't dispatch single-topic questions \u2014 answer from the Soul Map + 1-2 reads yourself. Dispatch is for parallel multi-file work.
411154
- - Each task is self-contained \u2014 the agent can't see your conversation.
411155
- - State what you ALREADY KNOW and what you NEED. Ask for specifics, not file summaries.
411190
+ Agents have limited context. YOU pre-digest: look up files/symbols in the Soul Map BEFORE dispatching, give exact paths + line ranges + symbol names + which tools to use. Write directives, not research briefs (BAD: "Find how cost reporting works." GOOD: "Read \`statusbar.ts:119-155\` (\`computeCost\`) + \`TokenDisplay.tsx:28-71\`. Report: how tokens map to dollars, what triggers re-render."). Each task is self-contained \u2014 agent can't see your conversation. State what you KNOW and what you NEED. Don't dispatch single-topic questions \u2014 answer from the map + 1-2 reads yourself. Dispatch is for parallel multi-file work.
411156
411191
  </dispatch>
411157
411192
  </tool_usage>`, TOOL_GUIDANCE_NO_MAP = `<tool_usage>
411158
411193
  Use dedicated tools over shell for file reads, searches, definitions, and edits.
@@ -411936,6 +411971,7 @@ var DEFAULT_CONTEXT_WINDOW2 = 200000, ContextManager;
411936
411971
  var init_manager5 = __esm(() => {
411937
411972
  init_dist5();
411938
411973
  init_errors();
411974
+ init_model_events();
411939
411975
  init_repomap();
411940
411976
  init_neovim();
411941
411977
  init_instance2();
@@ -412697,6 +412733,7 @@ ${s.signature ? `${s.signature}
412697
412733
  }).join(`
412698
412734
 
412699
412735
  `);
412736
+ const semStartedAt = Date.now();
412700
412737
  const { text: text2, usage } = await generateText({
412701
412738
  model,
412702
412739
  ...supportsTemperature(modelId) ? { temperature: 0 } : {},
@@ -412715,6 +412752,16 @@ ${s.signature ? `${s.signature}
412715
412752
  });
412716
412753
  const cacheRead = usage.inputTokenDetails?.cacheReadTokens ?? 0;
412717
412754
  store.addSemanticTokens(usage.inputTokens ?? 0, usage.outputTokens ?? 0, cacheRead);
412755
+ recordModelCall({
412756
+ modelId,
412757
+ source: "other",
412758
+ startedAt: semStartedAt,
412759
+ durationMs: Math.max(0, Date.now() - semStartedAt),
412760
+ state: "ok",
412761
+ input: usage.inputTokens ?? 0,
412762
+ output: usage.outputTokens ?? 0,
412763
+ cacheRead
412764
+ });
412718
412765
  for (const line2 of text2.split(`
412719
412766
  `)) {
412720
412767
  const trimmed = line2.trim();
@@ -432026,7 +432073,7 @@ ${opts.system}` : opts.system;
432026
432073
  providerOptions: providerOpts.providerOptions,
432027
432074
  headers: providerOpts.headers,
432028
432075
  cwd: cwd2,
432029
- disablePruning: !["subagents", "both"].includes(merged.contextManagement?.pruningTarget ?? "subagents")
432076
+ disablePruning: !["subagents", "both"].includes(merged.contextManagement?.pruningTarget ?? "none")
432030
432077
  });
432031
432078
  return {
432032
432079
  cwd: cwd2,
@@ -482460,6 +482507,9 @@ function handleStatus(_input, _ctx) {
482460
482507
  useUIStore.setState({ statusDashboardTab: "System" });
482461
482508
  useUIStore.getState().openModal("statusDashboard");
482462
482509
  }
482510
+ function handleModelEvents(_input, _ctx) {
482511
+ useUIStore.getState().openModal("modelEvents");
482512
+ }
482463
482513
  function handleDiagnose(_input, _ctx) {
482464
482514
  useUIStore.getState().openModal("diagnosePopup");
482465
482515
  }
@@ -482493,6 +482543,8 @@ async function handleLspRestart(input, ctx) {
482493
482543
  }
482494
482544
  function register6(map2) {
482495
482545
  map2.set("/status", handleStatus);
482546
+ map2.set("/model-events", handleModelEvents);
482547
+ map2.set("/model events", handleModelEvents);
482496
482548
  map2.set("/diagnose", handleDiagnose);
482497
482549
  map2.set("/setup", handleSetup);
482498
482550
  map2.set("/lsp", handleLsp);
@@ -488682,6 +488734,13 @@ var init_registry = __esm(() => {
488682
488734
  category: "System",
488683
488735
  tags: ["info", "health", "context", "tokens"]
488684
488736
  },
488737
+ {
488738
+ cmd: "/model-events",
488739
+ ic: "info",
488740
+ desc: "Model events \u2014 per-call latency, tokens, errors (opt-in)",
488741
+ category: "System",
488742
+ tags: ["debug", "metrics", "latency", "errors", "performance", "models", "telemetry"]
488743
+ },
488685
488744
  {
488686
488745
  cmd: "/storage",
488687
488746
  ic: "system",
@@ -489197,7 +489256,7 @@ function useGlobalKeyboard({
489197
489256
  return;
489198
489257
  const uiModals = useUIStore.getState().modals;
489199
489258
  if (selectIsAnyModalOpen(useUIStore.getState())) {
489200
- const hasOwnInput = uiModals.commandPalette || uiModals.skillSearch || uiModals.sessionPicker || uiModals.errorLog || uiModals.compactionLog || uiModals.llmSelector || uiModals.floatingTerminal || uiModals.firstRunWizard || uiModals.mcpSettings || uiModals.tabNamePopup;
489259
+ const hasOwnInput = uiModals.commandPalette || uiModals.skillSearch || uiModals.sessionPicker || uiModals.errorLog || uiModals.compactionLog || uiModals.llmSelector || uiModals.floatingTerminal || uiModals.firstRunWizard || uiModals.mcpSettings || uiModals.modelEvents || uiModals.tabNamePopup;
489201
489260
  if (evt.ctrl && evt.name === "c" && !hasOwnInput) {
489202
489261
  handleExit();
489203
489262
  }
@@ -491696,6 +491755,8 @@ async function buildV2Summary(opts) {
491696
491755
  }
491697
491756
  let gapFill;
491698
491757
  let llmUsage;
491758
+ const v2StartedAt = Date.now();
491759
+ const v2ModelId = getModelId(model);
491699
491760
  try {
491700
491761
  const genResult = await generateText({
491701
491762
  model,
@@ -491743,8 +491804,27 @@ async function buildV2Summary(opts) {
491743
491804
  cacheWriteTokens: details?.cacheWriteTokens ?? 0
491744
491805
  };
491745
491806
  }
491807
+ recordModelCall({
491808
+ modelId: v2ModelId,
491809
+ source: "compaction",
491810
+ startedAt: v2StartedAt,
491811
+ durationMs: Math.max(0, Date.now() - v2StartedAt),
491812
+ state: "ok",
491813
+ input: llmUsage?.inputTokens ?? 0,
491814
+ output: llmUsage?.outputTokens ?? 0,
491815
+ cacheRead: llmUsage?.cacheReadTokens ?? 0,
491816
+ cacheWrite: llmUsage?.cacheWriteTokens ?? 0
491817
+ });
491746
491818
  } catch (err2) {
491747
491819
  logBackgroundError("compaction-summarize", err2 instanceof Error ? err2.message : String(err2));
491820
+ recordModelCall({
491821
+ modelId: v2ModelId,
491822
+ source: "compaction",
491823
+ startedAt: v2StartedAt,
491824
+ durationMs: Math.max(0, Date.now() - v2StartedAt),
491825
+ state: "error",
491826
+ errorMessage: (err2 instanceof Error ? err2.message : String(err2)).slice(0, 500)
491827
+ });
491748
491828
  return { summary: structuredState };
491749
491829
  }
491750
491830
  if (!gapFill || gapFill.trim() === "COMPLETE" || gapFill.trim().length < 20) {
@@ -491804,6 +491884,7 @@ function messageTextFull(msg) {
491804
491884
  var init_summarize = __esm(() => {
491805
491885
  init_dist5();
491806
491886
  init_errors();
491887
+ init_model_events();
491807
491888
  init_provider_options();
491808
491889
  });
491809
491890
 
@@ -492428,7 +492509,8 @@ function useChat({
492428
492509
  openEditor,
492429
492510
  initialState,
492430
492511
  getWorkspaceSnapshot,
492431
- visible = true
492512
+ visible = true,
492513
+ onModelChange
492432
492514
  }) {
492433
492515
  const [messages, setMessages] = import_react32.useState(initialState?.messages ?? []);
492434
492516
  const [coreMessages, setCoreMessages] = import_react32.useState(initialState?.coreMessages ?? []);
@@ -492561,6 +492643,7 @@ function useChat({
492561
492643
  const outsideCwdMutexRef = import_react32.useRef(Promise.resolve());
492562
492644
  const remoteApprovalActiveRef = import_react32.useRef(0);
492563
492645
  const webSearchModelLabelRef = import_react32.useRef(null);
492646
+ const userAbortedRef = import_react32.useRef(false);
492564
492647
  const [activePlan, setActivePlanRaw] = import_react32.useState(initialState?.activePlan ?? null);
492565
492648
  const activePlanRef = import_react32.useRef(activePlan);
492566
492649
  const setActivePlan = import_react32.useCallback((v4) => {
@@ -493005,6 +493088,7 @@ function useChat({
493005
493088
  const convoText = olderMessages.map((m5) => formatMessage(m5, 6000)).join(`
493006
493089
 
493007
493090
  `);
493091
+ const compactStartedAt = Date.now();
493008
493092
  const v1Result = await generateText({
493009
493093
  model,
493010
493094
  ...supportsTemperature(activeModelRef.current) ? { temperature: 0 } : {},
@@ -493062,6 +493146,18 @@ INCLUDE the plan progress above VERBATIM in ## Current State so the agent knows
493062
493146
  cacheWriteTokens: v1Details?.cacheWriteTokens ?? 0
493063
493147
  };
493064
493148
  }
493149
+ recordModelCall({
493150
+ modelId: compactModelId,
493151
+ source: "compaction",
493152
+ startedAt: compactStartedAt,
493153
+ durationMs: Math.max(0, Date.now() - compactStartedAt),
493154
+ state: "ok",
493155
+ tabId,
493156
+ input: compactUsage?.inputTokens ?? 0,
493157
+ output: compactUsage?.outputTokens ?? 0,
493158
+ cacheRead: compactUsage?.cacheReadTokens ?? 0,
493159
+ cacheWrite: compactUsage?.cacheWriteTokens ?? 0
493160
+ });
493065
493161
  }
493066
493162
  if (!summary || summary.trim().length < 50) {
493067
493163
  setMessages((prev) => [
@@ -493216,7 +493312,7 @@ INCLUDE the plan progress above VERBATIM in ## Current State so the agent knows
493216
493312
  });
493217
493313
  }
493218
493314
  }
493219
- }, [setTokenUsage, effectiveConfig, contextManager, cwd2]);
493315
+ }, [setTokenUsage, effectiveConfig, contextManager, cwd2, tabId]);
493220
493316
  summarizeConversationRef.current = summarizeConversation;
493221
493317
  const autoSummarizedRef = import_react32.useRef(false);
493222
493318
  import_react32.useEffect(() => {
@@ -493740,16 +493836,24 @@ ${description}`,
493740
493836
  queueMicrotaskFlush();
493741
493837
  }
493742
493838
  });
493743
- const STALL_MAX_RETRIES = resolveRetrySettings(effectiveConfig2.retry).maxRetries;
493839
+ const {
493840
+ maxTransientRetries: MAX_TRANSIENT_RETRIES,
493841
+ maxStallRetries: STALL_MAX_RETRIES,
493842
+ baseDelayMs: RETRY_BASE_DELAY_MS
493843
+ } = resolveRetrySettings(effectiveConfig2.retry);
493744
493844
  let stallWatchdog = null;
493745
493845
  let unsubStallWatch1 = null;
493746
493846
  let unsubStallWatch2 = null;
493747
493847
  let unsubStallWatch3 = null;
493748
- let userAborted = false;
493749
493848
  let stallTriggered = false;
493750
493849
  let stallAborted = false;
493751
- const { maxRetries: MAX_TRANSIENT_RETRIES, baseDelayMs: RETRY_BASE_DELAY_MS } = resolveRetrySettings(effectiveConfig2.retry);
493752
493850
  let streamRetryCount = 0;
493851
+ const rawFallback = effectiveConfig2.modelFallback;
493852
+ const fallbackModels = rawFallback && typeof rawFallback === "object" && !Array.isArray(rawFallback) ? (rawFallback[activeModelRef.current] ?? []).filter((m5) => m5 && m5.trim().length > 0) : [];
493853
+ let fallbackIndex = -1;
493854
+ const primaryModelId = activeModelRef.current;
493855
+ let cycleCount = 0;
493856
+ const MAX_CYCLES = 3;
493753
493857
  let lengthRetryCount = 0;
493754
493858
  const MAX_LENGTH_RETRIES = 2;
493755
493859
  if (input !== "Continue." || !stallRetryPendingRef.current) {
@@ -493759,6 +493863,7 @@ ${description}`,
493759
493863
  const responseStartedAt = Date.now();
493760
493864
  for (;; ) {
493761
493865
  let proxyBounced = false;
493866
+ userAbortedRef.current = false;
493762
493867
  abortController = new AbortController;
493763
493868
  abortRef.current = abortController;
493764
493869
  fullText = "";
@@ -493971,7 +494076,7 @@ Proceeding without it will significantly reduce capabilities \u2014 no soul tool
493971
494076
  },
493972
494077
  planExecution: planExecutionRef.current,
493973
494078
  drainSteering,
493974
- disablePruning: !["subagents", "both"].includes(effectiveConfig2.contextManagement?.pruningTarget ?? "subagents"),
494079
+ disablePruning: !["subagents", "both"].includes(effectiveConfig2.contextManagement?.pruningTarget ?? "none"),
493975
494080
  disabledTools: useToolsStore.getState().disabledTools,
493976
494081
  tabId,
493977
494082
  tabLabel
@@ -494010,7 +494115,7 @@ Proceeding without it will significantly reduce capabilities \u2014 no soul tool
494010
494115
  },
494011
494116
  planExecution: planExecutionRef.current,
494012
494117
  drainSteering,
494013
- disablePruning: !["subagents", "both"].includes(effectiveConfig2.contextManagement?.pruningTarget ?? "subagents"),
494118
+ disablePruning: !["subagents", "both"].includes(effectiveConfig2.contextManagement?.pruningTarget ?? "none"),
494014
494119
  disabledTools: useToolsStore.getState().disabledTools,
494015
494120
  tabId,
494016
494121
  tabLabel
@@ -494104,6 +494209,7 @@ Proceeding without it will significantly reduce capabilities \u2014 no soul tool
494104
494209
  let toolsInFlight = 0;
494105
494210
  let gotFirstContent = false;
494106
494211
  let betweenSteps = false;
494212
+ let stepStartedAt = 0;
494107
494213
  const markActivity = () => {
494108
494214
  lastActivityTs = Date.now();
494109
494215
  };
@@ -494118,7 +494224,7 @@ Proceeding without it will significantly reduce capabilities \u2014 no soul tool
494118
494224
  };
494119
494225
  const onUserAbort = () => {
494120
494226
  if (!stallAborted) {
494121
- userAborted = true;
494227
+ userAbortedRef.current = true;
494122
494228
  }
494123
494229
  };
494124
494230
  abortController.signal.addEventListener("abort", onUserAbort, { once: true });
@@ -494215,6 +494321,7 @@ Proceeding without it will significantly reduce capabilities \u2014 no soul tool
494215
494321
  switch (part.type) {
494216
494322
  case "start-step": {
494217
494323
  betweenSteps = false;
494324
+ stepStartedAt = Date.now();
494218
494325
  const warnings = part.warnings;
494219
494326
  if (warnings && warnings.length > 0) {
494220
494327
  const msg = warnings.map((w5) => `[${w5.type}]${w5.message ? ` ${w5.message}` : ""}`).join("; ");
@@ -494502,6 +494609,18 @@ Proceeding without it will significantly reduce capabilities \u2014 no soul tool
494502
494609
  output: turnTokensRef.current.output + stepOut,
494503
494610
  cacheRead: turnTokensRef.current.cacheRead + stepCache
494504
494611
  };
494612
+ recordModelCall({
494613
+ modelId,
494614
+ source: "main",
494615
+ startedAt: stepStartedAt || Date.now(),
494616
+ durationMs: stepStartedAt ? Math.max(0, Date.now() - stepStartedAt) : 0,
494617
+ state: "ok",
494618
+ tabId,
494619
+ input: stepIn,
494620
+ output: stepOut,
494621
+ cacheRead: stepCache,
494622
+ cacheWrite: stepCacheWrite
494623
+ });
494505
494624
  queueMicrotaskFlush();
494506
494625
  if (completedCalls.length > 0 && Date.now() - lastIncrementalSave > 1e4) {
494507
494626
  lastIncrementalSave = Date.now();
@@ -494565,7 +494684,7 @@ ${errStack}` : `Error: ${displayErr}`);
494565
494684
  unsubStallWatch1?.();
494566
494685
  unsubStallWatch2?.();
494567
494686
  unsubStallWatch3?.();
494568
- if (stallTriggered && abortController.signal.aborted && !userAborted) {
494687
+ if (stallTriggered && abortController.signal.aborted && !userAbortedRef.current) {
494569
494688
  throw new Error("Stream stall \u2014 abort did not throw");
494570
494689
  }
494571
494690
  if (streamErrors.length > 0) {
@@ -494681,7 +494800,7 @@ ${errStack}` : `Error: ${displayErr}`);
494681
494800
  }).map((m5) => m5 ?? { role: "assistant", content: "(continued)" });
494682
494801
  setCoreMessages((prev) => {
494683
494802
  const updated = [...prev, ...filteredResponseMessages];
494684
- const target = effectiveConfig2.contextManagement?.pruningTarget ?? "subagents";
494803
+ const target = effectiveConfig2.contextManagement?.pruningTarget ?? "none";
494685
494804
  return ["main", "both"].includes(target) ? pruneOldToolResults(updated) : updated;
494686
494805
  });
494687
494806
  streamSegmentsBuffer.current = [];
@@ -494786,7 +494905,7 @@ ${errStack}` : `Error: ${displayErr}`);
494786
494905
  ]);
494787
494906
  }
494788
494907
  }
494789
- const isStallRetry = isAbort && stallTriggered && !userAborted && stallRetryCountRef.current <= STALL_MAX_RETRIES;
494908
+ const isStallRetry = isAbort && stallTriggered && !userAbortedRef.current && stallRetryCountRef.current <= STALL_MAX_RETRIES;
494790
494909
  if (isTransient && !isStallRetry) {
494791
494910
  streamRetryCount++;
494792
494911
  if (streamRetryCount <= MAX_TRANSIENT_RETRIES && !abortController.signal.aborted) {
@@ -494862,6 +494981,46 @@ ${errStack}` : `Error: ${displayErr}`);
494862
494981
  continue;
494863
494982
  }
494864
494983
  }
494984
+ if (userAbortedRef.current) {} else if (fallbackIndex < fallbackModels.length - 1) {
494985
+ fallbackIndex++;
494986
+ const nextModel = fallbackModels[fallbackIndex];
494987
+ activeModelRef.current = nextModel;
494988
+ streamRetryCount = 0;
494989
+ notifyProviderSwitch(nextModel).catch(() => {});
494990
+ setActiveModel(nextModel);
494991
+ onModelChange?.(nextModel);
494992
+ setMessages((prev) => [
494993
+ ...prev,
494994
+ {
494995
+ id: crypto.randomUUID(),
494996
+ role: "system",
494997
+ content: `Switched to fallback model: ${nextModel}`,
494998
+ timestamp: Date.now()
494999
+ }
495000
+ ]);
495001
+ continue;
495002
+ } else if (fallbackModels.length > 0) {
495003
+ cycleCount++;
495004
+ if (cycleCount > MAX_CYCLES) {
495005
+ throw new Error(`Exhausted ${String(MAX_CYCLES)} cycles of model fallbacks. Last error: ${msg}`, { cause: err2 });
495006
+ }
495007
+ fallbackIndex = -1;
495008
+ activeModelRef.current = primaryModelId;
495009
+ streamRetryCount = 0;
495010
+ notifyProviderSwitch(primaryModelId).catch(() => {});
495011
+ setActiveModel(primaryModelId);
495012
+ onModelChange?.(primaryModelId);
495013
+ setMessages((prev) => [
495014
+ ...prev,
495015
+ {
495016
+ id: crypto.randomUUID(),
495017
+ role: "system",
495018
+ content: `All fallbacks exhausted, retrying primary model: ${primaryModelId} (cycle ${String(cycleCount)}/${String(MAX_CYCLES)})`,
495019
+ timestamp: Date.now()
495020
+ }
495021
+ ]);
495022
+ continue;
495023
+ }
494865
495024
  }
494866
495025
  if (isStallRetry) {
494867
495026
  if (flushTimerRef.current) {
@@ -494952,14 +495111,25 @@ ${errStack}` : `Error: ${displayErr}`);
494952
495111
  cwd: cwd2
494953
495112
  }).catch(() => {});
494954
495113
  }
494955
- const isTransientStream = /overloaded|529|429|rate.?limit|too many requests|503|502/i.test(rawMsg);
494956
495114
  const errObj = err2 != null && typeof err2 === "object" ? err2 : null;
494957
495115
  const apiBody = errObj && typeof errObj.responseBody === "string" && errObj.responseBody.length > 0 ? errObj.responseBody : undefined;
494958
495116
  const apiData = errObj?.data != null ? JSON.stringify(errObj.data).slice(0, 500) : undefined;
494959
495117
  const detail = apiBody?.slice(0, 500) ?? apiData;
494960
495118
  const enrichedMsg = detail ? `${rawMsg} \xB7 ${detail}` : rawMsg;
495119
+ const isTransientStream = /overloaded|529|429|rate.?limit|too many requests|503|502/i.test(rawMsg) || /403/i.test(rawMsg) && /overloaded|rate/i.test(apiBody ?? "");
494961
495120
  const errorMsg = isTransientStream ? `Provider returned a transient error (${rawMsg.slice(0, 120)}). Please retry.` : enrichedMsg;
494962
495121
  const errorStack = !isTransientStream && err2 instanceof Error ? err2.stack : undefined;
495122
+ if (!isAbort) {
495123
+ recordModelCall({
495124
+ modelId: activeModelRef.current,
495125
+ source: "main",
495126
+ startedAt: Date.now(),
495127
+ durationMs: 0,
495128
+ state: "error",
495129
+ tabId,
495130
+ errorMessage: errorMsg.slice(0, 500)
495131
+ });
495132
+ }
494963
495133
  if (isAbort) {
494964
495134
  const completedIds = new Set(completedCalls.map((c) => c.id));
494965
495135
  const liveBuf = abortedToolCallsSnapshot.current.length > 0 ? abortedToolCallsSnapshot.current : liveToolCallsBuffer.current;
@@ -495237,7 +495407,8 @@ ${pContent}`;
495237
495407
  promptDestructive,
495238
495408
  tabId,
495239
495409
  tabLabel,
495240
- setForgeMode
495410
+ setForgeMode,
495411
+ onModelChange
495241
495412
  ]);
495242
495413
  handleSubmitRef.current = handleSubmit;
495243
495414
  const abort2 = import_react32.useCallback(() => {
@@ -495258,6 +495429,7 @@ ${pContent}`;
495258
495429
  }
495259
495430
  ]);
495260
495431
  }
495432
+ userAbortedRef.current = true;
495261
495433
  if (abortRef.current) {
495262
495434
  const pq = pendingQuestionRef.current;
495263
495435
  if (pq) {
@@ -495432,6 +495604,7 @@ var init_useChat = __esm(() => {
495432
495604
  init_io_client();
495433
495605
  init_compaction_logs();
495434
495606
  init_errors();
495607
+ init_model_events();
495435
495608
  init_repomap();
495436
495609
  init_statusbar();
495437
495610
  init_tools2();
@@ -504180,7 +504353,7 @@ function GroupedListImpl({
504180
504353
  const parent = filteredGroupOf(groups, r4.groupId);
504181
504354
  const accent = parent?.accent ?? t2.brand;
504182
504355
  const rowBg = isSelected ? t2.bgPopupHighlight : fill;
504183
- const fg2 = it.disabled ? t2.textDim : isSelected ? t2.textPrimary : it.active ? accent : focused ? t2.textSecondary : t2.textMuted;
504356
+ const fg2 = it.disabled ? t2.textDim : isSelected ? t2.textPrimary : it.active ? accent : it.subdued ? t2.textFaint : focused ? t2.textSecondary : t2.textMuted;
504184
504357
  const hlFg = isSelected ? t2.textPrimary : accent;
504185
504358
  const labelBold = isSelected || !!it.active;
504186
504359
  const spans = renderLabelSpans(it.label, it.highlightIndices, fg2, hlFg, labelBold);
@@ -507780,6 +507953,7 @@ var init_TabInstance = __esm(async () => {
507780
507953
  onSuspend,
507781
507954
  onCommand,
507782
507955
  onModeChange,
507956
+ onModelChange,
507783
507957
  onExit,
507784
507958
  registerChat,
507785
507959
  unregisterChat,
@@ -507863,7 +508037,8 @@ var init_TabInstance = __esm(async () => {
507863
508037
  onSuspend,
507864
508038
  initialState,
507865
508039
  getWorkspaceSnapshot,
507866
- visible
508040
+ visible,
508041
+ onModelChange
507867
508042
  });
507868
508043
  import_react71.useEffect(() => {
507869
508044
  if (effectiveConfig.coAuthorCommits !== undefined)
@@ -509352,6 +509527,7 @@ function ApiKeySettings({ visible, onClose }) {
509352
509527
  const keys2 = useApiKeyStore((s2) => s2.keys);
509353
509528
  const priority = useApiKeyStore((s2) => s2.priority);
509354
509529
  const refresh = useApiKeyStore((s2) => s2.refresh);
509530
+ const refreshOne = useApiKeyStore((s2) => s2.refreshOne);
509355
509531
  const [cursor, setCursor] = import_react80.useState(0);
509356
509532
  const [mode, setMode] = import_react80.useState("menu");
509357
509533
  const [inputValue, setInputValue] = import_react80.useState("");
@@ -509405,9 +509581,9 @@ function ApiKeySettings({ visible, onClose }) {
509405
509581
  id: `${k5.id}-remove`,
509406
509582
  kind: "remove",
509407
509583
  targetKey: k5.id,
509408
- label: `Remove ${k5.label}`,
509409
- status: "error",
509410
- meta: "delete stored key"
509584
+ prefix: " \u21B3",
509585
+ label: "remove stored key",
509586
+ subdued: true
509411
509587
  });
509412
509588
  }
509413
509589
  return out2;
@@ -509468,17 +509644,18 @@ function ApiKeySettings({ visible, onClose }) {
509468
509644
  setMode("menu");
509469
509645
  return;
509470
509646
  }
509471
- const result = setSecret(inputTarget, inputValue.trim());
509647
+ const target = inputTarget;
509648
+ setMode("menu");
509649
+ setInputValue("");
509650
+ setInputTarget(null);
509651
+ const result = setSecret(target, inputValue.trim());
509472
509652
  if (result.success) {
509473
509653
  const where = result.storage === "keychain" ? "OS keychain" : result.path ?? "secrets.json";
509474
509654
  popFlash("ok", `Saved to ${where}`);
509475
509655
  } else {
509476
509656
  popFlash("err", "Failed to save key");
509477
509657
  }
509478
- refresh(keyItems);
509479
- setMode("menu");
509480
- setInputValue("");
509481
- setInputTarget(null);
509658
+ refreshOne(target);
509482
509659
  };
509483
509660
  const removeKey = (keyId) => {
509484
509661
  const result = deleteSecret(keyId);
@@ -509486,7 +509663,7 @@ function ApiKeySettings({ visible, onClose }) {
509486
509663
  popFlash("ok", `Removed from ${result.storage}`);
509487
509664
  else
509488
509665
  popFlash("err", "Key not found");
509489
- refresh(keyItems);
509666
+ refreshOne(keyId);
509490
509667
  };
509491
509668
  useKeyboard((evt) => {
509492
509669
  if (!visible)
@@ -509621,7 +509798,8 @@ var init_ApiKeySettings = __esm(async () => {
509621
509798
  useApiKeyStore = create()((set3, get) => ({
509622
509799
  keys: {},
509623
509800
  priority: getDefaultKeyPriority(),
509624
- refresh: (items) => set3({ keys: buildKeys(items, get().priority) })
509801
+ refresh: (items) => set3({ keys: buildKeys(items, get().priority) }),
509802
+ refreshOne: (id) => set3((state) => ({ keys: { ...state.keys, [id]: getSecretSources(id, state.priority) } }))
509625
509803
  }));
509626
509804
  });
509627
509805
 
@@ -523469,6 +523647,253 @@ var init_MCPSettings = __esm(async () => {
523469
523647
  });
523470
523648
  });
523471
523649
 
523650
+ // src/components/settings/ModelEventsPopup.tsx
523651
+ function fmtMs(ms) {
523652
+ if (ms <= 0)
523653
+ return "\u2014";
523654
+ if (ms < 1000)
523655
+ return `${String(Math.round(ms))}ms`;
523656
+ return `${(ms / 1000).toFixed(2)}s`;
523657
+ }
523658
+ function fmtTok(n) {
523659
+ if (n <= 0)
523660
+ return "\u2014";
523661
+ if (n < 1000)
523662
+ return String(n);
523663
+ if (n < 1e6)
523664
+ return `${(n / 1000).toFixed(1)}k`;
523665
+ return `${(n / 1e6).toFixed(2)}M`;
523666
+ }
523667
+ function fmtAge(now2, at) {
523668
+ const d3 = Math.max(0, now2 - at);
523669
+ if (d3 < 1000)
523670
+ return "now";
523671
+ if (d3 < 60000)
523672
+ return `${String(Math.round(d3 / 1000))}s`;
523673
+ if (d3 < 3600000)
523674
+ return `${String(Math.round(d3 / 60000))}m`;
523675
+ return `${String(Math.round(d3 / 3600000))}h`;
523676
+ }
523677
+ function shortModel(id) {
523678
+ const slash = id.lastIndexOf("/");
523679
+ return slash >= 0 ? id.slice(slash + 1) : id;
523680
+ }
523681
+ function ModelEventsPopup({ visible, onClose }) {
523682
+ const t2 = useTheme();
523683
+ const { width: termCols, height: termRows } = useTerminalDimensions();
523684
+ const popupW = Math.min(110, Math.max(80, Math.floor(termCols * 0.85)));
523685
+ const popupH = Math.min(Math.max(20, Math.floor(termRows * 0.82)), termRows - 2);
523686
+ const SIDEBAR_W4 = 16;
523687
+ const contentW = Math.max(40, popupW - SIDEBAR_W4 - 6);
523688
+ const enabled = useModelEventsStore((s2) => s2.enabled);
523689
+ const events = useModelEventsStore((s2) => s2.events);
523690
+ const setEnabled = useModelEventsStore((s2) => s2.setEnabled);
523691
+ const clear = useModelEventsStore((s2) => s2.clear);
523692
+ const [tab, setTab] = import_react136.useState("Models");
523693
+ const [now2, setNow] = import_react136.useState(Date.now());
523694
+ import_react136.useEffect(() => {
523695
+ if (!visible)
523696
+ return;
523697
+ const i4 = setInterval(() => setNow(Date.now()), 1000);
523698
+ return () => clearInterval(i4);
523699
+ }, [visible]);
523700
+ useKeyboard((evt) => {
523701
+ if (!visible)
523702
+ return;
523703
+ if (evt.name === "escape" || evt.name === "q") {
523704
+ onClose();
523705
+ return;
523706
+ }
523707
+ if (evt.name === "tab" || evt.name === "right" || evt.name === "l") {
523708
+ setTab((cur) => TABS5[(TABS5.indexOf(cur) + 1) % TABS5.length] ?? "Models");
523709
+ return;
523710
+ }
523711
+ if (evt.name === "left" || evt.name === "h") {
523712
+ setTab((cur) => TABS5[(TABS5.indexOf(cur) - 1 + TABS5.length) % TABS5.length] ?? "Models");
523713
+ return;
523714
+ }
523715
+ if (evt.name === "e") {
523716
+ setEnabled(!enabled);
523717
+ return;
523718
+ }
523719
+ if (evt.name === "c") {
523720
+ clear();
523721
+ return;
523722
+ }
523723
+ });
523724
+ const aggregates = import_react136.useMemo(() => aggregateModelEvents(events), [events]);
523725
+ const errors4 = import_react136.useMemo(() => modelErrorEvents(events), [events]);
523726
+ const recent = import_react136.useMemo(() => [...events].reverse().slice(0, 200), [events]);
523727
+ if (!visible)
523728
+ return null;
523729
+ const sidebarTabs = TABS5.map((id) => ({
523730
+ id,
523731
+ label: id,
523732
+ icon: id === "Models" ? "model" : id === "Recent" ? "clock" : "error",
523733
+ status: id === "Errors" && errors4.length > 0 ? "error" : undefined
523734
+ }));
523735
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(PremiumPopup, {
523736
+ visible,
523737
+ width: popupW,
523738
+ height: popupH,
523739
+ title: "Model Events",
523740
+ titleIcon: icon("info"),
523741
+ tabs: sidebarTabs,
523742
+ activeTab: tab,
523743
+ sidebarWidth: 16,
523744
+ footerHints: [
523745
+ { key: "e", label: enabled ? "Disable" : "Enable" },
523746
+ { key: "c", label: "Clear" },
523747
+ { key: "tab", label: "Switch tab" },
523748
+ { key: "esc", label: "Close" }
523749
+ ],
523750
+ children: [
523751
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
523752
+ flexDirection: "column",
523753
+ backgroundColor: t2.bgPopup,
523754
+ paddingX: 2,
523755
+ paddingY: 1,
523756
+ children: /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
523757
+ flexDirection: "row",
523758
+ backgroundColor: t2.bgPopup,
523759
+ children: [
523760
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Toggle, {
523761
+ label: "Record model events",
523762
+ description: enabled ? "Capturing per-call latency, tokens, and errors." : "Off by default \u2014 press 'e' to enable. Clears on disable.",
523763
+ on: enabled
523764
+ }, undefined, false, undefined, this),
523765
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
523766
+ flexGrow: 1,
523767
+ backgroundColor: t2.bgPopup
523768
+ }, undefined, false, undefined, this),
523769
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523770
+ bg: t2.bgPopup,
523771
+ fg: t2.textFaint,
523772
+ children: [
523773
+ String(events.length),
523774
+ " events"
523775
+ ]
523776
+ }, undefined, true, undefined, this)
523777
+ ]
523778
+ }, undefined, true, undefined, this)
523779
+ }, undefined, false, undefined, this),
523780
+ tab === "Models" ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Section, {
523781
+ title: "Per-model aggregates",
523782
+ bg: t2.bgPopup,
523783
+ children: aggregates.length === 0 ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523784
+ bg: t2.bgPopup,
523785
+ fg: t2.textFaint,
523786
+ children: enabled ? "No model events recorded yet." : "Enable recording to see model usage."
523787
+ }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Table, {
523788
+ width: contentW,
523789
+ columns: [
523790
+ { key: "Model", render: (r4) => shortModel(r4.modelId) },
523791
+ { key: "Calls", width: 5, align: "right", render: (r4) => String(r4.calls) },
523792
+ {
523793
+ key: "Err",
523794
+ width: 4,
523795
+ align: "right",
523796
+ render: (r4) => r4.errors > 0 ? String(r4.errors) : "\u2014"
523797
+ },
523798
+ { key: "Avg", width: 6, align: "right", render: (r4) => fmtMs(r4.avgMs) },
523799
+ { key: "Last", width: 6, align: "right", render: (r4) => fmtMs(r4.lastMs) },
523800
+ { key: "In", width: 6, align: "right", render: (r4) => fmtTok(r4.input) },
523801
+ { key: "Out", width: 6, align: "right", render: (r4) => fmtTok(r4.output) },
523802
+ { key: "Cache", width: 6, align: "right", render: (r4) => fmtTok(r4.cacheRead) }
523803
+ ],
523804
+ rows: aggregates,
523805
+ maxRows: Math.max(4, popupH - 12)
523806
+ }, undefined, false, undefined, this)
523807
+ }, undefined, false, undefined, this) : tab === "Recent" ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Section, {
523808
+ title: "Recent calls",
523809
+ bg: t2.bgPopup,
523810
+ children: recent.length === 0 ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523811
+ bg: t2.bgPopup,
523812
+ fg: t2.textFaint,
523813
+ children: enabled ? "No recent calls." : "Enable recording first."
523814
+ }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Table, {
523815
+ width: contentW,
523816
+ columns: [
523817
+ { key: "When", width: 5, render: (r4) => fmtAge(now2, r4.startedAt) },
523818
+ { key: "Source", width: 7, render: (r4) => r4.source },
523819
+ { key: "Model", render: (r4) => shortModel(r4.modelId) },
523820
+ {
523821
+ key: "State",
523822
+ width: 5,
523823
+ render: (r4) => r4.state === "error" ? "err" : "ok"
523824
+ },
523825
+ { key: "Time", width: 6, align: "right", render: (r4) => fmtMs(r4.durationMs) },
523826
+ { key: "In", width: 6, align: "right", render: (r4) => fmtTok(r4.input ?? 0) },
523827
+ { key: "Out", width: 6, align: "right", render: (r4) => fmtTok(r4.output ?? 0) }
523828
+ ],
523829
+ rows: recent,
523830
+ maxRows: Math.max(4, popupH - 12)
523831
+ }, undefined, false, undefined, this)
523832
+ }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(Section, {
523833
+ title: "Errors",
523834
+ bg: t2.bgPopup,
523835
+ children: errors4.length === 0 ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523836
+ bg: t2.bgPopup,
523837
+ fg: t2.textFaint,
523838
+ children: "No errors recorded."
523839
+ }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
523840
+ flexDirection: "column",
523841
+ backgroundColor: t2.bgPopup,
523842
+ children: errors4.slice(-Math.max(4, popupH - 12)).reverse().map((ev) => /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
523843
+ flexDirection: "column",
523844
+ backgroundColor: t2.bgPopup,
523845
+ children: [
523846
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
523847
+ flexDirection: "row",
523848
+ backgroundColor: t2.bgPopup,
523849
+ children: [
523850
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523851
+ bg: t2.bgPopup,
523852
+ fg: t2.error,
523853
+ attributes: BOLD18,
523854
+ children: shortModel(ev.modelId)
523855
+ }, undefined, false, undefined, this),
523856
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523857
+ bg: t2.bgPopup,
523858
+ fg: t2.textFaint,
523859
+ children: [
523860
+ " ",
523861
+ ev.source,
523862
+ " \xB7 ",
523863
+ fmtAge(now2, ev.startedAt),
523864
+ " \xB7 ",
523865
+ fmtMs(ev.durationMs)
523866
+ ]
523867
+ }, undefined, true, undefined, this)
523868
+ ]
523869
+ }, undefined, true, undefined, this),
523870
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
523871
+ bg: t2.bgPopup,
523872
+ fg: t2.textPrimary,
523873
+ children: [
523874
+ " ",
523875
+ (ev.errorMessage ?? "").slice(0, contentW - 4)
523876
+ ]
523877
+ }, undefined, true, undefined, this)
523878
+ ]
523879
+ }, ev.id, true, undefined, this))
523880
+ }, undefined, false, undefined, this)
523881
+ }, undefined, false, undefined, this)
523882
+ ]
523883
+ }, undefined, true, undefined, this);
523884
+ }
523885
+ var import_react136, BOLD18 = 1, TABS5;
523886
+ var init_ModelEventsPopup = __esm(async () => {
523887
+ init_icons();
523888
+ init_theme();
523889
+ init_model_events();
523890
+ init_ui2();
523891
+ init_jsx_dev_runtime();
523892
+ await init_react2();
523893
+ import_react136 = __toESM(require_react(), 1);
523894
+ TABS5 = ["Models", "Recent", "Errors"];
523895
+ });
523896
+
523472
523897
  // src/components/settings/ProviderSettings.tsx
523473
523898
  function readValuesFromLayer(layer) {
523474
523899
  if (!layer)
@@ -523639,18 +524064,18 @@ function ProviderSettings({
523639
524064
  const popupWidth = Math.min(MAX_POPUP_WIDTH4, Math.floor(termCols * 0.85));
523640
524065
  const maxVisible = Math.max(6, Math.floor(containerRows * 0.85) - CHROME_ROWS6);
523641
524066
  const t2 = useTheme();
523642
- const [tab, setTab] = import_react136.useState("claude");
523643
- const [cursor, setCursor] = import_react136.useState(0);
523644
- const [scope, setScope] = import_react136.useState(() => detectInitialScope(projectConfig));
524067
+ const [tab, setTab] = import_react138.useState("claude");
524068
+ const [cursor, setCursor] = import_react138.useState(0);
524069
+ const [scope, setScope] = import_react138.useState(() => detectInitialScope(projectConfig));
523645
524070
  const vals = effectiveValues(globalConfig2, projectConfig);
523646
524071
  const items = TAB_ITEMS[tab];
523647
- const tabIdx = TABS5.indexOf(tab);
524072
+ const tabIdx = TABS6.indexOf(tab);
523648
524073
  const firstRowIdx = items.findIndex((i4) => i4.type !== "section" && i4.type !== "info");
523649
- import_react136.useEffect(() => {
524074
+ import_react138.useEffect(() => {
523650
524075
  if (visible)
523651
524076
  setScope(detectInitialScope(projectConfig));
523652
524077
  }, [visible, projectConfig]);
523653
- import_react136.useEffect(() => {
524078
+ import_react138.useEffect(() => {
523654
524079
  setCursor(Math.max(0, firstRowIdx));
523655
524080
  }, [tab]);
523656
524081
  const isBudgetDisabled = vals.thinkingMode !== "enabled";
@@ -523708,8 +524133,8 @@ function ProviderSettings({
523708
524133
  }
523709
524134
  if (evt.name === "tab" || evt.shift && evt.name === "tab") {
523710
524135
  const dir = evt.shift ? -1 : 1;
523711
- const next = (tabIdx + dir + TABS5.length) % TABS5.length;
523712
- setTab(TABS5[next]);
524136
+ const next = (tabIdx + dir + TABS6.length) % TABS6.length;
524137
+ setTab(TABS6[next]);
523713
524138
  return;
523714
524139
  }
523715
524140
  if (evt.name === "up") {
@@ -523980,15 +524405,15 @@ function ProviderSettings({
523980
524405
  ]
523981
524406
  }, undefined, true, undefined, this);
523982
524407
  }
523983
- var import_react136, MAX_POPUP_WIDTH4 = 110, CHROME_ROWS6 = 10, TABS5, CLAUDE_ITEMS, OPENAI_ITEMS, GENERAL_ITEMS, GOOGLE_ITEMS, XAI_ITEMS, DEEPSEEK_ITEMS, OPENROUTER_ITEMS, COMPAT_ITEMS, TAB_ITEMS, DEFAULTS;
524408
+ var import_react138, MAX_POPUP_WIDTH4 = 110, CHROME_ROWS6 = 10, TABS6, CLAUDE_ITEMS, OPENAI_ITEMS, GENERAL_ITEMS, GOOGLE_ITEMS, XAI_ITEMS, DEEPSEEK_ITEMS, OPENROUTER_ITEMS, COMPAT_ITEMS, TAB_ITEMS, DEFAULTS;
523984
524409
  var init_ProviderSettings = __esm(async () => {
523985
524410
  init_theme();
523986
524411
  init_shared2();
523987
524412
  init_ui2();
523988
524413
  init_jsx_dev_runtime();
523989
524414
  await init_react2();
523990
- import_react136 = __toESM(require_react(), 1);
523991
- TABS5 = [
524415
+ import_react138 = __toESM(require_react(), 1);
524416
+ TABS6 = [
523992
524417
  "claude",
523993
524418
  "openai",
523994
524419
  "google",
@@ -524345,19 +524770,19 @@ function RepoMapStatusPopup({
524345
524770
  const { width: termCols, height: termRows } = useTerminalDimensions();
524346
524771
  const popupWidth = Math.min(POPUP_W, Math.floor(termCols * 0.8));
524347
524772
  const innerW = popupWidth - 2;
524348
- const stateRef = import_react138.useRef(useRepoMapStore.getState());
524349
- const [, setRenderTick] = import_react138.useState(0);
524350
- const spinnerRef = import_react138.useRef(0);
524773
+ const stateRef = import_react140.useRef(useRepoMapStore.getState());
524774
+ const [, setRenderTick] = import_react140.useState(0);
524775
+ const spinnerRef = import_react140.useRef(0);
524351
524776
  const initialMode = currentMode ?? "off";
524352
524777
  const initialLimit = currentLimit ?? 300;
524353
- const [selectedMode, setSelectedMode] = import_react138.useState(initialMode);
524354
- const [selectedLimit, setSelectedLimit] = import_react138.useState(initialLimit);
524355
- const [selectedAutoRegen, setSelectedAutoRegen] = import_react138.useState(currentAutoRegen ?? false);
524356
- const [selectedTokenBudget, setSelectedTokenBudget] = import_react138.useState(currentTokenBudget);
524357
- const [selectedScope, setSelectedScope] = import_react138.useState(currentScope ?? "project");
524358
- const [focusRow, setFocusRow] = import_react138.useState(0 /* Mode */);
524359
- const [confirmClear, setConfirmClear] = import_react138.useState(false);
524360
- import_react138.useEffect(() => {
524778
+ const [selectedMode, setSelectedMode] = import_react140.useState(initialMode);
524779
+ const [selectedLimit, setSelectedLimit] = import_react140.useState(initialLimit);
524780
+ const [selectedAutoRegen, setSelectedAutoRegen] = import_react140.useState(currentAutoRegen ?? false);
524781
+ const [selectedTokenBudget, setSelectedTokenBudget] = import_react140.useState(currentTokenBudget);
524782
+ const [selectedScope, setSelectedScope] = import_react140.useState(currentScope ?? "project");
524783
+ const [focusRow, setFocusRow] = import_react140.useState(0 /* Mode */);
524784
+ const [confirmClear, setConfirmClear] = import_react140.useState(false);
524785
+ import_react140.useEffect(() => {
524361
524786
  if (!visible)
524362
524787
  return;
524363
524788
  setSelectedMode(currentMode ?? "off");
@@ -524368,7 +524793,7 @@ function RepoMapStatusPopup({
524368
524793
  setFocusRow(0 /* Mode */);
524369
524794
  setConfirmClear(false);
524370
524795
  }, [visible, currentMode, currentLimit, currentAutoRegen, currentTokenBudget, currentScope]);
524371
- import_react138.useEffect(() => {
524796
+ import_react140.useEffect(() => {
524372
524797
  if (!visible)
524373
524798
  return;
524374
524799
  stateRef.current = useRepoMapStore.getState();
@@ -524378,7 +524803,7 @@ function RepoMapStatusPopup({
524378
524803
  setRenderTick((n) => n + 1);
524379
524804
  });
524380
524805
  }, [visible]);
524381
- import_react138.useEffect(() => {
524806
+ import_react140.useEffect(() => {
524382
524807
  if (!visible)
524383
524808
  return;
524384
524809
  const timer = setInterval(() => {
@@ -524736,7 +525161,7 @@ function RepoMapStatusPopup({
524736
525161
  }, undefined, true, undefined, this)
524737
525162
  }, undefined, false, undefined, this);
524738
525163
  }
524739
- var import_react138, LABEL_W = 18, POPUP_W = 72, SEMANTIC_MODES, MODE_DESCRIPTIONS, MODE_LABELS2, LLM_LIMIT_PRESETS, TOKEN_BUDGET_PRESETS;
525164
+ var import_react140, LABEL_W = 18, POPUP_W = 72, SEMANTIC_MODES, MODE_DESCRIPTIONS, MODE_LABELS2, LLM_LIMIT_PRESETS, TOKEN_BUDGET_PRESETS;
524740
525165
  var init_RepoMapStatusPopup = __esm(async () => {
524741
525166
  init_theme();
524742
525167
  init_repomap();
@@ -524744,7 +525169,7 @@ var init_RepoMapStatusPopup = __esm(async () => {
524744
525169
  init_ui2();
524745
525170
  init_jsx_dev_runtime();
524746
525171
  await init_react2();
524747
- import_react138 = __toESM(require_react(), 1);
525172
+ import_react140 = __toESM(require_react(), 1);
524748
525173
  SEMANTIC_MODES = ["off", "ast", "synthetic", "llm", "full"];
524749
525174
  MODE_DESCRIPTIONS = {
524750
525175
  off: "disabled",
@@ -524775,24 +525200,57 @@ function truncate4(s2, max) {
524775
525200
  function RouterSettings({
524776
525201
  visible,
524777
525202
  router: router2,
525203
+ defaultModel,
525204
+ modelFallback,
524778
525205
  activeModel,
524779
525206
  scope,
524780
525207
  onScopeChange,
524781
525208
  onPickSlot,
524782
525209
  onClearSlot,
524783
525210
  onPickerChange,
525211
+ onAddFallback,
525212
+ onClearFallbacks,
524784
525213
  onClose
524785
525214
  }) {
524786
525215
  const t2 = useTheme();
524787
525216
  const { width: tw2, height: th } = useTerminalDimensions();
524788
- const [cursor, setCursor] = import_react140.useState(0);
525217
+ const [cursor, setCursor] = import_react142.useState(0);
524789
525218
  const popupW = Math.min(100, Math.max(72, Math.floor(tw2 * 0.78)));
524790
525219
  const popupH = Math.min(40, Math.max(26, th - 4));
524791
525220
  const contentW = popupW - 4;
524792
- const rows = import_react140.useMemo(() => {
525221
+ const modelsInUse = import_react142.useMemo(() => {
525222
+ const set3 = new Set;
525223
+ if (defaultModel)
525224
+ set3.add(defaultModel);
525225
+ if (router2) {
525226
+ const keys2 = [
525227
+ "default",
525228
+ "spark",
525229
+ "ember",
525230
+ "webSearch",
525231
+ "desloppify",
525232
+ "verify",
525233
+ "compact",
525234
+ "semantic"
525235
+ ];
525236
+ for (const k5 of keys2) {
525237
+ const v4 = router2[k5];
525238
+ if (typeof v4 === "string" && v4.trim())
525239
+ set3.add(v4);
525240
+ }
525241
+ }
525242
+ return Array.from(set3);
525243
+ }, [router2, defaultModel]);
525244
+ const rows = import_react142.useMemo(() => {
524793
525245
  const out2 = [];
524794
525246
  for (const s2 of SECTIONS) {
524795
525247
  out2.push({ kind: "header", section: s2 });
525248
+ if (s2.id === "fallback") {
525249
+ for (const modelId of modelsInUse) {
525250
+ out2.push({ kind: "fallback", section: s2, modelId });
525251
+ }
525252
+ continue;
525253
+ }
524796
525254
  for (const d3 of s2.defs) {
524797
525255
  if (d3.kind === "slot")
524798
525256
  out2.push({ kind: "slot", section: s2, def: d3 });
@@ -524801,8 +525259,8 @@ function RouterSettings({
524801
525259
  }
524802
525260
  }
524803
525261
  return out2;
524804
- }, []);
524805
- const selectableIndices = import_react140.useMemo(() => rows.map((r4, i4) => r4.kind === "slot" || r4.kind === "picker" ? i4 : -1).filter((i4) => i4 >= 0), [rows]);
525262
+ }, [modelsInUse]);
525263
+ const selectableIndices = import_react142.useMemo(() => rows.map((r4, i4) => r4.kind === "header" ? -1 : i4).filter((i4) => i4 >= 0), [rows]);
524806
525264
  const moveItem = (dir) => {
524807
525265
  if (selectableIndices.length === 0)
524808
525266
  return;
@@ -524811,7 +525269,7 @@ function RouterSettings({
524811
525269
  const nextPos = (base + dir + selectableIndices.length) % selectableIndices.length;
524812
525270
  setCursor(selectableIndices[nextPos] ?? selectableIndices[0] ?? 0);
524813
525271
  };
524814
- import_react140.useMemo(() => {
525272
+ import_react142.useMemo(() => {
524815
525273
  if (cursor === 0 && selectableIndices.length > 0 && selectableIndices[0] !== 0) {
524816
525274
  setCursor(selectableIndices[0] ?? 0);
524817
525275
  }
@@ -524819,6 +525277,7 @@ function RouterSettings({
524819
525277
  const selectedRow = rows[cursor];
524820
525278
  const selectedSlot = selectedRow?.kind === "slot" ? selectedRow.def : null;
524821
525279
  const selectedPicker = selectedRow?.kind === "picker" ? selectedRow.def : null;
525280
+ const selectedFallbackModelId = selectedRow?.kind === "fallback" ? selectedRow.modelId : null;
524822
525281
  useKeyboard((evt) => {
524823
525282
  if (!visible)
524824
525283
  return;
@@ -524835,12 +525294,16 @@ function RouterSettings({
524835
525294
  return;
524836
525295
  }
524837
525296
  if (evt.name === "return") {
524838
- if (selectedSlot)
525297
+ if (selectedFallbackModelId)
525298
+ onAddFallback(selectedFallbackModelId);
525299
+ else if (selectedSlot)
524839
525300
  onPickSlot(selectedSlot.key);
524840
525301
  return;
524841
525302
  }
524842
525303
  if (evt.name === "d" || evt.name === "delete" || evt.name === "backspace") {
524843
- if (selectedSlot)
525304
+ if (selectedFallbackModelId)
525305
+ onClearFallbacks(selectedFallbackModelId);
525306
+ else if (selectedSlot)
524844
525307
  onClearSlot(selectedSlot.key);
524845
525308
  return;
524846
525309
  }
@@ -524872,7 +525335,7 @@ function RouterSettings({
524872
525335
  const labelCol = 12;
524873
525336
  const modelCol = Math.min(30, Math.max(18, Math.floor(contentW * 0.32)));
524874
525337
  const descCol = Math.max(8, contentW - 4 - labelCol - modelCol - 2);
524875
- const shortModel = (m5) => {
525338
+ const shortModel2 = (m5) => {
524876
525339
  const slash = m5.indexOf("/");
524877
525340
  return slash > 0 ? m5.slice(slash + 1) : m5;
524878
525341
  };
@@ -524882,7 +525345,7 @@ function RouterSettings({
524882
525345
  height: popupH,
524883
525346
  title: "Task Router",
524884
525347
  titleIcon: "router",
524885
- blurb: `${customCount}/${slotCount} set \xB7 ${scope} \xB7 default: ${shortModel(activeModel)}`,
525348
+ blurb: `${customCount}/${slotCount} set \xB7 ${scope} \xB7 default: ${shortModel2(activeModel)}`,
524886
525349
  footerHints: [
524887
525350
  { key: "\u2191\u2193", label: "nav" },
524888
525351
  { key: "Enter", label: "set" },
@@ -524908,13 +525371,56 @@ function RouterSettings({
524908
525371
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
524909
525372
  bg: t2.bgPopup,
524910
525373
  fg: t2.brandAlt,
524911
- attributes: BOLD18,
525374
+ attributes: BOLD19,
524912
525375
  children: row.section.title
524913
525376
  }, undefined, false, undefined, this)
524914
525377
  ]
524915
525378
  }, `h-${idx}`, true, undefined, this);
524916
525379
  }
524917
525380
  const isSelected = idx === cursor;
525381
+ if (row.kind === "fallback") {
525382
+ const rowBg2 = isSelected ? t2.bgPopupHighlight : t2.bgPopup;
525383
+ const fbs = modelFallback?.[row.modelId] ?? [];
525384
+ const fallbackLabelCol = Math.min(28, Math.max(18, Math.floor(contentW * 0.32)));
525385
+ const label2 = truncate4(shortModel2(row.modelId), fallbackLabelCol).padEnd(fallbackLabelCol).slice(0, fallbackLabelCol);
525386
+ return /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
525387
+ flexDirection: "row",
525388
+ height: 1,
525389
+ backgroundColor: rowBg2,
525390
+ children: [
525391
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
525392
+ bg: rowBg2,
525393
+ fg: isSelected ? t2.brandSecondary : t2.textFaint,
525394
+ attributes: BOLD19,
525395
+ children: isSelected ? "\u25B8 " : " "
525396
+ }, undefined, false, undefined, this),
525397
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
525398
+ bg: rowBg2,
525399
+ fg: t2.textPrimary,
525400
+ attributes: BOLD19,
525401
+ children: label2
525402
+ }, undefined, false, undefined, this),
525403
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("box", {
525404
+ flexGrow: 1,
525405
+ backgroundColor: rowBg2
525406
+ }, undefined, false, undefined, this),
525407
+ fbs.length > 0 ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
525408
+ bg: rowBg2,
525409
+ fg: t2.brandAlt,
525410
+ attributes: BOLD19,
525411
+ children: truncate4(`\u2192 ${fbs.map((m5) => shortModel2(m5)).join(", ")}`, Math.max(8, contentW - 4 - fallbackLabelCol - 2))
525412
+ }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
525413
+ bg: rowBg2,
525414
+ fg: t2.textDim,
525415
+ children: "\u2014"
525416
+ }, undefined, false, undefined, this),
525417
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
525418
+ bg: rowBg2,
525419
+ children: " "
525420
+ }, undefined, false, undefined, this)
525421
+ ]
525422
+ }, `f-${idx}`, true, undefined, this);
525423
+ }
524918
525424
  if (row.kind === "picker") {
524919
525425
  const cur = router2?.[row.def.key];
524920
525426
  const num = typeof cur === "number" ? cur : row.def.defaultValue;
@@ -524940,13 +525446,13 @@ function RouterSettings({
524940
525446
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
524941
525447
  bg: rowBg,
524942
525448
  fg: isSelected ? t2.brandSecondary : t2.textFaint,
524943
- attributes: BOLD18,
525449
+ attributes: BOLD19,
524944
525450
  children: isSelected ? "\u25B8 " : " "
524945
525451
  }, undefined, false, undefined, this),
524946
525452
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
524947
525453
  bg: rowBg,
524948
525454
  fg: t2.textPrimary,
524949
- attributes: BOLD18,
525455
+ attributes: BOLD19,
524950
525456
  children: label
524951
525457
  }, undefined, false, undefined, this),
524952
525458
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
@@ -524961,8 +525467,8 @@ function RouterSettings({
524961
525467
  modelId ? /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
524962
525468
  bg: rowBg,
524963
525469
  fg: t2.brandAlt,
524964
- attributes: BOLD18,
524965
- children: truncate4(shortModel(modelId), modelCol)
525470
+ attributes: BOLD19,
525471
+ children: truncate4(shortModel2(modelId), modelCol)
524966
525472
  }, undefined, false, undefined, this) : /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV("text", {
524967
525473
  bg: rowBg,
524968
525474
  fg: t2.textDim,
@@ -524987,14 +525493,14 @@ function RouterSettings({
524987
525493
  }, undefined, true, undefined, this)
524988
525494
  }, undefined, false, undefined, this);
524989
525495
  }
524990
- var import_react140, BOLD18 = 1, SECTIONS, ALL_DEFS;
525496
+ var import_react142, BOLD19 = 1, SECTIONS, ALL_DEFS;
524991
525497
  var init_RouterSettings = __esm(async () => {
524992
525498
  init_theme();
524993
525499
  init_shared2();
524994
525500
  init_ui2();
524995
525501
  init_jsx_dev_runtime();
524996
525502
  await init_react2();
524997
- import_react140 = __toESM(require_react(), 1);
525503
+ import_react142 = __toESM(require_react(), 1);
524998
525504
  SECTIONS = [
524999
525505
  {
525000
525506
  id: "main",
@@ -525066,6 +525572,12 @@ var init_RouterSettings = __esm(async () => {
525066
525572
  hint: "Symbol summaries"
525067
525573
  }
525068
525574
  ]
525575
+ },
525576
+ {
525577
+ id: "fallback",
525578
+ title: "Model Fallback",
525579
+ subtitle: "Per-model fallback chains for transient errors",
525580
+ defs: []
525069
525581
  }
525070
525582
  ];
525071
525583
  ALL_DEFS = SECTIONS.flatMap((s2) => s2.defs);
@@ -525207,25 +525719,25 @@ function SkillSearch({ visible, contextManager, onClose, onSystemMessage }) {
525207
525719
  const t2 = useTheme();
525208
525720
  const popupBg = t2.bgPopup;
525209
525721
  const popupHl = t2.bgPopupHighlight;
525210
- const [tab, setTab] = import_react142.useState("search");
525211
- const [query2, setQuery] = import_react142.useState("");
525212
- const [popular, setPopular] = import_react142.useState([]);
525213
- const [results, setResults] = import_react142.useState([]);
525214
- const [installed2, setInstalled] = import_react142.useState([]);
525215
- const [activeSkills, setActiveSkills] = import_react142.useState([]);
525216
- const [searching, setSearching] = import_react142.useState(false);
525217
- const [installing, setInstalling] = import_react142.useState(false);
525218
- const [pendingInstall, setPendingInstall] = import_react142.useState(null);
525219
- const [scopeCursor, setScopeCursor] = import_react142.useState(0);
525220
- const debounceRef = import_react142.useRef(null);
525722
+ const [tab, setTab] = import_react144.useState("search");
525723
+ const [query2, setQuery] = import_react144.useState("");
525724
+ const [popular, setPopular] = import_react144.useState([]);
525725
+ const [results, setResults] = import_react144.useState([]);
525726
+ const [installed2, setInstalled] = import_react144.useState([]);
525727
+ const [activeSkills, setActiveSkills] = import_react144.useState([]);
525728
+ const [searching, setSearching] = import_react144.useState(false);
525729
+ const [installing, setInstalling] = import_react144.useState(false);
525730
+ const [pendingInstall, setPendingInstall] = import_react144.useState(null);
525731
+ const [scopeCursor, setScopeCursor] = import_react144.useState(0);
525732
+ const debounceRef = import_react144.useRef(null);
525221
525733
  const isInProject = existsSync50(join58(process.cwd(), ".git"));
525222
525734
  const { width: termCols, height: termRows } = useTerminalDimensions();
525223
525735
  const containerRows = termRows - 2;
525224
525736
  const popupWidth = Math.min(MAX_POPUP_WIDTH5, Math.floor(termCols * 0.88));
525225
525737
  const maxVisible = Math.max(4, Math.floor(containerRows * 0.85) - CHROME_ROWS7);
525226
525738
  const contentW = popupWidth - 22 - 3;
525227
- const [cursor, setCursor] = import_react142.useState(0);
525228
- const resetScroll = import_react142.useCallback(() => setCursor(0), []);
525739
+ const [cursor, setCursor] = import_react144.useState(0);
525740
+ const resetScroll = import_react144.useCallback(() => setCursor(0), []);
525229
525741
  const filterQuery = query2.toLowerCase().trim();
525230
525742
  const installedNames = new Set(installed2.map((s2) => s2.name));
525231
525743
  const filteredInstalled = filterQuery ? installed2.filter((s2) => s2.name.toLowerCase().includes(filterQuery)) : installed2;
@@ -525238,13 +525750,13 @@ function SkillSearch({ visible, contextManager, onClose, onSystemMessage }) {
525238
525750
  return filteredInstalled.length;
525239
525751
  return filteredActive.length;
525240
525752
  })();
525241
- const refreshInstalled = import_react142.useCallback(() => {
525753
+ const refreshInstalled = import_react144.useCallback(() => {
525242
525754
  setInstalled(listInstalledSkills());
525243
525755
  }, []);
525244
- const refreshActive = import_react142.useCallback(() => {
525756
+ const refreshActive = import_react144.useCallback(() => {
525245
525757
  setActiveSkills(contextManager.getActiveSkills());
525246
525758
  }, [contextManager]);
525247
- import_react142.useEffect(() => {
525759
+ import_react144.useEffect(() => {
525248
525760
  if (visible) {
525249
525761
  setTab("search");
525250
525762
  setQuery("");
@@ -525255,7 +525767,7 @@ function SkillSearch({ visible, contextManager, onClose, onSystemMessage }) {
525255
525767
  listPopularSkills().then((r4) => setPopular(r4)).catch(() => {});
525256
525768
  }
525257
525769
  }, [visible, refreshActive, refreshInstalled]);
525258
- import_react142.useEffect(() => {
525770
+ import_react144.useEffect(() => {
525259
525771
  if (!visible || tab !== "search")
525260
525772
  return;
525261
525773
  if (debounceRef.current)
@@ -525281,7 +525793,7 @@ function SkillSearch({ visible, contextManager, onClose, onSystemMessage }) {
525281
525793
  clearTimeout(debounceRef.current);
525282
525794
  };
525283
525795
  }, [query2, visible, tab, popular.length]);
525284
- import_react142.useEffect(() => {
525796
+ import_react144.useEffect(() => {
525285
525797
  setQuery("");
525286
525798
  setResults([]);
525287
525799
  resetScroll();
@@ -525357,8 +525869,8 @@ function SkillSearch({ visible, contextManager, onClose, onSystemMessage }) {
525357
525869
  return;
525358
525870
  }
525359
525871
  if (evt.name === "tab") {
525360
- const idx = TABS6.indexOf(tab);
525361
- const next = TABS6[(idx + 1) % TABS6.length];
525872
+ const idx = TABS7.indexOf(tab);
525873
+ const next = TABS7[(idx + 1) % TABS7.length];
525362
525874
  setTab(next);
525363
525875
  return;
525364
525876
  }
@@ -525596,7 +526108,7 @@ function SkillSearch({ visible, contextManager, onClose, onSystemMessage }) {
525596
526108
  ]
525597
526109
  }, undefined, true, undefined, this);
525598
526110
  }
525599
- var import_react142, MAX_POPUP_WIDTH5 = 120, CHROME_ROWS7 = 9, TABS6;
526111
+ var import_react144, MAX_POPUP_WIDTH5 = 120, CHROME_ROWS7 = 9, TABS7;
525600
526112
  var init_SkillSearch = __esm(async () => {
525601
526113
  init_manager3();
525602
526114
  init_theme();
@@ -525606,22 +526118,22 @@ var init_SkillSearch = __esm(async () => {
525606
526118
  init_core4(),
525607
526119
  init_react2()
525608
526120
  ]);
525609
- import_react142 = __toESM(require_react(), 1);
525610
- TABS6 = ["search", "installed", "active"];
526121
+ import_react144 = __toESM(require_react(), 1);
526122
+ TABS7 = ["search", "installed", "active"];
525611
526123
  });
525612
526124
 
525613
526125
  // src/components/settings/ToolsPopup.tsx
525614
526126
  function ToolsPopup({ visible, disabledTools, onToggleTool, onClose }) {
525615
526127
  const { width: tw2, height: th } = useTerminalDimensions();
525616
- const [cursor, setCursor] = import_react144.useState(0);
525617
- import_react144.useEffect(() => {
526128
+ const [cursor, setCursor] = import_react146.useState(0);
526129
+ import_react146.useEffect(() => {
525618
526130
  if (visible)
525619
526131
  setCursor(0);
525620
526132
  }, [visible]);
525621
526133
  const popupW = Math.min(110, Math.max(72, tw2 - 4));
525622
526134
  const popupH = Math.min(32, Math.max(16, th - 4));
525623
526135
  const contentW = popupW - 4;
525624
- const groups = import_react144.useMemo(() => {
526136
+ const groups = import_react146.useMemo(() => {
525625
526137
  return [
525626
526138
  {
525627
526139
  id: "tools",
@@ -525637,7 +526149,7 @@ function ToolsPopup({ visible, disabledTools, onToggleTool, onClose }) {
525637
526149
  }
525638
526150
  ];
525639
526151
  }, [disabledTools]);
525640
- const rows = import_react144.useMemo(() => buildGroupedRows(groups, new Set(["tools"])), [groups]);
526152
+ const rows = import_react146.useMemo(() => buildGroupedRows(groups, new Set(["tools"])), [groups]);
525641
526153
  useKeyboard((evt) => {
525642
526154
  if (!visible)
525643
526155
  return;
@@ -525681,13 +526193,13 @@ function ToolsPopup({ visible, disabledTools, onToggleTool, onClose }) {
525681
526193
  }, undefined, false, undefined, this)
525682
526194
  }, undefined, false, undefined, this);
525683
526195
  }
525684
- var import_react144;
526196
+ var import_react146;
525685
526197
  var init_ToolsPopup = __esm(async () => {
525686
526198
  init_constants();
525687
526199
  init_ui2();
525688
526200
  init_jsx_dev_runtime();
525689
526201
  await init_react2();
525690
- import_react144 = __toESM(require_react(), 1);
526202
+ import_react146 = __toESM(require_react(), 1);
525691
526203
  });
525692
526204
 
525693
526205
  // src/components/modals/MemoryBrowser.tsx
@@ -525734,29 +526246,29 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
525734
526246
  const popupH = Math.min(36, Math.max(20, th - 4));
525735
526247
  const SIDEBAR_W4 = 22;
525736
526248
  const contentW = popupW - SIDEBAR_W4 - 9;
525737
- const [tab, setTab] = import_react146.useState("All");
525738
- const [query2, setQuery] = import_react146.useState("");
525739
- const [cursor, setCursor] = import_react146.useState(0);
525740
- const [generation, setGeneration] = import_react146.useState(0);
525741
- const [flash, setFlash] = import_react146.useState(null);
525742
- const [confirmPurge, setConfirmPurge] = import_react146.useState(false);
525743
- const [cleanupRows, setCleanupRows] = import_react146.useState([]);
525744
- const [cleanupSelected, setCleanupSelected] = import_react146.useState(new Map);
525745
- const [settingsModal, setSettingsModal] = import_react146.useState(null);
525746
- const cursorRef = import_react146.useRef(0);
526249
+ const [tab, setTab] = import_react148.useState("All");
526250
+ const [query2, setQuery] = import_react148.useState("");
526251
+ const [cursor, setCursor] = import_react148.useState(0);
526252
+ const [generation, setGeneration] = import_react148.useState(0);
526253
+ const [flash, setFlash] = import_react148.useState(null);
526254
+ const [confirmPurge, setConfirmPurge] = import_react148.useState(false);
526255
+ const [cleanupRows, setCleanupRows] = import_react148.useState([]);
526256
+ const [cleanupSelected, setCleanupSelected] = import_react148.useState(new Map);
526257
+ const [settingsModal, setSettingsModal] = import_react148.useState(null);
526258
+ const cursorRef = import_react148.useRef(0);
525747
526259
  cursorRef.current = cursor;
525748
- const popFlash = import_react146.useCallback((kind, message) => {
526260
+ const popFlash = import_react148.useCallback((kind, message) => {
525749
526261
  setFlash({ kind, message });
525750
526262
  setTimeout(() => setFlash(null), 1800);
525751
526263
  }, []);
525752
- const fileExists = import_react146.useCallback((p2) => {
526264
+ const fileExists = import_react148.useCallback((p2) => {
525753
526265
  try {
525754
526266
  return existsSync51(join59(cwd2, p2));
525755
526267
  } catch {
525756
526268
  return false;
525757
526269
  }
525758
526270
  }, [cwd2]);
525759
- const refreshCleanup = import_react146.useCallback(() => {
526271
+ const refreshCleanup = import_react148.useCallback(() => {
525760
526272
  const dupes = memMgr.findDuplicates("all");
525761
526273
  const dead = memMgr.findDeadFileRefs("all", fileExists);
525762
526274
  const stale = memMgr.staleCandidates("all", 25);
@@ -525830,7 +526342,7 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
525830
526342
  setCleanupRows(rows);
525831
526343
  setCleanupSelected(new Map);
525832
526344
  }, [memMgr, fileExists]);
525833
- import_react146.useEffect(() => {
526345
+ import_react148.useEffect(() => {
525834
526346
  if (!visible)
525835
526347
  return;
525836
526348
  setQuery("");
@@ -525841,13 +526353,13 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
525841
526353
  if (tab === "Cleanup")
525842
526354
  refreshCleanup();
525843
526355
  }, [visible, tab, refreshCleanup]);
525844
- const allRows = import_react146.useMemo(() => {
526356
+ const allRows = import_react148.useMemo(() => {
525845
526357
  return memMgr.list("all", { includeHidden: false }).map(toRow2);
525846
526358
  }, [memMgr, generation]);
525847
- const hiddenRows = import_react146.useMemo(() => {
526359
+ const hiddenRows = import_react148.useMemo(() => {
525848
526360
  return memMgr.list("all", { includeHidden: true }).filter((m5) => m5.hidden).map(toRow2);
525849
526361
  }, [memMgr, generation]);
525850
- const filteredRows = import_react146.useMemo(() => {
526362
+ const filteredRows = import_react148.useMemo(() => {
525851
526363
  const source = tab === "Hidden" ? hiddenRows : allRows;
525852
526364
  const fq = query2.toLowerCase().trim();
525853
526365
  if (!fq)
@@ -525857,7 +526369,7 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
525857
526369
  return hay.toLowerCase().includes(fq);
525858
526370
  });
525859
526371
  }, [allRows, hiddenRows, tab, query2]);
525860
- const settingsGroups = import_react146.useMemo(() => {
526372
+ const settingsGroups = import_react148.useMemo(() => {
525861
526373
  const scopeCfg = memMgr.scopeConfig;
525862
526374
  return [
525863
526375
  {
@@ -525887,8 +526399,8 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
525887
526399
  }
525888
526400
  ];
525889
526401
  }, [memMgr]);
525890
- const settingsRows = import_react146.useMemo(() => buildGroupedRows(settingsGroups, new Set(["scopes"])), [settingsGroups]);
525891
- import_react146.useEffect(() => {
526402
+ const settingsRows = import_react148.useMemo(() => buildGroupedRows(settingsGroups, new Set(["scopes"])), [settingsGroups]);
526403
+ import_react148.useEffect(() => {
525892
526404
  let len;
525893
526405
  if (tab === "Cleanup")
525894
526406
  len = cleanupRows.length + 1;
@@ -526027,9 +526539,9 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
526027
526539
  return;
526028
526540
  }
526029
526541
  if (evt.name === "tab") {
526030
- const idx = TABS7.indexOf(tab);
526542
+ const idx = TABS8.indexOf(tab);
526031
526543
  const dir = evt.shift ? -1 : 1;
526032
- const next = TABS7[(idx + dir + TABS7.length) % TABS7.length];
526544
+ const next = TABS8[(idx + dir + TABS8.length) % TABS8.length];
526033
526545
  if (next)
526034
526546
  setTab(next);
526035
526547
  setCursor(0);
@@ -526365,13 +526877,13 @@ function MemoryBrowser({ visible, contextManager, cwd: cwd2, onClose, onSystemMe
526365
526877
  }, undefined, true, undefined, this)
526366
526878
  }, undefined, false, undefined, this);
526367
526879
  }
526368
- var import_react146, SETTINGS_MODAL_TITLE, SETTINGS_MODAL_OPTIONS, TABS7, COLUMNS2, CLEANUP_COLUMNS;
526880
+ var import_react148, SETTINGS_MODAL_TITLE, SETTINGS_MODAL_OPTIONS, TABS8, COLUMNS2, CLEANUP_COLUMNS;
526369
526881
  var init_MemoryBrowser = __esm(async () => {
526370
526882
  init_theme();
526371
526883
  init_ui2();
526372
526884
  init_jsx_dev_runtime();
526373
526885
  await init_react2();
526374
- import_react146 = __toESM(require_react(), 1);
526886
+ import_react148 = __toESM(require_react(), 1);
526375
526887
  SETTINGS_MODAL_TITLE = {
526376
526888
  write: "Write Scope",
526377
526889
  read: "Read Scope",
@@ -526394,7 +526906,7 @@ var init_MemoryBrowser = __esm(async () => {
526394
526906
  { value: "global", label: "Global", description: "preferences saved everywhere" }
526395
526907
  ]
526396
526908
  };
526397
- TABS7 = ["All", "Hidden", "Cleanup", "Settings"];
526909
+ TABS8 = ["All", "Hidden", "Cleanup", "Settings"];
526398
526910
  COLUMNS2 = [
526399
526911
  { key: "scope", width: 4, render: (r4) => r4.scope === "project" ? "proj" : "glob" },
526400
526912
  { key: "cat", width: 7, render: (r4) => r4.category.slice(0, 7) },
@@ -526513,9 +527025,9 @@ function ShutdownSplash({
526513
527025
  height
526514
527026
  }) {
526515
527027
  const shortId = sessionId?.slice(0, 8);
526516
- const [tick, setTick] = import_react148.useState(0);
527028
+ const [tick, setTick] = import_react150.useState(0);
526517
527029
  const { width: termWidth } = useTerminalDimensions();
526518
- import_react148.useEffect(() => {
527030
+ import_react150.useEffect(() => {
526519
527031
  const timer = setInterval(() => setTick((t3) => t3 + 1), 80);
526520
527032
  return () => clearInterval(timer);
526521
527033
  }, []);
@@ -526718,12 +527230,12 @@ function App({
526718
527230
  const { height: termHeight, width: termWidth } = useTerminalDimensions();
526719
527231
  useThemeStore((s2) => s2.name);
526720
527232
  const t2 = useTheme();
526721
- const [providerStatuses, setProviderStatuses] = import_react148.useState(() => {
527233
+ const [providerStatuses, setProviderStatuses] = import_react150.useState(() => {
526722
527234
  return getCachedProviderStatuses() ?? bootProviders;
526723
527235
  });
526724
- const [shutdownPhase, setShutdownPhase] = import_react148.useState(-1);
526725
- const savedSessionIdRef = import_react148.useRef(null);
526726
- import_react148.useEffect(() => {
527236
+ const [shutdownPhase, setShutdownPhase] = import_react150.useState(-1);
527237
+ const savedSessionIdRef = import_react150.useRef(null);
527238
+ import_react150.useEffect(() => {
526727
527239
  const stdin = process.stdin;
526728
527240
  const originalRead = stdin.read.bind(stdin);
526729
527241
  const patchedRead = (size) => {
@@ -526746,16 +527258,16 @@ function App({
526746
527258
  stdin.read = originalRead;
526747
527259
  };
526748
527260
  }, []);
526749
- const copyToClipboard2 = import_react148.useCallback((text3) => {
527261
+ const copyToClipboard2 = import_react150.useCallback((text3) => {
526750
527262
  if (!renderer2.copyToClipboardOSC52(text3)) {
526751
527263
  copyToClipboard(text3);
526752
527264
  }
526753
527265
  }, [renderer2]);
526754
- import_react148.useEffect(() => {
527266
+ import_react150.useEffect(() => {
526755
527267
  setProviderStatuses(getCachedProviderStatuses() ?? bootProviders);
526756
527268
  }, [bootProviders]);
526757
- import_react148.useEffect(() => subscribeProviderStatuses(setProviderStatuses), []);
526758
- import_react148.useEffect(() => {
527269
+ import_react150.useEffect(() => subscribeProviderStatuses(setProviderStatuses), []);
527270
+ import_react150.useEffect(() => {
526759
527271
  const onSelection = (sel) => {
526760
527272
  const text3 = sel.getSelectedText();
526761
527273
  if (text3)
@@ -526766,22 +527278,22 @@ function App({
526766
527278
  renderer2.off("selection", onSelection);
526767
527279
  };
526768
527280
  }, [renderer2, copyToClipboard2]);
526769
- import_react148.useEffect(() => {
527281
+ import_react150.useEffect(() => {
526770
527282
  fetchOpenRouterMetadata();
526771
527283
  }, []);
526772
- const [globalConfig2, setGlobalConfig] = import_react148.useState(config2);
526773
- const [projConfig, setProjConfig] = import_react148.useState(projectConfig ?? null);
526774
- const [routerScope, setRouterScope] = import_react148.useState(() => projectConfig && ("taskRouter" in projectConfig) ? "project" : "global");
526775
- const modelScope = import_react148.useMemo(() => projConfig && ("defaultModel" in projConfig) ? "project" : "global", [projConfig]);
526776
- const effectiveConfig = import_react148.useMemo(() => mergeConfigs(globalConfig2, projConfig), [globalConfig2, projConfig]);
527284
+ const [globalConfig2, setGlobalConfig] = import_react150.useState(config2);
527285
+ const [projConfig, setProjConfig] = import_react150.useState(projectConfig ?? null);
527286
+ const [routerScope, setRouterScope] = import_react150.useState(() => projectConfig && ("taskRouter" in projectConfig) ? "project" : "global");
527287
+ const modelScope = import_react150.useMemo(() => projConfig && ("defaultModel" in projConfig) ? "project" : "global", [projConfig]);
527288
+ const effectiveConfig = import_react150.useMemo(() => mergeConfigs(globalConfig2, projConfig), [globalConfig2, projConfig]);
526777
527289
  const { focusMode, editorOpen, toggleEditor, openEditor, closeEditor, focusChat, focusEditor } = useEditorFocus();
526778
- const [editorVisible, setEditorVisible] = import_react148.useState(false);
527290
+ const [editorVisible, setEditorVisible] = import_react150.useState(false);
526779
527291
  const tabMgr = useTabs();
526780
- const tabMgrRef = import_react148.useRef(tabMgr);
527292
+ const tabMgrRef = import_react150.useRef(tabMgr);
526781
527293
  tabMgrRef.current = tabMgr;
526782
- const hasTabBarRef = import_react148.useRef(false);
527294
+ const hasTabBarRef = import_react150.useRef(false);
526783
527295
  hasTabBarRef.current = tabMgr.tabCount > 1;
526784
- const editorSplitRef = import_react148.useRef(60);
527296
+ const editorSplitRef = import_react150.useRef(60);
526785
527297
  const {
526786
527298
  ready: nvimReady,
526787
527299
  ptyWrite,
@@ -526797,8 +527309,8 @@ function App({
526797
527309
  openFile: nvimOpen,
526798
527310
  error: nvimError
526799
527311
  } = useNeovim(true, effectiveConfig.nvimPath, effectiveConfig.nvimConfig, closeEditor, hasTabBarRef.current, editorSplitRef.current);
526800
- const pendingEditorFileRef = import_react148.useRef(null);
526801
- import_react148.useEffect(() => {
527312
+ const pendingEditorFileRef = import_react150.useRef(null);
527313
+ import_react150.useEffect(() => {
526802
527314
  if (nvimReady && pendingEditorFileRef.current) {
526803
527315
  const file2 = pendingEditorFileRef.current;
526804
527316
  pendingEditorFileRef.current = null;
@@ -526807,7 +527319,7 @@ function App({
526807
527319
  });
526808
527320
  }
526809
527321
  }, [nvimReady, nvimOpen]);
526810
- const openEditorWithFile = import_react148.useCallback((file2) => {
527322
+ const openEditorWithFile = import_react150.useCallback((file2) => {
526811
527323
  if (editorOpen && nvimReady) {
526812
527324
  nvimOpen(file2).catch((err2) => {
526813
527325
  logBackgroundError("editor", `failed to open ${file2}: ${err2 instanceof Error ? err2.message : String(err2)}`);
@@ -526817,24 +527329,24 @@ function App({
526817
527329
  openEditor();
526818
527330
  }
526819
527331
  }, [editorOpen, nvimReady, nvimOpen, openEditor]);
526820
- import_react148.useEffect(() => {
527332
+ import_react150.useEffect(() => {
526821
527333
  setEditorRequestCallback((file2) => {
526822
527334
  if (file2)
526823
527335
  openEditorWithFile(file2);
526824
527336
  });
526825
527337
  return () => setEditorRequestCallback(null);
526826
527338
  }, [openEditorWithFile]);
526827
- import_react148.useEffect(() => {
527339
+ import_react150.useEffect(() => {
526828
527340
  if (editorOpen)
526829
527341
  setEditorVisible(true);
526830
527342
  }, [editorOpen]);
526831
527343
  const reasoningExpanded = useUIStore((s2) => s2.reasoningExpanded);
526832
527344
  const codeExpanded = useUIStore((s2) => s2.codeExpanded);
526833
527345
  const hasTabBar = tabMgr.tabCount > 1;
526834
- import_react148.useEffect(() => {
527346
+ import_react150.useEffect(() => {
526835
527347
  renderer2.requestRender();
526836
527348
  }, [editorOpen, editorVisible, focusMode, reasoningExpanded, codeExpanded, hasTabBar, renderer2]);
526837
- const handleEditorClosed = import_react148.useCallback(() => {
527349
+ const handleEditorClosed = import_react150.useCallback(() => {
526838
527350
  setEditorVisible(false);
526839
527351
  }, []);
526840
527352
  useEditorInput({
@@ -526867,6 +527379,7 @@ function App({
526867
527379
  const modalInfoPopup = useUIStore((s2) => s2.modals.infoPopup);
526868
527380
  const modalDiagnose = useUIStore((s2) => s2.modals.diagnosePopup);
526869
527381
  const modalStatusDashboard = useUIStore((s2) => s2.modals.statusDashboard);
527382
+ const modalModelEvents = useUIStore((s2) => s2.modals.modelEvents);
526870
527383
  const modalToolsPopup = useUIStore((s2) => s2.modals.toolsPopup);
526871
527384
  const modalMCPSettings = useUIStore((s2) => s2.modals.mcpSettings);
526872
527385
  const modalHearthSettings = useUIStore((s2) => s2.modals.hearthSettings);
@@ -526876,24 +527389,24 @@ function App({
526876
527389
  const modalMemoryBrowser = useUIStore((s2) => s2.modals.memoryBrowser);
526877
527390
  const modalUiDemo = useUIStore((s2) => s2.modals.uiDemo);
526878
527391
  const toolsState = useToolsStore();
526879
- import_react148.useEffect(() => {
527392
+ import_react150.useEffect(() => {
526880
527393
  toolsState.initFromConfig(effectiveConfig.disabledTools);
526881
527394
  }, [effectiveConfig.disabledTools, toolsState.initFromConfig]);
526882
- import_react148.useEffect(() => {
527395
+ import_react150.useEffect(() => {
526883
527396
  saveGlobalConfig({ disabledTools: [...toolsState.disabledTools] });
526884
527397
  }, [toolsState.disabledTools]);
526885
527398
  const statusDashboardTab = useUIStore((s2) => s2.statusDashboardTab);
526886
527399
  const modalRepoMapStatus = useUIStore((s2) => s2.modals.repoMapStatus);
526887
527400
  const isModalOpen = useUIStore(selectIsAnyModalOpen);
526888
- const wizardOpenedLlm = import_react148.useRef(false);
526889
- const closerCache2 = import_react148.useRef({});
527401
+ const wizardOpenedLlm = import_react150.useRef(false);
527402
+ const closerCache2 = import_react150.useRef({});
526890
527403
  const getCloser2 = (name39) => closerCache2.current[name39] ??= () => useUIStore.getState().closeModal(name39);
526891
527404
  useVersionCheck();
526892
527405
  const versionCurrent = useVersionStore((s2) => s2.current);
526893
527406
  const versionLatest = useVersionStore((s2) => s2.latest);
526894
527407
  const versionUpdateAvailable = useVersionStore((s2) => s2.updateAvailable);
526895
- const updateModalShown = import_react148.useRef(false);
526896
- import_react148.useEffect(() => {
527408
+ const updateModalShown = import_react150.useRef(false);
527409
+ import_react150.useEffect(() => {
526897
527410
  if (!versionUpdateAvailable || !versionLatest || updateModalShown.current)
526898
527411
  return;
526899
527412
  if (isDismissed(versionLatest))
@@ -526907,7 +527420,7 @@ function App({
526907
527420
  }, 500);
526908
527421
  return () => clearTimeout(timer);
526909
527422
  }, [versionUpdateAvailable, versionLatest]);
526910
- import_react148.useEffect(() => {
527423
+ import_react150.useEffect(() => {
526911
527424
  if (getMissingRequired().length > 0) {
526912
527425
  useUIStore.getState().openModal("setup");
526913
527426
  } else if (forceWizard || !config2.onboardingComplete && !resumeSessionId) {
@@ -526915,7 +527428,7 @@ function App({
526915
527428
  }
526916
527429
  }, [config2.onboardingComplete, forceWizard, resumeSessionId]);
526917
527430
  const cwd2 = process.cwd();
526918
- const saveToScope = import_react148.useCallback((patch, toScope, fromScope) => {
527431
+ const saveToScope = import_react150.useCallback((patch, toScope, fromScope) => {
526919
527432
  if (toScope === "global") {
526920
527433
  saveGlobalConfig(patch);
526921
527434
  setGlobalConfig((prev) => applyConfigPatch(prev, patch));
@@ -526935,12 +527448,12 @@ function App({
526935
527448
  }
526936
527449
  }
526937
527450
  }, [cwd2]);
526938
- const detectScope = import_react148.useCallback((key3) => {
527451
+ const detectScope = import_react150.useCallback((key3) => {
526939
527452
  if (projConfig && key3 in projConfig)
526940
527453
  return "project";
526941
527454
  return "global";
526942
527455
  }, [projConfig]);
526943
- import_react148.useEffect(() => {
527456
+ import_react150.useEffect(() => {
526944
527457
  initForbidden(cwd2);
526945
527458
  for (const cfg of PROVIDER_CONFIGS) {
526946
527459
  if (cfg.grouped)
@@ -526949,19 +527462,19 @@ function App({
526949
527462
  fetchProviderModels(cfg.id).catch(() => {});
526950
527463
  }
526951
527464
  }, []);
526952
- const contextManager = import_react148.useMemo(() => preloadedContextManager ?? new ContextManager(cwd2), [cwd2, preloadedContextManager]);
526953
- const sessionManager = import_react148.useMemo(() => new SessionManager(cwd2), [cwd2]);
526954
- const mcpManager = import_react148.useMemo(() => getMCPManager(), []);
526955
- import_react148.useEffect(() => {
527465
+ const contextManager = import_react150.useMemo(() => preloadedContextManager ?? new ContextManager(cwd2), [cwd2, preloadedContextManager]);
527466
+ const sessionManager = import_react150.useMemo(() => new SessionManager(cwd2), [cwd2]);
527467
+ const mcpManager = import_react150.useMemo(() => getMCPManager(), []);
527468
+ import_react150.useEffect(() => {
526956
527469
  mcpManager.connectAll(effectiveConfig.mcpServers ?? []);
526957
527470
  }, [mcpManager, effectiveConfig.mcpServers]);
526958
- import_react148.useEffect(() => {
527471
+ import_react150.useEffect(() => {
526959
527472
  return () => {
526960
527473
  disposeMCPManager();
526961
527474
  };
526962
527475
  }, []);
526963
527476
  const git = useGitStatus(cwd2);
526964
- const [forgeMode, setForgeModeHeader] = import_react148.useState("default");
527477
+ const [forgeMode, setForgeModeHeader] = import_react150.useState("default");
526965
527478
  const modeLabel = getModeLabel(forgeMode);
526966
527479
  const modeColor = getModeColor(forgeMode);
526967
527480
  useConfigSync({
@@ -526975,7 +527488,7 @@ function App({
526975
527488
  cursorCol,
526976
527489
  visualSelection
526977
527490
  });
526978
- const handleSuspend = import_react148.useCallback(async (opts) => {
527491
+ const handleSuspend = import_react150.useCallback(async (opts) => {
526979
527492
  useUIStore.getState().setSuspended(true);
526980
527493
  await new Promise((r4) => setTimeout(r4, 50));
526981
527494
  const result = await suspendAndRun({ ...opts, cwd: cwd2 });
@@ -526995,33 +527508,33 @@ function App({
526995
527508
  git.refresh();
526996
527509
  }, [cwd2, git]);
526997
527510
  editorSplitRef.current = editorSplit;
526998
- const sharedResources = import_react148.useMemo(() => ({
527511
+ const sharedResources = import_react150.useMemo(() => ({
526999
527512
  ...contextManager.getSharedResources(),
527000
527513
  workspaceCoordinator: getWorkspaceCoordinator()
527001
527514
  }), [contextManager]);
527002
- const workspaceSnapshotRef = import_react148.useRef(null);
527515
+ const workspaceSnapshotRef = import_react150.useRef(null);
527003
527516
  workspaceSnapshotRef.current = () => ({
527004
527517
  tabStates: tabMgr.getAllTabStates(),
527005
527518
  activeTabId: tabMgr.activeTabId
527006
527519
  });
527007
- const getWorkspaceSnapshot = import_react148.useCallback(() => workspaceSnapshotRef.current?.() ?? {
527520
+ const getWorkspaceSnapshot = import_react150.useCallback(() => workspaceSnapshotRef.current?.() ?? {
527008
527521
  tabStates: [],
527009
527522
  activeTabId: ""
527010
527523
  }, []);
527011
- const addSystemMessage = import_react148.useCallback((msg) => {
527524
+ const addSystemMessage = import_react150.useCallback((msg) => {
527012
527525
  const activeChat = tabMgrRef.current?.getActiveChat();
527013
527526
  activeChat?.setMessages((prev) => [
527014
527527
  ...prev,
527015
527528
  { id: crypto.randomUUID(), role: "system", content: msg, timestamp: Date.now() }
527016
527529
  ]);
527017
527530
  }, []);
527018
- const refreshGit = import_react148.useCallback(() => {
527531
+ const refreshGit = import_react150.useCallback(() => {
527019
527532
  git.refresh();
527020
527533
  }, [git]);
527021
- const shutdownPhaseRef = import_react148.useRef(shutdownPhase);
527534
+ const shutdownPhaseRef = import_react150.useRef(shutdownPhase);
527022
527535
  shutdownPhaseRef.current = shutdownPhase;
527023
- const exitTimersRef = import_react148.useRef([]);
527024
- const handleCycleTab = import_react148.useCallback((direction) => {
527536
+ const exitTimersRef = import_react150.useRef([]);
527537
+ const handleCycleTab = import_react150.useCallback((direction) => {
527025
527538
  if (tabMgr.tabCount <= 1)
527026
527539
  return;
527027
527540
  if (direction === 1)
@@ -527029,7 +527542,7 @@ function App({
527029
527542
  else
527030
527543
  tabMgr.prevTab();
527031
527544
  }, [tabMgr.tabCount, tabMgr.nextTab, tabMgr.prevTab]);
527032
- const handleExit = import_react148.useCallback(() => {
527545
+ const handleExit = import_react150.useCallback(() => {
527033
527546
  if (shutdownPhaseRef.current >= 0)
527034
527547
  return;
527035
527548
  setShutdownPhase(0);
@@ -527083,8 +527596,8 @@ function App({
527083
527596
  }, 300);
527084
527597
  }, 250);
527085
527598
  }, [cwd2, sessionManager, contextManager, renderer2]);
527086
- const hasRestoredRef = import_react148.useRef(false);
527087
- import_react148.useEffect(() => {
527599
+ const hasRestoredRef = import_react150.useRef(false);
527600
+ import_react150.useEffect(() => {
527088
527601
  if (hasRestoredRef.current || !resumeSessionId)
527089
527602
  return;
527090
527603
  hasRestoredRef.current = true;
@@ -527118,8 +527631,8 @@ function App({
527118
527631
  }, 100);
527119
527632
  }
527120
527633
  }, []);
527121
- const hasBootedHearthRef = import_react148.useRef(false);
527122
- import_react148.useEffect(() => {
527634
+ const hasBootedHearthRef = import_react150.useRef(false);
527635
+ import_react150.useEffect(() => {
527123
527636
  if (hasBootedHearthRef.current)
527124
527637
  return;
527125
527638
  hasBootedHearthRef.current = true;
@@ -527334,9 +527847,9 @@ function App({
527334
527847
  } catch {}
527335
527848
  })();
527336
527849
  }, []);
527337
- const [activeModelForHeader, setActiveModelForHeader] = import_react148.useState(effectiveConfig.defaultModel);
527338
- const activeChatRef = import_react148.useRef(null);
527339
- import_react148.useEffect(() => {
527850
+ const [activeModelForHeader, setActiveModelForHeader] = import_react150.useState(effectiveConfig.defaultModel);
527851
+ const activeChatRef = import_react150.useRef(null);
527852
+ import_react150.useEffect(() => {
527340
527853
  const chat = tabMgr.getActiveChat();
527341
527854
  activeChatRef.current = chat;
527342
527855
  if (chat) {
@@ -527346,7 +527859,7 @@ function App({
527346
527859
  setExitSessionId(hasContent ? chat.sessionId : null);
527347
527860
  }
527348
527861
  }, [tabMgr.activeTabId]);
527349
- import_react148.useEffect(() => {
527862
+ import_react150.useEffect(() => {
527350
527863
  if (tabMgr.tabCount <= 1)
527351
527864
  return;
527352
527865
  (async () => {
@@ -527364,7 +527877,7 @@ function App({
527364
527877
  } catch {}
527365
527878
  })();
527366
527879
  }, [tabMgr.tabCount, tabMgr.activeTabId]);
527367
- const { displayProvider, displayModel, isGateway, isProxy } = import_react148.useMemo(() => {
527880
+ const { displayProvider, displayModel, isGateway, isProxy } = import_react150.useMemo(() => {
527368
527881
  const model = activeModelForHeader;
527369
527882
  if (model === "none") {
527370
527883
  return {
@@ -527395,11 +527908,11 @@ function App({
527395
527908
  isProxy: false
527396
527909
  };
527397
527910
  }, [activeModelForHeader]);
527398
- import_react148.useEffect(() => {
527911
+ import_react150.useEffect(() => {
527399
527912
  if (nvimError)
527400
527913
  addSystemMessage(`Neovim error: ${nvimError}`);
527401
527914
  }, [nvimError]);
527402
- import_react148.useEffect(() => {
527915
+ import_react150.useEffect(() => {
527403
527916
  const memMgr = contextManager.getMemoryManager();
527404
527917
  const { project: project2, global: global2 } = memMgr.getLegacyBackupPaths();
527405
527918
  const parts2 = [];
@@ -527411,7 +527924,7 @@ function App({
527411
527924
  addSystemMessage(`Legacy memory schema detected \u2014 your old DB was preserved at ${parts2.join(", ")}. Phase 1 schema is now active in .soulforge/memory.db.`);
527412
527925
  }
527413
527926
  }, []);
527414
- const handleNewSession = import_react148.useCallback(async () => {
527927
+ const handleNewSession = import_react150.useCallback(async () => {
527415
527928
  const activeChat = tabMgrRef.current?.getActiveChat();
527416
527929
  const hasContent = activeChat?.messages.some((m5) => m5.role === "user" || m5.role === "assistant");
527417
527930
  if (hasContent && activeChat) {
@@ -527440,7 +527953,7 @@ function App({
527440
527953
  cpStore.skipCleanup(tab.id);
527441
527954
  restart();
527442
527955
  }, [cwd2, sessionManager]);
527443
- const handleTabCommand = import_react148.useCallback((input, chat) => {
527956
+ const handleTabCommand = import_react150.useCallback((input, chat) => {
527444
527957
  const cmd = input.trim().toLowerCase().split(/\s+/)[0] ?? "";
527445
527958
  const twoWord = input.trim().toLowerCase().split(/\s+/).slice(0, 2).join(" ");
527446
527959
  if (chat.isLoading && (ABORT_ON_LOADING.has(cmd) || ABORT_ON_LOADING.has(twoWord))) {
@@ -527577,24 +528090,26 @@ function App({
527577
528090
  handleNewSession,
527578
528091
  effectiveConfig.watchdog
527579
528092
  ]);
527580
- const closeLlmSelector = import_react148.useCallback(() => {
528093
+ const closeLlmSelector = import_react150.useCallback(() => {
527581
528094
  const wasPickingSlot = useUIStore.getState().routerSlotPicking != null;
528095
+ const wasFallbackForModel = useUIStore.getState().fallbackForModel != null;
527582
528096
  const wasFromWizard = wizardOpenedLlm.current;
527583
528097
  useUIStore.getState().closeModal("llmSelector");
527584
528098
  useUIStore.getState().setRouterSlotPicking(null);
528099
+ useUIStore.getState().setFallbackForModel(null);
527585
528100
  wizardOpenedLlm.current = false;
527586
- if (wasPickingSlot) {
528101
+ if (wasPickingSlot || wasFallbackForModel) {
527587
528102
  useUIStore.getState().openModal("routerSettings");
527588
528103
  } else if (wasFromWizard) {
527589
528104
  useUIStore.getState().openModal("firstRunWizard");
527590
528105
  }
527591
528106
  }, []);
527592
- const closeInfoPopup = import_react148.useCallback(() => {
528107
+ const closeInfoPopup = import_react150.useCallback(() => {
527593
528108
  const cfg = useUIStore.getState().infoPopupConfig;
527594
528109
  useUIStore.getState().closeInfoPopup();
527595
528110
  cfg?.onClose?.();
527596
528111
  }, []);
527597
- const onGitMenuCommit = import_react148.useCallback(() => {
528112
+ const onGitMenuCommit = import_react150.useCallback(() => {
527598
528113
  useUIStore.getState().closeModal("gitMenu");
527599
528114
  useUIStore.getState().openModal("gitCommit");
527600
528115
  }, []);
@@ -527607,7 +528122,7 @@ function App({
527607
528122
  renderer: renderer2,
527608
528123
  copyToClipboard: copyToClipboard2,
527609
528124
  activeChatRef,
527610
- cycleMode: import_react148.useCallback(() => {
528125
+ cycleMode: import_react150.useCallback(() => {
527611
528126
  const chat = tabMgrRef.current?.getActiveChat();
527612
528127
  if (chat) {
527613
528128
  const next = chat.cycleMode();
@@ -527841,6 +528356,9 @@ function App({
527841
528356
  onSuspend: handleSuspend,
527842
528357
  onCommand: handleTabCommand,
527843
528358
  onModeChange: setForgeModeHeader,
528359
+ onModelChange: (modelId) => {
528360
+ setActiveModelForHeader(modelId);
528361
+ },
527844
528362
  onExit: handleExit,
527845
528363
  registerChat: tabMgr.registerChat,
527846
528364
  unregisterChat: tabMgr.unregisterChat,
@@ -527874,24 +528392,27 @@ function App({
527874
528392
  activeModel: activeModelForHeader,
527875
528393
  onSelect: (modelId) => {
527876
528394
  const slot = useUIStore.getState().routerSlotPicking;
528395
+ const fallbackForModel = useUIStore.getState().fallbackForModel;
528396
+ if (fallbackForModel) {
528397
+ const current = { ...effectiveConfig.modelFallback ?? {} };
528398
+ const fallbacks = [...current[fallbackForModel] ?? []];
528399
+ fallbacks.push(modelId);
528400
+ current[fallbackForModel] = fallbacks;
528401
+ saveToScope({ modelFallback: current }, modelScope);
528402
+ closeLlmSelector();
528403
+ return;
528404
+ }
527877
528405
  if (slot) {
527878
528406
  const current = effectiveConfig.taskRouter ?? DEFAULT_TASK_ROUTER;
527879
528407
  const updated = { ...current, [slot]: modelId };
527880
528408
  saveToScope({ taskRouter: updated }, routerScope);
527881
- useUIStore.getState().setRouterSlotPicking(null);
527882
- useUIStore.getState().closeModal("llmSelector");
527883
- useUIStore.getState().openModal("routerSettings");
528409
+ closeLlmSelector();
527884
528410
  } else {
527885
528411
  activeChatRef.current?.setActiveModel(modelId);
527886
528412
  notifyProviderSwitch(modelId);
527887
528413
  setActiveModelForHeader(modelId);
527888
528414
  saveToScope({ defaultModel: modelId }, modelScope);
527889
- const wasFromWizard = wizardOpenedLlm.current;
527890
- wizardOpenedLlm.current = false;
527891
- useUIStore.getState().closeModal("llmSelector");
527892
- if (wasFromWizard) {
527893
- useUIStore.getState().openModal("firstRunWizard");
527894
- }
528415
+ closeLlmSelector();
527895
528416
  }
527896
528417
  },
527897
528418
  onClose: closeLlmSelector
@@ -528011,6 +528532,8 @@ function App({
528011
528532
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(RouterSettings, {
528012
528533
  visible: modalRouterSettings && !routerSlotPicking,
528013
528534
  router: effectiveConfig.taskRouter,
528535
+ defaultModel: effectiveConfig.defaultModel,
528536
+ modelFallback: effectiveConfig.modelFallback,
528014
528537
  activeModel: activeModelForHeader,
528015
528538
  scope: routerScope,
528016
528539
  onScopeChange: (toScope, fromScope) => {
@@ -528033,6 +528556,15 @@ function App({
528033
528556
  const updated = { ...current, [key3]: value };
528034
528557
  saveToScope({ taskRouter: updated }, routerScope);
528035
528558
  },
528559
+ onAddFallback: (modelId) => {
528560
+ useUIStore.getState().setFallbackForModel(modelId);
528561
+ useUIStore.getState().openModal("llmSelector");
528562
+ },
528563
+ onClearFallbacks: (modelId) => {
528564
+ const current = { ...effectiveConfig.modelFallback ?? {} };
528565
+ delete current[modelId];
528566
+ saveToScope({ modelFallback: current }, modelScope);
528567
+ },
528036
528568
  onClose: getCloser2("routerSettings")
528037
528569
  }, undefined, false, undefined, this),
528038
528570
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(CommandPalette, {
@@ -528069,6 +528601,10 @@ function App({
528069
528601
  onClose: getCloser2("diagnosePopup"),
528070
528602
  runHealthCheck: runIntelligenceHealthCheck
528071
528603
  }, undefined, false, undefined, this),
528604
+ /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(ModelEventsPopup, {
528605
+ visible: modalModelEvents,
528606
+ onClose: getCloser2("modelEvents")
528607
+ }, undefined, false, undefined, this),
528072
528608
  /* @__PURE__ */ import_jsx_dev_runtime2.jsxDEV(RepoMapStatusPopup, {
528073
528609
  visible: modalRepoMapStatus,
528074
528610
  onClose: getCloser2("repoMapStatus"),
@@ -528157,7 +528693,7 @@ function App({
528157
528693
  ]
528158
528694
  }, undefined, true, undefined, this);
528159
528695
  }
528160
- var import_react148, ABORT_ON_LOADING, DEFAULT_TASK_ROUTER, SHUTDOWN_STEPS, KITTY_PROTOCOL_RESPONSE_RE;
528696
+ var import_react150, ABORT_ON_LOADING, DEFAULT_TASK_ROUTER, SHUTDOWN_STEPS, KITTY_PROTOCOL_RESPONSE_RE;
528161
528697
  var init_App = __esm(async () => {
528162
528698
  init_shallow2();
528163
528699
  init_config2();
@@ -528229,6 +528765,7 @@ var init_App = __esm(async () => {
528229
528765
  init_HearthSettings(),
528230
528766
  init_LspInstallSearch(),
528231
528767
  init_MCPSettings(),
528768
+ init_ModelEventsPopup(),
528232
528769
  init_ProviderSettings(),
528233
528770
  init_RepoMapStatusPopup(),
528234
528771
  init_RouterSettings(),
@@ -528236,7 +528773,7 @@ var init_App = __esm(async () => {
528236
528773
  init_ToolsPopup(),
528237
528774
  init_MemoryBrowser()
528238
528775
  ]);
528239
- import_react148 = __toESM(require_react(), 1);
528776
+ import_react150 = __toESM(require_react(), 1);
528240
528777
  startMemoryPoll();
528241
528778
  ABORT_ON_LOADING = new Set([
528242
528779
  "/clear",