@sayknow-cli/coding-agent 0.5.1 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +185 -14
  2. package/dist/types/cli/update-cli.d.ts +8 -1
  3. package/dist/types/commands/session.d.ts +7 -0
  4. package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
  5. package/dist/types/config/keybindings.d.ts +10 -0
  6. package/dist/types/config/model-registry.d.ts +1 -1
  7. package/dist/types/config/models-config-schema.d.ts +4 -0
  8. package/dist/types/config/settings-schema.d.ts +9 -0
  9. package/dist/types/config/telegram-autostart.d.ts +9 -1
  10. package/dist/types/dap/client.d.ts +1 -0
  11. package/dist/types/lsp/client.d.ts +5 -0
  12. package/dist/types/modes/components/pet-capability.d.ts +8 -7
  13. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  14. package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
  15. package/dist/types/modes/components/welcome.d.ts +1 -0
  16. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +13 -1
  18. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
  19. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
  20. package/dist/types/modes/types.d.ts +7 -1
  21. package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
  22. package/dist/types/session/agent-session.d.ts +9 -0
  23. package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
  24. package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
  25. package/dist/types/session/session-manager.d.ts +9 -0
  26. package/dist/types/session/session-storage.d.ts +17 -0
  27. package/dist/types/session-import/claude.d.ts +26 -0
  28. package/dist/types/session-import/codex.d.ts +22 -0
  29. package/dist/types/session-import/command.d.ts +20 -0
  30. package/dist/types/session-import/detect.d.ts +22 -0
  31. package/dist/types/session-import/index.d.ts +7 -0
  32. package/dist/types/session-import/redact.d.ts +23 -0
  33. package/dist/types/session-import/service.d.ts +28 -0
  34. package/dist/types/session-import/types.d.ts +125 -0
  35. package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
  36. package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
  37. package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
  38. package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
  39. package/dist/types/skc-runtime/session-restore.d.ts +99 -0
  40. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
  41. package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
  42. package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
  43. package/dist/types/slash-commands/types.d.ts +5 -0
  44. package/dist/types/tools/ask.d.ts +169 -4
  45. package/package.json +16 -7
  46. package/scripts/generate-sdk-operation-inventory.ts +5 -6
  47. package/src/cli/config-cli.ts +23 -8
  48. package/src/cli/update-cli.ts +265 -21
  49. package/src/commands/session.ts +88 -2
  50. package/src/config/atomic-yaml-patch.ts +167 -30
  51. package/src/config/keybindings.ts +10 -0
  52. package/src/config/model-profiles.ts +69 -1
  53. package/src/config/model-registry.ts +72 -38
  54. package/src/config/models-config-schema.ts +1 -1
  55. package/src/config/settings-schema.ts +9 -0
  56. package/src/config/settings.ts +13 -2
  57. package/src/config/telegram-autostart.ts +11 -4
  58. package/src/dap/client.ts +78 -30
  59. package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
  60. package/src/internal-urls/docs-index.generated.ts +5 -4
  61. package/src/lsp/client.ts +17 -29
  62. package/src/main.ts +1 -1
  63. package/src/modes/action-registry.ts +2 -0
  64. package/src/modes/components/pet-capability.ts +22 -13
  65. package/src/modes/components/pet-selector.ts +1 -1
  66. package/src/modes/components/sayknow-pet-widget.ts +41 -7
  67. package/src/modes/components/welcome.ts +5 -0
  68. package/src/modes/controllers/event-controller.ts +1 -1
  69. package/src/modes/controllers/goal-mode-controller.ts +7 -2
  70. package/src/modes/controllers/input-controller.ts +71 -6
  71. package/src/modes/controllers/plan-mode-controller.ts +8 -1
  72. package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
  73. package/src/modes/controllers/selector-controller.ts +47 -19
  74. package/src/modes/interactive-mode.ts +54 -4
  75. package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
  76. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
  77. package/src/modes/types.ts +5 -1
  78. package/src/notifications/lifecycle-control-runtime.ts +258 -179
  79. package/src/prompts/system/eager-todo.md +2 -0
  80. package/src/prompts/system/plan-mode-approved.md +1 -1
  81. package/src/prompts/system/system-prompt.md +4 -2
  82. package/src/runtime-mcp/transports/stdio.ts +96 -33
  83. package/src/sdk/broker/lifecycle.ts +6 -5
  84. package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
  85. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  86. package/src/sdk/session.ts +25 -18
  87. package/src/session/agent-session.ts +120 -25
  88. package/src/session/internal/managed-session-scope.ts +41 -6
  89. package/src/session/internal/managed-session-storage.ts +14 -0
  90. package/src/session/session-manager.ts +113 -11
  91. package/src/session/session-storage.ts +70 -0
  92. package/src/session-import/claude.ts +382 -0
  93. package/src/session-import/codex.ts +458 -0
  94. package/src/session-import/command.ts +61 -0
  95. package/src/session-import/detect.ts +133 -0
  96. package/src/session-import/index.ts +27 -0
  97. package/src/session-import/redact.ts +125 -0
  98. package/src/session-import/service.ts +749 -0
  99. package/src/session-import/types.ts +163 -0
  100. package/src/skc-runtime/boot-generation.ts +172 -0
  101. package/src/skc-runtime/launch-tmux.ts +219 -41
  102. package/src/skc-runtime/launch-worktree.ts +277 -4
  103. package/src/skc-runtime/session-restore-runtime.ts +120 -0
  104. package/src/skc-runtime/session-restore.ts +296 -0
  105. package/src/skc-runtime/session-state-sidecar.ts +41 -0
  106. package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
  107. package/src/skc-runtime/tmux-sessions.ts +284 -108
  108. package/src/slash-commands/acp-builtins.ts +5 -1
  109. package/src/slash-commands/builtin-registry.ts +109 -5
  110. package/src/slash-commands/types.ts +5 -0
  111. package/src/tools/ask.ts +188 -10
  112. package/src/tools/eval.ts +2 -2
  113. package/src/export/html/template.css +0 -1060
  114. package/src/export/html/template.html +0 -47
  115. package/src/export/html/template.js +0 -2348
  116. package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
  117. package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
  118. package/vendor/insane-search/engine/tests/test_u1.py +0 -200
  119. package/vendor/insane-search/engine/tests/test_u4.py +0 -131
  120. package/vendor/insane-search/engine/tests/test_u5.py +0 -163
  121. package/vendor/insane-search/engine/tests/test_u7.py +0 -124
  122. package/vendor/insane-search/engine/tests/test_u8.py +0 -216
