@oh-my-pi/pi-coding-agent 15.11.1 → 15.11.3

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 (69) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/dist/cli.js +643 -627
  3. package/dist/types/config/settings-schema.d.ts +36 -0
  4. package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
  5. package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
  6. package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
  7. package/dist/types/extensibility/extensions/types.d.ts +2 -2
  8. package/dist/types/extensibility/hooks/types.d.ts +8 -4
  9. package/dist/types/irc/bus.d.ts +15 -2
  10. package/dist/types/lsp/format-options.d.ts +32 -0
  11. package/dist/types/mnemopi/state.d.ts +29 -1
  12. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  13. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  14. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  15. package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
  16. package/dist/types/modes/theme/theme.d.ts +1 -1
  17. package/dist/types/session/agent-session.d.ts +17 -3
  18. package/dist/types/slash-commands/available-commands.d.ts +34 -0
  19. package/dist/types/tools/bash.d.ts +1 -1
  20. package/dist/types/tools/browser/attach.d.ts +4 -4
  21. package/dist/types/tools/browser/registry.d.ts +1 -0
  22. package/dist/types/tools/irc.d.ts +3 -2
  23. package/dist/types/tools/path-utils.d.ts +5 -5
  24. package/dist/types/utils/git.d.ts +1 -1
  25. package/package.json +11 -11
  26. package/src/config/settings-schema.ts +40 -0
  27. package/src/exec/bash-executor.ts +21 -6
  28. package/src/extensibility/custom-commands/loader.ts +3 -1
  29. package/src/extensibility/custom-commands/types.ts +6 -3
  30. package/src/extensibility/custom-tools/loader.ts +4 -7
  31. package/src/extensibility/custom-tools/types.ts +8 -4
  32. package/src/extensibility/extensions/loader.ts +2 -1
  33. package/src/extensibility/extensions/types.ts +2 -2
  34. package/src/extensibility/hooks/loader.ts +3 -1
  35. package/src/extensibility/hooks/types.ts +8 -4
  36. package/src/internal-urls/docs-index.generated.ts +4 -4
  37. package/src/irc/bus.ts +14 -3
  38. package/src/lsp/clients/lsp-linter-client.ts +2 -10
  39. package/src/lsp/defaults.json +6 -0
  40. package/src/lsp/format-options.ts +119 -0
  41. package/src/lsp/index.ts +2 -10
  42. package/src/lsp/render.ts +2 -28
  43. package/src/memories/index.ts +2 -0
  44. package/src/mnemopi/backend.ts +4 -8
  45. package/src/mnemopi/state.ts +42 -3
  46. package/src/modes/acp/acp-agent.ts +4 -67
  47. package/src/modes/components/plan-review-overlay.ts +32 -3
  48. package/src/modes/controllers/streaming-reveal.ts +16 -8
  49. package/src/modes/interactive-mode.ts +54 -2
  50. package/src/modes/rpc/rpc-client.ts +32 -0
  51. package/src/modes/rpc/rpc-mode.ts +82 -7
  52. package/src/modes/rpc/rpc-types.ts +23 -0
  53. package/src/modes/theme/theme.ts +7 -7
  54. package/src/modes/utils/ui-helpers.ts +13 -4
  55. package/src/prompts/memories/consolidation_system.md +4 -0
  56. package/src/prompts/system/irc-autoreply.md +6 -0
  57. package/src/prompts/system/irc-incoming.md +1 -1
  58. package/src/prompts/tools/bash.md +1 -0
  59. package/src/prompts/tools/irc.md +1 -1
  60. package/src/session/agent-session.ts +96 -7
  61. package/src/slash-commands/available-commands.ts +105 -0
  62. package/src/tools/bash.ts +5 -1
  63. package/src/tools/browser/attach.ts +26 -7
  64. package/src/tools/browser/registry.ts +11 -1
  65. package/src/tools/irc.ts +16 -4
  66. package/src/tools/job.ts +7 -3
  67. package/src/tools/path-utils.ts +56 -25
  68. package/src/tools/search.ts +11 -0
  69. package/src/utils/git.ts +7 -2
