@oh-my-pi/pi-coding-agent 16.1.7 → 16.1.9

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 (120) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli.js +4266 -3446
  3. package/dist/types/cli/flag-tables.d.ts +17 -0
  4. package/dist/types/cli/session-picker.d.ts +0 -1
  5. package/dist/types/cli/tiny-models-cli.d.ts +2 -0
  6. package/dist/types/cli-commands.d.ts +4 -2
  7. package/dist/types/collab/protocol.d.ts +20 -1
  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/command-controller.d.ts +1 -1
  23. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  24. package/dist/types/modes/controllers/streaming-reveal.d.ts +3 -2
  25. package/dist/types/modes/interactive-mode.d.ts +4 -1
  26. package/dist/types/modes/types.d.ts +4 -1
  27. package/dist/types/session/agent-session.d.ts +18 -3
  28. package/dist/types/session/session-history-format.d.ts +25 -0
  29. package/dist/types/session/snapcompact-inline.d.ts +1 -1
  30. package/dist/types/slash-commands/builtin-registry.d.ts +7 -4
  31. package/dist/types/slash-commands/types.d.ts +2 -0
  32. package/dist/types/system-prompt.d.ts +1 -0
  33. package/dist/types/tiny/models.d.ts +11 -7
  34. package/dist/types/tools/browser/launch.d.ts +5 -0
  35. package/dist/types/tools/todo.d.ts +1 -0
  36. package/dist/types/utils/thinking-display.d.ts +2 -0
  37. package/package.json +12 -12
  38. package/src/advisor/__tests__/advisor.test.ts +104 -0
  39. package/src/advisor/runtime.ts +38 -2
  40. package/src/cli/args.ts +0 -1
  41. package/src/cli/flag-tables.ts +42 -0
  42. package/src/cli/profile-bootstrap.ts +1 -11
  43. package/src/cli/session-picker.ts +1 -2
  44. package/src/cli/tiny-models-cli.ts +7 -2
  45. package/src/cli-commands.ts +48 -3
  46. package/src/collab/guest.ts +172 -20
  47. package/src/collab/host.ts +47 -5
  48. package/src/collab/protocol.ts +16 -1
  49. package/src/config/model-registry.ts +6 -7
  50. package/src/config/settings-schema.ts +59 -5
  51. package/src/edit/renderer.ts +8 -12
  52. package/src/extensibility/legacy-pi-ai-shim.ts +16 -4
  53. package/src/internal-urls/docs-index.generated.txt +1 -1
  54. package/src/internal-urls/filesystem-resource.ts +34 -0
  55. package/src/internal-urls/local-protocol.ts +7 -1
  56. package/src/internal-urls/memory-protocol.ts +5 -1
  57. package/src/internal-urls/skill-protocol.ts +20 -4
  58. package/src/internal-urls/vault-protocol.ts +5 -2
  59. package/src/lsp/client.ts +39 -25
  60. package/src/main.ts +8 -8
  61. package/src/mcp/loader.ts +4 -3
  62. package/src/mcp/manager.ts +35 -15
  63. package/src/mcp/startup-events.ts +106 -11
  64. package/src/mnemopi/config.ts +2 -0
  65. package/src/mnemopi/state.ts +3 -1
  66. package/src/modes/components/agent-hub.ts +4 -0
  67. package/src/modes/components/agent-transcript-viewer.ts +2 -0
  68. package/src/modes/components/assistant-message.ts +217 -18
  69. package/src/modes/components/btw-panel.ts +15 -3
  70. package/src/modes/components/chat-transcript-builder.ts +8 -2
  71. package/src/modes/components/model-selector.ts +72 -9
  72. package/src/modes/components/omfg-panel.ts +1 -1
  73. package/src/modes/components/session-selector.ts +4 -9
  74. package/src/modes/components/snapcompact-shape-preview.ts +1 -1
  75. package/src/modes/components/tool-execution.ts +10 -2
  76. package/src/modes/controllers/btw-controller.ts +32 -7
  77. package/src/modes/controllers/command-controller.ts +18 -3
  78. package/src/modes/controllers/event-controller.ts +24 -4
  79. package/src/modes/controllers/input-controller.ts +43 -21
  80. package/src/modes/controllers/mcp-command-controller.ts +40 -12
  81. package/src/modes/controllers/selector-controller.ts +23 -13
  82. package/src/modes/controllers/streaming-reveal.ts +78 -20
  83. package/src/modes/controllers/todo-command-controller.ts +9 -10
  84. package/src/modes/interactive-mode.ts +84 -9
  85. package/src/modes/types.ts +4 -1
  86. package/src/modes/utils/ui-helpers.ts +1 -0
  87. package/src/prompts/advisor/system.md +1 -1
  88. package/src/prompts/system/side-channel-no-tools.md +3 -0
  89. package/src/sdk.ts +12 -8
  90. package/src/session/agent-session.ts +385 -85
  91. package/src/session/history-storage.ts +15 -16
  92. package/src/session/session-history-format.ts +41 -1
  93. package/src/session/snapcompact-inline.ts +9 -6
  94. package/src/slash-commands/builtin-registry.ts +171 -87
  95. package/src/slash-commands/helpers/todo.ts +12 -6
  96. package/src/slash-commands/types.ts +2 -0
  97. package/src/system-prompt.ts +6 -11
  98. package/src/tiny/models.ts +7 -3
  99. package/src/tiny/title-client.ts +8 -2
  100. package/src/tiny/worker.ts +1 -0
  101. package/src/tools/browser/launch.ts +107 -31
  102. package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
  103. package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
  104. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
  105. package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
  106. package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
  107. package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
  108. package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
  109. package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
  110. package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
  111. package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
  112. package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
  113. package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
  114. package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
  115. package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
  116. package/src/tools/search.ts +10 -1
  117. package/src/tools/sqlite-reader.ts +59 -3
  118. package/src/tools/todo.ts +6 -0
  119. package/src/tools/write.ts +4 -6
  120. 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
  }
