Skip to content
Open
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
19979bb
Port file and command activity boxes
Quicksaver Jun 18, 2026
90e55b1
Add browser coverage for activity boxes
Quicksaver Jun 18, 2026
8d29c2d
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
86c8d7e
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
69f6014
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
be445eb
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
500bb58
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
3f8ad3a
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
19abc64
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 19, 2026
d1583db
Skip blank stdout in command output extraction
Quicksaver Jun 20, 2026
c0fe7fa
Preserve whitespace command stream chunks
Quicksaver Jun 20, 2026
de5480b
Fix streamed command output merging
Quicksaver Jun 20, 2026
7ae5682
Revert "Fix streamed command output merging"
Quicksaver Jun 20, 2026
58d0272
Revert "Preserve whitespace command stream chunks"
Quicksaver Jun 20, 2026
aeb2b11
Revert "Skip blank stdout in command output extraction"
Quicksaver Jun 20, 2026
4ff626c
Reapply "Skip blank stdout in command output extraction"
Quicksaver Jun 20, 2026
206b4e2
Reapply "Preserve whitespace command stream chunks"
Quicksaver Jun 20, 2026
69ecd2f
Reapply "Fix streamed command output merging"
Quicksaver Jun 20, 2026
065af3e
Fix regressive command output snapshots
Quicksaver Jun 20, 2026
5661ec9
Fix repeated prefix command snapshots
Quicksaver Jun 20, 2026
bc958ca
Preserve prefix chunks after newline output
Quicksaver Jun 20, 2026
3ab6958
Handle repeated prefixes in multiline chunks
Quicksaver Jun 20, 2026
d6ada8b
Refine multiline prefix snapshot detection
Quicksaver Jun 20, 2026
995bfb2
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 21, 2026
a130e61
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 25, 2026
6748522
Port command activity review fixes
Quicksaver Jun 25, 2026
3452e61
Refine command activity output merging
Quicksaver Jun 25, 2026
70b8635
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 25, 2026
81e7486
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 26, 2026
4c033af
Refactor file command activity boxes
Quicksaver Jun 26, 2026
f379709
Exclude collab-agent rows from detail boxes
Quicksaver Jun 26, 2026
adc6981
Harden activity detail expansion
Quicksaver Jun 26, 2026
7ae4dd6
Address activity detail review comments
Quicksaver Jun 26, 2026
546dbe5
Fix activity detail review issues
Quicksaver Jun 26, 2026
da52c27
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 27, 2026
2c15fd9
Refactor timeline work entry details
Quicksaver Jun 27, 2026
03d41e8
Defer command row detail derivation until expansion
Quicksaver Jun 27, 2026
a52db95
Fix activity row previews and keyboard toggles
Quicksaver Jun 27, 2026
9d4ab99
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jun 27, 2026
026ec2d
Export file diff path matcher
Quicksaver Jun 27, 2026
adf7132
Share diff path matching for changed files
Quicksaver Jun 27, 2026
79290ee
Expand chat timeline anchoring and layout
Quicksaver Jun 29, 2026
74aadfa
Fix timeline anchor and minimap interactions
Quicksaver Jun 30, 2026
31c71a0
Cancel live follow on keyboard and scrollbar scrolls
Quicksaver Jun 30, 2026
6eabd53
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jul 1, 2026
bead72d
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jul 3, 2026
477d4d5
Extract work log detail helpers
Quicksaver Jul 3, 2026
0736454
Harden work log detail rendering
Quicksaver Jul 3, 2026
920bbff
Dedupe root activity diff chips
Quicksaver Jul 3, 2026
81daf1c
Remove activity branch pollution
Quicksaver Jul 3, 2026
dd63456
Fix work log detail classification
Quicksaver Jul 3, 2026
c2e921d
Merge remote-tracking branch 'upstream/main' into split/file-command-…
Quicksaver Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
598 changes: 598 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.logic.test.ts

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "../../session-logic";
import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";
import { type MessageId, type OrchestrationLatestTurn, type TurnId } from "@t3tools/contracts";
import { formatWorkspaceRelativePath } from "../../filePathDisplay";

export const MAX_VISIBLE_WORK_LOG_ENTRIES = 1;
export const TIMELINE_MINIMAP_ITEM_SPACING = 8;
Expand Down Expand Up @@ -166,6 +167,83 @@ export function normalizeCompactToolLabel(value: string): string {
return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();
}

function capitalizePhrase(value: string): string {
const trimmed = value.trim();
if (trimmed.length === 0) {
return value;
}
return `${trimmed.charAt(0).toUpperCase()}${trimmed.slice(1)}`;
}

export function deriveToolWorkEntryHeading(workEntry: WorkLogEntry): string {
if (!workEntry.toolTitle) {
return capitalizePhrase(normalizeCompactToolLabel(workEntry.label));
}
return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle));
}

