Commit Graph
136 Commits
Author SHA1 Message Date
Bowen Liang 185c7ffced feat(host): show net freed tokens on compaction and prune rows
The row carried the producer's gross shadow price, while the trend chart
visualizes the net drop. The shadow-price protocol guarantees the
replacement folds in synchronously after the metering event, so rewrite
the event's tokens to removed-minus-replacement at that moment; a
metering event left without a replacement keeps the gross price.
2026-08-24 03:08:59 +08:00
Bowen Liang d0560b6b6b feat(host,client): record plan-mode toggles and resume model switches
Plan mode includes a guidance section in every model request while
active, so its durable plan/mode event is a real context-composition
change and earns a neutral mode event row (own kind chip + filter).
A request header logged with reason 'resume' carrying a different model
is likewise a real switch made between sessions — record it alongside
the in-session 'change' ones.
2026-08-24 03:06:07 +08:00
Bowen Liang e4dab4d17e feat(host,client): show the notice summary on inject event rows
A notice-form source already carries the producer's bounded one-line
account; record it as the event's detail and render the row as
title · producer · account, matching the dsh transcript row shape.
2026-08-24 03:02:07 +08:00
Bowen Liang dc29e0bc67 fix(host): classify any non-'user' source kind as injected context
The dsh transcript treats a user/message as injected context whenever its
durable source kind is not 'user'; the fold's narrower check (plugin /
skill-invocation / has form) misclassified formless producers — goal
continuation rounds, team relays — as user messages, skewing the token
attribution and hiding their inject events. Keep the form check as a
fallback for foreign sources without a readable kind.
2026-08-24 02:58:46 +08:00
Bowen Liang e30e33fc57 feat(host): label injection events with their producer source
Mirror the dsh transcript's context provenance on inject events:
agent-instructions names its reconciled file paths, a plugin source its
plugin id, and any other producer (skill-catalog, ...) its durable kind,
so the events panel shows '目录更新 · skill-catalog' style rows like the
harness GUI does.
2026-08-24 02:54:15 +08:00
Bowen Liang e207083a56 feat(client): frame attachment cards with a hover affordance
Each image attachment card now carries its own border (border-l1, 8px
radius); hover strengthens the frame to border-l2 and washes the card with
the interactive row hover token — the same affordance as the browser's
clickable category and message rows.
2026-08-24 02:36:49 +08:00
Bowen Liang e6dcfeea1c feat(client): preview image attachments in a chat-style lightbox
The attachment card's thumbnail click used to open the full image in a new
browser tab. Now the whole card (tile + metadata column) is one click
target, and clicking it opens an in-page preview modal mirroring the chat
history's ImageLightbox recipe: body portal via the module-table react-dom,
blurred mask on the same dsw theme variables, contain-fit image, circular
close control (IconCloseOutline16 from the seeded ui-primitives), Escape/
mask close, and focus restored to the opener. The ui-attachment package
itself is not seeded into the browser module table and cross-plugin value
imports are gated off, so the recipe is ported onto the plugin's lc-*
classes with fallbacks; load failures still retry on click.
2026-08-24 02:36:41 +08:00
Bowen Liang 239ce675f0 feat(client): restate the Context Stats hint as current-context contents 2026-08-24 00:18:16 +08:00
Bowen Liang d68422a630 feat: count Context Stats images within the current context only
The image count moves off the persisted whole-session counter onto a
per-node imgs field (absent when zero), and the view sums it over the
live surface — matching the Tool Calls cell's semantics: images of a
message compacted or pruned out of the context stop counting. The
persisted-state shape changes, so stateVersion bumps to 8 and cached
rows are refolded.
2026-08-24 00:02:08 +08:00
Bowen Liang 39554df985 feat: add a Tool Calls cell to the Context Stats board
The cell sits left of Images and counts the tool calls whose result is
live in the current context: one tool/result folds to exactly one tool
surface node, so the host derives the figure at view time from the live
surface (no persisted counter, no state-version bump). Calls still in
flight and results compacted or pruned out of the surface are excluded.
2026-08-24 00:01:47 +08:00
Bowen Liang 69c175e76a feat(client): narrow the Context Stats board cells
Shrink the stat-cell grid minimum (76px -> 62px), gap (8px -> 6px) and
padding (8px -> 6px) so the board's nine cells pack tighter.
2026-08-24 00:01:43 +08:00
Bowen Liang e53c375a15 feat(client): hide the composer while the Context tab is active (#14)
* feat: add PR checks workflow for automated testing and validation

* feat(client): hide the composer while the Context tab is active

The view ring mounts one view at a time, so the tab's .lc-root existing
inside the shell scrollport is exactly 'Context tab active' — a single
:has() rule hides the sticky composer seat and hands its height to the
dashboard. display:none keeps the seat mounted, so drafts survive the
round trip.

Interactive takeovers (approval / question / plan-review panels) replace
the bar inside the same seat; keep the seat visible while one is present
so a pending decision is never hidden behind the focus mode.

Closes #11

* ci: build before test in PR checks

Client tests mount the built bundle (viewBed.ts reads lib/client.js), so
pnpm test without a prior build fails with ENOENT in a clean checkout.
2026-08-23 23:33:15 +08:00
Bowen Liang a26f5437c3 fix(client): hoist hooks above early returns — React #310 on data/event arrival
ContextViewBody returned the loading frame BEFORE its displayRequests/
markers useMemos: when the projection value landed on the same mounted
instance (a pure timing race), the hook count grew between renders and
React threw #310, which the ErrorBoundary masked as a '数据读取失败'
card. EventList returned its empty branch before useRef/useLayoutEffect:
an empty -> non-empty transition in one instance at best corrupts the
hook list (React logs 'Expected static flag was missing') and at worst
throws the same #310 on builds that don't recover.

Both early returns now sit below every hook; data-less renders fall back
to empty collections so the memos stay unconditional.

Regression tests mount the built client bundle on real React in jsdom and
drive both transitions (loading -> loaded, events empty -> non-empty),
failing on a boundary catch or any hook-order console error.

Closes #12
2026-08-23 23:03:47 +08:00
sxgitmiao be751da48d feat(client): add Total/Diff display mode to the Context History chart
Adds a display-mode pair (全量/增量, Total/Diff) next to the Step/Turn granularity group in the Context History card. Diff mode plots each request's per-category |change| vs the previous request, with the hover tip reporting the signed net change (Δ +1.2k / Δ −535.5k); Total keeps the cumulative composition and stays the default.

Original PR by @xieshuaix (delta toggle); reworked before merge: the on/off delta switch became a two-way mode control (mode: 'total' | 'diff' end to end), the two toggle groups were wrapped so the title's auto-margin push happens once, trend.hint was shortened/localized, and the fiber-slot/button-count tests were updated.
2026-08-23 22:49:09 +08:00
Bowen Liang 095e2f525e fix(host): tag skill results even when the tool/call event is gone
- drop the redundant node.cat === 'tool' guard (categoryOf always
  returns 'tool' in the tool/result case)
- fall back to the rendered <skill_content> wrapper when node.tool is
  unresolvable (trimmed window, replay), so a skill load never silently
  degrades to an ordinary untagged tool row; the wrapper only appears in
  genuine skill results
- regression test for the fallback path, and that a plain tool result
  without the wrapper stays untagged
2026-08-23 22:25:30 +08:00
Bowen Liang 02807de68c Update src/client/i18n.ts 2026-08-23 21:34:56 +08:00
tangjunyan.5703 a2641b7d29 feat(host): surface skill-tool loads as tagged tool results + Skill inject event
A skill load via the `skill` tool returns the loaded skill's instructions
as a tool result. Keep it a tool result (that is what it is), but make it
findable: tag the node with the skill name (so the Context browser labels the
row `Skill · <name>`) and record an inject event so a `Skill 注入(name)`
entry shows in the Context Events card.

- fold: detect tool/result with node.tool === 'skill', parse the skill name
  from the rendered <skill_content name="…"> wrapper, set node.skill, emit a
  sub:'skill' inject event. Category stays 'tool'.
- browser: tool row shows the Skill · <name> tag when node.skill is set.
- i18n: add node.skillTag (zh/en).
- test: regression for the skill-tool result path.
2026-08-23 18:45:35 +08:00
bowenliang123 c27904b5f5 feat(client): chip the collapsed User Messages rows that carry image uploads
The row joins the conversation snapshot and counts image blocks via the
same imageRefOf the expanded body uses; the chip (localized 'attach.image',
×N when several) shows only while collapsed — expanded, the image grid
speaks for itself.
2026-08-23 05:13:56 +08:00
bowenliang123 93cf924445 fix(host): drop unnecessary ?? 0 guards on TimelineState.images (stateVersion 7 refold guarantees it) 2026-08-23 05:11:27 +08:00
bowenliang123 271f4e3395 refactor(client): unify Context browser detail rendering into one section-card system
Every expanded element body is now a uniform stack of Section cards
(labeled head + body): prose carries the Raw/MD switch in the head, tool
calls/params/images/raw JSON share the same frame, and one blocks→sections
pipeline serves user/assistant/tool-result/injection bodies. Replaces the
floating-switch, bare-pre, and per-kind card idioms; drops the dead
lc-br-blk/lc-br-pre/lc-br-md/lc-br-rich-inline styles and the unused
RichCard/inHead/dim parameters. Element-row tool tags lose their inverted
chip and share the subtle injection-form tag style. Test bed: function
components now receive props.children like real React.
2026-08-23 04:56:17 +08:00
bowenliang123 af8f695854 feat(client): Context tab browser stretches to the left column's height
The /context modal keeps the browser content-sized.
2026-08-23 04:32:56 +08:00
bowenliang123 93dfe3941d feat(client): shorter stats labels (Cache Hit / Cost) 2026-08-23 04:32:55 +08:00
bowenliang123 f15b9ecddc style(client): narrower stats cells (76px min, 8px side padding) 2026-08-23 04:18:32 +08:00
bowenliang123 78245e1b75 feat: whole-session image count in Context Stats
Fold counts image blocks (user uploads + nested tool-result images)
cumulatively over the complete session log, rides the wire snapshot as
an optional additive field, and the stats board shows it as a new cell
between prunes and cache hit. stateVersion 7 refolds cached rows.
2026-08-23 04:18:26 +08:00
bowenliang123 3c48acbed0 fix(host): weekends bill at off-peak rates all day
Follow DeepSeek's peak/off-peak rule adjustment: Saturdays and Sundays
(Beijing Time) no longer distinguish peak windows, so the whole weekend
prices at the half-price off-peak rate. Peak windows are now expressed in
Beijing Time (weekdays 09:00-12:00, 14:00-18:00), equivalent to the old
UTC windows on weekdays. List prices themselves are unchanged.
2026-08-23 03:55:18 +08:00
bowenliang123 c208f55d07 feat(client): labeled Raw/Sent/Token rows on image cards, shown only when known
Each fact gets its own labeled row: Raw (pre-normalization dims), Sent
(normalized dims + stored size, dynamic B/kB/MB units), Token (estimate).
Facts that dsh never recorded (e.g. no originalDimensions, no byte size)
simply leave no row behind.
2026-08-22 18:08:41 +08:00
bowenliang123 eb5c8ebf2d fix(client): wrap long image attachment filenames instead of truncating 2026-08-22 18:08:35 +08:00
bowenliang123 c003916360 feat(client): two equal-width image attachment columns with token badges
The attachment grid becomes a two-column equal-width grid
(repeat(2, minmax(0, 1fr))) and every image card gains a token-cost
line (≈N tokens, with an explanatory tooltip) estimated through the
official DeepSeek image-size conversion on the stored dimensions.
2026-08-22 16:31:13 +08:00
bowenliang123 d3cdc263d6 feat(client): read the official contextBreakdown projection for the composition card
The composition legend and tooltips now show the exact system/tools/
messages counts of the chat composer ring's click-open panel (the
token-meter's contextBreakdown projection) instead of the fold's own
sums re-anchored to the provider total: parts carry the heuristic count
(raw) for display beside the provider-anchored bar width (value), and
the message bucket subdivides into the four surface categories by the
fold's per-category ratios, always summing exactly to the official
message figure. Absent the projection (older harness), the fold's own
sums serve unchanged.
2026-08-22 16:31:05 +08:00
bowenliang123 d9c09782fc feat(host): price image blocks by DeepSeek's official image token calculator
The token-meter heuristic prices image blocks through its generic JSON
branch (~40 tokens for the durable ref), while DeepSeek's vision model
actually bills 117-384 tokens per image by pixel dimensions. Port the
official docs image-token calculator (patch grid 14px, downsample x3,
384-token cap, minPixels 384^2) into src/shared/imageTokens.ts and price
image blocks with it in the fold; unknown dimensions keep the JSON
fallback. stateVersion bumps to 6 so cached rows refold.

The differential fold check replays a parallel image-delta surface so
image-bearing logs still match the official contextBreakdown exactly,
and adapts to the 0.1.1 wire.view contract.
2026-08-22 16:30:55 +08:00
bowenliang123 ad1e6872ba feat(client): collapsed browser rows lead with an inverted tool breadcrumb + call summary
Tool-result and call-bearing assistant rows now open with an inverted
mono chip naming the tool(s) (label/surface tokens swapped — black on
white in light mode, white on black in dark), multiple calls joining as
a breadcrumb (bash › write). The preview then carries the call's own
summary line: a bash-style `description`, else the target path of
path-taking tools (edit/read/write via file_path/path/filePath), falling
back to the reply text or the generic label. The redundant 'calls: …'
preview is gone — the chip says it once.
2026-08-22 04:31:34 +08:00
bowenliang123 654eca8cdb feat(client): tool calls in assistant messages and tool results share one arg card
Both render the call as a card whose head names the tool (→ / ←) with
the argument count, the body lists arguments as name/value rows — the
type column is gone, the name sits in a narrow wrapping left column,
the value wraps in a mono right column (height-capped). The tool name
steps up to 13px mono. The tool result's raw lc-br-call block is
replaced by the same card.
2026-08-22 04:16:56 +08:00
bowenliang123 bef73a5723 feat(client): clickable turn strip — click a turn block to jump to turn granularity and center it 2026-08-22 03:58:26 +08:00
bowenliang123 db56081cd0 fix: keep the trend chart scrollbar off the turn strip
Reserve 8px below the scrollable chart so the horizontal scrollbar no
longer overlays the turn labels.
2026-08-22 03:50:57 +08:00
bowenliang123 c55ab3da3d perf(client): memoize trend-chart bars; fix stale retention comment
The retained request log is bounded by maxKeptTurns/maxRequestSteps
(defaults 300/1500), not 160 as the comment claimed — a long session's
chart is thousands of bar nodes, and every bar hover re-rendered all of
them through ContextViewBody.

Extract the bar as a React.memo component (hover flips flags on at most
two bars; req/marker identities stay stable between projection pushes)
and memoize the parent's turn-aggregation and marker attachment so turn
mode stops allocating fresh bar props on every hover render. Bars carry
data-seq for identification now that the element key lives on the
memoized component; the fake-React test beds learn memo().
2026-08-22 03:49:32 +08:00
bowenliang123 5967200a9b perf(host): consume callNames entries on tool-result fold
The callId→name map is looked up exactly once — when the result event
folds in — but entries were kept for the session's whole lifetime. As
persisted state, the map was serialized on every projection checkpoint
and shallow-copied by every fold step's lazy clone.

Rebuild the map without the consumed ids at lookup time (no dynamic
delete, per repo lint); consume-once holds it at pending-call size, so
the copy is trivial.
2026-08-22 03:45:50 +08:00
bowenliang123 47b7d9eacd feat(client): assistant tool calls render as parsed argument cards
A tool call inside an assistant message's blocks now forms its own card,
mirroring the tool definition's parameter card: the head names the call
target with the parsed argument count, the body lists each argument as a
name/type/value row (values render raw when strings, JSON otherwise).
Arguments that are not a parseable JSON object fall back to the raw
payload inside the same card.

The spec now arms two calls — one parseable, one not — and asserts the
card chrome, the parsed row, the count badge, and the raw fallback. The
view-bed text helpers learned to read number children (React renders
them as text), which the count badge needs.

No new CSS: the card and rows reuse the tool-schema classes.
2026-08-22 03:25:26 +08:00
bowenliang123 8692c31162 build: adopt the official lint stack (oxlint + tsgolint, type-aware)
Mirror dsh's own lint setup (oxlint 1.76.0 pinned exact, oxlint-tsgolint
7.0.2001 with options.typeAware, correctness category off, hand-picked
rule list; jsPlugins: eslint-plugin-sonarjs + @stylistic/eslint-plugin
with the official style profile: indent 2, semi never, single quotes,
comma-dangle multiline, max-len 140). Scope mirrors official too: src TS
only, generated/bundled output (lib, *.js, *.mjs, *.config.ts) ignored.