@@ -14,7 +14,14 @@ import {
14
14
  import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
15
15
  import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
16
16
  import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
17
- import type { Component, EditorTheme, LoaderMessageColorFn, OverlayHandle, SlashCommand } from "@oh-my-pi/pi-tui";
17
+ import type {
18
+ Component,
19
+ EditorTheme,
20
+ LoaderMessageColorFn,
21
+ NativeScrollbackLiveRegion,
22
+ OverlayHandle,
23
+ SlashCommand,
24
+ } from "@oh-my-pi/pi-tui";
18
25
  import {
19
26
  Container,
20
27
  clearRenderCache,
@@ -257,6 +264,19 @@ export interface InteractiveModeOptions {
257
264
  initialMessages?: string[];
258
265
  }
259
266
 
267
+ /**
268
+ * Hosts the working loader and transient status rows. While anything is
269
+ * mounted, every row is live: report a seam at 0 so the engine never commits
270
+ * a still-animating loader to native scrollback (stale `Working…` rows would
271
+ * otherwise pile up above the live one). The transcript's own seam, when
272
+ * present, sits higher and wins (topmost-seam merge in TUI.render).
273
+ */
274
+ class StatusContainer extends Container implements NativeScrollbackLiveRegion {
275
+ getNativeScrollbackLiveRegionStart(): number | undefined {
276
+ return this.children.length > 0 ? 0 : undefined;
277
+ }
278
+ }
279
+
260
280
  export class InteractiveMode implements InteractiveModeContext {
261
281
  session: AgentSession;
262
282
  sessionManager: SessionManager;
@@ -418,7 +438,7 @@ export class InteractiveMode implements InteractiveModeContext {
418
438
  setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
419
439
  this.chatContainer = new TranscriptContainer();
420
440
  this.pendingMessagesContainer = new Container();
421
- this.statusContainer = new Container();
441
+ this.statusContainer = new StatusContainer();
422
442
  this.todoContainer = new Container();
423
443
  this.btwContainer = new Container();
424
444
  this.omfgContainer = new Container();
@@ -1785,6 +1805,7 @@ export class InteractiveMode implements InteractiveModeContext {
1785
1805
  onPick: choice => finish(choice),
1786
1806
  onCancel: () => finish(undefined),
1787
1807
  onExternalEditor: dialogOptions?.onExternalEditor,
1808
+ onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
1788
1809
  onPlanEdited: dialogOptions?.onPlanEdited,
1789
1810
  onFeedbackChange: dialogOptions?.onFeedbackChange,
1790
1811
  },
@@ -1899,6 +1920,37 @@ export class InteractiveMode implements InteractiveModeContext {
1899
1920
  }
1900
1921
  }
1901
1922
 
1923
+ async #openPlanAnnotationInExternalEditor(draft: string, commit: (text: string | null) => void): Promise<void> {
1924
+ const editorCmd = getEditorCommand();
1925
+ if (!editorCmd) {
1926
+ this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
1927
+ return;
1928
+ }
1929
+
1930
+ let ttyHandle: fs.FileHandle | null = null;
1931
+ try {
1932
+ ttyHandle = await this.#openEditorTerminalHandle();
1933
+ this.ui.stop();
1934
+
1935
+ const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
1936
+ ? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
1937
+ : ["inherit", "inherit", "inherit"];
1938
+
1939
+ const result = await openInEditor(editorCmd, draft, { extension: ".md", stdio });
1940
+ if (result !== null) {
1941
+ commit(result);
1942
+ }
1943
+ } catch (error) {
1944
+ this.showWarning(`Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`);
1945
+ } finally {
1946
+ if (ttyHandle) {
1947
+ await ttyHandle.close();
1948
+ }
1949
+ this.ui.start();
1950
+ this.ui.requestRender(true);
1951
+ }
1952
+ }
1953
+
1902
1954
  async #applyPlanExecutionModel(entry: ResolvedRoleModel | undefined): Promise<void> {
1903
1955
  if (!entry) return;
1904
1956
  try {
@@ -13,6 +13,8 @@ import type { FileSink } from "bun";
13
13
  import type { BashResult } from "../../exec/bash-executor";
14
14
  import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
15
15
  import type {
16
+ RpcAvailableCommandsUpdateFrame,
17
+ RpcAvailableSlashCommand,
16
18
  RpcCommand,
17
19
  RpcExtensionUIRequest,
18
20
  RpcHandoffResult,
@@ -63,6 +65,7 @@ export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
63
65
  export type RpcSubagentLifecycleListener = (payload: RpcSubagentLifecycleFrame["payload"]) => void;
64
66
  export type RpcSubagentProgressListener = (payload: RpcSubagentProgressFrame["payload"]) => void;
65
67
  export type RpcSubagentEventListener = (payload: RpcSubagentEventFrame["payload"]) => void;
68
+ export type RpcAvailableCommandsUpdateListener = (commands: RpcAvailableSlashCommand[]) => void;
66
69
 
67
70
  export interface RpcClientToolContext<TDetails = unknown> {
68
71
  toolCallId: string;
@@ -161,6 +164,11 @@ function isRpcSubagentEventFrame(value: unknown): value is RpcSubagentEventFrame
161
164
  return value.type === "subagent_event" && isRecord(value.payload);
162
165
  }
163
166
 
167
+ function isRpcAvailableCommandsUpdateFrame(value: unknown): value is RpcAvailableCommandsUpdateFrame {
168
+ if (!isRecord(value)) return false;
169
+ return value.type === "available_commands_update" && Array.isArray(value.commands);
170
+ }
171
+
164
172
  function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
165
173
  if (!isRecord(value)) return false;
166
174
  return (
@@ -202,6 +210,7 @@ export class RpcClient {
202
210
  #subagentLifecycleListeners = new Set<RpcSubagentLifecycleListener>();
203
211
  #subagentProgressListeners = new Set<RpcSubagentProgressListener>();
204
212
  #subagentEventListeners = new Set<RpcSubagentEventListener>();
213
+ #availableCommandsUpdateListeners = new Set<RpcAvailableCommandsUpdateListener>();
205
214
  #pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
206
215
  new Map();
207
216
  #customTools: RpcClientCustomTool[] = [];
@@ -377,6 +386,14 @@ export class RpcClient {
377
386
  return () => this.#subagentEventListeners.delete(listener);
378
387
  }
379
388
 
389
+ /**
390
+ * Subscribe to slash-command availability updates emitted by the RPC server.
391
+ */
392
+ onAvailableCommandsUpdate(listener: RpcAvailableCommandsUpdateListener): () => void {
393
+ this.#availableCommandsUpdateListeners.add(listener);
394
+ return () => this.#availableCommandsUpdateListeners.delete(listener);
395
+ }
396
+
380
397
  /**
381
398
  * Get collected stderr output (useful for debugging).
382
399
  */
@@ -511,6 +528,14 @@ export class RpcClient {
511
528
  return this.#getData<{ models: ModelInfo[] }>(response).models;
512
529
  }
513
530
 
531
+ /**
532
+ * Get list of available slash commands.
533
+ */
534
+ async getAvailableCommands(): Promise<RpcAvailableSlashCommand[]> {
535
+ const response = await this.#send({ type: "get_available_commands" });
536
+ return this.#getData<{ commands: RpcAvailableSlashCommand[] }>(response).commands;
537
+ }
538
+
514
539
  /**
515
540
  * Set thinking level.
516
541
  */
@@ -825,6 +850,13 @@ export class RpcClient {
825
850
  return;
826
851
  }
827
852
 
853
+ if (isRpcAvailableCommandsUpdateFrame(data)) {
854
+ for (const listener of this.#availableCommandsUpdateListeners) {
855
+ listener(data.commands);
856
+ }
857
+ return;
858
+ }
859
+
828
860
  if (!isAgentSessionEvent(data)) return;
829
861
 
830
862
  for (const listener of this.#sessionEventListeners) {
@@ -12,6 +12,8 @@
12
12
  */
13
13
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
14
14
  import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
15
+ import { reset as resetCapabilities } from "../../capability";
16
+ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
15
17
  import {
16
18
  type ExtensionUIContext,
17
19
  type ExtensionUIDialogOptions,
@@ -19,8 +21,13 @@ import {
19
21
  type ExtensionWidgetOptions,
20
22
  getExtensionUISelectOptionLabel,
21
23
  } from "../../extensibility/extensions";
24
+ import { buildSkillPromptMessage } from "../../extensibility/skills";
25
+ import { loadSlashCommands } from "../../extensibility/slash-commands";
22
26
  import { type Theme, theme } from "../../modes/theme/theme";
23
27
  import type { AgentSession } from "../../session/agent-session";
28
+ import { SKILL_PROMPT_MESSAGE_TYPE } from "../../session/messages";
29
+ import { executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
30
+ import { buildAvailableSlashCommands } from "../../slash-commands/available-commands";
24
31
  import type { EventBus } from "../../utils/event-bus";
25
32
  import { initializeExtensions } from "../runtime-init";
26
33
  import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
@@ -70,6 +77,28 @@ export type RpcSessionChangeResult =
70
77
  | { type: "branch"; data: { text: string; cancelled: boolean } };
71
78
 
72
79
  export type RpcSessionChangeSession = Pick<AgentSession, "newSession" | "switchSession" | "branch">;
80
+
81
+ export type RpcSkillCommandSession = Pick<AgentSession, "promptCustomMessage" | "skills" | "skillsSettings">;
82
+
83
+ export async function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string): Promise<boolean> {
84
+ if (!text.startsWith("/skill:")) return false;
85
+ if (!session.skillsSettings?.enableSkillCommands) return false;
86
+ const spaceIndex = text.indexOf(" ");
87
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
88
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
89
+ const skillName = commandName.slice("skill:".length);
90
+ const skill = session.skills.find(candidate => candidate.name === skillName);
91
+ if (!skill) return false;
92
+ const built = await buildSkillPromptMessage(skill, args);
93
+ await session.promptCustomMessage({
94
+ customType: SKILL_PROMPT_MESSAGE_TYPE,
95
+ content: built.message,
96
+ display: true,
97
+ details: built.details,
98
+ attribution: "user",
99
+ });
100
+ return true;
101
+ }
73
102
  export type RpcSubagentResetRegistry = Pick<RpcSubagentRegistry, "clear">;
74
103
 
75
104
  export async function handleRpcSessionChange(
@@ -511,6 +540,24 @@ export async function runRpcMode(
511
540
  output(event);
512
541
  });
513
542
 
543
+ const getAvailableCommands = async () => buildAvailableSlashCommands(session);
544
+ const reloadPluginState = async () => {
545
+ const cwd = session.sessionManager.getCwd();
546
+ const projectPath = await resolveActiveProjectRegistryPath(cwd);
547
+ clearPluginRootsAndCaches(projectPath ? [projectPath] : undefined);
548
+ resetCapabilities();
549
+ session.setSlashCommands(await loadSlashCommands({ cwd }));
550
+ await session.refreshSshTool({ activateIfAvailable: true });
551
+ await emitAvailableCommandsUpdate();
552
+ };
553
+ const emitAvailableCommandsUpdate = async () => {
554
+ output({ type: "available_commands_update", commands: await getAvailableCommands() });
555
+ };
556
+ session.subscribeCommandMetadataChanged(() => {
557
+ void emitAvailableCommandsUpdate();
558
+ });
559
+ await emitAvailableCommandsUpdate();
560
+
514
561
  // Handle a single command
515
562
  const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
516
563
  const id = command.id;
@@ -521,6 +568,33 @@ export async function runRpcMode(
521
568
  // =================================================================
522
569
 
523
570
  case "prompt": {
571
+ if (await tryRunRpcSkillCommand(session, command.message)) {
572
+ return success(id, "prompt");
573
+ }
574
+ const builtinResult = await executeAcpBuiltinSlashCommand(command.message, {
575
+ session,
576
+ sessionManager: session.sessionManager,
577
+ settings: session.settings,
578
+ cwd: session.sessionManager.getCwd(),
579
+ output: text => output({ type: "command_output", text }),
580
+ refreshCommands: emitAvailableCommandsUpdate,
581
+ reloadPlugins: reloadPluginState,
582
+ notifyTitleChanged: async () => {
583
+ output({ type: "session_info_update", title: session.sessionName, sessionId: session.sessionId });
584
+ },
585
+ notifyConfigChanged: async () => {
586
+ output({ type: "config_update", model: session.model, thinkingLevel: session.thinkingLevel });
587
+ },
588
+ });
589
+ if (builtinResult !== false) {
590
+ if ("prompt" in builtinResult) {
591
+ session
592
+ .prompt(builtinResult.prompt, { images: command.images })
593
+ .catch(e => output(error(id, "prompt", e.message)));
594
+ }
595
+ return success(id, "prompt");
596
+ }
597
+
524
598
  // Don't await - events will stream
525
599
  // Extension commands are executed immediately, file prompt templates are expanded
526
600
  // If streaming and streamingBehavior specified, queues via steer/followUp
@@ -556,8 +630,11 @@ export async function runRpcMode(
556
630
  return success(id, "abort_and_prompt");
557
631
  }
558
632
 
559
- case "new_session": {
633
+ case "new_session":
634
+ case "switch_session":
635
+ case "branch": {
560
636
  const result = await handleRpcSessionChange(session, command, subagentRegistry);
637
+ if (!result.data.cancelled) await emitAvailableCommandsUpdate();
561
638
  return success(id, result.type, result.data);
562
639
  }
563
640
 
@@ -592,6 +669,10 @@ export async function runRpcMode(
592
669
  return success(id, "get_state", state);
593
670
  }
594
671
 
672
+ case "get_available_commands": {
673
+ return success(id, "get_available_commands", { commands: await getAvailableCommands() });
674
+ }
675
+
595
676
  case "set_todos": {
596
677
  session.setTodoPhases(command.phases);
597
678
  return success(id, "set_todos", { todoPhases: session.getTodoPhases() });
@@ -770,12 +851,6 @@ export async function runRpcMode(
770
851
  return success(id, "export_html", { path });
771
852
  }
772
853
 
773
- case "switch_session":
774
- case "branch": {
775
- const result = await handleRpcSessionChange(session, command, subagentRegistry);
776
- return success(id, result.type, result.data);
777
- }
778
-
779
854
  case "get_branch_messages": {
780
855
  const messages = session.getUserMessagesForBranching();
781
856
  return success(id, "get_branch_messages", { messages });
@@ -11,6 +11,7 @@ import type { BashResult } from "../../exec/bash-executor";
11
11
  import type { ContextUsage } from "../../extensibility/extensions/types";
12
12
  import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
13
13
  import type { FileEntry } from "../../session/session-manager";
14
+ import type { AvailableSlashCommandSource } from "../../slash-commands/available-commands";
14
15
  import type {
15
16
  AgentProgress,
16
17
  SubagentEventPayload,
@@ -34,6 +35,7 @@ export type RpcCommand =
34
35
 
35
36
  // State
36
37
  | { id?: string; type: "get_state" }
38
+ | { id?: string; type: "get_available_commands" }
37
39
  | { id?: string; type: "set_todos"; phases: TodoPhase[] }
38
40
  | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
39
41
  | { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
@@ -110,6 +112,20 @@ export interface RpcSessionState {
110
112
  contextUsage?: ContextUsage;
111
113
  }
112
114
 
115
+ export interface RpcAvailableSlashCommand {
116
+ name: string;
117
+ aliases?: string[];
118
+ description?: string;
119
+ input?: { hint?: string };
120
+ subcommands?: Array<{ name: string; description?: string; usage?: string }>;
121
+ source: AvailableSlashCommandSource;
122
+ }
123
+
124
+ export interface RpcAvailableCommandsUpdateFrame {
125
+ type: "available_commands_update";
126
+ commands: RpcAvailableSlashCommand[];
127
+ }
128
+
113
129
  export interface RpcHandoffResult {
114
130
  savedPath?: string;
115
131
  }
@@ -156,6 +172,13 @@ export type RpcResponse =
156
172
 
157
173
  // State
158
174
  | { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
175
+ | {
176
+ id?: string;
177
+ type: "response";
178
+ command: "get_available_commands";
179
+ success: true;
180
+ data: { commands: RpcAvailableSlashCommand[] };
181
+ }
159
182
  | { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
160
183
  | { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
161
184
  | { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
@@ -2565,10 +2565,10 @@ const HIGHLIGHT_CACHE_MAX = 256;
2565
2565
  const highlightCache = new LRUCache<string, string>({ max: HIGHLIGHT_CACHE_MAX });
2566
2566
  let highlightCacheTheme: Theme | undefined;
2567
2567
 
2568
- function highlightCached(code: string, validLang: string | undefined): string | null {
2569
- if (highlightCacheTheme !== theme) {
2568
+ function highlightCached(code: string, validLang: string | undefined, highlightTheme: Theme): string | null {
2569
+ if (highlightCacheTheme !== highlightTheme) {
2570
2570
  highlightCache.clear();
2571
- highlightCacheTheme = theme;
2571
+ highlightCacheTheme = highlightTheme;
2572
2572
  }
2573
2573
  const key = `${validLang ?? ""}\x00${code}`;
2574
2574
  const hit = highlightCache.get(key);
@@ -2577,7 +2577,7 @@ function highlightCached(code: string, validLang: string | undefined): string |
2577
2577
  }
2578
2578
  let highlighted: string;
2579
2579
  try {
2580
- highlighted = nativeHighlightCode(code, validLang, getHighlightColors(theme));
2580
+ highlighted = nativeHighlightCode(code, validLang, getHighlightColors(highlightTheme));
2581
2581
  } catch {
2582
2582
  return null;
2583
2583
  }
@@ -2589,9 +2589,9 @@ function highlightCached(code: string, validLang: string | undefined): string |
2589
2589
  * Highlight code with syntax coloring based on file extension or language.
2590
2590
  * Returns array of highlighted lines.
2591
2591
  */
2592
- export function highlightCode(code: string, lang?: string): string[] {
2592
+ export function highlightCode(code: string, lang?: string, highlightTheme: Theme = theme): string[] {
2593
2593
  const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2594
- const highlighted = highlightCached(code, validLang);
2594
+ const highlighted = highlightCached(code, validLang, highlightTheme);
2595
2595
  // Always return a fresh array: callers (e.g. renderCodeCell) push extra lines
2596
2596
  // onto the result, which would corrupt the cached string otherwise.
2597
2597
  return (highlighted ?? code).split("\n");
@@ -2639,7 +2639,7 @@ export function getMarkdownTheme(): MarkdownTheme {
2639
2639
  resolveMermaidAscii,
2640
2640
  highlightCode: (code: string, lang?: string): string[] => {
2641
2641
  const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2642
- const highlighted = highlightCached(code, validLang);
2642
+ const highlighted = highlightCached(code, validLang, theme);
2643
2643
  if (highlighted !== null) return highlighted.split("\n");
2644
2644
  return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
2645
2645
  },
@@ -191,7 +191,11 @@ export class UiHelpers {
191
191
  this.ctx.chatContainer.addChild(component);
192
192
  break;
193
193
  }
194
- if (message.customType === "irc:incoming" || message.customType === "irc:relay") {
194
+ if (
195
+ message.customType === "irc:incoming" ||
196
+ message.customType === "irc:autoreply" ||
197
+ message.customType === "irc:relay"
198
+ ) {
195
199
  const details = (
196
200
  message as CustomMessage<{
197
201
  from?: string;
@@ -201,13 +205,18 @@ export class UiHelpers {
201
205
  replyTo?: string;
202
206
  }>
203
207
  ).details;
204
- const incoming = message.customType === "irc:incoming";
208
+ const kind =
209
+ message.customType === "irc:incoming"
210
+ ? ("incoming" as const)
211
+ : message.customType === "irc:autoreply"
212
+ ? ("autoreply" as const)
213
+ : ("relay" as const);
205
214
  const card = createIrcMessageCard(
206
215
  {
207
- kind: incoming ? "incoming" : "relay",
216
+ kind,
208
217
  from: details?.from,
209
218
  to: details?.to,
210
- body: incoming ? details?.message : details?.body,
219
+ body: kind === "incoming" ? details?.message : details?.body,
211
220
  replyTo: details?.replyTo,
212
221
  timestamp: message.timestamp,
213
222
  },
@@ -0,0 +1,4 @@
1
+ You are the memory-stage-two consolidator.
2
+
3
+ Follow the user-provided consolidation task exactly.
4
+ Return strict JSON only — no markdown, no commentary.
@@ -0,0 +1,6 @@
1
+ <irc>
2
+ You received an IRC message from agent `{{from}}`{{#if replyTo}} (replying to {{replyTo}}){{/if}} while you are busy mid-task. This is a side-channel turn: reply briefly and directly using the conversation context already available to you. NEVER call tools. The text you write is delivered back to `{{from}}` as your answer.
3
+
4
+ Message:
5
+ {{message}}
6
+ </irc>
@@ -3,5 +3,5 @@ Incoming IRC message from agent `{{from}}`{{#if replyTo}} (replying to {{replyTo
3
3
 
4
4
  {{message}}
5
5
 
6
- If a response is expected, reply with the `irc` tool (`op: "send"`, `to: "{{from}}"`) — you may finish your current step first. Nobody replies on your behalf.
6
+ {{#if autoReplied}}You are mid-task, so a side-channel auto-reply was generated from your context and delivered to `{{from}}` on your behalf (recorded after this message). Follow up with the `irc` tool (`op: "send"`, `to: "{{from}}"`) only if that auto-reply needs correcting.{{else}}If a response is expected, reply with the `irc` tool (`op: "send"`, `to: "{{from}}"`) — you may finish your current step first. Nobody replies on your behalf.{{/if}}
7
7
  </irc>
@@ -6,6 +6,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca
6
6
  - Quote variable expansions like `"$NAME"` to preserve exact content
7
7
  - PTY mode is opt-in: set `pty: true` only when the command needs a real terminal (e.g. `sudo`, `ssh` requiring user input); default is `false`
8
8
  - Use `;` only when later commands should run regardless of earlier failures
9
+ - Multiple bash calls in one message run concurrently. NEVER split order-dependent commands across parallel calls — chain them with `&&` in a single call.
9
10
  - Internal URIs (`skill://`, `agent://`, etc.) are auto-resolved to filesystem paths
10
11
  {{#if asyncEnabled}}
11
12
  - Use `async: true` for long-running commands when you don't need immediate output; the call returns a background job ID and the result is delivered automatically as a follow-up.
@@ -9,7 +9,7 @@ Sends short text messages to other agents in this process and receives theirs.
9
9
  - `op: "wait"` — block until a message arrives (optionally only `from` a specific peer); consumes and returns it. A timeout is a clean "no message" result, not an error.
10
10
  - `op: "inbox"` — drain pending messages without blocking (`peek: true` to leave them unread).
11
11
  - `replyTo` — set it to the id of the message you are answering so the sender can correlate.
12
- - Nobody answers on a peer's behalf anymore: a reply only arrives when the recipient actually sends one. For background on what a peer has been doing, `read` `history://<id>` instead of interrogating them.
12
+ - Nobody answers on a peer's behalf a reply normally arrives only when the recipient sends one — with one exception: `send` with `await: true` to a peer that is mid-turn and cannot reach a step boundary (async execution disabled, e.g. blocked in a synchronous task spawn) gets a side-channel auto-reply generated from that peer's context. For background on what a peer has been doing, `read` `history://<id>` instead of interrogating them.
13
13
  </instruction>
14
14
 
15
15
  <when_to_use>