feat(FN-783): make agent log fill full modal body height

- Add CSS classes for full-height agent log layout with flex-based stretching
- Update AgentLogViewer and TaskDetailModal to use full-height container classes
- Add regression tests for AgentLogViewer full-height behavior
- Add regression tests for TaskDetailModal agent log panel height
- Update dashboard README with agent log layout documentation
This commit is contained in:
gsxdsm
2026-04-03 12:38:01 -07:00
parent d4feee0999
commit db2924549e
6 changed files with 152 additions and 5 deletions

View File

@@ -47,7 +47,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
- **AI-Assisted Creation Controls**: Plan, Subtask, and Refine buttons appear directly below the description textarea in all task creation surfaces (quick entry box, inline create card, and task form modal). These description-adjacent controls make AI-assisted creation and refinement feel directly associated with the text being edited. Deps, Models, and Save actions remain in the expanded controls footer.
- **Layered Model Dropdowns**: Shared model combobox menus render in a top-level portal attached to `document.body`, so they stay above board columns and scrollable modal content instead of being clipped behind surrounding dashboard surfaces.
- **Bulk Model Editing**: Update AI model configuration for multiple tasks at once in the list view. Select tasks via checkboxes (archived tasks excluded), then use the "Bulk Edit Models" toolbar to apply executor and/or validator model changes to all selected tasks. Selection persists in localStorage across page reloads.
- **Task Details**: View full task specifications, agent logs, and attachments. The Agent Log tab header shows the effective executor and validator model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime.
- **Task Details**: View full task specifications, agent logs, and attachments. The Agent Log tab expands to fill the full modal body height above the action bar, providing maximum vertical space for watching live agent output. The tab header shows the effective executor and validator model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime.
- **Changed Files Viewer**: Click a task card's "files changed" button to open a dedicated diff viewer showing only files changed in that task worktree, with per-file statuses and sidebar navigation. On mobile (≤768px), the viewer switches to a single-pane flow: the file list and diff are shown one at a time with a back button for navigation between them
- **GitHub Import**: Import issues directly from GitHub repositories
- **PR Management**: Create, monitor, and merge pull requests for in-review tasks

View File

@@ -100,7 +100,7 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
return (
<div
ref={containerRef}
className="agent-log-viewer"
className="agent-log-viewer agent-log-viewer--streaming"
data-testid="agent-log-viewer"
style={{
fontFamily: "monospace",
@@ -108,7 +108,6 @@ export function AgentLogViewer({ entries, loading, executorModel, validatorModel
lineHeight: "1.5",
overflowY: "auto",
overflowX: "hidden",
maxHeight: "500px",
padding: "12px",
background: "var(--bg-secondary)",
borderRadius: "6px",

View File

@@ -677,7 +677,7 @@ export function TaskDetailModal({
</button>
</div>
</div>
<div className="detail-body">
<div className={`detail-body${activeTab === "agent-log" && !isEditing ? " detail-body--agent-log" : ""}`}>
{isEditing ? (
<div className="modal-edit-form">
<TaskForm
@@ -799,7 +799,7 @@ export function TaskDetailModal({
<ModelSelectorTab task={task} addToast={addToast} />
</div>
) : activeTab === "agent-log" ? (
<div className="detail-section">
<div className="detail-section detail-section--agent-log">
<AgentLogViewer
entries={agentLogEntries}
loading={agentLogLoading}

View File

@@ -365,6 +365,41 @@ describe("AgentLogViewer", () => {
});
});
describe("full-height layout", () => {
it("does not have a fixed maxHeight constraint", () => {
const entries = [makeEntry()];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
// The viewer should NOT have a maxHeight of 500px (the old fixed constraint)
expect(viewer.style.maxHeight).not.toBe("500px");
// maxHeight should be empty (unset) so the viewer can grow to fill available space
expect(viewer.style.maxHeight).toBe("");
});
it("maintains overflow-y auto for internal scrolling", () => {
const entries = [makeEntry()];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
// The viewer itself should remain the scroll container
expect(viewer.style.overflowY).toBe("auto");
});
it("uses agent-log-viewer--streaming class when entries are present", () => {
const entries = [makeEntry()];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
expect(viewer.classList.contains("agent-log-viewer")).toBe(true);
expect(viewer.classList.contains("agent-log-viewer--streaming")).toBe(true);
});
it("does not use streaming class on loading state", () => {
const { container } = render(<AgentLogViewer entries={[]} loading={true} />);
const viewer = container.querySelector("[data-testid='agent-log-viewer']") as HTMLElement;
expect(viewer.classList.contains("agent-log-viewer")).toBe(true);
expect(viewer.classList.contains("agent-log-viewer--streaming")).toBe(false);
});
});
describe("auto-scroll behavior", () => {
it("scrolls to top when new entries arrive and user is near the top", () => {
const { rerender, container } = render(<AgentLogViewer entries={[makeEntry({ text: "first" })]} loading={false} />);

View File

@@ -900,6 +900,84 @@ describe("TaskDetailModal", () => {
});
});
describe("Agent Log full-height layout", () => {
it("applies detail-body--agent-log class when Agent Log tab is active", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ prompt: "# Hello\n\nContent" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
// Initially, detail-body should NOT have the agent-log modifier
expect(container.querySelector(".detail-body--agent-log")).toBeNull();
// Switch to Agent Log tab
fireEvent.click(screen.getByText("Agent Log"));
// detail-body should now have the agent-log modifier class
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
// Switch back to Definition tab
fireEvent.click(screen.getByText("Definition"));
// modifier class should be removed
expect(container.querySelector(".detail-body--agent-log")).toBeNull();
});
it("wraps AgentLogViewer in detail-section--agent-log class", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ prompt: "# Hello\n\nContent" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
// Switch to Agent Log tab
fireEvent.click(screen.getByText("Agent Log"));
// The section wrapping AgentLogViewer should have the full-height class
const section = container.querySelector(".detail-section--agent-log");
expect(section).toBeTruthy();
expect(section!.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
});
it("does not apply detail-body--agent-log when editing", () => {
const { container } = render(
<TaskDetailModal
task={makeTask({ column: "triage", prompt: "# Hello\n\nContent" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
// Switch to Agent Log tab first
fireEvent.click(screen.getByText("Agent Log"));
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
// Now enter edit mode via the pencil button in the header
const editBtn = screen.getByLabelText("Edit task");
fireEvent.click(editBtn);
// The detail-body--agent-log class should be removed while editing
expect(container.querySelector(".detail-body--agent-log")).toBeNull();
});
});
describe("Agent Log model resolution", () => {
// AgentLogViewer only renders the model header when entries.length > 0,
// so we mock useAgentLogs to return at least one entry.

View File

@@ -3060,6 +3060,15 @@ body {
flex: 1;
}
/* When the Agent Log tab is active, switch to flex layout so the log section
can stretch to fill the remaining space above the action bar.
The detail-body itself should NOT scroll; the agent-log-viewer scrolls instead. */
.detail-body--agent-log {
display: flex;
flex-direction: column;
overflow-y: hidden;
}
.detail-title {
font-size: 18px;
font-weight: 600;
@@ -3113,6 +3122,32 @@ body {
margin-top: var(--space-lg);
}
/* Agent Log tab: stretch to fill the remaining modal body height
so the log viewer uses all available space above the action bar. */
.detail-section--agent-log {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
margin-top: var(--space-lg);
}
.detail-section--agent-log .agent-log-viewer {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
font-family: monospace;
font-size: 13px;
line-height: 1.5;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
.detail-spec-edit-trigger {
margin-bottom: var(--space-md);
}