Merge remote-tracking branch 'origin/v0.3' into feat/2204-caserun-runtime-observation
This commit is contained in:
@@ -27,9 +27,16 @@ test("Agent observer relation tree consumes the YAML node limit", () => {
|
||||
assert.equal(window.rows.length, 3);
|
||||
});
|
||||
|
||||
test("Agent observer relation tree keeps a stable order across live activity reordering", () => {
|
||||
const first = buildRelationWindow([observerRun("run_2"), observerRun("run_1")], null, new Set(), new Set(), 10);
|
||||
const second = buildRelationWindow([observerRun("run_1"), observerRun("run_2")], null, new Set(), new Set(), 10);
|
||||
assert.deepEqual(first.rows.map((row) => row.id), second.rows.map((row) => row.id));
|
||||
});
|
||||
|
||||
function observerRun(id: string): AgentObserverRun {
|
||||
return {
|
||||
id,
|
||||
title: null,
|
||||
taskId: null,
|
||||
taskStatus: null,
|
||||
commandId: null,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick } from "vue";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import dagre from "@dagrejs/dagre";
|
||||
import { Handle, MarkerType, Position, VueFlow, useVueFlow, type Edge, type Node } from "@vue-flow/core";
|
||||
import { Focus, Minus, Plus } from "lucide-vue-next";
|
||||
@@ -32,23 +32,35 @@ const emit = defineEmits<{
|
||||
const flowId = "agentrun-mind-map";
|
||||
const { fitView, zoomIn, zoomOut } = useVueFlow({ id: flowId });
|
||||
const runById = computed(() => new Map(props.runs.map((run) => [run.id, run])));
|
||||
const layoutCache = new Map<string, Map<string, { x: number; y: number }>>();
|
||||
const initialFitDone = ref(false);
|
||||
|
||||
const graph = computed(() => {
|
||||
const layout = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
layout.setGraph({ rankdir: "LR", ranksep: 74, nodesep: 24, edgesep: 12, marginx: 32, marginy: 32 });
|
||||
|
||||
for (const row of props.rows) layout.setNode(row.id, { width: nodeWidth(row), height: 70 });
|
||||
for (const row of props.rows) if (row.parentId && layout.hasNode(row.parentId)) layout.setEdge(row.parentId, row.id);
|
||||
dagre.layout(layout);
|
||||
// 状态和消息更新只刷新节点内容,不重新计算位置。
|
||||
const layoutKey = props.rows.map((row) => `${row.id}|${row.parentId ?? ""}|${row.kind}|${row.childCount}|${row.expanded}`).join("\n");
|
||||
let positions = layoutCache.get(layoutKey);
|
||||
if (!positions) {
|
||||
const layout = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
layout.setGraph({ rankdir: "LR", ranksep: 74, nodesep: 24, edgesep: 12, marginx: 32, marginy: 32 });
|
||||
for (const row of props.rows) layout.setNode(row.id, { width: nodeWidth(row), height: 70 });
|
||||
for (const row of props.rows) if (row.parentId && layout.hasNode(row.parentId)) layout.setEdge(row.parentId, row.id);
|
||||
dagre.layout(layout);
|
||||
positions = new Map(props.rows.map((row) => {
|
||||
const position = layout.node(row.id);
|
||||
return [row.id, { x: position.x - position.width / 2, y: position.y - position.height / 2 }];
|
||||
}));
|
||||
layoutCache.set(layoutKey, positions);
|
||||
if (layoutCache.size > 8) layoutCache.delete(layoutCache.keys().next().value!);
|
||||
}
|
||||
|
||||
const nodes: Node<MindMapNodeData>[] = props.rows.map((row) => {
|
||||
const position = layout.node(row.id);
|
||||
const position = positions.get(row.id) ?? { x: 0, y: 0 };
|
||||
const run = row.runId ? runById.value.get(row.runId) : undefined;
|
||||
const title = row.kind === "run" && run?.title ? run.title : row.label;
|
||||
return {
|
||||
id: row.id,
|
||||
type: "observer",
|
||||
position: { x: position.x - position.width / 2, y: position.y - position.height / 2 },
|
||||
position,
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
selectable: false,
|
||||
@@ -57,7 +69,7 @@ const graph = computed(() => {
|
||||
};
|
||||
});
|
||||
const edges: Edge[] = props.rows
|
||||
.filter((row) => row.parentId && layout.hasNode(row.parentId))
|
||||
.filter((row) => row.parentId && positions.has(row.parentId))
|
||||
.map((row) => ({
|
||||
id: `${row.parentId}->${row.id}`,
|
||||
source: row.parentId!,
|
||||
@@ -89,6 +101,15 @@ async function fit(): Promise<void> {
|
||||
await nextTick();
|
||||
await fitView({ padding: 0.14, duration: 220, maxZoom: 1 });
|
||||
}
|
||||
|
||||
async function fitInitial(): Promise<void> {
|
||||
if (initialFitDone.value || !props.rows.length) return;
|
||||
await fit();
|
||||
initialFitDone.value = true;
|
||||
}
|
||||
|
||||
watch(() => graph.value.nodes.length, () => { void fitInitial(); }, { flush: "post" });
|
||||
onMounted(() => { void fitInitial(); });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -103,7 +124,7 @@ async function fit(): Promise<void> {
|
||||
:nodes-draggable="false"
|
||||
:nodes-connectable="false"
|
||||
:elements-selectable="false"
|
||||
fit-view-on-init
|
||||
:fit-view-on-init="false"
|
||||
class="agent-mind-map-flow"
|
||||
aria-label="AgentRun 思维导图"
|
||||
>
|
||||
|
||||
@@ -591,7 +591,8 @@ export function buildRelationWindow(
|
||||
});
|
||||
|
||||
let omittedRunCount = 0;
|
||||
for (const [runIndex, run] of runs.entries()) {
|
||||
const stableRuns = [...runs].sort((left, right) => compareText(relationRunOrderKey(left), relationRunOrderKey(right)));
|
||||
for (const [runIndex, run] of stableRuns.entries()) {
|
||||
const hasCompleteContainment = Boolean((run.projectId || run.workspace) && run.sessionId && run.taskId);
|
||||
const scopeId = hasCompleteContainment ? relationId("scope", `${run.projectId ?? ""}\u0000${run.workspace ?? ""}`) : unassociatedId;
|
||||
const sessionId = hasCompleteContainment && run.sessionId ? relationId("session", `${scopeId}\u0000${run.sessionId}`) : null;
|
||||
@@ -603,7 +604,7 @@ export function buildRelationWindow(
|
||||
const commandId = run.commandId ? relationId("command", `${commandParentId}\u0000${run.commandId}`) : null;
|
||||
const missingNodeCount = [scopeId, sessionId, taskId, runNodeId, attemptId, commandId].filter((id): id is string => typeof id === "string" && !nodes.has(id)).length;
|
||||
if (nodes.size + missingNodeCount > nodeLimit) {
|
||||
omittedRunCount = runs.length - runIndex;
|
||||
omittedRunCount = stableRuns.length - runIndex;
|
||||
break;
|
||||
}
|
||||
let parent = hasCompleteContainment
|
||||
@@ -713,6 +714,14 @@ function relationId(kind: AgentObserverRelationKind, value: string): string {
|
||||
return `${kind}:${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
function relationRunOrderKey(run: AgentObserverRun): string {
|
||||
return [run.projectId, run.workspace, run.sessionId, run.taskId, run.id].map((value) => value ?? "").join("\u0000");
|
||||
}
|
||||
|
||||
function compareText(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
function latestTimestamp(previous: string | null, next: string | null): string | null {
|
||||
if (!previous) return next;
|
||||
if (!next) return previous;
|
||||
|
||||
Reference in New Issue
Block a user