Wiring: pnpm run lint / lint:fix scripts, plus a Lint step in the
release workflow between Build and Test.

Fixes applied (runtime behavior unchanged; verified against dsh
0.1.0-rc.7 and 0.1.1-rc.2 wire shapes, see below):

- autofix pass: arrow-parens, comma-dangle, eol-last, indent, quotes
- restrict-plus-operands: 31 string+number concatenations -> template
  literals (restrict-template-expressions allows numbers)
- no-unused-vars: drop unused imports/destructured names
- no-unnecessary-type-conversion: drop redundant String() where the
  field is already string-typed
- no-unsafe-*: type the fetch-json boundary in latestVersion, narrow
  unknown before spread/cast instead of trusting any
- no-unnecessary-condition, three honest categories:
  * boundary sanitizers (services.timelineOf/headersOf, RawBlocks,
    headers.recordOf) now read through Record<string, unknown> instead
    of casting first, so the defensive checks are type-meaningful again
  * post-sanitize consumers (contextView/headline/assemble/browser)
    drop the fallback-empty-array on collections the sanitizer
    guarantees
  * array-index-undefined gaps (fold turn attach, headers dedupe) use
    .at() so the past-the-end guard is real to the type system
- no-non-null-assertion: stackedBar reserve carries the checked max
- no-floating-promises: void the fire-and-forget version check
- intentional-contract suppressions with rationale: react.ts keeps its
  injected require (closure-factory loader contract)
