@ctliz/agent-intercom-pi 0.12.2 → 0.14.0

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.
@@ -0,0 +1,124 @@
1
+ import { mkdirSync, rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { ensureIntercomRuntimeDir, getAgentDirPath, getIntercomDirPath } from "./broker/paths.ts";
5
+ import { writeDurableJson } from "./durable-json.ts";
6
+ import { generateNamedTeamScope, listNamedTeams, NAMED_TEAMS_FILE, NAMED_TEAMS_VERSION, parseTeamName, type NamedTeam } from "./named-teams.ts";
7
+ import type { SessionInfo } from "./types.ts";
8
+
9
+ export function namedTeamMemberIds(team: NamedTeam): string[] {
10
+ return team.memberSessionIds ?? [team.managerSessionId];
11
+ }
12
+
13
+ export function sessionNamedTeams(sessionId: string, agentDir?: string): NamedTeam[] {
14
+ return listNamedTeams(agentDir).filter((team) => namedTeamMemberIds(team).includes(sessionId));
15
+ }
16
+
17
+ /** Serialize the complete read-modify-write across independently launched Pi processes. */
18
+ export async function appendNamedTeamMembership(input: {
19
+ name: string;
20
+ selfId: string;
21
+ members?: string[];
22
+ create?: boolean;
23
+ work?: string;
24
+ agentDir?: string;
25
+ }): Promise<NamedTeam> {
26
+ const name = parseTeamName(input.name);
27
+ const memberIds = [...new Set([input.selfId, ...(input.members ?? [])])];
28
+ if (memberIds.some((id) => !id || id.trim() !== id || /[\u0000-\u001f\u007f]/.test(id))) {
29
+ throw new Error("Invalid team member session ID");
30
+ }
31
+ if (input.work !== undefined && (!input.work.trim() || input.work.length > 2000)) {
32
+ throw new Error("Work must be a non-empty task description (max 2000 characters)");
33
+ }
34
+ const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
35
+ ensureIntercomRuntimeDir(dir);
36
+ const lock = join(dir, "named-teams.lock");
37
+ const deadline = Date.now() + 5000;
38
+ for (;;) {
39
+ try {
40
+ mkdirSync(lock, { mode: 0o700 });
41
+ break;
42
+ } catch (error) {
43
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
44
+ if (Date.now() >= deadline) throw new Error("Team registry is busy; retry. If a writer crashed, remove the stale named-teams.lock directory after verifying no writer is active.");
45
+ await delay(25);
46
+ }
47
+ }
48
+ try {
49
+ const teams = listNamedTeams(input.agentDir);
50
+ let team = teams.find((entry) => entry.name === name);
51
+ if (input.create) {
52
+ if (team) throw new Error(`A named team called ${name} already exists; join it instead.`);
53
+ team = { name, scopeId: generateNamedTeamScope(teams.map((entry) => entry.scopeId)), managerSessionId: input.selfId, createdAt: Date.now() };
54
+ teams.push(team);
55
+ }
56
+ if (!team) throw new Error(`Team "${name}" does not exist; create it explicitly.`);
57
+ if (memberIds.some((id) => id !== input.selfId) && team.managerSessionId !== input.selfId) {
58
+ throw new Error(`Only the manager of team "${name}" may add other sessions`);
59
+ }
60
+ if (input.work !== undefined && !input.create) {
61
+ throw new Error("Work is set when creating a team; use a new team for a different task");
62
+ }
63
+ const updated: NamedTeam = {
64
+ ...team,
65
+ memberSessionIds: [...new Set([...namedTeamMemberIds(team), ...memberIds])],
66
+ ...(input.work === undefined ? {} : { work: input.work.trim() }),
67
+ };
68
+ writeDurableJson(join(dir, NAMED_TEAMS_FILE), {
69
+ version: NAMED_TEAMS_VERSION,
70
+ teams: teams.map((entry) => entry.name === name ? updated : entry),
71
+ });
72
+ return updated;
73
+ } finally {
74
+ rmSync(lock, { recursive: true });
75
+ }
76
+ }
77
+
78
+ export function requireNamedTeamMembers(name: string, selfId: string, peerId: string, agentDir?: string): NamedTeam {
79
+ const team = listNamedTeams(agentDir).find((entry) => entry.name === name);
80
+ if (!team) throw new Error(`Unknown team "${name}"`);
81
+ const members = namedTeamMemberIds(team);
82
+ if (!members.includes(selfId) || !members.includes(peerId)) {
83
+ throw new Error(`Both sessions must belong to team "${name}" before messaging`);
84
+ }
85
+ return team;
86
+ }
87
+
88
+ export function resolveNamedMessageTeam(selfId: string, peerId: string, requested?: string, agentDir?: string): string | undefined {
89
+ if (requested !== undefined) return requireNamedTeamMembers(requested, selfId, peerId, agentDir).name;
90
+ const mine = sessionNamedTeams(selfId, agentDir);
91
+ const shared = mine.filter((team) => namedTeamMemberIds(team).includes(peerId));
92
+ if (shared.length === 1) return shared[0]!.name;
93
+ if (shared.length > 1) throw new Error("Multiple shared teams; specify `team` for this task");
94
+ // No shared team means an ungrouped contact, even when either session belongs
95
+ // to unrelated teams. Do not silently create a team or add the recipient.
96
+ return undefined;
97
+ }
98
+
99
+ export function namedTeamRoster(team: NamedTeam, selfId: string, sessions: SessionInfo[]) {
100
+ return {
101
+ name: team.name,
102
+ ...(team.work ? { work: team.work } : {}),
103
+ self: { id: selfId, isManager: selfId === team.managerSessionId },
104
+ manager: { target: team.managerSessionId, connected: sessions.some((entry) => entry.id === team.managerSessionId) },
105
+ members: namedTeamMemberIds(team).map((id) => ({
106
+ id,
107
+ target: id,
108
+ name: sessions.find((entry) => entry.id === id)?.name,
109
+ role: id === team.managerSessionId ? "manager" : "member",
110
+ connected: sessions.some((entry) => entry.id === id),
111
+ })),
112
+ };
113
+ }
114
+
115
+ export function formatNamedTeamRoster(team: ReturnType<typeof namedTeamRoster>): string {
116
+ return [
117
+ `Team: ${team.name}`,
118
+ ...(team.work ? [`Work: ${team.work}`] : []),
119
+ `You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
120
+ `Manager: ${team.manager.target}${team.manager.connected ? "" : " [offline]"}`,
121
+ "Members:",
122
+ ...team.members.map((member) => `- ${member.name || member.id} (${member.id}) [${member.role}]${member.connected ? "" : " [offline]"}`),
123
+ ].join("\n");
124
+ }
package/named-teams.ts CHANGED
@@ -14,6 +14,9 @@ export interface NamedTeam {
14
14
  scopeId: string;
15
15
  managerSessionId: string;
16
16
  createdAt: number;
17
+ /** Pi task-team membership; absent in legacy single-scope teams. */
18
+ memberSessionIds?: string[];
19
+ work?: string;
17
20
  }
18
21
 
19
22
  export type JoinableCircleKind = "named" | "tmuxdeck";
@@ -57,11 +60,20 @@ function parseStoredTeam(value: unknown): NamedTeam | undefined {
57
60
  if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
58
61
  return undefined;
59
62
  }
63
+ if (value.memberSessionIds !== undefined && (
64
+ !Array.isArray(value.memberSessionIds)
65
+ || !value.memberSessionIds.every((id) => typeof id === "string" && id.trim() === id && id.length > 0 && !/[\u0000-\u001f\u007f]/.test(id))
66
+ || new Set(value.memberSessionIds).size !== value.memberSessionIds.length
67
+ || !value.memberSessionIds.includes(value.managerSessionId)
68
+ )) return undefined;
69
+ if (value.work !== undefined && (typeof value.work !== "string" || !value.work.trim() || value.work.length > 2000)) return undefined;
60
70
  return {
61
71
  name: value.name,
62
72
  scopeId: value.scopeId,
63
73
  managerSessionId: value.managerSessionId,
64
74
  createdAt: value.createdAt,
75
+ ...(value.memberSessionIds === undefined ? {} : { memberSessionIds: value.memberSessionIds as string[] }),
76
+ ...(value.work === undefined ? {} : { work: value.work as string }),
65
77
  };
66
78
  }
67
79
 
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-pi",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
4
4
  "description": "Pi coding-agent intercom for local messaging with Codex, Claude Code, OpenCode, Grok Build, and AGY agents.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
7
7
  "main": "index.ts",
8
+ "bin": {
9
+ "intercom-send": "./bin/intercom-send.mjs"
10
+ },
8
11
  "exports": {
9
12
  ".": "./index.ts"
10
13
  },
11
14
  "files": [
12
15
  "banner.png",
16
+ "bin/*.mjs",
13
17
  "*.ts",
14
18
  "!*.test.ts",
15
19
  "broker/**/*.ts",
@@ -54,14 +58,20 @@
54
58
  },
55
59
  "dependencies": {
56
60
  "@ctliz/agent-intercom-core": "0.2.0",
61
+ "tsx": "^4.20.0"
62
+ },
63
+ "peerDependencies": {
57
64
  "@earendil-works/pi-ai": "*",
58
65
  "@earendil-works/pi-coding-agent": "*",
59
66
  "@earendil-works/pi-tui": "*",
60
- "tsx": "^4.20.0",
61
67
  "typebox": "*"
62
68
  },
63
69
  "devDependencies": {
70
+ "@earendil-works/pi-ai": "^1.0.0",
71
+ "@earendil-works/pi-coding-agent": "^1.0.0",
72
+ "@earendil-works/pi-tui": "^1.0.0",
64
73
  "@types/node": "^22.15.0",
74
+ "typebox": "*",
65
75
  "typescript": "^5.9.3"
66
76
  },
67
77
  "publishConfig": {
package/reply-tracker.ts CHANGED
@@ -21,6 +21,10 @@ export function pendingAskId(fromSessionId: string, messageId: string): string {
21
21
  return `ask-${digest}`;
22
22
  }
23
23
 
24
+ export function replyContextId(fromSessionId: string, messageId: string): string {
25
+ return pendingAskId(fromSessionId, messageId).replace(/^ask-/, "ctx-");
26
+ }
27
+
24
28
  function matchesPendingSender(context: IntercomContext, to: string): boolean {
25
29
  if (context.from.id === to) {
26
30
  return true;
@@ -91,8 +95,23 @@ export class ReplyTracker {
91
95
  this.currentTurnContexts = [];
92
96
  }
93
97
 
94
- resolveReplyTarget(options: { to?: string; replyTo?: string; askId?: string; which?: ReplyWhich }, now = Date.now()): IntercomContext {
98
+ resolveReplyTarget(options: { to?: string; replyTo?: string; askId?: string; which?: ReplyWhich; team?: string; contextId?: string }, now = Date.now()): IntercomContext {
95
99
  this.pruneExpired(now);
100
+ const checkTeam = (context: IntercomContext): IntercomContext => {
101
+ if (options.team !== undefined && context.message.content.team !== options.team) {
102
+ throw new Error("Reply team must match the original message; it cannot be changed");
103
+ }
104
+ return context;
105
+ };
106
+ if (options.contextId) {
107
+ const match = [...this.currentTurnContexts, ...this.pendingAsks.values()].find((context) =>
108
+ replyContextId(context.from.id, context.message.id) === options.contextId
109
+ );
110
+ if (!match) throw new Error(`No active message with context ID "${options.contextId}"`);
111
+ if (options.to && !matchesPendingSender(match, options.to)) throw new Error("Reply context is not from the selected sender");
112
+ if (options.askId && pendingAskId(match.from.id, match.message.id) !== options.askId) throw new Error("Reply selectors refer to different messages");
113
+ return checkTeam(match);
114
+ }
96
115
 
97
116
  if (options.askId) {
98
117
  const match = Array.from(this.pendingAsks.values()).find((context) =>
@@ -104,7 +123,7 @@ export class ReplyTracker {
104
123
  if (options.to && !matchesPendingSender(match, options.to)) {
105
124
  throw new Error(`Pending ask "${options.askId}" is not from "${options.to}"`);
106
125
  }
107
- return match;
126
+ return checkTeam(match);
108
127
  }
109
128
 
110
129
  if (options.replyTo) {
@@ -119,13 +138,18 @@ export class ReplyTracker {
119
138
  if (matches.length > 1) {
120
139
  throw new Error(`Multiple pending asks use message ID "${options.replyTo}" — specify \`to\``);
121
140
  }
122
- return matches[0]!;
141
+ return checkTeam(matches[0]!);
123
142
  }
124
143
 
125
- if (this.currentTurnContexts.length > 0) {
144
+ const inTeam = (context: IntercomContext) => options.team === undefined || context.message.content.team === options.team;
145
+ const currentTurnContexts = this.currentTurnContexts.filter(inTeam);
146
+ if (currentTurnContexts.length > 0) {
126
147
  const turnMatches = options.to
127
- ? this.currentTurnContexts.filter((context) => matchesPendingSender(context, options.to!))
128
- : this.currentTurnContexts;
148
+ ? currentTurnContexts.filter((context) => matchesPendingSender(context, options.to!))
149
+ : currentTurnContexts;
150
+ if (new Set(turnMatches.map((context) => context.message.content.team)).size > 1) {
151
+ throw new Error("Messages from multiple teams are active — specify `contextId`, `askId`, or `team`");
152
+ }
129
153
  const replyableMatches = turnMatches.filter((context) => context.message.expectsReply);
130
154
  if (replyableMatches.length === 1) {
131
155
  return replyableMatches[0]!;
@@ -146,14 +170,13 @@ export class ReplyTracker {
146
170
  }
147
171
  }
148
172
 
149
- const pending = Array.from(this.pendingAsks.values());
150
- if (pending.length === 1) {
151
- return pending[0]!;
152
- }
153
-
173
+ const pending = Array.from(this.pendingAsks.values()).filter(inTeam);
154
174
  const matches = options.to
155
175
  ? pending.filter((context) => matchesPendingSender(context, options.to!))
156
176
  : pending;
177
+ if (new Set(matches.map((context) => context.message.content.team)).size > 1) {
178
+ throw new Error("Pending asks belong to multiple teams — specify `askId` or `team`");
179
+ }
157
180
  if (matches.length === 1) return matches[0]!;
158
181
  if (matches.length > 1) {
159
182
  if (!options.to && distinctSenders(matches) > 1) {
@@ -174,9 +197,9 @@ export class ReplyTracker {
174
197
  this.dismissPendingAsk(replyTo, fromSessionId);
175
198
  }
176
199
 
177
- dismissOrdinarySender(fromSessionId: string): void {
200
+ dismissOrdinarySender(fromSessionId: string, messageId?: string): void {
178
201
  this.currentTurnContexts = this.currentTurnContexts.filter((context) =>
179
- context.message.expectsReply || context.from.id !== fromSessionId
202
+ context.message.expectsReply || context.from.id !== fromSessionId || (messageId !== undefined && context.message.id !== messageId)
180
203
  );
181
204
  }
182
205
 
@@ -2,8 +2,8 @@
2
2
  name: pi-intercom
3
3
  description: |
4
4
  Streamline session-to-session coordination with pi-intercom. Send messages,
5
- delegate tasks, and coordinate work across multiple pi sessions on the same
6
- machine. Use for planner-worker workflows, cross-session context sharing,
5
+ delegate tasks, and form task teams after user approval across multiple Pi
6
+ sessions on the same machine. Use for planner-worker workflows, cross-session context sharing,
7
7
  and real-time collaboration between sessions.
8
8
  ---
9
9
 
@@ -24,6 +24,73 @@ This skill covers how to handle those orchestrator-side escalations.
24
24
  - **Clarification loops**: Worker asks questions, planner answers, work continues
25
25
  - **Multi-session workflows**: Coordinate between specialized sessions (frontend/backend, research/implementation)
26
26
 
27
+ ## Task Teams: Ask Once, Then Organize (Pi 0.14+)
28
+
29
+ Independently opened Pi sessions do not form a team merely by sharing a directory.
30
+ When the user asks you to collaborate with named sessions (for example `front`
31
+ and `writer`) or delegate work, and this task has no approved team, ask once:
32
+
33
+ > 要把我、front、writer 组成一个 team 来负责这个任务吗?
34
+
35
+ Wait for approval before creating a team or adding peers. An explicit instruction
36
+ to create/join a team is already approval. Do not ask again for the same approved
37
+ team, and do not prompt the user when replying to an inbound team message. If the
38
+ user declines, do not repeatedly ask or silently create a team. Without a shared
39
+ team, omit `team` to send an ungrouped direct message, even if either session
40
+ belongs to unrelated teams. This allows initial contact before forming a team.
41
+ Explicit team messages still require both sessions to be members; multiple
42
+ shared teams still require selecting the intended task team.
43
+
44
+ After approval, discover the named sessions and form the team in one operation:
45
+
46
+ ```typescript
47
+ intercom_list({})
48
+ intercom_join({
49
+ name: "launch",
50
+ create: true,
51
+ members: ["front", "writer"],
52
+ work: "Build the product page; front owns UI, writer owns copy",
53
+ })
54
+ intercom_send({ team: "launch", to: "front", message: "Implement the product page UI." })
55
+ intercom_send({ team: "launch", to: "writer", message: "Write the product page copy." })
56
+ ```
57
+
58
+ The creator is the manager of this task team and does not need a `/name`.
59
+ Members are resolved to stable session IDs; no user needs to join each terminal
60
+ manually. Only the manager can add other sessions with `members`; a session can
61
+ join itself with `intercom_join({ name: "launch" })`. Joining appends membership
62
+ without leaving other named teams or changing their managers. The approved
63
+ manager can append a new approved peer with
64
+ `intercom_join({ name: "launch", members: ["reviewer"] })`.
65
+
66
+ Reuse the team for this task, not for every message. A different task may need a
67
+ different team even with identical participants. `intercom_team({})` shows all
68
+ your named teams; `intercom_team({ team: "launch" })` inspects one.
69
+
70
+ Messages carry their team separately from their text. Read each `[Team: ...]`
71
+ label independently, including mixed-team batches. Use the exact `contextId`
72
+ reply hint to answer ordinary messages, or `askId` from `intercom_pending` for
73
+ asks. `intercom_reply` inherits the original message's team; never choose a reply
74
+ team using a mutable current-team setting. When peers share multiple teams,
75
+ explicitly pass `team` to sends and asks. Ambiguous replies fail rather than guess.
76
+
77
+ This is Pi-local task grouping, not a broker security boundary or separate model
78
+ history for each team. Other adapters and managed-team integrations are unchanged;
79
+ all participating Pi sessions must load adapter 0.14.0 or newer.
80
+
81
+ ## Codemode (Pi 0.99.1+)
82
+
83
+ Active Intercom tools can be called from codemode as `await tools.intercom_send({...})`, `await tools.intercom_team({})`, and so on. Every result is `{ ok, text, data }`: check `ok`, then read structured delivery flags, session lists, team roster, or pending asks from `data`. Returned errors retain this object; invalid arguments, blocked calls, and thrown exceptions can still reject. Use `try/catch` or `Promise.allSettled()` for independent operations.
84
+
85
+ ```javascript
86
+ const result = await tools.intercom_send({ to: "worker", message: "Tests passed." });
87
+ return { ok: result.ok, delivered: result.data.delivered };
88
+ ```
89
+
90
+ A deferred ask has `ok: true` and `data.pending: true`; it is not a failure. Do not proceed with dependent work until the actual answer arrives. Different recipients may be asked concurrently; never create a second unresolved ask to the same recipient. Delivery means durable queue acknowledgement, not task completion.
91
+
92
+ For shell notifications, use `intercom-send <session-name-or-id> <message>`. It registers an independent send-only identity and prints JSON; it does not take over the current Pi session or track replies.
93
+
27
94
  ## Core Patterns
28
95
 
29
96
  ### Pattern 1: Planner-Worker Delegation
@@ -78,12 +145,11 @@ Do not add artificial sleeps to form a batch. Each original message keeps its se
78
145
 
79
146
  ### Pattern 3: Find Your Manager or Team
80
147
 
81
- If no team exists yet, create one and have the other session join. This does not require tmux:
148
+ For independent Pi sessions, ask for approval first, then create a task team and add the connected peer:
82
149
 
83
150
  ```typescript
84
- intercom_join({ name: "billing", create: true }) // manager
85
- intercom_join({ name: "billing" }) // other session
86
- intercom_team({})
151
+ intercom_join({ name: "billing", create: true, members: ["worker"], work: "Implement billing" })
152
+ intercom_team({ team: "billing" })
87
153
  ```
88
154
 
89
155
  `/intercom-create billing` and `/intercom-join billing` do the same from the command line. `intercom_join({})` lists named teams and TmuxDeck workspaces without printing raw scopes.
@@ -122,7 +188,7 @@ intercom_pending({ askId: "ask-..." }) // Retrieve the full untruncated body whe
122
188
  intercom_reply({ askId: "ask-...", message: "Use exponential backoff starting at 100ms." })
123
189
  ```
124
190
 
125
- `intercom_reply` preserves exact threading internally. `intercom_pending` returns stable receiver-local ask IDs and short previews; models never see or construct the protocol message ID. Managers can pass `session` with an owned coworker target from `intercom_team` to inspect that coworker's pending inbox.
191
+ `intercom_reply` preserves exact threading and the original team internally. Team-message reply hints provide an exact receiver-local `contextId`, including for ordinary messages. `intercom_pending` returns stable receiver-local ask IDs, team labels, and short previews; models never see or construct the protocol message ID. Managers can pass `session` with an owned coworker target from `intercom_team` to inspect that coworker's pending inbox.
126
192
 
127
193
  ### Pattern 5: Broadcast to Multiple Workers
128
194
 
@@ -467,7 +533,7 @@ if (result.details?.pending) {
467
533
 
468
534
  ### Session name flips or registration reports `SESSION_ID_IN_USE`
469
535
 
470
- The same Pi session is open in more than one live runtime, such as a desktop terminal and a mobile/RPC host. Intercom keeps the first runtime authoritative instead of allowing the two clients to evict each other. Close or switch away from the duplicate runtime; one transcript/session ID must have only one live owner.
536
+ The same Pi session is open in more than one live runtime, such as a desktop terminal and a mobile/RPC host. Intercom keeps the first runtime authoritative instead of allowing the two clients to evict each other. The conflicting runtime pauses automatic reconnect; `intercom_status` returns `ok: false` with `data.code: "SESSION_ID_IN_USE"`. Switch to a different session, or release the duplicate owner and `/reload`. One transcript/session ID must have only one live owner; do not auto-generate a replacement identity for a running session.
471
537
 
472
538
  ### Message not delivered
473
539
 
package/tool-result.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+
4
+ export const intercomOutputSchema = Type.Object({
5
+ ok: Type.Boolean(),
6
+ text: Type.String(),
7
+ data: Type.Record(Type.String(), Type.Unknown()),
8
+ });
9
+
10
+ type ToolResult = Awaited<ReturnType<ToolDefinition<any, any>["execute"]>>;
11
+
12
+ /** Keep model/TUI content unchanged while giving codemode callers JSON data. */
13
+ export function structuredIntercomResult(result: ToolResult): ToolResult {
14
+ const details = result.details ?? {};
15
+ const isError = result.isError === true || details.error === true || details.delivered === false;
16
+ return {
17
+ ...result,
18
+ details,
19
+ isError,
20
+ structuredContent: {
21
+ ok: !isError,
22
+ text: result.content.filter((item) => item.type === "text").map((item) => item.text).join("\n"),
23
+ data: JSON.parse(JSON.stringify(details)),
24
+ },
25
+ };
26
+ }
package/types.ts CHANGED
@@ -36,6 +36,8 @@ export interface Message {
36
36
  expectsReply?: boolean;
37
37
  content: {
38
38
  text: string;
39
+ /** Public, immutable task-team name; not a broker registration scope. */
40
+ team?: string;
39
41
  attachments?: Attachment[];
40
42
  control?: IntercomCommonControlEnvelope;
41
43
  };
@@ -47,7 +47,8 @@ export class InlineMessageComponent implements Component {
47
47
  }
48
48
  const bodyWidth = Math.max(1, width - 2);
49
49
 
50
- const header = ` 📨 From: ${senderName} (${senderCwd}) `;
50
+ const teamLabel = this.message.content.team ? `[Team: ${sanitizeDisplayText(this.message.content.team)}] ` : "";
51
+ const header = ` ${teamLabel}📨 From: ${senderName} (${senderCwd}) `;
51
52
  const headerText = truncateToWidth(this.collapsed ? `${header} Ctrl+O expands ` : header, bodyWidth, "");
52
53
  const headerPadding = Math.max(0, bodyWidth - visibleWidth(headerText));
53
54
  lines.push(this.theme.fg("accent", `╭${headerText}${borderChar.repeat(headerPadding)}╮`));