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

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 (158) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/dist/cli.js +869 -825
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/capability/mcp.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/config/keybindings.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +66 -34
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  10. package/dist/types/extensibility/shared-events.d.ts +2 -2
  11. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  12. package/dist/types/internal-urls/index.d.ts +1 -0
  13. package/dist/types/internal-urls/types.d.ts +1 -1
  14. package/dist/types/irc/bus.d.ts +66 -0
  15. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  17. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  18. package/dist/types/mcp/types.d.ts +2 -0
  19. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  21. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  22. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  23. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  25. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  26. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  27. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  28. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  29. package/dist/types/modes/components/welcome.d.ts +3 -9
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  32. package/dist/types/modes/interactive-mode.d.ts +3 -2
  33. package/dist/types/modes/theme/theme.d.ts +3 -1
  34. package/dist/types/modes/types.d.ts +3 -2
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  36. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  37. package/dist/types/registry/agent-registry.d.ts +16 -5
  38. package/dist/types/session/agent-session.d.ts +35 -30
  39. package/dist/types/session/messages.d.ts +2 -4
  40. package/dist/types/session/session-history-format.d.ts +12 -0
  41. package/dist/types/session/session-manager.d.ts +21 -3
  42. package/dist/types/session/streaming-output.d.ts +23 -0
  43. package/dist/types/task/executor.d.ts +11 -2
  44. package/dist/types/task/index.d.ts +11 -4
  45. package/dist/types/task/output-manager.d.ts +0 -7
  46. package/dist/types/task/repair-args.d.ts +8 -7
  47. package/dist/types/task/types.d.ts +55 -51
  48. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  49. package/dist/types/tools/find.d.ts +0 -11
  50. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  51. package/dist/types/tools/index.d.ts +1 -3
  52. package/dist/types/tools/irc.d.ts +76 -38
  53. package/dist/types/tools/job.d.ts +7 -1
  54. package/dist/types/tools/render-utils.d.ts +22 -0
  55. package/examples/extensions/with-deps/package.json +1 -0
  56. package/package.json +11 -10
  57. package/scripts/bundle-dist.ts +28 -19
  58. package/src/async/index.ts +0 -1
  59. package/src/capability/mcp.ts +1 -0
  60. package/src/cli/gallery-cli.ts +6 -5
  61. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  62. package/src/cli/gallery-fixtures/types.ts +5 -0
  63. package/src/cli.ts +20 -6
  64. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  65. package/src/config/keybindings.ts +6 -1
  66. package/src/config/mcp-schema.json +4 -0
  67. package/src/config/settings-schema.ts +68 -41
  68. package/src/config/settings.ts +7 -0
  69. package/src/edit/renderer.ts +96 -46
  70. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  71. package/src/eval/agent-bridge.ts +3 -16
  72. package/src/eval/js/shared/prelude.txt +1 -1
  73. package/src/eval/py/prelude.py +5 -6
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +44 -14
  76. package/src/extensibility/custom-tools/types.ts +2 -2
  77. package/src/extensibility/shared-events.ts +2 -2
  78. package/src/internal-urls/docs-index.generated.ts +9 -9
  79. package/src/internal-urls/history-protocol.ts +113 -0
  80. package/src/internal-urls/index.ts +1 -0
  81. package/src/internal-urls/router.ts +3 -1
  82. package/src/internal-urls/types.ts +1 -1
  83. package/src/irc/bus.ts +292 -0
  84. package/src/main.ts +8 -60
  85. package/src/mcp/manager.ts +3 -0
  86. package/src/mcp/oauth-discovery.ts +27 -2
  87. package/src/mcp/oauth-flow.ts +47 -1
  88. package/src/mcp/transports/stdio.ts +3 -0
  89. package/src/mcp/types.ts +2 -0
  90. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  91. package/src/modes/components/assistant-message.ts +15 -0
  92. package/src/modes/components/btw-panel.ts +5 -1
  93. package/src/modes/components/compaction-summary-message.ts +68 -32
  94. package/src/modes/components/custom-editor.ts +10 -0
  95. package/src/modes/components/mcp-add-wizard.ts +13 -0
  96. package/src/modes/components/settings-selector.ts +2 -0
  97. package/src/modes/components/status-line/component.ts +22 -12
  98. package/src/modes/components/status-line/types.ts +3 -0
  99. package/src/modes/components/tool-execution.ts +31 -1
  100. package/src/modes/components/transcript-container.ts +99 -18
  101. package/src/modes/components/tree-selector.ts +6 -1
  102. package/src/modes/components/ttsr-notification.ts +72 -30
  103. package/src/modes/components/welcome.ts +9 -33
  104. package/src/modes/controllers/event-controller.ts +93 -4
  105. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  106. package/src/modes/controllers/input-controller.ts +18 -2
  107. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  108. package/src/modes/controllers/selector-controller.ts +25 -17
  109. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  110. package/src/modes/interactive-mode.ts +17 -15
  111. package/src/modes/theme/theme.ts +24 -5
  112. package/src/modes/types.ts +3 -5
  113. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  114. package/src/modes/utils/ui-helpers.ts +51 -49
  115. package/src/prompts/system/irc-incoming.md +3 -4
  116. package/src/prompts/system/orchestrate-notice.md +2 -2
  117. package/src/prompts/system/subagent-system-prompt.md +0 -5
  118. package/src/prompts/system/system-prompt.md +1 -0
  119. package/src/prompts/system/workflow-notice.md +2 -2
  120. package/src/prompts/tools/eval.md +3 -3
  121. package/src/prompts/tools/irc.md +29 -19
  122. package/src/prompts/tools/read.md +2 -2
  123. package/src/prompts/tools/task-summary.md +5 -16
  124. package/src/prompts/tools/task.md +43 -29
  125. package/src/registry/agent-lifecycle.ts +218 -0
  126. package/src/registry/agent-registry.ts +16 -5
  127. package/src/sdk.ts +29 -9
  128. package/src/session/agent-session.ts +268 -241
  129. package/src/session/messages.ts +11 -78
  130. package/src/session/session-history-format.ts +246 -0
  131. package/src/session/session-manager.ts +59 -5
  132. package/src/session/streaming-output.ts +60 -0
  133. package/src/task/executor.ts +855 -466
  134. package/src/task/index.ts +723 -794
  135. package/src/task/output-manager.ts +0 -11
  136. package/src/task/render.ts +142 -66
  137. package/src/task/repair-args.ts +21 -9
  138. package/src/task/types.ts +73 -66
  139. package/src/tools/ask.ts +4 -2
  140. package/src/tools/bash.ts +15 -5
  141. package/src/tools/browser/tab-worker.ts +26 -7
  142. package/src/tools/browser.ts +28 -1
  143. package/src/tools/find.ts +2 -27
  144. package/src/tools/grouped-file-output.ts +1 -118
  145. package/src/tools/index.ts +4 -12
  146. package/src/tools/irc.ts +596 -171
  147. package/src/tools/job.ts +41 -7
  148. package/src/tools/read.ts +57 -1
  149. package/src/tools/render-utils.ts +56 -0
  150. package/src/tools/renderers.ts +2 -0
  151. package/src/tools/resolve.ts +4 -1
  152. package/src/tools/write.ts +65 -47
  153. package/src/web/search/providers/anthropic.ts +29 -4
  154. package/dist/types/async/support.d.ts +0 -2
  155. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  156. package/dist/types/task/simple-mode.d.ts +0 -8
  157. package/src/async/support.ts +0 -5
  158. package/src/task/simple-mode.ts +0 -27
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Protocol handler for history:// URLs.
3
+ *
4
+ * Exposes agent transcripts as concise markdown. Live refs render from the
5
+ * in-memory message array; parked refs (session disposed, sessionFile
6
+ * retained) load read-only from the JSONL session file — no writer, no lock.
7
+ *
8
+ * URL forms:
9
+ * - history:// - Index of all registry agents (id, status, kind, last activity)
10
+ * - history://<agentId> - Concise markdown transcript of that agent
11
+ */
12
+ import type { AgentRef } from "../registry/agent-registry";
13
+ import { AgentRegistry } from "../registry/agent-registry";
14
+ import { formatSessionHistoryMarkdown } from "../session/session-history-format";
15
+ import { loadSessionMessagesReadOnly } from "../session/session-manager";
16
+ import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
17
+
18
+ /** Humanize a last-activity timestamp as `Ns/Nm/Nh/Nd ago`. */
19
+ function formatAgo(timestamp: number): string {
20
+ const diffMs = Math.max(0, Date.now() - timestamp);
21
+ const secs = Math.floor(diffMs / 1000);
22
+ if (secs < 60) return `${secs}s ago`;
23
+ const mins = Math.floor(secs / 60);
24
+ if (mins < 60) return `${mins}m ago`;
25
+ const hours = Math.floor(mins / 60);
26
+ if (hours < 24) return `${hours}h ago`;
27
+ return `${Math.floor(hours / 24)}d ago`;
28
+ }
29
+
30
+ /**
31
+ * Handler for history:// URLs.
32
+ *
33
+ * Resolves agent ids against the global AgentRegistry, serving transcripts
34
+ * for both live and parked agents.
35
+ */
36
+ export class HistoryProtocolHandler implements ProtocolHandler {
37
+ readonly scheme = "history";
38
+ readonly immutable = false;
39
+
40
+ async resolve(url: InternalUrl): Promise<InternalResource> {
41
+ const agentId = url.rawHost || url.hostname;
42
+ const registry = AgentRegistry.global();
43
+
44
+ if (!agentId) {
45
+ const content = this.#renderIndex(registry.list());
46
+ return {
47
+ url: url.href,
48
+ content,
49
+ contentType: "text/markdown",
50
+ size: Buffer.byteLength(content, "utf-8"),
51
+ };
52
+ }
53
+
54
+ let ref = registry.get(agentId);
55
+ if (!ref) {
56
+ // Case-insensitive fallback: agent ids are human-typed (e.g. AuthLoader).
57
+ const lower = agentId.toLowerCase();
58
+ ref = registry.list().find(candidate => candidate.id.toLowerCase() === lower);
59
+ }
60
+ if (!ref) {
61
+ const known = registry.list().map(candidate => candidate.id);
62
+ const knownStr = known.length > 0 ? known.join(", ") : "none";
63
+ throw new Error(`Unknown agent: ${agentId}\nKnown agents: ${knownStr}\nList all with history://`);
64
+ }
65
+
66
+ const notes: string[] = [];
67
+ let messages: unknown[];
68
+ if (ref.session) {
69
+ messages = ref.session.messages;
70
+ notes.push("Source: live session");
71
+ } else if (ref.sessionFile) {
72
+ messages = await loadSessionMessagesReadOnly(ref.sessionFile);
73
+ notes.push(`Source: session file (read-only, ${ref.status})`);
74
+ } else {
75
+ throw new Error(`Agent ${ref.id} has no transcript: session is gone and no session file was retained`);
76
+ }
77
+
78
+ const content = formatSessionHistoryMarkdown(messages, { title: `${ref.id} (${ref.status})` });
79
+ return {
80
+ url: url.href,
81
+ content,
82
+ contentType: "text/markdown",
83
+ size: Buffer.byteLength(content, "utf-8"),
84
+ sourcePath: ref.sessionFile ?? undefined,
85
+ notes,
86
+ };
87
+ }
88
+
89
+ #renderIndex(refs: AgentRef[]): string {
90
+ const lines: string[] = ["# Agents", ""];
91
+ if (refs.length === 0) {
92
+ lines.push("No agents registered.");
93
+ return `${lines.join("\n")}\n`;
94
+ }
95
+ lines.push("| id | status | kind | parent | last activity |", "|---|---|---|---|---|");
96
+ for (const ref of refs) {
97
+ lines.push(
98
+ `| ${ref.id} | ${ref.status} | ${ref.kind} | ${ref.parentId ?? "—"} | ${formatAgo(ref.lastActivity)} |`,
99
+ );
100
+ }
101
+ lines.push("", "Read a transcript with `read history://<id>`.");
102
+ return `${lines.join("\n")}\n`;
103
+ }
104
+
105
+ async complete(): Promise<UrlCompletion[]> {
106
+ return AgentRegistry.global()
107
+ .list()
108
+ .map(ref => ({
109
+ value: ref.id,
110
+ description: `${ref.status} · ${ref.kind}${ref.parentId ? ` · parent ${ref.parentId}` : ""}`,
111
+ }));
112
+ }
113
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  export * from "./agent-protocol";
12
12
  export * from "./artifact-protocol";
