Skip to content

feat: improve the Markdown conversation export; shrink the component test module graph - #576

Merged
lodystage[bot] merged 4 commits into
mainfrom
feat/conversation-markdown-export
Sep 10, 2026
Merged

feat: improve the Markdown conversation export; shrink the component test module graph#576
lodystage[bot] merged 4 commits into
mainfrom
feat/conversation-markdown-export

Conversation

@lodystage

@lodystage lodystage Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Related issue

Same-repository branch; no intake Issue.

Problem / pressure

Two independent pressures, both surfaced while reviewing what Copy as Markdown
actually produces.

The export loses the reasoning it exists to carry. Copy as Markdown is the
fallback when native forking is unavailable — the user's goal is to move a
conversation's context to another agent, machine, or workspace. The
degradation ladder was ordered as if it were producing a transcript: thinking
was dropped one level before tool calls were touched, and the most aggressive
level spent up to 120 characters per tool call on bold, mid-word-truncated
command strings whose results were already gone. Message bodies routinely
contain #/## headings, which outranked the ## User / ## Assistant turn
headings and destroyed the outline of any long paste.

The component suite takes about ten minutes, and almost none of it is
assertions.
A --shard=1/4 run spends its time in Vitest's collect phase —
importing a test file and evaluating its transitive graph — which isolation
repeats for every one of 446 files. DEBUG=vite-node:* on one menu test showed
991 modules loaded, only 11 of them from node_modules.

Summary

Commit 1 — feat(shared): Markdown export format.

  • LevelConfig.includeThinking: boolean becomes thinkingCap: number. Thinking
    degrades through the same clampMiddle as tool results, so head and
    conclusion survive; it is never dropped whole.
  • Tool calls collapse one level earlier than thinking is capped, into a single
    <details> per turn with counts by toolName.
  • demoteMarkdownHeadings shifts ATX headings in message bodies down two levels,
    capped at h6, skipping fenced blocks.
  • Turn headings carry a round number, local MM-DD HH:mm, and for assistant
    turns the recorded model and effective working time; a rule opens each round.
  • A header blockquote carries the time range, repo/branch, participants, models,
    and the trim notice, which moved from the footer.
  • Budget rises from 50k/20k to 150k/60k characters/tokens.
  • Speaker names reach turn headings only when the conversation has more than one
    distinct userId; the caller supplies the userId → name map.