@@ -1,8 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
3
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
5
- import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
6
4
  import { type AutocompleteItem, Spacer } from "@oh-my-pi/pi-tui";
7
5
  import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils";
8
6
  import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
@@ -24,6 +22,7 @@ import {
24
22
  MarketplaceManager,
25
23
  } from "../extensibility/plugins/marketplace";
26
24
  import { resolveMemoryBackend } from "../memory-backend";
25
+ import { describeLoopLimitRuntime } from "../modes/loop-limit";
27
26
  import { theme } from "../modes/theme/theme";
28
27
  import type { InteractiveModeContext } from "../modes/types";
29
28
  import type { AgentSession, FreshSessionResult } from "../session/agent-session";
@@ -58,6 +57,12 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
58
57
  /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
59
58
  export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
60
59
 
60
+ export interface TuiBuiltinSlashCommand extends BuiltinSlashCommand {
61
+ getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
62
+ getInlineHint?: (argumentText: string) => string | null;
63
+ getAutocompleteDescription?: () => string | undefined;
64
+ }
65
+
61
66
  function refreshStatusLine(ctx: InteractiveModeContext): void {
62
67
  ctx.statusLine.invalidate();
63
68
  ctx.updateEditorTopBorder();
@@ -77,6 +82,17 @@ function formatFastModeStatus(session: AgentSession): string {
77
82
  }
78
83
  }
79
84
 
85
+ const AUTOCOMPLETE_DETAIL_LIMIT = 48;
86
+
87
+ function shortDetail(value: string, limit = AUTOCOMPLETE_DETAIL_LIMIT): string {
88
+ const singleLine = value.replace(/\s+/g, " ").trim();
89
+ return singleLine.length <= limit ? singleLine : `${singleLine.slice(0, limit - 1)}…`;
90
+ }
91
+
92
+ function formatTokenCount(value: number): string {
93
+ return value.toLocaleString();
94
+ }
95
+
80
96
  /** Scheme-less display form of a browser deep link: accent + underline, OSC-8 linked to the full URL. */
