Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b232892
feat(core): pass contextWindow through the session trace
Astro-Han Aug 5, 2026
24691c8
feat(desktop): derive the inspector overview model
Astro-Han Aug 5, 2026
a993591
feat(desktop): lead the session trace with an overview
Astro-Han Aug 5, 2026
aa0c346
refactor(desktop): simplify the trace overview to Astryx-native compo…
Astro-Han Aug 5, 2026
75150b2
refactor(desktop): demote the coverage notice and redesign the raw re…
Astro-Han Aug 5, 2026
39e2382
feat(desktop): draw the context window as bands, not one fill
Astro-Han Aug 6, 2026
35a25ce
refactor(desktop): lay the session trace out on the Astryx properties…
Astro-Han Aug 6, 2026
3725876
refactor(desktop): split the trace into two views and draw the record…
Astro-Han Aug 6, 2026
b470ff4
refactor(desktop): cut the overview to what someone acts on
Astro-Han Aug 6, 2026
1222cf6
refactor(desktop): rank the trace timeline instead of switching views
Astro-Han Aug 6, 2026
32dc856
refactor(desktop): cut the trace timeline to what it is a record of
Astro-Han Aug 6, 2026
508801f
refactor(desktop): put the trace on three type sizes and give the fil…
Astro-Han Aug 6, 2026
241699c
refactor(desktop): keep the model, drop the token tail
Astro-Han Aug 6, 2026
0c94c11
refactor(desktop): title the overview with its model
Astro-Han Aug 6, 2026
bb7ec9d
refactor(desktop): drop the model line from the trace overview
Astro-Han Aug 6, 2026
88bae12
fix(desktop): say the trace's facts in plain Chinese
Astro-Han Aug 6, 2026
e1448ac
fix(desktop): stop leaking the trace's enums into the reader's language
Astro-Han Aug 6, 2026
e47abad
style(desktop): give the trace a rhythm and a backbone
Astro-Han Aug 6, 2026
5dba80b
fix(desktop): make the trace's derived figures and its filter tell th…
Astro-Han Aug 6, 2026
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
17 changes: 9 additions & 8 deletions apps/desktop/e2e/session-workbar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW
await expect(resize).toHaveAttribute('aria-valuemax', '600');
await resize.focus();
await resize.press('ArrowLeft');
await expect(resize).toHaveAttribute('aria-valuenow', '410');
await expect(workbar).toHaveCSS('width', '410px');
// 480 default (session-workbar-layout.ts) + the hook's 10px keyboard step.
await expect(resize).toHaveAttribute('aria-valuenow', '490');
await expect(workbar).toHaveCSS('width', '490px');

// The seam is the canvas between two plates, and it is asserted as a GAP,
// not as a colour. Comparing the workbar's tone to another surface is what
Expand Down Expand Up @@ -65,8 +66,8 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW
await page.mouse.down();
await page.mouse.move(box.x - 20.5, y, { steps: 2 });
await page.mouse.up();
await expect(workbar).toHaveCSS('width', '431px');
await expect(resize).toHaveAttribute('aria-valuenow', '431');
await expect(workbar).toHaveCSS('width', '511px');
await expect(resize).toHaveAttribute('aria-valuenow', '511');

// Cmd+Tab mid-drag and release outside the app: Astryx only ends a drag on
// pointerup/pointercancel, so without a blur guard the listeners survive and
Expand All @@ -77,7 +78,7 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW
await page.evaluate(() => window.dispatchEvent(new Event('blur')));
await page.mouse.move(box.x - 200, y, { steps: 2 });
await page.mouse.up();
await expect(workbar).toHaveCSS('width', '431px');
await expect(workbar).toHaveCSS('width', '511px');
await expect(page.locator('body')).toHaveCSS('user-select', 'auto');

