Skip to content

feat(github): say what the forge already knows about a thread - #34

Merged
fiddur merged 3 commits into
developfrom
github-thread-state
Aug 25, 2026
Merged

feat(github): say what the forge already knows about a thread#34
fiddur merged 3 commits into
developfrom
github-thread-state

Conversation

@fiddur

Copy link
Copy Markdown
Collaborator

Two things a thread could not tell you: that it had already been posted, and that the author had already resolved it. Both were invisible in the same way — a finding sitting on a line looked the same whether nobody had read it or it had been ticked off yesterday.

Posted marker.submitted_at was already being written and never shown. The card now reads Posted to GitHub 15:37, yesterday 15:37, or 12 Aug 15:37. A time of day rather than 19h ago, because the question is whether the author has seen it, which is about when it landed.

Resolution. REST carries no resolution state, so this is a second call to GraphQL for reviewThreads { isResolved }, run as part of pulling comments.

Matched on file and wording, not line — and this is the part worth reading. GitHub nulls a thread's line once it goes outdated, and outdated is the usual state of a resolved thread: every one of #14378's 13 threads and #14364's came back with line: null. Keyed on the line, this sync would have quietly done nothing on exactly the threads it exists for. Two findings with identical wording in one file would now resolve together, which is the cheaper way to be wrong than missing every resolution.

Pulling also refreshes the page when only resolutions changed — the old branch called onPulled() only when comments arrived, so resolutions would have landed invisibly.

Checked against real data, since a GraphQL shape is not unit-testable: pullThreadState parses #14378 (13 threads, 13 resolved) and #14380 (12, 6 resolved), and the matcher picks the sent thread while leaving an unsent one alone. Mutation-checked the marker too.

parser 43, git 44, github 21, ui 384, cli 166; tsc at its baseline 6. Bumped to v0.9.8 per AGENTS.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs

fiddurand others added 3 commits August 25, 2026 13:22
A finding that has been posted looked identical to one that had not, and
a finding the author had already resolved looked identical to one nobody
had read. Both facts existed — `submitted_at` was being written and never
shown, and resolution was never fetched at all.
The card now carries "Posted to GitHub 15:37", a time of day rather than
"19h ago" because the question it answers is whether the author has seen
it yet.
Resolution comes from GraphQL, since REST does not carry it, and is
matched on file and wording rather than on line: GitHub nulls a thread's
line once it goes outdated, and outdated is the usual state of a resolved
thread, so a line in the key would have made the sync do nothing on
exactly the threads it exists for.
Pulling now refreshes the page when only resolutions changed, which the
old count-based branch did not.
v0.9.8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
Resolving here does not resolve there, and nothing said so. The notice
fires only for a finding that was posted — anything else has nowhere
else to be resolved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
Self-review found the amend case, which a test of mine was concealing:
it passed a thread holding both the old body and the new one, a shape
`editComment` cannot produce because it overwrites in place. So the test
passed while the behaviour it claimed to protect did not exist, and an
amended finding would have stopped matching its thread on the forge
entirely — which since the finding-only change is most of the ones
carrying an answer.
`submitted_body` records what went out, and the matcher compares against
that. Threads sent before the column fall back to their current bodies.
Two smaller ones. `pullThreadState` returned an empty array for both
"nothing is resolved" and "the call failed", so a broken sync read as a
clean one; it returns null for the second and the page says so. And
dismissing a posted finding is as local as resolving one, so it carries
the same notice.
v0.9.9.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
@fiddur
fiddur marked this pull request as ready for review August 25, 2026 11:54
@fiddur
fiddur merged commit 220bbee into developAug 25, 2026
@fiddur
fiddur deleted the github-thread-state branch August 25, 2026 11:54
fiddur added a commit that referenced this pull request Aug 26, 2026
#36)
Everything on the known list, plus the reason two of them were
invisible.
### A review taken by an unrelated one
`reviewScope` maps every base sha to one scope, because a branch's base
moves as the branch is updated. With no branch that reasoning is gone —
so a second PR opened in the same detached checkout looked like the same
review continuing, and `gatherOpenWork` moved the first one's findings
into it. Work may now only move when a branch connects two sessions, or
when the ref is identical.
Redirecting a stale tab is deliberately **not** guarded: pointing
someone at a newer session shows them something they can check, and the
legacy-migration tests depend on it. Only taking work is narrowed.
### A detached checkout called its branch HEAD
`rev-parse --abbrev-ref HEAD` returns the literal `HEAD`, which matches
nothing, stranding any session opened before `gh pr checkout` moved the
worktree onto a real branch. Recorded as unknown now, which is what it
is.
### The wrong diff decided where a comment could go
`gh pr diff --patch` is the mbox commit series, not the PR diff.
Measured on our own PRs:
| | `--patch` | correct |
|---|---|---|
| #34 `github-resolution.test.ts` | 25 lines | **89** |
| #34 `github-resolution.ts` | 29 | **59** |
| #31 `agent.ts` | 15 | **32** |
| #31 file count | 49 | 47 |
Two of #31's "files" were historical paths from before a rename. And the
error message sent you auditing your own line numbers; it names the diff
now.
### Text typed into the page was rewritten
Shell unescaping lived in `threads.ts`/`tours.ts`, which the browser
routes share, so a comment discussing `split('\n')` was stored with a
real line break. Moved to the CLI boundary. The CLI still unescapes —
verified end to end both ways.
### The two type errors, and why they survived
`packages/cli` had **no typecheck script at all**, and the UI's failed
on four TS6059 errors from its own tsconfig (`rootDir: src` against an
`include` that reaches outside it), so its output had been noise long
enough to ignore. Both fixed, `npm test` now runs `typecheck` first, and
**the count is zero**.
Mutation-checked: reverting each session fix turns the new tests red (1
and 4 of 4 respectively), and putting the unescape back in storage turns
2 of 3 red.
parser 43, git 44, github 21, ui 390, cli 179, scripts 4. v0.9.11.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_018PkYQzbsnMihHesafWvXKs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@fiddur