@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.
- package/CHANGELOG.md +90 -4
- package/dist/cli.js +869 -825
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +66 -34
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -2
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +3 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- package/dist/types/session/agent-session.d.ts +35 -30
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/task/executor.d.ts +11 -2
- package/dist/types/task/index.d.ts +11 -4
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +55 -51
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +1 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +6 -5
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli.ts +20 -6
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/config/keybindings.ts +6 -1
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +68 -41
- package/src/config/settings.ts +7 -0
- package/src/edit/renderer.ts +96 -46
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/js/shared/prelude.txt +1 -1
- package/src/eval/py/prelude.py +5 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +44 -14
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +8 -60
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/event-controller.ts +93 -4
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +18 -2
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +25 -17
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +17 -15
- package/src/modes/theme/theme.ts +24 -5
- package/src/modes/types.ts +3 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +43 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +29 -9
- package/src/session/agent-session.ts +268 -241
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +60 -0
- package/src/task/executor.ts +855 -466
- package/src/task/index.ts +723 -794
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +142 -66
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +73 -66
- package/src/tools/ask.ts +4 -2
- package/src/tools/bash.ts +15 -5
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/index.ts +4 -12
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
package/src/tools/irc.ts
CHANGED
|
@@ -1,62 +1,94 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* IRC tool — agent-to-agent messaging.
|
|
2
|
+
* IRC tool — agent-to-agent messaging over the process-global IrcBus.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* history are used to compute a reply without persisting it through the
|
|
11
|
-
* normal stream path. After the reply is generated, both the incoming
|
|
12
|
-
* message and the auto-reply are queued for injection into the recipient's
|
|
13
|
-
* persisted history (deferred until the recipient is idle), so the model
|
|
14
|
-
* sees the exchange on its next turn.
|
|
15
|
-
*
|
|
16
|
-
* This avoids the deadlock that arises when the recipient is blocked on a
|
|
17
|
-
* long-running tool call: the side-channel call does not depend on the
|
|
18
|
-
* recipient's main agent loop being free.
|
|
4
|
+
* `send` is fire-and-forget: the bus routes the message to the recipient
|
|
5
|
+
* (waking idle agents with a real turn, reviving parked ones via the
|
|
6
|
+
* lifecycle manager, injecting a non-interrupting aside into busy ones) and
|
|
7
|
+
* returns delivery receipts immediately. Replies are real turns by the
|
|
8
|
+
* recipient, observed with `wait` (or the `await: true` send sugar). `inbox`
|
|
9
|
+
* drains pending messages; `list` shows every addressable peer.
|
|
19
10
|
*/
|
|
20
11
|
|
|
21
12
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
22
|
-
import {
|
|
13
|
+
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
14
|
+
import { formatAge, formatDuration, prompt } from "@oh-my-pi/pi-utils";
|
|
23
15
|
import * as z from "zod/v4";
|
|
16
|
+
import type { Settings } from "../config/settings";
|
|
17
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
18
|
+
import { IrcBus, type IrcDeliveryReceipt, type IrcMessage } from "../irc/bus";
|
|
19
|
+
import type { Theme } from "../modes/theme/theme";
|
|
24
20
|
import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
|
|
25
|
-
import type {
|
|
21
|
+
import type { AgentRegistry } from "../registry/agent-registry";
|
|
22
|
+
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
26
23
|
import type { ToolSession } from ".";
|
|
24
|
+
import {
|
|
25
|
+
createCachedComponent,
|
|
26
|
+
formatBadge,
|
|
27
|
+
formatErrorDetail,
|
|
28
|
+
getPreviewLines,
|
|
29
|
+
PREVIEW_LIMITS,
|
|
30
|
+
replaceTabs,
|
|
31
|
+
type ToolUIColor,
|
|
32
|
+
} from "./render-utils";
|
|
27
33
|
|
|
28
34
|
const DEFAULT_IRC_TIMEOUT_MS = 120_000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* IRC availability: there must be someone to chat with. True for every
|
|
38
|
+
* subagent (it always has a parent, and possibly siblings) and for any
|
|
39
|
+
* session that can still spawn subagents through the task tool. Only a
|
|
40
|
+
* top-level session with task spawning unavailable has no peers — no irc.
|
|
41
|
+
*/
|
|
42
|
+
export function isIrcEnabled(settings: Settings, taskDepth: number): boolean {
|
|
43
|
+
if (taskDepth > 0) return true;
|
|
44
|
+
const maxDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
45
|
+
return maxDepth < 0 || taskDepth < maxDepth;
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
const ircSchema = z.object({
|
|
30
|
-
op: z.enum(["send", "list"]).describe("irc operation"),
|
|
31
|
-
to: z.string().optional().describe('recipient agent id or "all"'),
|
|
32
|
-
message: z.string().optional().describe("message body"),
|
|
33
|
-
|
|
49
|
+
op: z.enum(["send", "wait", "inbox", "list"]).describe("irc operation"),
|
|
50
|
+
to: z.string().optional().describe('send: recipient agent id or "all"'),
|
|
51
|
+
message: z.string().optional().describe("send: message body"),
|
|
52
|
+
replyTo: z.string().optional().describe("send: message id being answered"),
|
|
53
|
+
await: z.boolean().optional().describe('send: wait for the recipient\'s reply (invalid with to:"all")'),
|
|
54
|
+
from: z.string().optional().describe("wait: only accept a message from this agent id"),
|
|
55
|
+
timeoutMs: z.number().optional().describe("wait: timeout in milliseconds (0 waits indefinitely)"),
|
|
56
|
+
peek: z.boolean().optional().describe("inbox: list messages without consuming them"),
|
|
34
57
|
});
|
|
35
58
|
|
|
36
59
|
type IrcParams = z.infer<typeof ircSchema>;
|
|
37
60
|
|
|
38
|
-
interface
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
interface IrcPeerInfo {
|
|
62
|
+
id: string;
|
|
63
|
+
displayName: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
status: string;
|
|
66
|
+
parentId?: string;
|
|
67
|
+
unread: number;
|
|
68
|
+
lastActivity: number;
|
|
41
69
|
}
|
|
42
70
|
|
|
43
71
|
export interface IrcDetails {
|
|
44
|
-
op: "send" | "list";
|
|
72
|
+
op: "send" | "wait" | "inbox" | "list";
|
|
45
73
|
from?: string;
|
|
46
74
|
to?: string;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
peers?:
|
|
52
|
-
|
|
75
|
+
receipts?: IrcDeliveryReceipt[];
|
|
76
|
+
/** Message consumed by `wait` / `send await:true`; null when the wait timed out. */
|
|
77
|
+
waited?: IrcMessage | null;
|
|
78
|
+
inbox?: IrcMessage[];
|
|
79
|
+
peers?: IrcPeerInfo[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatIncoming(msg: IrcMessage): string {
|
|
83
|
+
const replyTag = msg.replyTo ? ` (reply to ${msg.replyTo})` : "";
|
|
84
|
+
return `[${msg.id}] ${msg.from}${replyTag}: ${msg.body}`;
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
56
88
|
readonly name = "irc";
|
|
57
89
|
readonly approval = "read" as const;
|
|
58
90
|
readonly label = "IRC";
|
|
59
|
-
readonly summary = "Send and receive messages between agents
|
|
91
|
+
readonly summary = "Send and receive messages between agents";
|
|
60
92
|
readonly description: string;
|
|
61
93
|
readonly parameters = ircSchema;
|
|
62
94
|
readonly strict = true;
|
|
@@ -66,7 +98,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
66
98
|
}
|
|
67
99
|
|
|
68
100
|
static createIf(session: ToolSession): IrcTool | null {
|
|
69
|
-
if (!session.settings.
|
|
101
|
+
if (!isIrcEnabled(session.settings, session.taskDepth ?? 0)) return null;
|
|
70
102
|
if (!session.agentRegistry || !session.getAgentId) return null;
|
|
71
103
|
return new IrcTool(session);
|
|
72
104
|
}
|
|
@@ -87,41 +119,55 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
87
119
|
return errorResult("IRC is unavailable: caller has no agent id.", { op: params.op });
|
|
88
120
|
}
|
|
89
121
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
switch (params.op) {
|
|
123
|
+
case "list":
|
|
124
|
+
return this.#executeList(registry, senderId);
|
|
125
|
+
case "send":
|
|
126
|
+
return this.#executeSend(registry, senderId, params, signal);
|
|
127
|
+
case "wait":
|
|
128
|
+
return this.#executeWait(senderId, params, signal);
|
|
129
|
+
case "inbox":
|
|
130
|
+
return this.#executeInbox(senderId, params);
|
|
131
|
+
default:
|
|
132
|
+
return errorResult("Unknown irc op.", { op: params.op });
|
|
95
133
|
}
|
|
96
|
-
return errorResult("Unknown irc op.", { op: params.op as "send" | "list" });
|
|
97
134
|
}
|
|
98
135
|
|
|
99
136
|
#executeList(registry: AgentRegistry, senderId: string): AgentToolResult<IrcDetails> {
|
|
100
|
-
const
|
|
137
|
+
const bus = IrcBus.global();
|
|
138
|
+
const peers = registry
|
|
139
|
+
.list()
|
|
140
|
+
.filter(ref => ref.id !== senderId && ref.status !== "aborted")
|
|
141
|
+
.map(ref => ({
|
|
142
|
+
id: ref.id,
|
|
143
|
+
displayName: ref.displayName,
|
|
144
|
+
kind: ref.kind,
|
|
145
|
+
status: ref.status,
|
|
146
|
+
parentId: ref.parentId,
|
|
147
|
+
unread: bus.unreadCount(ref.id),
|
|
148
|
+
lastActivity: ref.lastActivity,
|
|
149
|
+
}));
|
|
101
150
|
const lines: string[] = [];
|
|
102
151
|
if (peers.length === 0) {
|
|
103
|
-
lines.push("No other
|
|
152
|
+
lines.push("No other agents.");
|
|
104
153
|
} else {
|
|
105
154
|
lines.push(`${peers.length} peer(s):`);
|
|
106
155
|
for (const peer of peers) {
|
|
107
|
-
|
|
156
|
+
const extras = [
|
|
157
|
+
peer.unread > 0 ? `unread ${peer.unread}` : undefined,
|
|
158
|
+
peer.parentId ? `parent ${peer.parentId}` : undefined,
|
|
159
|
+
`active ${formatDuration(Date.now() - peer.lastActivity)} ago`,
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
lines.push(`- ${peer.id} [${peer.displayName} · ${peer.kind} · ${peer.status}] — ${extras.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
if (peers.some(peer => peer.status === "parked")) {
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push("Parked agents are revived automatically when you message them.");
|
|
108
166
|
}
|
|
109
167
|
}
|
|
110
|
-
const channels = ["all", ...peers.map(p => p.id)];
|
|
111
168
|
return {
|
|
112
169
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
113
|
-
details: {
|
|
114
|
-
op: "list",
|
|
115
|
-
from: senderId,
|
|
116
|
-
peers: peers.map(p => ({
|
|
117
|
-
id: p.id,
|
|
118
|
-
displayName: p.displayName,
|
|
119
|
-
kind: p.kind,
|
|
120
|
-
status: p.status,
|
|
121
|
-
parentId: p.parentId,
|
|
122
|
-
})),
|
|
123
|
-
channels,
|
|
124
|
-
},
|
|
170
|
+
details: { op: "list", from: senderId, peers },
|
|
125
171
|
};
|
|
126
172
|
}
|
|
127
173
|
|
|
@@ -139,105 +185,153 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
139
185
|
if (!message) {
|
|
140
186
|
return errorResult('`message` is required for op="send".', { op: "send", from: senderId });
|
|
141
187
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const notFound: string[] = [];
|
|
188
|
+
if (to === senderId) {
|
|
189
|
+
return errorResult("Cannot send an IRC message to yourself.", { op: "send", from: senderId, to });
|
|
190
|
+
}
|
|
146
191
|
const isBroadcast = to === "all";
|
|
147
|
-
if (isBroadcast) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
192
|
+
if (isBroadcast && params.await) {
|
|
193
|
+
return errorResult('`await` is invalid with to:"all" — broadcasts have no single replier.', {
|
|
194
|
+
op: "send",
|
|
195
|
+
from: senderId,
|
|
196
|
+
to,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const bus = IrcBus.global();
|
|
201
|
+
let waited: IrcMessage | null | undefined;
|
|
202
|
+
const timeoutMs = params.await ? this.#resolveTimeoutMs(params) : undefined;
|
|
203
|
+
const awaitAbort = params.await ? new AbortController() : undefined;
|
|
204
|
+
const awaitCancelled = new Error("IRC await cancelled");
|
|
205
|
+
let removeAwaitAbortListener: (() => void) | undefined;
|
|
206
|
+
const waiting = params.await
|
|
207
|
+
? bus
|
|
208
|
+
.wait(senderId, { from: to }, timeoutMs ?? DEFAULT_IRC_TIMEOUT_MS, awaitAbort?.signal, {
|
|
209
|
+
drainPending: false,
|
|
210
|
+
})
|
|
211
|
+
.then(
|
|
212
|
+
message => ({ message, error: null as Error | null }),
|
|
213
|
+
error => ({
|
|
214
|
+
message: null,
|
|
215
|
+
error: error === awaitCancelled ? null : error instanceof Error ? error : new Error(String(error)),
|
|
216
|
+
}),
|
|
217
|
+
)
|
|
218
|
+
: undefined;
|
|
219
|
+
if (params.await && signal && awaitAbort) {
|
|
220
|
+
if (signal.aborted) {
|
|
221
|
+
awaitAbort.abort(signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted"));
|
|
157
222
|
} else {
|
|
158
|
-
|
|
223
|
+
const onAbort = (): void => {
|
|
224
|
+
awaitAbort.abort(signal.reason instanceof Error ? signal.reason : new Error("IRC wait aborted"));
|
|
225
|
+
};
|
|
226
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
227
|
+
removeAwaitAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
159
228
|
}
|
|
160
229
|
}
|
|
161
230
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
231
|
+
try {
|
|
232
|
+
// Broadcasts fan out to live peers only (running | idle); reviving every
|
|
233
|
+
// parked agent on a broadcast would be a stampede. Direct sends go
|
|
234
|
+
// through the bus unfiltered so parked recipients are revived.
|
|
235
|
+
const targets = isBroadcast ? registry.listVisibleTo(senderId).map(ref => ref.id) : [to];
|
|
236
|
+
const receipts = await Promise.all(
|
|
237
|
+
targets.map(target => bus.send({ from: senderId, to: target, body: message, replyTo: params.replyTo })),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const lines: string[] = [];
|
|
241
|
+
const delivered = receipts.filter(receipt => receipt.outcome !== "failed");
|
|
242
|
+
if (targets.length === 0) {
|
|
243
|
+
lines.push("No live peers to broadcast to.");
|
|
244
|
+
} else if (delivered.length === 0) {
|
|
245
|
+
lines.push("No recipients received the message.");
|
|
246
|
+
} else {
|
|
247
|
+
lines.push(`Delivered to ${delivered.length} peer(s):`);
|
|
178
248
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
targetSession.respondAsBackground({
|
|
185
|
-
from: senderId,
|
|
186
|
-
message,
|
|
187
|
-
awaitReply,
|
|
188
|
-
signal: timeoutSignal,
|
|
189
|
-
}),
|
|
190
|
-
target.id,
|
|
249
|
+
for (const receipt of receipts) {
|
|
250
|
+
lines.push(
|
|
251
|
+
receipt.outcome === "failed"
|
|
252
|
+
? `- ${receipt.to}: failed — ${receipt.error ?? "unknown error"}`
|
|
253
|
+
: `- ${receipt.to}: ${receipt.outcome}`,
|
|
191
254
|
);
|
|
192
|
-
delivered.push(target.id);
|
|
193
|
-
if (awaitReply && result.replyText) {
|
|
194
|
-
replies.push({ from: target.id, text: result.replyText });
|
|
195
|
-
}
|
|
196
|
-
} catch (err) {
|
|
197
|
-
failed.push({ id: target.id, error: err instanceof Error ? err.message : String(err) });
|
|
198
255
|
}
|
|
199
|
-
});
|
|
200
|
-
await Promise.all(dispatches);
|
|
201
256
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
257
|
+
if (params.await && waiting && timeoutMs !== undefined) {
|
|
258
|
+
lines.push("");
|
|
259
|
+
if (delivered.length > 0) {
|
|
260
|
+
const reply = await waiting;
|
|
261
|
+
if (reply.error) throw reply.error;
|
|
262
|
+
waited = reply.message;
|
|
263
|
+
if (waited) {
|
|
264
|
+
lines.push(`Reply from ${waited.from}:`);
|
|
265
|
+
lines.push(waited.body);
|
|
266
|
+
} else {
|
|
267
|
+
lines.push(
|
|
268
|
+
`No reply from ${to} within ${formatDuration(timeoutMs)}. ` +
|
|
269
|
+
"They may answer later — check `inbox` or `wait` again.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
awaitAbort?.abort(awaitCancelled);
|
|
274
|
+
const reply = await waiting;
|
|
275
|
+
if (reply.error) throw reply.error;
|
|
276
|
+
}
|
|
221
277
|
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
281
|
+
details: {
|
|
282
|
+
op: "send",
|
|
283
|
+
from: senderId,
|
|
284
|
+
to,
|
|
285
|
+
receipts,
|
|
286
|
+
...(waited !== undefined ? { waited } : {}),
|
|
287
|
+
},
|
|
288
|
+
isError: delivered.length === 0 && targets.length > 0,
|
|
289
|
+
};
|
|
290
|
+
} finally {
|
|
291
|
+
awaitAbort?.abort(awaitCancelled);
|
|
292
|
+
removeAwaitAbortListener?.();
|
|
222
293
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async #executeWait(senderId: string, params: IrcParams, signal?: AbortSignal): Promise<AgentToolResult<IrcDetails>> {
|
|
297
|
+
const from = params.from?.trim() || undefined;
|
|
298
|
+
const timeoutMs = this.#resolveTimeoutMs(params);
|
|
299
|
+
const waited = await IrcBus.global().wait(senderId, { from }, timeoutMs, signal);
|
|
300
|
+
if (!waited) {
|
|
301
|
+
const filterNote = from ? ` from ${from}` : "";
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: "text", text: `No message${filterNote} within ${formatDuration(timeoutMs)}.` }],
|
|
304
|
+
details: { op: "wait", from: senderId, waited: null },
|
|
305
|
+
};
|
|
226
306
|
}
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: "text", text: formatIncoming(waited) }],
|
|
309
|
+
details: { op: "wait", from: senderId, waited },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
227
312
|
|
|
313
|
+
#executeInbox(senderId: string, params: IrcParams): AgentToolResult<IrcDetails> {
|
|
314
|
+
const messages = IrcBus.global().inbox(senderId, { peek: params.peek });
|
|
315
|
+
if (messages.length === 0) {
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: "text", text: "Inbox empty." }],
|
|
318
|
+
details: { op: "inbox", from: senderId, inbox: [] },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const header = params.peek ? `${messages.length} unread message(s):` : `${messages.length} message(s):`;
|
|
322
|
+
const lines = [header, ...messages.map(msg => `- ${formatIncoming(msg)}`)];
|
|
228
323
|
return {
|
|
229
324
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
230
|
-
details: {
|
|
231
|
-
op: "send",
|
|
232
|
-
from: senderId,
|
|
233
|
-
to,
|
|
234
|
-
delivered,
|
|
235
|
-
...(replies.length > 0 ? { replies } : {}),
|
|
236
|
-
...(failed.length > 0 ? { failed } : {}),
|
|
237
|
-
...(notFound.length > 0 ? { notFound } : {}),
|
|
238
|
-
},
|
|
325
|
+
details: { op: "inbox", from: senderId, inbox: messages },
|
|
239
326
|
};
|
|
240
327
|
}
|
|
328
|
+
|
|
329
|
+
#resolveTimeoutMs(params: IrcParams): number {
|
|
330
|
+
if (params.timeoutMs !== undefined) {
|
|
331
|
+
return normalizeIrcTimeoutMs(params.timeoutMs);
|
|
332
|
+
}
|
|
333
|
+
return normalizeIrcTimeoutMs(this.session.settings.get("irc.timeoutMs"));
|
|
334
|
+
}
|
|
241
335
|
}
|
|
242
336
|
|
|
243
337
|
function errorResult(text: string, details: IrcDetails): AgentToolResult<IrcDetails> {
|
|
@@ -256,43 +350,374 @@ function normalizeIrcTimeoutMs(value: number): number {
|
|
|
256
350
|
return Math.max(1, Math.trunc(value));
|
|
257
351
|
}
|
|
258
352
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
353
|
+
// =============================================================================
|
|
354
|
+
// TUI Renderer
|
|
355
|
+
// =============================================================================
|
|
356
|
+
|
|
357
|
+
type IrcRenderArgs = Partial<IrcParams>;
|
|
358
|
+
|
|
359
|
+
const BODY_LINES_COLLAPSED = 2;
|
|
360
|
+
const BODY_LINES_EXPANDED = 12;
|
|
361
|
+
const BODY_LINE_WIDTH = 100;
|
|
362
|
+
|
|
363
|
+
const PEER_STATUS_ORDER: Record<string, number> = { running: 0, idle: 1, parked: 2 };
|
|
364
|
+
|
|
365
|
+
function ircGlyph(theme: Theme): string {
|
|
366
|
+
return theme.styledSymbol("tool.irc", "accent");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function outcomeColor(outcome: IrcDeliveryReceipt["outcome"]): ToolUIColor {
|
|
370
|
+
switch (outcome) {
|
|
371
|
+
case "woken":
|
|
372
|
+
return "success";
|
|
373
|
+
case "revived":
|
|
374
|
+
return "warning";
|
|
375
|
+
case "injected":
|
|
376
|
+
return "accent";
|
|
377
|
+
case "failed":
|
|
378
|
+
return "error";
|
|
267
379
|
}
|
|
380
|
+
}
|
|
268
381
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
382
|
+
/** Glyph + status word, matching the agent-hub status conventions. */
|
|
383
|
+
function peerStatusBadge(status: string, theme: Theme): string {
|
|
384
|
+
switch (status) {
|
|
385
|
+
case "running":
|
|
386
|
+
return theme.fg("accent", `${theme.status.running} running`);
|
|
387
|
+
case "idle":
|
|
388
|
+
return theme.fg("success", `${theme.status.enabled} idle`);
|
|
389
|
+
case "parked":
|
|
390
|
+
return theme.fg("muted", `${theme.status.shadowed} parked`);
|
|
391
|
+
default:
|
|
392
|
+
return theme.fg("error", `${theme.status.aborted} ${status}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
273
395
|
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
396
|
+
function messageAge(ts: number | undefined): string {
|
|
397
|
+
if (!ts) return "";
|
|
398
|
+
return formatAge(Math.max(1, Math.round((Date.now() - ts) / 1000)));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function textContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
402
|
+
return result.content.find(part => part.type === "text")?.text?.trim() ?? "";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Quote-bordered message body preview. `tone` separates outbound text (dim)
|
|
407
|
+
* from received text (toolOutput); a trailing dim counter marks elided lines.
|
|
408
|
+
*/
|
|
409
|
+
function bodyLines(
|
|
410
|
+
body: string,
|
|
411
|
+
expanded: boolean,
|
|
412
|
+
theme: Theme,
|
|
413
|
+
options: { indent?: string; tone?: "dim" | "toolOutput"; collapsedLines?: number } = {},
|
|
414
|
+
): string[] {
|
|
415
|
+
const indent = options.indent ?? "";
|
|
416
|
+
const tone = options.tone ?? "toolOutput";
|
|
417
|
+
const max = expanded ? BODY_LINES_EXPANDED : (options.collapsedLines ?? BODY_LINES_COLLAPSED);
|
|
418
|
+
const total = body.split("\n").filter(line => line.trim()).length;
|
|
419
|
+
const quote = theme.fg("dim", theme.md.quoteBorder);
|
|
420
|
+
const lines = getPreviewLines(body, max, BODY_LINE_WIDTH, Ellipsis.Unicode).map(
|
|
421
|
+
line => `${indent}${quote} ${theme.fg(tone, replaceTabs(line))}`,
|
|
422
|
+
);
|
|
423
|
+
const hidden = total - Math.min(total, max);
|
|
424
|
+
if (hidden > 0) {
|
|
425
|
+
lines.push(`${indent}${quote} ${theme.fg("dim", `… +${hidden} more ${hidden === 1 ? "line" : "lines"}`)}`);
|
|
426
|
+
}
|
|
427
|
+
return lines;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Header title carrying the op direction: `IRC ➤ peer` out, `IRC ⟵ peer` in. */
|
|
431
|
+
function callTitle(args: IrcRenderArgs | undefined, theme: Theme): string {
|
|
432
|
+
switch (args?.op) {
|
|
433
|
+
case "send":
|
|
434
|
+
return `IRC ${theme.nav.selected} ${args.to?.trim() || "…"}`;
|
|
435
|
+
case "wait":
|
|
436
|
+
return `IRC ${theme.nav.back} ${args.from?.trim() || "anyone"}`;
|
|
437
|
+
case "inbox":
|
|
438
|
+
return "IRC inbox";
|
|
439
|
+
case "list":
|
|
440
|
+
return "IRC peers";
|
|
441
|
+
default:
|
|
442
|
+
return "IRC";
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function callMeta(args: IrcRenderArgs | undefined): string[] {
|
|
447
|
+
const meta: string[] = [];
|
|
448
|
+
if (args?.op === "send") {
|
|
449
|
+
if (args.to === "all") meta.push("broadcast");
|
|
450
|
+
if (args.await) meta.push("await reply");
|
|
451
|
+
if (args.replyTo) meta.push("reply");
|
|
452
|
+
}
|
|
453
|
+
if (args?.op === "wait" && args.timeoutMs) meta.push(`timeout ${formatDuration(args.timeoutMs)}`);
|
|
454
|
+
if (args?.op === "inbox" && args.peek) meta.push("peek");
|
|
455
|
+
return meta;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Display-only transcript card for live IRC traffic: `irc:incoming` DMs
|
|
460
|
+
* delivered to this session and `irc:relay` observations of agent↔agent
|
|
461
|
+
* traffic. Shares the tool renderer's glyph + quote-border conventions so
|
|
462
|
+
* cards and `irc` tool output look identical in the transcript.
|
|
463
|
+
*/
|
|
464
|
+
export function createIrcMessageCard(
|
|
465
|
+
card: {
|
|
466
|
+
kind: "incoming" | "relay";
|
|
467
|
+
from?: string;
|
|
468
|
+
to?: string;
|
|
469
|
+
body?: string;
|
|
470
|
+
replyTo?: string;
|
|
471
|
+
timestamp?: number;
|
|
472
|
+
},
|
|
473
|
+
getExpanded: () => boolean,
|
|
474
|
+
uiTheme: Theme,
|
|
475
|
+
): Component {
|
|
476
|
+
const from = card.from?.trim() || "?";
|
|
477
|
+
const title =
|
|
478
|
+
card.kind === "incoming"
|
|
479
|
+
? `IRC ${uiTheme.nav.back} ${from}`
|
|
480
|
+
: `IRC ${from} ${uiTheme.nav.selected} ${card.to?.trim() || "?"}`;
|
|
481
|
+
const body = card.body ?? "";
|
|
482
|
+
const meta: string[] = [];
|
|
483
|
+
if (card.replyTo) meta.push("reply");
|
|
484
|
+
const age = messageAge(card.timestamp);
|
|
485
|
+
if (age) meta.push(age);
|
|
486
|
+
return createCachedComponent(
|
|
487
|
+
getExpanded,
|
|
488
|
+
(width, expanded) => {
|
|
489
|
+
const lines = [renderStatusLine({ iconOverride: ircGlyph(uiTheme), title, meta }, uiTheme)];
|
|
490
|
+
if (body.trim()) {
|
|
491
|
+
lines.push(...bodyLines(body, expanded, uiTheme, { indent: " ", collapsedLines: 3 }));
|
|
492
|
+
}
|
|
493
|
+
return lines.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
|
|
494
|
+
},
|
|
495
|
+
{ paddingX: 1 },
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function renderSendResult(
|
|
500
|
+
result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
|
|
501
|
+
details: Partial<IrcDetails>,
|
|
502
|
+
args: IrcRenderArgs | undefined,
|
|
503
|
+
expanded: boolean,
|
|
504
|
+
theme: Theme,
|
|
505
|
+
): string[] {
|
|
506
|
+
const receipts = details.receipts ?? [];
|
|
507
|
+
const to = details.to ?? args?.to?.trim() ?? "?";
|
|
508
|
+
const title = `IRC ${theme.nav.selected} ${to}`;
|
|
509
|
+
|
|
510
|
+
// Pre-delivery failures (validation) and empty broadcasts carry no receipts.
|
|
511
|
+
if (receipts.length === 0) {
|
|
512
|
+
const text = textContent(result) || (result.isError ? "Send failed." : "Nothing to deliver.");
|
|
513
|
+
return [
|
|
514
|
+
renderStatusLine({ icon: result.isError ? "error" : "warning", title }, theme),
|
|
515
|
+
result.isError ? formatErrorDetail(text, theme) : ` ${theme.fg("muted", replaceTabs(text))}`,
|
|
516
|
+
];
|
|
284
517
|
}
|
|
285
518
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
519
|
+
const delivered = receipts.filter(receipt => receipt.outcome !== "failed");
|
|
520
|
+
const failedCount = receipts.length - delivered.length;
|
|
521
|
+
const waited = details.waited;
|
|
522
|
+
const timedOut = waited === null;
|
|
523
|
+
|
|
524
|
+
const meta: string[] = [];
|
|
525
|
+
if (to === "all") meta.push("broadcast");
|
|
526
|
+
if (receipts.length === 1) {
|
|
527
|
+
const receipt = receipts[0]!;
|
|
528
|
+
meta.push(theme.fg(outcomeColor(receipt.outcome), receipt.outcome));
|
|
529
|
+
} else {
|
|
530
|
+
if (delivered.length > 0) meta.push(theme.fg("success", `${delivered.length} delivered`));
|
|
531
|
+
if (failedCount > 0) meta.push(theme.fg("error", `${failedCount} failed`));
|
|
532
|
+
}
|
|
533
|
+
if (timedOut) meta.push(theme.fg("warning", "no reply"));
|
|
534
|
+
|
|
535
|
+
const icon = result.isError
|
|
536
|
+
? { icon: "error" as const }
|
|
537
|
+
: timedOut
|
|
538
|
+
? { icon: "warning" as const }
|
|
539
|
+
: { iconOverride: ircGlyph(theme) };
|
|
540
|
+
const lines = [renderStatusLine({ ...icon, title, meta }, theme)];
|
|
541
|
+
|
|
542
|
+
const sent = args?.message?.trim();
|
|
543
|
+
if (sent) lines.push(...bodyLines(sent, expanded, theme, { indent: " ", tone: "dim" }));
|
|
544
|
+
|
|
545
|
+
if (receipts.length > 1 || failedCount > 0) {
|
|
546
|
+
lines.push(
|
|
547
|
+
...renderTreeList<IrcDeliveryReceipt>(
|
|
548
|
+
{
|
|
549
|
+
items: receipts,
|
|
550
|
+
expanded,
|
|
551
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
552
|
+
itemType: "recipient",
|
|
553
|
+
renderItem: receipt => {
|
|
554
|
+
const badge = formatBadge(receipt.outcome, outcomeColor(receipt.outcome), theme);
|
|
555
|
+
const error =
|
|
556
|
+
receipt.outcome === "failed" && receipt.error
|
|
557
|
+
? ` ${theme.fg("error", `${theme.format.dash} ${receipt.error}`)}`
|
|
558
|
+
: "";
|
|
559
|
+
return `${theme.fg("toolOutput", receipt.to)} ${badge}${error}`;
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
theme,
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (waited) {
|
|
568
|
+
const age = messageAge(waited.ts);
|
|
569
|
+
lines.push(
|
|
570
|
+
` ${theme.fg("dim", theme.nav.back)} ${theme.fg("accent", waited.from)}${age ? ` ${theme.fg("dim", age)}` : ""}`,
|
|
571
|
+
);
|
|
572
|
+
lines.push(...bodyLines(waited.body, expanded, theme, { indent: " " }));
|
|
573
|
+
} else if (timedOut) {
|
|
574
|
+
lines.push(` ${theme.fg("warning", "No reply yet — they may answer later; check inbox or wait again.")}`);
|
|
575
|
+
}
|
|
576
|
+
return lines;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function renderWaitResult(
|
|
580
|
+
result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
|
|
581
|
+
details: Partial<IrcDetails>,
|
|
582
|
+
args: IrcRenderArgs | undefined,
|
|
583
|
+
expanded: boolean,
|
|
584
|
+
theme: Theme,
|
|
585
|
+
): string[] {
|
|
586
|
+
const waited = details.waited;
|
|
587
|
+
if (!waited) {
|
|
588
|
+
const text = textContent(result) || "No message arrived.";
|
|
589
|
+
return [
|
|
590
|
+
renderStatusLine(
|
|
591
|
+
{ icon: "warning", title: `IRC ${theme.nav.back} ${args?.from?.trim() || "anyone"}`, meta: ["timed out"] },
|
|
592
|
+
theme,
|
|
593
|
+
),
|
|
594
|
+
` ${theme.fg("muted", replaceTabs(text))}`,
|
|
595
|
+
];
|
|
596
|
+
}
|
|
597
|
+
const meta = [messageAge(waited.ts)];
|
|
598
|
+
if (waited.replyTo) meta.push("reply");
|
|
599
|
+
return [
|
|
600
|
+
renderStatusLine({ iconOverride: ircGlyph(theme), title: `IRC ${theme.nav.back} ${waited.from}`, meta }, theme),
|
|
601
|
+
...bodyLines(waited.body, expanded, theme, { indent: " " }),
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function renderInboxResult(
|
|
606
|
+
details: Partial<IrcDetails>,
|
|
607
|
+
args: IrcRenderArgs | undefined,
|
|
608
|
+
expanded: boolean,
|
|
609
|
+
theme: Theme,
|
|
610
|
+
): string[] {
|
|
611
|
+
const messages = details.inbox ?? [];
|
|
612
|
+
if (messages.length === 0) {
|
|
613
|
+
return [renderStatusLine({ iconOverride: ircGlyph(theme), title: "IRC inbox", meta: ["empty"] }, theme)];
|
|
297
614
|
}
|
|
615
|
+
const meta = [`${messages.length} ${messages.length === 1 ? "message" : "messages"}`];
|
|
616
|
+
if (args?.peek) meta.push("peek");
|
|
617
|
+
const header = renderStatusLine({ iconOverride: ircGlyph(theme), title: "IRC inbox", meta }, theme);
|
|
618
|
+
const items = renderTreeList<IrcMessage>(
|
|
619
|
+
{
|
|
620
|
+
items: messages,
|
|
621
|
+
expanded,
|
|
622
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
623
|
+
itemType: "message",
|
|
624
|
+
renderItem: msg => {
|
|
625
|
+
const age = messageAge(msg.ts);
|
|
626
|
+
const replyBadge = msg.replyTo ? ` ${formatBadge("reply", "muted", theme)}` : "";
|
|
627
|
+
const head = `${theme.fg("accent", msg.from)}${age ? ` ${theme.fg("dim", age)}` : ""}${replyBadge}`;
|
|
628
|
+
return [head, ...bodyLines(msg.body, expanded, theme, { collapsedLines: 1 })];
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
theme,
|
|
632
|
+
);
|
|
633
|
+
return [header, ...items];
|
|
298
634
|
}
|
|
635
|
+
|
|
636
|
+
function renderListResult(details: Partial<IrcDetails>, expanded: boolean, theme: Theme): string[] {
|
|
637
|
+
const peers = [...(details.peers ?? [])].sort(
|
|
638
|
+
(a, b) =>
|
|
639
|
+
(PEER_STATUS_ORDER[a.status] ?? 9) - (PEER_STATUS_ORDER[b.status] ?? 9) || b.lastActivity - a.lastActivity,
|
|
640
|
+
);
|
|
641
|
+
if (peers.length === 0) {
|
|
642
|
+
return [renderStatusLine({ icon: "info", title: "IRC peers", meta: ["no other agents"] }, theme)];
|
|
643
|
+
}
|
|
644
|
+
const counts = new Map<string, number>();
|
|
645
|
+
for (const peer of peers) counts.set(peer.status, (counts.get(peer.status) ?? 0) + 1);
|
|
646
|
+
const meta = [...counts].map(([status, count]) => `${count} ${status}`);
|
|
647
|
+
const unreadTotal = peers.reduce((sum, peer) => sum + peer.unread, 0);
|
|
648
|
+
if (unreadTotal > 0) meta.push(theme.fg("warning", `${unreadTotal} unread`));
|
|
649
|
+
const header = renderStatusLine({ iconOverride: ircGlyph(theme), title: "IRC peers", meta }, theme);
|
|
650
|
+
const items = renderTreeList(
|
|
651
|
+
{
|
|
652
|
+
items: peers,
|
|
653
|
+
expanded,
|
|
654
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
655
|
+
itemType: "peer",
|
|
656
|
+
renderItem: peer => {
|
|
657
|
+
const kindText = peer.parentId ? `${peer.kind}${theme.sep.dot}of ${peer.parentId}` : peer.kind;
|
|
658
|
+
const unread = peer.unread > 0 ? ` ${formatBadge(`${peer.unread} unread`, "warning", theme)}` : "";
|
|
659
|
+
const age = messageAge(peer.lastActivity);
|
|
660
|
+
return `${peerStatusBadge(peer.status, theme)} ${theme.bold(replaceTabs(peer.id))} ${theme.fg("dim", kindText)}${unread}${age ? ` ${theme.fg("dim", age)}` : ""}`;
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
theme,
|
|
664
|
+
);
|
|
665
|
+
return [header, ...items];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function buildResultLines(
|
|
669
|
+
result: { content: Array<{ type: string; text?: string }>; isError?: boolean },
|
|
670
|
+
details: Partial<IrcDetails>,
|
|
671
|
+
args: IrcRenderArgs | undefined,
|
|
672
|
+
expanded: boolean,
|
|
673
|
+
theme: Theme,
|
|
674
|
+
): string[] {
|
|
675
|
+
switch (details.op ?? args?.op) {
|
|
676
|
+
case "send":
|
|
677
|
+
return renderSendResult(result, details, args, expanded, theme);
|
|
678
|
+
case "wait":
|
|
679
|
+
return renderWaitResult(result, details, args, expanded, theme);
|
|
680
|
+
case "inbox":
|
|
681
|
+
return renderInboxResult(details, args, expanded, theme);
|
|
682
|
+
case "list":
|
|
683
|
+
return renderListResult(details, expanded, theme);
|
|
684
|
+
default: {
|
|
685
|
+
const text = textContent(result) || (result.isError ? "IRC call failed." : "Done.");
|
|
686
|
+
return [
|
|
687
|
+
renderStatusLine({ icon: result.isError ? "error" : "success", title: callTitle(args, theme) }, theme),
|
|
688
|
+
result.isError ? formatErrorDetail(text, theme) : ` ${theme.fg("muted", replaceTabs(text))}`,
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export const ircToolRenderer = {
|
|
695
|
+
inline: true,
|
|
696
|
+
mergeCallAndResult: true,
|
|
697
|
+
|
|
698
|
+
renderCall(args: IrcRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
699
|
+
const lines = [
|
|
700
|
+
renderStatusLine({ icon: "pending", title: callTitle(args, uiTheme), meta: callMeta(args) }, uiTheme),
|
|
701
|
+
];
|
|
702
|
+
if (args?.op === "send" && args.message?.trim()) {
|
|
703
|
+
lines.push(...bodyLines(args.message, false, uiTheme, { indent: " ", tone: "dim", collapsedLines: 1 }));
|
|
704
|
+
}
|
|
705
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
renderResult(
|
|
709
|
+
result: { content: Array<{ type: string; text?: string }>; details?: IrcDetails; isError?: boolean },
|
|
710
|
+
options: RenderResultOptions,
|
|
711
|
+
uiTheme: Theme,
|
|
712
|
+
args?: IrcRenderArgs,
|
|
713
|
+
): Component {
|
|
714
|
+
const details: Partial<IrcDetails> = result.details ?? {};
|
|
715
|
+
return createCachedComponent(
|
|
716
|
+
() => options.expanded,
|
|
717
|
+
(width, expanded) =>
|
|
718
|
+
buildResultLines(result, details, args, expanded, uiTheme).map(line =>
|
|
719
|
+
truncateToWidth(line, width, Ellipsis.Unicode),
|
|
720
|
+
),
|
|
721
|
+
);
|
|
722
|
+
},
|
|
723
|
+
};
|