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
3 changes: 3 additions & 0 deletions design-system/packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,6 +56,8 @@ const CustomDisclosure = forwardRef<HTMLElement, CustomDisclosureProps>(
function Disclosure({
actions,
renderHeader,
headerClassName,
triggerClassName,
contentClassName,
contentInnerClassName,
unmountOnClose = false,
Expand Down Expand Up @@ -107,11 +112,11 @@ const CustomDisclosure = forwardRef<HTMLElement, CustomDisclosureProps>(
id: triggerId,
onClick: toggle,
type: 'button',
}) : <div className={styles.header} data-openbitfun-part="header">
}) : <div className={classNames(styles.header, headerClassName)} data-openbitfun-part="header">
<button data-overflow-trigger
aria-controls={contentId}
aria-expanded={resolvedOpen}
className={styles.trigger}
className={classNames(styles.trigger, triggerClassName)}
disabled={disabled}
id={triggerId}
onClick={toggle}
Expand Down
4 changes: 3 additions & 1 deletion design-system/packages/ui/tests/disclosure.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { Disclosure } from "../dist/index.js";
test("Disclosure connects its trigger and region with accessible state", () => {
const markup = renderToStaticMarkup(createElement(
Disclosure,
{ defaultOpen: true, summary: "Advanced" },
{ defaultOpen: true, summary: "Advanced", headerClassName: "report-heading", triggerClassName: "report-trigger" },
createElement("button", null, "Reset"),
));

assert.match(markup, /data-openbitfun-component="disclosure"/);
assert.match(markup, /class="[^"]*report-heading" data-openbitfun-part="header"/);
assert.match(markup, /<button[^>]*class="[^"]*report-trigger"/);
assert.match(markup, /data-open="true"/);
assert.match(markup, /aria-expanded="true"/);
assert.match(markup, /aria-controls="openbitfun-disclosure-[^"]+-content"/);
Expand Down
9 changes: 9 additions & 0 deletions docs/architecture/theme-token-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ accent,由 Web UI Appearance 提供默认值并接受皮肤覆盖。暗色全
青蓝两种材质色;其他组件的青色 Token 也不属于更新材质。该差异因此由更新组件的最小契约承载,
不新增基础色。旧皮肤通过现有组合逻辑补齐默认值。

Session Usage 的输入、输出数据系列使用 `--openbitfun-component-session-usage-input` 和
`--openbitfun-component-session-usage-output`,分别引用对应明暗设计系统主题的蓝色信息强调色与紫色次强调色。
产品主题的主、次强调色可能同时为黑灰色,不能保证数据系列的彩色区分。
这两个最小组件令牌由 Appearance 统一提供,环形图、分段图和图例共用,不新增基础色。旧皮肤自动补齐,显式覆盖继续保留。
Token 分段图将缓存命中与未命中归入输入分组:命中复用输入蓝色,未命中以同色与场景底色混合区分,输出保留紫色。
上下文的其他分类复用 success、warning、danger 强调色,并使用 `color-mix()` 与输入/输出系列色组合区分,
不增加基础色或图表组件令牌;图例文字与数值始终保留,颜色不作为唯一的信息来源。
工具定义预览的折叠标题复用这组颜色,与场景底色混合成柔和背景;展开与收起均保留底色,正文维持场景底色。

普通应用组件和页面必须满足:

- 颜色、阴影和 blur 只从 canonical Token 取得;允许用 `color-mix()`、gradient 等 CSS 运算组合 Token。
Expand Down
93 changes: 93 additions & 0 deletions docs/features/session-runtime-usage-report-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,99 @@

## Background

### Current side-panel scope

The side panel displays session/project identity, context
composition, and cumulative token shares. The former Usage details chapter and
its Overview, Models, Tools, Files, Errors, and Slowest tabs are removed. Chat
cards retain their report entry and compact summaries, but no longer offer jumps
to those removed tabs. Report data and Markdown export remain intact. Saved
panel tab hints are accepted and show the summary. Appearance parts now describe
the remaining UI; overrides for removed detail parts follow the existing
structured incompatibility diagnostics without rewriting saved packages. Detailed
tab and navigation descriptions in the earlier implementation stages below are
historical, not the current side-panel UI.

The session title is centered independently of the copy action. The generation
timestamp is omitted from the panel header; report and export timestamps remain intact.

The system-prompt, tool-definition, rules, skills, MCP/dynamic-tools and subagent
legend rows expose an external-link button on hover or keyboard focus. Each opens
its own read-only preview tab. Tool definitions use a searchable, expandable
`tool-definitions` document reader, showing tool descriptions and parameter types,
required flags, defaults, enums, nested structures and schema constraints. It
projects the existing captured definition array without altering persisted
sessions; the Markdown envelope and calling instructions remain compatible with
older captures. Other categories use the standard `markdown-editor`. The executing host replaces a single
session-local `usage-context.json` snapshot when preparing a request, using the
same typed sources as the category estimates. It includes prompt/reminder text,
native tool schemas and the model-visible Skill/GetToolSpec results; it does not
reload today's catalog or workspace files to reconstruct earlier content.

An explicit `contextPreviewCategory` request returns only that category in
`contextPreview`, including its capture time. Empty captured categories open an
empty-content message; absent snapshots/categories return unavailable. A snapshot
whose owning turn has been replaced or reverted is not exposed. This is the
latest prepared request, not a historical request selected from the chart.
The older `includeSystemPromptPreview` / `systemPromptPreview` cached-system-message
path remains readable as a fallback for older hosts and sessions. Ordinary usage
reports, persisted `/usage` results, and Markdown exports remain content-free.
Dispatch projections refuse preview instead of reading the controller's cache.
Tab identity and late responses remain scoped to the originating device, session,
and category; no filesystem editor path is created, and the standard Markdown
editor stays read-only in rich and source modes. Retained tool-definition Markdown
tabs also render through the structured reader and retain their tab identity.

### Context composition and token shares

The side panel separates the latest recorded parent request's context composition
from cumulative session token usage. The context donut has eight categories:
system prompt (including runtime context), tool definitions, rules (workspace/user
instructions), skills, MCP/dynamic tools, subagent definitions, summarized
conversation, and conversation. Typed runtime message/reminder provenance assigns
categories; user-authored text is never classified by matching prompt headings.
Counts use the runtime token estimator, are labelled estimates, and are not
provider billing counts or measured context-window occupancy.

Numeric-only composition snapshots travel in the existing opaque usage-record
`token_details.openbitfunModelRequest.contextComposition` field. The additive
optional report `context` field projects the newest in-scope parent request;
subagent records do not replace it. A newer request without a snapshot does not
reuse stale composition. Old records and external hosts without this telemetry
show an explicit empty state, while existing report totals remain usable.
The same runtime/report path runs on the executing host for local, SSH, Peer,
and detached jobs; no controller filesystem reads or new commands are required.

The leading project, cumulative token, and turn facts use the home overview's
rounded outline and inset vertical dividers, with centered values above labels.
The Token Usage section precedes context composition. Its continuous horizontal bar groups
cumulative tokens into input and output, with total tokens as the denominator.
With recorded cache counts, the input group splits into cached and remaining
parts in two shades of blue; output stays purple. Group labels above the bar show
input/output totals and shares. Cache hit rate appears in parentheses immediately
after the input share, with the cached/input calculation in its tooltip. The child legend
shows percentages of input, with counts and the denominator in tooltips. Parent
hover/focus highlights both input parts; a child highlights its own segment and
parent label. For complete telemetry, remaining input is labelled uncached. Partial
telemetry still plots recorded cache reads, but labels the remainder as remaining
input, including unknown cache usage. The inline hit percentage uses an explicit
lower-bound marker (≥) over all input tokens and a scope explanation in its tooltip;
it does not extrapolate the reported-call hit rate to unreported calls. Missing
or invalid cache counts keep a single input segment. Invalid or inconsistent
totals never produce invented segments.
Child widths are normalized within each parent group so they fill that group
instead of applying the overall share a second time. Nonzero groups and segments
have small visibility minimums; labels retain the actual percentages and zero
segments are omitted.

The token-level hit rate uses summed cache reads divided by summed total input
over the same records, not an average of per-request percentages or a share of
input plus output. This follows the [OpenAI caching guide](https://developers.openai.com/api/docs/guides/prompt-caching).
The adapter already normalizes [Anthropic usage](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)
by adding ordinary input, cache reads and cache writes; only reads enter the hit
numerator. The report's partial-coverage rate covers reported records only, so it
is not presented as the overall input hit rate or used to infer uncached input.

Long OpenBitFun sessions can include model streaming, tool execution, Git operations, file writes, Skills, MCP calls, context compression, subagents, retries, user confirmation waits, and file diffs. The chat transcript shows what happened, but it does not yet answer the user's operational questions:

- What recorded runtime spans are available for the session, and where are they approximate?
Expand Down
2 changes: 2 additions & 0 deletions src/apps/cli/src/dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,8 @@ async fn query_in_store(
workspace_path: Some(job.request.workspace_path.clone()),
remote_connection_id: None,
remote_ssh_host: None,
include_system_prompt_preview: false,
context_preview_category: None,
include_hidden_subagents: false,
},
)
Expand Down
2 changes: 2 additions & 0 deletions src/apps/cli/src/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,8 @@ pub(crate) async fn print_usage_report(session_id: Option<&str>) -> Result<()> {
workspace_path: Some(workspace_path.to_string_lossy().to_string()),
remote_connection_id: None,
remote_ssh_host: None,
include_system_prompt_preview: false,
context_preview_category: None,
include_hidden_subagents: true,
})
.await
Expand Down
2 changes: 2 additions & 0 deletions src/apps/cli/src/modes/chat/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ impl ChatMode {
workspace_path: Some(workspace_path),
remote_connection_id: None,
remote_ssh_host: None,
include_system_prompt_preview: false,
context_preview_category: None,
include_hidden_subagents: true,
})
.await
Expand Down
6 changes: 6 additions & 0 deletions src/apps/desktop/src/api/session_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ pub struct GetSessionUsageReportRequest {
pub remote_ssh_host: Option<String>,
#[serde(default = "default_include_hidden_subagents")]
pub include_hidden_subagents: bool,
#[serde(default)]
pub include_system_prompt_preview: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_preview_category: Option<openbitfun_core_types::UsageContextCategory>,
}

