@oh-my-pi/pi-coding-agent 16.1.6 → 16.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/cli.js +3160 -3139
  3. package/dist/types/cli/session-picker.d.ts +0 -1
  4. package/dist/types/cli/tiny-models-cli.d.ts +2 -0
  5. package/dist/types/collab/protocol.d.ts +20 -1
  6. package/dist/types/config/models-config-schema.d.ts +10 -0
  7. package/dist/types/config/models-config.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +57 -6
  9. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +5 -1
  10. package/dist/types/internal-urls/filesystem-resource.d.ts +9 -0
  11. package/dist/types/mcp/loader.d.ts +3 -2
  12. package/dist/types/mcp/manager.d.ts +4 -3
  13. package/dist/types/mcp/startup-events.d.ts +21 -4
  14. package/dist/types/mnemopi/config.d.ts +1 -0
  15. package/dist/types/modes/components/agent-hub.d.ts +1 -0
  16. package/dist/types/modes/components/agent-transcript-viewer.d.ts +1 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  18. package/dist/types/modes/components/btw-panel.d.ts +2 -0
  19. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -0
  20. package/dist/types/modes/components/session-selector.d.ts +0 -2
  21. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  23. package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
  24. package/dist/types/modes/interactive-mode.d.ts +3 -0
  25. package/dist/types/modes/types.d.ts +3 -0
  26. package/dist/types/session/agent-session.d.ts +5 -3
  27. package/dist/types/session/session-history-format.d.ts +25 -0
  28. package/dist/types/session/snapcompact-inline.d.ts +1 -1
  29. package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
  30. package/dist/types/slash-commands/types.d.ts +2 -0
  31. package/dist/types/system-prompt.d.ts +1 -0
  32. package/dist/types/tiny/models.d.ts +11 -7
  33. package/dist/types/tools/todo.d.ts +1 -0
  34. package/dist/types/utils/thinking-display.d.ts +2 -0
  35. package/package.json +12 -12
  36. package/src/advisor/__tests__/advisor.test.ts +104 -0
  37. package/src/advisor/runtime.ts +38 -2
  38. package/src/cli/session-picker.ts +1 -2
  39. package/src/cli/tiny-models-cli.ts +7 -2
  40. package/src/collab/guest.ts +172 -20
  41. package/src/collab/host.ts +47 -5
  42. package/src/collab/protocol.ts +16 -1
  43. package/src/config/models-config-schema.ts +1 -0
  44. package/src/config/settings-schema.ts +59 -5
  45. package/src/edit/renderer.ts +8 -12
  46. package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
  47. package/src/internal-urls/docs-index.generated.txt +1 -1
  48. package/src/internal-urls/filesystem-resource.ts +34 -0
  49. package/src/internal-urls/local-protocol.ts +7 -1
  50. package/src/internal-urls/memory-protocol.ts +5 -1
  51. package/src/internal-urls/skill-protocol.ts +20 -4
  52. package/src/internal-urls/vault-protocol.ts +5 -2
  53. package/src/main.ts +8 -8
  54. package/src/mcp/loader.ts +4 -3
  55. package/src/mcp/manager.ts +35 -15
  56. package/src/mcp/startup-events.ts +106 -11
  57. package/src/mnemopi/config.ts +2 -0
  58. package/src/mnemopi/state.ts +3 -1
  59. package/src/modes/components/agent-hub.ts +4 -0
  60. package/src/modes/components/agent-transcript-viewer.ts +2 -0
  61. package/src/modes/components/assistant-message.ts +217 -18
  62. package/src/modes/components/btw-panel.ts +15 -3
  63. package/src/modes/components/chat-transcript-builder.ts +8 -2
  64. package/src/modes/components/model-selector.ts +72 -9
  65. package/src/modes/components/omfg-panel.ts +1 -1
  66. package/src/modes/components/session-selector.ts +4 -9
  67. package/src/modes/components/snapcompact-shape-preview.ts +1 -1
  68. package/src/modes/components/tool-execution.ts +10 -2
  69. package/src/modes/controllers/btw-controller.ts +32 -7
  70. package/src/modes/controllers/event-controller.ts +24 -4
  71. package/src/modes/controllers/input-controller.ts +43 -21
  72. package/src/modes/controllers/selector-controller.ts +23 -13
  73. package/src/modes/controllers/streaming-reveal.ts +78 -20
  74. package/src/modes/controllers/todo-command-controller.ts +9 -10
  75. package/src/modes/interactive-mode.ts +83 -8
  76. package/src/modes/types.ts +3 -0
  77. package/src/modes/utils/ui-helpers.ts +1 -0
  78. package/src/prompts/advisor/system.md +1 -1
  79. package/src/prompts/system/side-channel-no-tools.md +3 -0
  80. package/src/prompts/system/system-prompt.md +164 -156
  81. package/src/sdk.ts +12 -8
  82. package/src/session/agent-session.ts +349 -84
  83. package/src/session/history-storage.ts +15 -16
  84. package/src/session/session-history-format.ts +41 -1
  85. package/src/session/snapcompact-inline.ts +9 -6
  86. package/src/slash-commands/builtin-registry.ts +147 -21
  87. package/src/slash-commands/helpers/todo.ts +12 -6
  88. package/src/slash-commands/types.ts +2 -0
  89. package/src/system-prompt.ts +6 -11
  90. package/src/tiny/models.ts +7 -3
  91. package/src/tiny/title-client.ts +8 -2
  92. package/src/tiny/worker.ts +1 -0
  93. package/src/tools/search.ts +10 -1
  94. package/src/tools/sqlite-reader.ts +59 -3
  95. package/src/tools/todo.ts +6 -0
  96. package/src/tools/write.ts +4 -6
  97. package/src/utils/thinking-display.ts +78 -0
