Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ function snapshot(
};
}

function operator(
overrides: Pick<AgentGraphClientSnapshot['operators'][number], 'operatorId' | 'status'> &
Partial<AgentGraphClientSnapshot['operators'][number]>,
): AgentGraphClientSnapshot['operators'][number] {
return {
childSessionId: `child-${overrides.operatorId}`,
provisionId: `provision-${overrides.operatorId}`,
agentId: 'reviewer',
provisionedAt: 1,
inboundEdgeIds: [],
outboundEdgeIds: [],
scheduledWorkIds: [],
readiness: [],
omitted: {
inboundEdgeIds: 0,
outboundEdgeIds: 0,
scheduledWorkIds: 0,
readiness: 0,
readinessWaits: 0,
},
...overrides,
};
}

function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
Expand Down Expand Up @@ -308,6 +332,69 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('renders bounded live and completed output facts without replacing child navigation', async () => {
const opened: string[] = [];
const source = snapshot({
graphId: 'graph-output',
status: 'active',
operators: [
operator({
operatorId: 'operator-live',
status: 'running',
output: {
activationId: 'run-live',
preview: 'Inspecting the renderer projection',
previewTruncated: true,
phase: 'streaming',
previewUpdatedAt: 2_000,
sourceEventId: 'event-live',
messageId: 'message-live',
sampleStartedAt: 1_000,
outputTokens: 21,
sampleDurationMs: 1_000,
tokensPerSecond: 21,
},
}),
operator({
operatorId: 'operator-done',
status: 'completed',
output: {
activationId: 'run-done',
preview: 'Projection verified',
previewTruncated: false,
phase: 'completed',
previewUpdatedAt: 3_000,
sourceEventId: 'event-done',
sampleStartedAt: 2_000,
},
}),
],
});
const harness = installGraphRenderer(source);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: (sessionId: string) => opened.push(sessionId),
}),
);
await Promise.resolve();
});

assert.match(harness.container.textContent ?? '', /Live output · 21\.0 token\/s/);
assert.match(harness.container.textContent ?? '', /Inspecting the renderer projection…/);
assert.match(harness.container.textContent ?? '', /Result preview/);
const open = [...harness.container.querySelectorAll('button')].find((button) =>
button.textContent?.includes('Open child task'),
);
assert.ok(open);
await act(async () => (open as HTMLElement).click());
assert.deepEqual(opened, ['child-operator-live']);
await act(async () => harness.root.unmount());
});