Commit 2 — perf(components): test module graph.

  • Delete src/ui/calendar.tsx and the react-day-picker dependency. The
    Calendar component had no callers anywhere; its re-export from
    src/ui/index.ts dragged date-fns, date-fns-jalali and @date-fns/tz
    into every @/ui consumer. Also removes 3.6 MB from the product bundle.
  • Eight modules now deep-import date-fns/locale/en-US / zh-CN instead of the
    date-fns/locale barrel, which natively loads all 96 locales.
  • The icon-asset new URL(\./files/${name}.svg`, import.meta.url)calls — which make Vite eagerly glob 324 SVGs into everyFileIconconsumer — move tofile-icons/asset-url.ts`, so the test config can alias only that module to a
    stub. Production behaviour is unchanged.
  • vitest.config.ts drops vite-plugin-top-level-await and replaces
    vite-tsconfig-paths with explicit aliases. vite.config.ts keeps both.

Visual explanation

Degradation ladder, before and after. The reordering is the change: prose-shaped
content is now everything above the dashed line, and nothing below it can be
lost before everything above it has degraded.

flowchart LR
  subgraph BEFORE["Before (6 levels)"]
    direction TB
    B0["0 · uncapped"] --> B1["1 · tool 4000 / term 2048"]
    B1 --> B2["2 · tool 1000 / term 512"]
    B2 --> B3["3 · terminal dropped, tool 300"]
    B3 --> B4["4 · THINKING DROPPED"]
    B4 --> B5["5 · one bold line per tool call"]
  end
  subgraph AFTER["After (7 levels)"]
    direction TB
    A0["0 · uncapped"] --> A1["1 · tool 4000 / term 2048"]
    A1 --> A2["2 · tool 1000 / term 512"]
    A2 --> A3["3 · terminal dropped, tool 300"]
    A3 --> A4["4 · tool calls → one counted summary"]
    A4 --> A5["5 · thinking capped 2000"]
    A5 --> A6["6 · thinking capped 500"]
  end
  B4:::lost
  A5:::kept
  A6:::kept
  classDef lost fill:#fdd,stroke:#c00
  classDef kept fill:#dfd,stroke:#0a0
Loading

Rendered output, same conversation:

# 分叉功能的 Markdown fallback 设计

> Lody session · 2026-09-08 14:32 → 2026-09-10 11:07 · 14 messages
>
> LodyAI/Lody · feat/markdown-2
>
> Participants: Zixuan, Leon
>
> Models: Opus 5
>
> **Trimmed to fit the copy budget:** tool calls collapsed to per-turn
> summaries (results and terminal output omitted).

---

## 1 · User · Zixuan · 09-08 14:32

现在做分叉时限制比较大…

## 1 · Assistant · 09-08 14:33 · Opus 5 · 4m12s

<details>
<summary>Thinking (from the original session)</summary>
…
</details>

<details>
<summary>32 tool calls (details omitted)</summary>

`Bash` ×11 · `Read` ×8 · `Grep` ×6 · `Edit` ×4 · `Write` ×3

</details>

### 结论          ← body was `# 结论`, demoted below the turn heading

```bash
# this stays a comment; fenced blocks are skipped
rg fork

2 · User · Leon · 09-08 15:10


## Before / after

| | Before | After |
| --- | --- | --- |
| Thinking at the floor | dropped entirely | capped at 500 chars, block still present |
| Tool calls at the floor | one bold ≤120-char line each | one `<details>` per turn with counts |
| Body `# heading` | outranks `## Assistant`, outline broken | demoted to `###` |
| Turn heading | `## User` | `## 3 · User · 09-08 14:32` |
| Trim notice | last line of the document | header blockquote |
| Budget | 50k chars / 20k tokens | 150k chars / 60k tokens |
| Modules loaded by `session-header-menu.test.tsx` | 991 | 667 |
| `date-fns` modules in that test | full 96-locale barrel | 3 |
| `react-day-picker` | loaded | 0 |

## Merge with `main`

`main` landed [#558](https://github.com/LodyAI/Lody/pull/558) (Markdown context
copy in fork menus) while this branch was open, touching the same files. Merged
at `938f5f3` with three conflicts resolved by keeping both behaviours:

- `handleCopyConversationHistory` now takes `#558`'s `throughMessageId` range and
  its "still generating" suffix **and** this branch's `source` / `participants`
  header inputs and the `thinkingTruncated` stat rename.
- `renderItem`'s new `file` / `image` / `comment_reference` /
  `visual_annotation_reference` / `image_group` cases from #558 auto-merged and
  sit alongside `demoteMarkdownHeadings` on `text` / `proposed_plan` / `thought`.
- The `sessions` doc and `AGENTS.md` rule keep both invariants in one sentence.

Post-merge: `tests/conversation-markdown.test.ts` is 28 tests (24 here + 4 from
#558), and `conversation-copy-range`, `session-fork-destination-menu`,
`session-header-menu` and `file-tree-virtual-rows` pass — 22 tests. `typecheck`,
`lint` (0 errors) and `docs check` (0 errors) re-run clean.

One follow-up deliberately not taken: #558 appends its "still generating" note
after the document, while this branch moved the trim notice into the header for
the same reason. Changing that placement during a conflict resolution would be
an unreviewed behaviour change, so it was left as #558 wrote it.

## Test plan

Run:

- `pnpm --filter @lody/shared run test` — 96 files, 1098 tests, pass.
  `tests/conversation-markdown.test.ts` grows to 24 assertions covering heading
  demotion (including a fenced `# comment` left intact and h6 clamping), collapse
  reached with thinking untouched, thinking capped with the block still present,
  round numbering with exactly one rule per round, `4m12s` derived from
  `endedAt - timestamp - permissionWaitMs`, speaker names gated on two `userId`s,
  and the trim notice preceding the first turn.
- `pnpm --filter @lody/components exec vitest run --shard=N/4` for N in 1..4, with
  `NODE_ENV=test`. Shards 1 and 3 pass clean (112/112 and 112/112). Shards 2 and
  4 each reported a handful of `Test timed out in 5000ms` failures whose set was
  not stable between runs; every affected file
  (`combined-mention-textarea-activation`, `use-sticky-scroll`,
  `agent-config-dialog`) passes in isolation. Treated as host-load flake, not a
  regression — none of them touch the changed modules.
- `pnpm run typecheck` (all packages), `pnpm run lint` (0 errors),
  `pnpm run lint:i18n`, `pnpm run docs check` (0 errors),
  `check:platform-boundaries`, `check:public-boundary`,
  `check:code-collab-imports` — all pass.
- `pnpm format` run; one unrelated file it reformatted
  (`app-updater-sparkle-policy.test.mjs`) was reverted.

Skipped / not established:

- **`pnpm check` never completed as a single command.** The full component suite
  was terminated by the environment three times; it was verified by shard instead.
- **The performance commit's wall-clock benefit is unverified.** Run-to-run
  variance on this host exceeded the effect: the same baseline config measured
  97 s cold and 128 s warm on an identical 112-file shard. Only the module counts
  in the table above are trustworthy. The commit message and the Agent Note both
  say so explicitly.
- No real session was exported and pasted into another agent; the export was
  verified against fixtures.
- The suite requires `NODE_ENV=test`. Under `NODE_ENV=production` React resolves
  to a build without `act` and 154 files fail for that reason alone — pre-existing,
  unrelated to this branch.

## Context handoff

<!-- context-handoff:begin -->

### Instructions for reviewing agents

- **Review focus:** `packages/shared/src/conversation-markdown.ts` — the
  `thinkingCap` ladder reordering and `demoteMarkdownHeadings`, which is the only
  code permitted to edit message prose. Also `vitest.config.ts`, where dropping
  `vite-tsconfig-paths` changes module resolution for the whole suite.
- **Decisions to challenge:** capping thinking rather than dropping it at the
  floor (it raises the floor and so the `overBudget` rate); the 2000/500 caps and
  the 150k/60k budget, all chosen against fixtures rather than session data; and
  stubbing `file-icons/asset-url` in tests, which is a test-only divergence from
  production behaviour.
- **Plausible failures / evidence gaps:** heading demotion could mis-detect a
  fence and rewrite a `#` inside a code block; the two shards with timeout flake
  were not proven green on a quiet host; the performance commit's wall-clock
  claim is deliberately not made.

### Authoring context

- **User goal / directives:** improve how Copy as Markdown presents a
  conversation (usernames, timestamps, turn correspondence), then — after the
  suite's slowness surfaced during verification — reduce component test time,
  with a stated target of 20 s.
- **Constraints / non-goals:** message text and pasted material must travel
  verbatim; the user explicitly rejected redacting secrets in message bodies
  because forking is meant to carry that context. `buildReplayPromptFromHistory`
  is a separate surface and is untouched. The 20 s target is not met and is not
  claimed to be.
- **Risk-bearing decisions:** raising the copy budget 3× changes what lands in a
  receiving model's context window; removing `react-day-picker` deletes a
  component that could be wanted later; the test-only alias means one module
  behaves differently under test than in production.
- **Destructive or irreversible behavior:** `src/ui/calendar.tsx` and the
  `react-day-picker` dependency are deleted, with `pnpm-lock.yaml` updated. Both
  are recoverable from history. No data, migration, or runtime state is touched.
- **Deliberately not done or tested:** `isolate: false`, the only change that
  reaches a 20 s suite, was measured and left out — it succeeds to six files and
  fails at seven regardless of which file is seventh, with tinypool reporting
  `Unhandled Rejection: Terminating worker thread`, and an 8 GB worker heap does
  not help. That is leaked pending async work, plus 89 of 446 files sharing a
  `vi.mock` registry once isolation is off; both are per-file cleanup projects.
  No Spec was updated because no Spec owns this surface today.
- **Unknowns / confidence:** high confidence in the export format changes, which
  are covered by deterministic tests. Low confidence in any timing claim for the
  performance commit, which is why none is made; the module-count reductions are
  exact and were taken from `DEBUG=vite-node:*`.

<!-- context-handoff:end -->

zxch3n and others added 2 commits September 10, 2026 06:06
Copy as Markdown is how a conversation reaches another agent when native
forking is unavailable, so prose-shaped content has to outrank everything
produced around it. Thinking was dropped one level before tool calls were
touched, and the floor spent up to 120 characters per call on bold,
mid-word-truncated command strings whose results were already gone.

- Thinking degrades by capping (`thinkingCap`) instead of being dropped, so
  the receiving conversation always inherits some of the reasoning.
- Tool calls collapse one level earlier, into one counted per-turn summary.
- Prose headings shift below the turn heading outside fenced blocks; agent
  bodies routinely emit `#` headings that destroyed the outline.
- Turns carry a round number, local time, and the recorded model and working
  time; a rule opens each round; the trim notice moved to the header.
- The budget rises to 150k characters / 60k tokens.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vitest isolates each of the suite's 446 files and re-evaluates every file's
whole import graph, so accidental graph weight is paid per file. Tracing
`DEBUG=vite-node:*` showed one menu test pulling 991 modules, only 11 of them
from `node_modules`.

- Delete the unused `Calendar` re-export and `react-day-picker`, which dragged
  `date-fns`, `date-fns-jalali` and `@date-fns/tz` into every `@/ui` consumer.
- Deep-import `date-fns/locale/<tag>` instead of the 96-locale barrel.
- Move the icon-asset `new URL` glob, which eagerly pulls 324 SVGs, into its
  own module so the test config can alias just that one to a stub.
- Drop `vite-plugin-top-level-await` and `vite-tsconfig-paths` from the test
  config only; `vite.config.ts` keeps both for the product bundle.

That test now loads 667 modules. Wall-clock improvement is NOT established:
host variance exceeded the effect, and a 20s suite needs `isolate: false`,
which currently fails past six files on leaked pending async work.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zxch3n and others added 2 commits September 10, 2026 06:14
…kdown-export

# Conflicts:
#	.agents/docs/sessions-surface.md
#	packages/components/src/components/sessions/AGENTS.md
#	packages/components/src/components/sessions/session-chat-interface.tsx
The fork-menu copy from #558 appended "the last response was still generating"
after the transcript, while the trim notice had moved to the header block. Two
warnings about the same thing — whether this copy is complete — in two places,
one of them after the content it qualifies, defeats the reason the notice moved:
the reader is often another agent, which needs to know before it reads.

The warning is now a caller-localized `incompleteFinalResponse` option rendered
in the header blockquote beside the trim notice, so both fork-menu and session-
header copies produce one consistent document.

Model: claude-opus-5[1m]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lodystage
lodystage Bot merged commit 71f89d6 into main Sep 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant