Skip to content

feat: standalone embeddable thread view with opt-in frame origins#100

Merged
steipete merged 4 commits into
mainfrom
feat/embed-thread-view
Jul 19, 2026
Merged

feat: standalone embeddable thread view with opt-in frame origins#100
steipete merged 4 commits into
mainfrom
feat/embed-thread-view

Conversation

@steipete

Copy link
Copy Markdown
Contributor

What Problem This Solves

There is no way to dock a single ClickClack conversation inside another application. Teams that anchor a discussion to an external object — for example an OpenClaw agent session whose Control UI wants a per-session team-chat panel — currently have to open the full ClickClack app in a separate window, with the whole workspace chrome, and navigate to the thread by hand.

Why This Change Was Made

Adds a standalone thread-only page at /embed/thread/{workspace_route_id}/{message_route_id} that renders the root message, replies, and the normal thread composer with live realtime updates — and nothing else (no rail, sidebar, or top bar). It reuses the existing ThreadPanel, ChatComposer, and message rendering rather than forking them. Framing is opt-in per exact origin via a new CLICKCLACK_EMBED_FRAME_ANCESTORS / --embed-frame-ancestors / embed_frame_ancestors setting; only /embed/* HTML responses receive the frame-ancestors CSP (default 'self'), and no global frame, cookie, or CORS behavior changes. Humans authenticate with their normal session cookie; a signed-out frame offers a sign-in link that opens the full app in a new tab and reconnects automatically when focus returns.

User Impact

Operators can embed any thread as an iframe panel in a host application they control: it loads with the viewer's real ClickClack identity, posts replies with nonce idempotency, follows realtime reply/edit/delete events with cursor recovery, matches light/dark appearance, and offers an "Open in ClickClack" escape hatch into the full app. Deployments that do not configure embed origins are unchanged.

Evidence

  • New Playwright e2e (tests/e2e/embed-thread.spec.ts): loads the embed by public route IDs, verifies standalone full-width rendering with no app chrome, posts a reply through the composer, and observes an externally posted reply, an edit, and a delete arrive via realtime.
  • Go tests: frame-ancestors CSP on /embed/* only (configured origins + default 'self', absent on /app/*), origin validation (rejects wildcards, paths, credentials), config/env/flag parsing.
  • Full pnpm test (web build + all Go packages), pnpm typecheck && pnpm -r typecheck, pnpm lint, pnpm fmt:check all pass.
  • Screenshots (dev instance, seeded thread with a human and a bot participant):
Light Dark
light dark

@steipete
steipete marked this pull request as ready for review July 19, 2026 05:54
@steipete
steipete force-pushed the feat/embed-thread-view branch from 6f28446 to 0f28673 Compare July 19, 2026 05:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f284465fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if env := os.Getenv("CLICKCLACK_PUBLIC_API_URL"); env != "" {
cfg.PublicAPIURL = env
}
if env := os.Getenv("CLICKCLACK_EMBED_FRAME_ANCESTORS"); env != "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward embed origins into Cloudflare containers

When ClickClack is deployed through the checked Cloudflare pipeline, this environment setting never reaches the API process: infra/cloudflare/worker.ts explicitly constructs ClickClackContainer.envVars but does not forward env.CLICKCLACK_EMBED_FRAME_ANCESTORS. Consequently, configuring the documented Worker variable leaves cfg.EmbedFrameAncestors empty and every embedded page remains restricted to 'self', making cross-origin embedding unusable on that deployment.

Useful? React with 👍 / 👎.

Comment on lines +204 to +208
parsed, err := url.Parse(value)
if err != nil || parsed.Opaque != "" || parsed.User != nil || parsed.Hostname() == "" ||
strings.Contains(parsed.Hostname(), "*") ||
(parsed.Scheme != "http" && parsed.Scheme != "https") ||
(parsed.Path != "" && parsed.Path != "/") || parsed.RawQuery != "" || parsed.Fragment != "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject origins with invalid ports

Reject malformed port forms while validating configured origins. Go's url.Parse accepts values such as https://control.example.com: and https://control.example.com:99999, and the current predicates accept and emit them into frame-ancestors; browsers cannot match these as valid origins, so an operator typo silently leaves the intended host unable to frame the embed instead of failing startup validation.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit 7314f18 into main Jul 19, 2026
7 checks passed
Sign up for free to 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