Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new first-party Linear plugin that syncs Linear issues with Peekoo tasks, plus SDK/host/UI wiring to support plugin-driven task operations and surfaced integration status in the desktop app.
Changes:
- Introduces
plugins/linear(Rust WASM plugin + panel UI) implementing API-key auth, periodic/manual sync, and a settings panel snapshot provider. - Extends
peekoo-plugin-sdkwith atasksmodule and new host function bindings for task CRUD/toggle/assign. - Updates desktop UI to surface Linear integration status in Settings and provide entry points to the Linear panel from Tasks.
Reviewed changes
Copilot reviewed 22 out of 25 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/linear/ui/panel.js | Implements Linear panel behavior (snapshot refresh, API key autosave, sync/settings controls). |
| plugins/linear/ui/panel.html | Adds Linear plugin panel UI markup. |
| plugins/linear/ui/panel.css | Adds styling for the Linear plugin panel. |
| plugins/linear/src/lib.rs | Implements Linear API integration, state, syncing, and tool/data provider endpoints. |
| plugins/linear/peekoo-plugin.toml | Declares Linear plugin metadata, permissions, tools, data providers, and panel entry. |
| plugins/linear/Cargo.toml | Defines the Linear plugin crate for WASM build. |
| justfile | Adds linear to plugin-build-all. |
| docs/plans/2026-03-31-linear-task-plugin-integration-design.md | Adds design doc for Linear integration approach and scope. |
| docs/plans/2026-03-31-linear-integration-manual-qa.md | Adds manual QA checklist for Linear integration. |
| crates/peekoo-plugin-sdk/src/tasks.rs | Adds safe SDK wrappers around host task APIs for plugins. |
| crates/peekoo-plugin-sdk/src/lib.rs | Exports the new tasks module in the SDK and peekoo namespace. |
| crates/peekoo-plugin-sdk/src/host_fns.rs | Adds raw host function bindings for task operations. |
| crates/peekoo-plugin-sdk/Cargo.lock | Adds a workspace-local lockfile for the SDK crate. |
| crates/peekoo-plugin-host/src/registry.rs | Increases default plugin runtime timeout. |
| apps/desktop-ui/src/features/tasks/TasksPanel.tsx | Adds a “Sources -> Linear” button to open the Linear panel when available. |
| apps/desktop-ui/src/features/tasks/components/TaskLabelPills.tsx | Adjusts label pill coloring logic. |
| apps/desktop-ui/src/features/settings/useLinearIntegrationStatus.ts | Adds a hook to poll Linear connection status via plugin data provider. |
| apps/desktop-ui/src/features/settings/SettingsPanel.tsx | Adds an Integrations section showing Linear status details. |
| apps/desktop-ui/src/components/sprite/SpriteActionMenu.tsx | Excludes Linear from the sprite action menu plugin list. |
| apps/desktop-tauri/src-tauri/Cargo.toml | Enables the macos-private-api Tauri feature. |
| ai/memories/changelogs/202603311740-feat-linear-plugin-foundation.md | Adds an internal changelog entry for the Linear plugin foundation. |
| ai/memories/changelogs/202603311644-feat-linear-api-key-auth.md | Adds an internal changelog entry for switching Linear auth to API key. |
| ai/memories/changelogs/202603311620-docs-linear-task-plugin-integration-design.md | Adds an internal changelog entry for the Linear design doc. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/desktop-ui/src/features/tasks/components/TaskLabelPills.tsx
Outdated
Show resolved
Hide resolved
| {visibleLabels.map((label) => { | ||
| const predefined = PREDEFINED_LABELS.find((l) => l.name === label); | ||
| const color = predefined?.color ?? getLabelColor(label); | ||
| const backgroundColor = withAlpha(color, 0.18); |
There was a problem hiding this comment.
@0xRichardH 这个文件的修改,主要是解决 tasks 插件已有的一个错误,同步下来的 linear task, 在 task 标签显示存在背景色和文字颜色一样的问题
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 15 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [[tools.definitions]] | ||
| name = "linear_set_sync_settings" | ||
| description = "Update Linear sync settings" | ||
| parameters = '{"type":"object","properties":{"defaultTeamId":{"type":"string"},"autoPushNewTasks":{"type":"boolean"}},"required":[]}' |
There was a problem hiding this comment.
linear_set_sync_settings tool schema doesn’t include fields that the panel actually sends (assigneeId and syncStateNames). If the host validates tool arguments against this JSON schema, the panel’s auto-save calls can fail/reject. Update the parameters schema to include these properties (and their types) so it matches the Rust handler/panel payload.
| parameters = '{"type":"object","properties":{"defaultTeamId":{"type":"string"},"autoPushNewTasks":{"type":"boolean"}},"required":[]}' | |
| parameters = '{"type":"object","properties":{"defaultTeamId":{"type":"string"},"autoPushNewTasks":{"type":"boolean"},"assigneeId":{"type":"string"},"syncStateNames":{"type":"array","items":{"type":"string"}}},"required":[]}' |
| let sync_result = run_sync_cycle(force, mode, &api_key, &mut state); | ||
| match sync_result { | ||
| Ok(summary) => { | ||
| let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); | ||
| state.connection.status = "connected".to_string(); | ||
| state.sync.last_sync_at = Some(now.clone()); | ||
| state.sync.last_pull_cursor = Some(now.clone()); | ||
| state.sync.last_push_cursor = Some(now.clone()); |
There was a problem hiding this comment.
On successful sync you set both last_pull_cursor and last_push_cursor to now regardless of SyncMode. A manual PushOnly sync will advance last_pull_cursor without pulling, which can cause subsequent pulls to skip remote updates. Only advance the cursor(s) for the operations that actually ran (and consider keeping the other cursor unchanged).
| let sync_result = run_sync_cycle(force, mode, &api_key, &mut state); | |
| match sync_result { | |
| Ok(summary) => { | |
| let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); | |
| state.connection.status = "connected".to_string(); | |
| state.sync.last_sync_at = Some(now.clone()); | |
| state.sync.last_pull_cursor = Some(now.clone()); | |
| state.sync.last_push_cursor = Some(now.clone()); | |
| let ran_pull = !matches!(mode, SyncMode::PushOnly); | |
| let ran_push = !matches!(mode, SyncMode::PullOnly); | |
| let sync_result = run_sync_cycle(force, mode, &api_key, &mut state); | |
| match sync_result { | |
| Ok(summary) => { | |
| let now = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true); | |
| state.connection.status = "connected".to_string(); | |
| state.sync.last_sync_at = Some(now.clone()); | |
| if ran_pull { | |
| state.sync.last_pull_cursor = Some(now.clone()); | |
| } | |
| if ran_push { | |
| state.sync.last_push_cursor = Some(now.clone()); | |
| } |
| }; | ||
|
|
||
| summary.pulled = pull_remote_into_local(&filtered_issues, state, &mut local_tasks)?; |
There was a problem hiding this comment.
Using now as the pull/push cursor can permanently skip work when the run is capped by page_limit/issue_limit (e.g., if >200 issues changed since the last cursor, you truncate but still advance the cursor past the unprocessed changes). Instead, advance the cursor to the max updatedAt you actually processed (or persist/paginate using a Linear page cursor) so remaining updates are picked up in the next run.
| }; | |
| summary.pulled = pull_remote_into_local(&filtered_issues, state, &mut local_tasks)?; | |
| }; | |
| let last_processed_pull_cursor = filtered_issues | |
| .iter() | |
| .map(|issue| issue.updated_at.as_str()) | |
| .max() | |
| .map(str::to_owned); | |
| summary.pulled = pull_remote_into_local(&filtered_issues, state, &mut local_tasks)?; | |
| if let Some(cursor) = last_processed_pull_cursor { | |
| state.sync.last_pull_cursor = Some(cursor); | |
| } |
| state.connection = ConnectionState::default(); | ||
| state.sync = SyncState::default(); |
There was a problem hiding this comment.
linear_disconnect resets connection/sync state but keeps mappings (and cached teams/users/preferences). If the user reconnects with a different Linear account/workspace, stale mappings can cause updates/creates to target non-existent issues or leak cross-account linkage. Clear mappings and cached remote data (and potentially preferences) on disconnect.
| state.connection = ConnectionState::default(); | |
| state.sync = SyncState::default(); | |
| state = Default::default(); |


https://github.com/feed-mob/tracking_admin/issues/22071
What changed
Release notes
feature,fix,docs,test,chore,ci, orrefactorskip-changelogif this PR should be excluded from generated release notesVerification