Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
452a5c6
docs: add FORK.md capturing fork workflow + conflict map
Jun 3, 2026
994ef4b
fix(web): suppress provider-status banner for disabled/unselected pro…
Jun 4, 2026
17422a5
chore(scripts): add pack-server tool to build a deployable fork serve…
Jun 4, 2026
82018bc
docs: capture CLI<->t3 conversation-continuity design (CONTEXT.md + A…
Jun 4, 2026
0841fa1
fix(web): resolve draft provider banner/picker to a ready provider
Jun 4, 2026
efbab7d
docs: record rewind feasibility + picker scope for /resume
Jun 4, 2026
3596620
feat(resume): server-side finder + resume.listImportableSessions RPC
Jun 4, 2026
faaad3f
feat(resume): /resume picker UI (list-only slice)
Jun 4, 2026
c998a4d
feat(resume): seed resume cursor on thread create (continue half)
Jun 4, 2026
f43fb44
feat(resume): add thread.message.user.record command (replay groundwork)
Jun 4, 2026
2ecb59e
feat(resume): transcript->commands mapper for replay display (text v1)
Jun 4, 2026
a3c907e
feat(resume): replay session transcript into the thread on create
Jun 4, 2026
f1b2ff5
feat(resume): pick a session to resume it into a new thread (UI)
Jun 4, 2026
a77dc93
feat(resume): cap replayed history to last N with a truncation notice
Jun 4, 2026
da6147b
fix(resume): render replayed slash-command invocations as clean text
Jun 4, 2026
3e28388
fix(dev): stop auto-bootstrapping a junk 'server' project on every de…
Jun 4, 2026
3ea0d1a
fix(resume): rejoin the existing thread instead of forking a duplicate
Jun 10, 2026
8c1b8c8
refactor(resume): brand existingThreadId as ThreadId in the contract
Jun 10, 2026
394c331
perf(resume): slice the transcript to the replay cap before mapping
Jun 10, 2026
40958d2
perf(resume): bound provider-binding parse concurrency
Jun 10, 2026
50dc300
chore(deploy): one-command VPS deploy script
Jun 10, 2026
a67d11b
chore(deploy): auto-deploy on commit via tracked git hook
Jun 10, 2026
4a0d9c5
docs(resume): note the replay cap is display-only (rewind-safe)
Jun 10, 2026
390bea6
fix(deploy): ship the full dist, smoke-test in the install tree, swap…
Jun 10, 2026
fcd75cc
perf(resume): scope the resume picker to the current project
Jun 10, 2026
1e0f7f1
test(resume): cover the unscoped-scan fallback in the resume picker
Jun 11, 2026
ca36a40
docs(rewind): add ADR-0002 conversation-rewind design + link from CON…
Jun 11, 2026
2c0c76f
feat(rewind): freeze conversation-rewind contracts + decider scaffold…
Jun 11, 2026
25581ee
feat(rewind): persist the provider message uuid through the write pat…
Jun 11, 2026
58def99
feat(rewind): wire resumeSessionAt into the Claude query on intention…
Jun 11, 2026
0905e83
feat(rewind): unified Claude-CLI-style conversation rewind UI (WS-3)
Jun 11, 2026
8b97936
feat(rewind): non-destructive abandoned-flip projection + read-path f…
Jun 11, 2026
6de8ed1
feat(rewind): rewind reactor + decoupled file-restore reactor (WS-2)
Jun 11, 2026
1cd761b
fix(rewind): stop the provider session so the rewind marker cold-starts
Jun 11, 2026
73787b3
fix(rewind): surface a reconnect error instead of silently no-op'ing
Jun 11, 2026
23fb863
fix(rewind): refresh the timeline when forward messages are abandoned
Jun 11, 2026
3142b0e
feat(rewind): cancel an un-sent conversation rewind (server)
Jun 11, 2026
f6c0d9a
feat(rewind): "Cancel rewind" composer banner (web)
Jun 11, 2026
2d23a6b
fix(rewind): persist the turn-final assistant uuid so rewind can anchor
Jun 11, 2026
a93019f
test(rewind): verify the anchor lands on the turn-final segment of a …
Jun 11, 2026
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
74 changes: 74 additions & 0 deletions .githooks/post-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
#
# Auto-deploy the fork server to the VPS after a commit that changed app code.
#
# Wired via `git config core.hooksPath .githooks` (see scripts/install-git-hooks.sh).
# Design goals:
# - NEVER fail the commit (always exit 0; a broken deploy must not block git).
# - Only deploy when the commit actually touched the bundle (apps/server, apps/web,
# packages/) — doc/memory/plan commits are silent no-ops.
# - Run detached so `git commit` returns instantly; the ~2-min build+deploy runs
# in the background and logs to .git/t3-deploy.log.
# - Be toggleable: `git config t3.autoDeploy false` disables it.
#
# Set T3_AUTO_DEPLOY_DRYRUN=1 to print the decision (and the command it would run)
# without launching anything.

# Resolve repo root; bail quietly if we somehow can't.
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
cd "$ROOT" || exit 0

DRY="${T3_AUTO_DEPLOY_DRYRUN:-}"
note() { printf '%s\n' "$*"; }

# 1. Toggle: default ON; only "false" disables.
if [[ "$(git config --bool t3.autoDeploy 2>/dev/null)" == "false" ]]; then
[[ -n "$DRY" ]] && note "[auto-deploy] skip (disabled via git config t3.autoDeploy=false)"
exit 0
fi

# 2. Path gate: did this commit touch bundle-affecting code?
if ! git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null \
| grep -Eq '^(apps/server/|apps/web/|packages/)'; then
[[ -n "$DRY" ]] && note "[auto-deploy] skip (commit touched no bundle paths: apps/server, apps/web, packages)"
exit 0
fi

# 3. Locate mise (bun is not on the bare hook PATH).
MISE="$(command -v mise 2>/dev/null || true)"
[[ -z "$MISE" && -x /opt/homebrew/bin/mise ]] && MISE=/opt/homebrew/bin/mise
[[ -z "$MISE" && -x /usr/local/bin/mise ]] && MISE=/usr/local/bin/mise
if [[ -z "$MISE" ]]; then
note "[auto-deploy] mise not found on PATH — skipping. Run 'bun run deploy' manually."
exit 0
fi

DEPLOY_CMD="$MISE exec -- bun run deploy"

if [[ -n "$DRY" ]]; then
note "[auto-deploy] would deploy — bundle paths changed."
note "[auto-deploy] command: $DEPLOY_CMD (logs -> .git/t3-deploy.log)"
exit 0
fi

# 4. Concurrency guard: non-blocking lock (atomic mkdir). If a deploy is already
# running, skip — the user can re-run `bun run deploy` or the next commit re-fires.
LOCK="$ROOT/.git/t3-deploy.lock"
LOG="$ROOT/.git/t3-deploy.log"
if ! mkdir "$LOCK" 2>/dev/null; then
note "🚀 auto-deploy: a deploy is already running — this commit will be picked up by re-running 'bun run deploy' if needed."
exit 0
fi

# 5. Launch detached. The subshell releases the lock when the deploy finishes.
# SSH key is inherited from the committing shell's SSH_AUTH_SOCK.
{
printf '\n===== auto-deploy %s (commit %s) =====\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$(git rev-parse --short HEAD)"
$DEPLOY_CMD
printf '===== auto-deploy exit %s =====\n' "$?"
rmdir "$LOCK" 2>/dev/null || true
} >>"$LOG" 2>&1 &
disown 2>/dev/null || true

note "🚀 auto-deploy started in background → tail -f .git/t3-deploy.log (disable: bun run deploy:auto:off)"
exit 0
54 changes: 54 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CONTEXT — Conversations & resume

The shared language for how a back-and-forth agent conversation is stored, identified, and
resumed across t3code and an agent's own standalone CLI. Seeded 2026-06-03 while designing
CLI ↔ t3 conversation continuity (Claude first). This is a fork — see [FORK.md](./FORK.md).

## Language

**Thread**:
t3code's term for one back-and-forth agent conversation (id = UUID, stored in t3's own SQLite at `~/.t3code/userdata/state.sqlite`).
_Avoid_: conversation, session, chat — at the t3 domain level it is always a Thread.

