switchroom 0.15.44 → 0.16.4
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/dist/agent-scheduler/index.js +122 -88
- package/dist/auth-broker/index.js +463 -177
- package/dist/cli/autoaccept-poll.js +4842 -35
- package/dist/cli/drive-write-pretool.mjs +17 -14
- package/dist/cli/notion-write-pretool.mjs +117 -86
- package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
- package/dist/cli/self-improve-stop.mjs +428 -0
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +3249 -1241
- package/dist/cli/ui/index.html +1 -1
- package/dist/host-control/main.js +2833 -355
- package/dist/vault/approvals/kernel-server.js +7482 -7439
- package/dist/vault/broker/server.js +11315 -11272
- package/examples/minimal.yaml +1 -0
- package/examples/switchroom.yaml +1 -0
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +88 -1
- package/profiles/_shared/execution-discipline.md.hbs +18 -0
- package/profiles/default/CLAUDE.md.hbs +3 -22
- package/telegram-plugin/.claude-plugin/plugin.json +2 -2
- package/telegram-plugin/answer-stream-flag.ts +12 -49
- package/telegram-plugin/answer-stream.ts +5 -150
- package/telegram-plugin/auth-snapshot-format.ts +280 -48
- package/telegram-plugin/auto-fallback-fleet.ts +44 -1
- package/telegram-plugin/context-exhaustion.ts +12 -0
- package/telegram-plugin/demo-mask.ts +154 -0
- package/telegram-plugin/dist/bridge/bridge.js +167 -124
- package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
- package/telegram-plugin/dist/server.js +215 -172
- package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
- package/telegram-plugin/draft-stream.ts +47 -410
- package/telegram-plugin/final-answer-detect.ts +17 -12
- package/telegram-plugin/fleet-fallback-resume.ts +131 -0
- package/telegram-plugin/format.ts +56 -19
- package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
- package/telegram-plugin/gateway/auth-command.ts +70 -14
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
- package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
- package/telegram-plugin/gateway/current-turn-map.ts +188 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
- package/telegram-plugin/gateway/effort-command.ts +8 -3
- package/telegram-plugin/gateway/emission-authority.ts +369 -0
- package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
- package/telegram-plugin/gateway/gateway.ts +1837 -291
- package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
- package/telegram-plugin/gateway/represent-guard.ts +72 -0
- package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
- package/telegram-plugin/gateway/status-surface-log.ts +14 -3
- package/telegram-plugin/history.ts +33 -11
- package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
- package/telegram-plugin/issues-card.ts +4 -0
- package/telegram-plugin/model-unavailable.ts +124 -0
- package/telegram-plugin/narrative-dedup.ts +69 -0
- package/telegram-plugin/over-ping-safety-net.ts +70 -4
- package/telegram-plugin/package.json +3 -3
- package/telegram-plugin/pending-work-progress.ts +12 -0
- package/telegram-plugin/permission-rule.ts +32 -5
- package/telegram-plugin/permission-title.ts +152 -9
- package/telegram-plugin/quota-check.ts +13 -0
- package/telegram-plugin/quota-watch.ts +135 -7
- package/telegram-plugin/registry/turns-schema.test.ts +24 -0
- package/telegram-plugin/registry/turns-schema.ts +9 -0
- package/telegram-plugin/runtime-metrics.ts +13 -0
- package/telegram-plugin/session-tail.ts +96 -11
- package/telegram-plugin/silence-poke.ts +170 -24
- package/telegram-plugin/slot-banner-driver.ts +3 -0
- package/telegram-plugin/status-no-truncate.ts +44 -0
- package/telegram-plugin/status-reactions.ts +20 -3
- package/telegram-plugin/stream-controller.ts +4 -23
- package/telegram-plugin/stream-reply-handler.ts +6 -24
- package/telegram-plugin/streaming-metrics.ts +91 -0
- package/telegram-plugin/subagent-watcher.ts +212 -66
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
- package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
- package/telegram-plugin/tests/answer-stream.test.ts +2 -411
- package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
- package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
- package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
- package/telegram-plugin/tests/demo-mask.test.ts +127 -0
- package/telegram-plugin/tests/draft-stream.test.ts +0 -827
- package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
- package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
- package/telegram-plugin/tests/feed-survival.test.ts +526 -0
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
- package/telegram-plugin/tests/history.test.ts +60 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
- package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
- package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
- package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
- package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
- package/telegram-plugin/tests/permission-rule.test.ts +17 -0
- package/telegram-plugin/tests/permission-title.test.ts +206 -17
- package/telegram-plugin/tests/quota-watch.test.ts +252 -9
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
- package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
- package/telegram-plugin/tests/represent-guard.test.ts +162 -0
- package/telegram-plugin/tests/session-tail.test.ts +147 -3
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
- package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
- package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
- package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
- package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
- package/telegram-plugin/tests/telegram-format.test.ts +101 -6
- package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
- package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
- package/telegram-plugin/tests/tool-labels.test.ts +67 -0
- package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
- package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
- package/telegram-plugin/tests/welcome-text.test.ts +32 -3
- package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
- package/telegram-plugin/tool-activity-summary.ts +375 -58
- package/telegram-plugin/turn-liveness-floor.ts +240 -0
- package/telegram-plugin/uat/assertions.ts +115 -0
- package/telegram-plugin/uat/driver.ts +68 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
- package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
- package/telegram-plugin/welcome-text.ts +13 -1
- package/telegram-plugin/worker-activity-feed.ts +157 -82
- package/telegram-plugin/draft-transport.ts +0 -122
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
- package/telegram-plugin/tests/draft-transport.test.ts +0 -211
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Each non-surface tool gets a human-friendly, present-tense line
|
|
7
7
|
* ("Reading CLAUDE.md", "Searching memory", "Running a command"); the
|
|
8
8
|
* feed renders them chronologically (oldest first, newest = the
|
|
9
|
-
* in-progress step), consecutive duplicates collapsed,
|
|
10
|
-
* most recent
|
|
9
|
+
* in-progress step), consecutive duplicates collapsed, windowed to the
|
|
10
|
+
* most recent STATUS_ROLLING_LINES with a "+N earlier…" overflow header.
|
|
11
11
|
*
|
|
12
12
|
* Two append entrypoints feed the same `lines: string[]` accumulator:
|
|
13
13
|
* - `appendActivityLabel` — for a pre-computed label from the
|
|
@@ -81,7 +81,8 @@ export function describeToolUse(
|
|
|
81
81
|
const server = mcpMatch[1].toLowerCase();
|
|
82
82
|
const tool = mcpMatch[2].toLowerCase();
|
|
83
83
|
// Surface tools ARE the conversation — never mirror them.
|
|
84
|
-
|
|
84
|
+
// Use isTelegramSurfaceTool (regex-based, key-agnostic) so forks/renames work.
|
|
85
|
+
if (isTelegramSurfaceTool(toolName)) return null;
|
|
85
86
|
if (server === "hindsight") {
|
|
86
87
|
if (tool === "recall" || tool === "reflect") return "Searching memory";
|
|
87
88
|
if (tool === "retain" || tool === "update_memory" || tool === "sync_retain")
|
|
@@ -162,10 +163,38 @@ export function describeToolUse(
|
|
|
162
163
|
// Accumulates the turn's actions into a running feed — like Claude Code's
|
|
163
164
|
// own UI — rendered into one Telegram message that edits in place and is
|
|
164
165
|
// cleared on reply. Chronological (oldest first, newest last), consecutive
|
|
165
|
-
// exact-duplicates collapsed
|
|
166
|
-
//
|
|
166
|
+
// exact-duplicates collapsed.
|
|
167
|
+
//
|
|
168
|
+
// Both this surface (agent) and the worker feed render through the single
|
|
169
|
+
// `renderStatusCard` primitive below: a rolling window of the last
|
|
170
|
+
// STATUS_ROLLING_LINES steps, each capped at STATUS_LINE_MAX chars, with a
|
|
171
|
+
// `+N earlier…` header when the feed overflows, bounded by the 4000-char
|
|
172
|
+
// wire-limit backstop (STATUS_CARD_CHAR_BUDGET).
|
|
167
173
|
|
|
168
|
-
|
|
174
|
+
import {
|
|
175
|
+
STATUS_CARD_CHAR_BUDGET,
|
|
176
|
+
STATUS_ROLLING_LINES,
|
|
177
|
+
STATUS_LINE_MAX,
|
|
178
|
+
NESTED_PREFIX,
|
|
179
|
+
} from './status-no-truncate.js'
|
|
180
|
+
import { escapeHtml, stripMarkdown, truncate } from './card-format.js'
|
|
181
|
+
import { isTelegramSurfaceTool } from './tool-names.js'
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Optional header for the main-session activity card, matching the worker
|
|
185
|
+
* card's two-line header style so both cards render consistently.
|
|
186
|
+
*
|
|
187
|
+
* `label` — first header line text (e.g. "Agent")
|
|
188
|
+
* `elapsedMs`— wall-clock since the turn started (for the elapsed field)
|
|
189
|
+
* `toolCount`— labeled (non-surface) tool calls so far
|
|
190
|
+
* `state` — 'running' | 'done' (controls finished vs. in-progress status wording)
|
|
191
|
+
*/
|
|
192
|
+
export interface SessionActivityHeader {
|
|
193
|
+
label: string
|
|
194
|
+
elapsedMs: number
|
|
195
|
+
toolCount: number
|
|
196
|
+
state: 'running' | 'done'
|
|
197
|
+
}
|
|
169
198
|
|
|
170
199
|
/**
|
|
171
200
|
* Append a tool_use's friendly line to the running feed (mutates `lines`)
|
|
@@ -188,42 +217,338 @@ export function appendActivityLine(
|
|
|
188
217
|
return renderActivityFeed(lines);
|
|
189
218
|
}
|
|
190
219
|
|
|
191
|
-
/**
|
|
192
|
-
|
|
193
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Clip a raw narrative text block down to a single durable feed line:
|
|
222
|
+
* first line only, trimmed, sliced to STATUS_LINE_MAX (200) chars.
|
|
223
|
+
*
|
|
224
|
+
* 200 chars matches the tool-label cap used by `escapeStepLine` / `renderStepFeed`
|
|
225
|
+
* so a narrative line is legible at the same length as a tool step — "Analysing
|
|
226
|
+
* the 12 changed files in /src/auth to understand the scope of…" rather than a
|
|
227
|
+
* hard-truncated 120-char fragment that drops context mid-sentence.
|
|
228
|
+
*
|
|
229
|
+
* Shared by the main-agent gateway path and the sub-agent watcher so both render
|
|
230
|
+
* narrative identically. Returns the raw (unescaped) clipped string — callers
|
|
231
|
+
* escape via the renderStepFeed path, exactly like a tool label.
|
|
232
|
+
*/
|
|
233
|
+
export function clipNarrative(s: string): string {
|
|
234
|
+
// `s` is a non-nullable string at every call site (showNarrativeStep passes
|
|
235
|
+
// ev.text; the foreground-sub path passes `progressLine ?? latestSummary`,
|
|
236
|
+
// both typed string), so no null guard is needed.
|
|
237
|
+
return s.split('\n')[0].trim().slice(0, STATUS_LINE_MAX);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render a two-line header for the activity card, matching the worker card's
|
|
242
|
+
* style. Used by both the main-session card and the worker card.
|
|
243
|
+
*
|
|
244
|
+
* Line 1: `<emoji> <b>Label</b> · <i>description</i>` (description optional)
|
|
245
|
+
* Line 2: status + elapsed + tool count
|
|
246
|
+
*
|
|
247
|
+
* `emoji` — leading emoji (e.g. "🤖", "🛠")
|
|
248
|
+
* `label` — bold name (e.g. "Agent", "Worker")
|
|
249
|
+
* `description` — italicised task description (optional)
|
|
250
|
+
* `elapsedMs` — wall-clock elapsed, rendered via `formatFeedElapsed`
|
|
251
|
+
* `toolCount` — labeled tool calls this turn
|
|
252
|
+
* `state` — 'running' | 'done' | 'failed' (controls the status line wording;
|
|
253
|
+
* 'failed' renders `failed · …` so a failed worker never reads as done)
|
|
254
|
+
*
|
|
255
|
+
* Returns a two-element array of ready Telegram HTML lines (no trailing newline).
|
|
256
|
+
*/
|
|
257
|
+
export function renderActivityHeader(
|
|
258
|
+
emoji: string,
|
|
259
|
+
label: string,
|
|
260
|
+
description: string,
|
|
261
|
+
elapsedMs: number,
|
|
262
|
+
toolCount: number,
|
|
263
|
+
state: 'running' | 'done' | 'failed',
|
|
264
|
+
): [string, string] {
|
|
265
|
+
const toolWord = toolCount === 1 ? 'tool' : 'tools'
|
|
266
|
+
const elapsed = formatFeedElapsed(elapsedMs)
|
|
267
|
+
const descPart = description.length > 0 ? ` · <i>${escapeHtml(description)}</i>` : ''
|
|
268
|
+
const line1 = `${emoji} <b>${escapeHtml(label)}</b>${descPart}`
|
|
269
|
+
const line2 = state === 'running'
|
|
270
|
+
? `<i>${elapsed} · ${toolCount} ${toolWord}</i>`
|
|
271
|
+
: `<i>${state} · ${toolCount} ${toolWord} · ${elapsed}</i>`
|
|
272
|
+
return [line1, line2]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Format elapsed milliseconds for display in the activity header (e.g. "12s", "2m05s"). */
|
|
276
|
+
export function formatFeedElapsed(ms: number): string {
|
|
277
|
+
const s = Math.floor(ms / 1000)
|
|
278
|
+
if (s < 60) return `${s}s`
|
|
279
|
+
const m = Math.floor(s / 60)
|
|
280
|
+
return `${m}m${(s % 60).toString().padStart(2, '0')}s`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Truncation pipeline (the single correctness-critical primitive) ────────
|
|
284
|
+
//
|
|
285
|
+
// Per RAW line, in this EXACT order:
|
|
286
|
+
// 1. stripMarkdown(raw)
|
|
287
|
+
// 2. .replace(/\s+/g, ' ').trim()
|
|
288
|
+
// 3. truncate(_, STATUS_LINE_MAX)
|
|
289
|
+
// 4. escapeHtml(_) ← escape is ALWAYS the last per-line op.
|
|
290
|
+
// Escaping last is load-bearing: clipping an already-escaped string can split
|
|
291
|
+
// an HTML entity (& → &), which Telegram's HTML parser rejects.
|
|
292
|
+
|
|
293
|
+
/** Clean + clip + escape a single raw step line. Returns ready-to-wrap HTML. */
|
|
294
|
+
function escapeStepLine(raw: string): string {
|
|
295
|
+
const cleaned = stripMarkdown(raw).replace(/\s+/g, ' ').trim()
|
|
296
|
+
return escapeHtml(truncate(cleaned, STATUS_LINE_MAX))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Shared step-feed emitter. Appends `✓`/`→` bullet lines to `out` for the
|
|
301
|
+
* given ALREADY-ESCAPED step strings, windowing to STATUS_ROLLING_LINES and
|
|
302
|
+
* prepending a `+N earlier…` header when the feed overflows the window (on
|
|
303
|
+
* BOTH surfaces now). The worker feed imports this directly.
|
|
304
|
+
*
|
|
305
|
+
* `out` — accumulator mutated in place
|
|
306
|
+
* `steps` — pre-cleaned + pre-escaped HTML step strings
|
|
307
|
+
* `allDone` — when true ALL steps render done (✓ italic); when false the
|
|
308
|
+
* newest renders in-progress (→ bold)
|
|
309
|
+
* `liveSuffix` — appended INSIDE the newest in-progress line (heartbeat tick)
|
|
310
|
+
*/
|
|
311
|
+
export function renderStepFeed(
|
|
312
|
+
out: string[],
|
|
313
|
+
steps: string[],
|
|
314
|
+
allDone: boolean,
|
|
315
|
+
liveSuffix = '',
|
|
316
|
+
): void {
|
|
317
|
+
if (steps.length === 0) return
|
|
318
|
+
const shown = steps.slice(-STATUS_ROLLING_LINES)
|
|
319
|
+
const hidden = steps.length - shown.length
|
|
320
|
+
if (hidden > 0) out.push(`<i>✓ +${hidden} earlier…</i>`)
|
|
321
|
+
const lastIdx = shown.length - 1
|
|
322
|
+
shown.forEach((s, i) => {
|
|
323
|
+
out.push(!allDone && i === lastIdx ? `<b>→ ${s}${liveSuffix}</b>` : `<i>✓ ${s}</i>`)
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Unified status-card primitive ──────────────────────────────────────────
|
|
328
|
+
//
|
|
329
|
+
// Both status surfaces (🤖 agent + 🛠 worker) render through `renderStatusCard`.
|
|
330
|
+
// It is the single place that runs the per-line truncation pipeline, windows
|
|
331
|
+
// the rolling feed, prepends `+N earlier…`, wraps bullets, indents child
|
|
332
|
+
// steps, and applies the total-budget backstop.
|
|
333
|
+
|
|
334
|
+
/** Header block for a status card. Emoji 🤖 (agent) / 🛠 (worker). */
|
|
335
|
+
export interface StatusCardHeader {
|
|
336
|
+
emoji: string
|
|
337
|
+
label: string
|
|
338
|
+
description?: string
|
|
339
|
+
elapsedMs: number
|
|
340
|
+
toolCount: number
|
|
341
|
+
state: 'running' | 'done' | 'failed'
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Inputs to the unified status-card renderer. */
|
|
345
|
+
export interface StatusCardOpts {
|
|
346
|
+
/** Optional two-line header. When omitted, the card is steps-only. */
|
|
347
|
+
header?: StatusCardHeader
|
|
348
|
+
/** RAW parent step strings (unstripped, unescaped). */
|
|
349
|
+
steps: string[]
|
|
350
|
+
/** RAW nested child step strings (foreground sub-agent), unstripped/unescaped. */
|
|
351
|
+
childSteps?: string[]
|
|
352
|
+
/** Terminal record render — newest line renders done (✓) and `liveSuffix` is ignored. */
|
|
353
|
+
final?: boolean
|
|
354
|
+
/** Heartbeat suffix appended INSIDE the newest in-progress line (live only). */
|
|
355
|
+
liveSuffix?: string
|
|
356
|
+
/** When `final` and > 0, appends a `✓ N steps` footer. */
|
|
357
|
+
stepCount?: number
|
|
358
|
+
/** Optional terminal result block (worker recap), already-cleaned text + emoji. */
|
|
359
|
+
result?: { emoji: string; text: string }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Render the unified status card as ready Telegram HTML, or null when there is
|
|
364
|
+
* no content to show (no header content, no steps, no children, no result).
|
|
365
|
+
*
|
|
366
|
+
* Pipeline: header → escape+clip every raw step/child → rolling-window each
|
|
367
|
+
* group with `+N earlier…` → wrap (parent done-styled when children present;
|
|
368
|
+
* newest-in-progress `→` bold else `✓` italic; NESTED_PREFIX for children) →
|
|
369
|
+
* optional `✓ N steps` footer → optional result block → fitCardToBudget.
|
|
370
|
+
*/
|
|
371
|
+
export function renderStatusCard(opts: StatusCardOpts): string | null {
|
|
372
|
+
const { header, final = false, liveSuffix = '', stepCount, result } = opts
|
|
373
|
+
const rawSteps = opts.steps.filter((s) => s != null)
|
|
374
|
+
const rawChildren = (opts.childSteps ?? []).map((s) => s.trim()).filter((s) => s.length > 0)
|
|
375
|
+
const hasChildren = rawChildren.length > 0
|
|
376
|
+
|
|
377
|
+
// Escape every line through the per-line pipeline (escape last).
|
|
378
|
+
const steps = rawSteps.map(escapeStepLine)
|
|
379
|
+
const children = rawChildren.map(escapeStepLine)
|
|
380
|
+
|
|
381
|
+
const headerLines = header != null
|
|
382
|
+
? renderActivityHeader(
|
|
383
|
+
header.emoji,
|
|
384
|
+
header.label,
|
|
385
|
+
header.description ?? '',
|
|
386
|
+
header.elapsedMs,
|
|
387
|
+
header.toolCount,
|
|
388
|
+
// Thread the terminal state straight through so a failed worker reads
|
|
389
|
+
// `failed · …` on line 2 — never byte-identical to a done worker even
|
|
390
|
+
// when the result block is empty. The agent surface never passes
|
|
391
|
+
// 'failed', so only the worker card is affected.
|
|
392
|
+
header.state,
|
|
393
|
+
)
|
|
394
|
+
: []
|
|
395
|
+
|
|
396
|
+
const out: string[] = [...headerLines]
|
|
397
|
+
|
|
398
|
+
if (hasChildren) {
|
|
399
|
+
// Parent lines all render done — the live → step lives in the nested block.
|
|
400
|
+
const shownParent = steps.slice(-STATUS_ROLLING_LINES)
|
|
401
|
+
const hiddenParent = steps.length - shownParent.length
|
|
402
|
+
if (hiddenParent > 0) out.push(`<i>✓ +${hiddenParent} earlier…</i>`)
|
|
403
|
+
for (const s of shownParent) out.push(`<i>✓ ${s}</i>`)
|
|
404
|
+
// Child block.
|
|
405
|
+
const shownChild = children.slice(-STATUS_ROLLING_LINES)
|
|
406
|
+
const hiddenChild = children.length - shownChild.length
|
|
407
|
+
if (hiddenChild > 0) out.push(`${NESTED_PREFIX}<i>+${hiddenChild} earlier…</i>`)
|
|
408
|
+
const lastChildIdx = shownChild.length - 1
|
|
409
|
+
shownChild.forEach((s, i) => {
|
|
410
|
+
out.push(
|
|
411
|
+
i === lastChildIdx && !final
|
|
412
|
+
? `${NESTED_PREFIX}<b>→ ${s}${liveSuffix}</b>`
|
|
413
|
+
: `${NESTED_PREFIX}<i>${s}</i>`,
|
|
414
|
+
)
|
|
415
|
+
})
|
|
416
|
+
} else {
|
|
417
|
+
renderStepFeed(out, steps, final, liveSuffix)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (final && stepCount != null && stepCount > 0) {
|
|
421
|
+
out.push(`<i>✓ ${stepCount} steps</i>`)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (result != null && result.text.length > 0) {
|
|
425
|
+
out.push(WORKER_RESULT_RULE)
|
|
426
|
+
out.push(`${result.emoji} <i>${escapeHtml(truncate(result.text, WORKER_RESULT_MAX))}</i>`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// out always carries the two header lines, so it is never empty — but guard
|
|
430
|
+
// against a degenerate header-less future caller.
|
|
431
|
+
if (out.length === 0) return null
|
|
432
|
+
const joined = out.join('\n')
|
|
433
|
+
if (joined.length <= STATUS_CARD_CHAR_BUDGET) return joined
|
|
434
|
+
return fitCardToBudget(opts, headerLines)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** Subtle horizontal rule between the running feed and the finished result. */
|
|
438
|
+
const WORKER_RESULT_RULE = '─────'
|
|
439
|
+
/** Hard cap on the terminal result paragraph. */
|
|
440
|
+
const WORKER_RESULT_MAX = 320
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Char-budget backstop. Keeps the header / footer / result block fixed and
|
|
444
|
+
* drops the oldest body bullets one at a time, re-inserting a `+N earlier…`
|
|
445
|
+
* marker, until the card fits STATUS_CARD_CHAR_BUDGET. In the extreme case
|
|
446
|
+
* (a single newest bullet that is itself oversized) it truncates the RAW
|
|
447
|
+
* newest text, THEN escapes, THEN wraps — never slicing already-escaped HTML.
|
|
448
|
+
*/
|
|
449
|
+
function fitCardToBudget(opts: StatusCardOpts, headerLines: string[]): string {
|
|
450
|
+
const { final = false, liveSuffix = '', stepCount, result } = opts
|
|
451
|
+
const rawSteps = opts.steps.filter((s) => s != null)
|
|
452
|
+
const rawChildren = (opts.childSteps ?? []).map((s) => s.trim()).filter((s) => s.length > 0)
|
|
453
|
+
const hasChildren = rawChildren.length > 0
|
|
454
|
+
|
|
455
|
+
// Fixed footer/result lines (always kept).
|
|
456
|
+
const footerLines: string[] = []
|
|
457
|
+
if (final && stepCount != null && stepCount > 0) footerLines.push(`<i>✓ ${stepCount} steps</i>`)
|
|
458
|
+
if (result != null && result.text.length > 0) {
|
|
459
|
+
footerLines.push(WORKER_RESULT_RULE)
|
|
460
|
+
footerLines.push(`${result.emoji} <i>${escapeHtml(truncate(result.text, WORKER_RESULT_MAX))}</i>`)
|
|
461
|
+
}
|
|
462
|
+
const fixedCost = [...headerLines, ...footerLines].join('\n').length
|
|
463
|
+
|
|
464
|
+
// The active "body" group whose oldest bullets we drop: children when
|
|
465
|
+
// present (parent collapses to a single "+N" marker), else parent steps.
|
|
466
|
+
const body = hasChildren ? rawChildren : rawSteps
|
|
467
|
+
const escapedBody = body.map(escapeStepLine)
|
|
468
|
+
const prefix = hasChildren ? NESTED_PREFIX : ''
|
|
469
|
+
|
|
470
|
+
const buildBullet = (esc: string, isLast: boolean): string =>
|
|
471
|
+
!final && isLast ? `${prefix}<b>→ ${esc}${liveSuffix}</b>` : `${prefix}<i>${esc}</i>`
|
|
472
|
+
|
|
473
|
+
// Parent-collapsed marker line when we are dropping children but parent steps exist.
|
|
474
|
+
const parentMarker =
|
|
475
|
+
hasChildren && rawSteps.length > 0 ? `<i>✓ +${rawSteps.length} earlier…</i>` : null
|
|
476
|
+
|
|
477
|
+
for (let drop = 1; drop < escapedBody.length; drop++) {
|
|
478
|
+
const shown = escapedBody.slice(drop)
|
|
479
|
+
const lines: string[] = [...headerLines]
|
|
480
|
+
if (parentMarker != null) lines.push(parentMarker)
|
|
481
|
+
lines.push(hasChildren ? `${NESTED_PREFIX}<i>+${drop} earlier…</i>` : `<i>✓ +${drop} earlier…</i>`)
|
|
482
|
+
const lastIdx = shown.length - 1
|
|
483
|
+
shown.forEach((esc, i) => lines.push(buildBullet(esc, i === lastIdx)))
|
|
484
|
+
lines.push(...footerLines)
|
|
485
|
+
const candidate = lines.join('\n')
|
|
486
|
+
if (candidate.length <= STATUS_CARD_CHAR_BUDGET) return candidate
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Extreme: the single newest bullet is itself oversized. Truncate the RAW
|
|
490
|
+
// newest text, then escape, then wrap — re-checking post-escape because
|
|
491
|
+
// escaping can expand the string (& → &).
|
|
492
|
+
const rawNewest = body.length > 0 ? stripMarkdown(body[body.length - 1]).replace(/\s+/g, ' ').trim() : ''
|
|
493
|
+
const wrapperOverhead = final
|
|
494
|
+
? (prefix + '<i>✓ </i>').length
|
|
495
|
+
: (prefix + '<b>→ </b>').length + liveSuffix.length
|
|
496
|
+
const headerFooterCost =
|
|
497
|
+
fixedCost + (fixedCost > 0 ? 1 : 0) + (parentMarker != null ? parentMarker.length + 1 : 0)
|
|
498
|
+
const budget = STATUS_CARD_CHAR_BUDGET - headerFooterCost - wrapperOverhead
|
|
499
|
+
let raw = rawNewest.slice(0, Math.max(0, budget))
|
|
500
|
+
let newest = escapeHtml(raw)
|
|
501
|
+
while (raw.length > 0 && wrapperOverhead + headerFooterCost + newest.length > STATUS_CARD_CHAR_BUDGET) {
|
|
502
|
+
const excess = wrapperOverhead + headerFooterCost + newest.length - STATUS_CARD_CHAR_BUDGET
|
|
503
|
+
raw = raw.slice(0, Math.max(0, raw.length - excess - 1))
|
|
504
|
+
newest = escapeHtml(raw)
|
|
505
|
+
}
|
|
506
|
+
const newestLine = final ? `${prefix}<i>✓ ${newest}</i>` : `${prefix}<b>→ ${newest}${liveSuffix}</b>`
|
|
507
|
+
const lines: string[] = [...headerLines]
|
|
508
|
+
if (parentMarker != null) lines.push(parentMarker)
|
|
509
|
+
lines.push(newestLine)
|
|
510
|
+
lines.push(...footerLines)
|
|
511
|
+
return lines.join('\n')
|
|
194
512
|
}
|
|
195
513
|
|
|
196
514
|
/**
|
|
197
515
|
* Render the accumulated feed as ready Telegram HTML — one action per line,
|
|
198
516
|
* newest last. The current (newest) step is bold with a `→`; finished steps
|
|
199
|
-
* are italic with a `✓`. Capped to the last
|
|
517
|
+
* are italic with a `✓`. Capped to the last STATUS_ROLLING_LINES with a dim
|
|
200
518
|
* `✓ +N earlier…` header when the turn ran longer. Returns null when empty.
|
|
201
519
|
* Callers send the result verbatim — do NOT re-escape or re-wrap it.
|
|
520
|
+
*
|
|
521
|
+
* Thin adapter over `renderStatusCard` (emoji 🤖, label 'Agent').
|
|
522
|
+
*
|
|
523
|
+
* `stepCount` (optional): when `final=true` and `stepCount > 0`, appends a
|
|
524
|
+
* `✓ N steps` footer line.
|
|
525
|
+
*
|
|
526
|
+
* `header` (optional): when provided, the two-line activity header carries
|
|
527
|
+
* elapsed + tool count.
|
|
202
528
|
*/
|
|
203
529
|
export function renderActivityFeed(
|
|
204
530
|
lines: string[],
|
|
205
531
|
final = false,
|
|
206
532
|
liveSuffix = "",
|
|
533
|
+
stepCount?: number,
|
|
534
|
+
header?: SessionActivityHeader,
|
|
207
535
|
): string | null {
|
|
208
|
-
if (lines.length === 0) return null;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
out.push(i === lastIdx && !final ? `<b>→ ${esc}${liveSuffix}</b>` : `<i>✓ ${esc}</i>`);
|
|
225
|
-
});
|
|
226
|
-
return out.join("\n");
|
|
536
|
+
if (lines.length === 0 && header == null) return null;
|
|
537
|
+
return renderStatusCard({
|
|
538
|
+
header: header != null
|
|
539
|
+
? {
|
|
540
|
+
emoji: '🤖',
|
|
541
|
+
label: header.label,
|
|
542
|
+
elapsedMs: header.elapsedMs,
|
|
543
|
+
toolCount: header.toolCount,
|
|
544
|
+
state: header.state,
|
|
545
|
+
}
|
|
546
|
+
: undefined,
|
|
547
|
+
steps: lines,
|
|
548
|
+
final,
|
|
549
|
+
liveSuffix,
|
|
550
|
+
stepCount,
|
|
551
|
+
})
|
|
227
552
|
}
|
|
228
553
|
|
|
229
554
|
// ─── Foreground sub-agent nesting (Model A) ─────────────────────────────────
|
|
@@ -238,10 +563,6 @@ export function renderActivityFeed(
|
|
|
238
563
|
|
|
239
564
|
/** Trailing nested child lines kept visible (Telegram length + readability). */
|
|
240
565
|
export const NESTED_MAX_LINES = 4;
|
|
241
|
-
/** Hard cap on a single nested narrative line. */
|
|
242
|
-
const NESTED_LINE_MAX = 90;
|
|
243
|
-
/** Indent marker for a nested sub-agent step. */
|
|
244
|
-
const NESTED_PREFIX = " ↳ ";
|
|
245
566
|
|
|
246
567
|
/**
|
|
247
568
|
* Render the parent activity feed with an active foreground sub-agent's steps
|
|
@@ -251,39 +572,35 @@ const NESTED_PREFIX = " ↳ ";
|
|
|
251
572
|
* and the child block is indented, newest = bold `→`, earlier = italic, with
|
|
252
573
|
* a `↳ +N earlier…` header when it overflows. Returns ready Telegram HTML
|
|
253
574
|
* (callers must NOT re-escape) or null when there is nothing to show.
|
|
575
|
+
*
|
|
576
|
+
* Thin adapter over `renderStatusCard` (emoji 🤖, label 'Agent', childSteps).
|
|
254
577
|
*/
|
|
255
578
|
export function renderActivityFeedWithNested(
|
|
256
579
|
lines: string[],
|
|
257
580
|
childLines: string[],
|
|
258
581
|
final = false,
|
|
259
582
|
liveSuffix = "",
|
|
583
|
+
stepCount?: number,
|
|
584
|
+
header?: SessionActivityHeader,
|
|
260
585
|
): string | null {
|
|
261
586
|
const children = childLines.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
262
|
-
if (children.length === 0) return renderActivityFeed(lines, final, liveSuffix);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const esc = escapeFeedHtml(t);
|
|
280
|
-
out.push(
|
|
281
|
-
i === lastChildIdx && !final
|
|
282
|
-
? `${NESTED_PREFIX}<b>→ ${esc}${liveSuffix}</b>`
|
|
283
|
-
: `${NESTED_PREFIX}<i>${esc}</i>`,
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
return out.length > 0 ? out.join("\n") : null;
|
|
587
|
+
if (children.length === 0) return renderActivityFeed(lines, final, liveSuffix, stepCount, header);
|
|
588
|
+
return renderStatusCard({
|
|
589
|
+
header: header != null
|
|
590
|
+
? {
|
|
591
|
+
emoji: '🤖',
|
|
592
|
+
label: header.label,
|
|
593
|
+
elapsedMs: header.elapsedMs,
|
|
594
|
+
toolCount: header.toolCount,
|
|
595
|
+
state: header.state,
|
|
596
|
+
}
|
|
597
|
+
: undefined,
|
|
598
|
+
steps: lines,
|
|
599
|
+
childSteps: children,
|
|
600
|
+
final,
|
|
601
|
+
liveSuffix,
|
|
602
|
+
stepCount,
|
|
603
|
+
})
|
|
287
604
|
}
|
|
288
605
|
|
|
289
606
|
/**
|