Skip to content

feat: workspace-scoped Todo/Linear sidebar (ADR-0003)#33410

Open
BoringLink wants to merge 34 commits into
anomalyco:2.0from
BoringLink:feature/todo-sidebar-linear
Open

feat: workspace-scoped Todo/Linear sidebar (ADR-0003)#33410
BoringLink wants to merge 34 commits into
anomalyco:2.0from
BoringLink:feature/todo-sidebar-linear

Conversation

@BoringLink

@BoringLink BoringLink commented Jun 22, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #N/A (no upstream issue — implementing the Todo/Linear sidebar that was planned in the project's OPENCODE_LINEAR_INTEGRATION.md design doc; targets the 2.0 line, see "Base branch" below).

Base branch

2.0, not dev. This branch was developed on top of the 2.0 exploration commit (7a6ce05d09). dev and 2.0 no longer share history — the 2.0 architectural refactor (PR #22335) was reverted/force-pushed out of dev after merge, so a PR into dev is structurally unmergeable. The only base where this work applies cleanly is 2.0 (0 conflicts, 34 commits ahead). If you want this work on dev instead, the changes need to be ported: IssueTable lands in packages/opencode, SidebarTodo in packages/app, etc. — not in the new packages/core / packages/cli / etc. that 2.0 introduced. That's a separate effort.

Type of change

  • New feature
  • Refactor / code improvement
  • Bug fix
  • Documentation

What does this PR do?

Adds a Linear-style Todo sidebar to OpenCode Desktop and TUI. Todos are workspace-scoped (persist per directory, survive across sessions) and the sidebar works independently of Linear MCP — Linear is an optional sync path, not a prerequisite.

Two specific issues this PR fixes:

  1. The Todo sidebar previously was empty unless the user had the Linear MCP server connected — there was no way to manage todos locally. Now there's a workspace-scoped IssueTable + CRUD service + UI sidebar, so todos are first-class regardless of Linear.
  2. When "Pull from Linear" was clicked with a connected Linear account, the toast said "Already up to date (1 already synced)" but the sidebar showed nothing. Root cause: pull was writing to a different table than the sidebar was reading from. The pull/push engine now target the same IssueTable the sidebar reads.

Mechanically, this PR introduces IssueTable (workspace-scoped SQLite, snake_case, with the full Linear status set + L1/L2 hierarchy), an Issue.Service that publishes issue.* bus events, a workspace-scoped Issue.AutoProgress engine, /issue/* HTTP routes, six issue_* tools the agent can call without Linear MCP, a SidebarTodo component in the Desktop sidebar (and a TUI equivalent), and a rewrite of SidebarLinear to read the new Issue SDK. The legacy packages/opencode/src/linear/ module is removed.

I understand why this works: the previous TodoTable was per-session (in the chat loop), not per-workspace, so it couldn't outlive a session and couldn't be the source of truth for the sidebar. The new IssueTable is keyed on directory, which is what the sidebar already uses to scope all project state — so they're speaking the same vocabulary. The "pull says 1 but sidebar shows 0" bug was the result of two different tables using different keys; collapsing them into one table with a single key (directory for the workspace scope, id for the row) fixes it.

I do not fully understand the long-term reconciliation with the in-session TodoTable (currently a parallel system carrying similar fields). The deferred-followup section at the bottom calls this out — I think paring it back to a flat per-session list is the right move, but I haven't done it here because it would need its own schema migration and ADR.

How did you verify your code works?

  • bun --cwd packages/opencode test: 1940 tests, 50 fail, 1 error. All failures are in pre-existing code paths (test/session/prompt-effect.test.ts, test/session/processor-effect.test.ts, test/session/snapshot-tool-race.test.ts, test/storage/json-migration.test.ts, test/util/flock.test.ts). None are in the new IssueTable / Issue.Service / issue_* tool code. Numbers vary slightly run-to-run because of flaky LLM mocks; the count of 50-51 is the steady state.
  • Verified on the bare origin/2.0 base: prompt-effect.test.ts is 0/34 pass without this branch, 19/34 pass with it. The branch fixes some pre-existing failures via the test-fixture Layer.provide(AutoProgress.defaultLayer) / Layer.provide(Issue.defaultLayer) wiring.
  • New tests added: packages/opencode/test/issue/issue-tools.test.ts (2 pass), packages/opencode/src/issue/sync-pull.test.ts (5/5 in isolation; 0/1 in the full suite due to known mock-Linear timing flake — confirmed pre-existing).
  • Manual end-to-end smoke (Hono backend on port 4100): GET /issue returns [], POST /issue creates an issue, GET /issue returns the created issue, GET /issue/{id} returns it again, POST /issue/{id}/status updates the status. The fix-migration test on a fresh DB succeeds.
  • Backend boots cleanly after the latest migration fix (2328738a9d fix(opencode): repair add_issue_table migration).
  • All four pr-standards / pr-management checks pass on this PR (re-runs on body edits still pass).

Screenshots / recordings

Not included in this PR. The sidebar UI changes are visible by opening OpenCode Desktop with a workspace that has a few issues; the TUI changes are visible by running the TUI against the same backend. I'm happy to record a screen capture if the reviewer wants.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR (the untracked packages/{cli,core,server,tui}/AGENTS.md files in the worktree are pre-existing placeholders from the 2.0 line, not part of this PR)
  • Conventional-commit prefixes used: feat(opencode):, feat(app):, feat(tui):, fix(app):, chore(opencode):, chore(sdk):, test(opencode):, docs:
  • Migration is additive (new issue table + rebuild of todo table via __new_todo pattern with proper defaults — SQLite-safe, no ALTER TABLE ADD COLUMN NOT NULL without DEFAULT)
  • New code follows the existing patterns in packages/opencode/AGENTS.md (snake_case Drizzle columns, Effect.fn("Domain.method") for tracing, no else/try/catch/any, single-word names)

Deferred follow-up

Reverting the existing in-session TodoTable workspace-scoped columns would require stripping Todo.Service, the auto-progress engine, and the todo_* tools down to a flat per-session list, plus a schema migration. The IssueTable now owns the hierarchy/Linear feature; the in-session todo is a parallel system and can be pared down in a follow-up PR. Documented in the step 10 commit message.

BoringLink and others added 30 commits June 20, 2026 20:50
…nfig

- Remove console.log in dialog-edit-todo.tsx
- Replace any with Todo type in event-reducer.ts
- Add owner/date format to 3 TODO comments
- Remove dead commented-out code in linear-sync-history.tsx
- Add Linear MCP server entry to opencode.jsonc (MCP auto-wiring)
…tep 1)

New project/workspace-scoped SQLite table for the Linear-style todo
feature. The existing in-session TodoTable is left untouched; this
table is the new system per ADR-0001 (it persists across sessions
in a directory and supports parent/child hierarchy + Linear-aligned
statuses and priorities).

Schema:
  - id, directory, parent_id, level (0=L1, 1=L2)
  - title, content, description
  - status (backlog/todo/in_progress/in_review/done/canceled)
  - priority (none/urgent/high/medium/low)
  - labels, due_date, assignee_id
  - linear_issue_id, linear_team_id, linear_project_id (set on pull)
  - position, last_pushed_at
  - time_created, time_updated (Timestamps)

Indexes on directory, parent_id, and linear_issue_id for the
sync-pull/sync-push lookups.

Migration is purely additive: CREATE TABLE issue, plus a no-op
rebuild of todo to its current extended shape (the existing 21 worktree
commits already extended TodoTable; step 10 of ADR-0003 will revert
that to its pre-feature state and remove the linear/ module).

Also: ADRs 0001/0002/0003 + glossary capturing the design decisions
from the grill-with-docs session on 2026-06-22.
…ents

Mirrors Session.Todo.Service shape but keyed by directory instead of
sessionID. CRUD: get, create, update, delete, patchStatus,
patchAssignee, reorder, getTree. Bus events: Issue.Created/Updated/
Deleted/Progressed, all carrying the directory for fan-out.

Status/priority enums are the Linear-aligned sets (backlog/todo/
in_progress/in_review/done/canceled; none/urgent/high/medium/low).
The in-session TodoTable is untouched; the two systems coexist.

Registered as Issue.defaultLayer alongside Todo.defaultLayer in
effect/app-runtime.ts.
ADR-0003 step 3. The Linear MCP client wrapper and the tool-name
constants now live in src/issue/. The src/linear/ copies are
single-line re-export shims so existing import paths keep resolving
unchanged.

The mcp-client test moved with the source (it doesn't import from
any path that needs updating yet — that's step 4). sync-pull,
sync-push, and integration tests stay in src/linear/ for now;
they'll be rewritten in step 4 to use Issue.Service.
…et IssueTable

ADR-0003 step 4. The previous sync layer in src/linear/ predated the
new IssueTable (introduced in step 1) and imported Session.Todo.Service
to manage its rows. It also returned a misleading "Already up to date"
label when Linear had issues that weren't yet linked locally — the
exact bug raised in the open issue ("Pull from Linear hints 'Already up
to date (1 already synced)' but Todos shows no todo").

SyncPull.pull({ directory }) now:
  - fetches active Linear issues (unstarted/started) for the configured
    project (paginated, 50/page)
  - inserts any whose `linear_issue_id` is not already linked locally
  - NEVER overwrites existing local rows (local edits are first-class
    per ADR-0002 D5)
  - returns honest pulled/skipped/failed counts — no "already up to
    date" euphemism (ADR-0002 D6)

SyncPush.push({ directory, issueIds? }) now:
  - targets IssueTable (not Session.Todo.Service)
  - skips issues with no `linear_issue_id` (local-only)
  - skips issues whose `time_updated <= last_pushed_at`
  - on success, sets `last_pushed_at = time_updated` in a single SQL
    UPDATE, keeping the watermark in lockstep so the next push skips
    unchanged rows (Drizzle's `$onUpdate` would otherwise bump
    `time_updated` past `last_pushed_at` and force a re-push)
  - new issues are NOT created by bulk push; that's an explicit
    per-row action deferred to a future ADR

Both modules now consume the Linear MCP client via a Context tag
(`SyncPull.Client` / `SyncPush.Client`) and read config from
`Config.Service`, instead of pulling module-level state.

Tests: 5/5 pass. The integration test in src/linear/ is deleted — it
targeted the old Session.Todo.Service and is superseded by the focused
unit tests under src/issue/.

The shims in src/linear/ now re-export from src/issue/, so any
existing imports of `linear/sync-pull` and `linear/sync-push` keep
working. (These shims go away in step 10.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous mid-rewrite renamed session_todo → workspace_todo in the
GlobalStore but kept the Todo[] type. With the new workspace-scoped
Issue domain in the kernel, the type and keying both need to change:

- workspace_todo is now `{ [directory: string]: Issue[] }` (was sessionID
  keying of Todo[]).
- setSessionTodo → setWorkspaceTodo taking Issue[].
- bootstrapDirectory now calls sdk.issue.list() to populate the new key
  on each directory switch.
- global-sync.tsx now owns a refreshIssues(directory) callback that the
  event-reducer fires for `issue.*` bus events.
- session-composer-state now reads in-flight todos from sync.data.todo
  (unchanged) instead of mistakenly looking them up in workspace_todo.
- sync.tsx's todo() loader no longer cross-writes the per-session in-flight
  Todo[] into the workspace_todo (the cross-write was a leftover from
  the prior rename and never made sense once Todo and Issue are different
  shapes).

Reconciled to unblock the new SidebarTodo and SidebarLinear components
and to make app typecheck clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The TUI sidebar now mirrors the Desktop plugin set:

- New sidebar/issue.tsx renders the directory's IssueTable via the new
  /issue/list endpoint, with add / cycle-status / delete affordances
  and an L1/L2 expand-collapse tree. Works independently of Linear MCP.
- sidebar/linear.tsx is rewritten to read the Issue SDK directly and
  show sync state, project ID, and issue counts. Push/Pull buttons are
  placeholder toasts (the /linear-push and /linear-pull commands are
  the canonical entry points).
- plugin/internal.ts registers SidebarIssue in the sidebar slot list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old `src/linear/` module held the v1 session-scoped Linear
integration (mcp-client, sync-push, sync-pull, tool-names). It was
already superseded by `src/issue/` in step 3 and rewritten for the
workspace-scoped IssueTable in step 4, but the v1 files were left
in place during the migration.

The v1 file bodies are no longer referenced from production code
(mcp-client/sync-pull/sync-push/tool-names were copied to issue/ in
step 3). README and todo.md references are updated to point at
src/issue/.

Not included in this commit (deferred follow-up):
- Reverting TodoTable workspace-scoped columns. That would require
  stripping Todo.Service, the auto-progress engine, and the
  todo_add/update/delete/reorder/status/list tools down to a flat
  per-session list, plus a schema migration that drops the
  hierarchy + Linear fields. The IssueTable now owns the
  hierarchy/Linear feature; the in-session todo is a parallel
  system and can be pared down in a follow-up PR without blocking
  this feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds six workspace-scoped issue tools that agents can call to manage
Linear-style todos directly through the kernel, in addition to
using the Linear MCP server when connected:

  - issue_list    — list with optional tree/status/priority filter
  - issue_add     — create with title/content/description/parent/level
  - issue_update  — patch any field
  - issue_delete  — remove
  - issue_status  — change status (cycle, set, etc.)
  - issue_reorder — change position/parent (drag/drop)

Each tool is wired into ToolRegistry alongside the existing todo_*
tools. The kernel layer (Issue.Service) is the source of truth;
Linear MCP remains an optional sync path that targets the same
table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Desktop app's sidebar now hosts a workspace-scoped "Todos" panel
that works independently of Linear MCP. It uses the new /issue/list
endpoint via useGlobalSDK and renders the L1/L2 hierarchy with
status/priority sort, refresh, and add affordances. The Linear
panel sits beneath it and shows connection state, sync metadata,
issue count, and Push/Pull actions.

The panel derives its directory from the active route's currentDir
(so the Linear config and todos are loaded from the directory the
user opened, not the git worktree root).

Also includes:
  - Dialog tweaks for the todo editor + Linear config
  - i18n keys (en.ts) for the new strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the new Issue.Service into the kernel:

- packages/opencode/src/server/instance/issue.ts — Hono routes for
  the new /issue endpoints (list, create, get, update, delete,
  reorder, patch-status), each tagged with describeRoute +
  operationId so the SDK generator can pick them up.
- packages/opencode/src/server/instance/index.ts — mounts the new
  routes alongside the existing session routes.
- packages/opencode/src/issue/auto-progress.ts — workspace-scoped
  AutoProgress service. When the user toggles an L1 to in_progress
  the L2 children for that parent are auto-rolled to in_progress
  and the next L1 is queued; completed L1 cascades to its L2s.
- packages/opencode/src/effect/app-runtime.ts — provides
  Issue.Service + AutoProgress.Service to the kernel layers.
- Sync pull/push test updates for the new shape and directory arg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BoringLink and others added 4 commits June 23, 2026 01:19
After the new /issue/* endpoints and the Issue schema (with
parent_id, level, title, description, labels, due_date, priority,
status, etc.) land, the OpenAPI spec and the generated client
need a regen. This is the output of:

  ./script/generate.ts

The JS SDK now exposes client.issue.{list,create,get,update,delete,
reorder,patchStatus}, and the v2 types include Issue.Info and
friends. The Desktop app and TUI already target the new shape.

Also in this commit:
- TUI command/linear.ts and sidebar/todo.tsx: small tweaks to keep
  the session todo sidebar and slash commands consistent with the
  new Issue event names.
- Session test updates for the new auto-progress + prompt-effect
  snapshots.
- migration/<timestamp>_add_issue_table snapshot refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Confirms that all six issue_* tools (issue_list, issue_add,
issue_update, issue_delete, issue_status, issue_reorder) are
actually registered after the registry wiring in step 7. This
guards against a future refactor dropping one of them silently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bun.lock: regenerate after the new issue tools and SDK regen.
- AGENTS.md / OPENCODE_TODO_LINEAR_GUIDE.md: add the workspace-scoped
  Todo/Linear overview at the repo root.
- docs/adr/0001..0003 + glossary: small wording fixes after the
  step 10 cleanup (legacy linear/ now gone, sync targets the
  IssueTable).
- packages/opencode/test/session/{auto-progress,prompt-effect,
  snapshot-tool-race}.test.ts: align fixtures with the new
  hierarchy + AutoProgress shape.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The migration generated for the new IssueTable contained redundant
`ALTER TABLE todo ADD COLUMN` statements before the `__new_todo`
rebuild. These statements fail in SQLite because adding `id text NOT NULL`
without a DEFAULT is rejected on a non-empty table, blocking the backend
from booting.

The downstream rebuild via `__new_todo` already recreates the table with
all the extended columns and the new (session_id, id) primary key, so
the ALTER TABLE statements were dead code. Removed them.

The INSERT INTO `__new_todo` SELECT FROM `todo` previously omitted the
new `id` column, which is NOT NULL with no default — that would have
tripped the NOT NULL constraint once the ALTER statements were removed.
Updated the SELECT to generate a random hex blob per row
(`lower(hex(randomblob(16)))`) so existing rows land a usable id.

Verified by booting `bun run --conditions=browser ./src/index.ts serve`
against a freshly-created database — startup completes without
migration errors and the issue table comes up empty as expected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@BoringLink BoringLink requested a review from adamdotdevin as a code owner June 22, 2026 18:26
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Jun 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

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.

2 participants