13
+ export * from "./history-protocol";
13
14
  export * from "./issue-pr-protocol";
14
15
  export * from "./json-query";
15
16
  export * from "./local-protocol";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Internal URL router for internal protocols (`agent://`, `artifact://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`).
2
+ * Internal URL router for internal protocols (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`).
3
3
  *
4
4
  * One process-global router with one handler per scheme. Access via
5
5
  * `InternalUrlRouter.instance()`. Handlers are stateless; per-session and
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { AgentProtocolHandler } from "./agent-protocol";
9
9
  import { ArtifactProtocolHandler } from "./artifact-protocol";
10
+ import { HistoryProtocolHandler } from "./history-protocol";
10
11
  import { IssueProtocolHandler, PrProtocolHandler } from "./issue-pr-protocol";
11
12
  import { LocalProtocolHandler } from "./local-protocol";
12
13
  import { McpProtocolHandler } from "./mcp-protocol";
@@ -35,6 +36,7 @@ export class InternalUrlRouter {
35
36
  this.register(new McpProtocolHandler());
36
37
  this.register(new IssueProtocolHandler());
37
38
  this.register(new PrProtocolHandler());
39
+ this.register(new HistoryProtocolHandler());
38
40
  }
39
41
 
40
42
  /** Process-global router instance. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Types for the internal URL routing system.
3
3
  *
4
- * Internal URLs (`agent://`, `artifact://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
4
+ * Internal URLs (`agent://`, `artifact://`, `history://`, `issue://`, `local://`, `mcp://`, `memory://`, `omp://`, `pr://`, `rule://`, `skill://`, and `vault://`) are resolved by tools like read,
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
package/src/irc/bus.ts ADDED
@@ -0,0 +1,292 @@
1
+ /**
2
+ * IrcBus - Process-global mailbox bus for agent-to-agent messaging.
3
+ *
4
+ * Replaces the old auto-reply model: a `send` never blocks on the recipient
5
+ * generating anything. Delivery resolves the recipient via the global
6
+ * AgentRegistry — parked agents are revived through the
7
+ * AgentLifecycleManager, idle agents are woken with a real turn, and busy
8
+ * agents receive the message as a non-interrupting aside at the next step
9
+ * boundary (see AgentSession.deliverIrcMessage). Replies are real turns by
10
+ * the recipient, observed via `wait`.
11
+ */
12
+
13
+ import { logger, Snowflake } from "@oh-my-pi/pi-utils";
14
+ import { AgentLifecycleManager } from "../registry/agent-lifecycle";
15
+ import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
16
+ import type { CustomMessage } from "../session/messages";
17
+
18
+ export interface IrcMessage {
19
+ id: string;
20
+ /** Sender agent id. */
21
+ from: string;
22
+ /** Recipient agent id (resolved; "all" is expanded by the tool, not stored). */
23
+ to: string;
24
+ body: string;
25
+ ts: number;
26
+ /** Message id being answered. */
27
+ replyTo?: string;
28
+ }
29
+
30
+ export interface IrcDeliveryReceipt {
31
+ to: string;
32
+ outcome: "injected" | "woken" | "revived" | "failed";
33
+ error?: string;
34
+ }
35
+
36
+ interface IrcWaiter {
37
+ from?: string;
38
+ resolve: (msg: IrcMessage) => void;
39
+ cancel: () => void;
40
+ }
41
+
42
+ /** Mailbox cap per agent; oldest messages are dropped beyond it. */
43
+ const MAILBOX_CAP = 100;
44
+
45
+ export class IrcBus {
46
+ static #global: IrcBus | undefined;
47
+
48
+ static global(): IrcBus {
49
+ if (!IrcBus.#global) {
50
+ IrcBus.#global = new IrcBus();
51
+ }
52
+ return IrcBus.#global;
53
+ }
54
+
55
+ /** Reset the global bus. Test-only. */
56
+ static resetGlobalForTests(): void {
57
+ IrcBus.#global = undefined;
58
+ }
59
+
60
+ readonly #registry: AgentRegistry;
61
+ readonly #lifecycle: () => AgentLifecycleManager;
62
+ readonly #mailboxes = new Map<string, IrcMessage[]>();
63
+ readonly #waiters = new Map<string, IrcWaiter[]>();
64
+
65
+ constructor(registry: AgentRegistry = AgentRegistry.global(), lifecycle?: AgentLifecycleManager) {
66
+ this.#registry = registry;
67
+ // Lazy: the lifecycle global self-constructs against the global registry,
68
+ // so only touch it when a parked recipient actually needs reviving.
69
+ this.#lifecycle = () => lifecycle ?? AgentLifecycleManager.global();
70
+ }
71
+
72
+ /**
73
+ * Fire-and-forget delivery. Never blocks on the recipient generating
74
+ * anything: the receipt reports how the message reached the recipient
75
+ * (waiter/aside = "injected", idle wake = "woken", park revival =
76
+ * "revived"), not what they did with it.
77
+ *
78
+ * Mailbox semantics: a successfully delivered message never lingers in
79
+ * the recipient's mailbox — injection/wake puts the full body into their
80
+ * context, so buffering it too would double-deliver via a later
81
+ * `wait`/`inbox` and inflate unread counts. Only a failed live hand-off
82
+ * is buffered for the recipient to drain later.
83
+ */
84
+ async send(msg: Omit<IrcMessage, "id" | "ts">): Promise<IrcDeliveryReceipt> {
85
+ const message: IrcMessage = { ...msg, id: Snowflake.next(), ts: Date.now() };
86
+ const ref = this.#registry.get(message.to);
87
+ if (!ref || ref.status === "aborted") {
88
+ return { to: message.to, outcome: "failed", error: `Unknown or terminated agent "${message.to}".` };
89
+ }
90
+
91
+ let revived = false;
92
+ if (ref.status === "parked") {
93
+ try {
94
+ await this.#lifecycle().ensureLive(message.to);
95
+ revived = true;
96
+ } catch (error) {
97
+ return {
98
+ to: message.to,
99
+ outcome: "failed",
100
+ error: error instanceof Error ? error.message : String(error),
101
+ };
102
+ }
103
+ }
104
+
105
+ // A pending `wait` from the recipient consumes the message directly —
106
+ // it is returned from their irc tool call and never hits the inbox or
107
+ // the session injection path.
108
+ const waiter = this.#takeMatchingWaiter(message.to, message.from);
109
+ if (waiter) {
110
+ waiter.resolve(message);
111
+ this.#relayToMainUi(message);
112
+ return { to: message.to, outcome: revived ? "revived" : "injected" };
113
+ }
114
+
115
+ const session = this.#registry.get(message.to)?.session;
116
+ if (!session) {
117
+ return { to: message.to, outcome: "failed", error: `Agent "${message.to}" has no live session.` };
118
+ }
119
+
120
+ try {
121
+ const delivery = await session.deliverIrcMessage(message);
122
+ this.#relayToMainUi(message);
123
+ return { to: message.to, outcome: revived ? "revived" : delivery };
124
+ } catch (error) {
125
+ // Live hand-off failed (e.g. recipient disposed mid-shutdown): buffer
126
+ // the message so a later `wait`/`inbox` from the recipient can still
127
+ // pick it up. The receipt stays "failed" — the recipient has not
128
+ // seen it.
129
+ this.#enqueue(message);
130
+ return {
131
+ to: message.to,
132
+ outcome: "failed",
133
+ error: error instanceof Error ? error.message : String(error),
134
+ };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Block until a message for `agentId` (optionally from `filter.from`)
140
+ * arrives; consume + return it. Null on timeout (`timeoutMs <= 0` waits
141
+ * forever). Rejects when `signal` aborts. By default, already-buffered
142
+ * mail satisfies the wait before parking a future waiter; callers that
143
+ * need a strictly future reply can disable that drain.
144
+ */
145
+ async wait(
146
+ agentId: string,
147
+ filter: { from?: string },
148
+ timeoutMs: number,
149
+ signal?: AbortSignal,
150
+ options?: { drainPending?: boolean },
151
+ ): Promise<IrcMessage | null> {
152
+ if (signal?.aborted) {
153
+ throw signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted");
154
+ }
155
+
156
+ if (options?.drainPending !== false) {
157
+ // Already-pending mail satisfies the wait without parking a waiter.
158
+ const pending = this.#takeFromMailbox(agentId, filter.from);
159
+ if (pending) return pending;
160
+ }
161
+
162
+ const { promise, resolve, reject } = Promise.withResolvers<IrcMessage | null>();
163
+ let timer: NodeJS.Timeout | undefined;
164
+ let onAbort: (() => void) | undefined;
165
+
166
+ const waiter: IrcWaiter = {
167
+ from: filter.from,
168
+ resolve: msg => {
169
+ cleanup();
170
+ resolve(msg);
171
+ },
172
+ cancel: () => {
173
+ cleanup();
174
+ },
175
+ };
176
+ const cleanup = (): void => {
177
+ this.#removeWaiter(agentId, waiter);
178
+ clearTimeout(timer);
179
+ if (signal && onAbort) signal.removeEventListener("abort", onAbort);
180
+ };
181
+
182
+ if (signal) {
183
+ onAbort = () => {
184
+ cleanup();
185
+ reject(signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted"));
186
+ };
187
+ signal.addEventListener("abort", onAbort, { once: true });
188
+ }
189
+ if (timeoutMs > 0) {
190
+ timer = setTimeout(() => {
191
+ cleanup();
192
+ resolve(null);
193
+ }, timeoutMs);
194
+ timer.unref?.();
195
+ }
196
+
197
+ let waiters = this.#waiters.get(agentId);
198
+ if (!waiters) {
199
+ waiters = [];
200
+ this.#waiters.set(agentId, waiters);
201
+ }
202
+ waiters.push(waiter);
203
+ return promise;
204
+ }
205
+
206
+ /** Drain (or peek) pending messages for `agentId`. */
207
+ inbox(agentId: string, opts?: { peek?: boolean }): IrcMessage[] {
208
+ const mailbox = this.#mailboxes.get(agentId);
209
+ if (!mailbox || mailbox.length === 0) return [];
210
+ if (opts?.peek) return [...mailbox];
211
+ this.#mailboxes.delete(agentId);
212
+ return mailbox;
213
+ }
214
+
215
+ unreadCount(agentId: string): number {
216
+ return this.#mailboxes.get(agentId)?.length ?? 0;
217
+ }
218
+
219
+ #enqueue(message: IrcMessage): void {
220
+ let mailbox = this.#mailboxes.get(message.to);
221
+ if (!mailbox) {
222
+ mailbox = [];
223
+ this.#mailboxes.set(message.to, mailbox);
224
+ }
225
+ mailbox.push(message);
226
+ if (mailbox.length > MAILBOX_CAP) {
227
+ const dropped = mailbox.shift();
228
+ logger.debug("IrcBus: mailbox full, dropped oldest message", {
229
+ agentId: message.to,
230
+ droppedId: dropped?.id,
231
+ droppedFrom: dropped?.from,
232
+ });
233
+ }
234
+ }
235
+
236
+ /** Resolve the OLDEST waiter for `agentId` whose from-filter accepts `from`. */
237
+ #takeMatchingWaiter(agentId: string, from: string): IrcWaiter | undefined {
238
+ const waiters = this.#waiters.get(agentId);
239
+ if (!waiters) return undefined;
240
+ const index = waiters.findIndex(waiter => !waiter.from || waiter.from === from);
241
+ if (index === -1) return undefined;
242
+ const [waiter] = waiters.splice(index, 1);
243
+ if (waiters.length === 0) this.#waiters.delete(agentId);
244
+ return waiter;
245
+ }
246
+
247
+ #removeWaiter(agentId: string, waiter: IrcWaiter): void {
248
+ const waiters = this.#waiters.get(agentId);
249
+ if (!waiters) return;
250
+ const index = waiters.indexOf(waiter);
251
+ if (index !== -1) waiters.splice(index, 1);
252
+ if (waiters.length === 0) this.#waiters.delete(agentId);
253
+ }
254
+
255
+ #takeFromMailbox(agentId: string, from?: string): IrcMessage | undefined {
256
+ const mailbox = this.#mailboxes.get(agentId);
257
+ if (!mailbox) return undefined;
258
+ const index = from ? mailbox.findIndex(msg => msg.from === from) : 0;
259
+ if (index === -1 || mailbox.length === 0) return undefined;
260
+ const [message] = mailbox.splice(index, 1);
261
+ if (mailbox.length === 0) this.#mailboxes.delete(agentId);
262
+ return message;
263
+ }
264
+
265
+ /**
266
+ * Surface agent↔agent traffic as a display-only card on the main session
267
+ * UI. Skipped when the main agent is either endpoint: as recipient its
268
+ * own `deliverIrcMessage` (or `wait` tool result) already shows the
269
+ * message, and as sender the irc send tool call already rendered the
270
+ * outbound body — relaying it again would duplicate it in the transcript.
271
+ */
272
+ #relayToMainUi(message: IrcMessage): void {
273
+ if (message.to === MAIN_AGENT_ID || message.from === MAIN_AGENT_ID) return;
274
+ const mainSession = this.#registry.get(MAIN_AGENT_ID)?.session;
275
+ if (!mainSession) return;
276
+ const record: CustomMessage = {
277
+ role: "custom",
278
+ customType: "irc:relay",
279
+ content: `[IRC \`${message.from}\` → \`${message.to}\`]\n\n${message.body}`,
280
+ display: true,
281
+ details: { from: message.from, to: message.to, body: message.body },
282
+ attribution: "agent",
283
+ timestamp: message.ts,
284
+ };
285
+ try {
286
+ mainSession.emitIrcRelayObservation(record);
287
+ } catch (error) {
288
+ // Display-only forwarding must never affect delivery semantics.
289
+ logger.debug("IrcBus: main UI relay failed", { to: message.to, error: String(error) });
290
+ }
291
+ }
292
+ }
package/src/main.ts CHANGED
@@ -51,7 +51,6 @@ import { ExtensionRunner } from "./extensibility/extensions/runner";
51
51
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
52
52
  import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
