Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
contents: read # Checkout only; every write-capable action declares its own scope.

jobs:
# One job, not three. A runner slot is scarcer than a minute on shared
Expand All @@ -54,7 +54,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
persist-credentials: false # Do not leave the checkout token in git config.

- id: comparison
name: Resolve checked-out comparison
Expand Down
21 changes: 6 additions & 15 deletions apps/desktop/e2e/streaming-remount.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,7 @@ test("a successor owns working status and remounting leaves accumulated output s

await liveBubble.evaluate((element) => {
const observed = { texts: [] as string[] };
(
window as typeof window & {
__makaStreamingRemountObserved?: typeof observed;
}
).__makaStreamingRemountObserved = observed;
Object.assign(window, { __makaStreamingRemountObserved: observed });
new MutationObserver(() => {
observed.texts.push(element.textContent ?? "");
}).observe(element, {
Expand Down Expand Up @@ -391,16 +387,11 @@ test("returning to a live conversation settles output accumulated while away", a
expect(
(await page.getByRole("log").textContent())?.split(accumulatedOutput),
).toHaveLength(2);
// Playwright's toContainText is a DOM check. Stop on the next animation
// frame so the already-queued sample() records that settled paint first.
const backgroundRestoreObserved = await page.evaluate(
stopStreamingPaintObserver,
);
expect(
backgroundRestoreObserved?.texts.some((text) =>
text.includes(backgroundSteering),
),
).toBe(true);
const stopObserver = stopStreamingPaintObserver;
const backgroundRestoreObserved = await page.evaluate(stopObserver);
const restoredAtPaint =
backgroundRestoreObserved?.texts.some((text) => text.includes(backgroundSteering)) ?? false;
expect(restoredAtPaint).toBe(true);
expect(
backgroundRestoreObserved?.texts.some(
(text) =>
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/resources/licenses/npm/THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10804,7 +10804,7 @@ SOFTWARE.

================================================================================

Package: ip-address@10.4.0
Package: ip-address@10.7.2
Declared license: MIT
Selected license: MIT
Repository: https://github.com/beaugunderson/ip-address.git
Expand Down Expand Up @@ -15022,7 +15022,7 @@ PERFORMANCE OF THIS SOFTWARE.

================================================================================

Package: undici@6.28.0
Package: undici@6.29.0
Declared license: MIT
Selected license: MIT
Repository: git+https://github.com/nodejs/undici.git
Expand Down Expand Up @@ -15052,7 +15052,7 @@ SOFTWARE.

================================================================================

Package: undici@7.29.0
Package: undici@7.30.0
Declared license: MIT
Selected license: MIT
Repository: git+https://github.com/nodejs/undici.git
Expand Down Expand Up @@ -15082,7 +15082,7 @@ SOFTWARE.

================================================================================

Package: undici@8.10.2
Package: undici@8.11.2
Declared license: MIT
Selected license: MIT
Repository: git+https://github.com/nodejs/undici.git
Expand Down
242 changes: 105 additions & 137 deletions apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import {
createAgentGraphPanelModel,
isAgentGraphLive,
Expand All @@ -27,166 +28,133 @@ import {
shouldShowAgentGraphPanel,
} from '../../renderer/agent-graph-panel-visibility.js';

describe('isAgentGraphLive', () => {
it('treats in-flight statuses as live and settled ones as not', () => {
for (const status of ['active', 'waiting', 'closing'] as const) {
assert.equal(isAgentGraphLive(status), true, status);
}
for (const status of ['empty', 'stopped', 'failed', 'completed'] as const) {
assert.equal(isAgentGraphLive(status), false, status);
}
assert.equal(isAgentGraphLive(undefined), false);
});
});
type Status = AgentGraphClientSnapshot['status'];

const STATUS_CONTRACT: ReadonlyArray<{
status: Status | undefined;
live: boolean;
dismissible: boolean;
}> = [
{ status: undefined, live: false, dismissible: false },
{ status: 'empty', live: false, dismissible: false },
{ status: 'active', live: true, dismissible: false },
{ status: 'waiting', live: true, dismissible: false },
{ status: 'closing', live: true, dismissible: false },
{ status: 'completed', live: false, dismissible: true },
{ status: 'stopped', live: false, dismissible: true },
{ status: 'failed', live: false, dismissible: true },
];

describe('isAgentGraphPanelDismissible', () => {
it('allows hiding a graph that no longer has active work', () => {
assert.equal(isAgentGraphPanelDismissible('completed'), true);
assert.equal(isAgentGraphPanelDismissible('stopped'), true);
assert.equal(isAgentGraphPanelDismissible('failed'), true);
const commit = (
state: ReturnType<typeof createAgentGraphPanelModel>,
rootSessionId: string,
graphId: string,
status: Status,
current = true,
) =>
reduceAgentGraphPanelModel(state, {
type: 'commit-snapshot',
current,
snapshot: { rootSessionId, graphId, status },
});

it('keeps the panel while the graph is still in flight', () => {
for (const status of ['empty', 'active', 'closing', 'waiting'] as const) {
assert.equal(isAgentGraphPanelDismissible(status), false, status);
describe('Agent Graph status partition', () => {
it('classifies every protocol status into disjoint live and dismissible sets', () => {
for (const row of STATUS_CONTRACT) {
assert.equal(isAgentGraphLive(row.status), row.live, String(row.status));
assert.equal(isAgentGraphPanelDismissible(row.status), row.dismissible, String(row.status));
assert.equal(row.live && row.dismissible, false, String(row.status));
}
assert.equal(isAgentGraphPanelDismissible(undefined), false);
});
});

describe('shouldShowAgentGraphPanel', () => {
it('hides a dismissed completed graph for that session', () => {
assert.equal(
shouldShowAgentGraphPanel({
describe('Agent Graph visibility invariant', () => {
it('depends on a dismissal only for the same terminal graph in the same session', () => {
for (const row of STATUS_CONTRACT) {
const hidden = shouldShowAgentGraphPanel({
enabled: true,
hasGraphActivity: true,
sessionId: 'session-1',
graphId: 'graph-1',
status: 'completed',
dismissedBySession: { 'session-1': 'graph-1' },
}),
false,
);
});

it('shows a new graph after the previous one was dismissed', () => {
assert.equal(
shouldShowAgentGraphPanel({
enabled: true,
hasGraphActivity: true,
sessionId: 'session-1',
graphId: 'graph-2',
status: 'active',
dismissedBySession: { 'session-1': 'graph-1' },
}),
true,
);
});
sessionId: 'session-a',
graphId: 'graph-a',
status: row.status,
dismissedBySession: { 'session-a': 'graph-a' },
});
assert.equal(hidden, !row.dismissible, String(row.status));

it('shows the same graph again if it leaves a terminal state', () => {
assert.equal(
shouldShowAgentGraphPanel({
enabled: true,
hasGraphActivity: true,
sessionId: 'session-1',
graphId: 'graph-1',
status: 'active',
dismissedBySession: { 'session-1': 'graph-1' },
}),
true,
);
});

it('does not apply another session\'s dismissal', () => {
assert.equal(
shouldShowAgentGraphPanel({
enabled: true,
hasGraphActivity: true,
sessionId: 'session-2',
graphId: 'graph-1',
status: 'completed',
dismissedBySession: { 'session-1': 'graph-1' },
}),
true,
);
});

it('keeps the existing empty-state hide when graph mode is off', () => {
assert.equal(
shouldShowAgentGraphPanel({
enabled: false,
hasGraphActivity: false,
sessionId: 'session-1',
dismissedBySession: {},
}),
false,
);
for (const changedIdentity of [
{ sessionId: 'session-b', graphId: 'graph-a' },
{ sessionId: 'session-a', graphId: 'graph-b' },
]) {
assert.equal(
shouldShowAgentGraphPanel({
enabled: true,
hasGraphActivity: true,
...changedIdentity,
status: row.status,
dismissedBySession: { 'session-a': 'graph-a' },
}),
true,
);
}
}
});

it('still shows an enabled graph that has not produced activity yet', () => {
assert.equal(
shouldShowAgentGraphPanel({
enabled: true,
hasGraphActivity: false,
sessionId: 'session-1',
dismissedBySession: {},
}),
true,
);
it('is monotone when graph activity or graph mode is added', () => {
for (const enabled of [false, true]) {
for (const hasGraphActivity of [false, true]) {
const visible = shouldShowAgentGraphPanel({
enabled,
hasGraphActivity,
sessionId: 'session-a',
dismissedBySession: {},
});
assert.equal(visible, enabled || hasGraphActivity);
}
}
});
});

describe('dismiss and reconcile', () => {
it('records the dismissed graph for the session', () => {
const state = reduceAgentGraphPanelModel(createAgentGraphPanelModel('session-1'), {
type: 'dismiss',
graphId: 'graph-1',
});
assert.deepEqual(state.dismissedBySession, { 'session-1': 'graph-1' });
});
describe('Agent Graph presentation state machine', () => {
it('retains a matching terminal dismissal and clears it for every revival', () => {
for (const terminal of ['completed', 'stopped', 'failed'] as const) {
let state = createAgentGraphPanelModel('session-a');
state = reduceAgentGraphPanelModel(state, { type: 'dismiss', graphId: 'graph-a' });
state = commit(state, 'session-a', 'graph-a', terminal);
assert.deepEqual(state.dismissedBySession, { 'session-a': 'graph-a' });

it('drops a stale dismissal when a later snapshot is a different graph', () => {
let state = createAgentGraphPanelModel('session-1');
state = reduceAgentGraphPanelModel(state, { type: 'dismiss', graphId: 'graph-1' });
state = reduceAgentGraphPanelModel(state, {
type: 'commit-snapshot',
current: true,
snapshot: { rootSessionId: 'session-1', graphId: 'graph-2', status: 'active' },
});
assert.deepEqual(state.dismissedBySession, {});
});

it('drops a stale dismissal when the same graph becomes active again', () => {
let state = createAgentGraphPanelModel('session-1');
state = reduceAgentGraphPanelModel(state, { type: 'dismiss', graphId: 'graph-1' });
state = reduceAgentGraphPanelModel(state, {
type: 'commit-snapshot',
current: true,
snapshot: { rootSessionId: 'session-1', graphId: 'graph-1', status: 'active' },
});
assert.deepEqual(state.dismissedBySession, {});
for (const live of ['active', 'waiting', 'closing'] as const) {
const revived = commit(state, 'session-a', 'graph-a', live);
assert.deepEqual(revived.dismissedBySession, {}, `${terminal} -> ${live}`);
}
}
});

it('keeps a matching terminal dismissal', () => {
let state = createAgentGraphPanelModel('session-1');
state = reduceAgentGraphPanelModel(state, { type: 'dismiss', graphId: 'graph-1' });
state = reduceAgentGraphPanelModel(state, {
type: 'commit-snapshot',
current: true,
snapshot: { rootSessionId: 'session-1', graphId: 'graph-1', status: 'completed' },
});
assert.deepEqual(state.dismissedBySession, { 'session-1': 'graph-1' });
it('treats graph rollover as a new identity regardless of terminal status', () => {
let state = createAgentGraphPanelModel('session-a');
state = reduceAgentGraphPanelModel(state, { type: 'dismiss', graphId: 'graph-a' });
for (const status of ['active', 'completed', 'failed'] as const) {
assert.deepEqual(commit(state, 'session-a', 'graph-b', status).dismissedBySession, {});
}
});

it('does not clear a dismissal against a snapshot still owned by the previous session', () => {
it('ignores stale snapshots after a session transition', () => {
let state = createAgentGraphPanelModel('session-a');
state = reduceAgentGraphPanelModel(state, { type: 'dismiss', graphId: 'graph-a' });
state = reduceAgentGraphPanelModel(state, { type: 'enter-session', rootSessionId: 'session-b' });
state = reduceAgentGraphPanelModel(state, {
type: 'commit-snapshot',
current: true,
snapshot: { rootSessionId: 'session-a', graphId: 'graph-a', status: 'completed' },
type: 'enter-session',
rootSessionId: 'session-b',
});
assert.deepEqual(state.dismissedBySession, { 'session-a': 'graph-a' });
const before = state;
state = commit(state, 'session-a', 'graph-a', 'active');
assert.strictEqual(state, before);
});

it('initializes collapse once, then preserves explicit user intent', () => {
let state = commit(createAgentGraphPanelModel('session-a'), 'session-a', 'graph-a', 'completed');
assert.equal(state.collapsed, true);
state = reduceAgentGraphPanelModel(state, { type: 'toggle-collapse' });
assert.equal(state.collapsed, false);
state = commit(state, 'session-a', 'graph-a', 'completed');
assert.equal(state.collapsed, false);
});
});
Loading
Loading