# pi-unshare — Design

This document explains why the extension is built the way it is. Every claim
about pi's behavior was verified against the installed source of
`@earendil-works/pi-coding-agent` 0.81.x (MIT). File references point into
that package's `dist/`.

## 1. What `/share` actually does

The whole implementation lives in `handleShareCommand`
(`dist/modes/interactive/interactive-mode.js`):

1. Runs `gh auth status`; aborts if the GitHub CLI is missing or logged out.
   **Auth is entirely the user's `gh` keyring** — pi holds no token of its own.
2. Exports the session to `os.tmpdir()/session.html`. The uploaded filename
   is therefore always `session.html`.
3. Runs `gh gist create --public=false <tmpfile>` — **no `--desc`**, so the
   gist description is empty, and **every invocation creates a new gist**:
   sharing the same session twice produces two independent gists.
4. Shows `${PI_SHARE_VIEWER_URL || "https://pi.dev/session/"}#<gistId>` once
   via a transient status message. **Nothing is persisted anywhere** — not in
   the session file, not in settings.

## 2. Why an extension cannot wrap `/share`

pi's docs say extension commands are checked before built-ins, which suggests
an extension could shadow `/share` and record the gist id at creation time.
It cannot. The TUI editor's submit handler hard-codes built-in commands with
early returns (`if (text === "/share") { …; return; }`) **before** the
pipeline where extension commands and the `input` event live. A `/share`
typed into the TUI never reaches extension code — neither takeover nor
passive observation is possible.

Consequence: share-time tracking is off the table. The only reliable source
of truth is GitHub itself, queried on demand.

## 3. The exact fingerprint

pi's HTML export embeds the full session as base64 JSON:

```html
<script id="session-data" type="application/json">eyJoZWFkZXIiOnsi…
```

Decoded, it begins with the session header:

```json
{"header":{"type":"session","version":3,"id":"<session-uuid>","timestamp":…}}
```

The session UUID sits within the first ~200 bytes of the base64 payload, and
the payload sits near the top of the file (after ~30 KB of CSS, before the
viewer code). Meanwhile the extension can read the current session's UUID via
`ctx.sessionManager.getSessionId()`.

So "is this gist a share of this session?" is answerable **exactly**:
decode a small prefix of the gist content and compare UUIDs. Filename,
secret-ness, and creation-time checks are used only to prune candidates
cheaply — correctness rests solely on the UUID match. This also means shares
made before the extension was installed, or from another machine, are found
just the same.

Two practical notes:

- Content larger than the gist API's ~1 MB inline limit is handled with a
  ranged fetch of the first 128 KB of `raw_url`, which always contains the
  data block under the current template.
- The embed format is the working basis of pi's own share viewer
  (pi.dev/session), which is what makes it a de-facto stable interface. An
  upstream change that additionally persists the gist id (or stamps the
  session UUID into the gist description) would let this extension drop the
  content fetch entirely; see the upstream notes in the README.

## 4. Design axioms

1. **GitHub is the database.** No local record is ever trusted over a live
   listing; local state is only a cache and a UX accelerator.
2. **Correctness rests on the UUID match alone.** Every other signal is an
   optimization.
3. **Revocation is best-effort and says so.** Deleting the gist kills the
   link instantly (the API 404s, the viewer dies), but copies already opened
   cannot be recalled. The confirmation dialog states this.

## 5. Architecture

```
┌─ L3  presentation ───────────────────────────────────────────┐
│  footer status lamp, transcript entry cards, /shares panel   │
├─ L2  memory (cache only — never a deletion basis) ───────────┤
│  session entries: "unshare:detected" / "unshare:revoked"     │
│  global cache: gistId → sessionId map                        │
├─ L1  discovery engine (the source of truth) ─────────────────┤
│  list  → gh api gists (paginated), prune by shape            │
│  verify→ fetch content prefix, decode, compare session UUID  │
│  delete→ gh api -X DELETE gists/<id>  (204 / 404 / 403       │
│          each mapped to a distinct user-facing outcome)      │
└──────────────────────────────────────────────────────────────┘
```

### `/unshare` flow

```
/unshare
  ├─ gh missing or logged out → same guidance /share gives
  ├─ 0 matches, no history    → "this session has never been shared"
  ├─ 0 matches, revoked prior → "shared once, already revoked at <time>"
  ├─ 1 match  (typical case)  → confirm card → delete → record → lamp off
  └─ N matches (same session shared N times)
        → list with timestamps, "revoke all" as the default action
```

Zero-question principle: the user is never asked to identify a gist the
extension can identify itself. Selection UIs appear only for genuine
ambiguity (multiple snapshots of the same session).

### Edge-case matrix

| Scenario | Behavior |
|---|---|
| Never shared | One-line notice, no side effects |
| Shared before the extension existed | Found by discovery (UUID is in the content, not in local state) |
| Same session shared N times | All listed; bulk revoke is the default |
| Already deleted via gist.github.com | `DELETE` returns 404 → reported as "already gone", state reconciled |
| `gh` switched to a different account | Nothing found; if local memory disagrees, say so explicitly |
| Session was forked/cloned | New session UUID — a fork never matches the parent's shares |
| Session imported from JSONL | Same UUID travels with the file → original shares are correctly claimed |
| Export exceeds 1 MB inline limit | Ranged fetch of `raw_url` prefix |
| Non-TUI mode (print/RPC) | Dialogs degrade per `ctx.hasUI`; deletion requires an explicit `--yes` |
| Network failure | Explicit error; never guess-delete from cache |

## 6. Security posture

- Deletion is gated behind an explicit confirmation, always.
- The only credential used is the user's own `gh` CLI login — identical to
  `/share`'s prerequisite. The extension reads no tokens and stores none.
- The extension deletes nothing it has not verified by session-UUID match,
  and never touches gists that merely *look* like pi shares.
- Scope of writes: gist deletion only. No repository, no settings, no
  session-file mutation beyond appending its own custom entries.
