-
Notifications
You must be signed in to change notification settings - Fork 0
add tasks source linear #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| ## 2026-03-31 16:20: docs: add Linear task plugin integration design | ||
|
|
||
| **What changed:** | ||
| - Added a new product/technical design document for Linear task integration as an independent plugin. | ||
| - Defined scope for OAuth connection, bidirectional task sync, and connection status management in Settings. | ||
| - Documented architecture decisions aligned with current plugin runtime, including required plugin permissions and SDK task wrapper extension. | ||
| - Added phased implementation plan, sync/conflict strategy, risks, and acceptance mapping. | ||
|
|
||
| **Why:** | ||
| - Provide a concrete implementation blueprint before coding the Linear integration. | ||
| - Ensure requirements are explicit: plugin install-gated behavior, two-way sync, and visible connection state. | ||
|
|
||
| **Files affected:** | ||
| - `docs/plans/2026-03-31-linear-task-plugin-integration-design.md` | ||
| - `ai/memories/changelogs/202603311620-docs-linear-task-plugin-integration-design.md` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| ## 2026-03-31 16:44: feat: switch Linear integration auth from OAuth to API key | ||
|
|
||
| **What changed:** | ||
| - Reworked `plugins/linear` authentication flow from OAuth to API key secret storage (`linear-api-key`). | ||
| - Replaced panel connect UX with API key input/save flow, keeping disconnect and manual sync actions. | ||
| - Updated plugin manifest tools to `linear_set_api_key` and removed OAuth-specific permissions/tool surface. | ||
| - Updated integration design and manual QA docs to align with API key onboarding, validation, and failure paths. | ||
| - Rebuilt and installed latest plugin artifact to `~/.peekoo/plugins/linear` for immediate local validation. | ||
|
|
||
| **Why:** | ||
| - Match product requirement update to use Linear personal/team API keys. | ||
| - Simplify desktop setup by avoiding browser callback/OAuth token lifecycle handling. | ||
|
|
||
| **Files affected:** | ||
| - `plugins/linear/peekoo-plugin.toml` | ||
| - `plugins/linear/src/lib.rs` | ||
| - `plugins/linear/ui/panel.html` | ||
| - `plugins/linear/ui/panel.js` | ||
| - `docs/plans/2026-03-31-linear-task-plugin-integration-design.md` | ||
| - `docs/plans/2026-03-31-linear-integration-manual-qa.md` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| ## 2026-03-31 17:40: feat: add Linear plugin foundation with task sync and settings status | ||
|
|
||
| **What changed:** | ||
| - Added a new first-party plugin at `plugins/linear` with OAuth connect/disconnect, panel snapshot provider, manual sync trigger, periodic scheduler sync, and two-way task sync scaffolding against Linear GraphQL. | ||
| - Added a new `tasks` module in `crates/peekoo-plugin-sdk` and wired raw host function bindings so plugins can create/list/update/delete/toggle/assign Peekoo tasks safely. | ||
| - Added a new settings hook and UI section to show Linear integration status in Settings, including install/enable/connected/sync/error surfaces. | ||
| - Updated `justfile` to include `linear` in `plugin-build-all`. | ||
|
|
||
| **Why:** | ||
| - Implement the approved design direction for Linear as an install-gated independent plugin. | ||
| - Enable plugin-side background synchronization without coupling Linear logic into core app crates. | ||
| - Satisfy acceptance criteria that connection status is visible in Settings. | ||
|
|
||
| **Files affected:** | ||
| - `plugins/linear/Cargo.toml` | ||
| - `plugins/linear/peekoo-plugin.toml` | ||
| - `plugins/linear/src/lib.rs` | ||
| - `plugins/linear/ui/panel.html` | ||
| - `plugins/linear/ui/panel.css` | ||
| - `plugins/linear/ui/panel.js` | ||
| - `crates/peekoo-plugin-sdk/src/host_fns.rs` | ||
| - `crates/peekoo-plugin-sdk/src/tasks.rs` | ||
| - `crates/peekoo-plugin-sdk/src/lib.rs` | ||
| - `apps/desktop-ui/src/features/settings/useLinearIntegrationStatus.ts` | ||
| - `apps/desktop-ui/src/features/settings/SettingsPanel.tsx` | ||
| - `justfile` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,17 +17,23 @@ export function TaskLabelPills({ labels, maxVisible = 3 }: TaskLabelPillsProps) | |
| {visibleLabels.map((label) => { | ||
| const predefined = PREDEFINED_LABELS.find((l) => l.name === label); | ||
| const color = predefined?.color ?? getLabelColor(label); | ||
| const backgroundColor = withAlpha(color, 0.18); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @0xRichardH 这个文件的修改,主要是解决 tasks 插件已有的一个错误,同步下来的 linear task, 在 task 标签显示存在背景色和文字颜色一样的问题 |
||
| const borderColor = withAlpha(color, 0.35); | ||
|
|
||
| return ( | ||
| <span | ||
| key={label} | ||
| className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-medium leading-none" | ||
| style={{ | ||
| backgroundColor: `${color}20`, | ||
| color, | ||
| border: `1px solid ${color}40`, | ||
| backgroundColor, | ||
| color: "var(--text-primary)", | ||
| border: `1px solid ${borderColor}`, | ||
| }} | ||
| > | ||
| <span | ||
| className="mr-1 inline-block h-1.5 w-1.5 rounded-full" | ||
| style={{ backgroundColor: color }} | ||
| /> | ||
| {label} | ||
| </span> | ||
| ); | ||
|
|
@@ -38,3 +44,30 @@ export function TaskLabelPills({ labels, maxVisible = 3 }: TaskLabelPillsProps) | |
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function withAlpha(color: string, alpha: number): string { | ||
| if (color.startsWith("#")) { | ||
| const hex = color.slice(1); | ||
| const normalized = | ||
| hex.length === 3 | ||
| ? hex | ||
| .split("") | ||
| .map((part) => part + part) | ||
| .join("") | ||
| : hex; | ||
| const r = Number.parseInt(normalized.slice(0, 2), 16); | ||
| const g = Number.parseInt(normalized.slice(2, 4), 16); | ||
| const b = Number.parseInt(normalized.slice(4, 6), 16); | ||
| return `rgba(${r}, ${g}, ${b}, ${alpha})`; | ||
| } | ||
|
|
||
| if (color.startsWith("hsl(") && color.endsWith(")")) { | ||
| return color.replace(/^hsl\((.*)\)$/, `hsla($1, ${alpha})`); | ||
| } | ||
|
|
||
| if (color.startsWith("rgb(") && color.endsWith(")")) { | ||
| return color.replace(/^rgb\((.*)\)$/, `rgba($1, ${alpha})`); | ||
| } | ||
|
|
||
| return color; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| //! Task operations for plugins. | ||
| //! | ||
| //! Requires the `tasks` permission in `peekoo-plugin.toml`. | ||
|
|
||
| use extism_pdk::{Error, Json}; | ||
| use serde::de::DeserializeOwned; | ||
| use serde_json::{json, Value}; | ||
|
|
||
| use crate::host_fns::{ | ||
| peekoo_task_assign, peekoo_task_create, peekoo_task_delete, peekoo_task_list, | ||
| peekoo_task_toggle, peekoo_task_update, TaskListRequest, | ||
| }; | ||
|
|
||
| fn decode<T: DeserializeOwned>(value: Value, op: &str) -> Result<T, Error> { | ||
| serde_json::from_value(value).map_err(|e| Error::msg(format!("{op} decode error: {e}"))) | ||
| } | ||
|
|
||
| pub fn create<T: DeserializeOwned>(payload: Value) -> Result<T, Error> { | ||
| let response = unsafe { peekoo_task_create(Json(payload))? }; | ||
| decode(response.0, "tasks::create") | ||
| } | ||
|
|
||
| pub fn list<T: DeserializeOwned>(status_filter: Option<&str>) -> Result<Vec<T>, Error> { | ||
| let response = unsafe { | ||
| peekoo_task_list(Json(TaskListRequest { | ||
| status_filter: status_filter.map(ToString::to_string), | ||
| }))? | ||
| }; | ||
| decode(response.0, "tasks::list") | ||
| } | ||
|
|
||
| pub fn update<T: DeserializeOwned>(payload: Value) -> Result<T, Error> { | ||
| let response = unsafe { peekoo_task_update(Json(payload))? }; | ||
| decode(response.0, "tasks::update") | ||
| } | ||
|
|
||
| pub fn delete(id: &str) -> Result<bool, Error> { | ||
| let response = unsafe { peekoo_task_delete(Json(json!({ "id": id })))? }; | ||
| Ok(response.0.ok) | ||
| } | ||
|
|
||
| pub fn toggle<T: DeserializeOwned>(id: &str) -> Result<T, Error> { | ||
| let response = unsafe { peekoo_task_toggle(Json(json!({ "id": id })))? }; | ||
| decode(response.0, "tasks::toggle") | ||
| } | ||
|
|
||
| pub fn assign<T: DeserializeOwned>(id: &str, assignee: &str) -> Result<T, Error> { | ||
| let response = unsafe { | ||
| peekoo_task_assign(Json(json!({ | ||
| "id": id, | ||
| "assignee": assignee, | ||
| })))? | ||
| }; | ||
| decode(response.0, "tasks::assign") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| [package] | ||
| name = "linear" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
|
|
||
| [workspace] | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib", "rlib"] | ||
|
|
||
| [dependencies] | ||
| chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } | ||
| serde = { version = "1", features = ["derive"] } | ||
| serde_json = "1" | ||
| peekoo-plugin-sdk = { path = "../../crates/peekoo-plugin-sdk" } | ||
| extism-pdk = "1.4" |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,80 @@ | ||||||||||
| [plugin] | ||||||||||
| key = "linear" | ||||||||||
| name = "Linear" | ||||||||||
| version = "0.1.2" | ||||||||||
| author = "Peekoo Team" | ||||||||||
| description = "Sync Linear issues with Peekoo tasks." | ||||||||||
| min_peekoo_version = "0.1.0" | ||||||||||
| wasm = "linear-v012.wasm" | ||||||||||
kaiji95 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
|
||||||||||
| [permissions] | ||||||||||
| required = ["scheduler", "state:read", "state:write", "secrets:read", "secrets:write", "net:http", "tasks"] | ||||||||||
| allowed_hosts = ["api.linear.app", "linear.app"] | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_set_api_key" | ||||||||||
| description = "Set and verify Linear API key" | ||||||||||
| parameters = '{"type":"object","properties":{"apiKey":{"type":"string"}},"required":["apiKey"]}' | ||||||||||
| return_type = "object" | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_disconnect" | ||||||||||
| description = "Disconnect the current Linear account" | ||||||||||
| parameters = '{"type":"object","properties":{},"required":[]}' | ||||||||||
| return_type = "object" | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_sync_now" | ||||||||||
| description = "Run one immediate two-way sync" | ||||||||||
| parameters = '{"type":"object","properties":{},"required":[]}' | ||||||||||
| return_type = "object" | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_sync_linear" | ||||||||||
| description = "Pull issues from Linear into Peekoo tasks" | ||||||||||
| parameters = '{"type":"object","properties":{},"required":[]}' | ||||||||||
| return_type = "object" | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_sync_local" | ||||||||||
| description = "Push local Peekoo tasks to Linear issues" | ||||||||||
| parameters = '{"type":"object","properties":{},"required":[]}' | ||||||||||
| return_type = "object" | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_sync_preview" | ||||||||||
| description = "Preview how many local tasks are pending for push to Linear" | ||||||||||
| parameters = '{"type":"object","properties":{},"required":[]}' | ||||||||||
| return_type = "object" | ||||||||||
|
|
||||||||||
| [[tools.definitions]] | ||||||||||
| name = "linear_set_sync_settings" | ||||||||||
| description = "Update Linear sync settings" | ||||||||||
| parameters = '{"type":"object","properties":{"defaultTeamId":{"type":"string"},"autoPushNewTasks":{"type":"boolean"}},"required":[]}' | ||||||||||
|
||||||||||
| 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":[]}' |
Copilot
AI
Apr 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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":[]}' |
Uh oh!
There was an error while loading. Please reload this page.