81
97
  function collabWebLinkClickable(webLink: string): string {
82
98
  const display = theme.fg("accent", `\x1b[4m${webLink.replace(/^https?:\/\//, "")}\x1b[24m`);
@@ -174,46 +190,6 @@ async function handleUsageResetCommand(
174
190
  await output(describeRedeemOutcome(outcome, target.label));
175
191
  }
176
192
 
177
- const DEBUG_DUMP_NEXT_REQUEST_USAGE = "Usage: /debug dump-next-request <path>";
178
-
179
- function resolveDebugRequestDumpPath(target: string, cwd: string): string {
180
- const expanded =
181
- target === "~"
182
- ? os.homedir()
183
- : target.startsWith("~/") || target.startsWith("~\\")
184
- ? path.join(os.homedir(), target.slice(2))
185
- : target;
186
- return path.resolve(cwd, expanded);
187
- }
188
-
189
- async function handleDebugSubcommand(
190
- args: string,
191
- cwd: string,
192
- output: (text: string) => Promise<void> | void,
193
- ): Promise<SlashCommandResult> {
194
- const { verb, rest } = parseSubcommand(args);
195
- switch (verb) {
196
- case "":
197
- await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
198
- return commandConsumed();
199
- case "dump-next-request":
200
- case "dump-request":
201
- case "next-request": {
202
- if (!rest) {
203
- await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
204
- return commandConsumed();
205
- }
206
- const requestPath = resolveDebugRequestDumpPath(rest, cwd);
207
- setNextRequestDebugPath(requestPath);
208
- await output(`Next AI provider request will be dumped to ${requestPath}`);
209
- return commandConsumed();
210
- }
211
- default:
212
- await output(`Unknown /debug subcommand "${verb}". ${DEBUG_DUMP_NEXT_REQUEST_USAGE}`);
213
- return commandConsumed();
214
- }
215
- }
216
-
217
193
  /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
218
194
  function parseShakeMode(args: string): ShakeMode | { error: string } {
219
195
  const verb = args.trim().toLowerCase();
@@ -253,6 +229,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
253
229
  description: "Toggle plan mode (agent plans before executing)",
254
230
  inlineHint: "[prompt]",
255
231
  allowArgs: true,
232
+ getTuiAutocompleteDescription: runtime => {
233
+ if (!runtime.ctx.settings.get("plan.enabled" as SettingPath)) return "Plan: disabled in settings";
234
+ if (runtime.ctx.planModeEnabled) {
235
+ const planFile = runtime.ctx.planModePlanFilePath;
236
+ return `Plan: on${planFile ? ` (${path.basename(planFile)})` : ""}`;
237
+ }
238
+ if (runtime.ctx.goalModeEnabled) return "Plan: blocked by goal mode";
239
+ return "Plan: off";
240
+ },
256
241
  handleTui: async (command, runtime) => {
257
242
  const hadArgs = !!command.args;
258
243
  await runtime.ctx.handlePlanModeCommand(command.args || undefined);
@@ -265,6 +250,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
265
250
  {
266
251
  name: "plan-review",
267
252
  description: "Re-open the plan review for the latest plan (plan mode only)",
253
+ getTuiAutocompleteDescription: runtime =>
254
+ runtime.ctx.planModeEnabled ? "Plan review: available" : "Plan review: plan mode inactive",
268
255
  handleTui: async (_command, runtime) => {
269
256
  await runtime.ctx.openPlanReview();
270
257
  runtime.ctx.editor.setText("");
@@ -283,6 +270,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
283
270
  ],
284
271
  inlineHint: "[objective]",
285
272
  allowArgs: true,
273
+ getTuiAutocompleteDescription: runtime => {
274
+ if (!runtime.ctx.settings.get("goal.enabled" as SettingPath)) return "Goal: disabled in settings";
275
+ if (runtime.ctx.planModeEnabled) return "Goal: blocked by plan mode";
276
+ const state = runtime.ctx.session.getGoalModeState();
277
+ return state ? `Goal: ${state.goal.status} (${shortDetail(state.goal.objective)})` : "Goal: off";
278
+ },
286
279
  handleTui: async (command, runtime) => {
287
280
  const hadArgs = !!command.args;
288
281
  await runtime.ctx.handleGoalModeCommand(command.args || undefined);
@@ -308,6 +301,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
308
301
  "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
302
  inlineHint: "[count|duration] [prompt]",
310
303
  allowArgs: true,
304
+ getTuiAutocompleteDescription: runtime => {
305
+ if (!runtime.ctx.loopModeEnabled) return "Loop: off";
306
+ if (runtime.ctx.loopLimit) return `Loop: on (${describeLoopLimitRuntime(runtime.ctx.loopLimit)})`;
307
+ if (runtime.ctx.loopPrompt) return "Loop: on (repeating prompt)";
308
+ return "Loop: on (waiting for next prompt)";
309
+ },
311
310
  handleTui: async (command, runtime) => {
312
311
  const prompt = await runtime.ctx.handleLoopCommand(command.args);
313
312
  runtime.ctx.editor.setText("");
@@ -321,6 +320,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
321
320
  aliases: ["models"],
322
321
  description: "Switch model for this session",
323
322
  acpDescription: "Show current model selection",
323
+ getTuiAutocompleteDescription: runtime => {
324
+ const model = runtime.ctx.session.model;
325
+ return model ? `Model: ${model.provider}/${model.id}` : "Model: none selected";
326
+ },
324
327
  handle: async (command, runtime) => {
325
328
  if (command.args) {
326
329
  const modelId = command.args.trim();
@@ -359,6 +362,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
359
362
  {
360
363
  name: "switch",
361
364
  description: "Switch model for this session (same as alt+p)",
365
+ getTuiAutocompleteDescription: runtime => {
366
+ const model = runtime.ctx.session.model;
367
+ return model ? `Model: ${model.provider}/${model.id}` : "Model: none selected";
368
+ },
362
369
  handleTui: (_command, runtime) => {
363
370
  runtime.ctx.showModelSelector({ temporaryOnly: true });
364
371
  runtime.ctx.editor.setText("");
@@ -375,6 +382,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
375
382
  { name: "status", description: "Show fast mode status" },
376
383
  ],
377
384
  allowArgs: true,
385
+ getTuiAutocompleteDescription: runtime => `Fast: ${formatFastModeStatus(runtime.ctx.session)}`,
378
386
  handle: async (command, runtime) => {
379
387
  const arg = command.args.toLowerCase();
380
388
  if (!arg || arg === "toggle") {
@@ -442,6 +450,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
442
450
  { name: "dump", description: "Copy the advisor's transcript to clipboard", usage: "[raw]" },
443
451
  ],
444
452
  allowArgs: true,
453
+ getTuiAutocompleteDescription: runtime => {
454
+ const stats = runtime.ctx.session.getAdvisorStats();
455
+ if (stats.active && stats.model) return `Advisor: on (${stats.model.provider}/${stats.model.id})`;
456
+ if (stats.configured) return "Advisor: configured, no model";
457
+ return "Advisor: off";
458
+ },
445
459
  handle: async (command, runtime) => {
446
460
  const { verb, rest } = parseSubcommand(command.args);
447
461
  if (!verb || verb === "toggle") {
@@ -556,16 +570,33 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
556
570
  },
557
571
  {
558
572
  name: "dump",
559
- description: "Copy session transcript to clipboard",
560
- acpDescription: "Return full transcript as plain text",
573
+ description: "Copy session transcript to clipboard (and write LLM request JSON to tmp)",
574
+ acpDescription: "Return full transcript as plain text, with LLM request JSON path",
561
575
  allowArgs: true,
562
576
  handle: async (_command, runtime) => {
563
577
  const text = runtime.session.formatSessionAsText();
564
- await runtime.output(text || "No messages to dump yet.");
578
+ if (!text) {
579
+ await runtime.output("No messages to dump yet.");
580
+ return commandConsumed();
581
+ }
582
+ let sidecarPath: string | undefined;
583
+ try {
584
+ sidecarPath = await runtime.session.dumpLlmRequestToTmpDir();
585
+ } catch {
586
+ // Sidecar is best-effort; the transcript is still output below.
587
+ }
588
+ const lines = [text];
589
+ if (sidecarPath)
590
+ lines.push(
591
+ "",
592
+ `LLM request JSON: ${sidecarPath}`,
593
+ "This file persists on disk and may contain raw context/secrets — treat accordingly.",
594
+ );
595
+ await runtime.output(lines.join("\n"));
565
596
  return commandConsumed();
566
597
  },
567
- handleTui: (_command, runtime) => {
568
- runtime.ctx.handleDumpCommand();
598
+ handleTui: async (_command, runtime) => {
599
+ await runtime.ctx.handleDumpCommand();
569
600
  runtime.ctx.editor.setText("");
570
601
  },
571
602
  },
@@ -603,6 +634,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
603
634
  { name: "stop", description: "Stop sharing" },
604
635
  ],
605
636
  allowArgs: true,
637
+ getTuiAutocompleteDescription: runtime => {
638
+ if (runtime.ctx.collabHost) {
639
+ return `Collab: hosting (${Math.max(0, runtime.ctx.collabHost.participants.length - 1)} guests)`;
640
+ }
641
+ if (runtime.ctx.collabGuest?.readOnly) return "Collab: read-only guest";
642
+ if (runtime.ctx.collabGuest) return "Collab: guest";
643
+ return "Collab: off";
644
+ },
606
645
  handleTui: async (command, runtime) => {
607
646
  const ctx = runtime.ctx;
608
647
  ctx.editor.setText("");
@@ -702,6 +741,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
702
741
  {
703
742
  name: "leave",
704
743
  description: "Leave the collab session",
744
+ getTuiAutocompleteDescription: runtime => {
745
+ if (runtime.ctx.collabHost) return "Leave collab: hosting";
746
+ if (runtime.ctx.collabGuest) return "Leave collab: guest";
747
+ return "Leave collab: not in collab";
748
+ },
705
749
  handleTui: async (_command, runtime) => {
706
750
  const ctx = runtime.ctx;
707
751
  ctx.editor.setText("");
@@ -726,6 +770,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
726
770
  { name: "visible", description: "Switch to visible mode" },
727
771
  ],
728
772
  allowArgs: true,
773
+ getTuiAutocompleteDescription: runtime => {
774
+ if (!runtime.ctx.settings.get("browser.enabled" as SettingPath)) return "Browser: disabled";
775
+ return runtime.ctx.settings.get("browser.headless" as SettingPath) ? "Browser: headless" : "Browser: visible";
776
+ },
729
777
  handle: async (command, runtime) => {
730
778
  const arg = command.args.toLowerCase();
731
779
  const enabled = runtime.settings.get("browser.enabled" as SettingPath) as boolean;
@@ -817,6 +865,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
817
865
  { name: "rm", description: "Remove task/phase/all (fuzzy-matched)", usage: "[<task|phase>]" },
818
866
  ],
819
867
  allowArgs: true,
868
+ getTuiAutocompleteDescription: runtime => {
869
+ const tasks = runtime.ctx.todoPhases.flatMap(phase => phase.tasks);
870
+ if (tasks.length === 0) return "Todos: none";
871
+ const pending = tasks.filter(task => task.status === "pending").length;
872
+ const inProgress = tasks.filter(task => task.status === "in_progress").length;
873
+ const completed = tasks.filter(task => task.status === "completed").length;
874
+ return `Todos: ${pending + inProgress} open (${inProgress} in progress, ${completed} done)`;
875
+ },
820
876
  handle: handleTodoAcp,
821
877
  handleTui: async (command, runtime) => {
822
878
  await runtime.ctx.handleTodoCommand(command.args);
@@ -881,6 +937,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
881
937
  name: "jobs",
882
938
  description: "Show async background jobs status",
883
939
  acpDescription: "Show background jobs",
940
+ getTuiAutocompleteDescription: runtime => {
941
+ const snapshot = runtime.ctx.session.getAsyncJobSnapshot({ recentLimit: 5 });
942
+ if (!snapshot || (snapshot.running.length === 0 && snapshot.recent.length === 0)) return "Jobs: none";
943
+ return `Jobs: ${snapshot.running.length} running, ${snapshot.recent.length} recent`;
944
+ },
884
945
  handle: async (_command, runtime) => {
885
946
  const snapshot = runtime.session.getAsyncJobSnapshot({ recentLimit: 5 });
886
947
  if (!snapshot || (snapshot.running.length === 0 && snapshot.recent.length === 0)) {
@@ -1016,6 +1077,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1016
1077
  name: "tools",
1017
1078
  description: "Show tools currently visible to the agent",
1018
1079
  acpDescription: "Show available tools",
1080
+ getTuiAutocompleteDescription: runtime => {
1081
+ const active = runtime.ctx.session.getActiveToolNames().length;
1082
+ const all = runtime.ctx.session.getAllToolNames().length;
1083
+ return all === 0 ? "Tools: none available" : `Tools: ${active} active / ${all} available`;
1084
+ },
1019
1085
  handle: async (_command, runtime) => {
1020
1086
  const active = runtime.session.getActiveToolNames();
1021
1087
  const all = runtime.session.getAllToolNames();
@@ -1035,6 +1101,11 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1035
1101
  name: "context",
1036
1102
  description: "Show estimated context usage breakdown",
1037
1103
  acpDescription: "Show context usage",
1104
+ getTuiAutocompleteDescription: runtime => {
1105
+ const usage = runtime.ctx.session.getContextUsage();
1106
+ if (!usage) return "Context: unavailable";
1107
+ return `Context: ${Math.round(usage.percent)}% (${formatTokenCount(usage.tokens)}/${formatTokenCount(usage.contextWindow)})`;
1108
+ },
1038
1109
  handle: async (_command, runtime) => {
1039
1110
  await runtime.output(buildContextReportText(runtime));
1040
1111
  return commandConsumed();
@@ -1094,6 +1165,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1094
1165
  description: "Login with OAuth provider",
1095
1166
  inlineHint: "[provider|redirect URL]",
1096
1167
  allowArgs: true,
1168
+ getTuiAutocompleteDescription: runtime =>
1169
+ runtime.ctx.oauthManualInput.hasPending()
1170
+ ? `Login: waiting for ${runtime.ctx.oauthManualInput.pendingProviderId ?? "OAuth"} callback`
1171
+ : "Login: choose provider",
1097
1172
  handleTui: (command, runtime) => {
1098
1173
  const manualInput = runtime.ctx.oauthManualInput;
1099
1174
  const args = command.args.trim();
@@ -1233,6 +1308,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1233
1308
  {
1234
1309
  name: "fresh",
1235
1310
  description: "Reset provider stream state without changing the local transcript",
1311
+ getTuiAutocompleteDescription: runtime =>
1312
+ runtime.ctx.session.isStreaming ? "Fresh: unavailable while streaming" : "Fresh: ready",
1236
1313
  handle: async (_command, runtime) => {
1237
1314
  const result = runtime.session.freshSession();
1238
1315
  if (!result) {
@@ -1268,6 +1345,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1268
1345
  })),
1269
1346
  acpInputHint: `[${COMPACT_MODES.map(mode => mode.name).join("|")}] [focus]`,
1270
1347
  allowArgs: true,
1348
+ getTuiAutocompleteDescription: runtime => {
1349
+ const usage = runtime.ctx.session.getContextUsage();
1350
+ return usage ? `Compact: context ${Math.round(usage.percent)}% used` : "Compact: context unavailable";
1351
+ },
1271
1352
  handle: async (command, runtime) => {
1272
1353
  const parsed = parseCompactArgs(command.args);
1273
1354
  if ("error" in parsed) return usage(parsed.error, runtime);
@@ -1354,6 +1435,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1354
1435
  allowArgs: true,
1355
1436
  handleTui: async (command, runtime) => {
1356
1437
  const question = command.text.slice(`/${command.name}`.length).trim();
1438
+ if (question) {
1439
+ runtime.ctx.editor.addToHistory(command.text);
1440
+ }
1357
1441
  runtime.ctx.editor.setText("");
1358
1442
  await runtime.ctx.handleBtwCommand(question);
1359
1443
  },
@@ -1365,6 +1449,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1365
1449
  allowArgs: true,
1366
1450
  handleTui: async (command, runtime) => {
1367
1451
  const work = command.text.slice(`/${command.name}`.length).trim();
1452
+ if (work) {
1453
+ runtime.ctx.editor.addToHistory(command.text);
1454
+ }
1368
1455
  runtime.ctx.editor.setText("");
1369
1456
  await runtime.ctx.handleTanCommand(work);
1370
1457
  },
@@ -1376,6 +1463,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1376
1463
  allowArgs: true,
1377
1464
  handleTui: async (command, runtime) => {
1378
1465
  const complaint = command.text.slice(`/${command.name}`.length).trim();
1466
+ if (complaint) {
1467
+ runtime.ctx.editor.addToHistory(command.text);
1468
+ }
1379
1469
  runtime.ctx.editor.setText("");
1380
1470
  await runtime.ctx.handleOmfgCommand(complaint);
1381
1471
  },
@@ -1394,25 +1484,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1394
1484
  {
1395
1485
  name: "debug",
1396
1486
  description: "Open debug tools selector",
1397
- allowArgs: true,
1398
- subcommands: [
1399
- {
1400
- name: "dump-next-request",
1401
- description: "Dump the next AI provider HTTP request as JSON",
1402
- usage: "<path>",
1403
- },
1404
- ],
1405
- handle: async (command, runtime) =>
1406
- handleDebugSubcommand(command.args, runtime.cwd, text => runtime.output(text)),
1407
- handleTui: async (command, runtime) => {
1408
- const args = command.args.trim();
1409
- if (args.length === 0) {
1410
- runtime.ctx.showDebugSelector();
1411
- } else {
1412
- await handleDebugSubcommand(args, runtime.ctx.sessionManager.getCwd(), text =>
1413
- runtime.ctx.showStatus(text),
1414
- );
1415
- }
1487
+ handleTui: async (_command, runtime) => {
1488
+ await runtime.ctx.showDebugSelector();
1416
1489
  runtime.ctx.editor.setText("");
1417
1490
  },
1418
1491
  },
@@ -1484,6 +1557,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1484
1557
  }
1485
1558
  },
1486
1559
  handleTui: async (command, runtime) => {
1560
+ runtime.ctx.editor.addToHistory(command.text);
1487
1561
  runtime.ctx.editor.setText("");
1488
1562
  await runtime.ctx.handleMemoryCommand(command.text);
1489
1563
  },
@@ -1511,6 +1585,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1511
1585
  runtime.ctx.editor.setText("");
1512
1586
  return;
1513
1587
  }
1588
+ runtime.ctx.editor.addToHistory(command.text);
1514
1589
  runtime.ctx.editor.setText("");
1515
1590
  await runtime.ctx.handleRenameCommand(title);
1516
1591
  },
@@ -1553,6 +1628,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1553
1628
  runtime.ctx.editor.setText("");
1554
1629
  return;
1555
1630
  }
1631
+ runtime.ctx.editor.addToHistory(command.text);
1556
1632
  runtime.ctx.editor.setText("");
1557
1633
  await runtime.ctx.handleMoveCommand(targetPath);
1558
1634
  },
@@ -2097,6 +2173,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
2097
2173
  aliases: ["force:"],
2098
2174
  inlineHint: "<tool-name> [prompt]",
2099
2175
  allowArgs: true,
2176
+ getTuiAutocompleteDescription: runtime => {
2177
+ const count = runtime.ctx.session.getActiveToolNames().length;
2178
+ return count === 0 ? "Force: no active tools" : `Force: ${count} active tools`;
2179
+ },
2100
2180
  handle: async (command, runtime) => {
2101
2181
  const spaceIdx = command.args.indexOf(" ");
2102
2182
  const toolName = spaceIdx === -1 ? command.args : command.args.slice(0, spaceIdx);
@@ -2225,34 +2305,38 @@ export const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand> = BU
2225
2305
  description: command.description,
2226
2306
  subcommands: command.subcommands,
2227
2307
  inlineHint: command.inlineHint,
2308
+ getTuiAutocompleteDescription: command.getTuiAutocompleteDescription,
2228
2309
  }),
2229
2310
  );
2230
2311
 
2312
+ function materializeTuiBuiltinSlashCommand(
2313
+ cmd: BuiltinSlashCommand,
2314
+ runtime?: TuiSlashCommandRuntime,
2315
+ ): TuiBuiltinSlashCommand {
2316
+ const materialized: TuiBuiltinSlashCommand = { ...cmd };
2317
+ if (cmd.subcommands) {
2318
+ materialized.getArgumentCompletions = buildArgumentCompletions(cmd.subcommands);
2319
+ materialized.getInlineHint = buildSubcommandInlineHint(cmd.subcommands);
2320
+ } else if (cmd.inlineHint) {
2321
+ materialized.getInlineHint = buildStaticInlineHint(cmd.inlineHint);
2322
+ }
2323
+ if (runtime && cmd.getTuiAutocompleteDescription) {
2324
+ materialized.getAutocompleteDescription = () => cmd.getTuiAutocompleteDescription?.(runtime);
2325
+ }
2326
+ return materialized;
2327
+ }
2328
+
2231
2329
  /**
2232
2330
  * Materialized builtin slash commands with completion functions derived from
2233
2331
  * declarative subcommand/hint definitions.
2234
2332
  */
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
- });
2333
+ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<TuiBuiltinSlashCommand> = BUILTIN_SLASH_COMMAND_DEFS.map(cmd =>
2334
+ materializeTuiBuiltinSlashCommand(cmd),
2335
+ );
2336
+
2337
+ export function buildTuiBuiltinSlashCommands(runtime: TuiSlashCommandRuntime): ReadonlyArray<TuiBuiltinSlashCommand> {
2338
+ return BUILTIN_SLASH_COMMAND_DEFS.map(cmd => materializeTuiBuiltinSlashCommand(cmd, runtime));
2339
+ }
2256
2340
 
2257
2341
  /**
2258
2342
  * Unified registry exposed for cross-mode tooling. Each spec carries at least