switchroom 0.16.22 โ 0.16.24
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/cli/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2865 -48
- package/dist/cli/switchroom.js +338 -92
- package/dist/host-control/main.js +1 -1
- package/package.json +5 -5
- package/telegram-plugin/answer-stream.ts +13 -43
- package/telegram-plugin/auth-snapshot-format.ts +27 -30
- package/telegram-plugin/auto-fallback-fleet.ts +6 -11
- package/telegram-plugin/card-format.ts +28 -25
- package/telegram-plugin/credits-watch.ts +5 -10
- package/telegram-plugin/dist/gateway/gateway.js +1670 -1892
- package/telegram-plugin/draft-stream.ts +4 -4
- package/telegram-plugin/format.ts +101 -662
- package/telegram-plugin/gateway/approval-callback.ts +2 -3
- package/telegram-plugin/gateway/approval-card.test.ts +17 -4
- package/telegram-plugin/gateway/approval-card.ts +16 -6
- package/telegram-plugin/gateway/approvals-commands.ts +18 -24
- package/telegram-plugin/gateway/auth-command.ts +74 -74
- package/telegram-plugin/gateway/auth-line.ts +5 -15
- package/telegram-plugin/gateway/boot-card.ts +20 -22
- package/telegram-plugin/gateway/boot-version.ts +3 -2
- package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
- package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
- package/telegram-plugin/gateway/config-snapshot.ts +9 -9
- package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
- package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
- package/telegram-plugin/gateway/effort-command.ts +17 -17
- package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
- package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
- package/telegram-plugin/gateway/gateway.ts +751 -624
- package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
- package/telegram-plugin/gateway/inject-handler.ts +5 -5
- package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/linear-activity.ts +16 -14
- package/telegram-plugin/gateway/linear-setup.ts +1 -1
- package/telegram-plugin/gateway/model-command.ts +25 -25
- package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
- package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
- package/telegram-plugin/issues-card.ts +6 -7
- package/telegram-plugin/model-unavailable.ts +8 -12
- package/telegram-plugin/operator-events-history.ts +1 -1
- package/telegram-plugin/operator-events.ts +24 -28
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/pending-work-progress.ts +36 -36
- package/telegram-plugin/permission-title.ts +36 -20
- package/telegram-plugin/pty-partial-handler.ts +5 -13
- package/telegram-plugin/quota-check.ts +5 -5
- package/telegram-plugin/quota-watch.ts +13 -18
- package/telegram-plugin/recent-outbound-dedup.ts +5 -5
- package/telegram-plugin/registry/turns-schema.ts +44 -2
- package/telegram-plugin/retry-api-call.ts +15 -7
- package/telegram-plugin/rich-send.ts +57 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
- package/telegram-plugin/secret-detect/vault-error.ts +29 -22
- package/telegram-plugin/shared/bot-runtime.ts +23 -6
- package/telegram-plugin/silence-poke.ts +26 -69
- package/telegram-plugin/slot-banner-driver.ts +9 -6
- package/telegram-plugin/slot-banner.ts +5 -8
- package/telegram-plugin/steering.ts +0 -4
- package/telegram-plugin/stream-controller.ts +59 -62
- package/telegram-plugin/stream-reply-handler.ts +40 -97
- package/telegram-plugin/subagent-watcher.ts +2 -2
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
- package/telegram-plugin/tests/answer-stream.test.ts +54 -63
- package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
- package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/bot-api.harness.ts +23 -1
- package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
- package/telegram-plugin/tests/card-format.test.ts +6 -4
- package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
- package/telegram-plugin/tests/credits-watch.test.ts +5 -5
- package/telegram-plugin/tests/fake-bot-api.ts +58 -4
- package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
- package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
- package/telegram-plugin/tests/issues-card.test.ts +15 -12
- package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +2 -2
- package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
- package/telegram-plugin/tests/operator-events.test.ts +7 -9
- package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
- package/telegram-plugin/tests/permission-title.test.ts +45 -41
- package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
- package/telegram-plugin/tests/quota-check.test.ts +3 -3
- package/telegram-plugin/tests/quota-watch.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
- package/telegram-plugin/tests/silence-poke.test.ts +75 -112
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
- package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
- package/telegram-plugin/tests/slot-banner.test.ts +9 -6
- package/telegram-plugin/tests/status-accent.test.ts +29 -32
- package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts โ stream-controller-parse-fallback.test.ts} +40 -42
- package/telegram-plugin/tests/stream-controller.test.ts +63 -52
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
- package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
- package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
- package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
- package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
- package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
- package/telegram-plugin/tests/welcome-text.test.ts +72 -65
- package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
- package/telegram-plugin/text-voice-scrub.ts +8 -11
- package/telegram-plugin/tool-activity-summary.ts +29 -29
- package/telegram-plugin/welcome-text.ts +82 -83
- package/telegram-plugin/worker-activity-feed.ts +2 -3
- package/telegram-plugin/html-sanitize.ts +0 -244
- package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
|
@@ -81,13 +81,13 @@ describe("appendActivityLine + renderActivityFeed โ accumulating activity feed
|
|
|
81
81
|
it("accumulates distinct actions chronologically (newest = current โ bold, earlier = done โ italic)", () => {
|
|
82
82
|
const lines: string[] = [];
|
|
83
83
|
expect(appendActivityLine(lines, "Read", { file_path: "a/gateway.ts" })).toBe(
|
|
84
|
-
"
|
|
84
|
+
"**โ Reading gateway.ts**",
|
|
85
85
|
);
|
|
86
86
|
expect(appendActivityLine(lines, "mcp__hindsight__reflect", { query: "x" })).toBe(
|
|
87
|
-
"
|
|
87
|
+
"_โ Reading gateway.ts_\n**โ Searching memory**",
|
|
88
88
|
);
|
|
89
89
|
expect(appendActivityLine(lines, "Bash", { command: "ls", description: "List workspace" })).toBe(
|
|
90
|
-
"
|
|
90
|
+
"_โ Reading gateway.ts_\n_โ Searching memory_\n**โ List workspace**",
|
|
91
91
|
);
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -110,24 +110,28 @@ describe("appendActivityLine + renderActivityFeed โ accumulating activity feed
|
|
|
110
110
|
const lines = Array.from({ length: total }, (_, i) => `Action ${i + 1}`);
|
|
111
111
|
const out = renderActivityFeed(lines)!;
|
|
112
112
|
const hidden = total - STATUS_ROLLING_LINES;
|
|
113
|
-
expect(out.startsWith(
|
|
113
|
+
expect(out.startsWith(`_โ +${hidden} earlierโฆ_\n`)).toBe(true);
|
|
114
114
|
// Only the last STATUS_ROLLING_LINES actions are shown; older ones collapsed.
|
|
115
115
|
const firstVisible = total - STATUS_ROLLING_LINES + 1;
|
|
116
|
-
expect(out).toContain(
|
|
116
|
+
expect(out).toContain(`_โ Action ${firstVisible}_`);
|
|
117
117
|
expect(out).not.toContain(`Action ${firstVisible - 1}<`);
|
|
118
118
|
// The newest action is the in-progress step (bold โ); the rest are done (โ).
|
|
119
|
-
expect(out).toContain(
|
|
120
|
-
expect(out).toContain(
|
|
121
|
-
expect(out).not.toContain(
|
|
119
|
+
expect(out).toContain(`**โ Action ${total}**`);
|
|
120
|
+
expect(out).toContain(`_โ Action ${total - 1}_`);
|
|
121
|
+
expect(out).not.toContain(`**โ Action ${total - 1}**`);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it("
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
it("markdown-escapes emphasis specials in action text; passes < > & through (#2669)", () => {
|
|
125
|
+
// The per-line pipeline strips PAIRED markdown first, then escapeMarkdown
|
|
126
|
+
// escapes the survivors: only \ ` * _ ~ = [ ] | are escaped โ NOT < > &,
|
|
127
|
+
// which are literal in rich markdown. A lone `*` (3 * 4) and an
|
|
128
|
+
// intra-word `_` survive the strip and get escaped.
|
|
129
|
+
const out = renderActivityFeed(["Running <foo> & a_b 3 * 4"])!;
|
|
130
|
+
expect(out).toBe("**โ Running <foo> & a\\_b 3 \\* 4**");
|
|
127
131
|
});
|
|
128
132
|
|
|
129
133
|
it("renders a single line as the current (bold โ) step", () => {
|
|
130
|
-
expect(renderActivityFeed(["Reading a.ts"])).toBe("
|
|
134
|
+
expect(renderActivityFeed(["Reading a.ts"])).toBe("**โ Reading a.ts**");
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
it("renderActivityFeed returns null on empty", () => {
|
|
@@ -141,17 +145,17 @@ describe("appendActivityLine + renderActivityFeed โ accumulating activity feed
|
|
|
141
145
|
const lines = ["Reading a.ts", "Searching memory", "Running a command"];
|
|
142
146
|
const out = renderActivityFeed(lines, true)!;
|
|
143
147
|
expect(out).toBe(
|
|
144
|
-
"
|
|
148
|
+
"_โ Reading a.ts_\n_โ Searching memory_\n_โ Running a command_",
|
|
145
149
|
);
|
|
146
150
|
expect(out).not.toContain("โ"); // no in-progress arrow anywhere
|
|
147
151
|
});
|
|
148
152
|
|
|
149
153
|
it("final=true on a single line is also done (โ)", () => {
|
|
150
|
-
expect(renderActivityFeed(["Reading a.ts"], true)).toBe("
|
|
154
|
+
expect(renderActivityFeed(["Reading a.ts"], true)).toBe("_โ Reading a.ts_");
|
|
151
155
|
});
|
|
152
156
|
|
|
153
157
|
it("final defaults false (live render keeps the โ in-progress newest line)", () => {
|
|
154
|
-
expect(renderActivityFeed(["Reading a.ts"])).toBe("
|
|
158
|
+
expect(renderActivityFeed(["Reading a.ts"])).toBe("**โ Reading a.ts**");
|
|
155
159
|
});
|
|
156
160
|
|
|
157
161
|
// liveSuffix (PR1 heartbeat): appended INSIDE the newest in-progress line so a
|
|
@@ -160,19 +164,19 @@ describe("appendActivityLine + renderActivityFeed โ accumulating activity feed
|
|
|
160
164
|
describe("liveSuffix (heartbeat)", () => {
|
|
161
165
|
it("appends the suffix to the newest in-progress line only", () => {
|
|
162
166
|
expect(renderActivityFeed(["Reading a.ts", "Running a command"], false, " ยท 18s")).toBe(
|
|
163
|
-
"
|
|
167
|
+
"_โ Reading a.ts_\n**โ Running a command ยท 18s**",
|
|
164
168
|
);
|
|
165
169
|
});
|
|
166
170
|
it("single live line gets the suffix", () => {
|
|
167
171
|
expect(renderActivityFeed(["Pulling Meta data"], false, " ยท 1m05s")).toBe(
|
|
168
|
-
"
|
|
172
|
+
"**โ Pulling Meta data ยท 1m05s**",
|
|
169
173
|
);
|
|
170
174
|
});
|
|
171
175
|
it("final=true ignores the suffix (a finalized record never ticks)", () => {
|
|
172
176
|
const out = renderActivityFeed(["Reading a.ts", "Running a command"], true, " ยท 18s")!;
|
|
173
177
|
expect(out).not.toContain("ยท");
|
|
174
178
|
expect(out).not.toContain("โ");
|
|
175
|
-
expect(out).toBe("
|
|
179
|
+
expect(out).toBe("_โ Reading a.ts_\n_โ Running a command_");
|
|
176
180
|
});
|
|
177
181
|
it("default empty suffix is byte-identical to no suffix", () => {
|
|
178
182
|
expect(renderActivityFeed(["Reading a.ts"], false, "")).toBe(
|
|
@@ -185,9 +189,9 @@ describe("appendActivityLine + renderActivityFeed โ accumulating activity feed
|
|
|
185
189
|
describe("appendActivityLabel โ precomputed label feed (tool_label path)", () => {
|
|
186
190
|
it("accumulates precomputed labels, dedups consecutive, ignores empty", () => {
|
|
187
191
|
const lines: string[] = [];
|
|
188
|
-
expect(appendActivityLabel(lines, "Searching memory")).toBe("
|
|
192
|
+
expect(appendActivityLabel(lines, "Searching memory")).toBe("**โ Searching memory**");
|
|
189
193
|
expect(appendActivityLabel(lines, "List workspace")).toBe(
|
|
190
|
-
"
|
|
194
|
+
"_โ Searching memory_\n**โ List workspace**",
|
|
191
195
|
);
|
|
192
196
|
// consecutive dup collapses
|
|
193
197
|
appendActivityLabel(lines, "List workspace");
|
|
@@ -220,7 +224,7 @@ describe("clipNarrative โ narrative-step clip (JSONL-text-narrative primitive)
|
|
|
220
224
|
const narrativeRender = appendActivityLabel(narrativeLines, clipNarrative(text));
|
|
221
225
|
const labelRender = appendActivityLabel(labelLines, clipNarrative(text));
|
|
222
226
|
expect(narrativeRender).toBe(labelRender);
|
|
223
|
-
expect(narrativeRender).toBe(
|
|
227
|
+
expect(narrativeRender).toBe(`**โ ${text}**`);
|
|
224
228
|
});
|
|
225
229
|
|
|
226
230
|
it("empty string yields an empty clip (appendActivityLabel then drops it)", () => {
|
|
@@ -234,16 +238,16 @@ describe("renderActivityFeed โ โ N steps total (final render, issue #2461)",
|
|
|
234
238
|
const lines = ["Reading CLAUDE.md", "Searching memory", "Running a command"];
|
|
235
239
|
const out = renderActivityFeed(lines, true, "", 5)!;
|
|
236
240
|
// All lines are done (โ) and the footer is appended.
|
|
237
|
-
expect(out).toContain("
|
|
238
|
-
expect(out).toContain("
|
|
239
|
-
expect(out.endsWith("
|
|
241
|
+
expect(out).toContain("_โ Running a command_");
|
|
242
|
+
expect(out).toContain("_โ 5 steps_");
|
|
243
|
+
expect(out.endsWith("_โ 5 steps_")).toBe(true);
|
|
240
244
|
});
|
|
241
245
|
|
|
242
246
|
it("stepCount=0 โ no footer (no tools fired that were surfaced)", () => {
|
|
243
247
|
const lines = ["Reading CLAUDE.md"];
|
|
244
248
|
const out = renderActivityFeed(lines, true, "", 0)!;
|
|
245
249
|
expect(out).not.toContain("steps");
|
|
246
|
-
expect(out).toBe("
|
|
250
|
+
expect(out).toBe("_โ Reading CLAUDE.md_");
|
|
247
251
|
});
|
|
248
252
|
|
|
249
253
|
it("stepCount undefined โ no footer (live/non-final callers omit it)", () => {
|
|
@@ -257,22 +261,22 @@ describe("renderActivityFeed โ โ N steps total (final render, issue #2461)",
|
|
|
257
261
|
const out = renderActivityFeed(lines, false, "", 7)!;
|
|
258
262
|
expect(out).not.toContain("steps");
|
|
259
263
|
// The newest line is still the live in-progress step.
|
|
260
|
-
expect(out).toContain("
|
|
264
|
+
expect(out).toContain("**โ Running a command**");
|
|
261
265
|
});
|
|
262
266
|
|
|
263
267
|
it("stepCount=1 footer reads 'โ 1 steps' (no special-casing)", () => {
|
|
264
268
|
const lines = ["Reading CLAUDE.md"];
|
|
265
269
|
const out = renderActivityFeed(lines, true, "", 1)!;
|
|
266
|
-
expect(out).toContain("
|
|
270
|
+
expect(out).toContain("_โ 1 steps_");
|
|
267
271
|
});
|
|
268
272
|
|
|
269
273
|
it("footer appears even when the feed overflows the rolling window", () => {
|
|
270
274
|
const total = STATUS_ROLLING_LINES + 4;
|
|
271
275
|
const lines = Array.from({ length: total }, (_, i) => `Action ${i + 1}`);
|
|
272
276
|
const out = renderActivityFeed(lines, true, "", total)!;
|
|
273
|
-
expect(out).toContain(
|
|
274
|
-
expect(out).toContain(
|
|
275
|
-
expect(out.endsWith(
|
|
277
|
+
expect(out).toContain(`_โ +${total - STATUS_ROLLING_LINES} earlierโฆ_`);
|
|
278
|
+
expect(out).toContain(`_โ ${total} steps_`);
|
|
279
|
+
expect(out.endsWith(`_โ ${total} steps_`)).toBe(true);
|
|
276
280
|
});
|
|
277
281
|
|
|
278
282
|
it("stepCount is surface-tool-excluded: reply/react count 0, Read+mcp count correctly", () => {
|
|
@@ -283,7 +287,7 @@ describe("renderActivityFeed โ โ N steps total (final render, issue #2461)",
|
|
|
283
287
|
const lines = ["Reading CLAUDE.md", "Searching memory"]; // 2 non-surface labels
|
|
284
288
|
// stepCount=2 (Read + mcp__hindsight__recall) โ reply is NOT counted.
|
|
285
289
|
const out = renderActivityFeed(lines, true, "", 2)!;
|
|
286
|
-
expect(out).toContain("
|
|
290
|
+
expect(out).toContain("_โ 2 steps_");
|
|
287
291
|
// stepCount=0 would mean only surface tools fired โ no footer.
|
|
288
292
|
const outSurfaceOnly = renderActivityFeed(["Reading CLAUDE.md"], true, "", 0)!;
|
|
289
293
|
expect(outSurfaceOnly).not.toContain("steps");
|
|
@@ -303,33 +307,33 @@ describe("renderActivityFeedWithNested โ foreground sub-agent nesting (Model A
|
|
|
303
307
|
const child = ["Reading schema.ts", "Looking for foreign keys"];
|
|
304
308
|
const out = renderActivityFeedWithNested(parent, child)!;
|
|
305
309
|
// Parent is blocked at the Task tool โ none of its lines is the live step.
|
|
306
|
-
expect(out).toContain("
|
|
307
|
-
expect(out).toContain("
|
|
308
|
-
expect(out).not.toContain("
|
|
310
|
+
expect(out).toContain("_โ Searching memory_");
|
|
311
|
+
expect(out).toContain("_โ Delegating: review the migration_");
|
|
312
|
+
expect(out).not.toContain("**โ Delegating");
|
|
309
313
|
// The live โ step is the newest nested child line; earlier child = italic.
|
|
310
|
-
expect(out).toContain(" โณ
|
|
311
|
-
expect(out).toContain(" โณ
|
|
314
|
+
expect(out).toContain(" โณ _Reading schema.ts_");
|
|
315
|
+
expect(out).toContain(" โณ **โ Looking for foreign keys**");
|
|
312
316
|
});
|
|
313
317
|
|
|
314
318
|
it("windows the nested block to STATUS_ROLLING_LINES with a 'โณ +N earlierโฆ' header", () => {
|
|
315
319
|
const total = STATUS_ROLLING_LINES + 3;
|
|
316
320
|
const child = Array.from({ length: total }, (_, i) => `step ${i + 1}`);
|
|
317
321
|
const out = renderActivityFeedWithNested(["Delegating: x"], child)!;
|
|
318
|
-
expect(out).toContain(` โณ
|
|
322
|
+
expect(out).toContain(` โณ _+${total - STATUS_ROLLING_LINES} earlierโฆ_`);
|
|
319
323
|
// newest nested line is the live โ step
|
|
320
|
-
expect(out).toContain(` โณ
|
|
324
|
+
expect(out).toContain(` โณ **โ step ${total}**`);
|
|
321
325
|
// the oldest (collapsed) lines are not rendered verbatim
|
|
322
326
|
expect(out).not.toContain("step 1<");
|
|
323
327
|
});
|
|
324
328
|
|
|
325
329
|
it("renders the child block even when the parent feed is empty", () => {
|
|
326
330
|
const out = renderActivityFeedWithNested([], ["Reading a.ts"]);
|
|
327
|
-
expect(out).toBe(" โณ
|
|
331
|
+
expect(out).toBe(" โณ **โ Reading a.ts**");
|
|
328
332
|
});
|
|
329
333
|
|
|
330
|
-
it("
|
|
331
|
-
const out = renderActivityFeedWithNested(["Delegating: x"], ["touch
|
|
332
|
-
expect(out).toContain(" โณ
|
|
334
|
+
it("markdown-escapes nested child text (emphasis specials only)", () => {
|
|
335
|
+
const out = renderActivityFeedWithNested(["Delegating: x"], ["touch a_b & 2 * 3"])!;
|
|
336
|
+
expect(out).toContain(" โณ **โ touch a\\_b & 2 \\* 3**");
|
|
333
337
|
});
|
|
334
338
|
|
|
335
339
|
it("final=true: the nested newest step renders done (โ), not in-progress (โ)", () => {
|
|
@@ -338,13 +342,13 @@ describe("renderActivityFeedWithNested โ foreground sub-agent nesting (Model A
|
|
|
338
342
|
["Reading schema.ts", "Looking for foreign keys"],
|
|
339
343
|
true,
|
|
340
344
|
)!;
|
|
341
|
-
expect(out).toContain(" โณ
|
|
345
|
+
expect(out).toContain(" โณ _Looking for foreign keys_"); // newest now italic done
|
|
342
346
|
expect(out).not.toContain("โ"); // no in-progress arrow in the finalized feed
|
|
343
347
|
});
|
|
344
348
|
|
|
345
349
|
it("final=true with no children delegates to the finalized flat render", () => {
|
|
346
350
|
expect(renderActivityFeedWithNested(["Reading a.ts"], [], true)).toBe(
|
|
347
|
-
"
|
|
351
|
+
"_โ Reading a.ts_",
|
|
348
352
|
);
|
|
349
353
|
});
|
|
350
354
|
|
|
@@ -355,13 +359,13 @@ describe("renderActivityFeedWithNested โ foreground sub-agent nesting (Model A
|
|
|
355
359
|
false,
|
|
356
360
|
" ยท 22s",
|
|
357
361
|
)!;
|
|
358
|
-
expect(out).toContain(" โณ
|
|
362
|
+
expect(out).toContain(" โณ **โ Looking for foreign keys ยท 22s**");
|
|
359
363
|
expect(out).not.toContain("Reading schema.ts ยท "); // only the newest line ticks
|
|
360
364
|
});
|
|
361
365
|
|
|
362
366
|
it("liveSuffix passes through to the flat render when there are no children", () => {
|
|
363
367
|
expect(renderActivityFeedWithNested(["Reading a.ts"], [], false, " ยท 9s")).toBe(
|
|
364
|
-
"
|
|
368
|
+
"**โ Reading a.ts ยท 9s**",
|
|
365
369
|
);
|
|
366
370
|
});
|
|
367
371
|
|
|
@@ -369,15 +373,15 @@ describe("renderActivityFeedWithNested โ foreground sub-agent nesting (Model A
|
|
|
369
373
|
const parent = ["Delegating: review the migration"];
|
|
370
374
|
const child = ["Reading schema.ts", "Looking for foreign keys"];
|
|
371
375
|
const out = renderActivityFeedWithNested(parent, child, true, "", 5)!;
|
|
372
|
-
expect(out).toContain("
|
|
373
|
-
expect(out.endsWith("
|
|
376
|
+
expect(out).toContain("_โ 5 steps_");
|
|
377
|
+
expect(out.endsWith("_โ 5 steps_")).toBe(true);
|
|
374
378
|
expect(out).not.toContain("โ");
|
|
375
379
|
});
|
|
376
380
|
|
|
377
381
|
it("stepCount footer appears on final=true with no children (delegates to flat render)", () => {
|
|
378
382
|
const out = renderActivityFeedWithNested(["Reading a.ts"], [], true, "", 3)!;
|
|
379
|
-
expect(out).toContain("
|
|
380
|
-
expect(out).toBe("
|
|
383
|
+
expect(out).toContain("_โ 3 steps_");
|
|
384
|
+
expect(out).toBe("_โ Reading a.ts_\n_โ 3 steps_");
|
|
381
385
|
});
|
|
382
386
|
|
|
383
387
|
// Liveness-driven feed open (dark-turn fix): a turn that emits no tool_label
|
|
@@ -389,12 +393,12 @@ describe("renderActivityFeedWithNested โ foreground sub-agent nesting (Model A
|
|
|
389
393
|
it("renders the live in-progress placeholder with climbing elapsed", () => {
|
|
390
394
|
expect(
|
|
391
395
|
renderActivityFeedWithNested(["Workingโฆ"], [], false, " ยท 12s"),
|
|
392
|
-
).toBe("
|
|
396
|
+
).toBe("**โ Workingโฆ ยท 12s**");
|
|
393
397
|
});
|
|
394
398
|
|
|
395
399
|
it("finalizes the placeholder to a done record (no frozen โ line)", () => {
|
|
396
400
|
expect(renderActivityFeedWithNested(["Workingโฆ"], [], true)).toBe(
|
|
397
|
-
"
|
|
401
|
+
"_โ Workingโฆ_",
|
|
398
402
|
);
|
|
399
403
|
});
|
|
400
404
|
|
|
@@ -470,8 +474,8 @@ describe("rolling window + +N earlier โ renderActivityFeed", () => {
|
|
|
470
474
|
expect(out).not.toContain(`XAct-${String(i).padStart(3, '0')}`);
|
|
471
475
|
}
|
|
472
476
|
// Overflow header present on the AGENT surface now.
|
|
473
|
-
expect(out).toContain(
|
|
474
|
-
expect(out).toContain(
|
|
477
|
+
expect(out).toContain(`_โ +${12 - STATUS_ROLLING_LINES} earlierโฆ_`);
|
|
478
|
+
expect(out).toContain(`**โ XAct-012**`);
|
|
475
479
|
});
|
|
476
480
|
|
|
477
481
|
it("STATUS_LINE_MAX=200: a 250-char step is clipped to 200 with a trailing โฆ", () => {
|
|
@@ -521,8 +525,8 @@ describe("rolling window + +N earlier โ renderActivityFeedWithNested", () => {
|
|
|
521
525
|
for (let i = 1; i < firstVisible; i++) {
|
|
522
526
|
expect(out).not.toContain(`Parent ${i}`);
|
|
523
527
|
}
|
|
524
|
-
expect(out).toContain(
|
|
525
|
-
expect(out).toContain(" โณ
|
|
528
|
+
expect(out).toContain(`_โ +${totalParent - STATUS_ROLLING_LINES} earlierโฆ_`);
|
|
529
|
+
expect(out).toContain(" โณ **โ Child step B**");
|
|
526
530
|
});
|
|
527
531
|
|
|
528
532
|
it("many child lines โ only the last STATUS_ROLLING_LINES child lines render with a โณ +N earlier header", () => {
|
|
@@ -537,8 +541,8 @@ describe("rolling window + +N earlier โ renderActivityFeedWithNested", () => {
|
|
|
537
541
|
for (let i = 1; i < firstVisible; i++) {
|
|
538
542
|
expect(out).not.toContain(`Child ${i}`);
|
|
539
543
|
}
|
|
540
|
-
expect(out).toContain(` โณ
|
|
541
|
-
expect(out).toContain(` โณ
|
|
544
|
+
expect(out).toContain(` โณ _+${totalChild - STATUS_ROLLING_LINES} earlierโฆ_`);
|
|
545
|
+
expect(out).toContain(` โณ **โ Child ${totalChild}**`);
|
|
542
546
|
});
|
|
543
547
|
|
|
544
548
|
it("STATUS_LINE_MAX=200: a 250-char child line is clipped to 200 (both surfaces)", () => {
|
|
@@ -559,37 +563,31 @@ describe("rolling window + +N earlier โ renderActivityFeedWithNested", () => {
|
|
|
559
563
|
});
|
|
560
564
|
});
|
|
561
565
|
|
|
562
|
-
// โโโ Extreme-edge: single oversized line with
|
|
563
|
-
// These tests cover the
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
// All opening tags have a corresponding close (simple check for <b>/<i>).
|
|
584
|
-
const bOpen = (s.match(/<b>/g) ?? []).length;
|
|
585
|
-
const bClose = (s.match(/<\/b>/g) ?? []).length;
|
|
586
|
-
const iOpen = (s.match(/<i>/g) ?? []).length;
|
|
587
|
-
const iClose = (s.match(/<\/i>/g) ?? []).length;
|
|
588
|
-
return bOpen === bClose && iOpen === iClose;
|
|
566
|
+
// โโโ Extreme-edge: single oversized line with markdown-special chars โโโโโโโโโ
|
|
567
|
+
// These tests cover the fitCardToBudget fallback path that is reachable when a
|
|
568
|
+
// Bash label contains special chars (e.g. && is extremely common) and is
|
|
569
|
+
// longer than the budget. Post-#2669 the wire format is GFM markdown.
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Cheap valid-rich-markdown checker: the emphasis wrappers the renderer
|
|
573
|
+
* emits (`**โฆ**`, `_โฆ_`) must be balanced, and no escape sequence may be
|
|
574
|
+
* left dangling at the end (a trailing lone backslash would consume the
|
|
575
|
+
* closing marker).
|
|
576
|
+
*/
|
|
577
|
+
function isValidMarkdown(s: string): boolean {
|
|
578
|
+
// No dangling escape backslash at the very end.
|
|
579
|
+
// Count trailing backslashes โ an odd count means the final char escapes
|
|
580
|
+
// nothing real and would swallow the wrapper close.
|
|
581
|
+
const trailing = /(\\*)$/.exec(s)?.[1].length ?? 0;
|
|
582
|
+
if (trailing % 2 === 1) return false;
|
|
583
|
+
// Balanced ** bold wrappers (count of '**' must be even).
|
|
584
|
+
const bold = (s.match(/\*\*/g) ?? []).length;
|
|
585
|
+
if (bold % 2 !== 0) return false;
|
|
586
|
+
return true;
|
|
589
587
|
}
|
|
590
588
|
|
|
591
|
-
describe("extreme-edge: single oversized line with
|
|
592
|
-
it("renderActivityFeed: single ~4100-char line with special chars โ โค budget and valid
|
|
589
|
+
describe("extreme-edge: single oversized line with markdown specials & && _ *", () => {
|
|
590
|
+
it("renderActivityFeed: single ~4100-char line with special chars โ โค budget and valid markdown", () => {
|
|
593
591
|
// Build a raw line >4000 chars with &, <, >, and a trailing &&.
|
|
594
592
|
// The && will escape to && (5 chars each), exercising the expansion guard.
|
|
595
593
|
const base = "Run script with args: foo=bar && baz=<qux> & corge=1 ";
|
|
@@ -599,13 +597,13 @@ describe("extreme-edge: single oversized line with &, <, >, &&", () => {
|
|
|
599
597
|
const out = renderActivityFeed([bigLine])!;
|
|
600
598
|
expect(out).not.toBeNull();
|
|
601
599
|
expect(out.length).toBeLessThanOrEqual(4000); // STATUS_CARD_CHAR_BUDGET
|
|
602
|
-
expect(
|
|
603
|
-
// Must be wrapped in
|
|
604
|
-
expect(out.startsWith("
|
|
605
|
-
expect(out.endsWith("
|
|
600
|
+
expect(isValidMarkdown(out)).toBe(true);
|
|
601
|
+
// Must be wrapped in **โฆ** (live/non-final) โ no dangling marker.
|
|
602
|
+
expect(out.startsWith("**โ ")).toBe(true);
|
|
603
|
+
expect(out.endsWith("**")).toBe(true);
|
|
606
604
|
});
|
|
607
605
|
|
|
608
|
-
it("renderActivityFeed final=true: single ~4100-char line โ โค budget and valid
|
|
606
|
+
it("renderActivityFeed final=true: single ~4100-char line โ โค budget and valid markdown", () => {
|
|
609
607
|
const base = "Deploy build && upload && notify && cleanup: ";
|
|
610
608
|
const bigLine = base.repeat(90) + "done";
|
|
611
609
|
expect(bigLine.length).toBeGreaterThan(4000);
|
|
@@ -613,12 +611,12 @@ describe("extreme-edge: single oversized line with &, <, >, &&", () => {
|
|
|
613
611
|
const out = renderActivityFeed([bigLine], true)!;
|
|
614
612
|
expect(out).not.toBeNull();
|
|
615
613
|
expect(out.length).toBeLessThanOrEqual(4000);
|
|
616
|
-
expect(
|
|
617
|
-
expect(out.startsWith("
|
|
618
|
-
expect(out.endsWith("
|
|
614
|
+
expect(isValidMarkdown(out)).toBe(true);
|
|
615
|
+
expect(out.startsWith("_โ ")).toBe(true);
|
|
616
|
+
expect(out.endsWith("_")).toBe(true);
|
|
619
617
|
});
|
|
620
618
|
|
|
621
|
-
it("renderActivityFeedWithNested: single ~4100-char parent line โ โค budget and valid
|
|
619
|
+
it("renderActivityFeedWithNested: single ~4100-char parent line โ โค budget and valid markdown", () => {
|
|
622
620
|
const base = "Parent action with & < > special chars && bash: ";
|
|
623
621
|
const bigLine = base.repeat(85);
|
|
624
622
|
expect(bigLine.length).toBeGreaterThan(4000);
|
|
@@ -626,10 +624,10 @@ describe("extreme-edge: single oversized line with &, <, >, &&", () => {
|
|
|
626
624
|
const out = renderActivityFeedWithNested([bigLine], [])!;
|
|
627
625
|
expect(out).not.toBeNull();
|
|
628
626
|
expect(out.length).toBeLessThanOrEqual(4000);
|
|
629
|
-
expect(
|
|
627
|
+
expect(isValidMarkdown(out)).toBe(true);
|
|
630
628
|
});
|
|
631
629
|
|
|
632
|
-
it("renderActivityFeedWithNested: single ~4100-char child line โ โค budget and valid
|
|
630
|
+
it("renderActivityFeedWithNested: single ~4100-char child line โ โค budget and valid markdown", () => {
|
|
633
631
|
const base = "Child step: run build && test && deploy with <args> & flags=1 ";
|
|
634
632
|
const bigChild = base.repeat(65) + "&&";
|
|
635
633
|
expect(bigChild.length).toBeGreaterThan(4000);
|
|
@@ -637,19 +635,19 @@ describe("extreme-edge: single oversized line with &, <, >, &&", () => {
|
|
|
637
635
|
const out = renderActivityFeedWithNested(["Delegating: big job"], [bigChild])!;
|
|
638
636
|
expect(out).not.toBeNull();
|
|
639
637
|
expect(out.length).toBeLessThanOrEqual(4000);
|
|
640
|
-
expect(
|
|
638
|
+
expect(isValidMarkdown(out)).toBe(true);
|
|
641
639
|
// Must contain the nested prefix.
|
|
642
640
|
expect(out).toContain(" โณ ");
|
|
643
641
|
// Regression guard: an operator-precedence bug made wrapperOverhead a string
|
|
644
642
|
// (NESTED_PREFIX + n) โ NaN budget โ slice(0, NaN) === "" โ the child content
|
|
645
|
-
// was silently discarded, leaving only " โณ
|
|
643
|
+
// was silently discarded, leaving only " โณ **โ **". The empty-wrapper
|
|
646
644
|
// output still satisfies the toContain(" โณ ") check above, so assert that
|
|
647
645
|
// real child content actually survives.
|
|
648
646
|
expect(out).toContain("Child step");
|
|
649
647
|
expect(out.length).toBeGreaterThan(100);
|
|
650
648
|
});
|
|
651
649
|
|
|
652
|
-
it("renderActivityFeedWithNested final=true: single ~4100-char child line โ โค budget and valid
|
|
650
|
+
it("renderActivityFeedWithNested final=true: single ~4100-char child line โ โค budget and valid markdown", () => {
|
|
653
651
|
const base = "Final child: compile && link && package & ship: ";
|
|
654
652
|
const bigChild = base.repeat(85);
|
|
655
653
|
expect(bigChild.length).toBeGreaterThan(4000);
|
|
@@ -657,7 +655,7 @@ describe("extreme-edge: single oversized line with &, <, >, &&", () => {
|
|
|
657
655
|
const out = renderActivityFeedWithNested(["Delegating: big job"], [bigChild], true)!;
|
|
658
656
|
expect(out).not.toBeNull();
|
|
659
657
|
expect(out.length).toBeLessThanOrEqual(4000);
|
|
660
|
-
expect(
|
|
658
|
+
expect(isValidMarkdown(out)).toBe(true);
|
|
661
659
|
// final=true โ nested line renders done (italic, no โ).
|
|
662
660
|
expect(out).not.toContain("โ");
|
|
663
661
|
});
|
|
@@ -673,30 +671,30 @@ describe("extreme-edge: single oversized line with &, <, >, &&", () => {
|
|
|
673
671
|
describe("renderActivityHeader โ two-line header builder", () => {
|
|
674
672
|
it("renders the running header with elapsed and tool count", () => {
|
|
675
673
|
const [h1, h2] = renderActivityHeader("๐ค", "Agent", "", 15_000, 7, "running");
|
|
676
|
-
expect(h1).toBe("๐ค
|
|
677
|
-
expect(h2).toBe("
|
|
674
|
+
expect(h1).toBe("๐ค **Agent**");
|
|
675
|
+
expect(h2).toBe("_15s ยท 7 tools_");
|
|
678
676
|
});
|
|
679
677
|
|
|
680
678
|
it("renders the done header with tool count and elapsed", () => {
|
|
681
679
|
const [h1, h2] = renderActivityHeader("๐ค", "Agent", "", 65_000, 3, "done");
|
|
682
|
-
expect(h1).toBe("๐ค
|
|
683
|
-
expect(h2).toBe("
|
|
680
|
+
expect(h1).toBe("๐ค **Agent**");
|
|
681
|
+
expect(h2).toBe("_done ยท 3 tools ยท 1m05s_");
|
|
684
682
|
});
|
|
685
683
|
|
|
686
684
|
it("includes the description in line 1 when non-empty", () => {
|
|
687
685
|
const [h1] = renderActivityHeader("๐ ", "Worker", "run tests", 10_000, 2, "running");
|
|
688
|
-
expect(h1).toBe("๐
|
|
686
|
+
expect(h1).toBe("๐ **Worker** ยท _run tests_");
|
|
689
687
|
});
|
|
690
688
|
|
|
691
689
|
it("omits the description part when empty", () => {
|
|
692
690
|
const [h1] = renderActivityHeader("๐ค", "Agent", "", 5_000, 1, "running");
|
|
693
|
-
expect(h1).toBe("๐ค
|
|
691
|
+
expect(h1).toBe("๐ค **Agent**");
|
|
694
692
|
expect(h1).not.toContain(" ยท ");
|
|
695
693
|
});
|
|
696
694
|
|
|
697
|
-
it("
|
|
698
|
-
const [h1] = renderActivityHeader("๐ค", "Agent", "run
|
|
699
|
-
expect(h1).toContain("run
|
|
695
|
+
it("markdown-escapes emphasis specials in description and label", () => {
|
|
696
|
+
const [h1] = renderActivityHeader("๐ค", "Agent", "run a_b & c*d", 5_000, 1, "running");
|
|
697
|
+
expect(h1).toContain("run a\\_b & c\\*d");
|
|
700
698
|
});
|
|
701
699
|
});
|
|
702
700
|
|
|
@@ -711,35 +709,36 @@ describe("agent flat path routes through the shared step-feed primitive", () =>
|
|
|
711
709
|
);
|
|
712
710
|
// And the +N earlier marker is present on the flat agent surface.
|
|
713
711
|
expect(renderActivityFeed(lines)!).toContain(
|
|
714
|
-
|
|
712
|
+
`_โ +${lines.length - STATUS_ROLLING_LINES} earlierโฆ_`,
|
|
715
713
|
);
|
|
716
714
|
});
|
|
717
715
|
});
|
|
718
716
|
|
|
719
|
-
describe("escape
|
|
720
|
-
it("a step ending in '
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
|
|
717
|
+
describe("markdown escape is never split mid-clip (clip raw โ escape last, #2669)", () => {
|
|
718
|
+
it("a step ending in a markdown special '*' clipped at STATUS_LINE_MAX stays valid", () => {
|
|
719
|
+
// The clip happens on the RAW string and escaping is the LAST per-line op,
|
|
720
|
+
// so the trailing '*' that survives the clip is escaped to a clean '\*'
|
|
721
|
+
// โ never a dangling backslash that would eat the bold close marker.
|
|
722
|
+
const line = "x".repeat(STATUS_LINE_MAX) + "*";
|
|
724
723
|
const out = renderActivityFeed([line])!;
|
|
725
|
-
// The clip keeps STATUS_LINE_MAX-1 chars + 'โฆ', then escapes โ no stray entity.
|
|
726
|
-
expect(out).not.toMatch(/&$/);
|
|
727
|
-
expect(out).not.toMatch(/&am[^p]/);
|
|
728
724
|
expect(out).toContain("โฆ");
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
expect(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
725
|
+
// The clip lands before the trailing '*', so the only '*' chars present are
|
|
726
|
+
// the renderer's own ** wrappers (an even count) โ no stray odd escaped one.
|
|
727
|
+
const stars = (out.match(/\*/g) ?? []).length;
|
|
728
|
+
expect(stars % 2).toBe(0);
|
|
729
|
+
// No dangling escape backslash that would swallow a following marker.
|
|
730
|
+
expect(out).not.toMatch(/\\$/);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("a step whose clipped tail IS a markdown special escapes cleanly", () => {
|
|
734
|
+
// Place an underscore right at the clip boundary so the surviving char is a
|
|
735
|
+
// special โ it must be escaped as '\_' with a balanced backslash.
|
|
736
|
+
const line = "y".repeat(STATUS_LINE_MAX - 1) + "_more";
|
|
738
737
|
const out = renderActivityFeed([line])!;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
expect(
|
|
738
|
+
// Any backslash present must be paired with the special it escapes โ no
|
|
739
|
+
// odd trailing backslash.
|
|
740
|
+
const trailing = /(\\*)$/.exec(out)?.[1].length ?? 0;
|
|
741
|
+
expect(trailing % 2).toBe(0);
|
|
743
742
|
});
|
|
744
743
|
});
|
|
745
744
|
|
|
@@ -768,11 +767,11 @@ describe("renderActivityFeed โ header param (main-session card fix)", () => {
|
|
|
768
767
|
};
|
|
769
768
|
const out = renderActivityFeed(["Reading CLAUDE.md", "Searching memory"], false, "", undefined, header)!;
|
|
770
769
|
// Must start with the header lines.
|
|
771
|
-
expect(out).toContain("๐ค
|
|
772
|
-
expect(out).toContain("
|
|
770
|
+
expect(out).toContain("๐ค **Agent**");
|
|
771
|
+
expect(out).toContain("_15s ยท 7 tools_");
|
|
773
772
|
// Step feed follows the header.
|
|
774
|
-
expect(out).toContain("
|
|
775
|
-
expect(out).toContain("
|
|
773
|
+
expect(out).toContain("_โ Reading CLAUDE.md_");
|
|
774
|
+
expect(out).toContain("**โ Searching memory**");
|
|
776
775
|
});
|
|
777
776
|
|
|
778
777
|
it("prepends the done header when final=true", () => {
|
|
@@ -783,9 +782,9 @@ describe("renderActivityFeed โ header param (main-session card fix)", () => {
|
|
|
783
782
|
state: "done",
|
|
784
783
|
};
|
|
785
784
|
const out = renderActivityFeed(["Reading CLAUDE.md"], true, "", undefined, header)!;
|
|
786
|
-
expect(out).toContain("๐ค
|
|
787
|
-
expect(out).toContain("
|
|
788
|
-
expect(out).toContain("
|
|
785
|
+
expect(out).toContain("๐ค **Agent**");
|
|
786
|
+
expect(out).toContain("_done ยท 3 tools ยท 1m05s_");
|
|
787
|
+
expect(out).toContain("_โ Reading CLAUDE.md_");
|
|
789
788
|
// No in-progress arrow (final=true).
|
|
790
789
|
expect(out).not.toContain("โ");
|
|
791
790
|
});
|
|
@@ -800,7 +799,7 @@ describe("renderActivityFeed โ header param (main-session card fix)", () => {
|
|
|
800
799
|
// renderActivityFeed normally returns null for empty lines; with header it returns content.
|
|
801
800
|
const out = renderActivityFeed([], false, "", undefined, header);
|
|
802
801
|
expect(out).not.toBeNull();
|
|
803
|
-
expect(out).toContain("๐ค
|
|
802
|
+
expect(out).toContain("๐ค **Agent**");
|
|
804
803
|
});
|
|
805
804
|
|
|
806
805
|
it("without header, renderActivityFeed still returns null for empty lines (no regression)", () => {
|
|
@@ -822,12 +821,12 @@ describe("renderActivityFeed โ header param (main-session card fix)", () => {
|
|
|
822
821
|
undefined,
|
|
823
822
|
header,
|
|
824
823
|
)!;
|
|
825
|
-
expect(out).toContain("๐ค
|
|
826
|
-
expect(out).toContain("
|
|
824
|
+
expect(out).toContain("๐ค **Agent**");
|
|
825
|
+
expect(out).toContain("_30s ยท 5 tools_");
|
|
827
826
|
// Parent step is done-styled (child is the live step).
|
|
828
|
-
expect(out).toContain("
|
|
827
|
+
expect(out).toContain("_โ Delegating: review_");
|
|
829
828
|
// Child step is the in-progress step.
|
|
830
|
-
expect(out).toContain(" โณ
|
|
829
|
+
expect(out).toContain(" โณ **โ Reading schema.ts**");
|
|
831
830
|
});
|
|
832
831
|
});
|
|
833
832
|
|