// Which key the width lands on is the load-bearing decision of #1861 and the
Expand All @@ -87,7 +88,7 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW
// `autoSaveId` would orphan the user's stored width silently.
await expect
.poll(() => page.evaluate(() => localStorage.getItem('maka-session-workbar-width-v1')))
.toBe('431');
.toBe('511');
expect(
await page.evaluate(() => Object.keys(localStorage).filter((key) => key.startsWith('astryx-resizable:'))),
).toEqual([]);
Expand Down Expand Up @@ -136,7 +137,7 @@ test('session tools share one user-controlled workbar', async ({ sessionWorkbarW
await expand.click();
await expect(workbar).toBeVisible();
// The width the drag above landed on, restored rather than reset.
await expect(motion).toHaveCSS('width', '431px');
await expect(motion).toHaveCSS('width', '511px');
await tabs.getByRole('button', { name: /文件/ }).click();
await expect(workbar.getByText('暂无生成文件')).toBeVisible();

Expand Down Expand Up @@ -198,6 +199,6 @@ test('the column stays mounted for the whole slide, every time', async ({
await collapseOnce();
await page.getByRole('button', { name: '展开会话工作栏' }).click();
await expect(page.getByRole('complementary', { name: '会话工作栏' })).toBeVisible();
await expect(motion).toHaveCSS('width', '400px');
await expect(motion).toHaveCSS('width', '480px');
await collapseOnce();
});
60 changes: 43 additions & 17 deletions apps/desktop/src/main/__tests__/session-inspector-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
applyInspectorFilter,
isEmptyInspectorFilter,
} from '../../renderer/session-inspector-filter.js';
import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js';
import type {
InspectorPanelModel,
InspectorStepRow,
Expand All @@ -24,7 +25,7 @@ function step(overrides: Partial<InspectorStepRow> = {}): InspectorStepRow {
function turn(overrides: Partial<InspectorTurnRow> = {}): InspectorTurnRow {
return {
turnId: 'turn-1',
startedAt: 1_000,
index: 1,
durationMs: 500,
totals: emptyTraceTotals(),
failed: false,
Expand All @@ -37,6 +38,7 @@ function model(turns: InspectorTurnRow[], totals: TraceTotals = emptyTraceTotals
return { turns, totals, empty: turns.length === 0 };
}

const copy = getDesktopConversationCopy('zh').inspector;
describe('inspector filter', () => {
it('treats an absent or blank filter as no filter at all', () => {
assert.equal(isEmptyInspectorFilter(undefined), true);
Expand All @@ -45,7 +47,7 @@ describe('inspector filter', () => {
assert.equal(isEmptyInspectorFilter({ failedOnly: true }), false);

const source = model([turn()]);
const result = applyInspectorFilter(source, undefined);
const result = applyInspectorFilter(source, undefined, copy);
assert.equal(result.filtered, false);
assert.deepEqual(result.turns, source.turns);
});
Expand All @@ -58,25 +60,25 @@ describe('inspector filter', () => {
}),
]);

const byTool = applyInspectorFilter(source, { query: 'bash' });
const byTool = applyInspectorFilter(source, { query: 'bash' }, copy);
assert.deepEqual(
byTool.turns[0]?.steps.map((entry) => entry.id),
['a'],
'case-insensitive on the tool name',
);
assert.equal(byTool.hiddenSteps, 1);

const byModel = applyInspectorFilter(source, { query: 'GPT' });
const byModel = applyInspectorFilter(source, { query: 'GPT' }, copy);
assert.deepEqual(byModel.turns[0]?.steps.map((entry) => entry.id), ['b']);
});

it('keeps a turn whose id matches even when it has no steps at all', () => {
it('keeps a turn whose display name matches even when it has no steps at all', () => {
// The zero-step path is the one worth pinning: with steps present, the step
// haystack could carry the match instead and the branch would never run.
const source = model([turn({ turnId: 'turn-a', steps: [] })]);
const result = applyInspectorFilter(source, { query: 'turn-a' });
const source = model([turn({ turnId: 'turn-a', index: 7, steps: [] })]);
const result = applyInspectorFilter(source, { query: '第 7 轮' }, copy);

assert.equal(result.turns.length, 1, 'answering "where is turn-a" with silence is wrong');
assert.equal(result.turns.length, 1, 'answering "where is 第 7 轮" with silence is wrong');
assert.equal(result.hiddenTurns, 0);
assert.equal(result.turns[0]?.steps.length, 0);
});
Expand All @@ -88,28 +90,30 @@ describe('inspector filter', () => {
turn({ turnId: 'ok' }),
turn({ turnId: 'bad', failed: true, steps: [] }),
]);
const result = applyInspectorFilter(source, { failedOnly: true });
const result = applyInspectorFilter(source, { failedOnly: true }, copy);

assert.deepEqual(
result.turns.map((entry) => entry.turnId),
['bad'],
);
});

it('does not let one turn-level failure message retain every step', () => {
it('does not let the turn\'s own failure retain every step under it', () => {
// Turn text belongs to the turn. Folding it into each step's haystack made
// a single match keep steps that explain nothing about it.
const source = model([
turn({
turnId: 'turn-a',
failed: true,
failureMessage: 'disk full',
failureCode: 'tool_failed',
steps: [step({ id: 'a', label: 'Bash' }), step({ id: 'b', label: 'claude' })],
}),
]);

const result = applyInspectorFilter(source, { query: 'disk full' });
assert.equal(result.turns.length, 0, 'no rendered row explains that phrase');
const result = applyInspectorFilter(source, { query: '工具失败' }, copy);
assert.equal(result.turns.length, 1, 'the turn announces it, so the turn matches');
assert.equal(result.turns[0]?.steps.length, 0, 'no step renders that phrase');
assert.equal(result.hiddenSteps, 2);
});

it('failed-only drops whole turns that succeeded', () => {
Expand All @@ -118,7 +122,7 @@ describe('inspector filter', () => {
turn({ turnId: 'bad', failed: true, failureCode: 'tool_failed' }),
]);

const result = applyInspectorFilter(source, { failedOnly: true });
const result = applyInspectorFilter(source, { failedOnly: true }, copy);
assert.deepEqual(result.turns.map((entry) => entry.turnId), ['bad']);
assert.equal(result.hiddenTurns, 1);
});
Expand All @@ -127,21 +131,43 @@ describe('inspector filter', () => {
// "Nothing matches your filter" and "this session did nothing" are
// different claims, and only the panel can tell the reader which it is.
const source = model([turn()]);
const result = applyInspectorFilter(source, { query: 'no-such-thing' });
const result = applyInspectorFilter(source, { query: 'no-such-thing' }, copy);

assert.equal(result.turns.length, 0);
assert.equal(result.filtered, true);
assert.equal(result.hiddenTurns, 1);
assert.equal(result.empty, false, 'the session still happened');
});

it('searches the words on screen, not the identifiers behind them', () => {
// The panel renders `语义压缩` and `已允许`; the trace records
// `semantic_compact` and `allow`. A reader types what they see.
const source = model([
turn({
steps: [
step({ id: 'a', label: 'claude', callKind: 'semantic_compact' }),
step({ id: 'b', kind: 'permission', label: 'Bash', decision: 'allow' }),
],
}),
]);

assert.equal(applyInspectorFilter(source, { query: '语义压缩' }, copy).turns[0]?.steps.length, 1);
assert.equal(applyInspectorFilter(source, { query: '已允许' }, copy).turns[0]?.steps.length, 1);
});

it('names a step by its kind when it has no identifier, and finds it by that name', () => {
const source = model([turn({ steps: [step({ id: 'c', kind: 'compaction', label: undefined })] })]);

assert.equal(applyInspectorFilter(source, { query: '上下文压缩' }, copy).turns[0]?.steps.length, 1);
});

it('leaves session totals alone so a filtered view cannot restate the bill', () => {
// Totals answer "what did this session cost". Recomputing them per filter
// would produce a number that is true of nothing.
const totals: TraceTotals = { ...emptyTraceTotals(), modelAttempts: 4, costUsd: 0.2 };
const source = model([turn({ steps: [step({ costUsd: 0.05 })] })], totals);
const source = model([turn()], totals);

const result = applyInspectorFilter(source, { query: 'no-such-thing' });
const result = applyInspectorFilter(source, { query: 'no-such-thing' }, copy);
assert.deepEqual(result.totals, totals);
});
});
Loading
Loading