switchroom 0.14.1 → 0.14.2

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.
@@ -49278,8 +49278,8 @@ var {
49278
49278
  } = import__.default;
49279
49279
 
49280
49280
  // src/build-info.ts
49281
- var VERSION = "0.14.1";
49282
- var COMMIT_SHA = "e51a8794";
49281
+ var VERSION = "0.14.2";
49282
+ var COMMIT_SHA = "3c7d0238";
49283
49283
 
49284
49284
  // src/cli/agent.ts
49285
49285
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31971,6 +31971,26 @@ function describeToolUse(toolName, input) {
31971
31971
  return "Working\u2026";
31972
31972
  }
31973
31973
  }
31974
+ var MIRROR_MAX_LINES = 6;
31975
+ function appendActivityLine(lines, toolName, input) {
31976
+ const line = describeToolUse(toolName, input);
31977
+ if (line == null)
31978
+ return null;
31979
+ if (lines.length === 0 || lines[lines.length - 1] !== line) {
31980
+ lines.push(line);
31981
+ }
31982
+ return renderActivityFeed(lines);
31983
+ }
31984
+ function renderActivityFeed(lines) {
31985
+ if (lines.length === 0)
31986
+ return null;
31987
+ const shown = lines.slice(-MIRROR_MAX_LINES);
31988
+ const hidden = lines.length - shown.length;
31989
+ const body = shown.map((l) => `\u00b7 ${l}`).join(`
31990
+ `);
31991
+ return hidden > 0 ? `\u00b7 +${hidden} earlier\u2026
31992
+ ${body}` : body;
31993
+ }
31974
31994
 
31975
31995
  // tool-labels.ts
31976
31996
  var MAX_LABEL_CHARS = 60;
@@ -50143,10 +50163,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
50143
50163
  }
50144
50164
 
50145
50165
  // ../src/build-info.ts
50146
- var VERSION = "0.14.1";
50147
- var COMMIT_SHA = "e51a8794";
50148
- var COMMIT_DATE = "2026-05-28T07:24:12Z";
50149
- var LATEST_PR = 1956;
50166
+ var VERSION = "0.14.2";
50167
+ var COMMIT_SHA = "3c7d0238";
50168
+ var COMMIT_DATE = "2026-05-28T07:47:55Z";
50169
+ var LATEST_PR = 1958;
50150
50170
  var COMMITS_AHEAD_OF_TAG = 0;
50151
50171
 
50152
50172
  // gateway/boot-version.ts
@@ -54223,6 +54243,7 @@ function handleSessionEvent(ev) {
54223
54243
  activityInFlight: null,
54224
54244
  activityPendingRender: null,
54225
54245
  activityLastSentRender: null,
54246
+ mirrorLines: [],
54226
54247
  answerStream: null,
54227
54248
  isDm: isDmChatId(ev.chatId)
54228
54249
  };
@@ -54293,7 +54314,7 @@ function handleSessionEvent(ev) {
54293
54314
  }
54294
54315
  }
54295
54316
  if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
