feat(provider): optional DeepLinker capability + provider-owned open affordance (ADR-0017) #12

Merged
dominik.polakovics merged 1 commit from feat/deeplinker-seam into main 2026-07-07 09:00:48 +02:00

Implements #6: make deep-link capture an optional provider capability (provider.DeepLinker = CaptureDeepLink + FallbackOpen) instead of a mandatory AgentProvider method, following the existing ConnectingReporter pattern. This unblocks #2 (Codex has no web Open link) and removes the provider-isolation violation where core instance code imported claudecode to recognise a capture miss and the SPA hardcoded the claude.ai fallback.

Changes

  • provider: AgentProvider loses CaptureDeepLink; new optional DeepLinker (CaptureDeepLink + FallbackOpen) and OpenAffordance metadata.
  • claude-code: a capture miss returns "" (still logs loudly); FallbackOpen() owns the claude.ai picker URL + v0 tooltip.
  • instance (core): no claudecode import; capture arms only for DeepLinker providers, at Start and startup re-adoption alike; "" is a miss so write-only-on-hit simplifies to "non-empty → persist". A link-less provider keeps deep_link_url NULL.
  • httpapi: GET /providers gains an additive fallback_open field, present only for DeepLinker providers.
  • providertest: Fake implements DeepLinker; new NoLinkFake (AgentProvider-only) covers the link-less path end to end.
  • SPA: openState is provider-driven (fallback as a tri-state, so a still-loading providers list never flashes tmux-attach); a link-less row shows a copyable tmux attach -t <session>; shared OpenAffordance component. No hardcoded provider URL/tooltip in the open path.
  • docs: ADR-0017 + CONTEXT.md.

