Minimal Electron shell over the DSH JSON-RPC runtime — a first-look at what a ChatGPT.app-style host on top of the DeepSeek Harness looks like, with the harness's normally-invisible internals (trace timeline, context surface, subagent tree, compaction, plugin registry, rubrics) brought forward as first-class UI surfaces so plugin authors and researchers can see what the agent is actually doing. Runs against three keyless-to-live profiles (stdio-echo works on master out of the box; daemon-echo / daemon-vibe-echo activate once the daemon-demo lands; stdio-deepseek and daemon-vibe hit the real DeepSeek API when you supply a key). HARNESS_DEV auto-resolves to the in-repo runtime when this shell ships under examples/desktop/, so a fresh clone launches without config; env DSH_DEV_ROOT overrides for custom layouts, and a sibling deepseek-harness-dev/ checkout is the original dev workflow. Cold-clone gate (P0 fixes for first-time-clone usability): - HARNESS_DEV: 3-candidate resolver (env → walk-up in-repo marker → sibling), unit-tested via mock fs so ordering is locked without needing either real layout on disk. - config yml leaves rewritten at assemble time so the sibling-clone paths (../../deepseek-harness-dev/examples/echo-agent/…) become the in-repo paths (../../echo-agent/…) in the released tree — source yml stays usable for local dev, released tree ships a working shape. - pnpm-workspace.yaml allowBuilds.electron = true (was placeholder). - missing-key card in stdio-deepseek offers a one-click switch to stdio-echo (the keyless profile that works on master) rather than daemon-echo (blocked on the not-yet-shipped daemon-demo). - assemble-oss-release.sh rewrites the source-side breadcrumb name 'dsh-desktop-demo' → 'dsh-desktop' for the released package.json. FOUC guard on the onboarding gate (41fc5df carried) keeps the first-launch splash from flashing before the runtime probe finishes. Test suite (1634 tests in source, 3990 in the runtime repo) covers resolver ordering, renderer classifiers, trace timeline shape, compaction diff rendering, rubric parity, and the missing-key onboarding paths.
223 lines
13 KiB
Markdown
223 lines
13 KiB
Markdown
# Expand-affordance audit (fix/expand-affordance, 2026-07-18)
|
||
|
||
User report (针对对话流里 `trace · ↑20 ↓58` 折叠行, 2026-07-18):
|
||
> "没有展开时候,看上去让人不是很知道它点击是可以展开的……哪怕加一个那种折叠小箭头……你这个东西看上去只是一行小字,人们根本不知道点它还可以展开(Tree / Timeline / Graph)三个。它展开对研究员蛮有信息增量的。包括其他点击可以展开的,看看是不是也都有这个问题。"
|
||
|
||
## Design language (density-spec §4/§7 + existing precedent)
|
||
|
||
Two indicator positions in the app today — we lock these two, no third:
|
||
|
||
- **Row-head left** (▸ collapsed / ∨ expanded): Fields tree, CoT, tool-block,
|
||
card-diff hunk, trace card, trace-event-row, trace-header rows, trace-usage,
|
||
compact-card `.shadowed-expander`, edit-rerun-header. **This is the default**.
|
||
- **Row-tail right** (∨ subtree fold decoration): trace-tree parent row (task #38).
|
||
Reserved for tree rows where the fold applies to a subtree, not the row.
|
||
|
||
Glyph: `▸` (U+25B8) collapsed, `∨` (U+2228, keyboard-typeable) or the same `▸`
|
||
with `transform: rotate(90deg)` when `[open]`. Existing precedent uses
|
||
rotate-90 exclusively — we match. **Color**: `var(--muted)` — never accent,
|
||
never status-tinted. **No emoji** anywhere.
|
||
|
||
Every expandable row also gets `:hover` background highlight (second cue),
|
||
`aria-expanded` reflecting state (a11y + plugin-author示范), and
|
||
`title="Click to expand …"` tooltip on the P0 surface (trace drawer).
|
||
|
||
## Full inventory (28 `<details>` sites + native `<details>` fallbacks)
|
||
|
||
Judgement legend: **达标** = has visible ▸/∨ or rotating chevron in collapsed
|
||
state; **缺失** = no visual indicator in collapsed state.
|
||
|
||
| # | Surface (CSS selector / file:line) | Collapsed-state indicator | Judgement |
|
||
|---|---|---|---|
|
||
| 1 | `.turn-trace-drawer > .turn-trace-drawer-summary` (style.css:6949, assistant-turn.js:390, renderer.js:1451) — **user-called-out P0** | none — muted text only | **缺失** |
|
||
| 2 | `.context-card summary` (style.css:244) | `⌄` down-arrow, rotate on `[open]` | 达标 |
|
||
| 3 | `.tool-block summary` (style.css:360) | CSS border-triangle chevron, rotate on `[open]` | 达标 |
|
||
| 4 | `.card-diff-hunk-summary` (style.css:1452) | `▸`, rotate on `[open]` | 达标 |
|
||
| 5 | `.tool-json-section > summary` (style.css:1524) | `▸`, rotate on `[open]` | 达标 |
|
||
| 6 | `details.prompt-blocked-row > summary.pb-row-head` (style.css:1619) | none — pb-row-icon (error ✗) + label only | **缺失** |
|
||
| 7 | `.devtools-row-summary` (style.css:1804) | none — glyph col carries type only | **缺失** |
|
||
| 8 | `.recall-card summary` (style.css:2267) | `⌕` magnifier glyph (semantic, not fold) — but no rotation, marks recall action not "expandable" | **缺失** (semantic mismatch) |
|
||
| 9 | `.compact-card summary` (style.css:2316) | dashed `----divider----` treats the row as a divider; user model = compact card is a break, not a chip. body always open via `.shadowed-expander` inner. | 达标 (divider affordance is a distinct pattern; inner expander has its own ▸ — see #10) |
|
||
| 10 | `.compact-card .shadowed-expander-summary` (style.css:2383) | `▸ `, rotate on `[open]` | 达标 |
|
||
| 11 | `.trace-card summary` (style.css:6022) | `▸`, rotate on `[open]` | 达标 |
|
||
| 12 | `.trace-event-row > summary` (style.css:6088) | `▸`, rotate on `[open]` | 达标 |
|
||
| 13 | `.trace-header-{system,tools,prefix} > summary` (style.css:6160) | `▸`, rotate on `[open]` | 达标 |
|
||
| 14 | `.trace-header-tool > summary` (style.css:6191) | `▸`, rotate on `[open]` | 达标 |
|
||
| 15 | `.trace-usage-table > summary` (style.css:6225) | `▸`, rotate on `[open]` | 达标 |
|
||
| 16 | `.inject-card summary` (style.css:6290) | family icon (paperclip/etc.), muted — no fold cue and no rotation | **缺失** |
|
||
| 17 | `.subagent-trace > .subagent-trace-summary` (style.css:6767) | `.subagent-trace-glyph` (kind letter, no rotation) | **缺失** |
|
||
| 18 | `.raw-inject-card > .raw-inject-summary` (style.css:6844) | `.raw-inject-icon` (kind letter) + accent badge chip | **缺失** |
|
||
| 19 | `.raw-inject-l2 > summary` (style.css:6913) | none — label only | **缺失** |
|
||
| 20 | `.runtime-row-head` (style.css:7096) | status dot only | **缺失** |
|
||
| 21 | `.context-page-row-summary` (style.css:9127) | none — turn# + counters only (row is per-turn context history) | **缺失** |
|
||
| 22 | `.trace-detail-row-fields-summary` (style.css:9656) | none — bracket glyph + label | **缺失** |
|
||
| 23 | `.trace-detail-section > summary` (style.css:9797) | none — label + controls only | **缺失** |
|
||
| 24 | `.trace-detail-attr-group > summary` (style.css:9854) | none — label only | **缺失** |
|
||
| 25 | `.trace-detail-field-block > summary` (style.css:9887) | none — key + copy button | **缺失** |
|
||
| 26 | `.edit-rerun-header-summary` (style.css:10501) | CSS border-triangle chevron, rotate on `[open]` | 达标 |
|
||
|
||
Plus custom (non-`<details>`) toggle patterns scanned via
|
||
`grep classList.toggle('collapsed'\|.hidden` — the only click-to-fold custom
|
||
sites are:
|
||
|
||
- **panels-c-controller.js:260** — Tasks drawer with explicit `Show`/`Hide` text button. Discoverable text label; treat as 达标.
|
||
- **trace-detail-pane.js:1743** — `dimRow.classList.toggle('hidden', !isTurn)` is a visibility gate driven by row type (turn vs step), not a user-clickable fold. N/A.
|
||
|
||
## Fold-count summary
|
||
|
||
- Total expandable surfaces: **26** `<details>` sites + 1 explicit-text button.
|
||
- 达标 (visible indicator): **12** (context-card, tool-block, card-diff, tool-json, `.shadowed-expander`, all trace-card/trace-event/trace-header/trace-usage, compact-card divider, edit-rerun, panels-c Show/Hide).
|
||
- **缺失** (no visible fold cue): **14** — items 1, 6, 7, 8, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25.
|
||
- Post-fix target: 14 → 0. All缺失 sites gain a row-head-left `▸`/`∨` marker via
|
||
a single reusable CSS class + per-selector `::-webkit-details-marker` reset
|
||
where the site already has one, or add both marker-hide + ::before.
|
||
|
||
## Fix plan
|
||
|
||
Two new reusable CSS classes appended to the tail of `style.css`:
|
||
|
||
```css
|
||
/* Universal fold-affordance decoration for <details> summaries that
|
||
* don't have a semantic glyph carrying the "click to expand" hint.
|
||
* Prepend on any summary that lacks a ▸/∨. Pairs with
|
||
* `.aff-summary::-webkit-details-marker { display: none }` on the
|
||
* summary itself. Keep in sync with the trace-card ▸ language. */
|
||
.aff-summary { list-style: none; }
|
||
.aff-summary::-webkit-details-marker { display: none; }
|
||
.aff-summary::before {
|
||
content: '\25B8'; /* ▸ */
|
||
color: var(--muted);
|
||
font-size: 10px;
|
||
width: 1em;
|
||
flex: 0 0 auto;
|
||
display: inline-block;
|
||
transition: transform 120ms ease;
|
||
}
|
||
details[open] > .aff-summary::before { transform: rotate(90deg); }
|
||
.aff-summary:hover { background: var(--surface-hover); }
|
||
```
|
||
|
||
Per site, we add the `aff-summary` class to the JS builder (or extend the
|
||
existing selector directly in CSS when the class is stable and heavily-tested).
|
||
For sites like `.raw-inject-summary`, `.inject-card summary`, etc. where a
|
||
semantic icon already sits at the head, the `▸` slots in *before* that icon —
|
||
so the reader reads: **▸ [family-icon] [label]** collapsed, **∨ [family-icon]
|
||
[label]** expanded.
|
||
|
||
**Two-position grammar exception** (per §4 of density-layering-spec.md's
|
||
"row-head-left OR row-tail only, max 2 position grammars app-wide"): where
|
||
the head is already occupied by a **semantic status glyph** we can't demote —
|
||
specifically `.recall-card` (⌕ semantic glyph) and `.subagent-trace`
|
||
(✓/✗/▸-running status glyph) — the fold chevron goes at the row **tail** via
|
||
`::after` with `margin-left: auto`, so it doesn't clobber the semantic head
|
||
glyph. This mirrors task #38's trace-tree parent-row right-side `∨`
|
||
precedent. All other 缺失 surfaces use head-left `::before`.
|
||
|
||
The `.turn-trace-drawer-summary` also gets a `title="Click to expand Tree /
|
||
Timeline / Graph views"` attribute (task user-facing tooltip).
|
||
|
||
`aria-expanded` (mirrors `.open` state via a MutationObserver on
|
||
`toggle` event) is set on every fixed summary so plugin authors have
|
||
a working accessibility reference.
|
||
|
||
## Test locks
|
||
|
||
- Extend `test/renderer-first-turn-drawer.test.js` with an assertion that
|
||
the trace drawer summary has `aria-expanded=false` collapsed, `true` after
|
||
`drawer.open = true`.
|
||
- New `test/expand-affordance.test.js`: for every fixed summary selector,
|
||
assert `getComputedStyle(el, '::before').content` is `"▸"` in collapsed
|
||
state and its ancestor has `aria-expanded=false`; after `.open = true`
|
||
parent has `aria-expanded=true`. Wire the CSS class detection instead of
|
||
::before (jsdom doesn't render pseudo-elements) by asserting the class
|
||
presence and `open` reflection.
|
||
|
||
## Verification
|
||
|
||
- Isolated Electron: user-data-dir `~/.dsh-demo-affordance/`, remote-debug
|
||
port `9269`; kill on exit; screenshots to `docs/qa-affordance/` per site
|
||
(collapsed + expanded pair).
|
||
|
||
### Verification results (2026-07-18)
|
||
|
||
Ran `scripts/qa-cdp-shoot-affordance.mjs`; mounted 5 representative
|
||
fixtures (trace-drawer, runtime-row, inject-card, subagent-trace,
|
||
recall-card) inside the real renderer's stream container and shot
|
||
collapsed/expanded pairs plus a hover shot for the P0 trace drawer.
|
||
|
||
Assertions verified live via CDP `getComputedStyle` and DOM inspection
|
||
(`docs/qa-affordance/aria-assertions.json`):
|
||
- Every collapsed summary has `aria-expanded="false"` AND a `::before` or
|
||
`::after` chevron marker (`content: '▸'`).
|
||
- Every expanded summary flips to `aria-expanded="true"` (the toggle event
|
||
wiring in `initDetailsAriaObserver` in `renderer.js`, backed by
|
||
`wireDetailsAria` in `details-aria.js`).
|
||
- subagent-trace row-tail placement confirmed (`hasAfter: true`,
|
||
`hasBefore: false`) — head keeps its status glyph.
|
||
- recall-card carries both glyphs (⌕ head + ▸ tail) as designed.
|
||
|
||
Screenshots:
|
||
- `docs/qa-affordance/01-collapsed-all.png` — all five fixtures collapsed
|
||
- `docs/qa-affordance/02-trace-drawer-hover.png` — P0 hover state
|
||
- `docs/qa-affordance/03-expanded-all.png` — all five fixtures expanded
|
||
- `docs/qa-affordance/04-trace-drawer-collapsed.png` — P0 collapsed close-up
|
||
- `docs/qa-affordance/05-trace-drawer-expanded.png` — P0 expanded close-up
|
||
|
||
Tests: `test/expand-affordance.test.js` 9/9 pass; the three static gate
|
||
tests (`emoji-ban-static`, `renderer-collisions`, `style-css-static`) 9/9
|
||
pass; full suite 1513 pass / 1 pre-existing `artifact-server.test.js`
|
||
`electron` module-resolution failure unrelated to this batch.
|
||
|
||
### Summary count
|
||
|
||
- 达标 (pre-existing markers, no change needed): **12** — lines 34, 36, 40,
|
||
42, 43, 44, 45, 46, 47, 51 (approval steer chip), 60 (edit-rerun), and
|
||
57 (Show/Hide text button, non-`<details>` explicit textual toggle).
|
||
- 缺失 (was missing a fold indicator before this batch): **14** — the
|
||
14 selectors listed in `AFFORDANCE_SELECTORS` in
|
||
`test/expand-affordance.test.js`.
|
||
- 已补 (fixed in this batch): **14** — 13 via `::before` (row-head-left)
|
||
+ 1 via `::after` on `.subagent-trace` (row-tail because head carries
|
||
a status glyph, per the grammar exception documented above; recall-card
|
||
also uses `::after` because ⌕ semantic glyph already sits at the head).
|
||
- Test locks: `test/expand-affordance.test.js` — 9 tests, all green.
|
||
|
||
### Postmortem: QA-probe overlay-write leak (fixed same day)
|
||
|
||
Symptom (reported by team-lead 2026-07-18): the user's real
|
||
`~/.dsh-desktop/user-overlay.cordis.yml` was rewritten with a
|
||
worktree-relative include path (`../harness/dsh-demo-worktrees/lane-affordance/config/daemon-echo.yml`), breaking their live stdio-deepseek profile.
|
||
|
||
Root cause: `scripts/qa-cdp-shoot-affordance.mjs` isolated
|
||
`--user-data-dir` (Chromium userdata) but **not** `DSH_DESKTOP_HOME` (our
|
||
shell's config root, read at `src/main/plugins.js:580`,
|
||
`src/main/main.js`, `src/main/growth-log.js`, `src/main/profiles.js`).
|
||
Falling back to `~/.dsh-desktop`, my Electron instance — booted with
|
||
`cwd=WORKTREE` — triggered the Plugins-tab / onboarding path that
|
||
rewrites the overlay, resolving the base include relative to
|
||
`process.cwd()`.
|
||
|
||
Fix (this commit): the shoot script now sets **both** isolation roots
|
||
under `$TMPDIR`, seeds a minimal overlay with an *absolute* include path,
|
||
marks `.onboarded` before the shell boots, and rebuilds both directories
|
||
fresh each run. Precedent copied from `scripts/interactive-sweep-v2.mjs:145`.
|
||
|
||
Verification of the fix (this run):
|
||
- BEFORE `~/.dsh-desktop/user-overlay.cordis.yml`
|
||
`b6c82b2dd9f9415d279bfadd93aeaf26a8a5cf9e8a67725088d575f8df2c9435`
|
||
- AFTER — identical hash. `stat` mtime unchanged (`Jul 18 08:04:13 2026`).
|
||
- All shell writes captured in `$TMPDIR/dsh-affordance-home/`
|
||
(`.onboarded`, `config.json`, `growth-log.jsonl`, `user-overlay.cordis.yml`).
|
||
|
||
Impact on prior screenshots (01–05 in `docs/qa-affordance/`): none
|
||
substantive. The fixtures are pure DOM mounted inside the renderer's
|
||
`#stream`; they don't read profile state, wire adapters, or hit any
|
||
runtime. Which host profile happened to load underneath is irrelevant
|
||
to what the shots prove (▸/∨ chevrons visible in `::before`/`::after`
|
||
pseudo-elements, aria-expanded flips on toggle). Reshot cleanly under the
|
||
fixed isolation to close the audit trail — hashes above prove non-interference.
|
||
|
||
General rule for anyone else writing an Electron QA probe: **isolate both
|
||
`--user-data-dir` and `DSH_DESKTOP_HOME`** to a tmp directory. Isolating
|
||
only one is a footgun that will silently rewrite the real user's config.
|