@bastani/atomic 0.9.19-alpha.8 → 0.9.19-alpha.9

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.19-alpha.9] - 2026-09-12
6
+
7
+ ### Breaking Changes
8
+
9
+ - Explicit Intercom `action: "reply"` selectors reject stale, unknown, empty, or sender-mismatched threads instead of falling back. Use `action: "pending"` and `replyTo: "<pending-ask-id>"` for unresolved questions.
10
+
11
+ ### Changed
12
+
13
+ - Intercom agent lists put exact copyable session IDs and workflow paths first, with useful metadata and meaningful names secondary rather than redundant generated aliases.
14
+
15
+ ### Fixed
16
+
17
+ - Intercom targeted replies now complete the requested ask when an unrelated message is active, rather than delivering a response with the wrong thread ID or refusing it.
18
+
5
19
  ## [0.9.19-alpha.7] - 2026-09-12
6
20
 
7
21
  ### Fixed
@@ -4,6 +4,20 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.19-alpha.9] - 2026-09-12
8
+
9
+ ### Breaking Changes
10
+
11
+ - Explicit `reply` selectors now fail closed: stale, unknown, empty, or sender-mismatched `replyTo` values no longer fall back to another active or pending thread. Use `pending` to select the exact unresolved question.
12
+
13
+ ### Changed
14
+
15
+ - Agent-visible Intercom lists lead with copyable full session IDs and canonical workflow paths, keeping status, working directory, meaningful names, and future queued counts without redundant generated aliases.
16
+
17
+ ### Fixed
18
+
19
+ - Targeted replies select the requested sender's pending ask even when an unrelated message triggered the current turn, so a delivered reply no longer leaves that ask waiting on the wrong thread.
20
+
7
21
  ## [0.9.19-alpha.6] - 2026-09-11
8
22
 
9
23
  ### Fixed
@@ -57,7 +57,7 @@ A session becomes intercom-connected when all of these are true:
57
57
 
58
58
  The session list, ALT+M picker, and group counts include connected agent sessions only. Internal workflow routing/control connections, model-less `ctx.ui` prompts, and `ctx.tool` nodes are hidden and cannot receive ordinary messages, even by a known ID or through a supervisor route. This includes run-level prompts and retained completed synthetic prompt stages. An agent executing a tool, including `tool:workflow`, or awaiting human input remains visible and messageable.
59
59
 
60
- If a session is unnamed, intercom exposes a runtime-only fallback alias like `subagent-chat-1a2b3c4d-1111-4222-8333-123456789abc` so other sessions can still target it. That alias is not persisted as the session title, so resume pickers can keep showing the transcript snippet instead of a generic `session-...` name.
60
+ Unnamed sessions retain a runtime-only alias such as `subagent-chat-1a2b3c4d-1111-4222-8333-123456789abc` for name lookup. Agent lists omit this redundant alias and lead with the copyable full session ID. Meaningful names remain secondary metadata; resume titles are unchanged.
61
61
 
62
62
  ## Quick Start
63
63
 
@@ -76,10 +76,10 @@ The agent can list sessions and send messages using the `intercom` tool. Tool ca
76
76
  ```typescript
77
77
  // List active sessions
78
78
  intercom({ action: "list" })
79
- // → **Current session:**
80
- // → executor (20d43841-1111-4222-8333-123456789abc) ~/projects/api (claude-sonnet-4) [self, idle]
81
- // → **Other sessions:**
82
- // → research (6332faab-1111-4222-8333-123456789abc) ~/projects/api (claude-sonnet-4) [same cwd, thinking]
79
+ // → **Current session** (groups: default):
80
+ // → - `20d43841-1111-4222-8333-123456789abc` [self, idle] ~/projects/api (claude-sonnet-4) name: executor
81
+ // → **Other visible sessions and workflow stages:**
82
+ // → - `6332faab-1111-4222-8333-123456789abc` [same cwd, thinking] ~/projects/api (claude-sonnet-4) name: research
83
83
 
84
84
  // Add a named membership (it is created if no session is there yet)
85
85
  intercom({ action: "join", group: "api-review" })
@@ -156,7 +156,7 @@ Verify they see each other from either session:
156
156
 
157
157
  ```typescript
158
158
  intercom({ action: "list" })
