@fayz-ai/plugin-tasks 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react'
2
- import { ArrowLeft, Trash2, Calendar, ChevronDown } from 'lucide-react'
2
+ import { ArrowLeft, Trash2, Calendar, ChevronDown, Sparkles } from 'lucide-react'
3
3
  import { useTasksStore } from '../TasksContext'
4
4
  import { useTranslation } from '@fayz-ai/core'
5
+ import { useDelegateToAssistant } from '@fayz-ai/saas'
5
6
  import { TaskPriorityBadge } from './TaskPriorityBadge'
6
7
  import { TaskLabelBadges } from './TaskLabelBadge'
7
8
  import { TaskQuickAdd } from './TaskQuickAdd'
@@ -16,8 +17,13 @@ const STATUS_DOT: Record<TaskStatus, string> = {
16
17
  cancelled: 'bg-muted-foreground/60',
17
18
  }
18
19
 
19
- function formatDate(iso: string): string {
20
- return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
20
+ /** Same footer format as the Notes tab: time when today, short date otherwise. */
21
+ function formatDateTime(iso: string): string {
22
+ const d = new Date(iso)
23
+ const sameDay = d.toDateString() === new Date().toDateString()
24
+ return sameDay
25
+ ? d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
26
+ : d.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: '2-digit' })
21
27
  }
22
28
 
23
29
  export function TaskDetail({ taskId }: { taskId: string }) {
@@ -28,6 +34,7 @@ export function TaskDetail({ taskId }: { taskId: string }) {
28
34
  const deleteTask = useTasksStore((s) => s.deleteTask)
29
35
 
30
36
  const task = tasks.find((tt) => tt.id === taskId)
37
+ const delegate = useDelegateToAssistant()
31
38
 
32
39
  const [title, setTitle] = useState('')
33
40
  const [description, setDescription] = useState('')
@@ -72,28 +79,41 @@ export function TaskDetail({ taskId }: { taskId: string }) {
72
79
  }
73
80
 
74
81
  return (
75
- <div className="space-y-3">
76
- {/* Back + title row */}
77
- <div className="flex items-center gap-2">
82
+ // Same frame as the Notes editor: header row with back + icon actions,
83
+ // full-bleed px-4 content, sectioned by hairlines, thin timestamp footer.
84
+ <div className="flex min-h-full flex-col">
85
+ <div className="flex items-center gap-1 border-b border-border/40 px-2 py-1.5">
78
86
  <button
79
87
  type="button"
80
88
  onClick={() => selectTask(null)}
81
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
89
+ className="inline-flex items-center gap-1 rounded-lg px-1.5 py-1 text-[11.5px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
82
90
  >
83
- <ArrowLeft className="h-4 w-4" />
91
+ <ArrowLeft className="h-3.5 w-3.5" />
92
+ {t('tasks.drawer.title')}
93
+ </button>
94
+ <span className="flex-1" />
95
+ <button
96
+ type="button"
97
+ onClick={handleDelete}
98
+ aria-label={t('tasks.detail.delete')}
99
+ title={t('tasks.detail.delete')}
100
+ className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
101
+ >
102
+ <Trash2 className="h-3.5 w-3.5" />
84
103
  </button>
85
- <input
86
- type="text"
87
- value={title}
88
- onChange={(e) => setTitle(e.target.value)}
89
- onBlur={handleTitleBlur}
90
- onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() }}
91
- className="flex-1 bg-transparent text-sm font-semibold outline-none"
92
- />
93
104
  </div>
94
105
 
106
+ <input
107
+ type="text"
108
+ value={title}
109
+ onChange={(e) => setTitle(e.target.value)}
110
+ onBlur={handleTitleBlur}
111
+ onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() }}
112
+ className="bg-transparent px-4 pt-3 text-sm font-semibold text-foreground outline-none"
113
+ />
114
+
95
115
  {/* Inline property chips */}
