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
49 changes: 44 additions & 5 deletions patterns/session-explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,42 @@ If you cannot fetch the template, build the same structure from the requirements

## Required structure

All five are mandatory. A page missing any of them is not this pattern.
All six are mandatory. A page missing any of them is not this pattern.

1. **A sticky left rail** (`.rail`) holding the session's numbers and context. It stays on screen while the content column scrolls, so the reader keeps the session's identity while reading any one part.
2. **Two tabs** — `Highlights` (`#p1`, default) and `Dead ends` (`#p2`). Driven by `:target` and `:has()`, never JavaScript.
2. **Three tabs** — `Highlights` (`#p1`, default), `Dead ends` (`#p2`), `Trace` (`#p3`). Driven by `:target` and `:has()`, never JavaScript.
3. **A compressed timeline** in Highlights. One line per step. This is the index to the whole session.
4. **One card per dead end** in the second tab, `id="dead-N"`.
5. **Deep links from timeline to cards.** A timeline row that has a card is an `<a href="#dead-N">` whose gutter reads `dead-N →`. Clicking it switches tab, scrolls to the card, and rings it. Rows without a card stay a `<div>` and get a plain gutter label.
6. **A trace panel** in the third tab: every turn of the session in order, both sides. See below.

## The trace panel

Highlights is an editorial gloss and the cards are narrative. Neither shows what actually happened in sequence, which is the thing a reader most often wants and the thing hardest to fake. The trace panel is that sequence.

**Ship the input whole and compress the output.** This is the asymmetry that makes it fit, and it comes out of measurement rather than taste. Count both sides of a real session before deciding what to cut: the human's side is usually a rounding error. In the session that produced this pattern, 38 prompts came to about 1,600 words against a 12.2 MB transcript — roughly 0.08% of the bytes. So every prompt ships complete, and the agent's side is what gets budgeted.

Per turn, show:

- **Turn number and timestamp**, with a stable `id="tN"` anchor so a reader can link to one turn
- **The human's prompt**, complete. Collapse it behind a `<details>` above ~40 words, never truncate it away
- **The agent's reply**, collapsed by default, with its first line as the preview and a word count on the toggle so the reader knows what they are opening
- **What the turn did** — tool names with counts, files written, thinking-block count
- A link to the dead-end card if that turn earned one

**Tool results do not belong here.** They are the bulk of the megabytes and almost none of the meaning. Naming the tool and the count carries the information; pasting the output carries the weight.

Mark day boundaries when a session spans more than one, and say plainly when a turn produced no reply.

## Editing policy

"Verbatim" is the wrong promise. Prompts carry typos, and sessions occasionally touch things that should not be published. Three different problems, three different answers, and the page has to state which it applied:

- **Typos and dropped words: fix them silently.** No information lives in a misspelling.
- **Rambling, hedging, and changes of mind: keep them.** That is the thinking trail, and it is exactly what makes a trace worth more than a summary. A prompt that wandered should still read like it wandered. Smoothing it turns the trace back into the Highlights tab.
- **Sensitive or private spans: cut them and mark the cut.** Never silently reword. An unmarked edit makes every other line on the page unverifiable, which costs more credibility than the cut saves.

Keep the edits auditable rather than ambient: a per-turn marker on the turns you touched, and a one-line statement of the rule at the top of the panel. Do not describe the panel as verbatim if you edited it.

## Numbers must be measured

Expand All @@ -56,6 +85,8 @@ Required in the context block: `agent`, `model`. Add `window`, `repo`, `outcome`
| Insights | 2–4 items, **150 words total** |
| Timeline rows | 8–16, one line each |
| Dead-end cards | 2–6 |
| Trace turns | every one, no cap |
| Agent reply per turn | collapsed by default |
| Lede | 2 sentences |
| `<script>` tags | **0** |
| Page weight | under 500 KB |
Expand All @@ -76,11 +107,16 @@ Do not sanitize the author's voice, including the parts where they were wrong.

