switchroom 0.14.58 → 0.14.59

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.
@@ -146,6 +146,33 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
146
146
  it("final defaults false (live render keeps the → in-progress newest line)", () => {
147
147
  expect(renderActivityFeed(["Reading a.ts"])).toBe("<b>→ Reading a.ts</b>");
148
148
  });
149
+
150
+ // liveSuffix (PR1 heartbeat): appended INSIDE the newest in-progress line so a
151
+ // long single step visibly advances ("→ Pulling Meta data · 18s") even though
152
+ // the feed is pull-only and no new tool label arrived.
153
+ describe("liveSuffix (heartbeat)", () => {
154
+ it("appends the suffix to the newest in-progress line only", () => {
155
+ expect(renderActivityFeed(["Reading a.ts", "Running a command"], false, " · 18s")).toBe(
156
+ "<i>✓ Reading a.ts</i>\n<b>→ Running a command · 18s</b>",
157
+ );
158
+ });
159
+ it("single live line gets the suffix", () => {
160
+ expect(renderActivityFeed(["Pulling Meta data"], false, " · 1m05s")).toBe(
161
+ "<b>→ Pulling Meta data · 1m05s</b>",
162
+ );
163
+ });
164
+ it("final=true ignores the suffix (a finalized record never ticks)", () => {
165
+ const out = renderActivityFeed(["Reading a.ts", "Running a command"], true, " · 18s")!;
166
+ expect(out).not.toContain("·");
167
+ expect(out).not.toContain("→");
168
+ expect(out).toBe("<i>✓ Reading a.ts</i>\n<i>✓ Running a command</i>");
169
+ });
170
+ it("default empty suffix is byte-identical to no suffix", () => {
171
+ expect(renderActivityFeed(["Reading a.ts"], false, "")).toBe(
172
+ renderActivityFeed(["Reading a.ts"]),
173
+ );
174
+ });
175
+ });
149
176
  });
150
177
 
151
178
  describe("appendActivityLabel — precomputed label feed (tool_label path)", () => {
@@ -223,6 +250,23 @@ describe("renderActivityFeedWithNested — foreground sub-agent nesting (Model A
223
250
  );
224
251
  });
225
252
 
253
+ it("liveSuffix (heartbeat) lands on the nested newest in-progress step", () => {
254
+ const out = renderActivityFeedWithNested(
255
+ ["Delegating: x"],
256
+ ["Reading schema.ts", "Looking for foreign keys"],
257
+ false,
258
+ " · 22s",
259
+ )!;
260
+ expect(out).toContain(" ↳ <b>→ Looking for foreign keys · 22s</b>");
261
+ expect(out).not.toContain("Reading schema.ts · "); // only the newest line ticks
262
+ });
263
+
264
+ it("liveSuffix passes through to the flat render when there are no children", () => {
265
+ expect(renderActivityFeedWithNested(["Reading a.ts"], [], false, " · 9s")).toBe(
266
+ "<b>→ Reading a.ts · 9s</b>",
267
+ );
268
+ });
269
+
226
270
  // Pins the invariant the gateway's foreground handoff-clear path relies on:
227
271
  // on an ack-first turn the parent feed is empty (mirrorLines=[]) and the only
228
272
  // content is the foreground sub-agent's nested narrative. The finalized
@@ -200,7 +200,11 @@ function escapeFeedHtml(s: string): string {
200
200
  * `✓ +N earlier…` header when the turn ran longer. Returns null when empty.
201
201
  * Callers send the result verbatim — do NOT re-escape or re-wrap it.
202
202
  */
203
- export function renderActivityFeed(lines: string[], final = false): string | null {
203
+ export function renderActivityFeed(
204
+ lines: string[],
205
+ final = false,
206
+ liveSuffix = "",
207
+ ): string | null {
204
208
  if (lines.length === 0) return null;
205
209
  const shown = lines.slice(-MIRROR_MAX_LINES);
206
210
  const hidden = lines.length - shown.length;
@@ -210,10 +214,14 @@ export function renderActivityFeed(lines: string[], final = false): string | nul
210
214
  // Newest line = in-progress step (bold, →); earlier = done (italic, ✓).
211
215
  // `final` (turn complete, feed left as a record): ALL lines render done (✓)
212
216
  // so the persisted message doesn't freeze on a misleading "→ in-progress".
217
+ // `liveSuffix` (heartbeat): appended INSIDE the newest in-progress line only
218
+ // (e.g. " · 18s") so the feed visibly advances during a long single step that
219
+ // emits no new tool label — the feed is otherwise pull-only and freezes.
220
+ // Caller passes framework-generated, HTML-safe text; never final + suffix.
213
221
  // Returns ready Telegram HTML — callers must NOT re-escape or re-wrap it.
214
222
  shown.forEach((l, i) => {
215
223
  const esc = escapeFeedHtml(l);
216
- out.push(i === lastIdx && !final ? `<b>→ ${esc}</b>` : `<i>✓ ${esc}</i>`);
224
+ out.push(i === lastIdx && !final ? `<b>→ ${esc}${liveSuffix}</b>` : `<i>✓ ${esc}</i>`);
217
225
  });
218
226
  return out.join("\n");
219
227
  }
@@ -248,9 +256,10 @@ export function renderActivityFeedWithNested(
248
256
  lines: string[],
249
257
  childLines: string[],
250
258
  final = false,
259
+ liveSuffix = "",
251
260
  ): string | null {
252
261
  const children = childLines.map((s) => s.trim()).filter((s) => s.length > 0);
253
- if (children.length === 0) return renderActivityFeed(lines, final);
262
+ if (children.length === 0) return renderActivityFeed(lines, final, liveSuffix);
254
263
 
255
264
  const out: string[] = [];
256
265
  const shownParent = lines.slice(-MIRROR_MAX_LINES);
@@ -264,12 +273,13 @@ export function renderActivityFeedWithNested(
264
273
  const lastChildIdx = shownChild.length - 1;
265
274
  // `final`: the nested newest step also renders done (✓) so the left-behind
266
275
  // feed reads as completed, not stuck on a "→ in-progress" child step.
276
+ // `liveSuffix` (heartbeat): appended to the nested newest in-progress step.
267
277
  shownChild.forEach((l, i) => {
268
278
  const t = l.length > NESTED_LINE_MAX ? l.slice(0, NESTED_LINE_MAX - 1) + "…" : l;
269
279
  const esc = escapeFeedHtml(t);
270
280
  out.push(
271
281
  i === lastChildIdx && !final
272
- ? `${NESTED_PREFIX}<b>→ ${esc}</b>`
282
+ ? `${NESTED_PREFIX}<b>→ ${esc}${liveSuffix}</b>`
273
283
  : `${NESTED_PREFIX}<i>${esc}</i>`,
274
284
  );
275
285
  });