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
11 changes: 5 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,18 @@ jobs:
- name: Benchmark smoke run
run: pnpm run bench:frontend

# Coverage gate (T1.2): c8 enforces the configured line/branch/function
# Coverage gate: c8 enforces the configured line/branch/function
# thresholds from .c8rc.json. Fails the job if coverage drops below them.
- name: Coverage gate (overall >=85%)
run: pnpm run coverage:frontend

# Per-domain coverage gate (T1.2): lib/ files must each meet >=90% line
# Per-domain coverage gate: lib/ files must each meet >=90% line
# coverage (composables excluded — their onMounted/listen() lifecycle hooks
# are structurally unreachable in the headless test runner). Proven feasible
# at 98.79% overall lib/ line coverage.
# are structurally unreachable in the headless test runner).
- name: Coverage gate (lib/ per-file >=90%)
run: pnpm run coverage:lib

# Circular-dependency gate (T2.7): madge fails if any new import cycle is
# Circular-dependency gate: madge fails if any new import cycle is
# introduced. Scans the TS source (Vue SFCs excluded — babel can't parse
# them; their script blocks share the same module graph).
- name: Circular-dependency gate
Expand Down Expand Up @@ -99,7 +98,7 @@ jobs:
- name: Rust tests
run: cargo test --manifest-path src-tauri/Cargo.toml

# Rust coverage report (T1.2): cargo-tarpaulin runs only on Linux. Reported
# Rust coverage report: cargo-tarpaulin runs only on Linux. Reported
# as a build artifact; not yet a hard gate (no committed threshold), but
# surfaces coverage drift on the Rust side alongside the frontend c8 gate.
- name: Rust coverage report (tarpaulin)
Expand Down
392 changes: 192 additions & 200 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

502 changes: 190 additions & 312 deletions README.md

Large diffs are not rendered by default.

479 changes: 173 additions & 306 deletions README.zh-CN.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bbcom",
"private": true,
"version": "0.4.0",
"version": "0.4.1",
"type": "module",
"packageManager": "pnpm@11.5.3",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bbcom"
version = "0.4.0"
version = "0.4.1"
description = "Serial Port Assistant"
edition = "2024"
rust-version = "1.85"
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/commands/ai/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//!
//! The model dispatch is a `match` (a dispatch-table), not `Box<dyn Model>` —
//! `zai_rs`'s `ModelName: Into<String>` is not dyn-safe, so each supported model
//! is a concrete arm (see F13 in the project research). `send_chat` is the single
//! generic hot point that builds the `ChatCompletion` for a concrete model type.
//! is a concrete arm. `send_chat` is the single generic hot point that builds
//! the `ChatCompletion` for a concrete model type.

use serde::Serialize;
use serde_json::Value;
Expand Down
14 changes: 7 additions & 7 deletions src-tauri/src/commands/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,17 @@ pub async fn export_data(request: ExportRequest) -> Result<(), AppError> {
formatter::export(&request.frames, &request.format, &request.path).await
}

/// F12 IPC-bypass export (T2.3). The frontend serializes the capture to a
/// JSONL temp file (one `DataFrame` per line — the same shape the JSONL export
/// emits) and passes only the temp-file path through IPC, instead of pushing
/// up to 100 000 `DataFrame` objects (each with a `data: Vec<u8>` that serde
/// expands to a JSON number array) through `invoke`. The Rust side reads and
/// parses the file in a `spawn_blocking` task, then runs the normal formatter.
/// Capture-file export. The frontend serializes the capture to a JSONL temp
/// file (one `DataFrame` per line — the same shape the JSONL export emits) and
/// passes only the temp-file path through IPC, instead of pushing up to 100 000
/// `DataFrame` objects (each with a `data: Vec<u8>` that serde expands to a JSON
/// number array) through `invoke`. The Rust side reads and parses the file in a
/// `spawn_blocking` task, then runs the normal formatter.
///
/// This avoids the dominant export cost — serializing the `frames` argument
/// across the IPC boundary — at the price of one temp-file write+read. For a
/// 10k-frame capture the temp file is far cheaper to transfer than 10k JSON
/// objects (F12: very-large transfers are fastest via temp file).
/// objects.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureFileExportRequest {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/commands/ipc_contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ mod tests {
assert_eq!(req.session_meta.as_deref(), Some("COM1@115200"));
}

