fix(dashboard): card timer matches stats panel + resizable Changes diff modal
Two related dashboard fixes. 1. Card timer mismatch: the board card timer chip showed only workflow runtime (e.g. <1m on FN-2716) while the task detail Stats panel reported "Total execution time" of 7m+ for the same task. Cause — the slim board listing strips `task.log` to keep payloads small, so the card's client-side `[timing]` log scan returned 0. Now the slim path aggregates `[timing] … in <N>ms` durations server-side into a new `task.timedExecutionMs` field before stripping the log; the card prefers this aggregate, falling back to the client scan when the full log is loaded (TaskDetailModal). Wire payload stays slim. 2. View Changes diff modal: defaulted to `90vw × 80vh` and was not user-resizable. Switched to `min(95vw, 2200px) × min(90vh, ...)` default with `resize: both`, persisted via useModalResizePersist (`fusion:changes-diff-modal-size`). Mobile keeps fullscreen layout. Overlay dismiss switched to the shared `useOverlayDismiss` hook so resize-drags that release on the overlay don't close the modal. Updated the diff modal's regression tests to match the new constraint shape (still asserts max-height clamps to viewport via calc()). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -635,6 +635,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
attachments: slim ? undefined : entry.attachments,
|
||||
comments: entry.comments,
|
||||
log: slim ? [] : entry.log ?? [],
|
||||
timedExecutionMs: slim ? this.computeTimedExecutionMs(entry.log) : undefined,
|
||||
createdAt: entry.createdAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
columnMovedAt: entry.columnMovedAt,
|
||||
@@ -835,9 +836,38 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId",
|
||||
"checkedOutBy", "checkedOutAt",
|
||||
// `log` is fetched in slim mode so the server can aggregate
|
||||
// `timedExecutionMs` from `[timing] … in <N>ms` entries before
|
||||
// returning. The log itself is stripped from the response —
|
||||
// see `listTasks()` slim post-processing.
|
||||
"log",
|
||||
].map((column) => `${prefix}${column}`).join(", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum the durations of all `[timing] … in <N>ms` (or `… after <N>ms`) log
|
||||
* entries. Returns 0 when no timing entries are present.
|
||||
*
|
||||
* Mirrors the client-side `getTimedDurationMs` so slim board listings can
|
||||
* report the same total-execution figure that the task detail Stats panel
|
||||
* computes from the full log.
|
||||
*/
|
||||
private computeTimedExecutionMs(log: import("./types.js").TaskLogEntry[] | undefined): number {
|
||||
if (!log || log.length === 0) return 0;
|
||||
let total = 0;
|
||||
for (const entry of log) {
|
||||
const action = typeof entry.action === "string" ? entry.action : "";
|
||||
const outcome = typeof entry.outcome === "string" ? entry.outcome : "";
|
||||
if (!action.includes("[timing]") && !outcome.includes("[timing]")) continue;
|
||||
const haystack = `${action}\n${outcome}`;
|
||||
const match = haystack.match(/(\d+(?:\.\d+)?)ms\b/i);
|
||||
if (!match) continue;
|
||||
const ms = Number(match[1]);
|
||||
if (Number.isFinite(ms)) total += ms;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private getTaskSelectClauseWithActivityLogLimit(limit: number): string {
|
||||
const columns = [
|
||||
"id", "title", "description", "priority", "\"column\"", "status", "size", "reviewLevel", "currentStep",
|
||||
@@ -2255,6 +2285,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const rows = this.db.prepare(sql).all(...params);
|
||||
const activeTasks = await Promise.all((rows as unknown as TaskRow[]).map(async (row) => {
|
||||
const task = this.rowToTask(row);
|
||||
|
||||
// Slim path: aggregate the timed-execution total server-side, then
|
||||
// strip the heavy log payload from the wire response. Without this
|
||||
// the board card has no way to display the same total-execution
|
||||
// figure that the task detail panel shows.
|
||||
if (slim) {
|
||||
task.timedExecutionMs = this.computeTimedExecutionMs(task.log);
|
||||
task.log = [];
|
||||
}
|
||||
|
||||
if (!slim || task.steps.length > 0) {
|
||||
return task;
|
||||
}
|
||||
@@ -2383,6 +2423,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
const activeMatches = await Promise.all(rows.map(async (row) => {
|
||||
const task = this.rowToTask(row);
|
||||
|
||||
// Slim path mirrors `listTasks`: aggregate timed execution server-side
|
||||
// before stripping the heavy log payload from the wire response.
|
||||
if (slim) {
|
||||
task.timedExecutionMs = this.computeTimedExecutionMs(task.log);
|
||||
task.log = [];
|
||||
}
|
||||
|
||||
if (task.steps.length > 0) {
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -754,6 +754,12 @@ export interface Task {
|
||||
/** Durable source provenance for the originating external issue. */
|
||||
sourceIssue?: TaskSourceIssue;
|
||||
log: TaskLogEntry[];
|
||||
/** Pre-aggregated sum of `[timing] … in <N>ms` log durations, in milliseconds.
|
||||
* Computed server-side so slim board listings can render the card timer
|
||||
* without shipping the full agent log. The TaskDetailModal still derives
|
||||
* this on the fly from `log`, so this field is only populated by the slim
|
||||
* list path and may be omitted on the full-detail object. */
|
||||
timedExecutionMs?: number;
|
||||
/** Durable aggregate token usage totals for the task. Undefined when no usage has been recorded yet. */
|
||||
tokenUsage?: TaskTokenUsage;
|
||||
size?: "S" | "M" | "L";
|
||||
|
||||
Reference in New Issue
Block a user