54296
- const rendered = DRAFT_MIRROR_ENABLED ? describeToolUse(name, ev.input) : registerAndRender(turn.toolActivity, name);
54317
+ const rendered = DRAFT_MIRROR_ENABLED ? appendActivityLine(turn.mirrorLines, name, ev.input) : registerAndRender(turn.toolActivity, name);
54297
54318
  if (rendered != null) {
54298
54319
  turn.activityPendingRender = rendered;
54299
54320
  if (turn.activityInFlight == null) {
@@ -58,6 +58,7 @@ import {
58
58
  makeEmptyActivityState,
59
59
  registerAndRender,
60
60
  describeToolUse,
61
+ appendActivityLine,
61
62
  type ActivityState,
62
63
  } from '../tool-activity-summary.js'
63
64
  import { toolLabel } from '../tool-labels.js'
@@ -1338,6 +1339,11 @@ type CurrentTurn = {
1338
1339
  activityInFlight: Promise<void> | null
1339
1340
  activityPendingRender: string | null
1340
1341
  activityLastSentRender: string | null
1342
+ // Draft-mirror Phase 2: accumulating friendly-action feed for this turn
1343
+ // (DRAFT_MIRROR only). Each non-surface tool_use appends a line via
1344
+ // `appendActivityLine`; the feed renders as a capped chronological list
1345
+ // in the ephemeral draft and clears on reply. Reset per turn.
1346
+ mirrorLines: string[]
1341
1347
  // Issue #195 — answer-lane streaming. Lazily created on the first text
1342
1348
  // event of a turn (once enough text has accumulated, the stream itself
1343
1349
  // gates on minInitialChars). Materialized and cleared at turn_end.
@@ -7001,6 +7007,7 @@ function handleSessionEvent(ev: SessionEvent): void {
7001
7007
  activityInFlight: null,
7002
7008
  activityPendingRender: null,
7003
7009
  activityLastSentRender: null,
7010
+ mirrorLines: [],
7004
7011
  answerStream: null,
7005
7012
  isDm: isDmChatId(ev.chatId),
7006
7013
  }
@@ -7146,11 +7153,12 @@ function handleSessionEvent(ev: SessionEvent): void {
7146
7153
  // exactly once at a time and re-running until pending matches
7147
7154
  // the last-sent. Captures `turn` so a late drain after turn-swap
7148
7155
  // can't corrupt the next turn's atom.
7149
- // DRAFT_MIRROR (RFC draft-mirror-preview): render each tool_use as a
7150
- // human-friendly line in the live preview, using the model-authored
7151
- // descriptive field (Bash.description, Read/Edit file basename,
7152
- // hindsight→"Searching memory", etc. — see describeToolUse). Latest
7153
- // action wins (the draft shows "doing X" live), clears on reply.
7156
+ // DRAFT_MIRROR (RFC draft-mirror-preview): accumulate each tool_use
7157
+ // into a human-friendly running feed in the live preview, using the
7158
+ // model-authored descriptive field (Bash.description, Read/Edit file
7159
+ // basename, hindsight→"Searching memory", etc. — see describeToolUse
7160
+ // / appendActivityLine). The draft shows the turn's actions as a
7161
+ // capped chronological list (Claude Code-style), clears on reply.
7154
7162
  // Never surfaces raw shell/query syntax — option A, uniform across
7155
7163
  // code + non-code agents.
7156
7164
  //
@@ -7159,7 +7167,7 @@ function handleSessionEvent(ev: SessionEvent): void {
7159
7167
  // pre-draft-mirror behavior.
7160
7168
  if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
7161
7169
  const rendered = DRAFT_MIRROR_ENABLED
7162
- ? describeToolUse(name, ev.input)
7170
+ ? appendActivityLine(turn.mirrorLines, name, ev.input)
7163
7171
  : registerAndRender(turn.toolActivity, name)
7164
7172
  if (rendered != null) {
7165
7173
  turn.activityPendingRender = rendered
@@ -6,6 +6,9 @@ import {
6
6
  registerAndRender,
7
7
  verbForTool,
8
8
  describeToolUse,
9
+ appendActivityLine,
10
+ renderActivityFeed,
11
+ MIRROR_MAX_LINES,
9
12
  } from "../tool-activity-summary.js";
10
13
 
11
14
  describe("describeToolUse — friendly per-tool rendering (draft-mirror)", () => {
@@ -283,3 +286,45 @@ describe("registerAndRender — ergonomic full-pipeline call", () => {
283
286
  expect(s.firstToolName).toBeNull();
284
287
  });
285
288
  });
289
+
290
+ describe("appendActivityLine + renderActivityFeed — accumulating draft feed", () => {
291
+ it("accumulates distinct actions chronologically (newest last)", () => {
292
+ const lines: string[] = [];
293
+ expect(appendActivityLine(lines, "Read", { file_path: "a/gateway.ts" })).toBe(
294
+ "· Reading gateway.ts",
295
+ );
296
+ expect(appendActivityLine(lines, "mcp__hindsight__reflect", { query: "x" })).toBe(
297
+ "· Reading gateway.ts\n· Searching memory",
298
+ );
299
+ expect(appendActivityLine(lines, "Bash", { command: "ls", description: "List workspace" })).toBe(
300
+ "· Reading gateway.ts\n· Searching memory\n· List workspace",
301
+ );
302
+ });
303
+
304
+ it("collapses consecutive exact-duplicate lines", () => {
305
+ const lines: string[] = [];
306
+ appendActivityLine(lines, "Read", { file_path: "a.ts" });
307
+ appendActivityLine(lines, "Read", { file_path: "a.ts" }); // dup → collapsed
308
+ expect(lines).toEqual(["Reading a.ts"]);
309
+ });
310
+
311
+ it("returns null (no feed update) for surface tools", () => {
312
+ const lines: string[] = [];
313
+ expect(appendActivityLine(lines, "mcp__switchroom-telegram__reply", { text: "hi" })).toBeNull();
314
+ expect(lines).toEqual([]);
315
+ });
316
+
317
+ it("caps to the last MIRROR_MAX_LINES with a '+N earlier' header", () => {
318
+ const lines = Array.from({ length: 9 }, (_, i) => `Action ${i + 1}`);
319
+ const out = renderActivityFeed(lines)!;
320
+ expect(out.startsWith("· +3 earlier…\n")).toBe(true);
321
+ // Only the last 6 actions are shown.
322
+ expect(out).toContain("· Action 4");
323
+ expect(out).toContain("· Action 9");
324
+ expect(out).not.toContain("· Action 3\n");
325
+ });
326
+
327
+ it("renderActivityFeed returns null on empty", () => {
328
+ expect(renderActivityFeed([])).toBeNull();
329
+ });
330
+ });
@@ -335,3 +335,50 @@ export function describeToolUse(
335
335
  return "Working…";
336
336
  }
337
337
  }
338
+
339
+ // ─── Accumulating activity feed (draft-mirror Phase 2) ──────────────────────
340
+ //
341
+ // Phase 1 showed only the latest action; this accumulates the turn's actions
342
+ // into a running feed — like Claude Code's own UI — streamed into the
343
+ // ephemeral draft and cleared on reply. Chronological (oldest first, newest
344
+ // last), consecutive exact-duplicates collapsed, capped to the most recent
345
+ // MIRROR_MAX_LINES with a "+N earlier" header so a heavy turn stays readable
346
+ // inside Telegram's compose-area draft.
347
+
348
+ export const MIRROR_MAX_LINES = 6;
349
+
350
+ /**
351
+ * Append a tool_use's friendly line to the running feed (mutates `lines`)
352
+ * and return the rendered draft body — or null when the tool is a surface
353
+ * tool / produced no line (caller skips the draft update).
354
+ *
355
+ * Dedups only consecutive identical lines (e.g. a burst of parallel Reads of
356
+ * the same file) so distinct actions are all preserved.
357
+ */
358
+ export function appendActivityLine(
359
+ lines: string[],
360
+ toolName: string,
361
+ input: Record<string, unknown> | undefined,
362
+ ): string | null {
363
+ const line = describeToolUse(toolName, input);
364
+ if (line == null) return null;
365
+ if (lines.length === 0 || lines[lines.length - 1] !== line) {
366
+ lines.push(line);
367
+ }
368
+ return renderActivityFeed(lines);
369
+ }
370
+
371
+ /**
372
+ * Render the accumulated feed as a plain-text block (one action per line).
373
+ * The caller HTML-escapes + wraps it for Telegram. Returns null when empty.
374
+ *
375
+ * Newest-last chronological order; capped to the last MIRROR_MAX_LINES with a
376
+ * dim "+N earlier" header when the turn ran longer.
377
+ */
378
+ export function renderActivityFeed(lines: string[]): string | null {
379
+ if (lines.length === 0) return null;
380
+ const shown = lines.slice(-MIRROR_MAX_LINES);
381
+ const hidden = lines.length - shown.length;
382
+ const body = shown.map((l) => `· ${l}`).join("\n");
383
+ return hidden > 0 ? `· +${hidden} earlier…\n${body}` : body;
384
+ }