## Redaction floor

Session transcripts are the most credential-dense artifact on a developer's machine. Live API keys have been found in published session logs more than once. Every excerpt has to clear this list:
Session transcripts are the most credential-dense artifact on a developer's machine. Live API keys have been found in published session logs more than once.

**Scan the agent's replies, not just the human's prompts.** This is the part that gets missed. A prompt is short and the human wrote it, so it gets read carefully. The agent's side is fifteen times longer and nobody re-reads it — and it is where the employer name, the job title, the customer's real name, and the internal URL actually turn up, because the agent restated them while being helpful. Building this panel leaked all four on the first render, from replies rather than prompts.

Every excerpt, on either side, has to clear this list:

- **Scan for secrets.** API keys, bearer tokens, `.env` contents, database URLs, connection strings, signed URLs, private hostnames. High-entropy strings are guilty until proven innocent.
- **Strip absolute home paths.** `/Users/<name>/…` and `/home/<name>/…` leak identity and local layout. Rewrite as repo-relative.
- **Never publish an excerpt the human hasn't seen.** Sessions routinely contain customer names, unshipped work, internal URLs, and third-party code.
- **Never publish an excerpt the human hasn't seen.** Sessions routinely contain customer names, unshipped work, internal URLs, and third-party code. For a trace panel this means the whole panel, both sides. It is a short read: the human's side of a long session is usually under 2,000 words.
- **Third parties don't get published by default.** A named customer, colleague, or company that came up mid-session has not agreed to appear on a public page. Cut and mark, and ask before restoring. The same goes for the human's own employer and job title unless they put them there.
- **Default to a passcode for anything team-internal** (`POST /api/drops/:slug/passcode`). Be honest that it's a share gate, not encryption — the HTML is stored unencrypted.
- **If the session touched credentials at all, say so** and tell the human to rotate them, whether or not the value reached the drop.

Expand All @@ -97,7 +133,10 @@ Before publishing, verify each of these against the rendered page. Every one is
**One tab is always hidden, and a hidden panel hides its own defects.** Elements inside `display: none` report zero width and height, so an overflow or a small tap target in the inactive tab measures as passing. Run the layout checks twice, once per tab, or skip zero-size elements explicitly. A check that silently measures nothing is worse than no check.