// ---- export_data_from_capture_file: F12 IPC-bypass (T2.3) ----
// ---- export_data_from_capture_file: capture-file export ----
// The frontend writes a JSONL temp file (one DataFrame/line) and passes only
// the path; the wire shape is camelCase to match CaptureFileExportRequest.

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/commands/updater.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Auto-updater command (T3.9 F-f).
//! Auto-updater command.
//!
//! Wraps `tauri-plugin-updater`'s check + download + install flow behind a
//! typed command so the frontend can trigger "check for updates" without
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "bbcom",
"version": "0.4.0",
"version": "0.4.1",
"identifier": "com.bbcom.app",
"build": {
"beforeDevCommand": "pnpm dev",
Expand All @@ -19,7 +19,7 @@
}
],
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: http://asset.localhost https://asset.localhost data:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://localhost:5173 ws://localhost:5173"
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; style-src-elem 'self' 'unsafe-inline'; style-src-attr 'unsafe-inline'; img-src 'self' asset: http://asset.localhost https://asset.localhost data:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://localhost:5173 ws://localhost:5173"
}
},
"bundle": {
Expand Down
4 changes: 2 additions & 2 deletions src/components/session/SessionToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--
Session toolbar: the connection controls + display/view/format toggles.
Extracted from SessionView (T3.1) so SessionView stays a thin layout
Extracted from SessionView so SessionView stays a thin layout
orchestrator. The toolbar is purely presentational: it receives the reactive
state it needs (serialState refs, the session, appStore flags, viewMode,
isExporting) and emits one event per action — no business logic lives here.
Expand Down Expand Up @@ -482,7 +482,7 @@ const displayModeOptions: { label: string; value: DisplayMode }[] = [
}
}

