@tuturuuu/ui 0.5.0 → 0.6.0

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +50 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +21 -1
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  22. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +73 -26
  23. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  24. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +2 -1
  25. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +4 -4
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +298 -34
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +219 -46
  28. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  29. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  30. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  31. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  32. package/src/components/ui/finance/wallets/form.tsx +41 -197
  33. package/src/components/ui/finance/wallets/query-invalidation.ts +1 -0
  34. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  35. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  36. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  37. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  38. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  39. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  40. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  41. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +64 -2
  42. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +42 -35
  43. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  44. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  45. package/src/components/ui/finance/wallets/wallets-page.test.tsx +111 -37
  46. package/src/components/ui/finance/wallets/wallets-page.tsx +38 -78
  47. package/src/components/ui/storefront/accent-button.tsx +33 -0
  48. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  49. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  50. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  51. package/src/components/ui/storefront/image-panel.tsx +40 -0
  52. package/src/components/ui/storefront/index.ts +12 -0
  53. package/src/components/ui/storefront/listing-card.tsx +129 -0
  54. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  55. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  56. package/src/components/ui/storefront/types.ts +99 -0
  57. package/src/components/ui/storefront/utils.ts +90 -0
  58. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  59. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  60. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  61. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  62. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  63. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  64. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  65. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +114 -7
  66. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  67. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  68. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  69. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  70. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  71. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  72. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  73. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  74. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +128 -1
  75. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +104 -69
  76. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  77. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  78. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  79. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  80. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +17 -1
  81. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +151 -111
  82. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  83. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  85. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +584 -53
  86. package/src/hooks/useBoardRealtime.ts +54 -1
  87. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  88. package/src/hooks/useTaskUserRealtime.ts +338 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import '@testing-library/jest-dom/vitest';
