feat: workspace-scoped Todo/Linear sidebar (ADR-0003)#33410
Open
BoringLink wants to merge 34 commits into
Open
feat: workspace-scoped Todo/Linear sidebar (ADR-0003)#33410BoringLink wants to merge 34 commits into
BoringLink wants to merge 34 commits into
Conversation
…cs, and JSDoc for public APIs
…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>
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>
Contributor
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #N/A (no upstream issue — implementing the
Todo/Linearsidebar that was planned in the project'sOPENCODE_LINEAR_INTEGRATION.mddesign doc; targets the 2.0 line, see "Base branch" below).Base branch
Type of change
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:
IssueTable+ CRUD service + UI sidebar, so todos are first-class regardless of Linear.IssueTablethe sidebar reads.Mechanically, this PR introduces
IssueTable(workspace-scoped SQLite, snake_case, with the full Linear status set + L1/L2 hierarchy), anIssue.Servicethat publishesissue.*bus events, a workspace-scopedIssue.AutoProgressengine,/issue/*HTTP routes, sixissue_*tools the agent can call without Linear MCP, aSidebarTodocomponent in the Desktop sidebar (and a TUI equivalent), and a rewrite ofSidebarLinearto read the new Issue SDK. The legacypackages/opencode/src/linear/module is removed.I understand why this works: the previous
TodoTablewas 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 newIssueTableis keyed ondirectory, 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 (directoryfor the workspace scope,idfor 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 newIssueTable/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.origin/2.0base:prompt-effect.test.tsis 0/34 pass without this branch, 19/34 pass with it. The branch fixes some pre-existing failures via the test-fixtureLayer.provide(AutoProgress.defaultLayer)/Layer.provide(Issue.defaultLayer)wiring.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).GET /issuereturns[],POST /issuecreates an issue,GET /issuereturns the created issue,GET /issue/{id}returns it again,POST /issue/{id}/statusupdates the status. The fix-migration test on a fresh DB succeeds.2328738a9d fix(opencode): repair add_issue_table migration).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
packages/{cli,core,server,tui}/AGENTS.mdfiles in the worktree are pre-existing placeholders from the 2.0 line, not part of this PR)feat(opencode):,feat(app):,feat(tui):,fix(app):,chore(opencode):,chore(sdk):,test(opencode):,docs:issuetable + rebuild oftodotable via__new_todopattern with proper defaults — SQLite-safe, noALTER TABLE ADD COLUMN NOT NULLwithout DEFAULT)packages/opencode/AGENTS.md(snake_case Drizzle columns,Effect.fn("Domain.method")for tracing, noelse/try/catch/any, single-word names)Deferred follow-up
Reverting the existing in-session
TodoTableworkspace-scoped columns would require strippingTodo.Service, the auto-progress engine, and thetodo_*tools down to a flat per-session list, plus a schema migration. TheIssueTablenow 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.