feat(FN-1019): reflow Changes-tab header stats and fix terminal scrollback

- Reflow Changes-tab header stats to a second line for better layout
- Add CSS styles for two-line header stats display
- Fix useTerminal hook to clear buffered scrollback after first subscriber replay
- Add tests for TaskChangesTab header stats rendering
- Update README description for Changes tab header
This commit is contained in:
gsxdsm
2026-04-05 22:58:28 -07:00
parent 904e4c8e00
commit ec33691033
5 changed files with 60 additions and 6 deletions

View File

@@ -196,14 +196,16 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
)}
<div className="changes-header">
<h4>
<FileCode size={16} />
Files Changed ({stats.filesChanged})
<span className="changes-stat-summary">
<div className="task-changes-header-title">
<h4>
<FileCode size={16} />
Files Changed ({stats.filesChanged})
</h4>
<span className="task-changes-stats changes-stat-summary">
<span className="diff-add">+{stats.additions}</span>{" "}
<span className="diff-del">-{stats.deletions}</span>
</span>
</h4>
</div>
<div className="changes-header-actions-wrapper">
<div className="changes-header-actions">
{files.length > 0 && (

View File

@@ -216,6 +216,39 @@ describe("TaskChangesTab — commit-backed (done tasks)", () => {
expect(screen.getByText("-0")).toBeTruthy();
});
it("renders stat summary on a separate line from Files Changed title", async () => {
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
const { container } = render(
<TaskChangesTab
taskId="FN-001"
worktree={undefined}
column="done"
mergeDetails={MERGE_DETAILS}
/>,
);
await waitFor(() => {
expect(screen.getByText("Files Changed (2)")).toBeTruthy();
});
// The title and stats should be in a dedicated wrapper
const titleWrapper = container.querySelector(".task-changes-header-title");
expect(titleWrapper).toBeTruthy();
// The h4 should contain "Files Changed (N)" but NOT the stat summary
const h4 = titleWrapper?.querySelector("h4");
expect(h4).toBeTruthy();
expect(h4?.textContent).toContain("Files Changed (2)");
expect(h4?.querySelector(".changes-stat-summary")).toBeNull();
// The stat summary should be a sibling of h4, not a child
const statSummary = titleWrapper?.querySelector(".task-changes-stats");
expect(statSummary).toBeTruthy();
expect(statSummary?.querySelector(".diff-add")?.textContent).toBe("+3");
expect(statSummary?.querySelector(".diff-del")?.textContent).toBe("-0");
});
it("toggling file expansion shows/hides diff content", async () => {
mockFetchTaskDiff.mockResolvedValue(DONE_TASK_DIFF);
const { container } = render(

View File

@@ -117,6 +117,8 @@ export function useTerminal(sessionId: string | null): UseTerminalReturn {
const buffer = initialBufferRef.current;
if (buffer.connected) {
callback(buffer.connected);
// Clear after replay to prevent stale re-delivery to subsequent subscribers
buffer.connected = null;
}
return () => onConnectCallbacksRef.current.delete(callback);
}, []);
@@ -127,6 +129,8 @@ export function useTerminal(sessionId: string | null): UseTerminalReturn {
const buffer = initialBufferRef.current;
if (buffer.scrollback) {
callback(buffer.scrollback);
// Clear after replay to prevent stale re-delivery to subsequent subscribers
buffer.scrollback = null;
}
return () => onScrollbackCallbacksRef.current.delete(callback);
}, []);

View File

@@ -3715,6 +3715,21 @@ body {
color: var(--status-color-deleted, #f85149);
}
/* Task-specific changes header: stacked title + stats layout */
.task-changes-tab .task-changes-header-title {
display: flex;
flex-direction: column;
gap: 2px;
}
.task-changes-tab .task-changes-header-title h4 {
margin: 0;
}
.task-changes-tab .task-changes-stats {
margin-left: calc(16px + var(--space-sm)); /* align with text after icon + gap */
}
.changes-file-path {
flex: 1;
min-width: 0;