export function deriveWorkEntryPreview(
workEntry: Pick<WorkLogEntry, "detail" | "command" | "changedFiles" | "itemType" | "requestKind">,
workspaceRoot: string | undefined,
): string | null {
const changedFilesPreview = deriveChangedFilesPreview(workEntry, workspaceRoot);
if (workEntry.itemType === "file_change" || workEntry.requestKind === "file-change") {
return changedFilesPreview ?? workEntry.command ?? workEntry.detail ?? null;
}
if (workEntry.command) return workEntry.command;
if (workEntry.detail) return workEntry.detail;
return changedFilesPreview;
}

export interface DerivedWorkEntryDisplay {
heading: string;
preview: string | null;
displayText: string;
}

export function deriveWorkEntryDisplay(
workEntry: WorkLogEntry,
workspaceRoot: string | undefined,
): DerivedWorkEntryDisplay {
const heading = deriveToolWorkEntryHeading(workEntry);
const rawPreview = deriveWorkEntryPreview(workEntry, workspaceRoot);
const preview =
rawPreview &&
normalizeCompactToolLabel(rawPreview).toLowerCase() ===
normalizeCompactToolLabel(heading).toLowerCase()
? null
: rawPreview;

return {
heading,
preview,
displayText: preview ? `${heading} - ${preview}` : heading,
};
}

function deriveChangedFilesPreview(
workEntry: Pick<WorkLogEntry, "changedFiles">,
workspaceRoot: string | undefined,
): string | null {
if ((workEntry.changedFiles?.length ?? 0) === 0) return null;
const [firstPath] = workEntry.changedFiles ?? [];
if (!firstPath) return null;
const displayPath = formatWorkspaceRelativePath(firstPath, workspaceRoot);
return workEntry.changedFiles!.length === 1
? displayPath
: `${displayPath} +${workEntry.changedFiles!.length - 1} more`;
Comment thread
Quicksaver marked this conversation as resolved.
}

export function shouldToggleWorkEntryRowFromKeyDown({
key,
targetIsCurrentTarget,
}: {
key: string;
targetIsCurrentTarget: boolean;
}): boolean {
return targetIsCurrentTarget && (key === "Enter" || key === " ");
}

export function resolveAssistantMessageCopyState({
text,
showCopyButton,
Expand Down
128 changes: 128 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ beforeAll(() => {
documentElement: {
classList,
offsetHeight: 0,
removeAttribute: () => {},
setAttribute: () => {},
},
});
});
Expand Down Expand Up @@ -459,6 +461,132 @@ describe("MessagesTimeline", () => {
expect(markup).not.toContain("C:/Users/mike/dev-stuff/t3code/apps/web/src/session-logic.ts");
});

it("renders command work entries as expandable rows", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const stdout = Array.from({ length: 45 }, (_, index) => `stdout ${index + 1}`).join("\n");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
{
id: "entry-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: "work-1",
createdAt: "2026-03-17T19:12:28.000Z",
label: "Ran command",
tone: "tool",
itemType: "command_execution",
command: "vp test",
stdout,
stderr: "warning",
exitCode: 0,
durationMs: 1234,
},
},
]}
/>,
);

expect(markup).toContain("Ran command");
expect(markup).toContain("vp test");
expect(markup).toContain('aria-expanded="false"');
expect(markup).toContain('aria-label="Expand Ran command - vp test"');
});

it("renders dynamic tool command metadata as expandable command rows", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
{
id: "entry-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: "work-1",
createdAt: "2026-03-17T19:12:28.000Z",
label: "Dynamic tool",
tone: "tool",
itemType: "dynamic_tool_call",
command: "vp test",
stdout: "passed",
exitCode: 0,
durationMs: 1234,
},
},
]}
/>,
);

expect(markup).toContain("Dynamic tool");
expect(markup).toContain("vp test");
expect(markup).toContain('aria-expanded="false"');
expect(markup).toContain('aria-label="Expand Dynamic tool - vp test"');
});

it("does not render typed non-command stdout as command details", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
{
id: "entry-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: "work-1",
createdAt: "2026-03-17T19:12:28.000Z",
label: "Web search",
tone: "tool",
itemType: "web_search",
stdout: "search results",
durationMs: 1234,
},
},
]}
/>,
);

expect(markup).toContain("Web search");
expect(markup).not.toContain('aria-expanded="false"');
expect(markup).not.toContain('aria-label="Expand Web search"');
});

it("renders file-change work entries as expandable rows", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
{
id: "entry-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: {
id: "work-1",
createdAt: "2026-03-17T19:12:28.000Z",
label: "Changed files",
tone: "tool",
itemType: "file_change",
changedFiles: ["apps/web/src/session-logic.ts"],
patch:
"diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts\n--- a/apps/web/src/session-logic.ts\n+++ b/apps/web/src/session-logic.ts\n@@ -1 +1 @@\n-old\n+new\n",
},
},
]}
/>,
);

expect(markup).toContain("Changed files");
expect(markup).toContain("apps/web/src/session-logic.ts");
expect(markup).toContain('aria-expanded="false"');
expect(markup).toContain('aria-label="Expand Changed files - apps/web/src/session-logic.ts"');
});

it("renders review comment contexts as structured cards instead of raw tags", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
Expand Down
Loading
Loading