159
- // → worker ~/projects/api (claude-sonnet-4) [idle]
159
+ // → - `6332faab-1111-4222-8333-123456789abc` [same cwd, idle] ~/projects/api (claude-sonnet-4) name: worker
160
160
  ```
161
161
 
162
162
  ### The Conversation
@@ -383,7 +383,7 @@ When multiple live agent stages share a name, `ask` refuses the ambiguous target
383
383
 
384
384
  **`ask`** — Sends a message and waits for a live recipient to reply (10-minute timeout). Invocation control supports a live ask into an owned isolated subgroup, and the exact broker-recorded reply resolves the waiting tool call at the asker without opening reverse or lateral group access. Ask to an uninitialized stage remains refused with `pending_stage_ask_unsupported`; use queued `send` instead, because holding a reply waiter until a stage eventually starts would be unbounded. A recipient disconnect after live delivery fails only that peer's exact wait promptly; the timeout remains the backstop while the recipient stays connected. Up to `maxPendingAsks` blocking asks (default: 6) may run concurrently, including same-target and mixed-target fan-out. Replies resolve by exact sender and message ID, so out-of-order replies cannot cross-settle another call. When capacity is full, new asks receive a structured refusal.
385
385
 
386
- **`reply`** Replies to the current intercom-triggered message if there is one. Otherwise it falls back to the single unresolved inbound ask. If multiple asks are pending, pass an exact name/full session ID in `to`, or the listed message ID in `replyTo`; use `pending` to inspect them first. `replyTo` also disambiguates multiple asks from the same sender. Under the hood this is still a normal `send` with the exact `replyTo` value.
386
+ **`reply`** without selectors replies to the current intercom-triggered message, or otherwise the single unresolved inbound ask. Explicit `to` selects a pending ask from that exact name/full session ID, even during another sender's turn. If that sender has multiple asks, use `pending` and pass the exact message ID as `replyTo`. Explicit `replyTo` selects only that pending ask or the exact active ordinary message; stale, unknown, or empty IDs fail instead of falling back. When both selectors are supplied, the sender must match the selected thread. A successful reply keeps that sender/thread pair across internal retries.
387
387
 
388
388
  **`pending`** — Lists unresolved inbound asks with sender, message ID, elapsed time, and a short preview. Useful when replying after the original triggered turn.
389
389
 
@@ -38,6 +38,8 @@ export interface SendOptions {
38
38
  logicalTarget?: string;
39
39
  /** Public reply must use the broker's exact pending reverse question route. */
40
40
  requirePendingReply?: true;
41
+ /** An explicit reply selector must resolve to this frozen recipient, never a namesake. */
42
+ expectedRecipientId?: string;
41
43
  /** Broker-authorized identity for a canonical-path ask, delivered before its recipient can reply. */
42
44
  onReplyTarget?: (sessionId: string) => void;
43
45
  }
@@ -1004,6 +1006,7 @@ export class IntercomClient extends EventEmitter {
1004
1006
  transportTarget: to,
1005
1007
  logicalSignature: buildSendSignature(options.logicalTarget ?? to, options),
1006
1008
  requirePendingReply: options.requirePendingReply ?? false,
1009
+ ...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
1007
1010
  });
1008
1011
  acquired = this.pendingSends.acquire(messageId, pendingSignature, 10000);
1009
1012
  } catch (error) {
@@ -1028,6 +1031,7 @@ export class IntercomClient extends EventEmitter {
1028
1031
  to,
1029
1032
  ...(options.logicalTarget === undefined ? {} : { logicalTarget: options.logicalTarget }),
1030
1033
  ...(options.requirePendingReply === undefined ? {} : { requirePendingReply: options.requirePendingReply }),
1034
+ ...(options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }),
1031
1035
  ...(options.onReplyTarget === undefined ? {} : { resolveReplyTarget: true as const }),
1032
1036
  message,
1033
1037
  attemptId: acquired.attempt.attemptId,
@@ -143,6 +143,12 @@ export function handleBrokerSend(
143
143
  return;
144
144
  }
145
145
  const requirePendingReply = clientMessage.requirePendingReply === true;
146
+ const hasExpectedRecipientId = Object.prototype.hasOwnProperty.call(clientMessage, "expectedRecipientId");
147
+ if (hasExpectedRecipientId && (typeof clientMessage.expectedRecipientId !== "string" || !clientMessage.expectedRecipientId.trim())) {
148
+ write(socket, { type: "delivery_failed", messageId, attemptId, reason: "Invalid expectedRecipientId format" });
149
+ return;
150
+ }
151
+ const expectedRecipientId = hasExpectedRecipientId ? clientMessage.expectedRecipientId as string : undefined;
146
152
  if (Object.prototype.hasOwnProperty.call(clientMessage, "channel")) {
147
153
  write(socket, { type: "delivery_failed", messageId: message.id, attemptId, reason: "Invalid channel" });
148
154
  return;
@@ -152,6 +158,10 @@ export function handleBrokerSend(
152
158
  write(socket, { type: "delivery_failed", messageId, attemptId, reason: "Invalid requirePendingReply message" });
153
159
  return;
154
160
  }
161
+ if (expectedRecipientId !== undefined && (supervisorSend || message.replyTo === undefined || message.expectsReply === true)) {
162
+ write(socket, { type: "delivery_failed", messageId, attemptId, reason: "Invalid expectedRecipientId message" });
163
+ return;
164
+ }
155
165
 
156
166
 
157
167
  const fromSession = currentId ? sessions.get(currentId) : undefined;
@@ -161,7 +171,8 @@ export function handleBrokerSend(
161
171
  }
162
172
  const senderIdentity = fromSession.registrationReturnAddress ?? fromSession.info.id;
163
173
  const logicalTarget = typeof clientMessage.logicalTarget === "string" ? clientMessage.logicalTarget : clientMessage.to;
164
- const baseSignature = buildMessageSendSignature(logicalTarget, message, senderIdentity);
174
+ const messageSignature = buildMessageSendSignature(logicalTarget, message, senderIdentity);
175
+ const baseSignature = expectedRecipientId === undefined ? messageSignature : JSON.stringify({ expectedRecipientId, messageSignature });
165
176
  const signature = requirePendingReply
166
177
  ? JSON.stringify({
167
178
  requirePendingReply: true,
@@ -221,6 +232,12 @@ export function handleBrokerSend(
221
232
  const resolution = exactIdTarget
222
233
  ? ({ kind: "resolved", session: exactIdTarget.info } as const)
223
234
  : resolveSessionTarget(candidates, trimmedTo);
235
+ if (expectedRecipientId !== undefined && (resolution.kind !== "resolved" || resolution.session.id !== expectedRecipientId)) {
236
+ write(socket, { type: "delivery_failed", messageId, attemptId,
237
+ reason: resolution.kind === "resolved" ? "Reply target does not match the expected recipient" : sessionTargetFailureReason(clientMessage.to, resolution),
238
+ });
239
+ return;
240
+ }
224
241
  if (resolution.kind === "resolved") {
225
242
  const target = sessions.get(resolution.session.id);
226
243
  if (!target) {
@@ -1247,7 +1247,8 @@ var init_client = __esm(() => {
1247
1247
  const pendingSignature = JSON.stringify({
1248
1248
  transportTarget: to,
1249
1249
  logicalSignature: buildSendSignature(options.logicalTarget ?? to, options),
1250
- requirePendingReply: options.requirePendingReply ?? false
1250
+ requirePendingReply: options.requirePendingReply ?? false,
1251
+ ...options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId }
1251
1252
  });
1252
1253
  acquired = this.pendingSends.acquire(messageId, pendingSignature, 1e4);
1253
1254
  } catch (error) {
@@ -1273,6 +1274,7 @@ var init_client = __esm(() => {
1273
1274
  to,
1274
1275
  ...options.logicalTarget === undefined ? {} : { logicalTarget: options.logicalTarget },
1275
1276
  ...options.requirePendingReply === undefined ? {} : { requirePendingReply: options.requirePendingReply },
1277
+ ...options.expectedRecipientId === undefined ? {} : { expectedRecipientId: options.expectedRecipientId },
1276
1278
  ...options.onReplyTarget === undefined ? {} : { resolveReplyTarget: true },
1277
1279
  message,
1278
1280
  attemptId: acquired.attempt.attemptId,
@@ -2008,22 +2010,22 @@ class ReplyTracker {
2008
2010
  }
2009
2011
  resolveReplyTarget(options, now = Date.now()) {
2010
2012
  this.pruneExpired(now);
2011
- if (options.replyTo) {
2012
- const exact = this.pendingAsks.get(options.replyTo);
2013
- if (exact) {
2014
- if (options.to) {
2015
- const resolution = resolveSessionTarget([exact.from], options.to);
2016
- if (resolution.kind !== "resolved")
2017
- throw new Error(`Pending ask "${options.replyTo}" is not from "${options.to}"`);
2018
- }
2019
- return exact;
2013
+ if (options.replyTo !== undefined) {
2014
+ const exact = this.pendingAsks.get(options.replyTo) ?? (this.currentTurnContext?.message.id === options.replyTo && !this.currentTurnContext.message.expectsReply ? this.currentTurnContext : undefined);
2015
+ if (!exact)
2016
+ throw new Error(`No reply context for "${options.replyTo}"`);
2017
+ if (options.to !== undefined) {
2018
+ const resolution = resolveSessionTarget([exact.from], options.to);
2019
+ if (resolution.kind !== "resolved")
2020
+ throw new Error(`Reply context "${options.replyTo}" is not from "${options.to}"`);
2020
2021
  }
2022
+ return exact;
2021
2023
  }
2022
- if (this.currentTurnContext) {
2024
+ if (options.to === undefined && this.currentTurnContext) {
2023
2025
  return this.currentTurnContext;
2024
2026
  }
2025
2027
  const pending = Array.from(this.pendingAsks.values());
2026
- if (options.to) {
2028
+ if (options.to !== undefined) {
2027
2029
  const senders = [...new Map(pending.map((context) => [context.from.id, context.from])).values()];
2028
2030
  const resolution = resolveSessionTarget(senders, options.to);
2029
2031
  if (resolution.kind !== "resolved") {
@@ -2446,13 +2448,13 @@ function formatSessionLabel(session, duplicates) {
2446
2448
  return duplicates.has(session.name.toLowerCase()) ? `${session.name} (${session.id})` : session.name;
2447
2449
  }
2448
2450
  function formatSessionListRow(session, currentCwd, isSelf) {
2449
- const name = session.name || "Unnamed session";
2451
+ const name = session.name && session.name !== session.id && session.name !== resolveIntercomPresenceName(undefined, session.id) ? ` name: ${session.name}` : "";
2450
2452
  const normalizedGroup = normalizeGroup(session.group);
2451
2453
  const groupTag = normalizedGroup !== DEFAULT_GROUP ? `group: ${normalizedGroup}` : undefined;
2452
2454
  const capabilityTag = session.replyCapability === undefined ? undefined : `replyCapability: ${session.replyCapability}`;
2453
2455
  const tags = [isSelf ? "self" : session.cwd === currentCwd ? "same cwd" : undefined, session.status, capabilityTag, groupTag].filter((tag) => Boolean(tag));
2454
2456
  const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
2455
- return `• ${name} (${session.id}) ${session.cwd} (${session.model})${suffix}`;
2457
+ return `- \`${session.id}\`${suffix} ${session.cwd} (${session.model})${name}`;
2456
2458
  }
