Skip to content

feat(plugin): bridge workspace adaptors into opencode#34

Merged
Astro-Han merged 14 commits into
devfrom
codex/feat-sync-runtime-bridge
Apr 19, 2026
Merged

feat(plugin): bridge workspace adaptors into opencode#34
Astro-Han merged 14 commits into
devfrom
codex/feat-sync-runtime-bridge

Conversation

@Astro-Han

Copy link
Copy Markdown
Owner

Summary

  • expose plugin-side experimental workspace adaptor registration
  • scope custom workspace adaptors by project and checkout owner
  • persist workspace adaptor owners and recover them across cold starts, worktrees, and upgraded ownerless rows
  • restart routed cold-start sync, including ownerless non-git remote workspaces
  • add regression coverage for owner persistence, null-owner recovery, and routed sync restart

Why

Issue #27 needs a runtime bridge so plugin-defined workspace adaptors keep working after disposal, checkout switches, and cold starts. Before this slice, adaptor registration was effectively instance-local, persisted workspaces could resolve to the wrong checkout, upgraded rows with missing owner metadata had fragile recovery, and router-based cold starts could serve a request without actually restoring background sync.

Related Issue

Part of #27

How To Verify

bun test test/plugin/workspace-adaptor.test.ts test/plugin/loader-shared.test.ts test/plugin/meta.test.ts test/plugin/trigger.test.ts test/project/project.test.ts test/project/worktree.test.ts test/project/worktree-remove.test.ts test/server/workspace-router.test.ts test/plugin/github-copilot-models.test.ts --timeout 30000
bun run typecheck
bun run script/check-migrations.ts
bun --cwd packages/plugin run typecheck
bun turbo typecheck

Manual checks: none, no visible UI changes in this slice.

Screenshots or Recordings

Not applicable, no visible UI changes.

Checklist

  • I ran the relevant verification steps
  • I tested visible changes manually when needed
  • I am targeting the dev branch

@Astro-Han Astro-Han force-pushed the codex/feat-sync-runtime-bridge branch from 2cead6f to 82e3f85 Compare April 19, 2026 11:32
@Astro-Han Astro-Han merged commit 5a077d5 into dev Apr 19, 2026
9 checks passed
@Astro-Han Astro-Han deleted the codex/feat-sync-runtime-bridge branch April 19, 2026 11:40
Astro-Han added a commit that referenced this pull request May 9, 2026
Commit 81aa587 (joined-card dock refactor) hardcoded dockProgress={1}
on SessionTodoDock and removed the parent-side spring that drove it,
so the dock popped in the moment props.state.dock() flipped true
instead of sliding/fading in from 0.

Restore the mount animation:
- session-composer-region: re-introduce a useSpring tracking dockOpen,
  keep the segment mounted while it animates out (dockMounted memo),
  pass the spring value through as dockProgress.
- session-todo-dock: multiply dock() into the max-height expression
  so the segment slides 0 → collapsed-height (or 0 → full when
  mounting in expanded state), in addition to the existing opacity
  drive that was already wired.

Composer-dock spring options now appear in three places (Todo
collapse, useDockCollapse for Followup/Revert, and segment mount).
Extract them into a single DOCK_MOTION constant in composer/motion.ts
so the contract is referenceable, not scattered. Pinning the value
(0.3s, bounce:0) is the de facto standard across composer springs;
hoisting to a global SPRING_TOKENS tier ladder and wiring spring
through prefers-reduced-motion are tracked in #34.
Astro-Han added a commit that referenced this pull request May 9, 2026
Critical:
- dock-card: stop concatenating split.class into a single classList key.
  "foo bar" was becoming a literal token and tripping
  Element.classList.toggle (DOMException). Solid merges class+classList
  natively when both are passed; restored to that idiom for DockCard,
  DockSegment, and DockSegmentForm.

Major:
- editor-serialize: skip the block-separator newline when visit() has
  already contributed one (e.g. an empty <div><br></div>). Fixes
  multiline draft round-trips that were inflating "foo\n\nbar" into
  "foo\n\n\nbar".
- model-picker: nested ThinkingLevel popover renders into Kobalte.Portal,
  so its content sits outside the outer Content's DOM subtree. The
  outer popover's onPointerDownOutside fired on every inner-popover
  click and dismissed the model picker. Both popovers carry
  data-picker-content; outer now ignores the dismiss when
  event.target.closest("[data-picker-content]") matches a nested
  picker.
- session-composer-region: permission requests can arrive while
  prompt.ready() is still false (e.g. switching into a session whose
  prompt is hydrating). The hydrate fallback used to show only the
  loading card, leaving the user no way to approve or deny. The
  fallback now renders SessionPermissionContent when a request is
  pending and falls through to the loading card otherwise.
- send-button: SendButtonProps declared style?: JSX.CSSProperties |
  string but the merge silently dropped the string branch. No caller
  passes a string today, so narrow the prop type to JSX.CSSProperties
  to match actual behavior instead of inventing a string-merge code
  path.

Minor:
- keydown: the Enter-without-Shift branch had if (stopping())
  handleSubmit / else handleSubmit — both arms identical, so the
  conditional was dead. Inlined to a single handleSubmit call. The
  stopping accessor stays in scope for the keydown probe.