it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
Expand Down
28 changes: 28 additions & 0 deletions apps/desktop/src/renderer/agent-graph-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ type GraphPanelCopy = {
openSession: string;
operators: string;
selectedResults: string;
liveOutput: string;
completedOutput: string;
throughput(tokensPerSecond: number): string;
epoch: string;
currentEpoch: string;
historicalEpoch: string;
Expand Down Expand Up @@ -92,6 +95,9 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
openSession: '打开子任务',
operators: 'Operators',
selectedResults: '已选择结果',
liveOutput: '实时输出',
completedOutput: '结果预览',
throughput: (tokensPerSecond) => `${tokensPerSecond.toFixed(1)} token/s`,
epoch: 'Graph 运行轮次',
currentEpoch: '当前',
historicalEpoch: '历史记录(只读)',
Expand Down Expand Up @@ -140,6 +146,9 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
openSession: '開啟子任務',
operators: 'Operators',
selectedResults: '已選取結果',
liveOutput: '即時輸出',
completedOutput: '結果預覽',
throughput: (tokensPerSecond) => `${tokensPerSecond.toFixed(1)} token/s`,
epoch: 'Graph 執行輪次',
currentEpoch: '目前',
historicalEpoch: '歷史記錄(唯讀)',
Expand Down Expand Up @@ -187,6 +196,9 @@ export function getAgentGraphPanelCopy(locale: UiLocale): GraphPanelCopy {
openSession: 'Open child task',
operators: 'Operators',
selectedResults: 'Selected results',
liveOutput: 'Live output',
completedOutput: 'Result preview',
throughput: (tokensPerSecond) => `${tokensPerSecond.toFixed(1)} token/s`,
epoch: 'Graph run',
currentEpoch: 'Current',
historicalEpoch: 'History (read-only)',
Expand Down Expand Up @@ -537,6 +549,22 @@ export function AgentGraphPanel(props: {
<span className="maka-agent-graph-operator-copy">
<strong>{operator.agentId}</strong>
<span>{work?.instructionPreview ?? operator.operatorId}</span>
{operator.output ? (
<span className="maka-agent-graph-output">
<span className="maka-agent-graph-output-meta">
{operator.output.phase === 'streaming'
? copy.liveOutput
: copy.completedOutput}
{operator.output.tokensPerSecond === undefined
? null
: ` · ${copy.throughput(operator.output.tokensPerSecond)}`}
</span>
<span className="maka-agent-graph-output-preview">
{operator.output.preview}
{operator.output.previewTruncated ? '…' : ''}
</span>
</span>
) : null}
{wait ? <span className="maka-agent-graph-wait">{wait}</span> : null}
</span>
<span className="maka-agent-graph-operator-status">
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/src/renderer/styles/agent-graph.css
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,19 @@
font: var(--maka-text-supporting);
}

.maka-agent-graph-output {
display: grid;
gap: var(--space-0-5);
}

.maka-agent-graph-output-meta {
color: var(--muted-foreground);
}

.maka-agent-graph-output-preview {
color: var(--foreground);
}

.maka-agent-graph-wait {
color: var(--warning-text);
}
Expand Down
58 changes: 58 additions & 0 deletions apps/desktop/stories/agent-graph-panel.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,64 @@ export const BlockedOnUpstream: Story = {
},
};

// Real path: one child task is streaming while another has settled. The graph
// read model supplies bounded text and provider-reported throughput; the panel
// never reads either child Session independently.
export const OutputPreviews: Story = {
decorators: [
withScopedMakaBridge(
graphBridge(
snapshot({
status: 'active',
operators: [
operator({
operatorId: 'op-research',
status: 'running',
output: {
activationId: 'run-research',
preview: 'Comparing the three provider adapters and their retry boundaries…',
previewTruncated: false,
phase: 'streaming',
previewUpdatedAt: 2_000,
sourceEventId: 'event-research',
messageId: 'message-research',
sampleStartedAt: 1_000,
outputTokens: 42,
sampleDurationMs: 1_000,
tokensPerSecond: 42,
},
}),
operator({
operatorId: 'op-review',
status: 'completed',
output: {
activationId: 'run-review',
preview: 'Found one retry guard that needs a focused regression test.',
previewTruncated: false,
phase: 'completed',
previewUpdatedAt: 3_000,
sourceEventId: 'event-review',
sampleStartedAt: 2_000,
outputTokens: 18,
sampleDurationMs: 1_000,
tokensPerSecond: 18,
},
}),
],
}),
),
),
],
render: panel,
play: async ({ canvasElement }) => {
await waitFor(() => {
expect(canvasElement.textContent).toContain(copy.liveOutput);
expect(canvasElement.textContent).toContain(copy.completedOutput);
expect(canvasElement.textContent).toContain(copy.throughput(42));
});
},
};

// Real path: a wide fan-out — many operators provisioned at once, the breadth a
// small graph never shows. The read-model only elides operators past 256 (far
// beyond a story's scale), so this shows a genuine many-operator list rather
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,19 @@ describe('Host Agent Graph coordinator', () => {
test('projects only allowlisted Runtime fields onto the wire', () => {
const source = graphSnapshot();
Object.assign(source.operators[0]!, { privatePrompt: 'operator-secret' });
source.operators[0]!.output = {
activationId: 'activation-0',
preview: 'Bounded output',
previewTruncated: false,
phase: 'streaming',
previewUpdatedAt: 10,
sourceEventId: 'event-0',
sampleStartedAt: 1,
outputTokens: 18,
sampleDurationMs: 9,
tokensPerSecond: 2_000,
};
Object.assign(source.operators[0]!.output, { privateOutput: 'output-secret' });
Object.assign(source.operators[0]!.readiness[0]!, {
policyKind: 'map',
privatePolicy: 'readiness-secret',
Expand All @@ -351,6 +364,18 @@ describe('Host Agent Graph coordinator', () => {

const projected = projectAgentGraphClientSnapshot(source);
assert.equal(JSON.stringify(projected).includes('secret'), false);
assert.deepEqual(projected.operators[0]?.output, {
activationId: 'activation-0',
preview: 'Bounded output',
previewTruncated: false,
phase: 'streaming',
previewUpdatedAt: 10,
sourceEventId: 'event-0',
sampleStartedAt: 1,
outputTokens: 18,
sampleDurationMs: 9,
tokensPerSecond: 2_000,
});
assert.doesNotThrow(() => decodeAgentGraphClientSnapshot(projected));
});

Expand Down
24 changes: 24 additions & 0 deletions packages/runtime-host/src/__tests__/agent-graph-protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ describe('Agent Graph Client protocol', () => {
operators: [{ ...snapshot.operators[0], privatePrompt: 'secret' }],
}),
);
assertInvalid(() =>
decodeAgentGraphClientSnapshot({
...snapshot,
operators: [
{
...snapshot.operators[0],
output: { ...snapshot.operators[0]!.output, tokensPerSecond: -1 },
},
],
}),
);
assertInvalid(() =>
decodeAgentGraphClientSnapshot({
...snapshot,
Expand Down Expand Up @@ -267,6 +278,19 @@ function graphSnapshot(): AgentGraphClientSnapshot {
lastEventTime: 10,
run: { sessionId: 'child-1', agentRunId: 'run:1', turnId: 'turn:1' },
},
output: {
activationId: 'activation:1',
preview: 'Reviewing the graph projection.',
previewTruncated: false,
phase: 'streaming',
previewUpdatedAt: 10,
sourceEventId: 'event:1',
messageId: 'message:1',
sampleStartedAt: 1,
outputTokens: 18,
sampleDurationMs: 9,
tokensPerSecond: 2_000,
},
},
],
edges: [{ edgeId: 'edge:1', fromOperatorId: 'operator:0', toOperatorId: 'operator:1' }],
Expand Down
Loading