From e062034a994617520b2e23f8d0898ca92da5b0ae Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 23 Aug 2026 20:28:44 +0800 Subject: [PATCH 1/5] feat(ui): visualize trace composition by color Remove the operational database path from the trace panel and its now-unused workbar service contract. Add a proportional categorical color strip and matching legend swatches for the latest request composition, with a rendering contract test. Generated-by: Maka --- .../session-inspector-composition.test.ts | 61 +++++++++ .../main/__tests__/use-session-trace.test.ts | 1 - .../workbar-services-adapter.test.ts | 5 - .../src/renderer/features/workbar/ports.ts | 1 - .../src/renderer/features/workbar/testing.ts | 1 - .../inspector/session-inspector-panel.tsx | 129 +++--------------- .../src/renderer/locales/conversation-copy.ts | 12 -- .../desktop/create-workbar-services.ts | 2 - .../src/renderer/styles/workbar/inspector.css | 83 +++++------ .../stories/session-workbar.stories.tsx | 20 --- 10 files changed, 124 insertions(+), 191 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/session-inspector-composition.test.ts diff --git a/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts new file mode 100644 index 0000000000..c7627a0b38 --- /dev/null +++ b/apps/desktop/src/main/__tests__/session-inspector-composition.test.ts @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { InspectorCompositionSection } from '../../renderer/features/workbar/tools/inspector/session-inspector-panel.js'; +import { getDesktopConversationCopy } from '../../renderer/locales/conversation-copy.js'; + +test('maps each request-composition category to the same colour in the chart and legend', () => { + const markup = renderToStaticMarkup( + createElement(InspectorCompositionSection, { + copy: getDesktopConversationCopy('en').inspector, + state: { + status: 'available', + composition: { + parts: [ + { kind: 'system_instructions', estimatedTokens: 10 }, + { kind: 'tool_definitions', estimatedTokens: 20 }, + { kind: 'messages', estimatedTokens: 30 }, + { kind: 'other', estimatedTokens: 40 }, + ], + tools: [], + }, + }, + formatNumber: (value: number) => String(value), + }), + ); + + for (const [kind, tokens] of [ + ['system_instructions', 10], + ['tool_definitions', 20], + ['messages', 30], + ['other', 40], + ] as const) { + assert.match( + markup, + new RegExp(`class="maka-inspector-composition-band"[^>]*data-segment="${kind}"[^>]*flex-grow:${tokens}`), + ); + assert.match( + markup, + new RegExp(`class="maka-inspector-composition-swatch"[^>]*data-segment="${kind}"`), + ); + } +}); diff --git a/apps/desktop/src/main/__tests__/use-session-trace.test.ts b/apps/desktop/src/main/__tests__/use-session-trace.test.ts index f748f407b9..44dab5c291 100644 --- a/apps/desktop/src/main/__tests__/use-session-trace.test.ts +++ b/apps/desktop/src/main/__tests__/use-session-trace.test.ts @@ -195,7 +195,6 @@ function createTraceHarness( usageChangeHandlers.delete(handler); }; }, - getRecordFile: async () => '', }, }); harness.services = services; diff --git a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts index 4e94469231..010e5e2a4f 100644 --- a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts +++ b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts @@ -46,9 +46,6 @@ function createBridgeRecorder(): { get: (_target, property) => (...args: unknown[]) => { const callName = `${name}.${String(property)}`; calls.push({ name: callName, args }); - if (callName === 'app.info') { - return Promise.resolve({ operationalStateDatabasePath: '/tmp/runtime.sqlite' }); - } if (syncMethods.has(callName)) return () => undefined; return Promise.resolve(undefined); }, @@ -122,7 +119,6 @@ describe('createDesktopWorkbarServices', () => { await services.inspector.context('s'); services.inspector.subscribeSessionEvents('s', eventHandler)(); services.inspector.subscribeUsageChanges('s', eventHandler)(); - assert.equal(await services.inspector.getRecordFile(), '/tmp/runtime.sqlite'); await services.attachments.pickFiles(); await services.attachments.previewApproval('approval'); @@ -192,7 +188,6 @@ describe('createDesktopWorkbarServices', () => { 'inspector.context', 'sessions.subscribeEvents', 'inspector.subscribeUsageChanges', - 'app.info', 'attachments.pickFiles', 'attachments.previewApproval', 'sessions.list', diff --git a/apps/desktop/src/renderer/features/workbar/ports.ts b/apps/desktop/src/renderer/features/workbar/ports.ts index bca804e196..ca959adc65 100644 --- a/apps/desktop/src/renderer/features/workbar/ports.ts +++ b/apps/desktop/src/renderer/features/workbar/ports.ts @@ -174,7 +174,6 @@ export interface WorkbarInspectorService { sessionId: string, handler: () => void, ): WorkbarUnsubscribe; - getRecordFile(): Promise; } export interface WorkbarAttachmentsService { diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts index 26b076f045..06842e568a 100644 --- a/apps/desktop/src/renderer/features/workbar/testing.ts +++ b/apps/desktop/src/renderer/features/workbar/testing.ts @@ -109,7 +109,6 @@ export function createFakeWorkbarServices( }, subscribeSessionEvents: noopSubscription, subscribeUsageChanges: noopSubscription, - getRecordFile: async () => '', }, attachments: { pickFiles: async () => ({ ok: false, reason: 'cancelled' }), diff --git a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx index ce61df6412..6359665990 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/inspector/session-inspector-panel.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { type ReactNode, useEffect, useMemo, useState } from 'react'; +import { type ReactNode, useMemo } from 'react'; import { Banner } from '@astryxdesign/core/Banner'; import { Button } from '@astryxdesign/core/Button'; import { EmptyState } from '@astryxdesign/core/EmptyState'; @@ -25,7 +25,6 @@ import { Heading } from '@astryxdesign/core/Heading'; import { HStack, VStack } from '@astryxdesign/core/Layout'; import { Section } from '@astryxdesign/core/Section'; import { Text } from '@astryxdesign/core/Text'; -import { Tooltip } from '@astryxdesign/core/Tooltip'; import { uiLocaleToIntlLocale, type UiLocale } from '@maka/core/ui-locale'; import { traceTurnIdentityKey } from '@maka/core/session-trace'; import { useToast, useUiLocale } from '@maka/ui'; @@ -46,30 +45,6 @@ import { type InspectorTurnRow, } from './session-inspector-panel-model.js'; import { useSessionTrace } from './use-session-trace.js'; -import { useWorkbarServices } from '../../services-context.js'; - -/** - * The record file is the workspace's operational-state database — the file - * both trace ledgers live in. Its exact path is resolved once, in main - * (`app:info.operationalStateDatabasePath` via @maka/storage's - * `resolveOperationalStateDatabasePath`); the renderer cannot import that - * package at runtime (it pulls node:sqlite into the browser bundle), and - * recomputing the path here would create a second authority. The row only - * displays and copies the value it receives. - */ - -/** - * Split a record-file path for display only: `dir` is everything before the - * last separator (both platform separators, since the path came from main and - * may be POSIX or Windows), `name` is the filename. The name is never - * truncated by the row's ellipsis; the dir is. The full path is not rebuilt - * here — the row shows exactly the parts of the value it received. - */ -function splitRecordFileDisplayPath(path: string): { dir: string; name: string } { - const separatorIndex = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/')); - if (separatorIndex === -1) return { dir: '', name: path }; - return { dir: path.slice(0, separatorIndex + 1), name: path.slice(separatorIndex + 1) }; -} /** * Per-session trace (#1625), read top to bottom rather than through a @@ -87,7 +62,6 @@ function splitRecordFileDisplayPath(path: string): { dir: string; name: string } * paragraph. */ export function SessionInspectorPanel(props: { sessionId: string; active: boolean }) { - const { inspector } = useWorkbarServices(); const locale = useUiLocale(); const copy = getDesktopConversationCopy(locale).inspector; const toast = useToast(); @@ -101,42 +75,6 @@ export function SessionInspectorPanel(props: { sessionId: string; active: boolea [snapshot.context, snapshot.summary], ); - // The record file is a fact about the workspace, not about the session's - // activity: it exists whether the trace is empty or not, and it never - // changes while the app is running. `app:info` resolves the exact database - // path in main (the same value the data-settings row shows), so the row has - // no second authority and no separator guessing. Read once on mount — - // before the user can open the tab — so the row is already painted when the - // trace lands and the banner never shifts; a failure hides the row — it is - // auxiliary, and a path that will not load should not masquerade as a trace - // that failed to read. - const [recordFile, setRecordFile] = useState(); - useEffect(() => { - if (recordFile !== undefined) return; - let mounted = true; - void inspector - .getRecordFile() - .then((path) => { - if (mounted) { - setRecordFile(path); - } - }) - .catch(() => {}); - return () => { - mounted = false; - }; - }, [inspector, recordFile]); - - async function copyRecordFile() { - if (!recordFile) return; - try { - await navigator.clipboard.writeText(recordFile); - toast.success(copy.pathCopied); - } catch { - toast.error(copy.copyFailed, copy.copyFailedDetail); - } - } - async function copyPricingKey(key: string) { try { await navigator.clipboard.writeText(key); @@ -146,11 +84,6 @@ export function SessionInspectorPanel(props: { sessionId: string; active: boolea } } - // The path is split for display only — the directory part truncates while - // the filename never does, so a narrow panel still says which file the row - // is about. The full path stays the tooltip and clipboard value, and the - // authoritative string is the one `app:info` returned. - const pathParts = recordFile ? splitRecordFileDisplayPath(recordFile) : undefined; return (
- {recordFile && pathParts && ( - - - {copy.recordFile} - - - {/* Keyboard-reachable tooltip trigger: the row is one Tab stop, - the tooltip opens on focus-visible and dismisses on Escape, - and the full path never depends on the tooltip alone (it is - the visible filename and the clipboard value too). */} - - - {pathParts.dir} - - - {pathParts.name} - - - -