6
+ import { fireEvent, render, screen } from '@testing-library/react';
7
+ import type { WorkspaceTaskSuggestionTask } from '@tuturuuu/internal-api/tasks';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import {
10
+ SmartTaskSuggestionsButton,
11
+ SmartTaskSuggestionsPanel,
12
+ } from './smart-task-suggestions-panel';
13
+
14
+ vi.mock('next-intl', () => ({
15
+ useTranslations: () => (key: string, values?: Record<string, unknown>) =>
16
+ values ? `${key}:${JSON.stringify(values)}` : key,
17
+ }));
18
+
19
+ const baseSuggestion: WorkspaceTaskSuggestionTask = {
20
+ id: 'suggestion-1',
21
+ title: 'Prepare launch checklist',
22
+ description: 'Confirm owners and deadlines.',
23
+ priority: 'high',
24
+ listId: 'list-1',
25
+ listName: 'Inbox',
26
+ labelIds: ['label-1'],
27
+ labels: [
28
+ {
29
+ id: 'label-1',
30
+ name: 'Launch',
31
+ color: 'blue',
32
+ created_at: '2026-06-11T00:00:00.000Z',
33
+ },
34
+ ],
35
+ projectIds: ['project-1'],
36
+ projects: [{ id: 'project-1', name: 'Website', status: 'active' }],
37
+ endDate: '2026-06-12T16:59:59.000Z',
38
+ estimationPoints: 3,
39
+ durationMinutes: 90,
40
+ isSplittable: true,
41
+ minSplitDurationMinutes: 30,
42
+ maxSplitDurationMinutes: 60,
43
+ calendarHours: 'work_hours',
44
+ autoSchedule: true,
45
+ reason: 'Launch work with a deadline.',
46
+ };
47
+
48
+ describe('SmartTaskSuggestionsPanel', () => {
49
+ it('renders loading state from the sparkle button and panel', () => {
50
+ render(
51
+ <>
52
+ <SmartTaskSuggestionsButton isLoading={true} onClick={vi.fn()} />
53
+ <SmartTaskSuggestionsPanel
54
+ suggestions={[]}
55
+ selectedSuggestionIds={[]}
56
+ isLoading={true}
57
+ onApplyFirst={vi.fn()}
58
+ onApplySuggestion={vi.fn()}
59
+ onClose={vi.fn()}
60
+ onCreateSelected={vi.fn()}
61
+ onRetry={vi.fn()}
62
+ onToggleSuggestion={vi.fn()}
63
+ />
64
+ </>
65
+ );
66
+
67
+ expect(screen.getByLabelText('smart_suggest')).toBeDisabled();
68
+ expect(screen.getByText('smart_generating')).toBeInTheDocument();
69
+ });
70
+
71
+ it('applies a single suggestion without creating it immediately', () => {
72
+ const onApplySuggestion = vi.fn();
73
+
74
+ render(
75
+ <SmartTaskSuggestionsPanel
76
+ suggestions={[baseSuggestion]}
77
+ selectedSuggestionIds={['suggestion-1']}
78
+ onApplyFirst={vi.fn()}
79
+ onApplySuggestion={onApplySuggestion}
80
+ onClose={vi.fn()}
81
+ onCreateSelected={vi.fn()}
82
+ onRetry={vi.fn()}
83
+ onToggleSuggestion={vi.fn()}
84
+ />
85
+ );
86
+
87
+ fireEvent.click(
88
+ screen.getByRole('button', { name: 'smart_apply_suggestion' })
89
+ );
90
+
91
+ expect(onApplySuggestion).toHaveBeenCalledWith(baseSuggestion);
92
+ expect(
93
+ screen.queryByRole('button', { name: /smart_create_selected/ })
94
+ ).not.toBeInTheDocument();
95
+ });
96
+
97
+ it('creates selected tasks from multiple suggestions', () => {
98
+ const onCreateSelected = vi.fn();
99
+ const onToggleSuggestion = vi.fn();
100
+ const secondSuggestion = {
101
+ ...baseSuggestion,
102
+ id: 'suggestion-2',
103
+ title: 'Draft launch note',
104
+ };
105
+
106
+ render(
107
+ <SmartTaskSuggestionsPanel
108
+ suggestions={[baseSuggestion, secondSuggestion]}
109
+ selectedSuggestionIds={['suggestion-1']}
110
+ onApplyFirst={vi.fn()}
111
+ onApplySuggestion={vi.fn()}
112
+ onClose={vi.fn()}
113
+ onCreateSelected={onCreateSelected}
114
+ onRetry={vi.fn()}
115
+ onToggleSuggestion={onToggleSuggestion}
116
+ />
117
+ );
118
+
119
+ fireEvent.click(screen.getAllByLabelText('smart_select_suggestion')[1]!);
120
+ fireEvent.click(
121
+ screen.getByRole('button', {
122
+ name: 'smart_create_selected:{"count":1}',
123
+ })
124
+ );
125
+
126
+ expect(onToggleSuggestion).toHaveBeenCalledWith('suggestion-2');
127
+ expect(onCreateSelected).toHaveBeenCalledTimes(1);
128
+ });
129
+ });
@@ -0,0 +1,358 @@
1
+ 'use client';
2
+
3
+ import {
4
+ AlertCircle,
5
+ Calendar,
6
+ CheckCircle2,
7
+ Clock,
8
+ Flag,
9
+ FolderKanban,
10
+ ListTodo,
11
+ Loader2,
12
+ Sparkles,
13
+ Tag,
14
+ Timer,
15
+ X,
16
+ } from '@tuturuuu/icons';
17
+ import type { WorkspaceTaskSuggestionTask } from '@tuturuuu/internal-api/tasks';
18
+ import { Badge } from '@tuturuuu/ui/badge';
19
+ import { Button } from '@tuturuuu/ui/button';
20
+ import { Checkbox } from '@tuturuuu/ui/checkbox';
21
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
22
+ import { cn } from '@tuturuuu/utils/format';
23
+ import dayjs from 'dayjs';
24
+ import { useTranslations } from 'next-intl';
25
+
26
+ interface SmartTaskSuggestionsButtonProps {
27
+ disabled?: boolean;
28
+ isLoading?: boolean;
29
+ onClick: () => void;
30
+ }
31
+
32
+ interface SmartTaskSuggestionsPanelProps {
33
+ suggestions: WorkspaceTaskSuggestionTask[];
34
+ selectedSuggestionIds: string[];
35
+ createErrors?: Record<string, string>;
36
+ creatingSuggestionIds?: string[];
37
+ errorMessage?: string | null;
38
+ isCreatingSelected?: boolean;
39
+ isLoading?: boolean;
40
+ onApplyFirst: () => void;
41
+ onApplySuggestion: (suggestion: WorkspaceTaskSuggestionTask) => void;
42
+ onClose: () => void;
43
+ onCreateSelected: () => void;
44
+ onRetry: () => void;
45
+ onToggleSuggestion: (suggestionId: string) => void;
46
+ }
47
+
48
+ function formatDuration(minutes: number) {
49
+ if (minutes < 60) {
50
+ return `${minutes}m`;
51
+ }
52
+
53
+ const hours = Math.floor(minutes / 60);
54
+ const remainder = minutes % 60;
55
+
56
+ return remainder ? `${hours}h ${remainder}m` : `${hours}h`;
57
+ }
58
+
59
+ function SuggestionChip({
60
+ children,
61
+ icon,
62
+ }: {
63
+ children: React.ReactNode;
64
+ icon: React.ReactNode;
65
+ }) {
66
+ return (
67
+ <Badge
68
+ variant="secondary"
69
+ className="h-6 gap-1 rounded-md px-1.5 font-normal text-xs"
70
+ >
71
+ {icon}
72
+ <span className="max-w-28 truncate">{children}</span>
73
+ </Badge>
74
+ );
75
+ }
76
+
77
+ function SuggestionCard({
78
+ creating,
79
+ error,
80
+ multi,
81
+ onApply,
82
+ onToggle,
83
+ selected,
84
+ suggestion,
85
+ }: {
86
+ creating?: boolean;
87
+ error?: string;
88
+ multi: boolean;
89
+ onApply: () => void;
90
+ onToggle: () => void;
91
+ selected: boolean;
92
+ suggestion: WorkspaceTaskSuggestionTask;
93
+ }) {
94
+ const t = useTranslations('ws-task-boards.dialog');
95
+
96
+ return (
97
+ <div
98
+ className={cn(
99
+ 'rounded-lg border bg-background p-3 transition-colors',
100
+ selected && 'border-dynamic-blue/60 bg-dynamic-blue/5'
101
+ )}
102
+ >
103
+ <div className="flex items-start gap-2">
104
+ {multi && (
105
+ <Checkbox
106
+ aria-label={t('smart_select_suggestion')}
107
+ checked={selected}
108
+ className="mt-0.5"
109
+ onCheckedChange={onToggle}
110
+ />
111
+ )}
112
+ <div className="min-w-0 flex-1 space-y-2">
113
+ <div className="flex items-start justify-between gap-2">
114
+ <div className="min-w-0">
115
+ <div className="truncate font-medium text-sm">
116
+ {suggestion.title}
117
+ </div>
118
+ {suggestion.description && (
119
+ <div className="line-clamp-2 text-muted-foreground text-xs">
120
+ {suggestion.description}
121
+ </div>
122
+ )}
123
+ </div>
124
+ {!multi && (
125
+ <Button size="sm" type="button" onClick={onApply}>
126
+ <Sparkles className="h-4 w-4" />
127
+ {t('smart_apply_suggestion')}
128
+ </Button>
129
+ )}
130
+ </div>
131
+
132
+ <div className="flex flex-wrap gap-1.5">
133
+ {suggestion.listName && (
134
+ <SuggestionChip icon={<ListTodo className="h-3 w-3" />}>
135
+ {suggestion.listName}
136
+ </SuggestionChip>
137
+ )}
138
+ {suggestion.priority && (
139
+ <SuggestionChip icon={<Flag className="h-3 w-3" />}>
140
+ {t(`priority.${suggestion.priority}`)}
141
+ </SuggestionChip>
142
+ )}
143
+ {suggestion.endDate && (
144
+ <SuggestionChip icon={<Calendar className="h-3 w-3" />}>
145
+ {dayjs(suggestion.endDate).format('MMM D')}
146
+ </SuggestionChip>
147
+ )}
148
+ {suggestion.durationMinutes && (
149
+ <SuggestionChip icon={<Clock className="h-3 w-3" />}>
150
+ {formatDuration(suggestion.durationMinutes)}
151
+ </SuggestionChip>
152
+ )}
153
+ {suggestion.estimationPoints != null && (
154
+ <SuggestionChip icon={<Timer className="h-3 w-3" />}>
155
+ {suggestion.estimationPoints}
156
+ </SuggestionChip>
157
+ )}
158
+ {suggestion.labels.map((label) => (
159
+ <SuggestionChip key={label.id} icon={<Tag className="h-3 w-3" />}>
160
+ {label.name}
161
+ </SuggestionChip>
162
+ ))}
163
+ {suggestion.projects.map((project) => (
164
+ <SuggestionChip
165
+ key={project.id}
166
+ icon={<FolderKanban className="h-3 w-3" />}
167
+ >
168
+ {project.name}
169
+ </SuggestionChip>
170
+ ))}
171
+ </div>
172
+
173
+ {suggestion.reason && (
174
+ <div className="text-muted-foreground text-xs">
175
+ {suggestion.reason}
176
+ </div>
177
+ )}
178
+
179
+ {error && (
180
+ <div className="flex items-center gap-1.5 text-destructive text-xs">
181
+ <AlertCircle className="h-3.5 w-3.5" />
182
+ {error}
183
+ </div>
184
+ )}
185
+
186
+ {creating && (
187
+ <div className="flex items-center gap-1.5 text-muted-foreground text-xs">
188
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
189
+ {t('smart_creating_task')}
190
+ </div>
191
+ )}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ export function SmartTaskSuggestionsButton({
199
+ disabled,
200
+ isLoading,
201
+ onClick,
202
+ }: SmartTaskSuggestionsButtonProps) {
203
+ const t = useTranslations('ws-task-boards.dialog');
204
+
205
+ return (
206
+ <Tooltip>
207
+ <TooltipTrigger asChild>
208
+ <Button
209
+ type="button"
210
+ variant="ghost"
211
+ size="icon"
212
+ aria-label={t('smart_suggest')}
213
+ disabled={disabled || isLoading}
214
+ className="h-8 w-8 text-muted-foreground hover:text-foreground"
215
+ onClick={onClick}
216
+ >
217
+ {isLoading ? (
218
+ <Loader2 className="h-4 w-4 animate-spin" />
219
+ ) : (
220
+ <Sparkles className="h-4 w-4" />
221
+ )}
222
+ </Button>
223
+ </TooltipTrigger>
224
+ <TooltipContent side="bottom">{t('smart_suggest')}</TooltipContent>
225
+ </Tooltip>
226
+ );
227
+ }
228
+
229
+ export function SmartTaskSuggestionsPanel({
230
+ suggestions,
231
+ selectedSuggestionIds,
232
+ createErrors = {},
233
+ creatingSuggestionIds = [],
234
+ errorMessage,
235
+ isCreatingSelected,
236
+ isLoading,
237
+ onApplyFirst,
238
+ onApplySuggestion,
239
+ onClose,
240
+ onCreateSelected,
241
+ onRetry,
242
+ onToggleSuggestion,
243
+ }: SmartTaskSuggestionsPanelProps) {
244
+ const t = useTranslations('ws-task-boards.dialog');
245
+ const selectedCount = selectedSuggestionIds.length;
246
+
247
+ if (isLoading) {
248
+ return (
249
+ <div className="rounded-lg border bg-muted/30 p-3">
250
+ <div className="flex items-center gap-2 text-sm">
251
+ <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
252
+ <span>{t('smart_generating')}</span>
253
+ </div>
254
+ </div>
255
+ );
256
+ }
257
+
258
+ if (errorMessage) {
259
+ return (
260
+ <div className="rounded-lg border border-destructive/40 bg-destructive/5 p-3">
261
+ <div className="flex items-start justify-between gap-2">
262
+ <div className="flex min-w-0 gap-2">
263
+ <AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
264
+ <div className="min-w-0 space-y-1">
265
+ <div className="font-medium text-sm">
266
+ {t('smart_suggestions_failed')}
267
+ </div>
268
+ <div className="text-muted-foreground text-xs">
269
+ {errorMessage}
270
+ </div>
271
+ </div>
272
+ </div>
273
+ <Button size="sm" type="button" variant="ghost" onClick={onRetry}>
274
+ {t('retry')}
275
+ </Button>
276
+ </div>
277
+ </div>
278
+ );
279
+ }
280
+
281
+ if (!suggestions.length) {
282
+ return null;
283
+ }
284
+
285
+ const multi = suggestions.length > 1;
286
+
287
+ return (
288
+ <div className="rounded-lg border bg-muted/20 p-3">
289
+ <div className="mb-3 flex items-start justify-between gap-2">
290
+ <div className="min-w-0">
291
+ <div className="flex items-center gap-1.5 font-medium text-sm">
292
+ <Sparkles className="h-4 w-4" />
293
+ {multi
294
+ ? t('smart_multiple_suggestions')
295
+ : t('smart_one_suggestion')}
296
+ </div>
297
+ <div className="text-muted-foreground text-xs">
298
+ {multi
299
+ ? t('smart_multiple_suggestions_description')
300
+ : t('smart_one_suggestion_description')}
301
+ </div>
302
+ </div>
303
+ <Button
304
+ type="button"
305
+ variant="ghost"
306
+ size="icon"
307
+ aria-label={t('smart_dismiss')}
308
+ className="h-7 w-7 shrink-0"
309
+ onClick={onClose}
310
+ >
311
+ <X className="h-3.5 w-3.5" />
312
+ </Button>
313
+ </div>
314
+
315
+ <div className="space-y-2">
316
+ {suggestions.map((suggestion) => (
317
+ <SuggestionCard
318
+ key={suggestion.id}
319
+ creating={creatingSuggestionIds.includes(suggestion.id)}
320
+ error={createErrors[suggestion.id]}
321
+ multi={multi}
322
+ selected={selectedSuggestionIds.includes(suggestion.id)}
323
+ suggestion={suggestion}
324
+ onApply={() => onApplySuggestion(suggestion)}
325
+ onToggle={() => onToggleSuggestion(suggestion.id)}
326
+ />
327
+ ))}
328
+ </div>
329
+
330
+ {multi && (
331
+ <div className="mt-3 flex flex-wrap items-center justify-between gap-2">
332
+ <Button
333
+ type="button"
334
+ size="sm"
335
+ variant="secondary"
336
+ onClick={onApplyFirst}
337
+ >
338
+ <CheckCircle2 className="h-4 w-4" />
339
+ {t('smart_apply_first')}
340
+ </Button>
341
+ <Button
342
+ type="button"
343
+ size="sm"
344
+ disabled={!selectedCount || isCreatingSelected}
345
+ onClick={onCreateSelected}
346
+ >
347
+ {isCreatingSelected ? (
348
+ <Loader2 className="h-4 w-4 animate-spin" />
349
+ ) : (
350
+ <Sparkles className="h-4 w-4" />
351
+ )}
352
+ {t('smart_create_selected', { count: selectedCount })}
353
+ </Button>
354
+ </div>
355
+ )}
356
+ </div>
357
+ );
358
+ }
@@ -36,7 +36,7 @@ export interface TaskDescriptionEditorProps {
36
36
  // Refs
37
37
  editorRef: React.RefObject<HTMLDivElement | null>;
38
38
  richTextEditorRef: React.RefObject<HTMLDivElement | null>;
39
- titleInputRef: React.RefObject<HTMLInputElement | null>;
39
+ titleInputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement | null>;
40
40
  lastCursorPositionRef: React.RefObject<number | null>;
41
41
  targetEditorCursorRef: React.RefObject<number | null>;
42
42
  flushEditorPendingRef: React.RefObject<
@@ -222,6 +222,8 @@ interface TaskDialogHeaderProps {
222
222
  onOpenShareDialog?: () => void;
223
223
  /** Whether the dialog is in read-only mode */
224
224
  disabled?: boolean;
225
+ /** Whether task-dependent actions should be disabled while the task hydrates. */
226
+ controlsDisabled?: boolean;
225
227
  /** Callback to scroll the editor to a collaborator's cursor position */
226
228
  onScrollToUserCursor?: (userId: string, displayName: string) => void;
227
229
  }
@@ -262,6 +264,7 @@ export function TaskDialogHeader({
262
264
  isPersonalWorkspace = false,
263
265
  onOpenShareDialog,
264
266
  disabled = false,
267
+ controlsDisabled = false,
265
268
  onScrollToUserCursor,
266
269
  }: TaskDialogHeaderProps) {
267
270
  const t = useTranslations();
@@ -448,7 +451,7 @@ export function TaskDialogHeader({
448
451
  )}
449
452
 
450
453
  {/* Quick Settings */}
451
- {!disabled && (
454
+ {!controlsDisabled && (
452
455
  <QuickSettingsPopover isPersonalWorkspace={isPersonalWorkspace} />
453
456
  )}
454
457
 
@@ -467,10 +470,11 @@ export function TaskDialogHeader({
467
470
  onNavigateBack={onNavigateBack}
468
471
  onOpenShareDialog={onOpenShareDialog}
469
472
  disabled={disabled}
473
+ controlsDisabled={controlsDisabled}
470
474
  />
471
475
 
472
476
  {/* Hide save button in edit mode when realtime is enabled (either cursors or Yjs sync) */}
473
- {!disabled &&
477
+ {!controlsDisabled &&
474
478
  (isCreateMode || (!collaborationMode && !realtimeEnabled)) && (
475
479
  <Tooltip>
476
480
  <TooltipTrigger asChild>
@@ -27,7 +27,9 @@ function renderTaskNameInput({
27
27
  editorElement.tabIndex = -1;
28
28
  editorWrapper.append(editorElement);
29
29
 
30
- const titleInputRef = { current: null };
30
+ const titleInputRef = {
31
+ current: null as HTMLInputElement | HTMLTextAreaElement | null,
32
+ };
31
33
  const editorRef = { current: editorWrapper };
32
34
  const lastCursorPositionRef = { current: null };
33
35
  const targetEditorCursorRef = { current: targetCursor };
@@ -66,6 +68,18 @@ describe('TaskNameInput', () => {
66
68
  vi.restoreAllMocks();
67
69
  });
68
70
 
71
+ it.each([
72
+ 'fullscreen',
73
+ 'compact',
74
+ ] as const)('places the initial %s title caret at the end', (variant) => {
75
+ const { input } = renderTaskNameInput({ variant });
76
+ const expectedCursorPosition = 'APIs for Agent Pi'.length;
77
+
78
+ expect(input).toHaveFocus();
79
+ expect(input).toHaveProperty('selectionStart', expectedCursorPosition);
80
+ expect(input).toHaveProperty('selectionEnd', expectedCursorPosition);
81
+ });
82
+
69
83
  it('defers focusing the description editor after Enter', () => {
70
84
  const { focusEditor, targetEditorCursorRef, input } = renderTaskNameInput();
71
85
 
@@ -153,9 +167,11 @@ describe('TaskNameInput', () => {
153
167
 
154
168
  const eventAllowed = fireEvent.keyDown(input, { key: 'Enter' });
155
169
 
170
+ expect(input.tagName).toBe('TEXTAREA');
156
171
  expect(eventAllowed).toBe(false);
157
172
  expect(onSubmit).toHaveBeenCalledTimes(1);
158
173
  expect(targetEditorCursorRef.current).toBe(12);
174
+ expect(input).toHaveProperty('value', 'APIs for Agent Pi');
159
175
 
160
176
  vi.runOnlyPendingTimers();
161
177