- `.rail` exists and is `position: sticky` above 760px
- Both `#p1` and `#p2` exist; `#p1` shows by default
- `#p1`, `#p2` and `#p3` all exist; `#p1` shows by default
- Exactly one tab reads as active in every state, including `#p3` and a `.card` deep link. A `:not(:has(#id))` default rule inherits the id's specificity, so adding a third tab without adding it to that rule leaves two tabs highlighted
- The trace panel has one row per turn, each with a stable `id="tN"`
- Agent replies are `<details>`, closed on load
- `document.querySelectorAll('script').length === 0`
- Every `a.row[href^="#dead-"]` resolves to a card with that `id`
- Every `.card[id]` is reachable from a timeline row
Expand Down
97 changes: 95 additions & 2 deletions patterns/session-explainer.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,26 @@
.panels:has(.card:target) #p2{display:block}
.panels:has(#p1:target) #p1{display:block}
.panels:has(#p1:target) #p2{display:none}
.tabs:not(:has(#p2:target)):not(:has(.card:target)) .tabnav a.h,
.panels:has(#p3:target) #p1{display:none}
.panels:has(#p3:target) #p2{display:none}
.panels:has(#p3:target) #p3{display:block}
.panels:has(#p2:target) #p3{display:none}
.panels:has(.card:target) #p3{display:none}
.tabs:not(:has(#p2:target)):not(:has(#p3:target)):not(:has(.card:target)) .tabnav a.h,
.tabs:has(#p1:target) .tabnav a.h,
.tabs:has(#p3:target) .tabnav a.r,
.tabs:has(#p2:target) .tabnav a.d,
.tabs:has(.card:target) .tabnav a.d{
color:var(--ink);border-bottom-color:var(--accent);font-weight:500}
.tabs:not(:has(#p2:target)):not(:has(.card:target)) .tabnav a.h .n,
.tabs:not(:has(#p2:target)):not(:has(#p3:target)):not(:has(.card:target)) .tabnav a.h .n,
.tabs:has(#p1:target) .tabnav a.h .n,
.tabs:has(#p3:target) .tabnav a.r .n,
.tabs:has(#p2:target) .tabnav a.d .n,
.tabs:has(.card:target) .tabnav a.d .n{background:var(--accent-bg);color:var(--accent)}
.tabs:has(#p1:target) .tabnav a.d,
.tabs:has(.card:target) .tabnav a.h,
.tabs:has(#p3:target) .tabnav a.h,
.tabs:has(#p3:target) .tabnav a.d,
.tabs:has(#p2:target) .tabnav a.h{
color:var(--ink-soft);border-bottom-color:transparent;font-weight:400}

Expand Down Expand Up @@ -202,6 +211,61 @@
td.num{font-family:var(--mono);white-space:nowrap}
tr.hot td{color:var(--accent);font-weight:500}

/* trace panel — both sides of every turn */
.tnote{font-size:13.5px;color:var(--ink-soft);border-left:2px solid var(--rule);
padding-left:13px;margin:0 0 18px;max-width:70ch}
.tnote b{color:var(--ink)}
.trace{border:1px solid var(--rule);border-radius:5px;overflow:hidden}
.tday{font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;
color:var(--ink-softer);background:var(--bg-2);padding:6px 13px;border-bottom:1px solid var(--rule)}
.turn{display:grid;grid-template-columns:52px minmax(0,1fr);gap:12px;padding:11px 13px;
border-bottom:1px solid var(--rule-soft)}
.turn:last-child{border-bottom:0}
.turn:hover{background:var(--bg-2)}
.turn:target{background:var(--accent-bg)}
.tn{font-family:var(--mono);font-size:11px;color:var(--ink-softer);line-height:1.5}
.tn a{display:block;color:var(--ink-2);border:0;font-weight:500}
.tn a:hover{color:var(--accent)}
.tn span{display:block;font-size:10px}
.tb{min-width:0}
.rl{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:.07em;
color:var(--ink-softer);border:1px solid var(--rule);border-radius:3px;padding:1px 4px;
margin-right:7px;vertical-align:1px;white-space:nowrap}
.rl.you{color:var(--ink-2);border-color:var(--ink-softer)}
.tp{margin:0;font-size:14.5px;color:var(--ink);white-space:pre-wrap;overflow-wrap:anywhere}
details.tp>summary{cursor:pointer;list-style:none;color:var(--ink);display:block;
padding-right:16px;position:relative}
details.tp>summary::-webkit-details-marker{display:none}
details.tp>summary::after{content:"+";position:absolute;right:0;top:0;color:var(--ink-softer);
font-family:var(--mono);font-size:12px}
details.tp[open]>summary::after{content:"\2212"}
details.tp[open]>summary{color:var(--ink-softer);font-size:13px;margin-bottom:7px}
details.tp>div{white-space:pre-wrap;font-size:14.5px;color:var(--ink)}
details.tr{margin-top:7px}
details.tr>summary{cursor:pointer;list-style:none;font-size:13.5px;color:var(--ink-soft);
display:block;padding-right:44px;position:relative;overflow:hidden;text-overflow:ellipsis;
white-space:nowrap}
details.tr>summary::-webkit-details-marker{display:none}
details.tr>summary i{position:absolute;right:0;top:0;font-style:normal;font-family:var(--mono);
font-size:10px;color:var(--ink-softer)}
details.tr>summary:hover{color:var(--ink)}
details.tr[open]>summary{white-space:normal;color:var(--ink-softer);margin-bottom:8px}
details.tr>div{white-space:pre-wrap;font-size:13.5px;line-height:1.6;color:var(--ink-2);
background:var(--bg-2);border:1px solid var(--rule);border-radius:4px;padding:11px 13px;
overflow-wrap:anywhere;max-height:420px;overflow-y:auto}
.tnoreply{margin:7px 0 0;font-size:13px;color:var(--ink-softer)}
.tm2{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin-top:7px}
.tc,.tf,.ted{font-family:var(--mono);font-size:10px;border-radius:3px;padding:1px 5px;white-space:nowrap}
.tc{background:var(--bg-3);color:var(--ink-soft)}
.tc i{font-style:normal;color:var(--ink-softer);margin-left:3px}
.tc.tnone,.tc.tth{background:transparent;color:var(--ink-softer);padding-left:0}
.tf{border:1px solid var(--rule);color:var(--ink-softer);max-width:190px;overflow:hidden;
text-overflow:ellipsis}
.ted{border:1px dashed var(--rule);color:var(--ink-softer)}
.tcut{border:1px solid var(--accent-stroke);background:var(--accent-bg);color:var(--accent);
font-family:var(--mono);font-size:10px;border-radius:3px;padding:1px 5px}
.tdeep{font-family:var(--mono);font-size:10px;color:var(--accent);border:0;margin-left:auto}

@media (max-width:900px){
:root{--rail:216px}
.main{padding:24px 22px 36px}
Expand All @@ -225,6 +289,10 @@
.row .t{grid-row:2;grid-column:1/-1}
.tabnav a{padding:9px 9px;font-size:11.5px;gap:5px}
.brow{grid-template-columns:88px minmax(0,1fr) 34px;font-size:11px;gap:8px}
.turn{grid-template-columns:1fr;gap:5px}
.tn{display:flex;gap:8px}
.tn a,.tn span{display:inline}
.tf{max-width:140px}
}
@media (max-width:420px){
.nums .v{font-size:16px}
Expand Down Expand Up @@ -293,6 +361,7 @@ <h1>SLOT_TITLE</h1>
<div class="tabnav">
<a class="h" href="#p1">Highlights <span class="n">SLOT_N</span></a>
<a class="d" href="#p2">Dead ends <span class="n">SLOT_N</span></a>
<a class="r" href="#p3">Trace <span class="n">SLOT_N</span></a>
</div>

<div class="panels">
Expand Down Expand Up @@ -340,6 +409,30 @@ <h2>What I'd do differently</h2>
<div class="row"><span class="ph">lesson</span><span class="t">SLOT_LESSON</span><span class="g">#dead-1</span></div>
</div>
</div>

<div class="panel" id="p3">
<!-- SLOT: one sentence stating the editing rule you applied, and what
is not here. Do not call it verbatim if you edited it. -->
<p class="tnote">Both sides of every turn, in order. <b>SLOT_EDIT_RULE</b>
Tool results are not here, and that is most of the transcript's weight.</p>

<div class="trace">
<!-- SLOT: repeat one .turn per turn, in order. Day separators when the
session spans days. The human's prompt is complete; collapse it
above ~40 words rather than truncating. The agent's reply is
collapsed with its first line as the preview and a word count.
Add <span class="tcut">redacted</span> on any turn you cut from. -->
<div class="tday">SLOT_DATE</div>
<div class="turn" id="t1">
<div class="tn"><a href="#t1">1</a><span>SLOT_TIME</span></div>
<div class="tb">
<p class="tp"><span class="rl you">you</span>SLOT_PROMPT</p>
<details class="tr"><summary><span class="rl">agent</span>SLOT_REPLY_FIRST_LINE<i>SLOT_Nw</i></summary><div>SLOT_REPLY</div></details>
<div class="tm2"><span class="tc">Bash<i>SLOT_N</i></span><span class="tf">SLOT_FILE</span><span class="tc tth">thinking<i>SLOT_N</i></span></div>
</div>
</div>
</div>
</div>
</div>
</div>

Expand Down