@otto-code/protocol 0.8.12 → 0.8.13

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 (37) hide show
  1. package/dist/agent-labels.d.ts +3 -0
  2. package/dist/agent-labels.js +10 -0
  3. package/dist/agent-personalities.d.ts +4 -3
  4. package/dist/agent-personalities.js +13 -20
  5. package/dist/agent-types.d.ts +24 -4
  6. package/dist/binary-frames/terminal.d.ts +4 -0
  7. package/dist/binary-frames/terminal.js +1 -0
  8. package/dist/brain.d.ts +91 -0
  9. package/dist/brain.js +19 -1
  10. package/dist/chat/rpc-schemas.js +1 -0
  11. package/dist/chat/types.js +1 -0
  12. package/dist/client-capabilities.d.ts +1 -0
  13. package/dist/client-capabilities.js +4 -0
  14. package/dist/daemon-config.d.ts +6 -0
  15. package/dist/daemon-config.js +6 -0
  16. package/dist/generated/validation/ws-outbound.aot.js +64093 -60274
  17. package/dist/loop/rpc-schemas.js +1 -0
  18. package/dist/messages.d.ts +3546 -422
  19. package/dist/messages.js +445 -13
  20. package/dist/provider-manifest.js +7 -0
  21. package/dist/provider-snapshot-codec.d.ts +18 -0
  22. package/dist/provider-snapshot-codec.js +71 -0
  23. package/dist/schedule/rpc-schemas.d.ts +8 -64
  24. package/dist/schedule/types.d.ts +3 -24
  25. package/dist/schedule/types.js +1 -11
  26. package/dist/search/text-match.d.ts +55 -0
  27. package/dist/search/text-match.js +262 -0
  28. package/dist/suggested-tasks.js +1 -1
  29. package/dist/terminal-input-mode.d.ts +4 -0
  30. package/dist/terminal-input-mode.js +35 -6
  31. package/dist/terminal-key-input.js +15 -6
  32. package/dist/terminal-profiles.d.ts +35 -0
  33. package/dist/terminal-profiles.js +246 -4
  34. package/dist/tool-call-display.d.ts +2 -2
  35. package/dist/tool-call-display.js +6 -6
  36. package/dist/validation/ws-outbound-schema-metadata.d.ts +587 -77
  37. package/package.json +1 -1
@@ -1,9 +1,13 @@
1
1
  export const DEFAULT_TERMINAL_INPUT_MODE_STATE = {
2
2
  kittyKeyboardFlags: 0,
3
3
  win32InputMode: false,
4
+ applicationCursorKeys: false,
5
+ bracketedPaste: false,
4
6
  };
5
7
  const ESC = String.fromCharCode(0x1b);
8
+ const APPLICATION_CURSOR_KEYS_MODE = 1;
6
9
  const WIN32_INPUT_MODE = 9001;
10
+ const BRACKETED_PASTE_MODE = 2004;
7
11
  const CSI_INPUT_MODE_SEQUENCE = new RegExp(`${ESC}\\[(?:([<>=?]?)([0-9;]*)u|\\?([0-9;]*)([hl]))`, "g");
8
12
  const INCOMPLETE_CSI_INPUT_MODE_SEQUENCE = new RegExp(`${ESC}\\[[<>=?]?[0-9;]*$`);
9
13
  function parseFirstParam(params) {
@@ -34,12 +38,16 @@ export function terminalInputModeSupportsModifiedEnter(state) {
34
38
  }
35
39
  export function terminalInputModeStatesEqual(left, right) {
36
40
  return (left.kittyKeyboardFlags === right.kittyKeyboardFlags &&
37
- left.win32InputMode === right.win32InputMode);
41
+ left.win32InputMode === right.win32InputMode &&
42
+ Boolean(left.applicationCursorKeys) === Boolean(right.applicationCursorKeys) &&
43
+ Boolean(left.bracketedPaste) === Boolean(right.bracketedPaste));
38
44
  }