**Provider session**:
The underlying agent's own conversation instance that actually holds the model context (for Claude, a Claude session id). A Thread is backed by a Provider session.
_Avoid_: calling this a "thread" — that collides with t3's Thread.

**Resume cursor**:
The pointer t3 stores per Thread so it can reconnect to that Thread's Provider session. For Claude it is `{ resume: <sessionId>, resumeSessionAt: <lastAssistantUuid> }` — the exact data `claude --resume` needs.

**Provider / agent backend**:
The engine that runs a Thread: Claude, Codex, Cursor, or OpenCode. The first target is Claude.

**Project bucket**:
Claude's on-disk grouping of sessions by working directory: `~/.claude/projects/<cwd-hash>/<sessionId>.jsonl`. Both t3 and the standalone CLI share `~/.claude` by default (no `homePath` override).

**Worktree**:
An isolated git working copy t3 may create for a Thread. When present, the Thread runs there instead of the repo root — which puts its Provider session in a *different* Project bucket than a terminal session run from the repo.

**Importable session**:
A past Claude Provider session in this project's Project bucket that t3's `/resume` picker offers to pull into a new Thread. Defined as: a top-level chat (not a background sub-agent / sidechain session), with real content (not an empty or abandoned stub), that originated in the **terminal** (not one t3 itself created), in this project's working directory. A terminal session already pulled into a Thread is still listed but flagged "already in t3".
_Avoid_: listing t3-originated or sub-agent sessions in the picker by default.