2457
2459
  function previewText(value, maxLength = 72) {
2458
2460
  if (typeof value !== "string") {
@@ -3192,10 +3194,10 @@ async function resolveReplySender(client, logicalTarget, sendTarget) {
3192
3194
  return { kind: "unresolved" };
3193
3195
  }
3194
3196
  function formatWorkflowStageRow(stage) {
3195
- return `- **${stage.stageName}** — workflow stage [${stage.lifecycle.toUpperCase()}] target: \`${stage.target}\`${stage.sessionId === undefined ? "" : ` — intercom session: ${stage.sessionId}`}`;
3197
+ return `- \`${stage.target}\` [${stage.lifecycle.toUpperCase()}] workflow stage: ${stage.stageName}${stage.sessionId === undefined ? "" : ` session: \`${stage.sessionId}\``}`;
3196
3198
  }
3197
3199
  function formatWorkflowFutureStageRow(stage) {
3198
- return `- future workflow stage \`${stage.target}\` ${stage.queuedCount} queued message${stage.queuedCount === 1 ? "" : "s"}`;
3200
+ return `- \`${stage.target}\` [future] ${stage.queuedCount} queued message${stage.queuedCount === 1 ? "" : "s"}`;
3199
3201
  }
3200
3202
  function registerIntercomTool(pi, deps) {
3201
3203
  const retryIdentities = new RetryIdentityReservations({ maxEntries: deps.retryIdentityMaxEntries });
@@ -3949,12 +3951,13 @@ ${replyText}${replyAttachments}` }],
3949
3951
  messageId: replyMessageId,
3950
3952
  logicalTarget: replyLogicalTarget,
3951
3953
  ...route.expectsReply ? { requirePendingReply: true } : {},
3954
+ ...to === undefined && replyTo === undefined ? {} : { expectedRecipientId: route.senderId },
3952
3955
  text: message,
3953
3956
  attachments,
3954
3957
  replyTo: route.messageId
3955
3958
  });
3956
3959
  let result = await sendReply(replySendTo);
3957
- if (to === undefined && result.delivered === false && result.reasonCode === "session_not_found" && route.senderName !== undefined) {
3960
+ if (to === undefined && replyTo === undefined && result.delivered === false && result.reasonCode === "session_not_found" && route.senderName !== undefined) {
3958
3961
  result = await sendReply(route.senderName);
3959
3962
  }
3960
3963
  if (result.queued === true) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -67,11 +67,11 @@ intercom({
67
67
 
68
68
  ### Pattern 2: Quick Status Check
69
69
 
70
- Before sending, verify who's connected. The full session ID printed by `list` is directly usable by `send`, `ask`, and targeted `reply`:
70
+ Before sending, verify who's connected. Each list row leads with a copyable full session ID or canonical workflow path. Names are secondary; redundant generated aliases are omitted from the list but remain valid targets.
71
71
 
72
72
  ```typescript
73
73
  intercom({ action: "list" })
74
- // → planner (6332faab-1111-4222-8333-123456789abc) /workspace (model) [idle]
74
+ // → - `6332faab-1111-4222-8333-123456789abc` [idle] /workspace (model) name: planner
75
75
  intercom({ action: "ask", to: "6332faab-1111-4222-8333-123456789abc", message: "Which option should I use?" })
76
76
  ```
77
77
 
@@ -128,7 +128,7 @@ intercom({ action: "pending" })
128
128
  intercom({ action: "reply", to: "planner", message: "Use exponential backoff starting at 100ms." })
129
129
  ```
130
130
 
131
- `reply` still preserves exact threading under the hood by sending the response with the original `replyTo` value.
131
+ Explicit `to` selects that sender's pending ask even if another message triggered the current turn. Use `pending` and an exact `replyTo` when the sender has several asks. Stale, unknown, empty, or sender-mismatched explicit selectors fail without replying to another thread. Omit both selectors only when you intend to reply to the active message, or otherwise the single pending ask.
132
132
 
133
133
  ### Pattern 4: Broadcast to Multiple Workers
134
134
 
@@ -124,7 +124,7 @@ export type ClientMessage =
124
124
  | { type: "join_group"; requestId: string; group: string }
125
125
  | { type: "leave_group"; requestId: string; group?: string }
126
126
  | { type: "authorize_supervisor"; requestId: string; childName: string; capability?: string }
127
- | { type: "send" | "supervisor_send"; to: string; logicalTarget?: string; requirePendingReply?: true; resolveReplyTarget?: true; message: Message; attemptId?: string }
127
+ | { type: "send" | "supervisor_send"; to: string; logicalTarget?: string; requirePendingReply?: true; expectedRecipientId?: string; resolveReplyTarget?: true; message: Message; attemptId?: string }
128
128
  | {
129
129
  type: "send_pending_stage_notification";
130
130
  runId: string;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.19-alpha.9] - 2026-09-12
8
+
9
+ ### Fixed
10
+
11
+ - `fetch_content` ignores video frame options for non-video inputs, so webpages and mixed batches no longer fail with video-only errors. Tool and parameter descriptions now distinguish ordinary page fetching, video analysis, and frame extraction.
12
+
7
13
  ## [0.9.19-alpha.7] - 2026-09-12
8
14
 
9
15
  ### Fixed
@@ -74476,13 +74476,15 @@ async function extractContent(url, signal, options) {
74476
74476
  if (signal?.aborted) {
74477
74477
  return { url, title: "", content: "", error: "Aborted" };
74478
74478
  }
74479
- const frameResult = await extractRequestedFrames(url, options, signal);
74480
- if (frameResult)
74481
- return frameResult;
74482
74479
  const localVideo = safeVideoInfo2(url);
74483
74480
  if (localVideo.error) {
74484
74481
  return { url, title: "", content: "", error: localVideo.error };
74485
74482
  }
74483
+ if (localVideo.info || isYouTubeURL(url).isYouTube) {
74484
+ const frameResult = await extractRequestedFrames(url, options, signal);
74485
+ if (frameResult)
74486
+ return frameResult;
74487
+ }
74486
74488
  if (localVideo.info) {
74487
74489
  try {
74488
74490
  const result = await extractVideo(localVideo.info, signal, options);
@@ -90949,29 +90951,29 @@ function registerContentTools(pi, deps) {
90949
90951
  pi.registerTool({
90950
90952
  name: "fetch_content",
90951
90953
  label: "Fetch Content",
90952
- description: "Fetch URL(s) and extract readable content as markdown. Supports YouTube video transcripts (with thumbnail), GitHub repository contents, and local video files (with frame thumbnail). Video frames can be extracted via timestamp/range or sampled across the entire video with frames alone. Falls back to Gemini for pages that block bots or fail Readability extraction. For YouTube and video files: ALWAYS pass the user's specific question via the prompt parameter this directs the AI to focus on that aspect of the video, producing much better results than a generic extraction. Content is always stored and can be retrieved with get_search_content.",
90953
- promptSnippet: "Use to extract readable content from URL(s), YouTube, GitHub repos, or local videos. For video questions, pass the user's exact question in prompt.",
90954
+ description: `Fetch webpages, PDFs, GitHub repositories, YouTube videos, or local video files. For ordinary pages, pass only {"urls":["https://example.com"]}; readable content is returned as markdown. For video analysis, include the user's question in prompt. Optional frames and timestamp extract images only from YouTube or local videos and are ignored for other inputs, including in mixed batches. Content is stored for get_search_content. Blocked or unreadable pages use extraction fallbacks.`,
90955
+ promptSnippet: "Fetch webpages with urls only. For YouTube or local video analysis, include the user's question in prompt; omit frames and timestamp unless images are wanted.",
90954
90956
  parameters: Type2.Object({
90955
90957
  urls: Type2.Array(Type2.String({ minLength: 1 }), {
90956
90958
  minItems: 1,
90957
90959
  description: 'URLs or local video paths to fetch. Always use an array, even for one URL: {"urls":["https://example.com"]}. Multiple URLs are fetched in parallel.'
90958
90960
  }),
90959
90961
  forceClone: Type2.Optional(Type2.Boolean({
90960
- description: "Force cloning large GitHub repositories that exceed the size threshold"
90962
+ description: "GitHub repositories only: allow cloning above the configured size threshold. Omit for webpages and videos."
90961
90963
  })),
90962
90964
  prompt: Type2.Optional(Type2.String({
90963
- description: "Question or instruction for video analysis (YouTube and video files). Pass the user's specific question here — e.g. 'describe the book shown at the advice for beginners section'. Without this, a generic transcript extraction is used which may miss what the user is asking about."
90965
+ description: "YouTube/local video analysis only: the user's specific question. Omit for webpages; this is not a webpage search or extraction filter."
90964
90966
  })),
90965
90967
  timestamp: Type2.Optional(Type2.String({
90966
- description: "Extract video frame(s) at a timestamp or time range. Single: '1:23:45', '23:45', or '85' (seconds). Range: '23:41-25:00' extracts evenly-spaced frames across that span (default 6). Use frames with ranges to control density; single+frames uses a fixed 5s interval. YouTube requires yt-dlp + ffmpeg; local videos require ffmpeg. Use a range when you know the approximate area but not the exact moment — you'll get a contact sheet to visually identify the right frame."
90968
+ description: "YouTube/local video images only: seconds ('85'), time ('1:25'), or range ('1:25-2:00'). Omit for text/transcripts. Ignored for non-video inputs. Ranges default to 6 frames; use frames to adjust."
90967
90969
  })),
90968
90970
  frames: Type2.Optional(Type2.Integer({
90969
90971
  minimum: 1,
90970
90972
  maximum: 12,
90971
- description: "Number of frames to extract. Use with timestamp range for custom density, with single timestamp to get N frames at 5s intervals, or alone to sample across the entire video. Requires yt-dlp + ffmpeg for YouTube, ffmpeg for local video."
90973
+ description: "YouTube/local video images only: 1-12 frames. Alone, samples the whole video; with a range, samples that span; with a single timestamp, uses 5s intervals. Ignored for non-video inputs. Omit for text/transcripts. Requires ffmpeg, plus yt-dlp for YouTube."
90972
90974
  })),
90973
90975
  model: Type2.Optional(Type2.String({
90974
- description: "Override the Gemini model for video/YouTube analysis (e.g. 'gemini-2.5-flash', 'gemini-3-flash-preview'). Defaults to config or gemini-3-flash-preview."
90976
+ description: "Gemini model override for YouTube/local video analysis only. Omit to use the configured default; does not select the webpage extraction model."
90975
90977
  }))
90976
90978
  }, { additionalProperties: false }),
90977
90979
  async execute(_toolCallId, params, signal, onUpdate) {
@@ -97598,18 +97600,18 @@ function webAccess(pi) {
97598
97600
  pi.registerTool({
97599
97601
  name: "fetch_content",
97600
97602
  label: "Fetch Content",
97601
- description: "Fetch URL(s) and extract readable content as markdown. Supports YouTube video transcripts (with thumbnail), GitHub repository contents, and local video files (with frame thumbnail). Video frames can be extracted via timestamp/range or sampled across the entire video with frames alone. Falls back to Gemini for pages that block bots or fail Readability extraction. For YouTube and video files: ALWAYS pass the user's specific question via the prompt parameter this directs the AI to focus on that aspect of the video, producing much better results than a generic extraction. Content is always stored and can be retrieved with get_search_content.",
97602
- promptSnippet: "Use to extract readable content from URL(s), YouTube, GitHub repos, or local videos. For video questions, pass the user's exact question in prompt.",
97603
+ description: `Fetch webpages, PDFs, GitHub repositories, YouTube videos, or local video files. For ordinary pages, pass only {"urls":["https://example.com"]}; readable content is returned as markdown. For video analysis, include the user's question in prompt. Optional frames and timestamp extract images only from YouTube or local videos and are ignored for other inputs, including in mixed batches. Content is stored for get_search_content. Blocked or unreadable pages use extraction fallbacks.`,
97604
+ promptSnippet: "Fetch webpages with urls only. For YouTube or local video analysis, include the user's question in prompt; omit frames and timestamp unless images are wanted.",
97603
97605
  parameters: Type4.Object({
97604
97606
  urls: Type4.Array(Type4.String({ minLength: 1 }), {
97605
97607
  minItems: 1,
97606
97608
  description: 'URLs or local video paths to fetch. Always use an array, even for one URL: {"urls":["https://example.com"]}. Multiple URLs are fetched in parallel.'
97607
97609
  }),
97608
- forceClone: Type4.Optional(Type4.Boolean({ description: "Force cloning large GitHub repositories that exceed the size threshold" })),
97609
- prompt: Type4.Optional(Type4.String({ description: "Question or instruction for video analysis (YouTube and video files)." })),
97610
- timestamp: Type4.Optional(Type4.String({ description: "Extract video frame(s) at a timestamp or time range." })),
97611
- frames: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 12, description: "Number of frames to extract." })),
97612
- model: Type4.Optional(Type4.String({ description: "Override the Gemini model for video/YouTube analysis." }))
97610
+ forceClone: Type4.Optional(Type4.Boolean({ description: "GitHub repositories only: allow cloning above the configured size threshold. Omit for webpages and videos." })),
97611
+ prompt: Type4.Optional(Type4.String({ description: "YouTube/local video analysis only: the user's specific question. Omit for webpages; this is not a webpage search or extraction filter." })),
97612
+ timestamp: Type4.Optional(Type4.String({ description: "YouTube/local video images only: seconds ('85'), time ('1:25'), or range ('1:25-2:00'). Omit for text/transcripts. Ignored for non-video inputs. Ranges default to 6 frames; use frames to adjust." })),
97613
+ frames: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 12, description: "YouTube/local video images only: 1-12 frames. Alone, samples the whole video; with a range, samples that span; with a single timestamp, uses 5s intervals. Ignored for non-video inputs. Omit for text/transcripts. Requires ffmpeg, plus yt-dlp for YouTube." })),
97614
+ model: Type4.Optional(Type4.String({ description: "Gemini model override for YouTube/local video analysis only. Omit to use the configured default; does not select the webpage extraction model." }))
97613
97615
  }, { additionalProperties: false }),
97614
97616
  execute: (...args) => executeHeavyTool(loadHeavy, "fetch_content", args),
97615
97617
  renderResult: (...args) => renderHeavyToolResult(loadedHeavy?.heavy ?? null, "fetch_content", args)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.19-alpha.9] - 2026-09-12
10
+
11
+ ### Fixed
12
+
13
+ - Kept the background workflow summary within narrow terminal widths, including 27 columns. The shared workflow widget now stays within ten rows, shrinks on short terminals, and scrolls one row at a time with Alt+PageUp / Alt+PageDown without taking focus from the editor. All workflow rows remain reachable when a multiline draft reduces the visible widget area ([#3015](https://github.com/bastani-inc/atomic/issues/3015)).
14
+ - Kept the workflow being read in view when live runs are inserted or removed above it, including workflows with identical names. Resizing to a collapsed summary and back preserves the reading position; removing the viewed run selects its next surviving neighbour, or the previous one when no later run remains ([#3017](https://github.com/bastani-inc/atomic/pull/3017)).
15
+
9
16
  ## [0.9.19-alpha.8] - 2026-09-12
10
17
 
11
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -79093,7 +79093,9 @@ function plainCollapsed(counts, activeTools) {
79093
79093
  const tools = activeTools > 0 ? ` · ${activeTools} tool${activeTools === 1 ? "" : "s"}` : "";
79094
79094
  return ` ▾ ${total} background · ${counts.active} ●${paused}${quit}${blocked}${tools}`;
79095
79095
  }
79096
- function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79096
+ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now(), layout) {
79097
+ if (layout)
79098
+ layout.runs = [];
79097
79099
  const display = selectDisplayRuns(snap, now);
79098
79100
  if (display.length === 0)
79099
79101
  return [];
@@ -79112,7 +79114,7 @@ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79112
79114
  const activeTools = display.filter(isActive).reduce((total, run) => total + activeToolNodes(run).length, 0);
79113
79115
  if (width < COLLAPSED_BREAKPOINT_COLS) {
79114
79116
  return [
79115
- themed ? themedCollapsed(visibleCounts, activeTools, graphTheme) : plainCollapsed(visibleCounts, activeTools)
79117
+ truncateToWidth2(themed ? themedCollapsed(visibleCounts, activeTools, graphTheme) : plainCollapsed(visibleCounts, activeTools), width, "…")
79116
79118
  ];
79117
79119
  }
79118
79120
  const total = display.length;
@@ -79126,6 +79128,11 @@ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79126
79128
  const run = display[i];
79127
79129
  const runLines = themed ? themedRunLines(run, now, graphTheme, snap.runs, width, expandGraph) : plainRunLines(run, now, snap.runs, width, expandGraph);
79128
79130
  body.push(...runLines);
79131
+ layout?.runs.push({
79132
+ id: run.id,
79133
+ start: body.length - runLines.length + 1,
79134
+ end: body.length + (i === display.length - 1 ? 2 : 1)
79135
+ });
79129
79136
  if (i < display.length - 1)
79130
79137
  body.push("");
79131
79138
  }
@@ -79138,7 +79145,95 @@ function buildThemedWidgetLines(snap, piTheme, width = 120, now = Date.now()) {
79138
79145
  });
79139
79146
  }
79140
79147
 
79148
+ // dist/builtin/workflows/src/tui/widget-viewport.ts
79149
+ import { ScrollableComponentViewport } from "@bastani/atomic";
79150
+ var WORKFLOW_WIDGET_MAX_ROWS = 10;
79151
+
79152
+ class WorkflowWidgetViewport {
79153
+ content;
79154
+ terminalRows;
79155
+ requestRender;
79156
+ getRunRows;
79157
+ viewport = new ScrollableComponentViewport;
79158
+ lines = [];
79159
+ scrollable = false;
79160
+ runRows = [];
79161
+ constructor(content, terminalRows, requestRender, getRunRows) {
79162
+ this.content = content;
79163
+ this.terminalRows = terminalRows;
79164
+ this.requestRender = requestRender;
79165
+ this.getRunRows = getRunRows;
79166
+ this.viewport.setComponents([{ render: () => this.lines, invalidate() {} }]);
79167
+ this.viewport.scrollTo(0);
79168
+ }
79169
+ scroll(direction) {
79170
+ if (!this.scrollable)
79171
+ return;
79172
+ this.viewport.scrollBy(direction);
79173
+ this.requestRender();
79174
+ }
79175
+ render(width) {
79176
+ const cap = Math.max(1, Math.min(WORKFLOW_WIDGET_MAX_ROWS, Math.floor(this.terminalRows() / 3)));
79177
+ const nextLines = this.content.render(width);
79178
+ const nextRunRows = this.getRunRows?.() ?? [];
79179
+ if (this.getRunRows && nextLines.length === 1 && nextRunRows.length === 0) {
79180
+ this.scrollable = false;
79181
+ return nextLines;
79182
+ }
79183
+ const previousLineCount = this.lines.length;
79184
+ const offset = this.viewport.getMaxScroll() - this.viewport.getScrollFromBottom();
79185
+ const anchoredOffset = this.getRunRows ? this.resolveAnchor(offset, nextRunRows) : undefined;
79186
+ this.lines = nextLines;
79187
+ this.runRows = nextRunRows;
79188
+ if (anchoredOffset !== undefined) {
79189
+ this.viewport.scrollTo(anchoredOffset);
79190
+ } else if (previousLineCount <= 1 || this.lines.length < previousLineCount && this.lines.length <= cap) {
79191
+ this.viewport.scrollTo(0);
79192
+ }
79193
+ this.scrollable = this.lines.length > 1;
79194
+ this.viewport.setVisibleRows(1);
79195
+ this.viewport.render(width);
79196
+ if (!this.scrollable)
79197
+ return this.lines;
79198
+ const first = this.viewport.getMaxScroll() - this.viewport.getScrollFromBottom() + 1;
79199
+ const visible = this.lines.slice(first - 1, first - 1 + Math.max(1, cap - 1));
79200
+ if (cap === 1)
79201
+ return visible;
79202
+ const hint = ` ${first}–${first + visible.length - 1}/${this.lines.length} · Alt+PgUp/PgDn scroll workflows`;
79203
+ return [...visible, truncateToWidth2(hint, width, "…")];
79204
+ }
79205
+ resolveAnchor(offset, next) {
79206
+ if (offset === 0 || next.length === 0)
79207
+ return 0;
79208
+ const index = this.runRows.findIndex((run) => offset >= run.start - 1 && offset < run.end);
79209
+ const anchor = this.runRows[index];
79210
+ if (!anchor)
79211
+ return 0;
79212
+ const byId = new Map(next.map((run) => [run.id, run]));
79213
+ const retained = byId.get(anchor.id);
79214
+ if (retained)
79215
+ return Math.min(retained.end - 1, retained.start + offset - anchor.start);
79216
+ const neighbours = [...this.runRows.slice(index + 1), ...this.runRows.slice(0, index).reverse()];
79217
+ for (const neighbour of neighbours) {
79218
+ const survivor = byId.get(neighbour.id);
79219
+ if (survivor)
79220
+ return survivor.start;
79221
+ }
79222
+ return 0;
79223
+ }
79224
+ invalidate() {
79225
+ this.content.invalidate?.();
79226
+ }
79227
+ dispose() {
79228
+ this.content.dispose?.();
79229
+ }
79230
+ }
79231
+
79141
79232
  // dist/builtin/workflows/src/tui/store-widget-installer.ts
79233
+ var widgetViewports = new WeakMap;
79234
+ function scrollStoreWidget(storeInstance, direction) {
79235
+ widgetViewports.get(storeInstance)?.scroll(direction);
79236
+ }
79142
79237
  var defaultTimerApi = {
79143
79238
  setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
79144
79239
  clearTimeout: (handle) => clearTimeout(handle)
@@ -79169,9 +79264,19 @@ function installStoreWidget(pi, storeInstance, timers = defaultTimerApi) {
79169
79264
  const setWidget = ui.setWidget;
79170
79265
  const requestRender = ui.requestRender;
79171
79266
  const onWidgetRelease = ui.onWidgetRelease;
79267
+ const layout = { runs: [] };
79172
79268
  const controller = installReactiveWidget({
79173
79269
  ui: {
79174
- setWidget: (key, factory, opts) => setWidget.call(ui, key, factory, opts),
79270
+ setWidget: (key, factory, opts) => {
79271
+ if (!factory)
79272
+ widgetViewports.delete(storeInstance);
79273
+ setWidget.call(ui, key, factory ? (tui, theme) => {
79274
+ const host = tui;
79275
+ const viewport = new WorkflowWidgetViewport(factory(tui, theme), () => host?.terminal?.rows ?? 30, () => requestRender ? requestRender.call(ui) : host?.requestRender?.(), () => layout.runs);
79276
+ widgetViewports.set(storeInstance, viewport);
79277
+ return viewport;
79278
+ } : undefined, opts);
79279
+ },
79175
79280
  ...requestRender ? { requestRender: () => requestRender.call(ui) } : {},
79176
79281
  ...onWidgetRelease ? { onWidgetRelease: (key, listener) => onWidgetRelease.call(ui, key, listener) } : {}
79177
79282
  },
@@ -79181,7 +79286,7 @@ function installStoreWidget(pi, storeInstance, timers = defaultTimerApi) {
79181
79286
  getSnapshot: () => liveWidgetSnapshot(storeInstance),
79182
79287
  subscribe: (listener) => subscribeStoreInvalidation(storeInstance, listener),
79183
79288
  getPreviewLines: (snap, now) => buildThemedWidgetLines(snap, undefined, 120, now),
79184
- render: (snap, { theme, width, now }) => buildThemedWidgetLines(snap, theme, width, now),
79289
+ render: (snap, { theme, width, now }) => buildThemedWidgetLines(snap, theme, width, now, layout),
79185
79290
  getNextRefreshDelayMs: (snap, now) => nextWidgetRefreshDelayMs(snap, now),
79186
79291
  requestRenderOnStateNoop: false,
79187
79292
  isStaleError: isStaleExtensionContextError4,
@@ -79189,7 +79294,10 @@ function installStoreWidget(pi, storeInstance, timers = defaultTimerApi) {
79189
79294
  reportWidgetFailure(ui, `Workflow progress widget could not mount: ${error.message}`);
79190
79295
  }
79191
79296
  });
79192
- return () => controller.dispose();
79297
+ return () => {
79298
+ widgetViewports.delete(storeInstance);
79299
+ controller.dispose();
79300
+ };
79193
79301
  }
79194
79302
  function installToolExecutionHooks(pi, storeInstance) {
79195
79303
  const eventBusOn = pi.events?.on;
@@ -117735,6 +117843,14 @@ function registerWorkflowShortcut(pi, overlay) {
117735
117843
  description: "Open workflow orchestrator pane",
117736
117844
  handler: openPane
117737
117845
  });
117846
+ pi.registerShortcut("alt+pageUp", {
117847
+ description: "Scroll background workflows up",
117848
+ handler: () => scrollStoreWidget(store, -1)
117849
+ });
117850
+ pi.registerShortcut("alt+pageDown", {
117851
+ description: "Scroll background workflows down",
117852
+ handler: () => scrollStoreWidget(store, 1)
117853
+ });
117738
117854
  }
117739
117855
  function registerIntercomControl(pi, intercomControlRef) {
117740
117856
  intercomControlRef.current = subscribeIntercomControl(pi, buildIntercomCallbacks({
@@ -53,6 +53,8 @@ Sent and received messages are recorded in session history as `intercom_sent` /
53
53
 
54
54
  Live-session lookup accepts only an exact full Intercom session ID or an exact case-insensitive session name. Workflow stages use the canonical `workflow:<rootRunId>/<segment>[/<segment>...]` path printed by `intercom list` and workflow status surfaces; an exact target works while the row is `PENDING` and after it becomes `RUNNING`. Each segment may be a stage name, run id, or glob: `*` matches one segment and may be embedded, while `**` matches any depth. Status surfaces label pending stages whose pre-start delivery capability is unavailable without presenting a usable target and never advertise a retained pending stage after its run terminates. The `sessionId` shown by `workflow status` belongs to the workflow SDK and is **not** an Intercom target.
55
55
 
56
+ Agent list rows put the copyable exact ID or canonical workflow path first, followed by status and working directory. Meaningful names remain secondary metadata; redundant generated aliases are hidden only in the list, not removed from lookup. Pending and future workflow rows retain lifecycle and queued counts. Copy the target rather than reconstructing it from a name.
57
+
56
58
  Known non-agent IDs, names, and workflow paths are refused rather than delivered or queued for a future agent. This includes run-level `ctx.ui` prompts, synthetic prompt stages (including retained completed prompts), and `ctx.tool` nodes. Knowing an internal connection's ID does not bypass this broker policy, and supervisor delivery cannot bypass it either. Workflow patterns and `workflow:<rootRunId>/**` still queue for future agent stages, but never deliver to prompt/tool nodes or routing connections.
57
59
 
58
60
  This refusal also covers nested paths using boundary-stage names or IDs, mixed with materialized run-ID segments. The same spellings still resolve genuine agent stages.
@@ -100,6 +102,8 @@ An `idle` registration does not guarantee reply capability. Completed, failed, i
100
102
 
101
103
  **`reply`** is receiver-side sugar for replying to an inbound ask. In the turn triggered by an incoming intercom message, `intercom({ action: "reply", message: "..." })` targets that exact sender and message automatically. If you reply later, it falls back to the single unresolved inbound ask; with multiple pending asks, use `pending` and pass `to`, or pass the listed message ID as `replyTo` to disambiguate multiple asks from the same sender. Under the hood this is still a normal `send` with the exact `replyTo` value.
102
104
 
105
+ Explicit selectors override the active turn: `to` selects a pending ask from that exact name/full session ID, and `replyTo` selects only that pending ask or the exact active ordinary message. Stale, unknown, or empty thread IDs fail without falling back. If both selectors are supplied, the sender must match the thread. If an asker is still waiting, inspect `pending` and reply using its exact message ID rather than replying implicitly to an unrelated notification. Multiple asks from the same sender require `replyTo`.
106
+
103
107
  ### Attachments
104
108
 
105
109
  `send`, `ask`, and `reply` accept an `attachments` array of `{ type, name, content, language? }` objects where `type` is `"file"`, `"snippet"`, or `"context"`. Attachment content is included in the recipient's agent-visible message body. When a parent-targeted foreground `ask` is terminally handed off at the source, the same ordered attachment array is retained and rendered with the question for the launching parent; duplicate names and content are not rewritten. Attachments are supported in the protocol but not in the ALT+M compose overlay.
package/docs/intercom.md CHANGED
@@ -81,10 +81,10 @@ The agent can list sessions and send messages using the `intercom` tool. Tool ca
81
81
  ```typescript
82
82
  // List active sessions
83
83
  intercom({ action: "list" })
84
- // → **Current session:**
85
- // → executor (20d43841-1111-4222-8333-123456789abc) ~/projects/api (claude-sonnet-4) [self, idle]
86
- // → **Other sessions:**
87
- // → research (6332faab-1111-4222-8333-123456789abc) ~/projects/api (claude-sonnet-4) [same cwd, thinking]
84
+ // → **Current session** (groups: default):
85
+ // → - `20d43841-1111-4222-8333-123456789abc` [self, idle] ~/projects/api (claude-sonnet-4) name: executor
86
+ // → **Other visible sessions and workflow stages:**
87
+ // → - `6332faab-1111-4222-8333-123456789abc` [same cwd, thinking] ~/projects/api (claude-sonnet-4) name: research
88
88
 
89
89
  // Send a message
90
90
  intercom({ action: "send", to: "research", message: "Check if UserService.validate() handles null" })
@@ -22,6 +22,16 @@ For a video, include the question in `prompt`:
22
22
  {"urls": ["/path/to/recording.mp4"], "prompt": "What error appears on screen?"}
23
23
  ```
24
24
 
25
+ For webpages, `urls` is enough. `prompt` and `model` apply to video analysis, not webpage filtering or extraction. `forceClone` applies only to GitHub repositories.
26
+
27
+ Use `frames` and `timestamp` only when you want images from YouTube or local video files. Omit both for readable text or transcripts. `frames` alone samples the whole video; `timestamp` accepts seconds, a time such as `1:25`, or a range such as `1:25-2:00`.
28
+
29
+ ```json
30
+ {"urls": ["/path/to/recording.mp4"], "timestamp": "1:25-2:00", "frames": 3}
31
+ ```
32
+
33
+ Frame extraction requires ffmpeg, plus yt-dlp for YouTube. Non-video inputs ignore `frames` and `timestamp` and are fetched normally, even in a batch containing videos. Video inputs still validate timestamps and report extraction errors.
34
+
25
35
  Existing prompts or integrations using `{"url": "..."}` must change to `{"urls": ["..."]}`. The singular `url` field is no longer accepted by `fetch_content`. This does not change the `url` selector on `get_search_content`.
26
36
 
27
37
  Atomic can normalize a scalar `urls` string into a one-item array. Always use the documented array form in prompts and integrations; the legacy `url` field is not normalized.
@@ -27,6 +27,10 @@ Send material updates through Intercom to every affected workflow stage, includi
27
27
 
28
28
  At 80 columns and wider, each `BACKGROUND` card keeps the full run identity and preserves its mode, progress, live-tool details, and elapsed/status metadata. When the remaining single-row budget permits, it adds bounded pending-stage details: a target is either shown exactly or replaced by a `stage`-labeled canonical ID, and `… N more` reports omitted pending stages. If no bounded pending-stage form fits, the pending label is omitted entirely rather than displacing the existing metadata. Tool nodes are read-only durable graph nodes, not attachable stage chats. Below 80 columns, the panel keeps its aggregate collapsed form and omits run IDs, stage identities, targets, and tool names.
29
29
 
30
+ The shared background widget occupies at most **10 rows**, including its scroll hint, and at most one third of the terminal height, rounded down with a one-row minimum. From the main editor, use **Alt+PageUp** and **Alt+PageDown** to scroll one row at a time. Every row remains reachable even when a multiline draft leaves room for only one widget row. The row range at the bottom shows your position when there is room for the hint. Ordinary typing, arrows, PageUp/PageDown, and mouse scrolling keep their existing editor and transcript behavior.
31
+
32
+ Below 80 columns the widget remains a single count line, truncated to fit when necessary; widen the terminal to scroll the cards or use `/workflow connect` to inspect runs at narrow widths.
33
+
30
34
  The panel's stage progress includes recursively nested child workflows and updates as new stages appear. The numerator counts completed, failed, and skipped stages; the denominator counts all currently materialized stages, not future work. Expanded children replace their workflow boundary rather than counting both, and durable tool nodes are not stages. A failed, skipped, missing, or invalid child expansion keeps its boundary summary. `single`/`chain` follows this same stage count.
31
35
 
32
36
  For chat surfaces such as workflow status, run detail, dispatch confirmation, and the run picker, a full id wraps onto continuation rows when the card is narrower than the id. Pending-stage targets in run detail use the same rule: the exact address wraps instead of being ellipsized, and narrow status cards wrap the canonical stage ID or drop its display-name decoration rather than rendering a partial ID. The renderer keeps the card border closed at its minimum layout width, while terminals below that floor — including sub-30-column terminals — can hard-clip the box. An awaiting-input attribution banner is titled `AWAITING INPUT` and contains the same two identity rows — `?` plus the full run id, then the workflow name and optional metadata — while the existing prompt question and options remain below it in the normal prompt UI.
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bastani/atomic",
9
- "version": "0.9.19-alpha.8",
9
+ "version": "0.9.19-alpha.9",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@bastani/atomic-natives": "0.9.19-alpha.8",
13
- "@bastani/pi-ai": "0.9.19-alpha.8",
12
+ "@bastani/atomic-natives": "0.9.19-alpha.9",
13
+ "@bastani/pi-ai": "0.9.19-alpha.9",
14
14
  "@dbos-inc/dbos-sdk": "4.25.14",
15
15
  "@earendil-works/pi-agent-core": "0.85.1",
16
16
  "@earendil-works/pi-client": "0.85.1",
@@ -518,18 +518,18 @@
518
518
  }
519
519
  },
520
520
  "node_modules/@bastani/atomic-natives": {
521
- "version": "0.9.19-alpha.8",
522
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.19-alpha.8.tgz",
521
+ "version": "0.9.19-alpha.9",
522
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.19-alpha.9.tgz",
523
523
  "license": "MIT",
524
524
  "optionalDependencies": {
525
- "@bastani/atomic-natives-darwin-arm64": "0.9.19-alpha.8",
526
- "@bastani/atomic-natives-darwin-x64": "0.9.19-alpha.8",
527
- "@bastani/atomic-natives-linux-arm64-gnu": "0.9.19-alpha.8",
528
- "@bastani/atomic-natives-linux-arm64-musl": "0.9.19-alpha.8",
529
- "@bastani/atomic-natives-linux-x64-gnu": "0.9.19-alpha.8",
530
- "@bastani/atomic-natives-linux-x64-musl": "0.9.19-alpha.8",
531
- "@bastani/atomic-natives-win32-arm64-msvc": "0.9.19-alpha.8",
532
- "@bastani/atomic-natives-win32-x64-msvc": "0.9.19-alpha.8"
525
+ "@bastani/atomic-natives-darwin-arm64": "0.9.19-alpha.9",
526
+ "@bastani/atomic-natives-darwin-x64": "0.9.19-alpha.9",
527
+ "@bastani/atomic-natives-linux-arm64-gnu": "0.9.19-alpha.9",
528
+ "@bastani/atomic-natives-linux-arm64-musl": "0.9.19-alpha.9",
529
+ "@bastani/atomic-natives-linux-x64-gnu": "0.9.19-alpha.9",
530
+ "@bastani/atomic-natives-linux-x64-musl": "0.9.19-alpha.9",
531
+ "@bastani/atomic-natives-win32-arm64-msvc": "0.9.19-alpha.9",
532
+ "@bastani/atomic-natives-win32-x64-msvc": "0.9.19-alpha.9"
533
533
  },
534
534
  "engines": {
535
535
  "bun": ">=1.4.2",
@@ -537,8 +537,8 @@
537
537
  }
538
538
  },
539
539
  "node_modules/@bastani/atomic-natives-darwin-arm64": {
540
- "version": "0.9.19-alpha.8",
541
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.19-alpha.8.tgz",
540
+ "version": "0.9.19-alpha.9",
541
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.19-alpha.9.tgz",
542
542
  "license": "MIT",
543
543
  "os": [
544
544
  "darwin"
@@ -549,8 +549,8 @@
549
549
  "optional": true
550
550
  },
551
551
  "node_modules/@bastani/atomic-natives-darwin-x64": {
552
- "version": "0.9.19-alpha.8",
553
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.19-alpha.8.tgz",
552
+ "version": "0.9.19-alpha.9",
553
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.19-alpha.9.tgz",
554
554
  "license": "MIT",
555
555
  "os": [
556
556
  "darwin"
@@ -561,8 +561,8 @@
561
561
  "optional": true
562
562
  },
563
563
  "node_modules/@bastani/atomic-natives-linux-arm64-gnu": {
564
- "version": "0.9.19-alpha.8",
565
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.19-alpha.8.tgz",
564
+ "version": "0.9.19-alpha.9",
565
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.19-alpha.9.tgz",
566
566
  "license": "MIT",
567
567
  "os": [
568
568
  "linux"
@@ -576,8 +576,8 @@
576
576
  "optional": true
577
577
  },
578
578
  "node_modules/@bastani/atomic-natives-linux-arm64-musl": {
579
- "version": "0.9.19-alpha.8",
580
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.19-alpha.8.tgz",
579
+ "version": "0.9.19-alpha.9",
580
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-musl/-/atomic-natives-linux-arm64-musl-0.9.19-alpha.9.tgz",
581
581
  "license": "MIT",
582
582
  "os": [
583
583
  "linux"
@@ -591,8 +591,8 @@
591
591
  "optional": true
592
592
  },
593
593
  "node_modules/@bastani/atomic-natives-linux-x64-gnu": {
594
- "version": "0.9.19-alpha.8",
595
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.19-alpha.8.tgz",
594
+ "version": "0.9.19-alpha.9",
595
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.19-alpha.9.tgz",
596
596
  "license": "MIT",
597
597
  "os": [
598
598
  "linux"
@@ -606,8 +606,8 @@
606
606
  "optional": true
607
607
  },
608
608
  "node_modules/@bastani/atomic-natives-linux-x64-musl": {
609
- "version": "0.9.19-alpha.8",
610
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.19-alpha.8.tgz",
609
+ "version": "0.9.19-alpha.9",
610
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-musl/-/atomic-natives-linux-x64-musl-0.9.19-alpha.9.tgz",
611
611
  "license": "MIT",
612
612
  "os": [
613
613
  "linux"
@@ -621,8 +621,8 @@
621
621
  "optional": true
622
622
  },
623
623
  "node_modules/@bastani/atomic-natives-win32-arm64-msvc": {
624
- "version": "0.9.19-alpha.8",
625
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.19-alpha.8.tgz",
624
+ "version": "0.9.19-alpha.9",
625
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.19-alpha.9.tgz",
626
626
  "license": "MIT",
627
627
  "os": [
628
628
  "win32"
@@ -633,8 +633,8 @@
633
633
  "optional": true
634
634
  },
635
635
  "node_modules/@bastani/atomic-natives-win32-x64-msvc": {
636
- "version": "0.9.19-alpha.8",
637
- "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.19-alpha.8.tgz",
636
+ "version": "0.9.19-alpha.9",
637
+ "resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.19-alpha.9.tgz",
638
638
  "license": "MIT",
639
639
  "os": [
640
640
  "win32"
@@ -645,8 +645,8 @@
645
645
  "optional": true
646
646
  },
647
647
  "node_modules/@bastani/pi-ai": {
648
- "version": "0.9.19-alpha.8",
649
- "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.19-alpha.8.tgz",
648
+ "version": "0.9.19-alpha.9",
649
+ "resolved": "https://registry.npmjs.org/@bastani/pi-ai/-/pi-ai-0.9.19-alpha.9.tgz",
650
650
  "license": "MIT",
651
651
  "dependencies": {
652
652
  "@anthropic-ai/sdk": "0.124.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.9.19-alpha.8",
3
+ "version": "0.9.19-alpha.9",
4
4
  "description": "Atomic coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "atomicConfig": {
@@ -79,8 +79,8 @@
79
79
  "prepublishOnly": "bun run clean && bun run build && bun run shrinkwrap"
80
80
  },
81
81
  "dependencies": {
82
- "@bastani/atomic-natives": "0.9.19-alpha.8",
83
- "@bastani/pi-ai": "0.9.19-alpha.8",
82
+ "@bastani/atomic-natives": "0.9.19-alpha.9",
83
+ "@bastani/pi-ai": "0.9.19-alpha.9",
84
84
  "@dbos-inc/dbos-sdk": "4.25.14",
85
85
  "@earendil-works/pi-agent-core": "0.85.1",
86
86
  "@earendil-works/pi-client": "0.85.1",