feat(dashboard,cli): U13 — schema-driven task fields UI (TaskFieldsSection, card badges, PATCH route, board-workflows fields payload, TUI chips)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-04 12:26:05 -07:00
parent 2cfa8a3282
commit 5fa5740afc
26 changed files with 1613 additions and 8 deletions

View File

@@ -1613,6 +1613,15 @@ function TaskDetailScreen({
</Box>
)}
{/* Card-placed custom fields (U13/KTD-14): read-only bracketed labels. */}
{detail.customFields && detail.customFields.length > 0 && (
<Box flexDirection="row" gap={1} flexWrap="wrap" flexShrink={0}>
{detail.customFields.map((f) => (
<Text key={f.label} color="magenta">[{f.label}: {f.value}]</Text>
))}
</Box>
)}
<Box height={1} flexShrink={0} />
{/* Steps section */}

View File

@@ -230,6 +230,10 @@ export interface TaskDetailData {
currentStepIndex?: number;
steps: TaskStep[];
recentLogs: TaskLogEntry[]; // last ~200 entries on initial load
/** Card-placed custom field values, pre-rendered as read-only bracketed
* labels for the task detail view (U13/KTD-14). Absent/empty when the
* workflow declares no card fields or none have values. */
customFields?: Array<{ label: string; value: string }>;
}
export type TaskEvent =

View File

@@ -19,6 +19,7 @@ import {
isWorkflowColumnsEnabled,
resolveColumnFlags,
BUILTIN_CODING_WORKFLOW_IR,
parseWorkflowIr,
type WorkflowIrColumn,
type TraitFlags,
} from "@fusion/core";
@@ -2742,6 +2743,48 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
text: entry.outcome ? `${entry.action} → ${entry.outcome}` : entry.action,
source: entry.runContext?.agentId ? "agent" : "executor",
}));
// Card-placed custom fields → read-only bracketed labels
// (U13/KTD-14). Resolve the task's workflow IR, filter
// card-placed field defs, and render any present values.
// Best-effort: any resolution failure simply omits the chips.
let customFields: Array<{ label: string; value: string }> | undefined;
try {
const values = (t as { customFields?: Record<string, unknown> }).customFields;
if (values && Object.keys(values).length > 0) {
const selection = projectStore.getTaskWorkflowSelection(t.id);
const def = selection?.workflowId
? await projectStore.getWorkflowDefinition(selection.workflowId)
: undefined;
const ir = def
? (typeof def.ir === "string" ? parseWorkflowIr(def.ir) : def.ir)
: BUILTIN_CODING_WORKFLOW_IR;
const fields = ir.version === "v2" ? (ir.fields ?? []) : [];
const chips: Array<{ label: string; value: string }> = [];
for (const field of fields) {
if (field.render?.placement !== "card") continue;
const raw = values[field.id];
if (raw === undefined || raw === null || raw === "") continue;
const optLabel = (v: string): string =>
field.options?.find((o) => o.value === v)?.label ?? v;
let display: string;
if (field.type === "boolean") {
if (raw !== true) continue;
display = field.name;
} else if (field.type === "multi-enum" && Array.isArray(raw)) {
if (raw.length === 0) continue;
display = raw.map((v) => optLabel(String(v))).join(", ");
} else if (field.type === "enum") {
display = optLabel(String(raw));
} else {
display = String(raw);
}
chips.push({ label: field.name, value: display });
}
if (chips.length > 0) customFields = chips;
}
} catch {
customFields = undefined;
}
return {
id: t.id,
title: t.title,
@@ -2753,6 +2796,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
currentStepIndex: t.currentStep,
steps,
recentLogs,
...(customFields ? { customFields } : {}),
};
} catch {
// Task not found (deleted/archived between selection and fetch).