| @@ -8,10 +8,7 @@ import { · 7 unmodified lines |
| 8 | 8 | liveWorkEntryLabel, |
| 9 | 9 | normalizeCompactToolLabel, |
| 10 | 10 | resolveAssistantMessageCopyState, |
| 11 | | − resolveWorkGroupScrollIndex, |
| 12 | | − shouldFollowWorkGroupAppend, |
| 13 | | − shouldPreserveAssistantLineBreaks, |
| 14 | | − workEntryDisplayLabel, |
| 11 | + resolveTimelineToolPresentation, |
| 15 | 12 | } from "./MessagesTimeline.logic"; |
| 16 | 13 | import { |
| 17 | 14 | createMessageAttachmentPreviewProjector, |
| @@ -354,182 +351,6 @@ describe("streaming row projection", () => { · 336 unmodified lines |
| 354 | 351 | }); |
| 355 | 352 | }); |
| 356 | 353 | |
| 357 | | −describe("expanded tool group scrolling", () => { |
| 358 | | − const entries = [{ id: "first" }, { id: "second" }]; |
| 359 | | − |
| 360 | | − it("follows appended calls only at the hard end", () => { |
| 361 | | − const appended = [...entries, { id: "third" }]; |
| 362 | | − expect(shouldFollowWorkGroupAppend(entries, appended, 0)).toBe(true); |
| 363 | | − expect(shouldFollowWorkGroupAppend(entries, appended, 0.5)).toBe(true); |
| 364 | | − expect(shouldFollowWorkGroupAppend(entries, appended, 1)).toBe(true); |
| 365 | | − expect(shouldFollowWorkGroupAppend(entries, appended, 1.01)).toBe(false); |
| 366 | | − expect(shouldFollowWorkGroupAppend(entries, appended, 10)).toBe(false); |
| 367 | | − expect(shouldFollowWorkGroupAppend(entries, appended, Infinity)).toBe(false); |
| 368 | | − }); |
| 369 | | − |
| 370 | | − it("does not follow output updates, prepends, or replacements", () => { |
| 371 | | − expect( |
| 372 | | − shouldFollowWorkGroupAppend( |
| 373 | | − entries, |
| 374 | | − entries.map((entry) => ({ ...entry })), |
| 375 | | − 0, |
| 376 | | − ), |
| 377 | | − ).toBe(false); |
| 378 | | − expect(shouldFollowWorkGroupAppend(entries, [{ id: "older" }, ...entries], 0)).toBe(false); |
| 379 | | − expect( |
| 380 | | − shouldFollowWorkGroupAppend( |
| 381 | | − entries, |
| 382 | | − [{ id: "replacement" }, entries[1]!, { id: "third" }], |
| 383 | | − 0, |
| 384 | | − ), |
| 385 | | − ).toBe(false); |
| 386 | | − expect(shouldFollowWorkGroupAppend([], entries, 0)).toBe(false); |
| 387 | | − }); |
| 388 | | − |
| 389 | | − it("restores the visible tool and its offset inside expanded output", () => { |
| 390 | | − const anchor = { entryId: "second", offset: 120 }; |
| 391 | | − expect(resolveWorkGroupScrollIndex(entries, anchor)).toEqual({ index: 1, viewOffset: -120 }); |
| 392 | | − expect(resolveWorkGroupScrollIndex([{ id: "older" }, ...entries], anchor)).toEqual({ |
| 393 | | − index: 2, |
| 394 | | − viewOffset: -120, |
| 395 | | − }); |
| 396 | | − }); |
| 397 | | − |
| 398 | | − it("starts normally when the saved tool no longer exists", () => { |
| 399 | | − expect(resolveWorkGroupScrollIndex(entries, undefined)).toBeUndefined(); |
| 400 | | − expect( |
| 401 | | − resolveWorkGroupScrollIndex(entries, { entryId: "removed", offset: 120 }), |
| 402 | | − ).toBeUndefined(); |
| 403 | | − }); |
| 404 | | −}); |
| 405 | | − |
| 406 | | −describe("work entry labels", () => { |
| 407 | | − const entry = { |
| 408 | | − id: "tool-1", |
| 409 | | − createdAt: "2026-09-01T12:00:00Z", |
| 410 | | − label: "Tool call", |
| 411 | | − tone: "tool" as const, |
| 412 | | − }; |
| 413 | | − |
| 414 | | − it.each([ |
| 415 | | − ["inProgress", "Clicking in the preview browser"], |
| 416 | | − ["completed", "Clicked in the preview browser"], |
| 417 | | − ["failed", "Failed to click in the preview browser"], |
| 418 | | − ["declined", "Declined to click in the preview browser"], |
| 419 | | − ["stopped", "Stopped clicking in the preview browser"], |
| 420 | | − ] as const)("uses the same friendly %s label in both views", (toolLifecycleStatus, label) => { |
| 421 | | − const browserEntry = { |
| 422 | | − ...entry, |
| 423 | | − toolTitle: "T3-code.preview_click", |
| 424 | | − detail: '{"ok":true}', |
| 425 | | − toolLifecycleStatus, |
| 426 | | − }; |
| 427 | | − expect(liveWorkEntryLabel(browserEntry, undefined, toolLifecycleStatus === "inProgress")).toBe( |
| 428 | | − label, |
| 429 | | − ); |
| 430 | | − expect(workEntryDisplayLabel(browserEntry, undefined)).toBe(label); |
| 431 | | − }); |
| 432 | | − |
| 433 | | − it("uses the active summary state for legacy tools without a lifecycle status", () => { |
| 434 | | − const browserEntry = { ...entry, toolTitle: "T3-code.preview_click" }; |
| 435 | | − expect(liveWorkEntryLabel(browserEntry, undefined, true)).toBe( |
| 436 | | − "Clicking in the preview browser", |
| 437 | | − ); |
| 438 | | − expect(liveWorkEntryLabel(browserEntry, undefined, false)).toBe( |
| 439 | | − "Clicked in the preview browser", |
| 440 | | − ); |
| 441 | | − }); |
| 442 | | − |
| 443 | | − it("does not describe a finished call as still running while the turn continues", () => { |
| 444 | | − const browserEntry = { |
| 445 | | − ...entry, |
| 446 | | − toolTitle: "T3-code.preview_click", |
| 447 | | − toolLifecycleStatus: "completed" as const, |
| 448 | | − }; |
| 449 | | − expect(liveWorkEntryLabel(browserEntry, undefined, true)).toBe( |
| 450 | | − "Clicked in the preview browser", |
| 451 | | − ); |
| 452 | | − }); |
| 453 | | − |
| 454 | | − it("keeps custom titles and output for unrecognized tools", () => { |
| 455 | | − const unknownEntry = { ...entry, toolTitle: "mcp__github__search_issues" }; |
| 456 | | − expect(liveWorkEntryLabel(unknownEntry, undefined, true)).toBe("Mcp__github__search_issues"); |
| 457 | | − expect(workEntryDisplayLabel({ ...unknownEntry, detail: "Found 3 issues" }, undefined)).toBe( |
| 458 | | − "Found 3 issues", |
| 459 | | − ); |
| 460 | | − }); |
| 461 | | − |
| 462 | | − it("keeps command summaries compact without replacing the full command in expanded rows", () => { |
| 463 | | − const commandEntry = { ...entry, command: "vp test run", detail: "All tests passed" }; |
| 464 | | − expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe("Running vp"); |
| 465 | | − expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe("Ran vp"); |
| 466 | | − expect(workEntryDisplayLabel(commandEntry, undefined)).toBe("vp test run"); |
| 467 | | − }); |
| 468 | | − |
| 469 | | − it("summarizes the program inside a shell wrapper while preserving the expanded command", () => { |
| 470 | | − const command = "/bin/zsh -lc 'vp test run apps/web/src/session-logic.test.ts'"; |
| 471 | | − const commandEntry = { ...entry, command }; |
| 472 | | − expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe("Running vp"); |
| 473 | | − expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe("Ran vp"); |
| 474 | | − expect(workEntryDisplayLabel(commandEntry, undefined)).toBe(command); |
| 475 | | − }); |
| 476 | | − |
| 477 | | − it.each([ |
| 478 | | − ["inProgress", "Running vp"], |
| 479 | | − ["completed", "Ran vp"], |
| 480 | | − ["failed", "Failed vp"], |
| 481 | | − ["declined", "Declined vp"], |
| 482 | | − ["stopped", "Stopped vp"], |
| 483 | | − ] as const)( |
| 484 | | − "uses the command's %s outcome even while the turn continues", |
| 485 | | − (toolLifecycleStatus, label) => { |
| 486 | | − const commandEntry = { |
| 487 | | − ...entry, |
| 488 | | − command: "/bin/bash -lc 'vp test run'", |
| 489 | | − toolLifecycleStatus, |
| 490 | | − }; |
| 491 | | − expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe(label); |
| 492 | | − expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe(label); |
| 493 | | − }, |
| 494 | | − ); |
| 495 | | − |
| 496 | | − it("gives a completed browser group its own count and summary icon category", () => { |
| 497 | | − const rows = deriveMessagesTimelineRows({ |
| 498 | | − timelineEntries: [ |
| 499 | | − { |
| 500 | | − id: "browser-entry", |
| 501 | | − kind: "work", |
| 502 | | − createdAt: entry.createdAt, |
| 503 | | − entry: { |
| 504 | | − ...entry, |
| 505 | | − itemType: "mcp_tool_call", |
| 506 | | − toolLifecycleStatus: "completed", |
| 507 | | − toolData: { server: "t3-code", tool: "preview_click" }, |
| 508 | | − }, |
| 509 | | − }, |
| 510 | | − ], |
| 511 | | − isWorking: false, |
| 512 | | − activeTurnStartedAt: null, |
| 513 | | − turnDiffSummaryByAssistantMessageId: new Map(), |
| 514 | | − revertTurnCountByUserMessageId: new Map(), |
| 515 | | − }); |
| 516 | | − expect(rows).toMatchObject([ |
| 517 | | − { kind: "work-toggle", summary: "Used browser 1 time", summaryKind: "browser" }, |
| 518 | | − ]); |
| 519 | | − }); |
| 520 | | −}); |
| 521 | | − |
| 522 | | −describe("shouldPreserveAssistantLineBreaks", () => { |
| 523 | | − it("preserves Claude insight formatting without changing regular markdown", () => { |
| 524 | | − expect( |
| 525 | | − shouldPreserveAssistantLineBreaks( |
| 526 | | − "★ Insight ─────────────────\\nFirst observation\\nSecond observation\\n─────────────────", |
| 527 | | − ), |
| 528 | | − ).toBe(true); |
| 529 | | − expect(shouldPreserveAssistantLineBreaks("A normal\\nmarkdown paragraph")).toBe(false); |
| 530 | | − }); |
| 531 | | −}); |
| 532 | | − |
| 533 | 354 | describe("computeMessageDurationStart", () => { |
| 534 | 355 | it("returns message createdAt when there is no preceding user message", () => { |
| 535 | 356 | const result = computeMessageDurationStart([ |
| @@ -729,6 +550,33 @@ describe("normalizeCompactToolLabel", () => { · 193 unmodified lines |
| 729 | 550 | }); |
| 730 | 551 | }); |
| 731 | 552 | |
| 553 | +describe("resolveTimelineToolPresentation", () => { |
| 554 | + it("pretty prints Claude and Cursor T3 MCP tool names", () => { |
| 555 | + expect(resolveTimelineToolPresentation("mcp__t3-code__t3_thread_read")).toEqual({ |
| 556 | + displayName: "Read a T3 thread", |
| 557 | + logo: "t3-code", |
| 558 | + }); |
| 559 | + }); |
| 560 | + |
| 561 | + it("pretty prints Codex T3 MCP tool names", () => { |
| 562 | + expect(resolveTimelineToolPresentation("t3-code.create_threads")).toEqual({ |
| 563 | + displayName: "Create T3 threads", |
| 564 | + logo: "t3-code", |
| 565 | + }); |
| 566 | + }); |
| 567 | + |
| 568 | + it("pretty prints bare T3 MCP toolkit names", () => { |
| 569 | + expect(resolveTimelineToolPresentation("list_scheduled_tasks")).toEqual({ |
| 570 | + displayName: "List scheduled tasks", |
| 571 | + logo: "t3-code", |
| 572 | + }); |
| 573 | + }); |
| 574 | + |
| 575 | + it("keeps unknown MCP tools on the generic renderer path", () => { |
| 576 | + expect(resolveTimelineToolPresentation("mcp__github__search_issues")).toBeNull(); |
| 577 | + }); |
| 578 | +}); |
| 579 | + |
| 732 | 580 | describe("resolveAssistantMessageCopyState", () => { |
| 733 | 581 | it("returns enabled copy state for completed assistant messages", () => { |
| 734 | 582 | expect( |