/* U-a (T3.3): on very narrow screens the toolbar switches to horizontal
/* On very narrow screens the toolbar switches to horizontal
scroll rather than wrapping/clipping, so no controls are hidden. */
@media (max-width: 600px) {
.session-toolbar {
Expand Down
4 changes: 2 additions & 2 deletions src/components/session/SessionView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="session-view">
<!--
Toolbar extracted (T3.1): connection controls + display/view/format
Toolbar extracted from SessionView: connection controls + display/view/format
toggles live in SessionToolbar. This component is the layout orchestrator
(toolbar + display-area + send-area) and owns the connection/Modbus/export
state, wiring the toolbar's events to the composables.
Expand Down Expand Up @@ -162,7 +162,7 @@ const serialState = useSerialConnection(
onOverflow: (total) => {
message.warning(t('serial.error.rxOverflow', { bytes: formatBytes(total) }));
// Mirror the cumulative dropped-byte count onto the session so the
// StatusBar can surface it as a live metric (T3.5) without the connection composable.
// StatusBar can surface it as a live metric without the connection composable.
sessionStore.updateDroppedBytes(props.session.id, total);
},
autoReconnect: () => appStore.autoReconnect,
Expand Down
8 changes: 4 additions & 4 deletions src/components/status-bar/StatusBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ watch(
txRate.value = Math.round(txDelta / elapsed);
rxRate.value = Math.round(rxDelta / elapsed);
}
// Frames-per-second: sample the live frame count delta (T3.5).
// Frames-per-second: sample the live frame count delta.
let frameDelta = props.session.frames.length - prevFrames;
if (frameDelta < 0) frameDelta = props.session.frames.length;
frameRate.value = Math.round(frameDelta / elapsed);
Expand Down Expand Up @@ -161,14 +161,14 @@ const duration = computed(() => {
return formatDuration(now.value - props.session.startTime);
});

/** Buffer level: how full the rolling frame buffer is (T3.5). */
/** Buffer level: how full the rolling frame buffer is. */
const bufferLevel = computed(() => {
if (!props.session) return '';
const pct = Math.round((props.session.frames.length / maxBufferFrames.value) * 100);
return `${props.session.frames.length}/${maxBufferFrames.value} (${pct}%)`;
});

/** Cumulative dropped bytes this connection (T3.5). */
/** Cumulative dropped bytes this connection. */
const droppedDisplay = computed(() => {
if (!props.session || props.session.droppedBytes === 0) return '';
return formatBytes(props.session.droppedBytes);
Expand All @@ -195,7 +195,7 @@ const droppedDisplay = computed(() => {

.stat,
.status-pill {
/* U-b (T3.3): unified status-pill base — every metric chip (port, rate,
/* Unified status-pill base — every metric chip (port, rate,
frames/s, buffer, dropped, TX/RX) shares this contract so the StatusBar
has one visual rhythm instead of two subtly-different ones. */
display: flex;
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/ModbusAddRegisterForm.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!--
Add-register form: shares the table's grid columns so each field sits directly
under its column header. Owns the draft state and emits `add` with the new
register's fields. Extracted from ModbusPanel (T3.2). The R/W and Value
register's fields. Extracted from ModbusPanel. The R/W and Value
columns are omitted (no meaning until the row exists); the trailing Unit +
Add fields land in the last two columns.
-->
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/ModbusHeader.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!--
Modbus panel header: identity + transport + master enable + status, the
timing inputs (poll/write/timeout + write-source), and the action buttons
(read-all / send-all / replay / load / save). Extracted from ModbusPanel (T3.2)
(read-all / send-all / replay / load / save). Extracted from ModbusPanel
so the panel stays under 400 lines. Presentational: receives config + flags,
emits one event per action and a `patch` event for config edits.
-->
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/ModbusRegisterRow.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
One register table row. Extracted from ModbusPanel (T3.2) so the panel stays
One register table row. Extracted from ModbusPanel so the panel stays
under 400 lines. The row edits the store directly (it has the sessionId) and
emits plot/read/send/remove for the few actions the parent wires to the master.
Receives the shared option lists + the per-row send-success flash id.
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/ParserConfigBar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--
Parser config bar: title + preset/kind/delimiter/fixed/length config inputs +
close button. Extracted from ParserPanel (T3.2). Two-way binds the config
close button. Extracted from ParserPanel. Two-way binds the config
fields via v-model so the parent owns the parser-config state.
-->
<template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/ParserFrameDetail.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--
Parser frame detail panel: hex/ascii dump of the selected parsed frame, plus
the empty state. Extracted from ParserPanel (T3.2). Receives the selected
the empty state. Extracted from ParserPanel. Receives the selected
frame (or null) and the pre-computed dump rows; emits copy / copy-ascii.
-->
<template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/ParserStatsBar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--
Parser stats + search bar: live frame count, total bytes, throughput, largest
frame, and the filter search box. Extracted from ParserPanel (T3.2).
frame, and the filter search box. Extracted from ParserPanel.
Presentational; search is two-way bound via v-model:search-term.
-->
<template>
Expand Down
2 changes: 1 addition & 1 deletion src/components/terminal/WaveformLegend.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!--
Waveform legend + actions + per-channel stats. Extracted from WaveformPanel
(T3.2). Presentational: receives the channel state + stats, emits per-channel
Presentational: receives the channel state + stats, emits per-channel
toggle and the toolbar actions (toggle-mode/pause/clear/load/export). Pause
is two-way bound.
-->
Expand Down
25 changes: 12 additions & 13 deletions src/composables/useExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ export interface UseExportDeps {
/** Invoke the Rust export command (legacy: frames cross IPC as a JSON array). */
exportFrames?: (frames: DataFrame[], format: ExportFormat, path: string) => Promise<void>;
/**
* F12 IPC-bypass export path (T2.3): write the frames to a JSONL temp file
* and invoke `export_data_from_capture_file`. When provided, this is preferred
* over `exportFrames` because it avoids serializing the frames array through
* the `invoke` argument. Defaults to the real temp-file write + command.
* Capture-file export path: write the frames to a JSONL temp file and invoke
* `export_data_from_capture_file`. When provided, this is preferred over
* `exportFrames` because it avoids serializing the frames array through the
* `invoke` argument. Defaults to the real temp-file write + command.
*/
exportViaCaptureFile?: (
frames: DataFrame[],
format: ExportFormat,
targetPath: string,
) => Promise<void>;
/**
* Whether to use the F12 capture-file bypass (production default). Set to
* Whether to use the capture-file export path (production default). Set to
* false to force the legacy exportFrames path (used by unit tests that stub
* exportFrames, and as a fallback if the bypass is unavailable).
* exportFrames, and as a fallback if the capture-file path is unavailable).
*/
useCaptureFileBypass?: boolean;
}
Expand All @@ -63,8 +63,8 @@ export function useExport(deps: UseExportDeps = {}) {
if (!path) return { ok: false };

const format = resolveExportFormat(choice, displayMode);
// Prefer the F12 IPC-bypass path (production default): it writes the
// frames to a JSONL temp file and sends only the path, avoiding
// Prefer the capture-file path (production default): it writes the frames
// to a JSONL temp file and sends only the path, avoiding
// serialization of up to 100k frames through the invoke argument. A caller
// forces the legacy exportFrames path by passing useCaptureFileBypass:
// false (or by stubbing exportFrames, e.g. in unit tests).
Expand Down Expand Up @@ -105,13 +105,12 @@ async function defaultPromptSave(choice: ExportChoice): Promise<string | null> {
}

/**
* Default F12 IPC-bypass export: write each frame as one JSONL line to a temp
* Default capture-file export: write each frame as one JSONL line to a temp
* file (via the stateless append_log command), then invoke
* export_data_from_capture_file which reads+parses it on the Rust side. The
* temp file lives in the system temp dir; the filename is unique per call.
* export_data_from_capture_file which reads+parses it on the Rust side.
*
* Each frame crosses IPC as a small text append rather than as an element of a
* giant JSON array on the invoke argument — the dominant export cost (F12).
* giant JSON array on the invoke argument — the dominant export cost.
*/
async function defaultExportViaCaptureFile(
frames: DataFrame[],
Expand All @@ -134,7 +133,7 @@ async function defaultExportViaCaptureFile(
}
}

/** Resolve a unique temp-file path for the F12 capture. The path is constructed
/** Resolve a unique temp-file path for the capture. The path is constructed
* in the OS temp dir with a timestamp+random suffix to avoid collisions. */
async function defaultCaptureFilePath(): Promise<string> {
// The frontend cannot directly access the OS temp dir without the fs plugin,
Expand Down
4 changes: 2 additions & 2 deletions src/composables/useSerialConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const MAX_RX_QUEUE_CHUNKS = 512;
const RECONNECT_INTERVAL_MS = 1500;
const MAX_RECONNECT_ATTEMPTS = 10;

/** Result of validating + encoding a send payload (the input gate to COW-1's
* serialized write chain). Exported for unit testing. */
/** Result of validating + encoding a send payload before it enters the
* serialized write chain. Exported for unit testing. */
export type SendPayloadResult =
| { ok: true; payload: Uint8Array }
| { ok: false; reason: 'empty' | 'bad-hex' | 'too-large' };
Expand Down
10 changes: 5 additions & 5 deletions src/lib/ai-models.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/**
* AI model dispatch table (F13 / T3.8).
* AI model dispatch table.
*
* The Rust side (`commands/ai/service.rs` `send_chat_by_name`) already uses a
* `match` dispatch-table (not `Box<dyn Model>` — AP-2: `ModelName: Into<String>`
* `match` dispatch-table (not `Box<dyn Model>` — `ModelName: Into<String>`
* is not dyn-safe). This module is the frontend's mirror: the single source of
* truth for which models exist, their display labels, and whether each supports
* streaming (F14). The IPC layer validates model names against this table before
* streaming. The IPC layer validates model names against this table before
* invoking the Rust command, so an unknown model is caught client-side.
*/

Expand All @@ -14,7 +14,7 @@ export interface AiModelEntry {
id: string;
/** Human-readable label for the settings dropdown. */
label: string;
/** Whether this model supports SSE streaming output (F14). */
/** Whether this model supports SSE streaming output. */
streaming: boolean;
}

Expand All @@ -41,7 +41,7 @@ export function isValidAiModel(id: string): boolean {
return AI_MODEL_IDS.includes(id);
}

/** True if `id` supports SSE streaming output (F14). */
/** True if `id` supports SSE streaming output. */
export function supportsStreaming(id: string): boolean {
return AI_MODELS.find((m) => m.id === id)?.streaming ?? false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/ai-stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* SSE streaming token accumulator (F14 / T3.8).
* SSE streaming token accumulator.
*
* The Rust AI command streams response tokens via `enable_stream().stream_sse_for_each`
* (zai-rs 0.1.15). On the frontend, these tokens arrive incrementally (via a Tauri
Expand All @@ -11,7 +11,7 @@
* without a live SSE connection.
*/

/** One incremental token from the SSE stream (delta.content, per F14). */
/** One incremental token from the SSE stream (`delta.content`). */
export interface SseDelta {
/** The incremental text token (may be empty for keep-alive). */
delta: string;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bbrec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* `.bbrec` raw byte-stream record / replay (T3.6).
* `.bbrec` raw byte-stream record / replay.
*
* A capture file records the raw RX/TX byte chunks (with direction + a relative
* timestamp) exactly as they arrived on the wire, so a session can be replayed
Expand Down
2 changes: 1 addition & 1 deletion src/lib/export-filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Export filtering helpers (F-e / T3.9).
* Export filtering helpers.
*
* Filters captured frames by a time range (start/end in ms) and/or direction,
* so a user can export just the relevant portion of a long capture. Pure so it
Expand Down
Loading