@@ -93,7 +93,31 @@ export class AtomicYamlReplaceError extends Error {
93
93
  this.name = "AtomicYamlReplaceError";
94
94
  }
95
95
  }
96
+ /** Raised when the configured lexical route no longer resolves to its original target. */
97
+ export class AtomicYamlRetargetError extends Error {
98
+ readonly code = "ATOMIC_YAML_RETARGETED";
96
99
 
100
+ constructor() {
101
+ super("Atomic YAML configuration target changed before publication.");
102
+ this.name = "AtomicYamlRetargetError";
103
+ }
104
+ }
105
+
106
+ /** Test-only coordination hooks for deterministic target-authority races. */
107
+ export const AtomicYamlTestHooks: {
108
+ afterTargetBound?: (canonicalPath: string) => Promise<void> | void;
109
+ } = {};
110
+
111
+ type AtomicYamlTargetIdentity = {
112
+ device: bigint;
113
+ inode: bigint;
114
+ };
115
+
116
+ type AtomicYamlTarget = {
117
+ canonicalPath: string;
118
+ resolvedPath: string;
119
+ identity: AtomicYamlTargetIdentity | undefined;
120
+ };
97
121
  type PathState = { exists: boolean; value: unknown };
98
122
  type ReceiptChange = {
99
123
  path: string;
@@ -103,6 +127,7 @@ type ReceiptChange = {
103
127
  };
104
128
 
105
129
  const queues = new Map<string, Promise<void>>();
130
+ const trustedQueuedTargets = new Map<string, AtomicYamlTarget>();
106
131
  let nextReceiptRevision = 0;
107
132
  /** Bounded Windows sharing-violation retries: 10, 25, 50, 100, then 200 ms. */
108
133
  const WINDOWS_RENAME_BACKOFF_MS = [10, 25, 50, 100, 200] as const;
@@ -112,6 +137,80 @@ function canonicalConfigPath(configPath: string): string {
112
137
  return path.normalize(path.resolve(configPath));
113
138
  }
114
139
 
140
+ async function resolveMissingAtomicYamlPath(inputPath: string): Promise<string> {
141
+ let unresolvedPath = inputPath;
142
+ const missingSegments: string[] = [];
143
+ for (;;) {
144
+ try {
145
+ return path.join(await fs.realpath(unresolvedPath), ...missingSegments.reverse());
146
+ } catch (error) {
147
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
148
+ const parent = path.dirname(unresolvedPath);
149
+ if (parent === unresolvedPath) throw error;
150
+ missingSegments.push(path.basename(unresolvedPath));
151
+ unresolvedPath = parent;
152
+ }
153
+ }
154
+ }
155
+
156
+ async function resolveAtomicYamlTargetPath(inputPath: string, depth = 0): Promise<string> {
157
+ const canonicalPath = canonicalConfigPath(inputPath);
158
+ try {
159
+ return await fs.realpath(canonicalPath);
160
+ } catch (error) {
161
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
162
+ }
163
+ try {
164
+ const metadata = await fs.lstat(canonicalPath);
165
+ if (metadata.isSymbolicLink()) {
166
+ if (depth >= 32) throw new AtomicYamlRetargetError();
167
+ const linkedPath = await fs.readlink(canonicalPath);
168
+ const targetPath = path.isAbsolute(linkedPath)
169
+ ? linkedPath
170
+ : path.resolve(path.dirname(canonicalPath), linkedPath);
171
+ return await resolveAtomicYamlTargetPath(targetPath, depth + 1);
172
+ }
173
+ } catch (error) {
174
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
175
+ }
176
+ return await resolveMissingAtomicYamlPath(canonicalPath);
177
+ }
178
+
179
+ async function readAtomicYamlTargetIdentity(resolvedPath: string): Promise<AtomicYamlTargetIdentity | undefined> {
180
+ try {
181
+ const metadata = await fs.stat(resolvedPath, { bigint: true });
182
+ return { device: metadata.dev, inode: metadata.ino };
183
+ } catch (error) {
184
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ async function resolveAtomicYamlTarget(configPath: string): Promise<AtomicYamlTarget> {
190
+ const canonicalPath = canonicalConfigPath(configPath);
191
+ const resolvedPath = await resolveAtomicYamlTargetPath(canonicalPath);
192
+ return {
193
+ canonicalPath,
194
+ resolvedPath,
195
+ identity: await readAtomicYamlTargetIdentity(resolvedPath),
196
+ };
197
+ }
198
+
199
+ function sameAtomicYamlIdentity(
200
+ left: AtomicYamlTargetIdentity | undefined,
201
+ right: AtomicYamlTargetIdentity | undefined,
202
+ ): boolean {
203
+ if (!left || !right) return left === right;
204
+ return left.device === right.device && left.inode === right.inode;
205
+ }
206
+
207
+ async function assertAtomicYamlTarget(target: AtomicYamlTarget): Promise<void> {
208
+ const current = await resolveAtomicYamlTarget(target.canonicalPath);
209
+ if (current.resolvedPath !== target.resolvedPath || !sameAtomicYamlIdentity(current.identity, target.identity)) {
210
+ throw new AtomicYamlRetargetError();
211
+ }
212
+ }
213
+
115
214
  function assertPatch(patch: AtomicYamlPatch): void {
116
215
  if (
117
216
  !patch ||
@@ -266,12 +365,12 @@ async function replaceWithRetry(tempPath: string, configPath: string, options: A
266
365
  }
267
366
 
268
367
  async function writeAtomicYaml(
269
- configPath: string,
368
+ target: AtomicYamlTarget,
270
369
  value: Record<string, unknown>,
271
370
  options: AtomicYamlPatchOptions,
272
371
  ): Promise<void> {
273
- const directory = path.dirname(configPath);
274
- const tempPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
372
+ const directory = path.dirname(target.resolvedPath);
373
+ const tempPath = path.join(directory, `.${path.basename(target.resolvedPath)}.${process.pid}.${randomUUID()}.tmp`);
275
374
  try {
276
375
  const tempHandle = await fs.open(tempPath, "wx", 0o600);
277
376
  try {
@@ -280,8 +379,12 @@ async function writeAtomicYaml(
280
379
  } finally {
281
380
  await tempHandle.close();
282
381
  }
283
- await replaceWithRetry(tempPath, configPath, options);
382
+ await assertAtomicYamlTarget(target);
383
+ await replaceWithRetry(tempPath, target.resolvedPath, options);
284
384
  await syncParentDirectory(directory);
385
+ const publishedTarget = await resolveAtomicYamlTarget(target.canonicalPath);
386
+ if (publishedTarget.resolvedPath !== target.resolvedPath) throw new AtomicYamlRetargetError();
387
+ target.identity = publishedTarget.identity;
285
388
  } finally {
286
389
  await fs.rm(tempPath, { force: true }).catch(() => undefined);
287
390
  }
@@ -302,10 +405,12 @@ function createReceipt(
302
405
  },
303
406
  async restore(): Promise<CasRestoreResult> {
304
407
  if (discarded) return { status: "discarded" };
305
- return await enqueueAtomicYamlOperation(configPath, async canonicalPath => {
408
+ return await enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
306
409
  if (discarded) return { status: "discarded" };
307
- return await withFileLock(canonicalPath, async () => {
308
- const { current, root } = await readYaml(canonicalPath);
410
+ await assertAtomicYamlTarget(target);
411
+ return await withFileLock(target.resolvedPath, async () => {
412
+ await assertAtomicYamlTarget(target);
413
+ const { current, root } = await readYaml(target.resolvedPath);
309
414
  const conflicts = changes
310
415
  .filter(
311
416
  change =>
@@ -320,7 +425,7 @@ function createReceipt(
320
425
  : { path: change.path, op: "unset" },
321
426
  );
322
427
  await options.validateRoot?.(root, restorePatches);
323
- const receipt = await applyPatchesUnderLock(canonicalPath, current, restorePatches, options);
428
+ const receipt = await applyPatchesUnderLock(target, current, restorePatches, options);
324
429
  await options.onRestored?.(restorePatches);
325
430
  return { status: "restored", receipt };
326
431
  });
@@ -330,12 +435,12 @@ function createReceipt(
330
435
  }
331
436
 
332
437
  async function applyPatchesUnderLock(
333
- configPath: string,
438
+ target: AtomicYamlTarget,
334
439
  current: Record<string, unknown>,
335
440
  patches: readonly AtomicYamlPatch[],
336
441
  options: AtomicYamlPatchOptions,
337
442
  ): Promise<CasReceipt> {
338
- if (patches.length === 0) return createReceipt(configPath, [], options);
443
+ if (patches.length === 0) return createReceipt(target.canonicalPath, [], options);
339
444
 
340
445
  for (const patch of patches) {
341
446
  if (!patch.expected) continue;
@@ -373,8 +478,8 @@ async function applyPatchesUnderLock(
373
478
  }
374
479
  const changes = [...changesByPath.values()];
375
480
 
376
- await writeAtomicYaml(configPath, current, options);
377
- return createReceipt(configPath, changes, options);
481
+ await writeAtomicYaml(target, current, options);
482
+ return createReceipt(target.canonicalPath, changes, options);
378
483
  }
379
484
 
380
485
  /** Build patches from current durable YAML while holding the shared queue and file lock. */
@@ -385,14 +490,17 @@ export function applyAtomicYamlPatchesWithCurrent(
385
490
  ) => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
386
491
  options: AtomicYamlPatchOptions = {},
387
492
  ): Promise<CasReceipt> {
388
- return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
389
- await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
390
- return await withFileLock(canonicalPath, async () => {
391
- const { current, root } = await readYaml(canonicalPath);
493
+ return enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
494
+ await assertAtomicYamlTarget(target);
495
+ await fs.mkdir(path.dirname(target.resolvedPath), { recursive: true, mode: 0o700 });
496
+ await assertAtomicYamlTarget(target);
497
+ return await withFileLock(target.resolvedPath, async () => {
498
+ await assertAtomicYamlTarget(target);
499
+ const { current, root } = await readYaml(target.resolvedPath);
392
500
  const patches = await buildPatches(current);
393
501
  for (const patch of patches) assertPatch(patch);
394
502
  await options.validateRoot?.(root, patches);
395
- return await applyPatchesUnderLock(canonicalPath, current, patches, options);
503
+ return await applyPatchesUnderLock(target, current, patches, options);
396
504
  });
397
505
  });
398
506
  }
@@ -404,18 +512,41 @@ export function applyAtomicYamlPatchesWithCurrent(
404
512
  */
405
513
  export function enqueueAtomicYamlOperation<T>(
406
514
  configPath: string,
407
- operation: (canonicalConfigPath: string) => Promise<T>,
515
+ operation: (canonicalConfigPath: string, target: AtomicYamlTarget) => Promise<T>,
408
516
  ): Promise<T> {
409
517
  const canonicalPath = canonicalConfigPath(configPath);
410
518
  const prior = queues.get(canonicalPath) ?? Promise.resolve();
411
- const result = prior.catch(() => undefined).then(() => operation(canonicalPath));
519
+ const targetAtEnqueue = resolveAtomicYamlTarget(canonicalPath).then(async target => {
520
+ await AtomicYamlTestHooks.afterTargetBound?.(canonicalPath);
521
+ return target;
522
+ });
523
+ const result = prior
524
+ .catch(() => undefined)
525
+ .then(async () => {
526
+ const target = await targetAtEnqueue;
527
+ const current = await resolveAtomicYamlTarget(canonicalPath);
528
+ if (
529
+ current.resolvedPath !== target.resolvedPath ||
530
+ (!sameAtomicYamlIdentity(current.identity, target.identity) &&
531
+ !sameAtomicYamlIdentity(trustedQueuedTargets.get(canonicalPath)?.identity, current.identity))
532
+ ) {
533
+ throw new AtomicYamlRetargetError();
534
+ }
535
+ target.identity = current.identity;
536
+ const value = await operation(canonicalPath, target);
537
+ trustedQueuedTargets.set(canonicalPath, { ...target });
538
+ return value;
539
+ });
412
540
  const completion = result.then(
413
541
  () => undefined,
414
542
  () => undefined,
415
543
  );
416
544
  queues.set(canonicalPath, completion);
417
545
  void completion.finally(() => {
418
- if (queues.get(canonicalPath) === completion) queues.delete(canonicalPath);
546
+ if (queues.get(canonicalPath) === completion) {
547
+ queues.delete(canonicalPath);
548
+ trustedQueuedTargets.delete(canonicalPath);
549
+ }
419
550
  });
420
551
  return result;
421
552
  }
@@ -429,14 +560,17 @@ export function reserveAtomicYamlPatchSlot(
429
560
  patches: () => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
430
561
  options: AtomicYamlPatchOptions = {},
431
562
  ): Promise<CasReceipt> {
432
- return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
563
+ return enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
433
564
  const nextPatches = await patches();
434
565
  for (const patch of nextPatches) assertPatch(patch);
435
- await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
436
- return await withFileLock(canonicalPath, async () => {
437
- const { current, root } = await readYaml(canonicalPath);
566
+ await assertAtomicYamlTarget(target);
567
+ await fs.mkdir(path.dirname(target.resolvedPath), { recursive: true, mode: 0o700 });
568
+ await assertAtomicYamlTarget(target);
569
+ return await withFileLock(target.resolvedPath, async () => {
570
+ await assertAtomicYamlTarget(target);
571
+ const { current, root } = await readYaml(target.resolvedPath);
438
572
  await options.validateRoot?.(root, nextPatches);
439
- return await applyPatchesUnderLock(canonicalPath, current, nextPatches, options);
573
+ return await applyPatchesUnderLock(target, current, nextPatches, options);
440
574
  });
441
575
  });
442
576
  }
@@ -450,14 +584,17 @@ export function reserveAtomicYamlUpdateSlot<T>(
450
584
  update: () => Promise<AtomicYamlUpdate<T>> | AtomicYamlUpdate<T>,
451
585
  options: AtomicYamlPatchOptions = {},
452
586
  ): Promise<T> {
453
- return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
587
+ return enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
454
588
  const atomicUpdate = await update();
455
- await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
456
- return await withFileLock(canonicalPath, async () => {
457
- const { current } = await readYaml(canonicalPath);
589
+ await assertAtomicYamlTarget(target);
590
+ await fs.mkdir(path.dirname(target.resolvedPath), { recursive: true, mode: 0o700 });
591
+ await assertAtomicYamlTarget(target);
592
+ return await withFileLock(target.resolvedPath, async () => {
593
+ await assertAtomicYamlTarget(target);
594
+ const { current } = await readYaml(target.resolvedPath);
458
595
  const result = await atomicUpdate.apply(current);
459
596
  if (atomicUpdate.shouldWrite?.(result) !== false) {
460
- await writeAtomicYaml(canonicalPath, current, options);
597
+ await writeAtomicYaml(target, current, options);
461
598
  }
462
599
  await atomicUpdate.committed?.(current, result);
463
600
  return result;
@@ -29,6 +29,7 @@ interface AppKeybindings {
29
29
  "app.model.select": true;
30
30
  "app.model.selectTemporary": true;
31
31
  "app.tools.expand": true;
32
+ "app.todo.toggle": true;
32
33
  "app.tool.backgroundFold": true;
33
34
  "app.editor.external": true;
34
35
  "app.message.followUp": true;
@@ -37,6 +38,7 @@ interface AppKeybindings {
37
38
  "app.clipboard.pasteImage": true;
38
39
  "app.clipboard.copyLine": true;
39
40
  "app.clipboard.copyPrompt": true;
41
+ "app.oauth.copyUrl": true;
40
42
  "app.session.new": true;
41
43
  "app.session.tree": true;
42
44
  "app.session.fork": true;
@@ -131,6 +133,10 @@ export const KEYBINDINGS = {
131
133
  defaultKeys: "ctrl+o",
132
134
  description: "Expand tools",
133
135
  },
136
+ "app.todo.toggle": {
137
+ defaultKeys: "alt+shift+t",
138
+ description: "Toggle todo list expansion",
139
+ },
134
140
  "app.tool.backgroundFold": {
135
141
  defaultKeys: "ctrl+b",
136
142
  description: "Fold/background supported foreground tool",
@@ -163,6 +169,10 @@ export const KEYBINDINGS = {
163
169
  defaultKeys: "alt+shift+c",
164
170
  description: "Copy prompt",
165
171
  },
172
+ "app.oauth.copyUrl": {
173
+ defaultKeys: "alt+shift+u",
174
+ description: "Copy OAuth authorization URL",
175
+ },
166
176
  "app.session.new": {
167
177
  defaultKeys: "ctrl+n",
168
178
  description: "Start a new session",
@@ -92,6 +92,41 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
92
92
  critic: "openai-codex/gpt-5.6-sol:max",
93
93
  architect: "openai-codex/gpt-5.6-sol:xhigh",
94
94
  }),
95
+ profile("macos-omlx-fast", ["omlx"], {
96
+ default: "omlx/Qwen3.6-35B-A3B-4bit:low",
97
+ executor: "omlx/Qwen3.6-35B-A3B-4bit:low",
98
+ architect: "omlx/Qwen3.6-35B-A3B-4bit:high",
99
+ planner: "omlx/Qwen3.6-35B-A3B-4bit:medium",
100
+ critic: "omlx/Qwen3.6-35B-A3B-4bit:high",
101
+ }),
102
+ profile("macos-omlx-balanced", ["omlx"], {
103
+ default: "omlx/Qwen3.6-35B-A3B-8bit:low",
104
+ executor: "omlx/Qwen3.6-35B-A3B-8bit:low",
105
+ architect: "omlx/Qwen3.6-35B-A3B-8bit:high",
106
+ planner: "omlx/Qwen3.6-35B-A3B-8bit:medium",
107
+ critic: "omlx/Qwen3.6-35B-A3B-8bit:high",
108
+ }),
109
+ profile("macos-omlx-quality", ["omlx"], {
110
+ default: "omlx/Qwen3.6-35B-A3B-8bit:low",
111
+ executor: "omlx/Qwen3.6-35B-A3B-8bit:low",
112
+ architect: "omlx/Qwen3.6-35B-A3B-8bit:high",
113
+ planner: "omlx/Qwen3.6-35B-A3B-8bit:medium",
114
+ critic: "omlx/Qwen3.8-27B-8bit:high",
115
+ }),
116
+ profile("macos-omlx-abliterated-fast", ["omlx"], {
117
+ default: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
118
+ executor: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
119
+ architect: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
120
+ planner: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:medium",
121
+ critic: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
122
+ }),
123
+ profile("macos-omlx-abliterated-balanced", ["omlx"], {
124
+ default: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
125
+ executor: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
126
+ architect: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
127
+ planner: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:medium",
128
+ critic: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
129
+ }),
95
130
  profile("opencodego", ["opencode-go"], {
96
131
  default: "opencode-go/kimi-k2.6",
97
132
  executor: "opencode-go/deepseek-v4-flash",
@@ -327,6 +362,23 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
327
362
  "codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
328
363
  "codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
329
364
  "codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
365
+ "macos-omlx-fast": { displayName: "4-bit Fast (MoE measured 93.5 tok/s)", providerGroup: "macOS Local (oMLX)" },
366
+ "macos-omlx-balanced": {
367
+ displayName: "8-bit Balanced (MoE measured 71.1 tok/s)",
368
+ providerGroup: "macOS Local (oMLX)",
369
+ },
370
+ "macos-omlx-quality": {
371
+ displayName: "Quality mix (8-bit MoE + 8-bit dense critic)",
372
+ providerGroup: "macOS Local (oMLX)",
373
+ },
374
+ "macos-omlx-abliterated-fast": {
375
+ displayName: "Uncensored 4-bit Fast (measured 19.8 tok/s)",
376
+ providerGroup: "macOS Local (oMLX)",
377
+ },
378
+ "macos-omlx-abliterated-balanced": {
379
+ displayName: "Uncensored 4-bit Balanced (same winner as fast)",
380
+ providerGroup: "macOS Local (oMLX)",
381
+ },
330
382
  opencodego: { displayName: "OpenCodeGo", providerGroup: "OPENCODEGO" },
331
383
  "claude-opus": { displayName: "Claude Opus", providerGroup: "CLAUDE" },
332
384
  "claude-fable": { displayName: "Claude Fable", providerGroup: "CLAUDE" },
@@ -361,6 +413,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
361
413
 
362
414
  const PROFILE_GROUP_ORDER = [
363
415
  "CODEX",
416
+ "macOS Local (oMLX)",
364
417
  "OPENCODEGO",
365
418
  "CLAUDE",
366
419
  "GLM",
@@ -372,6 +425,14 @@ const PROFILE_GROUP_ORDER = [
372
425
  "ALIBABA TOKEN PLAN",
373
426
  "COMBOS",
374
427
  ];
428
+ const MACOS_OMLX_PROFILE_ORDER = [
429
+ "macos-omlx-fast",
430
+ "macos-omlx-balanced",
431
+ "macos-omlx-quality",
432
+ "macos-omlx-abliterated-fast",
433
+ "macos-omlx-abliterated-balanced",
434
+ ] as const;
435
+ const MACOS_OMLX_PROFILE_RANK = new Map<string, number>(MACOS_OMLX_PROFILE_ORDER.map((name, index) => [name, index]));
375
436
 
376
437
  const PROFILE_RECOMMENDATIONS: Record<string, string> = {
377
438
  "openai-codex": "codex-medium",
@@ -386,6 +447,7 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
386
447
  xai: "grok-medium",
387
448
  "grok-build": "grok-build-pro",
388
449
  cursor: "cursor-medium",
450
+ omlx: "macos-omlx-balanced",
389
451
  "minimax-code": "minimax-medium",
390
452
  "alibaba-token-plan": "alibaba-token-plan-balanced",
391
453
  };
@@ -420,7 +482,13 @@ export function groupModelProfilesForPresetLanding(
420
482
  }
421
483
  for (const [group, entries] of groups) {
422
484
  if (entries.length === 0) groups.delete(group);
423
- else entries.sort((a, b) => a.name.localeCompare(b.name));
485
+ else if (group === "macOS Local (oMLX)") {
486
+ entries.sort(
487
+ (a, b) =>
488
+ (MACOS_OMLX_PROFILE_RANK.get(a.name) ?? Number.MAX_SAFE_INTEGER) -
489
+ (MACOS_OMLX_PROFILE_RANK.get(b.name) ?? Number.MAX_SAFE_INTEGER),
490
+ );
491
+ } else entries.sort((a, b) => a.name.localeCompare(b.name));
424
492
  }
425
493
  return groups;
426
494
  }
@@ -7,7 +7,9 @@ import {
7
7
  type CacheRetention,
8
8
  type Context,
9
9
  createModelManager,
10
+ Effort,
10
11
  enrichModelThinking,
12
+ fetchOpenAICompatibleModels,
11
13
  getBundledModels,
12
14
  getBundledProviders,
13
15
  googleAntigravityModelManagerOptions,
@@ -18,6 +20,7 @@ import {
18
20
  type ModelRequestTransform,
19
21
  openaiCodexModelManagerOptions,
20
22
  PROVIDER_DESCRIPTORS,
23
+ type Provider,
21
24
  readModelCache,
22
25
  registerCustomApi,
23
26
  type SimpleStreamOptions,
@@ -597,6 +600,10 @@ function toPositiveNumberOrUndefined(value: unknown): number | undefined {
597
600
  return undefined;
598
601
  }
599
602
 
603
+ function toSafePositiveIntegerOrUndefined(value: unknown): number | undefined {
604
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
605
+ }
606
+
600
607
  function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
601
608
  const modelInfo = payload.model_info;
602
609
  if (isRecord(modelInfo)) {
@@ -1032,6 +1039,13 @@ export class ModelRegistry {
1032
1039
  #lastStaticLoadMtime: number | null = null;
1033
1040
  #registeredProviderSources: Set<string> = new Set();
1034
1041
  #providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
1042
+ /**
1043
+ * Monotonic per-provider discovery sequence. Overlapping refreshes for the
1044
+ * same provider can complete out of order; only the newest-started refresh
1045
+ * may publish discovery state or contribute models, so a slow stale fetch
1046
+ * never overwrites a fresher result.
1047
+ */
1048
+ #providerDiscoverySequences: Map<string, number> = new Map();
1035
1049
  #cacheDbPath?: string;
1036
1050
  #suppressedSelectors: Map<string, number> = new Map();
1037
1051
  #backgroundRefresh?: Promise<void>;
@@ -1815,11 +1829,15 @@ export class ModelRegistry {
1815
1829
  providerConfig: DiscoveryProviderConfig,
1816
1830
  strategy: ModelRefreshStrategy,
1817
1831
  ): Promise<Model<Api>[]> {
1832
+ const sequence = (this.#providerDiscoverySequences.get(providerConfig.provider) ?? 0) + 1;
1833
+ this.#providerDiscoverySequences.set(providerConfig.provider, sequence);
1834
+ const isCurrentDiscovery = () => this.#providerDiscoverySequences.get(providerConfig.provider) === sequence;
1818
1835
  const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1819
1836
  const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
1820
1837
  if (requiresAuth) {
1821
1838
  const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
1822
1839
  if (!isAuthenticated(apiKey)) {
1840
+ if (!isCurrentDiscovery()) return [];
1823
1841
  this.#providerDiscoveryStates.set(providerConfig.provider, {
1824
1842
  provider: providerConfig.provider,
1825
1843
  status: "unauthenticated",
@@ -1865,6 +1883,7 @@ export class ModelRegistry {
1865
1883
  : result.models.length > 0
1866
1884
  ? "ok"
1867
1885
  : "empty";
1886
+ if (!isCurrentDiscovery()) return [];
1868
1887
  this.#providerDiscoveryStates.set(providerId, {
1869
1888
  provider: providerId,
1870
1889
  status,
@@ -1893,6 +1912,8 @@ export class ModelRegistry {
1893
1912
  case "llama.cpp":
1894
1913
  return this.#discoverLlamaCppModels(providerConfig);
1895
1914
  case "lm-studio":
1915
+ case "omlx":
1916
+ case "sglang":
1896
1917
  case "openai-models-list":
1897
1918
  return this.#discoverOpenAIModelsList(providerConfig);
1898
1919
  }
@@ -2198,48 +2219,61 @@ export class ModelRegistry {
2198
2219
 
2199
2220
  async #discoverOpenAIModelsList(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
2200
2221
  const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
2201
- const modelsUrl = `${baseUrl}/models`;
2202
-
2203
2222
  const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
2204
2223
  const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
2205
- if (apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth) {
2206
- headers.Authorization = `Bearer ${apiKey}`;
2207
- }
2208
-
2209
- const response = await fetch(modelsUrl, {
2224
+ const bearerToken = apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
2225
+ const isOmlx = providerConfig.provider === "omlx";
2226
+ const parseDiscoveryLimit = isOmlx ? toSafePositiveIntegerOrUndefined : toPositiveNumberOrUndefined;
2227
+ const discovered = await fetchOpenAICompatibleModels({
2228
+ api: providerConfig.api,
2229
+ provider: providerConfig.provider as Provider,
2230
+ baseUrl,
2231
+ apiKey: bearerToken,
2210
2232
  headers,
2211
- signal: AbortSignal.timeout(250),
2233
+ signal: AbortSignal.timeout(providerConfig.provider === "sglang" ? 500 : 250),
2234
+ fetch: (input, init) => fetch(input, { ...init, redirect: "error" }),
2235
+ throwOnStatus: response => new Error(`HTTP ${response.status} from ${baseUrl}/models`),
2236
+ mapModel: (item, defaults) => ({
2237
+ ...defaults,
2238
+ reasoning: isOmlx,
2239
+ ...(isOmlx
2240
+ ? {
2241
+ thinking: {
2242
+ mode: "effort" as const,
2243
+ minLevel: Effort.Low,
2244
+ maxLevel: Effort.High,
2245
+ defaultLevel: Effort.Medium,
2246
+ levels: [Effort.Low, Effort.Medium, Effort.High],
2247
+ },
2248
+ }
2249
+ : {}),
2250
+ contextWindow:
2251
+ parseDiscoveryLimit(item.max_model_len) ??
2252
+ parseDiscoveryLimit(item.context_length) ??
2253
+ parseDiscoveryLimit(item.context_window) ??
2254
+ parseDiscoveryLimit(item.max_context_length) ??
2255
+ UNK_CONTEXT_WINDOW,
2256
+ maxTokens:
2257
+ parseDiscoveryLimit(item.max_completion_tokens) ??
2258
+ parseDiscoveryLimit(item.max_tokens) ??
2259
+ parseDiscoveryLimit(item.max_output_tokens) ??
2260
+ UNK_MAX_TOKENS,
2261
+ headers,
2262
+ compat: {
2263
+ ...(providerConfig.compat ?? {}),
2264
+ supportsStore: false,
2265
+ supportsDeveloperRole: false,
2266
+ supportsReasoningEffort: isOmlx,
2267
+ ...(isOmlx
2268
+ ? {
2269
+ thinkingFormat: "qwen-chat-template" as const,
2270
+ reasoningContentField: "reasoning_content" as const,
2271
+ }
2272
+ : {}),
2273
+ },
2274
+ }),
2212
2275
  });
2213
- if (!response.ok) {
2214
- throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
2215
- }
2216
- const payload = (await response.json()) as { data?: Array<{ id: string }> };
2217
- const models = payload.data ?? [];
2218
- const discovered: Model<Api>[] = [];
2219
- for (const item of models) {
2220
- const id = item.id;
2221
- if (!id) continue;
2222
- discovered.push(
2223
- enrichModelThinking({
2224
- id,
2225
- name: id,
2226
- api: providerConfig.api,
2227
- provider: providerConfig.provider,
2228
- baseUrl,
2229
- reasoning: false,
2230
- input: ["text"],
2231
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2232
- contextWindow: 128000,
2233
- maxTokens: 8192,
2234
- headers,
2235
- compat: {
2236
- supportsStore: false,
2237
- supportsDeveloperRole: false,
2238
- supportsReasoningEffort: false,
2239
- },
2240
- }),
2241
- );
2242
- }
2276
+ if (discovered === null) throw new Error(`Invalid OpenAI-compatible model catalog from ${baseUrl}`);
2243
2277
  return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
2244
2278
  }
2245
2279
 
@@ -184,7 +184,7 @@ export const ModelOverrideSchema = z
184
184
  export type ModelOverride = z.infer<typeof ModelOverrideSchema>;
185
185
 
186
186
  export const ProviderDiscoverySchema = z.object({
187
- type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list"]),
187
+ type: z.enum(["ollama", "llama.cpp", "lm-studio", "omlx", "sglang", "openai-models-list"]),
188
188
  });
189
189
 
190
190
  const LocalOpenAICompatSchema = z
@@ -1417,6 +1417,15 @@ export const SETTINGS_SCHEMA = {
1417
1417
  ],
1418
1418
  },
1419
1419
  },
1420
+ "startup.skipLogoAnimation": {
1421
+ type: "boolean",
1422
+ default: false,
1423
+ ui: {
1424
+ tab: "interaction",
1425
+ label: "Skip Logo Animation",
1426
+ description: "Show the final welcome logo immediately",
1427
+ },
1428
+ },
1420
1429
 
1421
1430
  "startup.checkUpdate": {
1422
1431
  type: "boolean",