From 931fdc8c00aee48070b0b357bf124dabc26924f6 Mon Sep 17 00:00:00 2001 From: GCWing Date: Thu, 29 Oct 2026 22:46:21 +0800 Subject: [PATCH 1/2] feat(flow-chat): add context usage previews and adjustable conversation width --- design-system/packages/ui/README.md | 3 + .../components/Disclosure/Disclosure.meta.ts | 2 + .../src/components/Disclosure/Disclosure.tsx | 9 +- .../packages/ui/tests/disclosure.test.mjs | 4 +- docs/architecture/theme-token-optimization.md | 9 + .../session-runtime-usage-report-design.md | 93 ++ src/apps/cli/src/dispatch/mod.rs | 2 + src/apps/cli/src/management.rs | 2 + src/apps/cli/src/modes/chat/selection.rs | 2 + src/apps/desktop/src/api/session_api.rs | 6 + .../src/runtime/session_application.rs | 4 + src/crates/assembly/core/AGENTS.md | 9 + .../core/src/agentic/execution/AGENTS.md | 10 + .../agentic/execution/context_composition.rs | 304 ++++ .../src/agentic/execution/execution_engine.rs | 173 ++- .../core/src/agentic/execution/mod.rs | 1 + .../src/agentic/execution/round_executor.rs | 13 + .../core/src/agentic/execution/types.rs | 1 + .../core/src/agentic/insights/collector.rs | 2 + .../core/src/agentic/persistence/manager.rs | 2 + .../persistence/manager/usage_context.rs | 66 + .../assembly/core/src/product_runtime.rs | 6 + .../core/src/service/session_usage/service.rs | 279 +++- .../contracts/core-types/src/session_usage.rs | 58 + .../session_usage_contracts.rs | 60 + .../contracts/runtime-ports/src/agent_api.rs | 34 + .../session_operation_ports.rs | 2 + .../app-server/tests/agent_kernel.rs | 2 + .../services-core/src/token_usage/types.rs | 3 + .../token_usage_contracts.rs | 30 + src/web-ui/AGENTS.md | 9 + src/web-ui/src/app/App.tsx | 6 - .../components/panels/base/FlexiblePanel.tsx | 20 + .../src/app/components/panels/base/utils.ts | 8 + .../content-canvas/stores/canvasStore.ts | 38 + .../app/scenes/session/ChatPane.appearance.ts | 2 + .../src/app/scenes/session/ChatPane.tsx | 11 +- src/web-ui/src/app/scenes/settings/AGENTS.md | 8 + .../application/AppearanceSettingsPage.tsx | 2 + .../FlowChatWidthSettingsSection.test.tsx | 33 + .../FlowChatWidthSettingsSection.tsx | 23 + .../app/scenes/settings/settingsRegistry.ts | 3 + .../src/flow_chat/components/ChatInput.tsx | 1 + .../components/FlowChatWidthBoundary.scss | 67 + .../components/FlowChatWidthBoundary.test.tsx | 130 ++ .../components/FlowChatWidthBoundary.tsx | 165 +++ .../modern/FLOWCHAT_SCROLL_STABILITY.md | 31 + .../modern/FLOWCHAT_VERIFICATION.md | 9 + .../modern/ModernFlowChatContainer.tsx | 4 + .../components/modern/VirtualMessageList.scss | 2 + ...rtualMessageList.session-boundary.test.tsx | 68 + .../components/modern/VirtualMessageList.tsx | 17 +- .../modern/flowChatScrollbarMask.test.ts | 60 + .../modern/flowChatScrollbarMask.ts | 23 + .../usage/SessionUsageComponents.test.tsx | 1119 +++++--------- .../usage/SessionUsageEmptyState.tsx | 8 +- .../usage/SessionUsageModal.appearance.ts | 8 - .../components/usage/SessionUsageModal.scss | 12 - .../usage/SessionUsageModal.test.tsx | 154 -- .../components/usage/SessionUsageModal.tsx | 106 -- .../components/usage/SessionUsagePanel.scss | 822 +++-------- .../components/usage/SessionUsagePanel.tsx | 1290 +---------------- .../usage/SessionUsageReportCard.tsx | 99 +- .../components/usage/SessionUsageSummary.tsx | 227 +++ .../usage/ToolDefinitionsPreview.scss | 103 ++ .../usage/ToolDefinitionsPreview.test.tsx | 125 ++ .../usage/ToolDefinitionsPreview.tsx | 149 ++ .../flow_chat/components/usage/appearance.ts | 34 +- .../usage/sessionUsageModalState.ts | 107 -- .../usage/toolDefinitionsDocument.ts | 57 + .../openSessionContextPreview.test.ts | 138 ++ .../services/openSessionContextPreview.ts | 64 + .../services/openSessionUsageReport.ts | 66 + .../services/usageReportService.test.ts | 131 +- .../flow_chat/services/usageReportService.ts | 26 +- .../src/flow_chat/session-drivers/types.ts | 1 + .../api/service-api/SessionAPI.test.ts | 16 + .../api/service-api/SessionAPI.ts | 24 +- .../appearance/appearanceTokenContract.ts | 2 + .../builtins/buildBuiltinAppearance.ts | 3 + .../builtins/composeAppearancePackage.test.ts | 31 + .../compiler/AppearanceCompiler.test.ts | 9 +- .../preferences/flowChatWidthStore.test.ts | 42 + .../preferences/flowChatWidthStore.ts | 42 + .../registry/defaultAppearanceRegistry.ts | 5 +- src/web-ui/src/locales/en-US/flow-chat.json | 91 +- .../locales/en-US/settings/appearance.json | 8 + src/web-ui/src/locales/zh-CN/flow-chat.json | 89 +- .../locales/zh-CN/settings/appearance.json | 8 + src/web-ui/src/locales/zh-TW/flow-chat.json | 89 +- .../locales/zh-TW/settings/appearance.json | 8 + src/web-ui/src/shared/types/panelContent.ts | 1 + 92 files changed, 4064 insertions(+), 3181 deletions(-) create mode 100644 src/crates/assembly/core/src/agentic/execution/context_composition.rs create mode 100644 src/crates/assembly/core/src/agentic/persistence/manager/usage_context.rs create mode 100644 src/web-ui/src/app/scenes/settings/pages/application/FlowChatWidthSettingsSection.test.tsx create mode 100644 src/web-ui/src/app/scenes/settings/pages/application/FlowChatWidthSettingsSection.tsx create mode 100644 src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.scss create mode 100644 src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.test.tsx create mode 100644 src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.tsx create mode 100644 src/web-ui/src/flow_chat/components/modern/flowChatScrollbarMask.test.ts create mode 100644 src/web-ui/src/flow_chat/components/modern/flowChatScrollbarMask.ts delete mode 100644 src/web-ui/src/flow_chat/components/usage/SessionUsageModal.appearance.ts delete mode 100644 src/web-ui/src/flow_chat/components/usage/SessionUsageModal.scss delete mode 100644 src/web-ui/src/flow_chat/components/usage/SessionUsageModal.test.tsx delete mode 100644 src/web-ui/src/flow_chat/components/usage/SessionUsageModal.tsx create mode 100644 src/web-ui/src/flow_chat/components/usage/SessionUsageSummary.tsx create mode 100644 src/web-ui/src/flow_chat/components/usage/ToolDefinitionsPreview.scss create mode 100644 src/web-ui/src/flow_chat/components/usage/ToolDefinitionsPreview.test.tsx create mode 100644 src/web-ui/src/flow_chat/components/usage/ToolDefinitionsPreview.tsx delete mode 100644 src/web-ui/src/flow_chat/components/usage/sessionUsageModalState.ts create mode 100644 src/web-ui/src/flow_chat/components/usage/toolDefinitionsDocument.ts create mode 100644 src/web-ui/src/flow_chat/services/openSessionContextPreview.test.ts create mode 100644 src/web-ui/src/flow_chat/services/openSessionContextPreview.ts create mode 100644 src/web-ui/src/infrastructure/appearance/preferences/flowChatWidthStore.test.ts create mode 100644 src/web-ui/src/infrastructure/appearance/preferences/flowChatWidthStore.ts diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index f245d23c4b..daa54c601e 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -467,6 +467,9 @@ except for error tone. Use `showIcon={false}` for text-only notices. uncontrolled open state, trigger/region accessibility wiring, focus exclusion while collapsed, reduced-motion behavior, and independent header actions. Product copy and the revealed content remain consumer-owned. +Use `headerClassName` to style the default header surface while retaining its +trigger, actions, and focus behavior. `triggerClassName` and `contentInnerClassName` +allow consumers to align the indicator and revealed content without replacing the trigger. Use `Disclosure presentation="native"` when the browser must own the existing `details`/`summary` behavior. This mode renders a real details element, a direct diff --git a/design-system/packages/ui/src/components/Disclosure/Disclosure.meta.ts b/design-system/packages/ui/src/components/Disclosure/Disclosure.meta.ts index 130120ea78..c6d2e9a1b5 100644 --- a/design-system/packages/ui/src/components/Disclosure/Disclosure.meta.ts +++ b/design-system/packages/ui/src/components/Disclosure/Disclosure.meta.ts @@ -7,6 +7,8 @@ export const disclosureMeta = { name: "Disclosure", props: [ { name: "renderHeader", type: "(triggerProps: DisclosureTriggerProps) => ReactNode" }, + { name: "headerClassName", type: "string" }, + { name: "triggerClassName", type: "string" }, { name: "contentClassName", type: "string" }, { name: "contentInnerClassName", type: "string" }, { name: "unmountOnClose", type: "boolean", defaultValue: "false" }, diff --git a/design-system/packages/ui/src/components/Disclosure/Disclosure.tsx b/design-system/packages/ui/src/components/Disclosure/Disclosure.tsx index 9ebc192457..0e7738a80d 100644 --- a/design-system/packages/ui/src/components/Disclosure/Disclosure.tsx +++ b/design-system/packages/ui/src/components/Disclosure/Disclosure.tsx @@ -23,6 +23,9 @@ interface CustomDisclosureProps actions?: ReactNode; /** Compose a header with independent controls using the supplied toggle attributes. */ renderHeader?: (triggerProps: DisclosureTriggerProps) => ReactNode; + /** Style the default header surface without replacing its trigger or actions. */ + headerClassName?: string; + triggerClassName?: string; contentClassName?: string; contentInnerClassName?: string; unmountOnClose?: boolean; @@ -53,6 +56,8 @@ const CustomDisclosure = forwardRef( function Disclosure({ actions, renderHeader, + headerClassName, + triggerClassName, contentClassName, contentInnerClassName, unmountOnClose = false, @@ -107,11 +112,11 @@ const CustomDisclosure = forwardRef( id: triggerId, onClick: toggle, type: 'button', - }) :
+ }) :
}> + + + + + ); +} diff --git a/src/web-ui/src/app/scenes/settings/settingsRegistry.ts b/src/web-ui/src/app/scenes/settings/settingsRegistry.ts index 50346c49aa..b83309acf2 100644 --- a/src/web-ui/src/app/scenes/settings/settingsRegistry.ts +++ b/src/web-ui/src/app/scenes/settings/settingsRegistry.ts @@ -88,6 +88,7 @@ export const SETTINGS_PAGE_MANIFESTS: readonly SettingsPageManifest[] = [ 'theme', 'font', 'size', 'motion', 'animation', 'appearance pack', 'skin', 'import', 'session', 'panel', 'layout', 'overlay', + 'chat', 'content', 'width', 'resize', ], namespaces: ['settings/appearance', 'settings/application'], searchPhrases: [ @@ -100,6 +101,8 @@ export const SETTINGS_PAGE_MANIFESTS: readonly SettingsPageManifest[] = [ phrase('settings/appearance', 'sessionPanel.displayMode'), phrase('settings/appearance', 'sessionPanel.split'), phrase('settings/appearance', 'sessionPanel.overlay'), + phrase('settings/appearance', 'contentWidth.title'), + phrase('settings/appearance', 'contentWidth.label'), ], load: () => import('./pages/application/AppearanceSettingsPage'), }), diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 3b798e60c9..ea2919c20c 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -3782,6 +3782,7 @@ export const ChatInput: React.FC = ({ effectiveTargetSessionId, { isProcessing: !!derivedState?.isProcessing, + reportTitle: t('usage.title'), busyMessage: t('chatInput.usageBusy'), noWorkspaceMessage: t('chatInput.usageNoWorkspace'), failedTitle: t('chatInput.usageFailed'), diff --git a/src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.scss b/src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.scss new file mode 100644 index 0000000000..3e4f560eec --- /dev/null +++ b/src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.scss @@ -0,0 +1,67 @@ +.flow-chat-width-boundary { + position: absolute; + inset-block: var(--openbitfun-space-12); + left: 50%; + transform: translateX(-50%); + pointer-events: none; + z-index: var(--openbitfun-layer-overlay); + + &[hidden] { display: none; } + + &__handle { + position: absolute; + inset-block: 0; + width: var(--openbitfun-space-3); + transform: translateX(-50%); + cursor: col-resize; + touch-action: none; + user-select: none; + pointer-events: auto; + outline: none; + + &--left { left: 0; } + &--right { left: 100%; } + + &::before, + &::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + background: var(--openbitfun-color-border-default); + opacity: 0; + transition: opacity var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + } + + &::before { + top: 50%; + height: 50%; + width: var(--openbitfun-border-width-default); + transform: translate(-50%, -50%); + } + + &::after { + top: 50%; + height: var(--openbitfun-space-8); + width: var(--openbitfun-border-width-strong); + border-radius: var(--openbitfun-radius-pill); + transform: translate(-50%, -50%); + background: var(--openbitfun-color-content-secondary); + } + + &:hover::before { opacity: 0.5; } + &:hover::after, + &:focus-visible::before, + &:focus-visible::after { opacity: 1; } + + &:focus-visible::after { background: var(--openbitfun-color-focus-ring); } + } + + &[data-resizing] &__handle::before { opacity: 0.5; } + &[data-resizing] &__handle::after { opacity: 1; } +} + +@media (prefers-reduced-motion: reduce) { + .flow-chat-width-boundary__handle::before, + .flow-chat-width-boundary__handle::after { transition: none; } +} diff --git a/src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.test.tsx b/src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.test.tsx new file mode 100644 index 0000000000..260c5f5dbc --- /dev/null +++ b/src/web-ui/src/flow_chat/components/FlowChatWidthBoundary.test.tsx @@ -0,0 +1,130 @@ +// @vitest-environment jsdom +import React, { act, useRef } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { flowChatWidthStore, FLOW_CHAT_WIDTH_STORAGE_KEY } from '@/infrastructure/appearance/preferences/flowChatWidthStore'; +import { FlowChatWidthBoundary } from './FlowChatWidthBoundary'; + +vi.mock('@/infrastructure/i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })); +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +describe('FlowChat content-width boundary', () => { + let host: HTMLDivElement; + let root: Root; + let paneWidth: number; + let resized: () => void; + let resizing: ReturnType; + let writes: ReturnType; + const pane = () => host.querySelector('[data-testid="pane"]')!; + const handle = (side = 'right') => host.querySelector(`.flow-chat-width-boundary__handle--${side}`)!; + const renderedWidth = () => pane().style.getPropertyValue('--openbitfun-control-flow-chat-transcript-max-width'); + function Harness({ active }: { active: boolean }) { + const paneRef = useRef(null); + return