@@ -38,24 +38,23 @@ class AsyncDrain<T> {
38
38
  let queue = this.#queue;
39
39
  if (!queue) {
40
40
  this.#queue = queue = [];
41
- this.#promise = new Promise((resolve, reject) => {
42
- const exec = () => {
43
- try {
44
- if (this.#queue === queue) {
45
- this.#queue = undefined;
46
- }
47
- resolve(hnd(queue!));
48
- } catch (error) {
49
- reject(error);
41
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
42
+ const exec = (): void => {
43
+ try {
44
+ if (this.#queue === queue) {
45
+ this.#queue = undefined;
50
46
  }
51
- };
52
-
53
- if (this.delayMs > 0) {
54
- setTimeout(exec, this.delayMs);
55
- } else {
56
- queueMicrotask(exec);
47
+ resolve(hnd(queue!));
48
+ } catch (error) {
49
+ reject(error);
57
50
  }
58
- });
51
+ };
52
+ if (this.delayMs > 0) {
53
+ setTimeout(exec, this.delayMs);
54
+ } else {
55
+ queueMicrotask(exec);
56
+ }
57
+ this.#promise = promise;
59
58
  }
60
59
  queue.push(value);
61
60
  return this.#promise;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
10
10
  import type { AssistantMessage, ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
11
+ import { escapeXmlText } from "@oh-my-pi/pi-utils";
11
12
  import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
12
13
  import type {
13
14
  BashExecutionMessage,
@@ -28,6 +29,17 @@ export interface HistoryFormatOptions {
28
29
  includeToolIntent?: boolean;
29
30
  /** Render watched-session roles as inline `**agent**:` / `**user**:` labels (collapsing consecutive same-role messages) instead of `## ` headings, so a primary transcript embedded inside an advisor turn stays visually distinct. */
30
31
  watchedRoles?: boolean;
32
+ /**
33
+ * Expand the primary agent's injected constraint context — plan mode's rules
34
+ * (`plan-mode-context`) and the approved plan it implements
35
+ * (`plan-mode-reference`) — verbatim instead of as a truncated one-liner,
36
+ * wrapped in a `<primary-context>` tag so a reviewer reads it as the primary's
37
+ * instructions, not its own. The advisor sets this: a truncated rule (plan
38
+ * mode's "NEVER create files … except the plan file") makes it raise false
39
+ * blockers. See {@link PRIMARY_CONTEXT_CUSTOM_TYPES}. Other custom messages
40
+ * still collapse to a one-liner.
41
+ */
42
+ expandPrimaryContext?: boolean;
31
43
  }
32
44
 
33
45
  /** Max length of the primary-arg summary inside `→ tool(...)` lines. */
@@ -156,6 +168,21 @@ function executionLine(
156
168
  return `→ ${kind}! ${oneLine(source)} ⇒ ${status} · ${lines} ${lines === 1 ? "line" : "lines"}`;
157
169
  }
158
170
 
171
+ /**
172
+ * Hidden custom messages that inject the primary agent's operative *constraints*
173
+ * — plan mode's rules and the approved plan it implements. A reviewer (the
174
+ * advisor) must read these verbatim; truncating them hides load-bearing
175
+ * exceptions (e.g. plan mode permits exactly one plan file). Every other custom
176
+ * type stays a one-liner.
177
+ *
178
+ * Deliberately excludes `goal-mode-context`: its body carries live budget
179
+ * counters (tokens/seconds used) that change every turn, so it can neither be
180
+ * deduped against a prior copy nor expanded each turn without flooding the
181
+ * reviewer — and its constraints don't drive the file-write misreads this
182
+ * targets.
183
+ */
184
+ export const PRIMARY_CONTEXT_CUSTOM_TYPES: ReadonlySet<string> = new Set(["plan-mode-context", "plan-mode-reference"]);
185
+
159
186
  /** One-liner for custom/hook messages: `[irc] A → B: body…`. */
160
187
  function customOneLiner(msg: CustomMessage | HookMessage): string {
161
188
  const details = (msg.details ?? {}) as Record<string, unknown>;
@@ -279,7 +306,20 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
279
306
  }
280
307
  case "custom":
281
308
  case "hookMessage": {
282
- lines.push(customOneLiner(msg as CustomMessage | HookMessage), "");
309
+ const custom = msg as CustomMessage | HookMessage;
310
+ if (opts?.expandPrimaryContext && PRIMARY_CONTEXT_CUSTOM_TYPES.has(custom.customType)) {
311
+ const text = contentToText(custom.content).trim();
312
+ if (text) {
313
+ lines.push(
314
+ `<primary-context kind="${custom.customType}">`,
315
+ escapeXmlText(text),
316
+ "</primary-context>",
317
+ "",
318
+ );
319
+ }
320
+ } else {
321
+ lines.push(customOneLiner(custom), "");
322
+ }
283
323
  lastWatchedLabel = undefined;
284
324
  break;
285
325
  }
@@ -414,7 +414,7 @@ export class SnapcompactInlineTransformer {
414
414
  private readonly onToolResultSavings?: SnapcompactSavingsSink,
415
415
  ) {}
416
416
 
417
- transform(context: Context, model: Model): Context {
417
+ async transform(context: Context, model: Model): Promise<Context> {
418
418
  // Vision gate: providers silently DROP images on text-only models —
419
419
  // rendering would lose the content entirely.
420
420
  if (!model.input.includes("image")) return context;
@@ -482,7 +482,7 @@ export class SnapcompactInlineTransformer {
482
482
  for (const swap of plan.toolResults) {
483
483
  const target = targets.get(swap.id);
484
484
  if (!target) continue;
485
- const frames = this.#framesFor(this.#toolCache, swap.id, target.text, shape);
485
+ const frames = await this.#framesFor(this.#toolCache, swap.id, target.text, shape);
486
486
  messages[target.index] = { ...target.message, content: [{ type: "text", text: toolResultNote }, ...frames] };
487
487
  changed = true;
488
488
  savings.push({
@@ -506,7 +506,10 @@ export class SnapcompactInlineTransformer {
506
506
  if (!cached || cached.hash !== hash) {
507
507
  cached = {
508
508
  hash,
509
- frames: snapcompact.renderMany(systemPromptTarget.text, { shape, maxFrames: MAX_SYSTEM_PROMPT_FRAMES }),
509
+ frames: await snapcompact.renderMany(systemPromptTarget.text, {
510
+ shape,
511
+ maxFrames: MAX_SYSTEM_PROMPT_FRAMES,
512
+ }),
510
513
  };
511
514
  this.#systemCache = cached;
512
515
  }
@@ -526,16 +529,16 @@ export class SnapcompactInlineTransformer {
526
529
  return { ...context, systemPrompt, messages };
527
530
  }
528
531
 
529
- #framesFor(
532
+ async #framesFor(
530
533
  cache: Map<string, FrameCacheEntry>,
531
534
  key: string,
532
535
  text: string,
533
536
  shape: snapcompact.Shape,
534
- ): ImageContent[] {
537
+ ): Promise<ImageContent[]> {
535
538
  const hash = Bun.hash(text);
536
539
  const cached = cache.get(key);
537
540
  if (cached && cached.hash === hash) return cached.frames;
538
- const frames = snapcompact.renderMany(text, { shape });
541
+ const frames = await snapcompact.renderMany(text, { shape });
539
542
  cache.set(key, { hash, frames });
540
543
  return frames;
541
544
  }
@@ -24,6 +24,7 @@ import {
24
24
  MarketplaceManager,
25
25
  } from "../extensibility/plugins/marketplace";
26
26
  import { resolveMemoryBackend } from "../memory-backend";
27
+ import { describeLoopLimitRuntime } from "../modes/loop-limit";
27
28
  import { theme } from "../modes/theme/theme";
28
29
  import type { InteractiveModeContext } from "../modes/types";
29
30
  import type { AgentSession, FreshSessionResult } from "../session/agent-session";
@@ -58,6 +59,12 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
58
59
  /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
59
60
  export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
60
61
 
62
+ export interface TuiBuiltinSlashCommand extends BuiltinSlashCommand {
63
+ getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
64
+ getInlineHint?: (argumentText: string) => string | null;
65
+ getAutocompleteDescription?: () => string | undefined;
66
+ }
67
+
61
68
  function refreshStatusLine(ctx: InteractiveModeContext): void {
62
69
  ctx.statusLine.invalidate();
63
70
  ctx.updateEditorTopBorder();
@@ -77,6 +84,17 @@ function formatFastModeStatus(session: AgentSession): string {
77
84
  }
78
85
  }
79
86
 
87
+ const AUTOCOMPLETE_DETAIL_LIMIT = 48;
88
+
89
+ function shortDetail(value: string, limit = AUTOCOMPLETE_DETAIL_LIMIT): string {
90
+ const singleLine = value.replace(/\s+/g, " ").trim();
91
+ return singleLine.length <= limit ? singleLine : `${singleLine.slice(0, limit - 1)}…`;
92
+ }
93
+
94
+ function formatTokenCount(value: number): string {
95
+ return value.toLocaleString();
96
+ }
97
+
80
98
  /** Scheme-less display form of a browser deep link: accent + underline, OSC-8 linked to the full URL. */
81
99
  function collabWebLinkClickable(webLink: string): string {
82
100
  const display = theme.fg("accent", `\x1b[4m${webLink.replace(/^https?:\/\//, "")}\x1b[24m`);
@@ -253,6 +271,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
253
271
  description: "Toggle plan mode (agent plans before executing)",
254
272
  inlineHint: "[prompt]",
255
273
  allowArgs: true,
274
+ getTuiAutocompleteDescription: runtime => {
275
+ if (!runtime.ctx.settings.get("plan.enabled" as SettingPath)) return "Plan: disabled in settings";
276
+ if (runtime.ctx.planModeEnabled) {
277
+ const planFile = runtime.ctx.planModePlanFilePath;
278
+ return `Plan: on${planFile ? ` (${path.basename(planFile)})` : ""}`;
279
+ }
280
+ if (runtime.ctx.goalModeEnabled) return "Plan: blocked by goal mode";
281
+ return "Plan: off";
282
+ },
256
283
  handleTui: async (command, runtime) => {
257
284
  const hadArgs = !!command.args;
258
285
  await runtime.ctx.handlePlanModeCommand(command.args || undefined);
@@ -265,6 +292,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
265
292
  {
266
293
  name: "plan-review",
267
294
  description: "Re-open the plan review for the latest plan (plan mode only)",
295
+ getTuiAutocompleteDescription: runtime =>
296
+ runtime.ctx.planModeEnabled ? "Plan review: available" : "Plan review: plan mode inactive",
268
297
  handleTui: async (_command, runtime) => {
269
298
  await runtime.ctx.openPlanReview();
270
299
  runtime.ctx.editor.setText("");
@@ -283,6 +312,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
283
312
  ],
284
313
  inlineHint: "[objective]",
285
314
  allowArgs: true,
315
+ getTuiAutocompleteDescription: runtime => {
316
+ if (!runtime.ctx.settings.get("goal.enabled" as SettingPath)) return "Goal: disabled in settings";
317
+ if (runtime.ctx.planModeEnabled) return "Goal: blocked by plan mode";
318
+ const state = runtime.ctx.session.getGoalModeState();
319
+ return state ? `Goal: ${state.goal.status} (${shortDetail(state.goal.objective)})` : "Goal: off";
320
+ },
286
321
  handleTui: async (command, runtime) => {
287
322
  const hadArgs = !!command.args;
288
323
  await runtime.ctx.handleGoalModeCommand(command.args || undefined);
@@ -308,6 +343,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
308
343
  "Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
309
344
  inlineHint: "[count|duration] [prompt]",
310
345
  allowArgs: true,
346
+ getTuiAutocompleteDescription: runtime => {
347
+ if (!runtime.ctx.loopModeEnabled) return "Loop: off";
348
+ if (runtime.ctx.loopLimit) return `Loop: on (${describeLoopLimitRuntime(runtime.ctx.loopLimit)})`;
349
+ if (runtime.ctx.loopPrompt) return "Loop: on (repeating prompt)";
350
+ return "Loop: on (waiting for next prompt)";
351
+ },
311
352
  handleTui: async (command, runtime) => {
312
353
  const prompt = await runtime.ctx.handleLoopCommand(command.args);
313
354
  runtime.ctx.editor.setText("");
@@ -321,6 +362,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
321
362
  aliases: ["models"],
322
363
  description: "Switch model for this session",
323
364
  acpDescription: "Show current model selection",
365
+ getTuiAutocompleteDescription: runtime => {
366
+ const model = runtime.ctx.session.model;
367
+ return model ? `Model: ${model.provider}/${model.id}` : "Model: none selected";
368
+ },
324
369
  handle: async (command, runtime) => {
325
370
  if (command.args) {
326
371
  const modelId = command.args.trim();
@@ -359,6 +404,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
359
404
  {
360
405
  name: "switch",
361
406
  description: "Switch model for this session (same as alt+p)",
407
+ getTuiAutocompleteDescription: runtime => {
408
+ const model = runtime.ctx.session.model;
409
+ return model ? `Model: ${model.provider}/${model.id}` : "Model: none selected";
410
+ },
362
411
  handleTui: (_command, runtime) => {
363
412
  runtime.ctx.showModelSelector({ temporaryOnly: true });
364
413
  runtime.ctx.editor.setText("");
@@ -375,6 +424,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
375
424
  { name: "status", description: "Show fast mode status" },
376
425
  ],
377
426
  allowArgs: true,
427
+ getTuiAutocompleteDescription: runtime => `Fast: ${formatFastModeStatus(runtime.ctx.session)}`,
378
428
  handle: async (command, runtime) => {
379
429
  const arg = command.args.toLowerCase();
380
430
  if (!arg || arg === "toggle") {
@@ -442,6 +492,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
442
492
  { name: "dump", description: "Copy the advisor's transcript to clipboard", usage: "[raw]" },
443
493
  ],
444
494
  allowArgs: true,
495
+ getTuiAutocompleteDescription: runtime => {
496
+ const stats = runtime.ctx.session.getAdvisorStats();
497
+ if (stats.active && stats.model) return `Advisor: on (${stats.model.provider}/${stats.model.id})`;
498
+ if (stats.configured) return "Advisor: configured, no model";
499
+ return "Advisor: off";
500
+ },
445
501
  handle: async (command, runtime) => {
446
502
  const { verb, rest } = parseSubcommand(command.args);
447
503
  if (!verb || verb === "toggle") {
@@ -603,6 +659,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
603
659
  { name: "stop", description: "Stop sharing" },
604
660
  ],
605
661
  allowArgs: true,
662
+ getTuiAutocompleteDescription: runtime => {
663
+ if (runtime.ctx.collabHost) {
664
+ return `Collab: hosting (${Math.max(0, runtime.ctx.collabHost.participants.length - 1)} guests)`;
665
+ }
666
+ if (runtime.ctx.collabGuest?.readOnly) return "Collab: read-only guest";
667
+ if (runtime.ctx.collabGuest) return "Collab: guest";
668
+ return "Collab: off";
669
+ },
606
670
  handleTui: async (command, runtime) => {
607
671
  const ctx = runtime.ctx;
608
672
  ctx.editor.setText("");
@@ -702,6 +766,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
702
766
  {
703
767
  name: "leave",
704
768
  description: "Leave the collab session",
769
+ getTuiAutocompleteDescription: runtime => {
770
+ if (runtime.ctx.collabHost) return "Leave collab: hosting";
771
+ if (runtime.ctx.collabGuest) return "Leave collab: guest";
772
+ return "Leave collab: not in collab";
773
+ },
705
774
  handleTui: async (_command, runtime) => {
706
775
  const ctx = runtime.ctx;
707
776
  ctx.editor.setText("");
@@ -726,6 +795,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
726
795
  { name: "visible", description: "Switch to visible mode" },
727
796
  ],
728
797
  allowArgs: true,
798
+ getTuiAutocompleteDescription: runtime => {
799
+ if (!runtime.ctx.settings.get("browser.enabled" as SettingPath)) return "Browser: disabled";
800
+ return runtime.ctx.settings.get("browser.headless" as SettingPath) ? "Browser: headless" : "Browser: visible";
801
+ },
729
802
  handle: async (command, runtime) => {
730
803
  const arg = command.args.toLowerCase();
731
804
  const enabled = runtime.settings.get("browser.enabled" as SettingPath) as boolean;
@@ -817,6 +890,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
817
890
  { name: "rm", description: "Remove task/phase/all (fuzzy-matched)", usage: "[<task|phase>]" },
818
891
  ],
819
892
  allowArgs: true,
893
+ getTuiAutocompleteDescription: runtime => {
894
+ const tasks = runtime.ctx.todoPhases.flatMap(phase => phase.tasks);
895
+ if (tasks.length === 0) return "Todos: none";
896
+ const pending = tasks.filter(task => task.status === "pending").length;
897
+ const inProgress = tasks.filter(task => task.status === "in_progress").length;
898
+ const completed = tasks.filter(task => task.status === "completed").length;
899
+ return `Todos: ${pending + inProgress} open (${inProgress} in progress, ${completed} done)`;
900
+ },
820
901
  handle: handleTodoAcp,
821
902
  handleTui: async (command, runtime) => {
822
903
  await runtime.ctx.handleTodoCommand(command.args);
@@ -881,6 +962,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
881
962
  name: "jobs",
882
963
  description: "Show async background jobs status",
883
964
  acpDescription: "Show background jobs",
965
+ getTuiAutocompleteDescription: runtime => {
966
+ const snapshot = runtime.ctx.session.getAsyncJobSnapshot({ recentLimit: 5 });
967
+ if (!snapshot || (snapshot.running.length === 0 && snapshot.recent.length === 0)) return "Jobs: none";
968
+ return `Jobs: ${snapshot.running.length} running, ${snapshot.recent.length} recent`;
969
+ },
884
970
  handle: async (_command, runtime) => {
885
971
  const snapshot = runtime.session.getAsyncJobSnapshot({ recentLimit: 5 });
886
972
  if (!snapshot || (snapshot.running.length === 0 && snapshot.recent.length === 0)) {
@@ -1016,6 +1102,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1016
1102
  name: "tools",
1017
1103
  description: "Show tools currently visible to the agent",
1018
1104
  acpDescription: "Show available tools",
1105
+ getTuiAutocompleteDescription: runtime => {
1106
+ const active = runtime.ctx.session.getActiveToolNames().length;
1107
+ const all = runtime.ctx.session.getAllToolNames().length;
1108
+ return all === 0 ? "Tools: none available" : `Tools: ${active} active / ${all} available`;
1109
+ },
1019
1110
  handle: async (_command, runtime) => {
1020
1111
  const active = runtime.session.getActiveToolNames();
1021
1112
  const all = runtime.session.getAllToolNames();
@@ -1035,6 +1126,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1035
1126
  name: "context",
1036
1127
  description: "Show estimated context usage breakdown",
1037
1128
  acpDescription: "Show context usage",
1129
+ getTuiAutocompleteDescription: runtime => {
1130
+ const usage = runtime.ctx.session.getContextUsage();
1131
+ if (!usage) return "Context: unavailable";
1132
+ return `Context: ${Math.round(usage.percent)}% (${formatTokenCount(usage.tokens)}/${formatTokenCount(usage.contextWindow)})`;
1133
+ },
1038
1134
  handle: async (_command, runtime) => {
1039
1135
  await runtime.output(buildContextReportText(runtime));
1040
1136
  return commandConsumed();
@@ -1094,6 +1190,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1094
1190
  description: "Login with OAuth provider",
1095
1191
  inlineHint: "[provider|redirect URL]",
1096
1192
  allowArgs: true,
1193
+ getTuiAutocompleteDescription: runtime =>
1194
+ runtime.ctx.oauthManualInput.hasPending()
1195
+ ? `Login: waiting for ${runtime.ctx.oauthManualInput.pendingProviderId ?? "OAuth"} callback`
1196
+ : "Login: choose provider",
1097
1197
  handleTui: (command, runtime) => {
1098
1198
  const manualInput = runtime.ctx.oauthManualInput;
1099
1199
  const args = command.args.trim();
@@ -1233,6 +1333,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1233
1333
  {
1234
1334
  name: "fresh",
1235
1335
  description: "Reset provider stream state without changing the local transcript",
1336
+ getTuiAutocompleteDescription: runtime =>
1337
+ runtime.ctx.session.isStreaming ? "Fresh: unavailable while streaming" : "Fresh: ready",
1236
1338
  handle: async (_command, runtime) => {
1237
1339
  const result = runtime.session.freshSession();
1238
1340
  if (!result) {
@@ -1268,6 +1370,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1268
1370
  })),
1269
1371
  acpInputHint: `[${COMPACT_MODES.map(mode => mode.name).join("|")}] [focus]`,
1270
1372
  allowArgs: true,
1373
+ getTuiAutocompleteDescription: runtime => {
1374
+ const usage = runtime.ctx.session.getContextUsage();
1375
+ return usage ? `Compact: context ${Math.round(usage.percent)}% used` : "Compact: context unavailable";
1376
+ },
1271
1377
  handle: async (command, runtime) => {
1272
1378
  const parsed = parseCompactArgs(command.args);
1273
1379
  if ("error" in parsed) return usage(parsed.error, runtime);
@@ -1354,6 +1460,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1354
1460
  allowArgs: true,
1355
1461
  handleTui: async (command, runtime) => {
1356
1462
  const question = command.text.slice(`/${command.name}`.length).trim();
1463
+ if (question) {
1464
+ runtime.ctx.editor.addToHistory(command.text);
1465
+ }
1357
1466
  runtime.ctx.editor.setText("");
1358
1467
  await runtime.ctx.handleBtwCommand(question);
1359
1468
  },
@@ -1365,6 +1474,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1365
1474
  allowArgs: true,
1366
1475
  handleTui: async (command, runtime) => {
1367
1476
  const work = command.text.slice(`/${command.name}`.length).trim();
1477
+ if (work) {
1478
+ runtime.ctx.editor.addToHistory(command.text);
1479
+ }
1368
1480
  runtime.ctx.editor.setText("");
1369
1481
  await runtime.ctx.handleTanCommand(work);
1370
1482
  },
@@ -1376,6 +1488,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1376
1488
  allowArgs: true,
1377
1489
  handleTui: async (command, runtime) => {
1378
1490
  const complaint = command.text.slice(`/${command.name}`.length).trim();
1491
+ if (complaint) {
1492
+ runtime.ctx.editor.addToHistory(command.text);
1493
+ }
1379
1494
  runtime.ctx.editor.setText("");
1380
1495
  await runtime.ctx.handleOmfgCommand(complaint);
1381
1496
  },
@@ -1484,6 +1599,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1484
1599
  }
1485
1600
  },
1486
1601
  handleTui: async (command, runtime) => {
1602
+ runtime.ctx.editor.addToHistory(command.text);
1487
1603
  runtime.ctx.editor.setText("");
1488
1604
  await runtime.ctx.handleMemoryCommand(command.text);
1489
1605
  },
@@ -1511,6 +1627,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1511
1627
  runtime.ctx.editor.setText("");
1512
1628
  return;
1513
1629
  }
1630
+ runtime.ctx.editor.addToHistory(command.text);
1514
1631
  runtime.ctx.editor.setText("");
1515
1632
  await runtime.ctx.handleRenameCommand(title);
1516
1633
  },
@@ -1553,6 +1670,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1553
1670
  runtime.ctx.editor.setText("");
1554
1671
  return;
1555
1672
  }
1673
+ runtime.ctx.editor.addToHistory(command.text);
1556
1674
  runtime.ctx.editor.setText("");
1557
1675
  await runtime.ctx.handleMoveCommand(targetPath);
1558
1676
  },
@@ -2097,6 +2215,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
2097
2215
  aliases: ["force:"],
2098
2216
  inlineHint: "<tool-name> [prompt]",
2099
2217
  allowArgs: true,
2218
+ getTuiAutocompleteDescription: runtime => {
2219
+ const count = runtime.ctx.session.getActiveToolNames().length;
2220
+ return count === 0 ? "Force: no active tools" : `Force: ${count} active tools`;
2221
+ },
2100
2222
  handle: async (command, runtime) => {
2101
2223
  const spaceIdx = command.args.indexOf(" ");
2102
2224
  const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
@@ -2225,34 +2347,38 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
2225
2347
  description: command.description,
2226
2348
  subcommands: command.subcommands,
2227
2349
  inlineHint: command.inlineHint,
2350
+ getTuiAutocompleteDescription: command.getTuiAutocompleteDescription,
2228
2351
  }),
2229
2352
  );
2230
2353
 
2354
+ function materializeTuiBuiltinSlashCommand(
2355
+ cmd: BuiltinSlashCommand,
2356
+ runtime?: TuiSlashCommandRuntime,
2357
+ ): TuiBuiltinSlashCommand {
2358
+ const materialized: TuiBuiltinSlashCommand = { ...cmd };
2359
+ if (cmd.subcommands) {
2360
+ materialized.getArgumentCompletions = buildArgumentCompletions(cmd.subcommands);
2361
+ materialized.getInlineHint = buildSubcommandInlineHint(cmd.subcommands);
2362
+ } else if (cmd.inlineHint) {
2363
+ materialized.getInlineHint = buildStaticInlineHint(cmd.inlineHint);
2364
+ }
2365
+ if (runtime && cmd.getTuiAutocompleteDescription) {
2366
+ materialized.getAutocompleteDescription = () => cmd.getTuiAutocompleteDescription?.(runtime);
2367
+ }
2368
+ return materialized;
2369
+ }
2370
+
2231
2371
  /**
2232
2372
  * Materialized builtin slash commands with completion functions derived from
2233
2373
  * declarative subcommand/hint definitions.
2234
2374
  */
2235
- export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<
2236
- BuiltinSlashCommand & {
2237
- getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
2238
- getInlineHint?: (argumentText: string) => string | null;
2239
- }
2240
- > = BUILTIN_SLASH_COMMAND_DEFS.map(cmd => {
2241
- if (cmd.subcommands) {
2242
- return {
2243
- ...cmd,
2244
- getArgumentCompletions: buildArgumentCompletions(cmd.subcommands),
2245
- getInlineHint: buildSubcommandInlineHint(cmd.subcommands),
2246
- };
2247
- }
2248
- if (cmd.inlineHint) {
2249
- return {
2250
- ...cmd,
2251
- getInlineHint: buildStaticInlineHint(cmd.inlineHint),
2252
- };
2253
- }
2254
- return cmd;
2255
- });
2375
+ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<TuiBuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_DEFS.map(cmd =>
2376
+ materializeTuiBuiltinSlashCommand(cmd),
2377
+ );
2378
+
2379
+ export function buildTuiBuiltinSlashCommands(runtime: TuiSlashCommandRuntime): ReadonlyArray<TuiBuiltinSlashCommand> {
2380
+ return BUILTIN_SLASH_COMMAND_DEFS.map(cmd => materializeTuiBuiltinSlashCommand(cmd, runtime));
2381
+ }
2256
2382
 
2257
2383
  /**
2258
2384
  * Unified registry exposed for cross-mode tooling. Each spec carries at least
@@ -1,14 +1,14 @@
1
- import * as path from "node:path";
2
1
  import type { TodoPhase } from "../../tools/todo";
3
2
  import {
4
3
  applyOpsToPhases,
5
4
  getLatestTodoPhasesFromEntries,
6
5
  markdownToPhases,
7
6
  phasesToMarkdown,
7
+ resolveTodoMarkdownPath,
8
8
  USER_TODO_EDIT_CUSTOM_TYPE,
9
9
  } from "../../tools/todo";
10
10
  import type { ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime } from "../types";
11
- import { commandConsumed, parseSubcommand, usage } from "./parse";
11
+ import { commandConsumed, errorMessage, parseSubcommand, usage } from "./parse";
12
12
 
13
13
  type TodoMutationVerb = "done" | "drop" | "rm";
14
14
 
@@ -127,19 +127,25 @@ async function handleTodoExportCommand(restArgs: string, runtime: SlashCommandRu
127
127
  await runtime.output("No todos to export.");
128
128
  return commandConsumed();
129
129
  }
130
- const target = restArgs ? path.resolve(runtime.cwd, restArgs) : path.resolve(runtime.cwd, "TODO.md");
131
- await Bun.write(target, phasesToMarkdown(phases));
130
+ let target: string;
131
+ try {
132
+ target = resolveTodoMarkdownPath(restArgs, runtime.sessionManager.getCwd());
133
+ await Bun.write(target, phasesToMarkdown(phases));
134
+ } catch (err) {
135
+ return usage(`Failed to write todos: ${errorMessage(err)}`, runtime);
136
+ }
132
137
  await runtime.output(`Wrote todos to ${target}`);
133
138
  return commandConsumed();
134
139
  }
135
140
 
136
141
  async function handleTodoImportCommand(restArgs: string, runtime: SlashCommandRuntime): Promise<SlashCommandResult> {
137
- const target = restArgs ? path.resolve(runtime.cwd, restArgs) : path.resolve(runtime.cwd, "TODO.md");
142
+ let target: string;
138
143
  let content: string;
139
144
  try {
145
+ target = resolveTodoMarkdownPath(restArgs, runtime.sessionManager.getCwd());
140
146
  content = await Bun.file(target).text();
141
147
  } catch (err) {
142
- return usage(`Failed to read ${target}: ${err instanceof Error ? err.message : String(err)}`, runtime);
148
+ return usage(`Failed to read todos: ${errorMessage(err)}`, runtime);
143
149
  }
144
150
  const { phases, errors } = markdownToPhases(content);
145
151
  if (errors.length > 0) return usage(`Could not parse ${target}:\n ${errors.join("\n ")}`, runtime);
@@ -20,6 +20,8 @@ export interface BuiltinSlashCommand {
20
20
  subcommands?: SubcommandDef[];
21
21
  /** Static inline hint when command takes a simple argument (no subcommands). */
22
22
  inlineHint?: string;
23
+ /** TUI-only dynamic status text for command-name autocomplete. Static `description` remains canonical for ACP/help. */
24
+ getTuiAutocompleteDescription?: (runtime: TuiSlashCommandRuntime) => string | undefined;
23
25
  }
24
26
 
25
27
  /** Parsed slash-command text after stripping the leading "/". */
@@ -327,6 +327,8 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
327
327
  return userLevel?.content ?? null;
328
328
  }
329
329
 
330
+ export const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES = ["read", "bash", "eval", "edit", "write"] as const;
331
+
330
332
  export interface SystemPromptToolMetadata {
331
333
  label: string;
332
334
  description: string;
@@ -584,18 +586,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
584
586
  const dateTime = date;
585
587
  const promptCwd = shortenPath(resolvedCwd.replace(/\\/g, "/"));
586
588
 
587
- // Build tool metadata for system prompt rendering
588
- // Priority: explicit list > tools map > defaults
589
- // Default includes both bash and python; actual availability determined by settings in createTools
589
+ // Build tool metadata for system prompt rendering.
590
+ // Priority: explicit list > tools map > conservative SDK fallback.
590
591
  let toolNames = providedToolNames;
591
592
  if (!toolNames) {
592
- if (tools) {
593
- // Tools map provided
594
- toolNames = Array.from(tools.keys());
595
- } else {
596
- // Use defaults
597
- toolNames = ["read", "bash", "eval", "edit", "write"]; // TODO: Why?
598
- }
593
+ toolNames = tools ? Array.from(tools.keys()) : [...DEFAULT_SYSTEM_PROMPT_TOOL_NAMES];
599
594
  }
600
595
 
601
596
  // Build tool descriptions for system prompt rendering.
@@ -625,7 +620,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
625
620
  // Filter skills for the rendered system prompt:
626
621
  // - require the `read` tool so the model can actually fetch skill content;
627
622
  // - drop skills with frontmatter `hide: true` (still loadable via skill:// and /skill:<name>).
628
- const hasRead = tools?.has("read");
623
+ const hasRead = toolNames.includes("read");
629
624
  const filteredSkills = hasRead ? skills.filter(skill => skill.hide !== true) : [];
630
625
 
631
626
  const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
@@ -12,6 +12,8 @@ export interface TinyTitleLocalModelSpec {
12
12
  contextNote: string;
13
13
  /** Model family emits hidden reasoning unless the chat template disables it. */
14
14
  reasoning?: boolean;
15
+ /** Reason this model is blocked before loading the ONNX runtime. */
16
+ unsupportedReason?: string;
15
17
  }
16
18
 
17
19
  export const TINY_TITLE_LOCAL_MODELS = [
@@ -108,7 +110,7 @@ export function getTinyTitleModelSpec(key: TinyTitleLocalModelKey): (typeof TINY
108
110
  /** Default memory model: the online path (the configured smol / remote LLM; no local download). */
109
111
  export const ONLINE_MEMORY_MODEL_KEY = "online";
110
112
  /** Recommended local model for memory tasks when none is named. */
111
- export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "qwen3-1.7b";
113
+ export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "lfm2-1.2b";
112
114
 
113
115
  /**
114
116
  * Local models for Mnemopi memory tasks (fact extraction + consolidation).
@@ -123,9 +125,11 @@ export const TINY_MEMORY_LOCAL_MODELS = [
123
125
  dtype: "q4",
124
126
  label: "Qwen3 1.7B",
125
127
  description:
126
- "Recommended; most disciplined extraction (ignores chit-chat), good consolidation, about 1.1 GB cached.",
127
- contextNote: "Best single-model pick for memory from the local experiment.",
128
+ "Disabled for local inference: onnxruntime-node cannot run this ONNX export's RotaryEmbedding cache updates.",
129
+ contextNote: "Blocked before load to avoid the unsupported RotaryEmbedding runtime path.",
128
130
  reasoning: true,
131
+ unsupportedReason:
132
+ "onnxruntime-node does not support Qwen3 RotaryEmbedding cache updates in onnx-community/Qwen3-1.7B-ONNX",
129
133
  },
130
134
  {
131
135
  key: "gemma-3-1b",