fn default_include_hidden_subagents() -> bool {
Expand Down Expand Up @@ -553,6 +557,8 @@ pub async fn get_session_usage_report(
),
request.session_id,
request.include_hidden_subagents,
request.include_system_prompt_preview,
request.context_preview_category,
)
.await
.map_err(|error| {
Expand Down
4 changes: 4 additions & 0 deletions src/apps/desktop/src/runtime/session_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,8 @@ impl DesktopSessionApplication {
request: DesktopSessionScopeRequest,
session_id: String,
include_hidden_subagents: bool,
include_system_prompt_preview: bool,
context_preview_category: Option<openbitfun_core_types::UsageContextCategory>,
) -> DesktopSessionApplicationResult<SessionUsageReport> {
let scope = self.resolved_scope(request).await?;
let storage_path = self.storage_path(&scope);
Expand All @@ -614,6 +616,8 @@ impl DesktopSessionApplication {
remote_connection_id: scope.remote_connection_id.clone(),
remote_ssh_host: scope.requested_remote_ssh_host.clone(),
include_hidden_subagents,
include_system_prompt_preview,
context_preview_category,
})
.await
.map_err(|error| DesktopSessionApplicationError::Runtime(error.into_message()))?;
Expand Down
9 changes: 9 additions & 0 deletions src/crates/assembly/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,15 @@ Narrower local guides already exist for some subtrees:

## Verification

For opt-in category/system-prompt previews and session-storage isolation:

```bash
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib -- system_prompt_preview usage_context_preview
```

This fixture verifies default report privacy and resolved host storage with a
simulated SSH workspace identity; it does not exercise live remote connections.

For Agent name references, enabled-catalog admission, and prompt catalog names:

```bash
Expand Down
10 changes: 10 additions & 0 deletions src/crates/assembly/core/src/agentic/execution/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
For numeric context composition, category provenance, latest-request report projection,
and attempt-ledger persistence, use:

```bash
cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib -- context_composition account_attempt_ledger_keeps_failed_usage_actual_accounts_and_idempotent_events
```

These fixtures exercise typed runtime sources, persisted attempts, and simulated
SSH report selection; they do not establish live remote transport behavior.

For `stream_processor.rs` changes, run the portable stream suite and the Core
compatibility tests, plus the relevant round-executor tests below:

Expand Down
Loading
Loading