96
- <div className="flex flex-wrap items-center gap-1.5">
116
+ <div className="flex flex-wrap items-center gap-1.5 px-4 pt-2">
97
117
  {/* Status chip */}
98
118
  <div className="relative">
99
119
  <select
@@ -138,47 +158,52 @@ export function TaskDetail({ taskId }: { taskId: string }) {
138
158
  {task.labels.length > 0 && <TaskLabelBadges labelIds={task.labels} max={3} />}
139
159
  </div>
140
160
 
141
- {/* Description */}
161
+ {/* Description — borderless, fills the middle like the note body */}
142
162
  <textarea
143
163
  value={description}
144
164
  onChange={(e) => setDescription(e.target.value)}
145
165
  onBlur={handleDescriptionBlur}
146
166
  placeholder={t('tasks.detail.descriptionPlaceholder')}
147
- rows={3}
148
- className="w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-xs outline-none resize-none placeholder:text-muted-foreground/50 leading-relaxed"
167
+ className="min-h-[72px] flex-1 resize-none bg-transparent px-4 py-3 text-sm leading-relaxed text-foreground outline-none placeholder:text-muted-foreground/60"
149
168
  />
150
169
 
151
- {/* Subtasks */}
152
- <div>
153
- <p className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider mb-1">
154
- {t('tasks.detail.subtasks')}
155
- </p>
156
- <div className="rounded-md border overflow-hidden">
157
- <TaskQuickAdd parentId={task.id} />
158
- {subtasks.length > 0 && (
159
- <div className="divide-y">
160
- {subtasks.map((st) => (
161
- <TaskCard key={st.id} task={st} />
162
- ))}
163
- </div>
164
- )}
165
- </div>
166
- </div>
167
-
168
- {/* Footer: timestamps + delete */}
169
- <div className="flex items-center justify-between pt-2 border-t">
170
- <span className="text-[10px] text-muted-foreground">
171
- {formatDate(task.createdAt)} · {t('tasks.detail.updated')} {formatDate(task.updatedAt)}
172
- </span>
170
+ {/* Agentic hand-off — same CTA pattern as the Notes tab */}
171
+ <div className="px-4 pb-2">
173
172
  <button
174
173
  type="button"
175
- onClick={handleDelete}
176
- className="inline-flex items-center gap-1 text-[11px] text-muted-foreground hover:text-destructive transition-colors"
174
+ onClick={() =>
175
+ delegate(
176
+ `${t('tasks.detail.chatWithAi.prompt')}\n\n"${task.title}"${task.description ? `\n${task.description}` : ''}${task.dueDate ? `\n(${t('tasks.detail.dueDate')}: ${task.dueDate})` : ''}`,
177
+ )
178
+ }
179
+ className="group flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-primary via-primary/80 to-primary bg-[length:200%_100%] bg-left px-3 py-2 text-[13px] font-semibold text-primary-foreground shadow-sm transition-all duration-500 hover:bg-right hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
177
180
  >
178
- <Trash2 className="h-3 w-3" />
179
- {t('tasks.detail.delete')}
181
+ <Sparkles className="h-4 w-4 transition-transform duration-300 group-hover:rotate-12" />
182
+ {t('tasks.detail.chatWithAi')}
180
183
  </button>
181
184
  </div>
185
+
186
+ {/* Subtasks — same section pattern as the note's todos */}
187
+ <div className="border-t border-border/40 px-4 py-2.5">
188
+ <p className="pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
189
+ {t('tasks.detail.subtasks')}{subtasks.length > 0 ? ` · ${subtasks.length}` : ''}
190
+ </p>
191
+ <div className="-mx-3">
192
+ <TaskQuickAdd parentId={task.id} />
193
+ </div>
194
+ {subtasks.length > 0 && (
195
+ <div className="divide-y divide-border/40">
196
+ {subtasks.map((st) => (
197
+ <TaskCard key={st.id} task={st} />
198
+ ))}
199
+ </div>
200
+ )}
201
+ </div>
202
+
203
+ {/* Footer: timestamps only — delete lives in the header, like a note */}
204
+ <p className="border-t border-border/40 px-4 py-1.5 text-[10px] text-muted-foreground/60">
205
+ {t('tasks.detail.created')} {formatDateTime(task.createdAt)} · {t('tasks.detail.updated')} {formatDateTime(task.updatedAt)}
206
+ </p>
182
207
  </div>
183
208
  )