53
53
  import type { MCPManager } from "./mcp";
54
- import { WelcomeComponent } from "./modes/components/welcome";
55
54
  import { InteractiveMode } from "./modes/interactive-mode";
56
55
  import type { PrintModeOptions } from "./modes/print-mode";
57
56
  import { CURRENT_SETUP_VERSION } from "./modes/setup-version";
@@ -70,7 +69,7 @@ import { resolveResumableSession, type SessionInfo, SessionManager } from "./ses
70
69
  import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
71
70
  import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
72
71
  import { AUTO_THINKING } from "./thinking";
73
- import { discoverStartupLspServers, type LspStartupServerInfo } from "./tools";
72
+ import type { LspStartupServerInfo } from "./tools";
74
73
  import {
75
74
  getChangelogPath,
76
75
  getNewEntries,
@@ -93,37 +92,12 @@ function maybeShowStartupSplash(options: {
93
92
  resuming: boolean;
94
93
  quiet: boolean;
95
94
  version: string;
96
- setupPending: boolean;
97
- modelName?: string;
98
- providerName?: string;
99
- lspServers?: LspStartupServerInfo[];
100
95
  }): void {
101
96
  if (!options.isInteractive) return;
102
97
  if (options.resuming || options.quiet) return;
103
98
  if ($env.PI_TIMING) return;
104
99
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
105
- // First-run launches go straight into the setup wizard, which paints its own
106
- // splash — keep the minimal two-line notice there.
107
- if (options.setupPending) {
108
- process.stdout.write(`${chalk.dim(`omp ${options.version}`)}\n${chalk.dim("Initializing session…")}\n`);
109
- return;
110
- }
111
- // Render the same welcome box the TUI paints first: recent sessions as a
112
- // loading placeholder (the fixed slot count keeps the box height stable) and
113
- // the logo held on the intro animation's first frame so the in-TUI intro
114
- // continues from the frame shown here. Clearing the screen first puts the
115
- // box at the same origin the TUI's first full paint (clearScrollback) uses,
116
- // so the live welcome replaces this frame in place without shifting.
117
- const welcome = new WelcomeComponent(
118
- options.version,
119
- options.modelName ?? "",
120
- options.providerName ?? "",
121
- null,
122
- options.lspServers ?? [],
123
- );
124
- welcome.holdIntroFirstFrame();
125
- const lines = welcome.render(process.stdout.columns || 80);
126
- process.stdout.write(`\x1b[2J\x1b[H\x1b[3J\n${lines.join("\n")}\n`);
100
+ //process.stdout.write(`${chalk.dim(`omp ${options.version}`)}\n${chalk.dim("Initializing session…")}\n`);
127
101
  }
128
102
 
129
103
  async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
@@ -156,7 +130,7 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
156
130
  "task.isolation.merge",
157
131
  "task.isolation.commits",
158
132
  "task.eager",
159
- "task.simple",
133
+ "task.batch",
160
134
  "task.maxConcurrency",
161
135
  "task.maxRecursionDepth",
162
136
  "task.disabledAgents",
@@ -425,7 +399,7 @@ async function runInteractiveMode(
425
399
  // Every in-process session load also uses `clearTerminalHistory`; cold launch
426
400
  // follows the same clean-cutover path instead of preserving a previous run's
427
401
  // transcript above the fresh one.
428
- mode.renderInitialMessages(undefined, { preserveExistingChat: true, clearTerminalHistory: true });
402
+ mode.renderInitialMessages({ preserveExistingChat: true, clearTerminalHistory: true });
429
403
 
430
404
  for (const notify of notifs) {
431
405
  if (!notify) {
@@ -1239,40 +1213,11 @@ export async function runRootCommand(
1239
1213
  stdinContent: pipedInput,
1240
1214
  });
1241
1215
 
1242
- // Resolve the model the session will most likely start with so the splash
1243
- // box matches the final welcome screen (the raw role selector, e.g.
1244
- // "anthropic/claude-fable-5:high", is wider than the left column and would
1245
- // collapse the box into the single-column layout).
1246
- let splashModel = sessionOptions.model;
1247
- if (!splashModel) {
1248
- const remembered = settingsInstance.getModelRole("default");
1249
- if (remembered) {
1250
- splashModel = resolveModelRoleValue(remembered, modelRegistry.getAll(), {
1251
- settings: settingsInstance,
1252
- matchPreferences: modelMatchPreferences,
1253
- modelRegistry,
1254
- }).model;
1255
- }
1256
- }
1257
- // Mirror createAgentSession's startup LSP discovery (sync and cheap: root
1258
- // markers + binary lookup) so the splash lists the same servers the live
1259
- // welcome screen will show.
1260
- const splashLspServers =
1261
- (sessionOptions.enableLsp ?? true)
1262
- ? discoverStartupLspServers(
1263
- sessionOptions.cwd ?? cwd,
1264
- settingsInstance.get("lsp.lazy") ? "available" : "connecting",
1265
- )
1266
- : [];
1267
1216
  maybeShowStartupSplash({
1268
1217
  isInteractive,
1269
1218
  resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1270
1219
  quiet: settingsInstance.get("startup.quiet"),
1271
1220
  version: VERSION,
1272
- setupPending: deps.forceSetupWizard === true || settingsInstance.get("setupVersion") < CURRENT_SETUP_VERSION,
1273
- modelName: splashModel?.name,
1274
- providerName: splashModel?.provider,
1275
- lspServers: splashLspServers,
1276
1221
  });
1277
1222
 
1278
1223
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
@@ -1322,7 +1267,10 @@ export async function runRootCommand(
1322
1267
  return `${scopedModel.model.id}${thinkingStr}`;
1323
1268
  })
1324
1269
  .join(", ");
1325
- process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
1270
+ // Routed through the TUI (not stdout): the startup capture owns the
1271
+ // terminal in raw mode here, and the TUI's first clearScrollback paint
1272
+ // would wipe a pre-TUI line anyway.
1273
+ notifs.push({ kind: "info", message: `Model scope: ${modelList} (Ctrl+P to cycle)` });
1326
1274
  }
1327
1275
 
1328
1276
  if ($env.PI_TIMING) {
@@ -1174,12 +1174,15 @@ export class MCPManager {
1174
1174
  const shouldRefresh =
1175
1175
  forceRefresh || (credential.expires && Date.now() >= credential.expires - REFRESH_BUFFER_MS);
1176
1176
  if (shouldRefresh && credential.refresh && auth.tokenUrl) {
1177
+ const resource =
1178
+ auth.resource ?? (config.type === "http" || config.type === "sse" ? config.url : undefined);
1177
1179
  try {
1178
1180
  const refreshed = await refreshMCPOAuthToken(
1179
1181
  auth.tokenUrl,
1180
1182
  credential.refresh,
1181
1183
  auth.clientId,
1182
1184
  auth.clientSecret,
1185
+ resource,
1183
1186
  );
1184
1187
  const refreshedCredential = { type: "oauth" as const, ...refreshed };
1185
1188
  await this.#authStorage.set(credentialId, refreshedCredential);