39
45
  export class TerminalInputModeTracker {
40
46
  constructor() {
41
47
  this.kittyKeyboardFlags = 0;
42
48
  this.win32InputMode = false;
49
+ this.applicationCursorKeys = false;
50
+ this.bracketedPaste = false;
43
51
  this.kittyKeyboardStack = [];
44
52
  this.pending = "";
45
53
  }
@@ -80,6 +88,8 @@ export class TerminalInputModeTracker {
80
88
  reset() {
81
89
  this.kittyKeyboardFlags = 0;
82
90
  this.win32InputMode = false;
91
+ this.applicationCursorKeys = false;
92
+ this.bracketedPaste = false;
83
93
  this.kittyKeyboardStack.length = 0;
84
94
  this.pending = "";
85
95
  }
@@ -87,6 +97,8 @@ export class TerminalInputModeTracker {
87
97
  return {
88
98
  kittyKeyboardFlags: this.kittyKeyboardFlags,
89
99
  win32InputMode: this.win32InputMode,
100
+ applicationCursorKeys: this.applicationCursorKeys,
101
+ bracketedPaste: this.bracketedPaste,
90
102
  };
91
103
  }
92
104
  getKittyKeyboardFlags() {
@@ -103,6 +115,12 @@ export class TerminalInputModeTracker {
103
115
  if (this.win32InputMode) {
104
116
  parts.push("\x1b[?9001h");
105
117
  }
118
+ if (this.applicationCursorKeys) {
119
+ parts.push("\x1b[?1h");
120
+ }
121
+ if (this.bracketedPaste) {
122
+ parts.push("\x1b[?2004h");
123
+ }
106
124
  return parts.join("");
107
125
  }
108
126
  applyKittyKeyboardSequence(prefix, params) {
@@ -140,12 +158,23 @@ export class TerminalInputModeTracker {
140
158
  }
141
159
  applyPrivateModeSequence(params, final) {
142
160
  const modes = parsePrivateModeParams(params);
143
- if (!modes.has(WIN32_INPUT_MODE)) {
144
- return false;
161
+ let changed = false;
162
+ if (modes.has(WIN32_INPUT_MODE)) {
163
+ const previous = this.win32InputMode;
164
+ this.win32InputMode = final === "h";
165
+ changed = this.win32InputMode !== previous || changed;
166
+ }
167
+ if (modes.has(APPLICATION_CURSOR_KEYS_MODE)) {
168
+ const previous = this.applicationCursorKeys;
169
+ this.applicationCursorKeys = final === "h";
170
+ changed = this.applicationCursorKeys !== previous || changed;
171
+ }
172
+ if (modes.has(BRACKETED_PASTE_MODE)) {
173
+ const previous = this.bracketedPaste;
174
+ this.bracketedPaste = final === "h";
175
+ changed = this.bracketedPaste !== previous || changed;
145
176
  }
146
- const previous = this.win32InputMode;
147
- this.win32InputMode = final === "h";
148
- return this.win32InputMode !== previous;
177
+ return changed;
149
178
  }
150
179
  }
151
180
  //# sourceMappingURL=terminal-input-mode.js.map
@@ -99,6 +99,9 @@ function csiWithModifier(finalByte, input) {
99
99
  const mod = modifierParam(input);
100
100
  return mod === 1 ? `\x1b[${finalByte}` : `\x1b[1;${mod}${finalByte}`;
101
101
  }
102
+ function ss3WithModifier(finalByte, input) {
103
+ return modifierParam(input) === 1 ? `\x1bO${finalByte}` : csiWithModifier(finalByte, input);
104
+ }
102
105
  function csiTilde(base, input) {
103
106
  const mod = modifierParam(input);
104
107
  return mod === 1 ? `\x1b[${base}~` : `\x1b[${base};${mod}~`;
@@ -133,16 +136,22 @@ function encodeFunctionKey(key, input) {
133
136
  return null;
134
137
  }
135
138
  }
136
- function encodeNavigationKey(key, input) {
139
+ function encodeArrowKey(finalByte, input, options) {
140
+ if (options.inputMode?.applicationCursorKeys) {
141
+ return ss3WithModifier(finalByte, input);
142
+ }
143
+ return csiWithModifier(finalByte, input);
144
+ }
145
+ function encodeNavigationKey(key, input, options) {
137
146
  switch (key) {
138
147
  case "ArrowUp":
139
- return csiWithModifier("A", input);
148
+ return encodeArrowKey("A", input, options);
140
149
  case "ArrowDown":
141
- return csiWithModifier("B", input);
150
+ return encodeArrowKey("B", input, options);
142
151
  case "ArrowRight":
143
- return csiWithModifier("C", input);
152
+ return encodeArrowKey("C", input, options);
144
153
  case "ArrowLeft":
145
- return csiWithModifier("D", input);
154
+ return encodeArrowKey("D", input, options);
146
155
  case "Home":
147
156
  return csiWithModifier("H", input);
148
157
  case "End":
@@ -190,7 +199,7 @@ export function encodeTerminalKeyInput(input, options = {}) {
190
199
  default:
191
200
  break;
192
201
  }
193
- const nav = encodeNavigationKey(key, input);
202
+ const nav = encodeNavigationKey(key, input, options);
194
203
  if (nav !== null)
195
204
  return nav;
196
205
  const fn = encodeFunctionKey(key, input);
@@ -1,5 +1,40 @@
1
1
  import type { TerminalProfile } from "./messages.js";
2
+ /**
3
+ * Marks where a typed prompt goes inside a profile's `command` or `args`. A
4
+ * profile carrying it accepts a prompt; one without it launches as-is.
5
+ *
6
+ * Substitution happens client-side before `create_terminal_request` is sent, so
7
+ * this is profile-format vocabulary rather than anything on the wire. It lives
8
+ * here because this module owns what a `TerminalProfile` means.
9
+ */
10
+ export declare const PROMPT_SENTINEL = "{{{prompt}}}";
2
11
  export declare const DEFAULT_TERMINAL_PROFILES: readonly TerminalProfile[];
12
+ export interface SubstitutableCommand {
13
+ command: string;
14
+ args?: string[];
15
+ }
16
+ export interface ResolvedCommand {
17
+ command: string;
18
+ args: string[];
19
+ }
20
+ /** True when the sentinel appears anywhere in `command` or an `args` entry. */
21
+ export declare function profileTakesPrompt(profile: SubstitutableCommand): boolean;
22
+ /** Replaces every sentinel occurrence with `prompt`, dropping prompt-only args when there is none. */
23
+ export declare function substitutePrompt(profile: SubstitutableCommand, prompt: string): ResolvedCommand;
24
+ /** Human-readable preview of the resolved command, for read-only display. */
25
+ export declare function formatResolvedCommand(resolved: ResolvedCommand): string;
26
+ export interface TerminalProfileLaunch {
27
+ name: string;
28
+ command: string;
29
+ args: string[];
30
+ }
31
+ /**
32
+ * What to spawn for a profile. Every launcher goes through here, so no caller
33
+ * can forward a raw `profile.args` still carrying the sentinel: launchers with
34
+ * nowhere to type (pinned targets, the workspace terminal menu) pass an empty
35
+ * prompt and get the bare command back.
36
+ */
37
+ export declare function resolveTerminalProfileLaunch(profile: TerminalProfile, prompt: string): TerminalProfileLaunch;
3
38
  export declare function guessTerminalProfileIcon(command: string): string | undefined;
4
39
  export declare function getTerminalProfileIcon(profile: TerminalProfile): string | undefined;
5
40
  export declare function resolveTerminalProfiles(terminalProfiles: TerminalProfile[] | undefined): readonly TerminalProfile[];
@@ -1,9 +1,78 @@
1
1
  import { KNOWN_PROVIDER_ICON_NAMES } from "./provider-icon-names.js";
2
+ /**
3
+ * Marks where a typed prompt goes inside a profile's `command` or `args`. A
4
+ * profile carrying it accepts a prompt; one without it launches as-is.
5
+ *
6
+ * Substitution happens client-side before `create_terminal_request` is sent, so
7
+ * this is profile-format vocabulary rather than anything on the wire. It lives
8
+ * here because this module owns what a `TerminalProfile` means.
9
+ */
10
+ export const PROMPT_SENTINEL = "{{{prompt}}}";
11
+ // Prompt forms are taken from each CLI's own `--help`. claude, codex, and pi
12
+ // take the prompt as a trailing positional. opencode's positional is the
13
+ // project directory, so its prompt goes through `--prompt`, written in the
14
+ // `--flag=value` form to keep it one argv entry that can be dropped whole when
15
+ // no prompt is typed.
2
16
  export const DEFAULT_TERMINAL_PROFILES = [
3
- { id: "claude", name: "Claude Code", command: "claude", icon: "claude" },
4
- { id: "codex", name: "Codex", command: "codex", icon: "codex" },
5
- { id: "opencode", name: "OpenCode", command: "opencode", icon: "opencode" },
17
+ { id: "claude", name: "Claude Code", command: "claude", args: [PROMPT_SENTINEL], icon: "claude" },
18
+ { id: "codex", name: "Codex", command: "codex", args: [PROMPT_SENTINEL], icon: "codex" },
19
+ {
20
+ id: "opencode",
21
+ name: "OpenCode",
22
+ command: "opencode",
23
+ args: [`--prompt=${PROMPT_SENTINEL}`],
24
+ icon: "opencode",
25
+ },
26
+ { id: "pi", name: "Pi", command: "pi", args: [PROMPT_SENTINEL], icon: "pi" },
6
27
  ];
28
+ function containsSentinel(value) {
29
+ return value.includes(PROMPT_SENTINEL);
30
+ }
31
+ /** True when the sentinel appears anywhere in `command` or an `args` entry. */
32
+ export function profileTakesPrompt(profile) {
33
+ return containsSentinel(profile.command) || (profile.args ?? []).some(containsSentinel);
34
+ }
35
+ function replaceSentinel(value, prompt) {
36
+ return value.split(PROMPT_SENTINEL).join(prompt);
37
+ }
38
+ /**
39
+ * An arg that exists only to carry a prompt: the sentinel alone
40
+ * (`{{{prompt}}}`), or as the whole value of an option assignment
41
+ * (`--prompt={{{prompt}}}`). Both are dropped when there is no prompt, because
42
+ * an empty positional and a valueless `--prompt=` are not the same as omitting
43
+ * them. A sentinel embedded in larger text (`echo {{{prompt}}}`) is not
44
+ * prompt-only and still substitutes to empty.
45
+ *
46
+ * This is why an option and its value belong in one entry. Split across two,
47
+ * the option would survive with nothing to carry.
48
+ */
49
+ function isPromptOnlyArg(arg) {
50
+ if (arg === PROMPT_SENTINEL) {
51
+ return true;
52
+ }
53
+ const separator = arg.indexOf("=");
54
+ return separator > 0 && arg.slice(separator + 1) === PROMPT_SENTINEL;
55
+ }
56
+ /** Replaces every sentinel occurrence with `prompt`, dropping prompt-only args when there is none. */
57
+ export function substitutePrompt(profile, prompt) {
58
+ return {
59
+ command: replaceSentinel(profile.command, prompt),
60
+ args: (profile.args ?? []).flatMap((arg) => prompt === "" && isPromptOnlyArg(arg) ? [] : [replaceSentinel(arg, prompt)]),
61
+ };
62
+ }
63
+ /** Human-readable preview of the resolved command, for read-only display. */
64
+ export function formatResolvedCommand(resolved) {
65
+ return [resolved.command, ...resolved.args].join(" ");
66
+ }
67
+ /**
68
+ * What to spawn for a profile. Every launcher goes through here, so no caller
69
+ * can forward a raw `profile.args` still carrying the sentinel: launchers with
70
+ * nowhere to type (pinned targets, the workspace terminal menu) pass an empty
71
+ * prompt and get the bare command back.
72
+ */
73
+ export function resolveTerminalProfileLaunch(profile, prompt) {
74
+ return { name: profile.name, ...substitutePrompt(profile, prompt) };
75
+ }
7
76
  const WELL_KNOWN_COMMAND_ICONS = new Map(KNOWN_PROVIDER_ICON_NAMES.map((name) => [name, name]));
8
77
  function getCommandBaseName(command) {
9
78
  const lastSlash = command.lastIndexOf("/");
@@ -19,10 +88,183 @@ export function guessTerminalProfileIcon(command) {
19
88
  export function getTerminalProfileIcon(profile) {
20
89
  return profile.icon ?? guessTerminalProfileIcon(profile.command);
21
90
  }
91
+ // Base command name to the sentinel-bearing args that command wants, derived
92
+ // from the shipped defaults rather than written out again, so adding a default
93
+ // profile later cannot silently miss the adoption below or get the wrong form.
94
+ const PROMPT_ARGS_BY_COMMAND = new Map(DEFAULT_TERMINAL_PROFILES.map((profile) => [
95
+ getCommandBaseName(profile.command),
96
+ (profile.args ?? []).filter(containsSentinel),
97
+ ]));
98
+ // Adoption applies only to the interactive, prompt-taking form of each CLI.
99
+ // These are the global options whose values would otherwise look positional
100
+ // while walking argv. Unknown options stay conservative: a following bare arg
101
+ // blocks adoption rather than risking a second prompt or changing a subcommand.
102
+ const GLOBAL_OPTIONS_WITH_VALUES_BY_COMMAND = new Map([
103
+ [
104
+ "claude",
105
+ new Set([
106
+ "--add-dir",
107
+ "--agent",
108
+ "--agents",
109
+ "--allowed-tools",
110
+ "--allowedTools",
111
+ "--append-system-prompt",
112
+ "--betas",
113
+ "--debug-file",
114
+ "--disallowed-tools",
115
+ "--disallowedTools",
116
+ "--effort",
117
+ "--fallback-model",
118
+ "--file",
119
+ "--input-format",
120
+ "--json-schema",
121
+ "--max-budget-usd",
122
+ "--mcp-config",
123
+ "--model",
124
+ "--name",
125
+ "--output-format",
126
+ "--permission-mode",
127
+ "--plugin-dir",
128
+ "--plugin-url",
129
+ "--remote-control-session-name-prefix",
130
+ "--settings",
131
+ "--system-prompt",
132
+ "--system-prompt-file",
133
+ "--tools",
134
+ "-n",
135
+ ]),
136
+ ],
137
+ [
138
+ "codex",
139
+ new Set([
140
+ "--add-dir",
141
+ "--ask-for-approval",
142
+ "--cd",
143
+ "--config",
144
+ "--disable",
145
+ "--enable",
146
+ "--image",
147
+ "--local-provider",
148
+ "--model",
149
+ "--profile",
150
+ "--remote",
151
+ "--remote-auth-token-env",
152
+ "--sandbox",
153
+ "-C",
154
+ "-a",
155
+ "-c",
156
+ "-i",
157
+ "-m",
158
+ "-p",
159
+ "-s",
160
+ ]),
161
+ ],
162
+ [
163
+ "opencode",
164
+ new Set([
165
+ "--agent",
166
+ "--cors",
167
+ "--hostname",
168
+ "--log-level",
169
+ "--mdns-domain",
170
+ "--model",
171
+ "--port",
172
+ "--prompt",
173
+ "--replay-limit",
174
+ "--session",
175
+ "-m",
176
+ "-s",
177
+ ]),
178
+ ],
179
+ [
180
+ "pi",
181
+ new Set([
182
+ "--api-key",
183
+ "--append-system-prompt",
184
+ "--exclude-tools",
185
+ "--export",
186
+ "--extension",
187
+ "--fork",
188
+ "--mode",
189
+ "--model",
190
+ "--models",
191
+ "--name",
192
+ "--prompt-template",
193
+ "--provider",
194
+ "--session",
195
+ "--session-dir",
196
+ "--session-id",
197
+ "--skill",
198
+ "--system-prompt",
199
+ "--theme",
200
+ "--thinking",
201
+ "--tools",
202
+ "-e",
203
+ "-n",
204
+ "-t",
205
+ "-xt",
206
+ ]),
207
+ ],
208
+ ]);
209
+ // Profiles predating the sentinel got materialized into user config the first
210
+ // time anyone touched the profile list (host-page.tsx patches the whole list on
211
+ // any add, edit, or reorder). Those users would otherwise be stuck with
212
+ // launch-only versions of the agents we ship, while a fresh install gets prompt
213
+ // support, so a profile still pointing at one of those agents adopts the
214
+ // trailing sentinel on read.
215
+ //
216
+ // Keyed on the command's base name, not the profile id: profiles created
217
+ // through the settings UI get generated ids (`profile_<timestamp>_<random>`),
218
+ // so a real user's Codex profile is never id `codex`. The base name also
219
+ // normalizes `/usr/local/bin/codex` and `codex.cmd` onto the same agent.
220
+ //
221
+ // User args are preserved: `codex --yolo` becomes `codex --yolo {{{prompt}}}`.
222
+ // The appended form comes from the shipped default for that command, so
223
+ // opencode gets `--prompt=` rather than a trailing positional, which it would
224
+ // read as a project directory. A profile that already carries the sentinel
225
+ // anywhere is left alone, and so is one pointing at any other command. Nothing
226
+ // is written back, so this stays a read-time adoption with no version stamp and
227
+ // no daemon-start hook.
228
+ // Any positional means this is not the bare interactive form we can safely
229
+ // adopt. It may be a subcommand (`codex --profile work login`) or an existing
230
+ // prompt/project argument. Option assignments carry their value inline; known
231
+ // split options consume the next argv entry before positional detection.
232
+ function hasPositionalArg(command, args) {
233
+ const optionsWithValues = GLOBAL_OPTIONS_WITH_VALUES_BY_COMMAND.get(command) ?? new Set();
234
+ let expectsOptionValue = false;
235
+ for (const arg of args) {
236
+ if (expectsOptionValue) {
237
+ expectsOptionValue = false;
238
+ continue;
239
+ }
240
+ if (arg === "--") {
241
+ return true;
242
+ }
243
+ if (!arg.startsWith("-")) {
244
+ return true;
245
+ }
246
+ const separator = arg.indexOf("=");
247
+ const option = separator === -1 ? arg : arg.slice(0, separator);
248
+ expectsOptionValue = separator === -1 && optionsWithValues.has(option);
249
+ }
250
+ return false;
251
+ }
252
+ function adoptPromptSentinel(profile) {
253
+ const command = getCommandBaseName(profile.command);
254
+ const promptArgs = PROMPT_ARGS_BY_COMMAND.get(command);
255
+ if (!promptArgs || promptArgs.length === 0) {
256
+ return profile;
257
+ }
258
+ const args = profile.args ?? [];
259
+ if (profileTakesPrompt(profile) || hasPositionalArg(command, args)) {
260
+ return profile;
261
+ }
262
+ return { ...profile, args: [...args, ...promptArgs] };
263
+ }
22
264
  export function resolveTerminalProfiles(terminalProfiles) {
23
265
  if (terminalProfiles === undefined) {
24
266
  return DEFAULT_TERMINAL_PROFILES;
25
267
  }
26
- return terminalProfiles;
268
+ return terminalProfiles.map(adoptPromptSentinel);
27
269
  }
28
270
  //# sourceMappingURL=terminal-profiles.js.map
@@ -12,8 +12,8 @@ export interface ToolCallDisplayModel {
12
12
  * shows a tool/action name without a full timeline item to run through
13
13
  * {@link buildToolCallDisplayModel} (the visualizer's action labels, sub-agent
14
14
  * activity rows). Strips the MCP/Otto namespace, consults the known-tool
15
- * registry, then title-cases as a fallback - so "mcp__otto__spawn_task",
16
- * "otto.spawn_task", and a bare "spawn_task" all render as "Spawn Task".
15
+ * registry, then title-cases as a fallback - so "mcp__otto__suggest_task",
16
+ * "otto.suggest_task", and a bare "suggest_task" all render as "Suggest Task".
17
17
  */
18
18
  export declare function getToolDisplayName(name: string): string;
19
19
  export declare function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCallDisplayModel;
@@ -11,7 +11,7 @@ function readString(value) {
11
11
  //
12
12
  // Only tools whose bare id does NOT title-case cleanly on its own need an entry
13
13
  // here: lowercase compound names ("websearch") a splitter can't segment, or
14
- // ones we want to word deliberately. Well-formed snake_case ("spawn_task") and
14
+ // ones we want to word deliberately. Well-formed snake_case ("suggest_task") and
15
15
  // camelCase ("WebSearch") names are handled by the algorithmic humanizer below
16
16
  // and do NOT need listing.
17
17
  //
@@ -35,7 +35,7 @@ const KNOWN_TOOL_DISPLAY_NAMES = {
35
35
  ls: "List Files",
36
36
  };
37
37
  // Split camelCase / PascalCase and separator-delimited identifiers into words,
38
- // then Title-Case them: "WebSearch" -> "Web Search", "spawn_task" -> "Spawn
38
+ // then Title-Case them: "WebSearch" -> "Web Search", "suggest_task" -> "Suggest
39
39
  // Task", "HTTPServer" -> "HTTP Server".
40
40
  function titleCaseToolId(value) {
41
41
  return value
@@ -52,8 +52,8 @@ function humanizeToolName(name) {
52
52
  if (!trimmed) {
53
53
  return name;
54
54
  }
55
- // Strip the transport namespace first ("mcp__otto__spawn_task" ->
56
- // "spawn_task", "otto.list_agents" -> "list_agents") so both the known-tool
55
+ // Strip the transport namespace first ("mcp__otto__suggest_task" ->
56
+ // "suggest_task", "otto.list_chats" -> "list_chats") so both the known-tool
57
57
  // lookup and the fallback operate on the bare tool id.
58
58
  const leaf = getMcpToolLeafName(trimmed) ?? getOttoToolLeafName(trimmed);
59
59
  if (leaf) {
@@ -66,8 +66,8 @@ function humanizeToolName(name) {
66
66
  * shows a tool/action name without a full timeline item to run through
67
67
  * {@link buildToolCallDisplayModel} (the visualizer's action labels, sub-agent
68
68
  * activity rows). Strips the MCP/Otto namespace, consults the known-tool
69
- * registry, then title-cases as a fallback - so "mcp__otto__spawn_task",
70
- * "otto.spawn_task", and a bare "spawn_task" all render as "Spawn Task".
69
+ * registry, then title-cases as a fallback - so "mcp__otto__suggest_task",
70
+ * "otto.suggest_task", and a bare "suggest_task" all render as "Suggest Task".
71
71
  */
72
72
  export function getToolDisplayName(name) {
73
73
  return humanizeToolName(name);