- popover-controllers: include cmd.source in the custom slash command
  id so workspace + user configs sharing a command name don't collapse
  into one entry under useFilteredList.

Medium (gemini):
- use-dock-collapse: contentRef is now a signal driven by the JSX ref
  callback, and createResizeObserver consumes that signal directly
  rather than running inside a createEffect. Idiomatic and tracks
  ref re-assignment correctly.

Push back:
- model-picker module-level externalOpen / openModelPicker(): the
  signal is a deliberate imperative API consumed by use-session-commands
  and the prompt-input keybind; multi-composer support would require
  reshaping the call surface, not just the state. YAGNI for the single
  composer this app has today; tracked under #34.
- editor-input draft carryover preserves text only: pre-existing
  intentional design from PR #185 (commit ea01514 "carry plain-text
  draft across directory switch"). Cross-workspace pill resolution
  needs workspace-aware path semantics — out of slice 10 scope.
Astro-Han added a commit that referenced this pull request May 9, 2026
The composer-slice-10 spec asserted boundingBox 32px but the actual
SendButton renders h-[30px] w-[30px] and DESIGN.md commits to the 30
dense tier. The assertion slipped through CI because it lacks the
@smoke tag the smoke job filters on; it would have failed the moment
someone ran the full e2e shard. Caught in external code review.

Push back deferred: openModelPicker() module-level externalOpen as
P3 follow-up — same reasoning as the prior round. The single-composer
path cleans externalOpen on onOpenChange(false), and a multi-composer
fix needs a mounted registry / focus-aware dispatch redesign rather
than a state shape tweak. Tracked under issue #34.
Astro-Han added a commit that referenced this pull request May 9, 2026
Slice 10 of issue #440 — full composer + dock + model picker rewrite. 38 commits squashed.

## Highlights

- **Composer joined-card shell**: new DockCard + DockSegment primitives in @opencode-ai/ui, dock widgets share the L34 joined-card layout. Hybrid composer shell renders permission UI inline.
- **prompt-input.tsx 1602 → ~570 lines**: split into prompt-input/{store-types, editor-serialize, editor-imperatives, comment-routing, history-navigation, popover-controllers, editor-input, keydown, model-controls}. No behavior change — all factories injected via deps; popover↔editor-input cycle resolved via mutable forward-ref.
- **DockWidgetHeader unified**: single 36px collapsed header with 30px chev IconButton (3+3 breathing) — consolidates Followup, Revert, Todo widgets.
- **DOCK_MOTION contract**: composer/dock/widgets share `{ visualDuration: 0.3, bounce: 0 }` via packages/app/src/pages/session/composer/motion.ts. Aligns with global useSpring convention.
- **Model picker rewrite**: variant control merged into model trigger; thinking-level promoted from separate popover to inline section. `[data-picker-content]` guard prevents nested popover dismiss.
- **Send button + ContextUsage parity**: 30px outer / 16px inner glyph, theme-locked colors, larger stop glyph.
- **Followup defaults to queue mode**: drops the legacy steer override.
- **Control heights**: single 30px dense tier across composer; sidebar trailing actions step down to 26 per nesting rule.
- **Picker contracts**: tighter padding, narrower width (340), dropped sticky labels.

## Post-review fixes (CI + 2 review rounds)

- Critical: DockCard manual classList merge (single key with spaces) was throwing DOMException InvalidCharacterError — replaced with Solid native class+classList co-rendering.
- CI typecheck: PopoverControllers types aligned with upstream solid-list `Accessor<string | null>` (was `undefined`).
- CI e2e: dropped stale `prompt-variant-control` selector (variant folded into model picker).
- Send button e2e contract: 32 → 30px to match dense tier.
- Editor serialize: fixed double-newline regression for blank rows.
- Keydown: removed dead `if (stopping())` branches (both arms identical).
- Custom slash command id includes source to prevent workspace+user collision.
- Model trigger label: pinned to `min-w-[80px] max-w-[180px]` to balance short names ("GPT-5.5") vs long ("DeepSeek V4 Pro") without jiggling the workspace control.

## Systemic finding (tracked under #34)

PawWork sets `html { font: var(--type-body) }` → root font-size 13px. This means **Tailwind rem-based units render at 81% of their pixel-named values**: `h-9` = 29.25px, not 36px. Multiple visual bugs in this slice (chev clipping, header height) traced back to this. Remediation: use absolute pixels (`h-[36px]`) for grid-pinned heights; avoid Tailwind's pixel-named scale for 4pt-grid alignment. DESIGN.md docs debt to be filed under #34.

## Hand-test backlog (open after merge)

- #32 dark theme regression sweep
- #33 single-prompt E2E

## Files of note

- packages/ui/src/components/dock-card.tsx (new)
- packages/app/src/pages/session/composer/{motion.ts, use-dock-collapse.ts, dock-widget-header.tsx, session-composer-region.tsx, session-todo-dock.tsx}
- packages/app/src/components/prompt-input/* (9 new files)
- packages/app/e2e/composer/composer-slice-10.spec.ts (new)

Closes part of #440.
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