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:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user