- type-honest hardening kept deliberately: MessageSource.sections
  entries stay nullable (foreign plugins author snapshot sources) and
  isInjection still rejects a null source — foreign message payloads
  must never crash the fold

Cross-version audit (0.1.0-rc.7 vs 0.1.1-rc.2, message parsing focus):
- core/session types.ts: only additive optional interrupted on
  assistant/message (unread by us); every event payload we fold
  (request/header, request/context, tool/call, tool/result,
  user/message, assistant/message, compaction/*) is byte-identical
- llm types.ts / message.ts / content.ts block shapes: identical
- token-meter estimate.ts: identical, and official has NO null-entry
  guards on content blocks — our removed guards match official trust,
  our retained fallback-empty-string / typeof reads stay a leniency
  superset
- deriveEventMessage (core/session surface.ts): identical
- ui-conversation chat-nodes contract: unchanged; our client reads are
  minimally-typed unknowns by design
- tool/call callId is a branded string in both versions, so the
  narrowed typeof guard drops only an impossible coercion
2026-08-22 03:10:41 +08:00
bowenliang123 1a8a240d43 fix(deps): zod is a required peer, not optional
The host half value-imports zod (Config schema, timeline/headers wire
validators) and the build keeps it external, so a profile install without
zod would fail at plugin load. optional:true would silence the one
warning that catches that. react and dsh-client-ui-primitives stay
optional: only the browser half uses them and the module table provides
them there.
2026-08-22 02:39:51 +08:00
bowenliang123 598a2e06fa refactor(client): move styles to a real stylesheet through the official CSS channel
- tsdown.config.ts gains the three CSS virtual loaders mirrored verbatim
  from packages/client/tsdown.client.ts: .module.css (lightningcss-hashed
  class map + tagged injection), .css?inline (compiled text), and plain
  .css (unhashed tagged injection), each registered as a watch dependency
- src/client/styles.ts (223 rules as a string array) becomes
  src/client/styles.css — real stylesheet authoring, minified by
  lightningcss at build (client bundle 134.8 -> 130.3 kB)
- injection now happens at factory materialization with the official tag
  shape (data-plugin + data-plugin-css + dedupe guard), replacing the
  hand-rolled ctx.effect; the HMR receiver owns removal by data-plugin,
  so the hmr spec pins that lifecycle instead of fiber disposal
- Decision: class names keep the flat lc-* namespace (the anti-collision
  discipline, per the research note) instead of per-component hashed
  modules — the stylesheet's rules are interconnected across components
  (parent:hover child, shared state classes) and 241 test selectors read
  literal classes off the built bundle, which a content hash would make
  unknowable; the hashed .module.css channel is mirrored and available
  for future self-contained components
- globals.d.ts declares the *.css / *.module.css module shapes for the
  strict typecheck
2026-08-22 02:39:19 +08:00
bowenliang123 021db5f2a2 build: migrate tsup to tsdown, mirroring the official client-bundle preset
- banner/intro/footer three-part closure-factory wrap (verbatim from
  packages/client/tsdown.client.ts), intro now owns var module/exports
- deps.neverBundle/alwaysBundle guards: host keeps production deps
  external, client keeps only module-table specifiers external
- bundle purity gate: any non-requested @deepseek-ai/* value import is
  a build error (INLINE_SAFE / vendored-library / generated-remote
  allowances mirrored)
- NODE_ENV + import.meta.env defines for inlined node-idiom deps
- client sourcemap on, matching the official artifact contract
- client smoke test now runs the bundle verbatim through the
  __ModuleLoader__ handoff instead of slicing the factory body by
  exact output strings (decoupled from bundler layout)
2026-08-22 02:14:44 +08:00
bowenliang123 af14769c0f feat(client): card-style message detail with image attachment rendering
Adapt to DeepSeek Harness 0.1.1's multimodal pipeline (and the vision
capability of DeepSeek-V4-Flash-Vision-Exp): a user message carrying
durable image refs expands into a card layout — prose in the text card,
image attachments as thumbnail cards (name, normalized + original
dimensions, stored size), and unrecognized blocks as raw JSON. Image
blocks in assistant messages and tool results (e.g. read_image output)
now render too, instead of being dropped or dumped as JSON.

Thumbnails load through the harness conversation service's resolveImage
(the same session-authorized loader the chat history uses); when the
service is absent the cards degrade to metadata only. The text card
uses an un-padded RichCard variant so all cards align at one indent.
2026-08-22 01:43:37 +08:00
bowenliang123 2ca9f76720 feat(client): title reasoning/response blocks in assistant message detail 2026-08-22 01:27:46 +08:00
bowenliang123 925c9ccca6 fix(client): no white screen on malformed backend timeline payload
Sanitize the contextTimeline projection into a render-safe shape at the
services boundary (collections -> arrays, non-object entries dropped,
current -> numeric breakdown, wrong-typed scalars dropped) so a corrupt
or failed host payload degrades to empty sections instead of throwing
during render, keeping every usable piece of data visible.

Wrap the Context tab and /context dialog bodies in a React error
boundary (styled error card + Retry) as the backstop for any residual
render error; harden fmtTime against invalid timestamps, headersOf
against malformed epochs, and the cache-hit/cost math against NaN.

Add client regression tests: corrupt payloads render with usable data,
the modal degrades, and a throwing projection reader lands in the error
card.
2026-08-21 23:12:51 +08:00
bowenliang123 1a625fef96 fix(host): dual-contract session-projection units for dsh 0.1.1-rc.1 compatibility
dsh 0.1.1-rc.1 changed the session-projection unit contract: registration
now reads stateSchema (persisted fold state) + wire (client view), and a
unit without a wire block is treated as host-only — its value is never
delivered to the browser. The units still emitted only the pre-0.1.1
fields (schema + top-level view), so on 0.1.1-rc.1 the Context tab stayed
on its loading screen forever: contextTimeline/contextHeaders never
arrived through useProjection.

Emit BOTH contracts on each unit: schema/view for dsh <= 0.1.0-rc.8, and
stateSchema/wire for dsh >= 0.1.1-rc.1 (each registry reads its own
fields off the same definition and ignores the extras). Added a strict
timelineStateSchema for the persisted state (the cold-read rung also
parses checkpoint rows with it) and mirror checks in host.test.mjs.

Verified against the real registries of both versions (snapshot
delivery, change feed, checkpoint, viewCheckpoint, restore) and the real
plugin-install flow on the 0.1.0-rc.7 and 0.1.1-rc.1 CLIs.
2026-08-21 20:41:10 +08:00
bowenliang123 1cc4da0a9f feat(stats): estimate whole-session cost at DeepSeek V4 list prices
Stats board gains an 预估费用/Est. Cost cell pricing the complete
session's billed tokens at hardcoded DeepSeek V4 list prices: the host
folds per-request usage totals (peak/off-peak split, cache hit/miss
input, output incl. reasoning) from the session log, flash and pro on
any provider, displayed in the locale's currency. The cell carries a
"?" marker whose hover bubble explains the whole-session estimate and
lists the per-1M-token price table straight from the pricing table, so
the printed rates can never drift from the math.
2026-08-21 17:01:22 +08:00
4d9ffcd3ca fix(host): keep timeline state plain JSON for the projection cache
The contextTimeline fold persisted `undefined`-valued properties (model/provider/lastModel/contextWindow at init, pendingShadowedSeqs set to undefined on consume, and turn/step written as undefined when absent from assistant/message data). The harness projection cache snapshots the whole session checkpoint through the lossless-JSON snapshotter, which rejects any `undefined` property — so every projection-cache write failed and the `title` projection (session list) degraded after restart. stateVersion 3→4 discards stale rows; a regression test now drives the real snapshotJsonValue.

Fixes #5, #7.

Co-authored-by: kiligzzz <kiligzzz@users.noreply.github.com>
Co-authored-by: Bowen Liang <liang.bowen.123@qq.com>
2026-08-20 22:39:42 +08:00
Bowen Liang 81415caad4 browser: raw/markdown view switch on detail cards
The context browser's detail cards (system prompt, tool description,
user/assistant message and injection bodies) gain a segmented Raw |
Markdown switch at the card's top-right corner — the same pill chrome
as the trend chart's Step/Turn control. Markdown is the default view,
rendered through the harness's shared MarkdownText (GFM, sanitized,
resolved from the platform module table — zero plugin-side markdown
dependency); Raw restores the exact source <pre>. Each card owns its
mode state via the new shared richText kit (explicit render props, no
React context), so two open details switch independently. Reasoning
blocks, tool-call args and tool results stay raw.

Markdown chrome is scoped to the detail cards: base size 13px (a
proportional face matching the raw view's 12px monospace in visual
weight), headings collapse to a weight-only bump, inline code keeps
full size and is pinned under the paragraph line so wrapped chips no
longer stick across lines.
2026-08-20 21:55:27 +08:00
bowenliang123 dab80c6232 build: switch to tsup and declare harness runtime deps as peers
Replace the hand-rolled esbuild script (scripts/build.mjs) with a single
tsup config that already existed but never ran: tsup re-bundles the config
into a temp .mjs, so import.meta.url pointed at the temp dir and the
package.json read failed, silently leaving no entries. Read the manifest
from process.cwd() instead. The host half now keeps @deepseek-ai/dsh-session
external (resolving to the harness's single runtime instance) instead of
inlining it; the client half keeps the __ModuleLoader__.load wrap.

Align deps with the official dsh convention: runtime harness packages
(cordis, dsh-session, client-ui-primitives, react) move to peerDependencies
with dev mirrors (client-only ones optional); zod stays a dependency;
exact rc pins become caret ranges (^0.1.0-rc.8) so rc9/0.2 releases don't
break installs. Drop the esbuild devDependency (tsup brings its own) and
add tsup itself.
2026-08-20 16:09:16 +08:00
bowenliang123 c347dd5b60 trend: keep the locked bar's solid frame on click
The selected (locked) bar was declared before the hovered rule at equal
specificity, so the dashed hover frame kept overriding the solid lock
frame while the pointer was still on the just-clicked bar — the lock only
became visible after the pointer left. Declare .lc-bar-selected last and
use the label-primary (near-black) frame so clicking immediately shows
the black solid frame and clicking again reverts to the dashed one.
2026-08-20 10:58:18 +08:00