Acceptance criteria

  • Link-less provider runs manual/AFK end to end, no capture goroutine armed, deep_link_url NULL.
  • Core (instance/reconcile/httpapi/store) has no in-scope provider coupling for deep links (internal/instance grep-clean; httpapi's claudecode imports are the out-of-scope auth routes + chat error mapping).
  • No hardcoded provider URL/tooltip in the SPA open path; renders exactly as before (miss → generic picker link, hit → exact link, poll → connecting pulse).
  • Link-less rows show the copyable tmux attach -t <session> affordance with a tooltip.
  • Empty capture never persisted; a real link never overwritten.
  • ADR recorded (ADR-0017).

Review & validation

A 7-agent adversarial review (4 dimensions → verify) surfaced one low-severity finding — a missed-capture claude-code row briefly flashing tmux-attach while the providers list loaded — fixed via the fallback tri-state with regression tests. Two other findings were adversarially refuted.

go build/vet/test ./... · gofmt · golangci-lint 0 issues · tsc · ESLint · Prettier · vitest 309 passed · vite build.

Unblocks #2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NP2Ai6CBkYY7Fj1fujtRDD

Implements #6: make deep-link capture an **optional provider capability** (`provider.DeepLinker` = `CaptureDeepLink` + `FallbackOpen`) instead of a mandatory `AgentProvider` method, following the existing `ConnectingReporter` pattern. This unblocks #2 (Codex has no web Open link) and removes the provider-isolation violation where core instance code imported `claudecode` to recognise a capture miss and the SPA hardcoded the claude.ai fallback. ## Changes - **provider**: `AgentProvider` loses `CaptureDeepLink`; new optional `DeepLinker` (`CaptureDeepLink` + `FallbackOpen`) and `OpenAffordance` metadata. - **claude-code**: a capture miss returns `""` (still logs loudly); `FallbackOpen()` owns the claude.ai picker URL + v0 tooltip. - **instance (core)**: no `claudecode` import; capture arms only for `DeepLinker` providers, at Start and startup re-adoption alike; `""` is a miss so write-only-on-hit simplifies to "non-empty → persist". A link-less provider keeps `deep_link_url` NULL. - **httpapi**: `GET /providers` gains an additive `fallback_open` field, present only for `DeepLinker` providers. - **providertest**: `Fake` implements `DeepLinker`; new `NoLinkFake` (AgentProvider-only) covers the link-less path end to end. - **SPA**: `openState` is provider-driven (fallback as a tri-state, so a still-loading providers list never flashes tmux-attach); a link-less row shows a copyable `tmux attach -t <session>`; shared `OpenAffordance` component. No hardcoded provider URL/tooltip in the open path. - **docs**: ADR-0017 + CONTEXT.md. ## Acceptance criteria - [x] Link-less provider runs manual/AFK end to end, no capture goroutine armed, `deep_link_url` NULL. - [x] Core (instance/reconcile/httpapi/store) has no in-scope provider coupling for deep links (`internal/instance` grep-clean; httpapi's `claudecode` imports are the out-of-scope auth routes + chat error mapping). - [x] No hardcoded provider URL/tooltip in the SPA open path; renders exactly as before (miss → generic picker link, hit → exact link, poll → connecting pulse). - [x] Link-less rows show the copyable `tmux attach -t <session>` affordance with a tooltip. - [x] Empty capture never persisted; a real link never overwritten. - [x] ADR recorded (ADR-0017). ## Review & validation A 7-agent adversarial review (4 dimensions → verify) surfaced one low-severity finding — a missed-capture claude-code row briefly flashing tmux-attach while the providers list loaded — **fixed** via the fallback tri-state with regression tests. Two other findings were adversarially refuted. `go build`/`vet`/`test ./...` · gofmt · **golangci-lint 0 issues** · tsc · ESLint · Prettier · **vitest 309 passed** · vite build. Unblocks #2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NP2Ai6CBkYY7Fj1fujtRDD
feat(provider): optional DeepLinker capability + provider-owned open affordance (ADR-0017)
Some checks failed
ci / flake-check (pull_request) Has been cancelled
f26e720031
Deep-link capture becomes an optional provider capability instead of a
mandatory AgentProvider method, following the ConnectingReporter pattern.
This unblocks #2 (Codex, no web Open link) and fixes the provider-isolation
violation where core instance code imported claudecode to recognise a miss
and the SPA hardcoded the claude.ai fallback.

- provider: AgentProvider loses CaptureDeepLink; new optional DeepLinker
  (CaptureDeepLink + FallbackOpen) and OpenAffordance metadata.
- claude-code: a capture miss returns "" (still logs loudly); FallbackOpen
  owns the claude.ai picker URL + v0 tooltip.
- instance: no claudecode import; capture arms only for DeepLinker providers,
  at Start and re-adoption alike; "" is a miss (write-only-on-hit simplifies
  to non-empty -> persist). A link-less provider keeps deep_link_url NULL.
- httpapi: GET /providers gains an additive fallback_open field, present
  only for DeepLinker providers.
- providertest: Fake implements DeepLinker; new NoLinkFake (AgentProvider
  only) covers the link-less path end to end.
- SPA: openState is provider-driven (fallback as a tri-state so a loading
  providers list never flashes tmux-attach); a link-less row shows a copyable
  `tmux attach -t <session>`; shared OpenAffordance component. No hardcoded
  provider URL or tooltip in the open path.
- docs: ADR-0017 and CONTEXT.md.

Closes #6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NP2Ai6CBkYY7Fj1fujtRDD
Author
Owner

This was generated by AI while landing a PR.

Validation audit — PR #12

Verdict: PASS.

Verification signal. CI (ci / flake-check) is stuck Waiting to run — this repo's Forgejo instance has no nix-label runner, so it is not a vouching signal. This repo has no commit-time gate either (no pre-commit hook). Per the "run the project's own checks when nothing vouches" rule, I ran the repo's own gate locally: nix flake check -Lall checks passed (exit 0). That covers:

  • Go build (lab + labctl), go vet, full go test ./... — all packages OK
  • golangci-lint0 issues
  • web tsc + ESLint + Prettier + vitest 309 passed + vite build
  • NixOS module eval

Convention lint. Title feat(provider): … is Conventional Commits. ADR recorded (ADR-0017); CONTEXT.md updated. No secrets.yaml / state-version concerns apply to this repo.

Diff review (23 files, +851/−160) against ADR-0017 intent.

  • provider.go: CaptureDeepLink removed from AgentProvider; new optional DeepLinker (CaptureDeepLink + FallbackOpen) + OpenAffordance struct — the ConnectingReporter pattern. ✓
  • instance.go: drops the claudecode import; capture arms only via a DeepLinker type assertion; a miss is "", so persistence simplifies to "non-empty → persist". launch.go now routes through ArmCapture, so both the NULL-guard and the capability gate apply on the launch path. Behaviour for claude-code is preserved (old == GenericDeepLink skip ≡ new == "" skip). ✓
  • httpapi/providers.go: additive fallback_open, present only for DeepLinker providers. ✓
  • SPA: openState is a tri-state — exact link wins (provider-independent), then connecting, then unknown (renders nothing while the providers list loads — the flash-fix), then the provider's generic web link, else a copyable tmux attach -t <session>. No hardcoded provider URL/tooltip in the open path. AttachButton guards navigator.clipboard for non-secure/permission contexts. ✓

Tests. NoLinkFake (AgentProvider-only) + TestStart_linklessProvider_noCaptureNoDeepLink prove the no-arm / NULL-link / non-connecting / re-adoption path end to end; TestAPI_Providers asserts the fallback_open field.

Note — issue #6 will not auto-close. The body says "Implements #6", which is not a Forgejo closing keyword. This is a manual feat/ branch (not afk/<N>), so there's no run-claim to release — but #6 should be closed manually on merge. I'll do that as part of closing the loop.

No blockers. Awaiting free-text merge go-ahead.

> *This was generated by AI while landing a PR.* ## Validation audit — PR #12 **Verdict: PASS.** **Verification signal.** CI (`ci / flake-check`) is stuck *Waiting to run* — this repo's Forgejo instance has no `nix`-label runner, so it is not a vouching signal. This repo has no commit-time gate either (no pre-commit hook). Per the "run the project's own checks when nothing vouches" rule, I ran the repo's own gate locally: `nix flake check -L` → **all checks passed** (exit 0). That covers: - Go build (`lab` + `labctl`), `go vet`, full `go test ./...` — all packages OK - `golangci-lint` — **0 issues** - web `tsc` + ESLint + Prettier + **vitest 309 passed** + `vite build` - NixOS module eval **Convention lint.** Title `feat(provider): …` is Conventional Commits. ADR recorded (ADR-0017); CONTEXT.md updated. No `secrets.yaml` / state-version concerns apply to this repo. **Diff review (23 files, +851/−160) against ADR-0017 intent.** - `provider.go`: `CaptureDeepLink` removed from `AgentProvider`; new optional `DeepLinker` (CaptureDeepLink + FallbackOpen) + `OpenAffordance` struct — the `ConnectingReporter` pattern. ✓ - `instance.go`: drops the `claudecode` import; capture arms only via a `DeepLinker` type assertion; a miss is `""`, so persistence simplifies to "non-empty → persist". `launch.go` now routes through `ArmCapture`, so both the NULL-guard and the capability gate apply on the launch path. Behaviour for claude-code is preserved (old `== GenericDeepLink` skip ≡ new `== ""` skip). ✓ - `httpapi/providers.go`: additive `fallback_open`, present only for `DeepLinker` providers. ✓ - SPA: `openState` is a tri-state — exact link wins (provider-independent), then `connecting`, then `unknown` (renders nothing while the providers list loads — the flash-fix), then the provider's generic web link, else a copyable `tmux attach -t <session>`. No hardcoded provider URL/tooltip in the open path. `AttachButton` guards `navigator.clipboard` for non-secure/permission contexts. ✓ **Tests.** `NoLinkFake` (AgentProvider-only) + `TestStart_linklessProvider_noCaptureNoDeepLink` prove the no-arm / NULL-link / non-connecting / re-adoption path end to end; `TestAPI_Providers` asserts the `fallback_open` field. **Note — issue #6 will not auto-close.** The body says "Implements #6", which is not a Forgejo closing keyword. This is a manual `feat/` branch (not `afk/<N>`), so there's no run-claim to release — but #6 should be closed manually on merge. I'll do that as part of closing the loop. No blockers. Awaiting free-text merge go-ahead.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!12
No description provided.