/ docs / todoist-integration-addendum.md
todoist-integration-addendum.md
   1  # Todoist Integration Addendum
   2  
   3  ## Nested Workspaces Browser Extension - Phase 10
   4  
   5  This addendum extends the master architecture with Todoist task management integration, enabling workspace-specific project synchronization and task CRUD operations within the dashboard.
   6  
   7  ---
   8  
   9  ## Overview
  10  
  11  The Todoist integration creates a bidirectional sync between browser workspaces and Todoist projects, organized under a master "Mnemonic" project. Users can view, create, edit, complete, and schedule tasks directly from the dashboard without leaving their workflow.
  12  
  13  ### Feature Summary
  14  
  15  | Capability | Description |
  16  |------------|-------------|
  17  | **Project Mapping** | Each workspace maps to a Todoist project under "Mnemonic" master |
  18  | **Hierarchical Structure** | Parent/child workspace relationships preserved in Todoist |
  19  | **Task CRUD** | Full create, read, update, delete operations |
  20  | **Due Dates** | Natural language scheduling ("tomorrow", "next Monday") |
  21  | **Priority Levels** | P1-P4 priority support with visual indicators |
  22  | **Completion Tracking** | Toggle completion with sync back to Todoist |
  23  | **Labels** | Workspace-aware labels for cross-cutting concerns |
  24  
  25  ---
  26  
  27  ## Official SDK
  28  
  29  ### Package Information
  30  
  31  The current official Todoist client for JavaScript/TypeScript is the `@doist/todoist-api-typescript` package, which targets the REST API v2 and is actively maintained.
  32  
  33  | Property | Value |
  34  |----------|-------|
  35  | **Package** | `@doist/todoist-api-typescript` |
  36  | **Version** | 6.2.1 (as of December 2024) |
  37  | **License** | MIT |
  38  | **API Version** | REST API v2 / Todoist API v1 |
  39  | **TypeScript** | Full type definitions included |
  40  
  41  ### Installation
  42  
  43  ```bash
  44  npm install @doist/todoist-api-typescript
  45  ```
  46  
  47  ### Documentation Resources
  48  
  49  | Resource | URL |
  50  |----------|-----|
  51  | GitHub Repository | https://github.com/Doist/todoist-api-typescript |
  52  | API Documentation | https://doist.github.io/todoist-api-typescript/ |
  53  | REST API Reference | https://developer.todoist.com/rest/v2/ |
  54  | Authorization Guide | https://developer.todoist.com/guides/ |
  55  
  56  ### Basic Usage
  57  
  58  ```typescript
  59  import { TodoistApi } from '@doist/todoist-api-typescript'
  60  
  61  const api = new TodoistApi('YOUR_API_TOKEN')
  62  
  63  // Fetch all tasks
  64  api.getTasks()
  65    .then((tasks) => console.log(tasks))
  66    .catch((error) => console.log(error))
  67  
  68  // Create a task
  69  api.addTask({
  70    content: 'Review documentation',
  71    dueString: 'tomorrow at 10am',
  72    priority: 4,  // P1 = 4, P2 = 3, P3 = 2, P4 = 1
  73    projectId: '2203306141'
  74  })
  75  ```
  76  
  77  ### Browser Extension Compatibility
  78  
  79  The SDK supports custom HTTP implementations for environments with specific networking requirements. This is critical for browser extensions:
  80  
  81  ```typescript
  82  import { TodoistApi } from '@doist/todoist-api-typescript'
  83  
  84  // Browser extension compatible initialization
  85  const api = new TodoistApi('YOUR_TOKEN', {
  86    customFetch: async (url, options) => {
  87      // Use browser extension's fetch or background script messaging
  88      const response = await fetch(url, options)
  89      return {
  90        ok: response.ok,
  91        status: response.status,
  92        statusText: response.statusText,
  93        headers: Object.fromEntries(response.headers.entries()),
  94        text: () => response.text(),
  95        json: () => response.json(),
  96      }
  97    }
  98  })
  99  ```
 100  
 101  ---
 102  
 103  ## Architecture
 104  
 105  ### System Diagram
 106  
 107  ```
 108  ┌─────────────────────────────────────────────────────────────────────────────┐
 109  │  Dashboard Tab                                                               │
 110  │  ┌─────────────────────────────────────────────────────────────────────────┐│
 111  │  │  TodoistCard.svelte                                                     ││
 112  │  │  ┌─────────────────────────────────────────────────────────────────┐   ││
 113  │  │  │  📋 Python Research Tasks                    [+ Add] [⟳] [⚙]   │   ││
 114  │  │  │  ───────────────────────────────────────────────────────────────│   ││
 115  │  │  │  ☐ Review asyncio documentation       📅 Today     ●●●○        │   ││
 116  │  │  │  ☐ Write unit tests                   📅 Tomorrow  ●●○○        │   ││
 117  │  │  │  ☑ Set up project structure           📅 Dec 10    ●○○○        │   ││
 118  │  │  │  ───────────────────────────────────────────────────────────────│   ││
 119  │  │  │  [Show completed ▾]  3 of 5 tasks                               │   ││
 120  │  │  └─────────────────────────────────────────────────────────────────┘   ││
 121  │  └─────────────────────────────────────────────────────────────────────────┘│
 122  └─────────────────────────────────────────────────────────────────────────────┘
 123 124 125  ┌─────────────────────────────────────────────────────────────────────────────┐
 126  │  Todoist Service Layer (src/lib/todoist/)                                    │
 127  │  ┌───────────────────┐  ┌───────────────────┐  ┌───────────────────┐       │
 128  │  │ TodoistClient     │  │ WorkspaceSync     │  │ TaskStore         │       │
 129  │  │ (API wrapper)     │  │ (project mapping) │  │ (Svelte store)    │       │
 130  │  └─────────┬─────────┘  └─────────┬─────────┘  └─────────┬─────────┘       │
 131  │            │                      │                      │                  │
 132  │            └──────────────────────┼──────────────────────┘                  │
 133  │                                   ▼                                         │
 134  │  ┌──────────────────────────────────────────────────────────────────────┐  │
 135  │  │  @doist/todoist-api-typescript (Official SDK)                         │  │
 136  │  └──────────────────────────────────────────────────────────────────────┘  │
 137  └─────────────────────────────────────────────────────────────────────────────┘
 138 139 140                      ┌───────────────────────────────┐
 141                      │  Todoist REST API v2          │
 142                      │  https://api.todoist.com/     │
 143                      └───────────────────────────────┘
 144  ```
 145  
 146  ### Todoist Project Hierarchy
 147  
 148  The extension creates and maintains a project structure mirroring workspaces:
 149  
 150  ```
 151  Todoist Account:
 152  └── 📁 Mnemonic                          ← Master project (auto-created)
 153 154      ├── 📁 Python Research               ← Parent Workspace
 155      │   ├── 📁 AsyncIO Deep Dive         ← Child Workspace  
 156      │   └── 📁 FastAPI Project           ← Child Workspace
 157 158      ├── 📁 Trading Systems               ← Parent Workspace
 159      │   ├── 📁 REACT Development         ← Child Workspace
 160      │   ├── 📁 UAEC Controller           ← Child Workspace
 161      │   └── 📁 UCAR Risk Management      ← Child Workspace
 162 163      └── 📁 Browser Extension Dev         ← Standalone Workspace
 164  ```
 165  
 166  ---
 167  
 168  ## Type Definitions
 169  
 170  ```typescript
 171  // src/lib/todoist/types.ts
 172  
 173  import type { Task, Project, Section, Label, Comment } from '@doist/todoist-api-typescript'
 174  
 175  /**
 176   * Extended task with workspace context
 177   */
 178  export interface WorkspaceTask extends Task {
 179    workspaceId: string
 180    workspaceName: string
 181  }
 182  
 183  /**
 184   * Mapping between workspace and Todoist project
 185   */
 186  export interface WorkspaceProjectMapping {
 187    workspaceId: string
 188    workspaceName: string
 189    workspaceType: 'parent' | 'child' | 'standalone'
 190    todoistProjectId: string
 191    todoistProjectName: string
 192    parentProjectId?: string  // For child workspaces
 193    lastSynced: string        // ISO timestamp
 194  }
 195  
 196  /**
 197   * Todoist connection state
 198   */
 199  export interface TodoistConnectionState {
 200    connected: boolean
 201    apiToken?: string
 202    masterProjectId?: string
 203    masterProjectName: string
 204    lastSync?: string
 205    error?: string
 206  }
 207  
 208  /**
 209   * Task creation request
 210   */
 211  export interface CreateTaskRequest {
 212    content: string
 213    description?: string
 214    dueString?: string       // Natural language: "tomorrow", "next Monday 3pm"
 215    dueDate?: string         // ISO date: "2024-12-25"
 216    dueDatetime?: string     // ISO datetime: "2024-12-25T10:00:00"
 217    priority?: 1 | 2 | 3 | 4 // 4 = P1 (highest), 1 = P4 (lowest)
 218    labels?: string[]
 219    sectionId?: string
 220  }
 221  
 222  /**
 223   * Task update request
 224   */
 225  export interface UpdateTaskRequest {
 226    taskId: string
 227    content?: string
 228    description?: string
 229    dueString?: string
 230    dueDate?: string
 231    dueDatetime?: string
 232    priority?: 1 | 2 | 3 | 4
 233    labels?: string[]
 234  }
 235  
 236  /**
 237   * Task filter options
 238   */
 239  export interface TaskFilter {
 240    showCompleted: boolean
 241    priority?: 1 | 2 | 3 | 4
 242    dueBefore?: string
 243    dueAfter?: string
 244    labels?: string[]
 245  }
 246  
 247  /**
 248   * Sync result
 249   */
 250  export interface SyncResult {
 251    success: boolean
 252    tasksUpdated: number
 253    tasksFailed: number
 254    projectsCreated: number
 255    errors: string[]
 256  }
 257  
 258  /**
 259   * Priority display configuration
 260   */
 261  export const PRIORITY_CONFIG = {
 262    4: { label: 'P1', color: '#d1453b', dots: 4 },  // Highest
 263    3: { label: 'P2', color: '#eb8909', dots: 3 },
 264    2: { label: 'P3', color: '#246fe0', dots: 2 },
 265    1: { label: 'P4', color: '#666666', dots: 1 },  // Lowest
 266  } as const
 267  ```
 268  
 269  ---
 270  
 271  ## Todoist Client Wrapper
 272  
 273  Wraps the official SDK with extension-specific functionality:
 274  
 275  ```typescript
 276  // src/lib/todoist/client.ts
 277  
 278  import { TodoistApi } from '@doist/todoist-api-typescript'
 279  import type { Task, Project, Section, Label } from '@doist/todoist-api-typescript'
 280  import type { 
 281    TodoistConnectionState, 
 282    CreateTaskRequest, 
 283    UpdateTaskRequest,
 284    WorkspaceProjectMapping 
 285  } from './types'
 286  import { settingsStore } from '../storage/settings'
 287  
 288  const MASTER_PROJECT_NAME = 'Mnemonic'
 289  
 290  /**
 291   * Todoist API client wrapper for browser extension
 292   */
 293  export class TodoistClient {
 294    private api: TodoistApi | null = null
 295    private masterProjectId: string | null = null
 296    
 297    /**
 298     * Initialize client with API token
 299     */
 300    async initialize(apiToken: string): Promise<TodoistConnectionState> {
 301      try {
 302        // Create API instance with browser-compatible fetch
 303        this.api = new TodoistApi(apiToken, {
 304          customFetch: this.createExtensionFetch()
 305        })
 306        
 307        // Verify connection by fetching projects
 308        const projects = await this.api.getProjects()
 309        
 310        // Find or create master project
 311        let masterProject = projects.find(p => p.name === MASTER_PROJECT_NAME && !p.parentId)
 312        
 313        if (!masterProject) {
 314          masterProject = await this.api.addProject({ name: MASTER_PROJECT_NAME })
 315        }
 316        
 317        this.masterProjectId = masterProject.id
 318        
 319        return {
 320          connected: true,
 321          apiToken,
 322          masterProjectId: masterProject.id,
 323          masterProjectName: masterProject.name,
 324          lastSync: new Date().toISOString(),
 325        }
 326      } catch (error) {
 327        return {
 328          connected: false,
 329          masterProjectName: MASTER_PROJECT_NAME,
 330          error: error instanceof Error ? error.message : 'Connection failed',
 331        }
 332      }
 333    }
 334    
 335    /**
 336     * Create browser extension compatible fetch wrapper
 337     */
 338    private createExtensionFetch() {
 339      return async (url: string, options?: RequestInit & { timeout?: number }) => {
 340        const controller = new AbortController()
 341        const timeout = options?.timeout ?? 30000
 342        
 343        const timeoutId = setTimeout(() => controller.abort(), timeout)
 344        
 345        try {
 346          const response = await fetch(url, {
 347            ...options,
 348            signal: controller.signal,
 349          })
 350          
 351          return {
 352            ok: response.ok,
 353            status: response.status,
 354            statusText: response.statusText,
 355            headers: Object.fromEntries(response.headers.entries()),
 356            text: () => response.text(),
 357            json: () => response.json(),
 358          }
 359        } finally {
 360          clearTimeout(timeoutId)
 361        }
 362      }
 363    }
 364    
 365    /**
 366     * Check if client is connected
 367     */
 368    isConnected(): boolean {
 369      return this.api !== null && this.masterProjectId !== null
 370    }
 371    
 372    /**
 373     * Get master project ID
 374     */
 375    getMasterProjectId(): string | null {
 376      return this.masterProjectId
 377    }
 378    
 379    // ─────────────────────────────────────────────────────────────────────────
 380    // Project Operations
 381    // ─────────────────────────────────────────────────────────────────────────
 382    
 383    /**
 384     * Get all projects under master Mnemonic project
 385     */
 386    async getWorkspaceProjects(): Promise<Project[]> {
 387      if (!this.api || !this.masterProjectId) {
 388        throw new Error('Todoist client not initialized')
 389      }
 390      
 391      const allProjects = await this.api.getProjects()
 392      
 393      // Filter to only Mnemonic children and grandchildren
 394      const mnemonicChildren = allProjects.filter(p => p.parentId === this.masterProjectId)
 395      const mnemonicGrandchildren = allProjects.filter(p => 
 396        mnemonicChildren.some(child => child.id === p.parentId)
 397      )
 398      
 399      return [...mnemonicChildren, ...mnemonicGrandchildren]
 400    }
 401    
 402    /**
 403     * Create a project for a workspace
 404     */
 405    async createWorkspaceProject(
 406      workspaceName: string,
 407      workspaceType: 'parent' | 'child' | 'standalone',
 408      parentWorkspaceProjectId?: string
 409    ): Promise<Project> {
 410      if (!this.api || !this.masterProjectId) {
 411        throw new Error('Todoist client not initialized')
 412      }
 413      
 414      const parentId = workspaceType === 'child' && parentWorkspaceProjectId
 415        ? parentWorkspaceProjectId
 416        : this.masterProjectId
 417      
 418      return await this.api.addProject({
 419        name: workspaceName,
 420        parentId,
 421      })
 422    }
 423    
 424    /**
 425     * Update project name
 426     */
 427    async updateProject(projectId: string, name: string): Promise<Project> {
 428      if (!this.api) throw new Error('Todoist client not initialized')
 429      return await this.api.updateProject(projectId, { name })
 430    }
 431    
 432    /**
 433     * Delete a project (and all its tasks)
 434     */
 435    async deleteProject(projectId: string): Promise<boolean> {
 436      if (!this.api) throw new Error('Todoist client not initialized')
 437      return await this.api.deleteProject(projectId)
 438    }
 439    
 440    // ─────────────────────────────────────────────────────────────────────────
 441    // Task Operations  
 442    // ─────────────────────────────────────────────────────────────────────────
 443    
 444    /**
 445     * Get tasks for a specific project
 446     */
 447    async getProjectTasks(projectId: string): Promise<Task[]> {
 448      if (!this.api) throw new Error('Todoist client not initialized')
 449      return await this.api.getTasks({ projectId })
 450    }
 451    
 452    /**
 453     * Get all tasks under Mnemonic
 454     */
 455    async getAllWorkspaceTasks(): Promise<Task[]> {
 456      if (!this.api || !this.masterProjectId) {
 457        throw new Error('Todoist client not initialized')
 458      }
 459      
 460      const projects = await this.getWorkspaceProjects()
 461      const projectIds = [this.masterProjectId, ...projects.map(p => p.id)]
 462      
 463      const taskPromises = projectIds.map(id => this.api!.getTasks({ projectId: id }))
 464      const taskArrays = await Promise.all(taskPromises)
 465      
 466      return taskArrays.flat()
 467    }
 468    
 469    /**
 470     * Create a new task
 471     */
 472    async createTask(projectId: string, request: CreateTaskRequest): Promise<Task> {
 473      if (!this.api) throw new Error('Todoist client not initialized')
 474      
 475      return await this.api.addTask({
 476        content: request.content,
 477        description: request.description,
 478        projectId,
 479        dueString: request.dueString,
 480        dueDate: request.dueDate,
 481        dueDatetime: request.dueDatetime,
 482        priority: request.priority,
 483        labels: request.labels,
 484        sectionId: request.sectionId,
 485      })
 486    }
 487    
 488    /**
 489     * Update an existing task
 490     */
 491    async updateTask(request: UpdateTaskRequest): Promise<Task> {
 492      if (!this.api) throw new Error('Todoist client not initialized')
 493      
 494      return await this.api.updateTask(request.taskId, {
 495        content: request.content,
 496        description: request.description,
 497        dueString: request.dueString,
 498        dueDate: request.dueDate,
 499        dueDatetime: request.dueDatetime,
 500        priority: request.priority,
 501        labels: request.labels,
 502      })
 503    }
 504    
 505    /**
 506     * Complete a task
 507     */
 508    async completeTask(taskId: string): Promise<boolean> {
 509      if (!this.api) throw new Error('Todoist client not initialized')
 510      return await this.api.closeTask(taskId)
 511    }
 512    
 513    /**
 514     * Reopen a completed task
 515     */
 516    async reopenTask(taskId: string): Promise<boolean> {
 517      if (!this.api) throw new Error('Todoist client not initialized')
 518      return await this.api.reopenTask(taskId)
 519    }
 520    
 521    /**
 522     * Delete a task
 523     */
 524    async deleteTask(taskId: string): Promise<boolean> {
 525      if (!this.api) throw new Error('Todoist client not initialized')
 526      return await this.api.deleteTask(taskId)
 527    }
 528    
 529    // ─────────────────────────────────────────────────────────────────────────
 530    // Section Operations
 531    // ─────────────────────────────────────────────────────────────────────────
 532    
 533    /**
 534     * Get sections for a project
 535     */
 536    async getProjectSections(projectId: string): Promise<Section[]> {
 537      if (!this.api) throw new Error('Todoist client not initialized')
 538      return await this.api.getSections(projectId)
 539    }
 540    
 541    /**
 542     * Create a section
 543     */
 544    async createSection(projectId: string, name: string): Promise<Section> {
 545      if (!this.api) throw new Error('Todoist client not initialized')
 546      return await this.api.addSection({ name, projectId })
 547    }
 548    
 549    // ─────────────────────────────────────────────────────────────────────────
 550    // Label Operations
 551    // ─────────────────────────────────────────────────────────────────────────
 552    
 553    /**
 554     * Get all labels
 555     */
 556    async getLabels(): Promise<Label[]> {
 557      if (!this.api) throw new Error('Todoist client not initialized')
 558      return await this.api.getLabels()
 559    }
 560    
 561    /**
 562     * Create a label
 563     */
 564    async createLabel(name: string, color?: string): Promise<Label> {
 565      if (!this.api) throw new Error('Todoist client not initialized')
 566      return await this.api.addLabel({ name, color })
 567    }
 568  }
 569  
 570  // Singleton instance
 571  export const todoistClient = new TodoistClient()
 572  ```
 573  
 574  ---
 575  
 576  ## Workspace Sync Service
 577  
 578  Manages the mapping between browser workspaces and Todoist projects:
 579  
 580  ```typescript
 581  // src/lib/todoist/workspace-sync.ts
 582  
 583  import { todoistClient } from './client'
 584  import type { Project } from '@doist/todoist-api-typescript'
 585  import type { Workspace, ParentWorkspace, ChildWorkspace } from '../types/workspace'
 586  import type { WorkspaceProjectMapping, SyncResult } from './types'
 587  import { storage } from '../storage'
 588  
 589  const MAPPING_STORAGE_KEY = 'todoist_workspace_mappings'
 590  
 591  /**
 592   * Manages synchronization between workspaces and Todoist projects
 593   */
 594  export class WorkspaceSyncService {
 595    private mappings: Map<string, WorkspaceProjectMapping> = new Map()
 596    
 597    /**
 598     * Load mappings from storage
 599     */
 600    async loadMappings(): Promise<void> {
 601      const stored = await storage.get<WorkspaceProjectMapping[]>(MAPPING_STORAGE_KEY)
 602      if (stored) {
 603        this.mappings = new Map(stored.map(m => [m.workspaceId, m]))
 604      }
 605    }
 606    
 607    /**
 608     * Save mappings to storage
 609     */
 610    private async saveMappings(): Promise<void> {
 611      const mappingsArray = Array.from(this.mappings.values())
 612      await storage.set(MAPPING_STORAGE_KEY, mappingsArray)
 613    }
 614    
 615    /**
 616     * Get Todoist project ID for a workspace
 617     */
 618    getProjectId(workspaceId: string): string | undefined {
 619      return this.mappings.get(workspaceId)?.todoistProjectId
 620    }
 621    
 622    /**
 623     * Get workspace ID for a Todoist project
 624     */
 625    getWorkspaceId(projectId: string): string | undefined {
 626      for (const mapping of this.mappings.values()) {
 627        if (mapping.todoistProjectId === projectId) {
 628          return mapping.workspaceId
 629        }
 630      }
 631      return undefined
 632    }
 633    
 634    /**
 635     * Sync a single workspace to Todoist
 636     * Creates project if it doesn't exist, updates name if changed
 637     */
 638    async syncWorkspace(
 639      workspace: Workspace,
 640      parentProjectId?: string
 641    ): Promise<WorkspaceProjectMapping> {
 642      const existing = this.mappings.get(workspace.id)
 643      
 644      if (existing) {
 645        // Check if name changed
 646        if (existing.workspaceName !== workspace.name) {
 647          await todoistClient.updateProject(existing.todoistProjectId, workspace.name)
 648          existing.workspaceName = workspace.name
 649          existing.lastSynced = new Date().toISOString()
 650          await this.saveMappings()
 651        }
 652        return existing
 653      }
 654      
 655      // Create new project
 656      const project = await todoistClient.createWorkspaceProject(
 657        workspace.name,
 658        workspace.type,
 659        parentProjectId
 660      )
 661      
 662      const mapping: WorkspaceProjectMapping = {
 663        workspaceId: workspace.id,
 664        workspaceName: workspace.name,
 665        workspaceType: workspace.type,
 666        todoistProjectId: project.id,
 667        todoistProjectName: project.name,
 668        parentProjectId,
 669        lastSynced: new Date().toISOString(),
 670      }
 671      
 672      this.mappings.set(workspace.id, mapping)
 673      await this.saveMappings()
 674      
 675      return mapping
 676    }
 677    
 678    /**
 679     * Sync all workspaces to Todoist
 680     * Handles parent/child relationships correctly
 681     */
 682    async syncAllWorkspaces(workspaces: Workspace[]): Promise<SyncResult> {
 683      const result: SyncResult = {
 684        success: true,
 685        tasksUpdated: 0,
 686        tasksFailed: 0,
 687        projectsCreated: 0,
 688        errors: [],
 689      }
 690      
 691      // Separate by type for ordered processing
 692      const parents = workspaces.filter(w => w.type === 'parent') as ParentWorkspace[]
 693      const standalones = workspaces.filter(w => w.type === 'standalone')
 694      
 695      // Sync parent workspaces first
 696      for (const parent of parents) {
 697        try {
 698          const mapping = await this.syncWorkspace(parent)
 699          if (!this.mappings.has(parent.id)) {
 700            result.projectsCreated++
 701          }
 702          
 703          // Sync child workspaces under this parent
 704          for (const child of parent.children) {
 705            try {
 706              await this.syncWorkspace(child, mapping.todoistProjectId)
 707              if (!this.mappings.has(child.id)) {
 708                result.projectsCreated++
 709              }
 710            } catch (error) {
 711              result.errors.push(`Failed to sync child workspace ${child.name}: ${error}`)
 712              result.success = false
 713            }
 714          }
 715        } catch (error) {
 716          result.errors.push(`Failed to sync parent workspace ${parent.name}: ${error}`)
 717          result.success = false
 718        }
 719      }
 720      
 721      // Sync standalone workspaces
 722      for (const standalone of standalones) {
 723        try {
 724          await this.syncWorkspace(standalone)
 725          if (!this.mappings.has(standalone.id)) {
 726            result.projectsCreated++
 727          }
 728        } catch (error) {
 729          result.errors.push(`Failed to sync standalone workspace ${standalone.name}: ${error}`)
 730          result.success = false
 731        }
 732      }
 733      
 734      return result
 735    }
 736    
 737    /**
 738     * Remove mapping when workspace is deleted
 739     */
 740    async removeMapping(workspaceId: string, deleteProject: boolean = false): Promise<void> {
 741      const mapping = this.mappings.get(workspaceId)
 742      
 743      if (mapping) {
 744        if (deleteProject) {
 745          try {
 746            await todoistClient.deleteProject(mapping.todoistProjectId)
 747          } catch (error) {
 748            console.error('Failed to delete Todoist project:', error)
 749          }
 750        }
 751        
 752        this.mappings.delete(workspaceId)
 753        await this.saveMappings()
 754      }
 755    }
 756    
 757    /**
 758     * Get all mappings
 759     */
 760    getAllMappings(): WorkspaceProjectMapping[] {
 761      return Array.from(this.mappings.values())
 762    }
 763    
 764    /**
 765     * Reconcile mappings with current Todoist projects
 766     * Removes orphaned mappings where project no longer exists
 767     */
 768    async reconcile(): Promise<number> {
 769      const projects = await todoistClient.getWorkspaceProjects()
 770      const projectIds = new Set(projects.map(p => p.id))
 771      
 772      let removedCount = 0
 773      
 774      for (const [workspaceId, mapping] of this.mappings) {
 775        if (!projectIds.has(mapping.todoistProjectId)) {
 776          this.mappings.delete(workspaceId)
 777          removedCount++
 778        }
 779      }
 780      
 781      if (removedCount > 0) {
 782        await this.saveMappings()
 783      }
 784      
 785      return removedCount
 786    }
 787  }
 788  
 789  export const workspaceSyncService = new WorkspaceSyncService()
 790  ```
 791  
 792  ---
 793  
 794  ## Task Store (Svelte)
 795  
 796  Reactive store for task state management:
 797  
 798  ```typescript
 799  // src/lib/todoist/task-store.ts
 800  
 801  import { writable, derived, get } from 'svelte/store'
 802  import type { Task } from '@doist/todoist-api-typescript'
 803  import type { WorkspaceTask, TaskFilter, CreateTaskRequest, UpdateTaskRequest } from './types'
 804  import { todoistClient } from './client'
 805  import { workspaceSyncService } from './workspace-sync'
 806  
 807  /**
 808   * Create the task store with CRUD operations
 809   */
 810  function createTaskStore() {
 811    const tasks = writable<Map<string, WorkspaceTask>>(new Map())
 812    const loading = writable<boolean>(false)
 813    const error = writable<string | null>(null)
 814    const filter = writable<TaskFilter>({ showCompleted: false })
 815    
 816    /**
 817     * Enrich task with workspace context
 818     */
 819    function enrichTask(task: Task): WorkspaceTask | null {
 820      const projectId = task.projectId
 821      const workspaceId = workspaceSyncService.getWorkspaceId(projectId)
 822      const mapping = workspaceSyncService.getAllMappings().find(m => m.todoistProjectId === projectId)
 823      
 824      if (!workspaceId || !mapping) {
 825        return null
 826      }
 827      
 828      return {
 829        ...task,
 830        workspaceId,
 831        workspaceName: mapping.workspaceName,
 832      }
 833    }
 834    
 835    return {
 836      subscribe: tasks.subscribe,
 837      loading,
 838      error,
 839      filter,
 840      
 841      /**
 842       * Load tasks for a specific workspace
 843       */
 844      async loadWorkspaceTasks(workspaceId: string): Promise<void> {
 845        const projectId = workspaceSyncService.getProjectId(workspaceId)
 846        
 847        if (!projectId) {
 848          error.set('Workspace not synced to Todoist')
 849          return
 850        }
 851        
 852        loading.set(true)
 853        error.set(null)
 854        
 855        try {
 856          const rawTasks = await todoistClient.getProjectTasks(projectId)
 857          const enrichedTasks = rawTasks
 858            .map(t => enrichTask(t))
 859            .filter((t): t is WorkspaceTask => t !== null)
 860          
 861          tasks.update(current => {
 862            // Clear existing tasks for this workspace
 863            for (const [id, task] of current) {
 864              if (task.workspaceId === workspaceId) {
 865                current.delete(id)
 866              }
 867            }
 868            // Add new tasks
 869            for (const task of enrichedTasks) {
 870              current.set(task.id, task)
 871            }
 872            return new Map(current)
 873          })
 874        } catch (e) {
 875          error.set(e instanceof Error ? e.message : 'Failed to load tasks')
 876        } finally {
 877          loading.set(false)
 878        }
 879      },
 880      
 881      /**
 882       * Load all tasks across all workspaces
 883       */
 884      async loadAllTasks(): Promise<void> {
 885        loading.set(true)
 886        error.set(null)
 887        
 888        try {
 889          const rawTasks = await todoistClient.getAllWorkspaceTasks()
 890          const enrichedTasks = rawTasks
 891            .map(t => enrichTask(t))
 892            .filter((t): t is WorkspaceTask => t !== null)
 893          
 894          tasks.set(new Map(enrichedTasks.map(t => [t.id, t])))
 895        } catch (e) {
 896          error.set(e instanceof Error ? e.message : 'Failed to load tasks')
 897        } finally {
 898          loading.set(false)
 899        }
 900      },
 901      
 902      /**
 903       * Create a new task
 904       */
 905      async createTask(workspaceId: string, request: CreateTaskRequest): Promise<WorkspaceTask | null> {
 906        const projectId = workspaceSyncService.getProjectId(workspaceId)
 907        
 908        if (!projectId) {
 909          error.set('Workspace not synced to Todoist')
 910          return null
 911        }
 912        
 913        try {
 914          const task = await todoistClient.createTask(projectId, request)
 915          const enriched = enrichTask(task)
 916          
 917          if (enriched) {
 918            tasks.update(current => {
 919              current.set(enriched.id, enriched)
 920              return new Map(current)
 921            })
 922          }
 923          
 924          return enriched
 925        } catch (e) {
 926          error.set(e instanceof Error ? e.message : 'Failed to create task')
 927          return null
 928        }
 929      },
 930      
 931      /**
 932       * Update a task
 933       */
 934      async updateTask(request: UpdateTaskRequest): Promise<WorkspaceTask | null> {
 935        try {
 936          const task = await todoistClient.updateTask(request)
 937          const enriched = enrichTask(task)
 938          
 939          if (enriched) {
 940            tasks.update(current => {
 941              current.set(enriched.id, enriched)
 942              return new Map(current)
 943            })
 944          }
 945          
 946          return enriched
 947        } catch (e) {
 948          error.set(e instanceof Error ? e.message : 'Failed to update task')
 949          return null
 950        }
 951      },
 952      
 953      /**
 954       * Toggle task completion
 955       */
 956      async toggleComplete(taskId: string): Promise<boolean> {
 957        const currentTasks = get(tasks)
 958        const task = currentTasks.get(taskId)
 959        
 960        if (!task) return false
 961        
 962        try {
 963          if (task.isCompleted) {
 964            await todoistClient.reopenTask(taskId)
 965          } else {
 966            await todoistClient.completeTask(taskId)
 967          }
 968          
 969          tasks.update(current => {
 970            const updated = current.get(taskId)
 971            if (updated) {
 972              updated.isCompleted = !updated.isCompleted
 973              current.set(taskId, { ...updated })
 974            }
 975            return new Map(current)
 976          })
 977          
 978          return true
 979        } catch (e) {
 980          error.set(e instanceof Error ? e.message : 'Failed to update task')
 981          return false
 982        }
 983      },
 984      
 985      /**
 986       * Delete a task
 987       */
 988      async deleteTask(taskId: string): Promise<boolean> {
 989        try {
 990          await todoistClient.deleteTask(taskId)
 991          
 992          tasks.update(current => {
 993            current.delete(taskId)
 994            return new Map(current)
 995          })
 996          
 997          return true
 998        } catch (e) {
 999          error.set(e instanceof Error ? e.message : 'Failed to delete task')
1000          return false
1001        }
1002      },
1003      
1004      /**
1005       * Clear error
1006       */
1007      clearError(): void {
1008        error.set(null)
1009      },
1010    }
1011  }
1012  
1013  export const taskStore = createTaskStore()
1014  
1015  /**
1016   * Derived store for filtered tasks by workspace
1017   */
1018  export function getWorkspaceTasks(workspaceId: string) {
1019    return derived(
1020      [taskStore, taskStore.filter],
1021      ([$tasks, $filter]) => {
1022        const filtered: WorkspaceTask[] = []
1023        
1024        for (const task of $tasks.values()) {
1025          if (task.workspaceId !== workspaceId) continue
1026          if (!$filter.showCompleted && task.isCompleted) continue
1027          if ($filter.priority && task.priority !== $filter.priority) continue
1028          
1029          filtered.push(task)
1030        }
1031        
1032        // Sort: incomplete first, then by due date, then by priority
1033        return filtered.sort((a, b) => {
1034          // Completed at bottom
1035          if (a.isCompleted !== b.isCompleted) {
1036            return a.isCompleted ? 1 : -1
1037          }
1038          
1039          // Then by due date (null dates at end)
1040          if (a.due?.date !== b.due?.date) {
1041            if (!a.due?.date) return 1
1042            if (!b.due?.date) return -1
1043            return a.due.date.localeCompare(b.due.date)
1044          }
1045          
1046          // Then by priority (higher number = higher priority)
1047          return (b.priority || 1) - (a.priority || 1)
1048        })
1049      }
1050    )
1051  }
1052  ```
1053  
1054  ---
1055  
1056  ## Svelte Components
1057  
1058  ### TodoistCard Component
1059  
1060  Main dashboard card component:
1061  
1062  ```svelte
1063  <!-- src/components/todoist/TodoistCard.svelte -->
1064  <script lang="ts">
1065    import { onMount } from 'svelte'
1066    import { taskStore, getWorkspaceTasks } from '../../lib/todoist/task-store'
1067    import { todoistClient } from '../../lib/todoist/client'
1068    import { workspaceSyncService } from '../../lib/todoist/workspace-sync'
1069    import type { Workspace } from '../../lib/types/workspace'
1070    import type { CreateTaskRequest } from '../../lib/todoist/types'
1071    import { PRIORITY_CONFIG } from '../../lib/todoist/types'
1072    import TaskItem from './TaskItem.svelte'
1073    import TaskEditor from './TaskEditor.svelte'
1074    import TodoistSettings from './TodoistSettings.svelte'
1075    
1076    export let workspace: Workspace
1077    
1078    let showAddTask = false
1079    let showSettings = false
1080    let syncing = false
1081    
1082    $: tasks = getWorkspaceTasks(workspace.id)
1083    $: isConnected = todoistClient.isConnected()
1084    $: projectId = workspaceSyncService.getProjectId(workspace.id)
1085    
1086    onMount(async () => {
1087      if (isConnected && projectId) {
1088        await taskStore.loadWorkspaceTasks(workspace.id)
1089      }
1090    })
1091    
1092    async function handleSync(): Promise<void> {
1093      if (!isConnected) return
1094      
1095      syncing = true
1096      try {
1097        await workspaceSyncService.syncWorkspace(workspace)
1098        await taskStore.loadWorkspaceTasks(workspace.id)
1099      } finally {
1100        syncing = false
1101      }
1102    }
1103    
1104    async function handleCreateTask(event: CustomEvent<CreateTaskRequest>): Promise<void> {
1105      const task = await taskStore.createTask(workspace.id, event.detail)
1106      if (task) {
1107        showAddTask = false
1108      }
1109    }
1110    
1111    function toggleShowCompleted(): void {
1112      taskStore.filter.update(f => ({ ...f, showCompleted: !f.showCompleted }))
1113    }
1114  </script>
1115  
1116  <div class="todoist-card">
1117    <header class="card-header">
1118      <div class="header-left">
1119        <span class="card-icon">📋</span>
1120        <h3>{workspace.name} Tasks</h3>
1121      </div>
1122      
1123      <div class="header-actions">
1124        {#if isConnected}
1125          <button 
1126            class="btn-icon" 
1127            on:click={() => showAddTask = true}
1128            title="Add task"
1129          >
1130            +
1131          </button>
1132          <button 
1133            class="btn-icon" 
1134            class:syncing
1135            on:click={handleSync}
1136            title="Sync with Todoist"
1137          >
11381139          </button>
1140        {/if}
1141        <button 
1142          class="btn-icon" 
1143          on:click={() => showSettings = true}
1144          title="Settings"
1145        >
11461147        </button>
1148      </div>
1149    </header>
1150    
1151    {#if !isConnected}
1152      <div class="not-connected">
1153        <p>Connect to Todoist to manage tasks</p>
1154        <button class="btn-primary" on:click={() => showSettings = true}>
1155          Connect
1156        </button>
1157      </div>
1158    {:else if $taskStore.loading}
1159      <div class="loading">
1160        <span class="spinner"></span>
1161        Loading tasks...
1162      </div>
1163    {:else if $taskStore.error}
1164      <div class="error">
1165        <p>{$taskStore.error}</p>
1166        <button on:click={() => taskStore.clearError()}>Dismiss</button>
1167      </div>
1168    {:else}
1169      {#if showAddTask}
1170        <TaskEditor 
1171          on:save={handleCreateTask}
1172          on:cancel={() => showAddTask = false}
1173        />
1174      {/if}
1175      
1176      <div class="task-list">
1177        {#each $tasks as task (task.id)}
1178          <TaskItem {task} />
1179        {:else}
1180          <div class="empty-state">
1181            <p>No tasks yet</p>
1182            <button class="btn-secondary" on:click={() => showAddTask = true}>
1183              Add your first task
1184            </button>
1185          </div>
1186        {/each}
1187      </div>
1188      
1189      <footer class="card-footer">
1190        <button 
1191          class="btn-text"
1192          on:click={toggleShowCompleted}
1193        >
1194          {$taskStore.filter.showCompleted ? 'Hide' : 'Show'} completed
1195        </button>
1196        <span class="task-count">
1197          {$tasks.filter(t => !t.isCompleted).length} active
1198        </span>
1199      </footer>
1200    {/if}
1201  </div>
1202  
1203  {#if showSettings}
1204    <TodoistSettings on:close={() => showSettings = false} />
1205  {/if}
1206  
1207  <style>
1208    .todoist-card {
1209      background: var(--bg-primary);
1210      border: 1px solid var(--border-color);
1211      border-radius: 8px;
1212      overflow: hidden;
1213    }
1214    
1215    .card-header {
1216      display: flex;
1217      align-items: center;
1218      justify-content: space-between;
1219      padding: 0.75rem 1rem;
1220      border-bottom: 1px solid var(--border-color);
1221      background: var(--bg-secondary);
1222    }
1223    
1224    .header-left {
1225      display: flex;
1226      align-items: center;
1227      gap: 0.5rem;
1228    }
1229    
1230    .card-icon {
1231      font-size: 1.25rem;
1232    }
1233    
1234    .card-header h3 {
1235      margin: 0;
1236      font-size: 0.9rem;
1237      font-weight: 600;
1238    }
1239    
1240    .header-actions {
1241      display: flex;
1242      gap: 0.25rem;
1243    }
1244    
1245    .btn-icon {
1246      width: 28px;
1247      height: 28px;
1248      border: none;
1249      background: transparent;
1250      border-radius: 4px;
1251      cursor: pointer;
1252      font-size: 1rem;
1253      display: flex;
1254      align-items: center;
1255      justify-content: center;
1256      color: var(--text-secondary);
1257    }
1258    
1259    .btn-icon:hover {
1260      background: var(--bg-hover);
1261      color: var(--text-primary);
1262    }
1263    
1264    .btn-icon.syncing {
1265      animation: spin 1s linear infinite;
1266    }
1267    
1268    @keyframes spin {
1269      from { transform: rotate(0deg); }
1270      to { transform: rotate(360deg); }
1271    }
1272    
1273    .not-connected,
1274    .loading,
1275    .error,
1276    .empty-state {
1277      padding: 2rem;
1278      text-align: center;
1279      color: var(--text-secondary);
1280    }
1281    
1282    .task-list {
1283      max-height: 400px;
1284      overflow-y: auto;
1285    }
1286    
1287    .card-footer {
1288      display: flex;
1289      align-items: center;
1290      justify-content: space-between;
1291      padding: 0.5rem 1rem;
1292      border-top: 1px solid var(--border-color);
1293      font-size: 0.8rem;
1294    }
1295    
1296    .btn-text {
1297      background: none;
1298      border: none;
1299      color: var(--text-secondary);
1300      cursor: pointer;
1301      padding: 0.25rem 0.5rem;
1302    }
1303    
1304    .btn-text:hover {
1305      color: var(--text-primary);
1306    }
1307    
1308    .task-count {
1309      color: var(--text-secondary);
1310    }
1311    
1312    .btn-primary {
1313      background: var(--accent-color);
1314      color: white;
1315      border: none;
1316      padding: 0.5rem 1rem;
1317      border-radius: 4px;
1318      cursor: pointer;
1319    }
1320    
1321    .btn-secondary {
1322      background: var(--bg-secondary);
1323      border: 1px solid var(--border-color);
1324      padding: 0.5rem 1rem;
1325      border-radius: 4px;
1326      cursor: pointer;
1327    }
1328    
1329    .spinner {
1330      display: inline-block;
1331      width: 16px;
1332      height: 16px;
1333      border: 2px solid var(--border-color);
1334      border-top-color: var(--accent-color);
1335      border-radius: 50%;
1336      animation: spin 0.8s linear infinite;
1337      margin-right: 0.5rem;
1338    }
1339  </style>
1340  ```
1341  
1342  ### TaskItem Component
1343  
1344  Individual task display:
1345  
1346  ```svelte
1347  <!-- src/components/todoist/TaskItem.svelte -->
1348  <script lang="ts">
1349    import { createEventDispatcher } from 'svelte'
1350    import type { WorkspaceTask } from '../../lib/todoist/types'
1351    import { PRIORITY_CONFIG } from '../../lib/todoist/types'
1352    import { taskStore } from '../../lib/todoist/task-store'
1353    import TaskEditor from './TaskEditor.svelte'
1354    
1355    export let task: WorkspaceTask
1356    
1357    const dispatch = createEventDispatcher()
1358    
1359    let editing = false
1360    let completing = false
1361    
1362    $: priorityConfig = PRIORITY_CONFIG[task.priority as keyof typeof PRIORITY_CONFIG] || PRIORITY_CONFIG[1]
1363    $: dueDisplay = formatDue(task.due)
1364    $: isOverdue = task.due?.date && new Date(task.due.date) < new Date() && !task.isCompleted
1365    
1366    function formatDue(due: WorkspaceTask['due']): string {
1367      if (!due?.date) return ''
1368      
1369      const date = new Date(due.date)
1370      const today = new Date()
1371      const tomorrow = new Date(today)
1372      tomorrow.setDate(tomorrow.getDate() + 1)
1373      
1374      if (date.toDateString() === today.toDateString()) {
1375        return 'Today'
1376      }
1377      if (date.toDateString() === tomorrow.toDateString()) {
1378        return 'Tomorrow'
1379      }
1380      
1381      return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
1382    }
1383    
1384    async function handleToggleComplete(): Promise<void> {
1385      completing = true
1386      await taskStore.toggleComplete(task.id)
1387      completing = false
1388    }
1389    
1390    async function handleDelete(): Promise<void> {
1391      if (confirm('Delete this task?')) {
1392        await taskStore.deleteTask(task.id)
1393      }
1394    }
1395  </script>
1396  
1397  {#if editing}
1398    <TaskEditor 
1399      {task}
1400      on:save={() => editing = false}
1401      on:cancel={() => editing = false}
1402    />
1403  {:else}
1404    <div 
1405      class="task-item"
1406      class:completed={task.isCompleted}
1407      class:overdue={isOverdue}
1408    >
1409      <button 
1410        class="checkbox"
1411        class:checked={task.isCompleted}
1412        class:completing
1413        style="--priority-color: {priorityConfig.color}"
1414        on:click={handleToggleComplete}
1415        disabled={completing}
1416      >
1417        {#if task.isCompleted}
14181419        {/if}
1420      </button>
1421      
1422      <div class="task-content" on:dblclick={() => editing = true}>
1423        <span class="task-text">{task.content}</span>
1424        
1425        {#if task.description}
1426          <span class="task-description">{task.description}</span>
1427        {/if}
1428        
1429        <div class="task-meta">
1430          {#if dueDisplay}
1431            <span class="due-date" class:overdue={isOverdue}>
1432              📅 {dueDisplay}
1433            </span>
1434          {/if}
1435          
1436          {#if task.labels?.length}
1437            <span class="labels">
1438              {#each task.labels as label}
1439                <span class="label">@{label}</span>
1440              {/each}
1441            </span>
1442          {/if}
1443        </div>
1444      </div>
1445      
1446      <div class="priority-indicator" title={priorityConfig.label}>
1447        {#each Array(4) as _, i}
1448          <span 
1449            class="dot"
1450            class:filled={i < priorityConfig.dots}
1451            style="--dot-color: {priorityConfig.color}"
1452          ></span>
1453        {/each}
1454      </div>
1455      
1456      <div class="task-actions">
1457        <button class="btn-action" on:click={() => editing = true} title="Edit">
1458          ✏️
1459        </button>
1460        <button class="btn-action" on:click={handleDelete} title="Delete">
1461          🗑️
1462        </button>
1463      </div>
1464    </div>
1465  {/if}
1466  
1467  <style>
1468    .task-item {
1469      display: flex;
1470      align-items: flex-start;
1471      gap: 0.75rem;
1472      padding: 0.75rem 1rem;
1473      border-bottom: 1px solid var(--border-color);
1474      transition: background-color 0.15s;
1475    }
1476    
1477    .task-item:hover {
1478      background: var(--bg-hover);
1479    }
1480    
1481    .task-item.completed {
1482      opacity: 0.6;
1483    }
1484    
1485    .task-item.overdue {
1486      background: rgba(209, 69, 59, 0.05);
1487    }
1488    
1489    .checkbox {
1490      width: 20px;
1491      height: 20px;
1492      min-width: 20px;
1493      border: 2px solid var(--priority-color);
1494      border-radius: 50%;
1495      background: transparent;
1496      cursor: pointer;
1497      display: flex;
1498      align-items: center;
1499      justify-content: center;
1500      font-size: 12px;
1501      color: var(--priority-color);
1502      transition: all 0.15s;
1503    }
1504    
1505    .checkbox:hover {
1506      background: var(--priority-color);
1507      color: white;
1508    }
1509    
1510    .checkbox.checked {
1511      background: var(--priority-color);
1512      color: white;
1513    }
1514    
1515    .checkbox.completing {
1516      opacity: 0.5;
1517    }
1518    
1519    .task-content {
1520      flex: 1;
1521      min-width: 0;
1522    }
1523    
1524    .task-text {
1525      display: block;
1526      font-size: 0.9rem;
1527    }
1528    
1529    .completed .task-text {
1530      text-decoration: line-through;
1531    }
1532    
1533    .task-description {
1534      display: block;
1535      font-size: 0.8rem;
1536      color: var(--text-secondary);
1537      margin-top: 0.25rem;
1538      white-space: nowrap;
1539      overflow: hidden;
1540      text-overflow: ellipsis;
1541    }
1542    
1543    .task-meta {
1544      display: flex;
1545      gap: 0.5rem;
1546      margin-top: 0.25rem;
1547      flex-wrap: wrap;
1548    }
1549    
1550    .due-date {
1551      font-size: 0.75rem;
1552      color: var(--text-secondary);
1553    }
1554    
1555    .due-date.overdue {
1556      color: #d1453b;
1557      font-weight: 500;
1558    }
1559    
1560    .labels {
1561      display: flex;
1562      gap: 0.25rem;
1563    }
1564    
1565    .label {
1566      font-size: 0.7rem;
1567      color: var(--accent-color);
1568      background: rgba(99, 102, 241, 0.1);
1569      padding: 0.1rem 0.3rem;
1570      border-radius: 3px;
1571    }
1572    
1573    .priority-indicator {
1574      display: flex;
1575      gap: 2px;
1576      align-items: center;
1577      padding-top: 2px;
1578    }
1579    
1580    .dot {
1581      width: 6px;
1582      height: 6px;
1583      border-radius: 50%;
1584      background: var(--border-color);
1585    }
1586    
1587    .dot.filled {
1588      background: var(--dot-color);
1589    }
1590    
1591    .task-actions {
1592      display: flex;
1593      gap: 0.25rem;
1594      opacity: 0;
1595      transition: opacity 0.15s;
1596    }
1597    
1598    .task-item:hover .task-actions {
1599      opacity: 1;
1600    }
1601    
1602    .btn-action {
1603      width: 24px;
1604      height: 24px;
1605      border: none;
1606      background: transparent;
1607      cursor: pointer;
1608      border-radius: 4px;
1609      font-size: 0.75rem;
1610    }
1611    
1612    .btn-action:hover {
1613      background: var(--bg-secondary);
1614    }
1615  </style>
1616  ```
1617  
1618  ### TaskEditor Component
1619  
1620  Create/edit task form:
1621  
1622  ```svelte
1623  <!-- src/components/todoist/TaskEditor.svelte -->
1624  <script lang="ts">
1625    import { createEventDispatcher } from 'svelte'
1626    import type { WorkspaceTask, CreateTaskRequest, UpdateTaskRequest } from '../../lib/todoist/types'
1627    import { PRIORITY_CONFIG } from '../../lib/todoist/types'
1628    import { taskStore } from '../../lib/todoist/task-store'
1629    
1630    export let task: WorkspaceTask | null = null
1631    
1632    const dispatch = createEventDispatcher<{
1633      save: CreateTaskRequest | UpdateTaskRequest
1634      cancel: void
1635    }>()
1636    
1637    let content = task?.content || ''
1638    let description = task?.description || ''
1639    let dueString = task?.due?.string || ''
1640    let priority: 1 | 2 | 3 | 4 = (task?.priority as 1 | 2 | 3 | 4) || 1
1641    let saving = false
1642    
1643    $: isValid = content.trim().length > 0
1644    $: isEditing = task !== null
1645    
1646    async function handleSubmit(): Promise<void> {
1647      if (!isValid) return
1648      
1649      saving = true
1650      
1651      try {
1652        if (isEditing && task) {
1653          // Update existing task
1654          await taskStore.updateTask({
1655            taskId: task.id,
1656            content,
1657            description: description || undefined,
1658            dueString: dueString || undefined,
1659            priority,
1660          })
1661          dispatch('save')
1662        } else {
1663          // Create new task
1664          dispatch('save', {
1665            content,
1666            description: description || undefined,
1667            dueString: dueString || undefined,
1668            priority,
1669          })
1670        }
1671      } finally {
1672        saving = false
1673      }
1674    }
1675    
1676    function handleKeydown(e: KeyboardEvent): void {
1677      if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1678        handleSubmit()
1679      }
1680      if (e.key === 'Escape') {
1681        dispatch('cancel')
1682      }
1683    }
1684  </script>
1685  
1686  <div class="task-editor" on:keydown={handleKeydown}>
1687    <input
1688      type="text"
1689      class="task-input"
1690      placeholder="Task name"
1691      bind:value={content}
1692      autofocus
1693    />
1694    
1695    <textarea
1696      class="description-input"
1697      placeholder="Description (optional)"
1698      bind:value={description}
1699      rows="2"
1700    ></textarea>
1701    
1702    <div class="editor-row">
1703      <div class="field">
1704        <label>Due</label>
1705        <input
1706          type="text"
1707          class="due-input"
1708          placeholder="e.g., tomorrow, next Monday"
1709          bind:value={dueString}
1710        />
1711      </div>
1712      
1713      <div class="field">
1714        <label>Priority</label>
1715        <div class="priority-buttons">
1716          {#each [4, 3, 2, 1] as p}
1717            {@const config = PRIORITY_CONFIG[p as keyof typeof PRIORITY_CONFIG]}
1718            <button
1719              type="button"
1720              class="priority-btn"
1721              class:selected={priority === p}
1722              style="--priority-color: {config.color}"
1723              on:click={() => priority = p as 1 | 2 | 3 | 4}
1724            >
1725              {config.label}
1726            </button>
1727          {/each}
1728        </div>
1729      </div>
1730    </div>
1731    
1732    <div class="editor-actions">
1733      <button 
1734        type="button" 
1735        class="btn-cancel"
1736        on:click={() => dispatch('cancel')}
1737      >
1738        Cancel
1739      </button>
1740      <button
1741        type="button"
1742        class="btn-save"
1743        disabled={!isValid || saving}
1744        on:click={handleSubmit}
1745      >
1746        {saving ? 'Saving...' : isEditing ? 'Save' : 'Add Task'}
1747      </button>
1748    </div>
1749    
1750    <div class="hint">
1751      <kbd>⌘</kbd>+<kbd>Enter</kbd> to save, <kbd>Esc</kbd> to cancel
1752    </div>
1753  </div>
1754  
1755  <style>
1756    .task-editor {
1757      padding: 1rem;
1758      border-bottom: 1px solid var(--border-color);
1759      background: var(--bg-secondary);
1760    }
1761    
1762    .task-input {
1763      width: 100%;
1764      padding: 0.5rem;
1765      border: 1px solid var(--border-color);
1766      border-radius: 4px;
1767      font-size: 0.9rem;
1768      background: var(--bg-primary);
1769    }
1770    
1771    .description-input {
1772      width: 100%;
1773      padding: 0.5rem;
1774      border: 1px solid var(--border-color);
1775      border-radius: 4px;
1776      font-size: 0.8rem;
1777      background: var(--bg-primary);
1778      margin-top: 0.5rem;
1779      resize: vertical;
1780      font-family: inherit;
1781    }
1782    
1783    .editor-row {
1784      display: flex;
1785      gap: 1rem;
1786      margin-top: 0.75rem;
1787    }
1788    
1789    .field {
1790      flex: 1;
1791    }
1792    
1793    .field label {
1794      display: block;
1795      font-size: 0.75rem;
1796      font-weight: 500;
1797      color: var(--text-secondary);
1798      margin-bottom: 0.25rem;
1799    }
1800    
1801    .due-input {
1802      width: 100%;
1803      padding: 0.4rem 0.5rem;
1804      border: 1px solid var(--border-color);
1805      border-radius: 4px;
1806      font-size: 0.8rem;
1807      background: var(--bg-primary);
1808    }
1809    
1810    .priority-buttons {
1811      display: flex;
1812      gap: 0.25rem;
1813    }
1814    
1815    .priority-btn {
1816      flex: 1;
1817      padding: 0.35rem 0.5rem;
1818      border: 1px solid var(--border-color);
1819      border-radius: 4px;
1820      background: var(--bg-primary);
1821      cursor: pointer;
1822      font-size: 0.75rem;
1823      font-weight: 500;
1824      color: var(--text-secondary);
1825      transition: all 0.15s;
1826    }
1827    
1828    .priority-btn:hover {
1829      border-color: var(--priority-color);
1830    }
1831    
1832    .priority-btn.selected {
1833      background: var(--priority-color);
1834      border-color: var(--priority-color);
1835      color: white;
1836    }
1837    
1838    .editor-actions {
1839      display: flex;
1840      justify-content: flex-end;
1841      gap: 0.5rem;
1842      margin-top: 0.75rem;
1843    }
1844    
1845    .btn-cancel {
1846      padding: 0.4rem 0.75rem;
1847      border: 1px solid var(--border-color);
1848      border-radius: 4px;
1849      background: var(--bg-primary);
1850      cursor: pointer;
1851      font-size: 0.8rem;
1852    }
1853    
1854    .btn-save {
1855      padding: 0.4rem 0.75rem;
1856      border: none;
1857      border-radius: 4px;
1858      background: var(--accent-color);
1859      color: white;
1860      cursor: pointer;
1861      font-size: 0.8rem;
1862    }
1863    
1864    .btn-save:disabled {
1865      opacity: 0.5;
1866      cursor: not-allowed;
1867    }
1868    
1869    .hint {
1870      margin-top: 0.5rem;
1871      font-size: 0.7rem;
1872      color: var(--text-muted);
1873      text-align: right;
1874    }
1875    
1876    kbd {
1877      display: inline-block;
1878      padding: 0.1rem 0.3rem;
1879      font-family: monospace;
1880      font-size: 0.65rem;
1881      background: var(--bg-primary);
1882      border: 1px solid var(--border-color);
1883      border-radius: 3px;
1884    }
1885  </style>
1886  ```
1887  
1888  ### TodoistSettings Component
1889  
1890  Settings modal for API token configuration:
1891  
1892  ```svelte
1893  <!-- src/components/todoist/TodoistSettings.svelte -->
1894  <script lang="ts">
1895    import { createEventDispatcher, onMount } from 'svelte'
1896    import { todoistClient } from '../../lib/todoist/client'
1897    import { workspaceSyncService } from '../../lib/todoist/workspace-sync'
1898    import type { TodoistConnectionState } from '../../lib/todoist/types'
1899    import { settingsStore } from '../../lib/storage/settings'
1900    
1901    const dispatch = createEventDispatcher<{ close: void }>()
1902    
1903    let apiToken = ''
1904    let connection: TodoistConnectionState | null = null
1905    let testing = false
1906    let saving = false
1907    let error: string | null = null
1908    
1909    onMount(async () => {
1910      // Load existing token from settings
1911      const settings = await settingsStore.get()
1912      if (settings?.todoistApiToken) {
1913        apiToken = settings.todoistApiToken
1914        // Test existing connection
1915        await testConnection()
1916      }
1917    })
1918    
1919    async function testConnection(): Promise<void> {
1920      if (!apiToken.trim()) {
1921        error = 'Please enter an API token'
1922        return
1923      }
1924      
1925      testing = true
1926      error = null
1927      
1928      try {
1929        connection = await todoistClient.initialize(apiToken)
1930        
1931        if (!connection.connected) {
1932          error = connection.error || 'Connection failed'
1933        }
1934      } catch (e) {
1935        error = e instanceof Error ? e.message : 'Connection failed'
1936      } finally {
1937        testing = false
1938      }
1939    }
1940    
1941    async function handleSave(): Promise<void> {
1942      if (!connection?.connected) {
1943        await testConnection()
1944        if (!connection?.connected) return
1945      }
1946      
1947      saving = true
1948      
1949      try {
1950        // Save token to settings
1951        await settingsStore.update({ todoistApiToken: apiToken })
1952        
1953        // Load workspace mappings
1954        await workspaceSyncService.loadMappings()
1955        
1956        dispatch('close')
1957      } catch (e) {
1958        error = e instanceof Error ? e.message : 'Failed to save settings'
1959      } finally {
1960        saving = false
1961      }
1962    }
1963    
1964    async function handleDisconnect(): Promise<void> {
1965      await settingsStore.update({ todoistApiToken: undefined })
1966      apiToken = ''
1967      connection = null
1968      dispatch('close')
1969    }
1970  </script>
1971  
1972  <div class="modal-overlay" on:click={() => dispatch('close')}>
1973    <div class="modal" on:click|stopPropagation>
1974      <header class="modal-header">
1975        <h2>Todoist Settings</h2>
1976        <button class="btn-close" on:click={() => dispatch('close')}>×</button>
1977      </header>
1978      
1979      <div class="modal-body">
1980        <div class="field">
1981          <label for="api-token">API Token</label>
1982          <input
1983            id="api-token"
1984            type="password"
1985            bind:value={apiToken}
1986            placeholder="Enter your Todoist API token"
1987          />
1988          <p class="hint">
1989            Find your token at 
1990            <a href="https://todoist.com/app/settings/integrations/developer" target="_blank">
1991              Todoist Settings → Integrations → Developer
1992            </a>
1993          </p>
1994        </div>
1995        
1996        {#if error}
1997          <div class="error-message">
1998            {error}
1999          </div>
2000        {/if}
2001        
2002        {#if connection?.connected}
2003          <div class="connection-status success">
2004            <span class="status-icon">✓</span>
2005            <div>
2006              <strong>Connected</strong>
2007              <p>Master project: {connection.masterProjectName}</p>
2008              {#if connection.lastSync}
2009                <p class="sync-time">Last sync: {new Date(connection.lastSync).toLocaleString()}</p>
2010              {/if}
2011            </div>
2012          </div>
2013        {/if}
2014        
2015        <div class="info-box">
2016          <h4>How it works</h4>
2017          <ul>
2018            <li>A "Mnemonic" project will be created in your Todoist</li>
2019            <li>Each workspace gets a sub-project under Mnemonic</li>
2020            <li>Child workspaces become sub-projects of their parent</li>
2021            <li>Tasks sync bidirectionally with Todoist</li>
2022          </ul>
2023        </div>
2024      </div>
2025      
2026      <footer class="modal-footer">
2027        {#if connection?.connected}
2028          <button class="btn-danger" on:click={handleDisconnect}>
2029            Disconnect
2030          </button>
2031        {/if}
2032        
2033        <div class="footer-right">
2034          <button class="btn-secondary" on:click={testConnection} disabled={testing}>
2035            {testing ? 'Testing...' : 'Test Connection'}
2036          </button>
2037          <button 
2038            class="btn-primary" 
2039            on:click={handleSave}
2040            disabled={saving || testing}
2041          >
2042            {saving ? 'Saving...' : 'Save'}
2043          </button>
2044        </div>
2045      </footer>
2046    </div>
2047  </div>
2048  
2049  <style>
2050    .modal-overlay {
2051      position: fixed;
2052      inset: 0;
2053      background: rgba(0, 0, 0, 0.5);
2054      display: flex;
2055      align-items: center;
2056      justify-content: center;
2057      z-index: 1000;
2058    }
2059    
2060    .modal {
2061      background: var(--bg-primary);
2062      border-radius: 8px;
2063      width: 90%;
2064      max-width: 500px;
2065      max-height: 90vh;
2066      overflow: hidden;
2067      display: flex;
2068      flex-direction: column;
2069    }
2070    
2071    .modal-header {
2072      display: flex;
2073      align-items: center;
2074      justify-content: space-between;
2075      padding: 1rem;
2076      border-bottom: 1px solid var(--border-color);
2077    }
2078    
2079    .modal-header h2 {
2080      margin: 0;
2081      font-size: 1.1rem;
2082    }
2083    
2084    .btn-close {
2085      width: 32px;
2086      height: 32px;
2087      border: none;
2088      background: transparent;
2089      font-size: 1.5rem;
2090      cursor: pointer;
2091      border-radius: 4px;
2092      color: var(--text-secondary);
2093    }
2094    
2095    .btn-close:hover {
2096      background: var(--bg-hover);
2097    }
2098    
2099    .modal-body {
2100      padding: 1rem;
2101      overflow-y: auto;
2102    }
2103    
2104    .field {
2105      margin-bottom: 1rem;
2106    }
2107    
2108    .field label {
2109      display: block;
2110      font-weight: 500;
2111      margin-bottom: 0.5rem;
2112    }
2113    
2114    .field input {
2115      width: 100%;
2116      padding: 0.5rem;
2117      border: 1px solid var(--border-color);
2118      border-radius: 4px;
2119      font-size: 0.9rem;
2120    }
2121    
2122    .hint {
2123      margin-top: 0.5rem;
2124      font-size: 0.8rem;
2125      color: var(--text-secondary);
2126    }
2127    
2128    .hint a {
2129      color: var(--accent-color);
2130    }
2131    
2132    .error-message {
2133      background: rgba(209, 69, 59, 0.1);
2134      color: #d1453b;
2135      padding: 0.75rem;
2136      border-radius: 4px;
2137      margin-bottom: 1rem;
2138    }
2139    
2140    .connection-status {
2141      display: flex;
2142      align-items: flex-start;
2143      gap: 0.75rem;
2144      padding: 0.75rem;
2145      border-radius: 4px;
2146      margin-bottom: 1rem;
2147    }
2148    
2149    .connection-status.success {
2150      background: rgba(0, 128, 0, 0.1);
2151      color: green;
2152    }
2153    
2154    .status-icon {
2155      font-size: 1.25rem;
2156    }
2157    
2158    .connection-status strong {
2159      display: block;
2160    }
2161    
2162    .connection-status p {
2163      margin: 0.25rem 0 0 0;
2164      font-size: 0.85rem;
2165    }
2166    
2167    .sync-time {
2168      color: var(--text-secondary);
2169    }
2170    
2171    .info-box {
2172      background: var(--bg-secondary);
2173      padding: 1rem;
2174      border-radius: 4px;
2175    }
2176    
2177    .info-box h4 {
2178      margin: 0 0 0.5rem 0;
2179      font-size: 0.9rem;
2180    }
2181    
2182    .info-box ul {
2183      margin: 0;
2184      padding-left: 1.25rem;
2185      font-size: 0.85rem;
2186      color: var(--text-secondary);
2187    }
2188    
2189    .info-box li {
2190      margin: 0.25rem 0;
2191    }
2192    
2193    .modal-footer {
2194      display: flex;
2195      align-items: center;
2196      justify-content: space-between;
2197      padding: 1rem;
2198      border-top: 1px solid var(--border-color);
2199      background: var(--bg-secondary);
2200    }
2201    
2202    .footer-right {
2203      display: flex;
2204      gap: 0.5rem;
2205    }
2206    
2207    .btn-primary {
2208      padding: 0.5rem 1rem;
2209      background: var(--accent-color);
2210      color: white;
2211      border: none;
2212      border-radius: 4px;
2213      cursor: pointer;
2214    }
2215    
2216    .btn-secondary {
2217      padding: 0.5rem 1rem;
2218      background: var(--bg-primary);
2219      border: 1px solid var(--border-color);
2220      border-radius: 4px;
2221      cursor: pointer;
2222    }
2223    
2224    .btn-danger {
2225      padding: 0.5rem 1rem;
2226      background: #d1453b;
2227      color: white;
2228      border: none;
2229      border-radius: 4px;
2230      cursor: pointer;
2231    }
2232    
2233    button:disabled {
2234      opacity: 0.5;
2235      cursor: not-allowed;
2236    }
2237  </style>
2238  ```
2239  
2240  ---
2241  
2242  ## Manifest Permissions
2243  
2244  Add to `wxt.config.ts`:
2245  
2246  ```typescript
2247  manifest: {
2248    // ... existing config ...
2249    
2250    host_permissions: [
2251      'https://api.todoist.com/*',  // Todoist REST API
2252    ],
2253  }
2254  ```
2255  
2256  ---
2257  
2258  ## Authentication Considerations
2259  
2260  ### Personal API Token (Recommended for Personal Use)
2261  
2262  The simplest approach for a personal browser extension:
2263  
2264  1. User obtains API token from Todoist Settings → Integrations → Developer
2265  2. User enters token in extension settings
2266  3. Token stored encrypted in browser.storage.local
2267  4. All API calls use Bearer token authentication
2268  
2269  ### OAuth2 (For Published Extensions)
2270  
2271  If distributing to other users, implement proper OAuth2:
2272  
2273  ```typescript
2274  // OAuth2 flow for browser extensions
2275  const CLIENT_ID = 'your-client-id'
2276  const REDIRECT_URI = browser.identity.getRedirectURL()
2277  
2278  async function initiateOAuth(): Promise<string> {
2279    const authUrl = new URL('https://todoist.com/oauth/authorize')
2280    authUrl.searchParams.set('client_id', CLIENT_ID)
2281    authUrl.searchParams.set('scope', 'data:read_write')
2282    authUrl.searchParams.set('redirect_uri', REDIRECT_URI)
2283    authUrl.searchParams.set('state', crypto.randomUUID())
2284    
2285    const responseUrl = await browser.identity.launchWebAuthFlow({
2286      url: authUrl.toString(),
2287      interactive: true,
2288    })
2289    
2290    const url = new URL(responseUrl)
2291    const code = url.searchParams.get('code')
2292    
2293    // Exchange code for token (requires backend server)
2294    // Token exchange must happen server-side to protect client_secret
2295  }
2296  ```
2297  
2298  ---
2299  
2300  ## Error Handling & Offline Support
2301  
2302  ```typescript
2303  // src/lib/todoist/error-handler.ts
2304  
2305  export class TodoistError extends Error {
2306    constructor(
2307      message: string,
2308      public code: TodoistErrorCode,
2309      public retryable: boolean = false
2310    ) {
2311      super(message)
2312      this.name = 'TodoistError'
2313    }
2314  }
2315  
2316  export enum TodoistErrorCode {
2317    UNAUTHORIZED = 'UNAUTHORIZED',
2318    RATE_LIMITED = 'RATE_LIMITED',
2319    NOT_FOUND = 'NOT_FOUND',
2320    NETWORK_ERROR = 'NETWORK_ERROR',
2321    OFFLINE = 'OFFLINE',
2322    UNKNOWN = 'UNKNOWN',
2323  }
2324  
2325  export function handleApiError(error: unknown): TodoistError {
2326    if (error instanceof TypeError && error.message.includes('fetch')) {
2327      return new TodoistError(
2328        'Network error - please check your connection',
2329        TodoistErrorCode.NETWORK_ERROR,
2330        true
2331      )
2332    }
2333    
2334    if (error instanceof Response) {
2335      switch (error.status) {
2336        case 401:
2337        case 403:
2338          return new TodoistError(
2339            'Authentication failed - please check your API token',
2340            TodoistErrorCode.UNAUTHORIZED
2341          )
2342        case 429:
2343          return new TodoistError(
2344            'Rate limited - please try again in a moment',
2345            TodoistErrorCode.RATE_LIMITED,
2346            true
2347          )
2348        case 404:
2349          return new TodoistError(
2350            'Resource not found',
2351            TodoistErrorCode.NOT_FOUND
2352          )
2353      }
2354    }
2355    
2356    return new TodoistError(
2357      'An unexpected error occurred',
2358      TodoistErrorCode.UNKNOWN
2359    )
2360  }
2361  ```
2362  
2363  ---
2364  
2365  ## Phase Integration
2366  
2367  | Dependency | Reason |
2368  |------------|--------|
2369  | Phase 2 (Dashboard Tab) | UI integration point for TodoistCard |
2370  | Phase 3 (Workspace Management) | Workspace creation/deletion triggers project sync |
2371  | Phase 4 (Context Switching) | Optional: Load tasks on context switch |
2372  | Phase 7 (Settings/Options) | Settings UI for API token |
2373  
2374  **Estimated effort:** 2-3 weeks
2375  
2376  ---
2377  
2378  ## Summary
2379  
2380  | Component | Lines | Purpose |
2381  |-----------|-------|---------|
2382  | types.ts | ~80 | Type definitions |
2383  | client.ts | ~200 | API wrapper with browser compatibility |
2384  | workspace-sync.ts | ~150 | Workspace ↔ Project mapping |
2385  | task-store.ts | ~200 | Reactive Svelte store |
2386  | TodoistCard.svelte | ~150 | Main dashboard card |
2387  | TaskItem.svelte | ~180 | Individual task display |
2388  | TaskEditor.svelte | ~160 | Create/edit form |
2389  | TodoistSettings.svelte | ~200 | Settings modal |
2390  | **Total** | **~1,320** | |
2391  
2392  ---
2393  
2394  *Addendum Version: 1.0 | December 2024*