**Session origin**:
Where a Provider session was started: the **terminal** (the user's own Claude CLI) or **t3** (the app drove Claude itself). The picker shows terminal-origin sessions; t3-origin ones are already Threads and are hidden by default. The two are distinguishable on disk.

## Relationships

- A **Thread** is backed by exactly one **Provider session** at a time, via a **Resume cursor**.
- A **Provider session** physically lives in one **Project bucket**, keyed by the **working directory** it ran in.
- Resuming a **Provider session** only works from *its own* **Project bucket** — i.e. the same working directory. Different directory ⇒ "No conversation found." (verified 2026-06-03)
- A **Thread** runs in its **Worktree** if it has one, otherwise the project's repo root.

## Flagged ambiguities

- **"the CLI"** — initially ambiguous. Resolved: the *agent's own* standalone CLI (Claude Code first), **not** a new t3 CLI, and **not** running one agent's chat inside a different agent. Same agent, two places.
- **Target experience (resolved 2026-06-03):** a t3 **Thread** should appear in Claude's native `/resume` picker like any normal Claude Code session — no copy-paste command, no special button. This works **iff** the Thread ran in the project's repo root (so its Provider session lands in the repo's **Project bucket**, which is what `/resume` lists for that folder).
- **Priority direction (resolved 2026-06-03):** the more important half is **terminal → t3** ("resume a CLI chat *inside* t3"), built first. The reverse (t3 → terminal) is nearly free and comes second.
- **VERIFIED with real data (2026-06-03):** t3 already stores Claude **Provider sessions** in the *shared* `~/.claude` store and runs Claude **Threads** in the repo root (no Worktree by default — real threads had `worktree_path: null`, branch `custom`/`main`). A real t3 thread's session file (`c55dc749…`, 26 msgs) physically sits in the **same Project bucket** as terminal sessions. macOS's case-insensitive filesystem merges the `Dev` vs `dev` path-casing difference into one bucket, so the two share a single session pool. t3 state lives at `~/.t3/userdata/state.sqlite`; resume pointer is `provider_session_runtime.resume_cursor_json` = `{resume, resumeSessionAt}`.
- **Display of an imported chat (resolved 2026-06-03):** **show** the earlier messages (readable text), so the user can see the chat and pick a point — not "show nothing". Full byte-perfect reproduction of every tool call/sidechain is *not* required for v1.
- **Rewind (resolved 2026-06-03):** two kinds, mirroring Claude Code's *native* rewind, which deliberately separates them (native menu offers "Restore conversation" vs "Restore code" as distinct actions — verified against Claude Code rewind docs 2026-06-03). **Conversation-rewind** (jump to an earlier message and continue) is in v1 — it uses the stable `resumeSessionAt` anchor. It mirrors native "Restore conversation": it stays in the **same Thread / Provider session** (not a new branch Thread), it is **non-destructive** (the skipped-past messages are retained in the on-disk transcript tree, as native keeps them), and it **leaves the working tree untouched**. Mechanically it just moves `resumeSessionAt` back to an earlier message uuid and continues the same session. **File-rewind** (native "Restore code" — restore the working tree to an earlier point of the *imported* chat) is **deferred**: the data exists (`~/.claude/file-history/<uuid>/` blobs + `file-history-snapshot` transcript entries with `trackedFileBackups`), so it's *possible*, but it means reading Claude's private, undocumented snapshot format and translating it to t3's git-checkpoint model — fragile against Claude updates, against the fork's "don't depend on churny internals" principle. Because native treats the two as separate actions, doing conversation-rewind without file-rewind is **faithful to native**, not a compromise. Revisit file-rewind only if it proves essential. **Refined 2026-06-11 → see [ADR-0002](docs/adr/0002-conversation-rewind.md):** v1 applies to **all** Threads (native + imported), behind **one Claude-CLI-style menu** ("restore conversation only" vs "also restore files"); abandoned forward messages are hidden-but-retained; the rewound prompt is pre-filled for editing; the existing destructive `thread.checkpoint.revert` is **decoupled** so code-restore no longer force-deletes the conversation; and a post-rewind "restore files to this point too" affordance covers a change of mind (git-checkpoint chats only).
- **Picker contents (resolved 2026-06-04):** the `/resume` picker lists **Importable sessions** only — terminal-origin chats for this project. t3-origin chats are hidden by default (they are already Threads; a future toggle can reveal them); background sub-agent / sidechain sessions and empty stubs are always hidden; an already-imported terminal chat is shown but flagged "already in t3". Distinguishing these on disk was verified against the real bucket on 2026-06-04 (origin marker, sidechain flag, content size, and a stored title for display all present).
- **Surface (resolved 2026-06-04):** two entry points, one picker — a `/resume` command in the t3 chat box (caught by t3 before it reaches Claude) and a button near "new thread" in the sidebar.
- **"conversation"** — maps to both a t3 **Thread** and a **Provider session**; they are linked, not the same thing.
- **"resume / pick up the context"** — means continue the *same* conversation on the other side. **Resolved (2026-06-03):** carry the *chat*; run **both sides in the same folder** (the real repo, no private Worktree for bridged Threads) so the files line up on their own. Trade-off accepted: bridged Threads edit the real working tree, giving up worktree isolation.
108 changes: 108 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# FORK.md — how this fork works

> This is a **fork** of [`pingdotgg/t3code`](https://github.com/pingdotgg/t3code) (Theo's "T3 Code").
> The whole point of the setup below is to **keep pulling Theo's updates cleanly** while our own
> work stays separate and doesn't fight his.
>
> This file is the shared understanding. If you (or a new Claude session) are picking this up,
> **read this first.** It is the source of truth; do not relearn it from scratch.
>
> _Last updated: 2026-06-03. Status: fork is bootstrapped, nothing custom built yet._

---

## The plain-English version

- **Theo's repo moves fast.** His own `AGENTS.md` says it's a "VERY EARLY WIP," so he ships big,
frequent changes. Our job is to ride along without constant merge pain.
- **Two branches, kept apart:**
- `main` is a **clean mirror** of Theo's repo. We never put our own work here.
- `custom` is **where all our work lives.**
- **The golden rule: add new files, don't edit his.** When our changes live in new files (or whole
new sub-projects), Theo's updates and ours touch different files and never collide. Editing his
existing files is what causes painful conflicts — so we only do it when a feature truly has no
other way, and then we keep the edit as small as possible.
- **Conflict auto-memory is on.** Git is set to remember how we resolve any conflict and replay
that fix automatically next time the same one shows up (`rerere`). So a conflict, once solved,
generally stays solved.
- **Pull his updates often, in small steps.** Small, frequent catch-ups are far easier than one
giant one months later.

## How easy a given feature is to keep separate — the three buckets

This is the heart of what we worked out. How clean things stay depends on *what kind* of feature it is:

1. **New screens / views / a whole new tool — nearly conflict-free.**
The web app discovers screens just from which files exist in a folder, and the project picks up
brand-new sub-projects automatically. New files only, nothing of Theo's touched.

2. **Features where the server has to do something new — manageable.**
These have to register themselves in a few central "switchboard" files (see the reference below).
Adding a background service is basically a one-line addition. Adding a new browser↔server command
is a real edit, not a clean plug-in, so expect small conflicts there — `rerere` softens the repeats.

3. **Changing how the core agent conversation itself works — genuinely conflict-prone.**
The full list of agent events/commands lives in one big central file the whole app keys off of.
It can't be sidestepped, and it's exactly the kind of file Theo reshapes often. Avoid extending
the core protocol if you can; if a feature truly needs it, treat that file as a known battleground.

**Cleanest option of all:** when a feature allows it, build it as its **own separate project that
talks to t3code**, rather than living inside his code. Then there is nothing of ours in his files.

We deliberately did **not** pre-build "plug your stuff in here" scaffolding — empty hooks would just
be guesses about his structure that rot as he changes things. We apply the small "seam" edit *when* a
feature actually needs a central file, not before.

---

## Reference (for precise, repeatable steps)

### Remotes & branches
- `origin` → `ziyadakl/t3code` (our fork — safe to push to)
- `upstream` → `pingdotgg/t3code` (push URL is blocked on purpose, so we can't accidentally PR to Theo)
- `main` — clean mirror of `upstream/main`; **never commit here**
- `custom` — all our work; branch off `custom` for individual features

### Pulling Theo's updates
```sh
git fetch upstream
git checkout main
git merge --ff-only upstream/main # main stays a pure mirror
git push origin main
git checkout custom
git rebase main # rerere auto-replays past conflict fixes
```
Do this frequently. Resolve any conflict once; `rerere` remembers it.

### Local git config already set (lives in `.git/config`, not committed)
- `rerere.enabled = true`
- `rerere.autoupdate = true`

### The conflict-prone "hot files" (verified 2026-06-03)
Touch these only when a feature genuinely requires it; keep edits minimal.

| File | What it is | Bucket | Conflict risk |
|---|---|---|---|
| `packages/contracts/src/orchestration.ts` | Core agent event/command unions (`OrchestrationCommand`, `OrchestrationEvent`); ~1,300 lines | 3 | **High** — central, heavily reshaped upstream |
| `apps/server/src/ws.ts` | Browser↔server command registry (`WsRpcGroup.of({…})`) + auth-scope map | 2 | Medium — real edits, handlers close over lots of local scope |
| `packages/contracts/src/rpc.ts` | Method-name constants (`WS_METHODS`) every command starts from | 2 | Medium |
| `apps/server/src/server.ts` | Effect service graph (`.pipe(Layer.provideMerge(…))` chain) | 2 | **Low** — new service = one line appended after `AuthLayerLive` |

### The low-conflict seams (prefer these)
- **New web screen:** add a file under `apps/web/src/routes/`. `routeTree.gen.ts` is autogenerated —
don't hand-edit it. No edit to Theo's files.
- **New package/sub-project:** create a folder under `apps/` or `packages/`. Root `package.json`
uses globs (`apps/*`, `packages/*`), so it's picked up with no edit.
- **New server service:** append one `Layer.provideMerge(YourLayerLive)` at the end of the chain in
`server.ts`; keep `YourLayerLive` in your own new file.

### Required checks before declaring work done (from Theo's `AGENTS.md`)
- `bun fmt`, `bun lint`, `bun typecheck` must all pass
- Use `bun run test` (Vitest) — **never** `bun test`
- If touching native mobile code, `bun lint:mobile` must also pass

### Toolchain notes
- `mise` pins Node 24.13.1 + Bun 1.3.9 (see `.mise.toml`).
- `bun` is **not** on the PATH directly — run everything via `mise exec -- bun …`
(e.g. `mise exec -- bun install`, `mise exec -- bun run test`).
- There is **no plugin / settings / config layer** — additions must be real code changes.
Loading