184
209
  }
@@ -4,45 +4,14 @@ import {
4
4
  } from '@fayz-ai/ui'
5
5
  import { useTasksStore } from '../TasksContext'
6
6
  import { useTranslation } from '@fayz-ai/core'
7
- import { TasksFilterBar } from './TasksFilterBar'
8
- import { TaskQuickAdd } from './TaskQuickAdd'
9
- import { TaskCard } from './TaskCard'
10
- import { TaskStatusGroup } from './TaskStatusGroup'
11
- import { TaskDetail } from './TaskDetail'
12
- import { ClipboardList } from 'lucide-react'
13
- import type { TaskStatus } from '../types'
14
-
15
- const STATUS_ORDER: TaskStatus[] = ['todo', 'in_progress', 'done', 'cancelled']
7
+ import { TasksPanel, useTasksSummaryLine } from './TasksPanel'
16
8
 
9
+ /** Tasks as a sheet — the fallback where the shell has no right rail. */
17
10
  export function TasksDrawer() {
18
11
  const t = useTranslation()
19
12
  const drawerOpen = useTasksStore((s) => s.drawerOpen)
20
13
  const closeDrawer = useTasksStore((s) => s.closeDrawer)
21
- const selectedTaskId = useTasksStore((s) => s.selectedTaskId)
22
- const tasks = useTasksStore((s) => s.tasks)
23
- const tasksLoading = useTasksStore((s) => s.tasksLoading)
24
- const summary = useTasksStore((s) => s.summary)
25
- const activeFilter = useTasksStore((s) => s.activeFilter)
26
-
27
- // Group tasks by status
28
- const grouped = React.useMemo(() => {
29
- const map = new Map<TaskStatus, typeof tasks>()
30
- for (const s of STATUS_ORDER) map.set(s, [])
31
- for (const task of tasks) {
32
- const group = map.get(task.status)
33
- if (group) group.push(task)
34
- }
35
- return map
36
- }, [tasks])
37
-
38
- // Summary line
39
- const summaryLine = React.useMemo(() => {
40
- if (!summary) return ''
41
- const parts: string[] = []
42
- if (summary.overdue > 0) parts.push(`${summary.overdue} ${t('tasks.summary.overdue')}`)
43
- if (summary.dueToday > 0) parts.push(`${summary.dueToday} ${t('tasks.summary.dueToday')}`)
44
- return parts.join(' · ')
45
- }, [summary, t])
14
+ const summaryLine = useTasksSummaryLine()
46
15
 
47
16
  return (
48
17
  <Sheet open={drawerOpen} onOpenChange={(v) => { if (!v) closeDrawer() }}>
@@ -54,39 +23,8 @@ export function TasksDrawer() {
54
23
  </SheetDescription>
55
24
  </SheetHeader>
56
25
 
57
- {!selectedTaskId && <TasksFilterBar />}
58
- {!selectedTaskId && <TaskQuickAdd />}
59
-
60
- <SheetBody className="px-0 py-0">
61
- {selectedTaskId ? (
62
- <div className="px-5 py-4">
63
- <TaskDetail taskId={selectedTaskId} />
64
- </div>
65
- ) : tasksLoading && tasks.length === 0 ? (
66
- <div className="flex items-center justify-center py-16">
67
- <div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
68
- </div>
69
- ) : tasks.length === 0 ? (
70
- <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
71
- <ClipboardList className="h-10 w-10 mb-3 opacity-40" />
72
- <p className="text-sm font-medium">{t('tasks.empty')}</p>
73
- <p className="text-xs mt-1">{t('tasks.empty.description')}</p>
74
- </div>
75
- ) : activeFilter !== 'all' ? (
76
- <div className="py-1">
77
- {tasks.map((task) => (
78
- <div key={task.id} className="px-2">
79
- <TaskCard task={task} />
80
- </div>
81
- ))}
82
- </div>
83
- ) : (
84
- <div className="py-2 space-y-2">
85
- {STATUS_ORDER.filter((s) => s !== 'cancelled' || (grouped.get(s)?.length ?? 0) > 0).map((status) => (
86
- <TaskStatusGroup key={status} status={status} tasks={grouped.get(status) ?? []} />
87
- ))}
88
- </div>
89
- )}
26
+ <SheetBody className="flex flex-col px-0 py-0">
27
+ <TasksPanel />
90
28
  </SheetBody>
91
29
  </SheetContent>
92
30
  </Sheet>
@@ -1,8 +1,16 @@
1
1
  import React from 'react'
2
2
  import { useTranslation } from '@fayz-ai/core'
3
+ import { useTenantPluginSettings } from '@fayz-ai/saas'
4
+
5
+ // Per-tenant defaults = the value that used to be the hardcoded `selected` option.
6
+ // Persisted + re-hydrated per tenant; behavioural consumption is a follow-up.
7
+ const TASKS_DEFAULTS = {
8
+ defaultPriority: 'medium',
9
+ }
3
10
 
4
11
  export function TasksGeneralSettings() {
5
12
  const t = useTranslation()
13
+ const s = useTenantPluginSettings('tasks', TASKS_DEFAULTS)
6
14
 
7
15
  return (
8
16
  <div className="space-y-6">
@@ -16,9 +24,13 @@ export function TasksGeneralSettings() {
16
24
  <div className="space-y-4">
17
25
  <div className="space-y-1.5">
18
26
  <label className="text-sm font-medium">{t('tasks.settings.defaultPriority')}</label>
19
- <select className="w-full max-w-xs rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
27
+ <select
28
+ className="w-full max-w-xs rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm"
29
+ value={s.get('defaultPriority')}
30
+ onChange={(e) => s.set('defaultPriority', e.target.value)}
31
+ >
20
32
  <option value="low">{t('tasks.priority.low')}</option>
21
- <option value="medium" selected>{t('tasks.priority.medium')}</option>
33
+ <option value="medium">{t('tasks.priority.medium')}</option>
22
34
  <option value="high">{t('tasks.priority.high')}</option>
23
35
  <option value="urgent">{t('tasks.priority.urgent')}</option>
24
36
  </select>
@@ -0,0 +1,94 @@
1
+ import React from 'react'
2
+ import { useTasksStore } from '../TasksContext'
3
+ import { useTranslation } from '@fayz-ai/core'
4
+ import { TasksFilterBar } from './TasksFilterBar'
5
+ import { TaskQuickAdd } from './TaskQuickAdd'
6
+ import { TaskCard } from './TaskCard'
7
+ import { TaskStatusGroup } from './TaskStatusGroup'
8
+ import { TaskDetail } from './TaskDetail'
9
+ import { ClipboardList } from 'lucide-react'
10
+ import type { TaskStatus } from '../types'
11
+
12
+ const STATUS_ORDER: TaskStatus[] = ['todo', 'in_progress', 'done', 'cancelled']
13
+
14
+ /** The task list with no box around it — shared by the sheet and the shell's
15
+ * right-rail tab. */
16
+ export function TasksPanel() {
17
+ const t = useTranslation()
18
+ const selectedTaskId = useTasksStore((s) => s.selectedTaskId)
19
+ const tasks = useTasksStore((s) => s.tasks)
20
+ const tasksLoading = useTasksStore((s) => s.tasksLoading)
21
+ const activeFilter = useTasksStore((s) => s.activeFilter)
22
+ const fetchTasks = useTasksStore((s) => s.fetchTasks)
23
+ const fetchLabels = useTasksStore((s) => s.fetchLabels)
24
+ const fetchSummary = useTasksStore((s) => s.fetchSummary)
25
+
26
+ // The rail tab mounts this panel directly — openDrawer() (the sheet path,
27
+ // where fetching used to live) never runs there, so load on mount. Fetches
28
+ // are deduped in the store, so the drawer path doesn't double-load.
29
+ React.useEffect(() => {
30
+ void fetchTasks()
31
+ void fetchLabels()
32
+ void fetchSummary()
33
+ }, [fetchTasks, fetchLabels, fetchSummary])
34
+
35
+ const grouped = React.useMemo(() => {
36
+ const map = new Map<TaskStatus, typeof tasks>()
37
+ for (const s of STATUS_ORDER) map.set(s, [])
38
+ for (const task of tasks) {
39
+ const group = map.get(task.status)
40
+ if (group) group.push(task)
41
+ }
42
+ return map
43
+ }, [tasks])
44
+
45
+ return (
46
+ <>
47
+ {!selectedTaskId && <TasksFilterBar />}
48
+ {!selectedTaskId && <TaskQuickAdd />}
49
+
50
+ <div className="min-h-0 flex-1 overflow-y-auto">
51
+ {selectedTaskId ? (
52
+ <TaskDetail taskId={selectedTaskId} />
53
+ ) : tasksLoading && tasks.length === 0 ? (
54
+ <div className="flex items-center justify-center py-16">
55
+ <div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
56
+ </div>
57
+ ) : tasks.length === 0 ? (
58
+ <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
59
+ <ClipboardList className="mb-3 h-10 w-10 opacity-40" />
60
+ <p className="text-sm font-medium">{t('tasks.empty')}</p>
61
+ <p className="mt-1 text-xs">{t('tasks.empty.description')}</p>
62
+ </div>
63
+ ) : activeFilter !== 'all' ? (
64
+ <div className="py-1">
65
+ {tasks.map((task) => (
66
+ <div key={task.id} className="px-2">
67
+ <TaskCard task={task} />
68
+ </div>
69
+ ))}
70
+ </div>
71
+ ) : (
72
+ <div className="space-y-2 py-2">
73
+ {STATUS_ORDER.filter((s) => s !== 'cancelled' || (grouped.get(s)?.length ?? 0) > 0).map((status) => (
74
+ <TaskStatusGroup key={status} status={status} tasks={grouped.get(status) ?? []} />
75
+ ))}
76
+ </div>
77
+ )}
78
+ </div>
79
+ </>
80
+ )
81
+ }
82
+
83
+ /** One-line summary of what needs attention ("3 atrasadas · 2 para hoje"). */
84
+ export function useTasksSummaryLine(): string {
85
+ const t = useTranslation()
86
+ const summary = useTasksStore((s) => s.summary)
87
+ return React.useMemo(() => {
88
+ if (!summary) return ''
89
+ const parts: string[] = []
90
+ if (summary.overdue > 0) parts.push(`${summary.overdue} ${t('tasks.summary.overdue')}`)
91
+ if (summary.dueToday > 0) parts.push(`${summary.dueToday} ${t('tasks.summary.dueToday')}`)
92
+ return parts.join(' · ')
93
+ }, [summary, t])
94
+ }
@@ -2,8 +2,11 @@ import React, { useEffect } from 'react'
2
2
  import { CheckSquare } from 'lucide-react'
3
3
  import { Button } from '@fayz-ai/ui'
4
4
  import { cn } from '@fayz-ai/ui'
5
+ import { useRightRailPanel, useRightRailStore } from '@fayz-ai/saas'
6
+ import { useTranslation } from '@fayz-ai/core'
5
7
  import { TasksContextProvider, type ResolvedTasksConfig } from '../TasksContext'
6
8
  import { TasksDrawer } from './TasksDrawer'
9
+ import { TasksPanel } from './TasksPanel'
7
10
  import type { TasksDataProvider } from '../data/types'
8
11
  import type { TasksUIState } from '../store'
9
12
  import { useStore, type StoreApi } from 'zustand'
@@ -18,19 +21,61 @@ interface TasksTopbarWidgetProps {
18
21
 
19
22
  export function TasksTopbarButton({ config }: TasksTopbarWidgetProps) {
20
23
  const { tasksConfig, tasksProvider, tasksStore } = config
24
+ const t = useTranslation()
25
+ // The shell's rail when there is one, this plugin's sheet when there is not.
26
+ const railMounted = useRightRailStore((s) => s.mounted)
27
+ // Open tasks (todo + in progress) — the rail tab's red badge.
28
+ const summary = useStore(tasksStore, (s) => s.summary)
29
+ const openCount = summary ? summary.todo + summary.inProgress : 0
30
+
31
+ // Carries its own providers (registered far from here) and must stay
32
+ // referentially stable or the list remounts every render.
33
+ const Panel = React.useMemo(() => {
34
+ const RailPanel = () => (
35
+ <TasksContextProvider config={tasksConfig} provider={tasksProvider} store={tasksStore}>
36
+ <TasksPanel />
37
+ </TasksContextProvider>
38
+ )
39
+ RailPanel.displayName = 'TasksRailPanel'
40
+ return RailPanel
41
+ }, [tasksConfig, tasksProvider, tasksStore])
42
+
43
+ useRightRailPanel(
44
+ React.useMemo(
45
+ () =>
46
+ railMounted
47
+ ? {
48
+ id: 'tasks',
49
+ label: t('tasks.drawer.title') || 'Tasks',
50
+ icon: CheckSquare,
51
+ order: 20,
52
+ badge: { count: openCount, tone: 'destructive' as const },
53
+ Component: Panel,
54
+ }
55
+ : null,
56
+ [railMounted, t, Panel, openCount],
57
+ ),
58
+ )
21
59
 
22
60
  return (
23
61
  <TasksContextProvider config={tasksConfig} provider={tasksProvider} store={tasksStore}>
24
- <TasksTopbarButtonInner store={tasksStore} />
25
- <TasksDrawer />
62
+ <TasksTopbarButtonInner store={tasksStore} railMounted={railMounted} />
63
+ {!railMounted && <TasksDrawer />}
26
64
  </TasksContextProvider>
27
65
  )
28
66
  }
29
67
 
30
- function TasksTopbarButtonInner({ store }: { store: StoreApi<TasksUIState> }) {
68
+ function TasksTopbarButtonInner({
69
+ store,
70
+ railMounted,
71
+ }: {
72
+ store: StoreApi<TasksUIState>
73
+ railMounted: boolean
74
+ }) {
31
75
  const toggleDrawer = useStore(store, (s) => s.toggleDrawer)
32
76
  const fetchSummary = useStore(store, (s) => s.fetchSummary)
33
77
  const summary = useStore(store, (s) => s.summary)
78
+ const togglePanel = useRightRailStore((s) => s.togglePanel)
34
79
 
35
80
  useEffect(() => {
36
81
  fetchSummary()
@@ -43,7 +88,7 @@ function TasksTopbarButtonInner({ store }: { store: StoreApi<TasksUIState> }) {
43
88
  variant="ghost"
44
89
  size="icon"
45
90
  className={cn('relative text-sidebar-foreground/70 hover:bg-sidebar/30 hover:text-sidebar-foreground')}
46
- onClick={toggleDrawer}
91
+ onClick={() => (railMounted ? togglePanel('tasks') : toggleDrawer())}
47
92
  aria-label="Tasks"
48
93
  >
49
94
  <CheckSquare className="h-5 w-5" />
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import { tasksLocales } from './locales'
11
11
  import { tasksRegistries } from './registries'
12
12
  import { TasksGeneralSettings } from './components/TasksGeneralSettings'
13
13
  import { TasksTopbarButton } from './components/TasksTopbarButton'
14
+ import { registerNotesTasksAdapter, NOTE_LABEL_PREFIX } from '@fayz-ai/saas'
14
15
  import { MIGRATION_000_PLG_RENAME, MIGRATION_001_TASKS_BASE } from './migrations'
15
16
 
16
17
  // ---------------------------------------------------------------------------
@@ -66,6 +67,37 @@ export function createTasksPlugin(options?: TasksPluginOptions): PluginManifest
66
67
  )
67
68
  const store = createTasksStore(provider)
68
69
 
70
+ // Notes ↔ Tasks bridge: note todos are real tasks, linked by a `note:<id>`
71
+ // label. The shell's Notes tab consumes this; the Tarefas tab (and its
72
+ // summary badge) stays the single source of truth.
73
+ registerNotesTasksAdapter({
74
+ async createTodo({ title, noteId, noteTitle }) {
75
+ const task = await provider.createTask({
76
+ title,
77
+ description: noteTitle ? `Nota: ${noteTitle}` : undefined,
78
+ labels: [`${NOTE_LABEL_PREFIX}${noteId}`],
79
+ })
80
+ void store.getState().fetchSummary()
81
+ return { id: task.id, title: task.title, done: task.status === 'done' }
82
+ },
83
+ async listTodos(noteId) {
84
+ const tasks = await provider.getTasks({})
85
+ const label = `${NOTE_LABEL_PREFIX}${noteId}`
86
+ return tasks
87
+ .filter((t) => t.labels.includes(label))
88
+ .sort((a, b) => (a.status === 'done' ? 1 : 0) - (b.status === 'done' ? 1 : 0))
89
+ .map((t) => ({ id: t.id, title: t.title, done: t.status === 'done' }))
90
+ },
91
+ async toggleTodo(id: string, done: boolean) {
92
+ await provider.updateTask(id, { status: done ? 'done' : 'todo' })
93
+ void store.getState().fetchSummary()
94
+ },
95
+ async removeTodo(id: string) {
96
+ await provider.deleteTask(id)
97
+ void store.getState().fetchSummary()
98
+ },
99
+ })
100
+
69
101
  return {
70
102
  id: 'tasks',
71
103
  name: config.labels.drawerTitle,
package/src/locales/en.ts CHANGED
@@ -26,6 +26,8 @@ export const en: Record<string, string> = {
26
26
  'tasks.detail.delete': 'Delete Task',
27
27
  'tasks.detail.deleteConfirm': 'Are you sure you want to delete this task?',
28
28
  'tasks.detail.created': 'Created',
29
+ 'tasks.detail.chatWithAi': 'Chat with AI',
30
+ 'tasks.detail.chatWithAi.prompt': 'Take over this task and help me resolve it now, using your tools when needed:',
29
31
  'tasks.detail.updated': 'Updated',
30
32
  'tasks.detail.addSubtask': 'Add subtask...',
31
33
  'tasks.detail.back': 'Back to list',
@@ -26,6 +26,8 @@ export const ptBR: Record<string, string> = {
26
26
  'tasks.detail.delete': 'Excluir Tarefa',
27
27
  'tasks.detail.deleteConfirm': 'Tem certeza que deseja excluir esta tarefa?',
28
28
  'tasks.detail.created': 'Criada em',
29
+ 'tasks.detail.chatWithAi': 'Conversar com IA',
30
+ 'tasks.detail.chatWithAi.prompt': 'Assuma esta tarefa e me ajude a resolver agora, usando suas ferramentas quando precisar:',
29
31
  'tasks.detail.updated': 'Atualizada em',
30
32
  'tasks.detail.addSubtask': 'Adicionar subtarefa...',
31
33
  'tasks.detail.back': 'Voltar à lista',