stagent 0.1.11 → 0.1.13

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 (145) hide show
  1. package/README.md +74 -49
  2. package/package.json +3 -2
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-workflow-confirm.png +0 -0
  13. package/public/readme/home-below-fold.png +0 -0
  14. package/public/readme/home-list.png +0 -0
  15. package/public/readme/inbox-list.png +0 -0
  16. package/public/readme/playbook-list.png +0 -0
  17. package/public/readme/profiles-list.png +0 -0
  18. package/public/readme/settings-list.png +0 -0
  19. package/public/readme/workflows-list.png +0 -0
  20. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  21. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  22. package/src/__tests__/e2e/helpers.ts +286 -0
  23. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  24. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  25. package/src/__tests__/e2e/setup.ts +156 -0
  26. package/src/__tests__/e2e/single-task.test.ts +170 -0
  27. package/src/app/api/command-palette/recent/route.ts +41 -18
  28. package/src/app/api/context/batch/route.ts +44 -0
  29. package/src/app/api/permissions/presets/route.ts +80 -0
  30. package/src/app/api/playbook/status/route.ts +15 -0
  31. package/src/app/api/profiles/route.ts +23 -20
  32. package/src/app/api/settings/pricing/route.ts +15 -0
  33. package/src/app/api/tasks/[id]/route.ts +54 -3
  34. package/src/app/api/workflows/[id]/route.ts +43 -4
  35. package/src/app/api/workflows/[id]/status/route.ts +70 -2
  36. package/src/app/api/workflows/from-assist/route.ts +6 -32
  37. package/src/app/costs/page.tsx +53 -43
  38. package/src/app/dashboard/page.tsx +59 -21
  39. package/src/app/documents/[id]/page.tsx +10 -8
  40. package/src/app/globals.css +11 -0
  41. package/src/app/page.tsx +60 -3
  42. package/src/app/playbook/[slug]/page.tsx +76 -0
  43. package/src/app/playbook/page.tsx +54 -0
  44. package/src/app/profiles/page.tsx +7 -4
  45. package/src/app/settings/page.tsx +2 -2
  46. package/src/app/tasks/[id]/page.tsx +22 -2
  47. package/src/components/costs/cost-dashboard.tsx +226 -320
  48. package/src/components/dashboard/activity-feed.tsx +6 -2
  49. package/src/components/dashboard/greeting.tsx +3 -1
  50. package/src/components/dashboard/priority-queue.tsx +58 -9
  51. package/src/components/dashboard/stats-cards.tsx +16 -2
  52. package/src/components/documents/document-chip-bar.tsx +183 -0
  53. package/src/components/documents/document-content-renderer.tsx +146 -0
  54. package/src/components/documents/document-detail-view.tsx +16 -239
  55. package/src/components/documents/image-zoom-view.tsx +60 -0
  56. package/src/components/documents/smart-extracted-text.tsx +47 -0
  57. package/src/components/documents/utils.ts +70 -0
  58. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  59. package/src/components/notifications/inbox-list.tsx +4 -5
  60. package/src/components/notifications/notification-item.tsx +73 -6
  61. package/src/components/notifications/pending-approval-host.tsx +63 -14
  62. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  63. package/src/components/playbook/journey-card.tsx +110 -0
  64. package/src/components/playbook/playbook-action-button.tsx +22 -0
  65. package/src/components/playbook/playbook-browser.tsx +143 -0
  66. package/src/components/playbook/playbook-card.tsx +102 -0
  67. package/src/components/playbook/playbook-detail-view.tsx +225 -0
  68. package/src/components/playbook/playbook-homepage.tsx +142 -0
  69. package/src/components/playbook/playbook-toc.tsx +90 -0
  70. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  71. package/src/components/playbook/related-docs.tsx +30 -0
  72. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  73. package/src/components/profiles/context-proposal-review.tsx +7 -3
  74. package/src/components/profiles/learned-context-panel.tsx +116 -8
  75. package/src/components/profiles/profile-browser.tsx +1 -0
  76. package/src/components/profiles/profile-card.tsx +16 -8
  77. package/src/components/profiles/profile-detail-view.tsx +12 -4
  78. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  79. package/src/components/settings/api-key-form.tsx +5 -43
  80. package/src/components/settings/auth-config-section.tsx +10 -6
  81. package/src/components/settings/auth-status-badge.tsx +8 -0
  82. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  83. package/src/components/settings/connection-test-control.tsx +63 -0
  84. package/src/components/settings/permissions-section.tsx +85 -75
  85. package/src/components/settings/permissions-sections.tsx +24 -0
  86. package/src/components/settings/presets-section.tsx +159 -0
  87. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  88. package/src/components/shared/app-sidebar.tsx +4 -2
  89. package/src/components/shared/command-palette.tsx +30 -0
  90. package/src/components/shared/light-markdown.tsx +134 -0
  91. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
  92. package/src/components/tasks/ai-assist-panel.tsx +108 -78
  93. package/src/components/tasks/content-preview.tsx +2 -1
  94. package/src/components/tasks/kanban-board.tsx +57 -5
  95. package/src/components/tasks/kanban-column.tsx +34 -23
  96. package/src/components/tasks/task-bento-cell.tsx +50 -0
  97. package/src/components/tasks/task-bento-grid.tsx +155 -0
  98. package/src/components/tasks/task-card.tsx +14 -16
  99. package/src/components/tasks/task-chip-bar.tsx +207 -0
  100. package/src/components/tasks/task-detail-view.tsx +42 -190
  101. package/src/components/tasks/task-result-renderer.tsx +33 -0
  102. package/src/components/workflows/blueprint-gallery.tsx +19 -12
  103. package/src/components/workflows/blueprint-preview.tsx +8 -1
  104. package/src/components/workflows/loop-status-view.tsx +2 -4
  105. package/src/components/workflows/swarm-dashboard.tsx +2 -3
  106. package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
  107. package/src/components/workflows/workflow-full-output.tsx +80 -0
  108. package/src/components/workflows/workflow-kanban-card.tsx +121 -0
  109. package/src/components/workflows/workflow-list.tsx +47 -42
  110. package/src/components/workflows/workflow-status-view.tsx +163 -16
  111. package/src/lib/agents/learned-context.ts +27 -15
  112. package/src/lib/agents/learning-session.ts +354 -0
  113. package/src/lib/agents/pattern-extractor.ts +19 -0
  114. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  115. package/src/lib/agents/profiles/sort.ts +7 -0
  116. package/src/lib/constants/card-icons.tsx +202 -0
  117. package/src/lib/constants/prose-styles.ts +7 -0
  118. package/src/lib/constants/settings.ts +1 -0
  119. package/src/lib/constants/task-status.ts +3 -0
  120. package/src/lib/db/schema.ts +3 -0
  121. package/src/lib/docs/adoption.ts +105 -0
  122. package/src/lib/docs/journey-tracker.ts +21 -0
  123. package/src/lib/docs/reader.ts +107 -0
  124. package/src/lib/docs/types.ts +54 -0
  125. package/src/lib/docs/usage-stage.ts +60 -0
  126. package/src/lib/documents/context-builder.ts +41 -0
  127. package/src/lib/notifications/actionable.ts +18 -10
  128. package/src/lib/queries/chart-data.ts +20 -1
  129. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  130. package/src/lib/settings/budget-guardrails.ts +213 -85
  131. package/src/lib/settings/permission-presets.ts +150 -0
  132. package/src/lib/settings/runtime-setup.ts +71 -0
  133. package/src/lib/usage/__tests__/ledger.test.ts +2 -2
  134. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  135. package/src/lib/usage/ledger.ts +1 -1
  136. package/src/lib/usage/pricing-registry.ts +570 -0
  137. package/src/lib/usage/pricing.ts +15 -95
  138. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  139. package/src/lib/utils/learned-context-history.ts +150 -0
  140. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  141. package/src/lib/validators/settings.ts +3 -9
  142. package/src/lib/workflows/engine.ts +75 -61
  143. package/src/lib/workflows/types.ts +2 -0
  144. package/tsconfig.json +2 -1
  145. package/src/components/documents/document-preview.tsx +0 -68
@@ -0,0 +1,134 @@
1
+ import { type ReactNode } from "react";
2
+
3
+ interface LightMarkdownProps {
4
+ content: string;
5
+ className?: string;
6
+ maxHeight?: string;
7
+ lineClamp?: number;
8
+ textSize?: "xs" | "sm";
9
+ stripBracketTags?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Lightweight markdown renderer for short agent output.
14
+ * Handles: headers, bullet lists, paragraphs, **bold**, `code`.
15
+ * For full markdown (tables, code fences, GFM), use ReactMarkdown instead.
16
+ */
17
+ export function LightMarkdown({
18
+ content,
19
+ className = "",
20
+ maxHeight,
21
+ lineClamp,
22
+ textSize = "xs",
23
+ stripBracketTags = false,
24
+ }: LightMarkdownProps) {
25
+ const sizeClass = textSize === "sm" ? "text-sm" : "text-xs";
26
+
27
+ const blocks = content.split(/\n{2,}/);
28
+
29
+ const rendered = blocks.map((block, i) => {
30
+ let text = block.trim();
31
+ if (!text) return null;
32
+
33
+ if (stripBracketTags) {
34
+ text = text.replace(/\s*\[.*?\]\s*/g, " ").trim();
35
+ }
36
+
37
+ // Header: ### Title
38
+ const headerMatch = text.match(/^(#{1,4})\s+(.+)/);
39
+ if (headerMatch) {
40
+ return (
41
+ <p key={i} className="font-semibold text-foreground">
42
+ {formatInline(headerMatch[2])}
43
+ </p>
44
+ );
45
+ }
46
+
47
+ // Bullet list: lines starting with - or *
48
+ const lines = text.split("\n");
49
+ const isList = lines.every(
50
+ (l) => /^\s*[-*]\s+/.test(l) || l.trim() === ""
51
+ );
52
+ if (isList && lines.some((l) => /^\s*[-*]\s+/.test(l))) {
53
+ return (
54
+ <ul key={i} className="list-disc pl-4 space-y-0.5 text-muted-foreground">
55
+ {lines
56
+ .filter((l) => /^\s*[-*]\s+/.test(l))
57
+ .map((l, j) => (
58
+ <li key={j}>{formatInline(l.replace(/^\s*[-*]\s+/, ""))}</li>
59
+ ))}
60
+ </ul>
61
+ );
62
+ }
63
+
64
+ // Paragraph
65
+ return (
66
+ <p key={i} className="leading-relaxed text-muted-foreground">
67
+ {formatInline(text)}
68
+ </p>
69
+ );
70
+ });
71
+
72
+ // Overflow / clamp styles
73
+ let containerClass = `${sizeClass} space-y-2 ${className}`;
74
+ const style: React.CSSProperties = {};
75
+
76
+ if (lineClamp) {
77
+ // Approximate: each "line" ~1.25rem at text-xs, ~1.5rem at text-sm
78
+ const lineHeight = textSize === "sm" ? 1.5 : 1.25;
79
+ style.maxHeight = `${lineClamp * lineHeight}rem`;
80
+ style.overflow = "hidden";
81
+ } else if (maxHeight) {
82
+ containerClass += ` ${maxHeight} overflow-auto`;
83
+ }
84
+
85
+ return (
86
+ <div className={containerClass} style={style}>
87
+ {rendered}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ /** Format inline **bold** and `code` spans */
93
+ function formatInline(text: string): ReactNode {
94
+ // Split on **bold** and `code` patterns
95
+ const parts: ReactNode[] = [];
96
+ const regex = /(\*\*(.+?)\*\*|`([^`]+)`)/g;
97
+ let lastIndex = 0;
98
+ let match: RegExpExecArray | null;
99
+
100
+ while ((match = regex.exec(text)) !== null) {
101
+ // Text before the match
102
+ if (match.index > lastIndex) {
103
+ parts.push(text.slice(lastIndex, match.index));
104
+ }
105
+
106
+ if (match[2]) {
107
+ // **bold**
108
+ parts.push(
109
+ <strong key={match.index} className="font-semibold text-foreground">
110
+ {match[2]}
111
+ </strong>
112
+ );
113
+ } else if (match[3]) {
114
+ // `code`
115
+ parts.push(
116
+ <code
117
+ key={match.index}
118
+ className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]"
119
+ >
120
+ {match[3]}
121
+ </code>
122
+ );
123
+ }
124
+
125
+ lastIndex = match.index + match[0].length;
126
+ }
127
+
128
+ // Remaining text
129
+ if (lastIndex < text.length) {
130
+ parts.push(text.slice(lastIndex));
131
+ }
132
+
133
+ return parts.length === 1 ? parts[0] : parts;
134
+ }
@@ -49,7 +49,7 @@ describe("kanban board accessibility", () => {
49
49
  />
50
50
  );
51
51
 
52
- const announcement = screen.getByText("Showing 1 task on the kanban board.");
52
+ const announcement = screen.getByText("Showing 1 item on the kanban board.");
53
53
  const board = screen.getByRole("region", { name: "Kanban board" });
54
54
 
55
55
  expect(announcement).toHaveAttribute("aria-live", "polite");
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
4
4
  import { Button } from "@/components/ui/button";
5
5
  import { Card, CardContent } from "@/components/ui/card";
6
6
  import { Badge } from "@/components/ui/badge";
7
- import { Sparkles, Check, X, GitBranch } from "lucide-react";
7
+ import { Sparkles, Check, X, GitBranch, Info } from "lucide-react";
8
8
 
9
9
  interface TaskSuggestion {
10
10
  title: string;
@@ -154,9 +154,14 @@ export function AIAssistPanel({
154
154
  );
155
155
  }
156
156
 
157
+ const showWorkflowCTA =
158
+ onCreateWorkflow &&
159
+ result.breakdown.length >= 2 &&
160
+ result.recommendedPattern !== "single";
161
+
157
162
  return (
158
- <div className="pt-2 space-y-3">
159
- {/* Header row — full width */}
163
+ <div className="p-4 space-y-4">
164
+ {/* Header row */}
160
165
  <div className="flex items-center gap-2 flex-wrap">
161
166
  <Sparkles className="h-4 w-4 text-primary" />
162
167
  <span className="text-sm font-medium">AI Suggestions</span>
@@ -171,86 +176,111 @@ export function AIAssistPanel({
171
176
  )}
172
177
  </div>
173
178
 
174
- {/* Two-column grid for cards */}
175
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
176
- {/* Improved description */}
177
- <Card>
178
- <CardContent className="p-3">
179
- <div className="flex items-center justify-between mb-1">
180
- <span className="text-xs font-medium text-muted-foreground">
181
- Improved Description
182
- </span>
183
- <Button
184
- type="button"
185
- variant="ghost"
186
- size="sm"
187
- className="h-6 text-xs"
188
- onClick={() => {
189
- onApplyDescription(result.improvedDescription);
190
- setDescriptionApplied(true);
191
- }}
192
- disabled={descriptionApplied}
193
- >
194
- {descriptionApplied ? (
195
- <><Check className="h-3 w-3 mr-1" /> Applied</>
196
- ) : (
197
- "Apply"
198
- )}
199
- </Button>
179
+ {/* Improved description full width */}
180
+ <Card>
181
+ <CardContent className="p-3">
182
+ <div className="flex items-center justify-between mb-1">
183
+ <span className="text-xs font-medium text-muted-foreground">
184
+ Improved Description
185
+ </span>
186
+ <Button
187
+ type="button"
188
+ variant="ghost"
189
+ size="sm"
190
+ className="h-6 text-xs"
191
+ onClick={() => {
192
+ onApplyDescription(result.improvedDescription);
193
+ setDescriptionApplied(true);
194
+ }}
195
+ disabled={descriptionApplied}
196
+ >
197
+ {descriptionApplied ? (
198
+ <><Check className="h-3 w-3 mr-1" /> Applied</>
199
+ ) : (
200
+ "Apply"
201
+ )}
202
+ </Button>
203
+ </div>
204
+ <p className="text-sm">{result.improvedDescription}</p>
205
+ </CardContent>
206
+ </Card>
207
+
208
+ {/* Reasoning + Workflow CTA combined callout */}
209
+ {(result.reasoning || showWorkflowCTA) && (
210
+ <div className="rounded-md border bg-muted/50 p-3 space-y-2.5">
211
+ {result.reasoning && (
212
+ <div className="flex gap-2">
213
+ <Info className="h-3.5 w-3.5 mt-0.5 shrink-0 text-muted-foreground" />
214
+ <p className="text-xs text-muted-foreground">
215
+ {result.reasoning}
216
+ {result.complexity === "complex" && result.recommendedPattern !== "single"
217
+ ? " Complex tasks benefit from workflow execution — each step builds on the previous result."
218
+ : ""}
219
+ </p>
220
+ </div>
221
+ )}
222
+ {showWorkflowCTA && (
223
+ <Button
224
+ type="button"
225
+ variant="outline"
226
+ size="sm"
227
+ className="w-full justify-start h-auto py-2 border-primary/50 bg-background"
228
+ onClick={() => onCreateWorkflow(result)}
229
+ >
230
+ <div className="text-left flex-1">
231
+ <div className="flex items-center gap-1.5">
232
+ <GitBranch className="h-3 w-3" />
233
+ <span className="text-xs font-medium">Create as Workflow</span>
234
+ <Badge variant="default" className="text-[10px] h-4 px-1">
235
+ Recommended
236
+ </Badge>
237
+ </div>
238
+ <div className="text-[11px] text-muted-foreground font-normal mt-0.5">
239
+ Runs as {patternLabels[result.recommendedPattern]?.toLowerCase() ?? result.recommendedPattern} — each step receives prior output
240
+ </div>
241
+ </div>
242
+ </Button>
243
+ )}
244
+ </div>
245
+ )}
246
+
247
+ {/* Breakdown as flat numbered list */}
248
+ {result.breakdown.length > 0 && (
249
+ <>
250
+ {/* "or" divider — only show when workflow CTA is present */}
251
+ {showWorkflowCTA && (
252
+ <div className="flex items-center gap-3">
253
+ <div className="flex-1 border-t" />
254
+ <span className="text-xs text-muted-foreground">or create as individual tasks</span>
255
+ <div className="flex-1 border-t" />
200
256
  </div>
201
- <p className="text-sm">{result.improvedDescription}</p>
202
- </CardContent>
203
- </Card>
257
+ )}
204
258
 
205
- {/* Task breakdown */}
206
- {result.breakdown.length > 0 && (
207
- <Card>
208
- <CardContent className="p-3">
209
- <div className="flex items-center justify-between mb-2">
210
- <span className="text-xs font-medium text-muted-foreground">
211
- Suggested Breakdown ({result.breakdown.length} sub-tasks)
259
+ <div className="space-y-2.5">
260
+ {result.breakdown.map((sub, i) => (
261
+ <div key={i} className="flex gap-2.5">
262
+ <span className="text-xs font-medium text-muted-foreground mt-0.5 shrink-0 w-4 text-right">
263
+ {i + 1}.
212
264
  </span>
213
- <div className="flex gap-1">
214
- {onCreateWorkflow &&
215
- result.breakdown.length >= 2 &&
216
- result.recommendedPattern !== "single" && (
217
- <Button
218
- type="button"
219
- variant="ghost"
220
- size="sm"
221
- className="h-6 text-xs"
222
- onClick={() => onCreateWorkflow(result)}
223
- >
224
- <GitBranch className="h-3 w-3 mr-1" />
225
- Workflow
226
- </Button>
227
- )}
228
- <Button
229
- type="button"
230
- variant="ghost"
231
- size="sm"
232
- className="h-6 text-xs"
233
- onClick={() => onCreateSubtasks(result.breakdown)}
234
- >
235
- Create All
236
- </Button>
265
+ <div className="min-w-0">
266
+ <span className="text-sm font-medium">{sub.title}</span>
267
+ <p className="text-xs text-muted-foreground">{sub.description}</p>
237
268
  </div>
238
269
  </div>
239
- <div className="max-h-60 overflow-y-auto space-y-1.5">
240
- {result.breakdown.map((sub, i) => (
241
- <div key={i} className="text-sm">
242
- <span className="font-medium">{sub.title}</span>
243
- <p className="text-xs text-muted-foreground">{sub.description}</p>
244
- </div>
245
- ))}
246
- </div>
247
- </CardContent>
248
- </Card>
249
- )}
250
- </div>
270
+ ))}
271
+ </div>
251
272
 
252
- {/* Reasoning + Dismiss — full width */}
253
- <p className="text-xs text-muted-foreground">{result.reasoning}</p>
273
+ <Button
274
+ type="button"
275
+ variant="outline"
276
+ size="sm"
277
+ className="w-full"
278
+ onClick={() => onCreateSubtasks(result.breakdown)}
279
+ >
280
+ Create {result.breakdown.length} Independent Tasks
281
+ </Button>
282
+ </>
283
+ )}
254
284
 
255
285
  <Button
256
286
  type="button"
@@ -260,7 +290,7 @@ export function AIAssistPanel({
260
290
  setResult(null);
261
291
  onResultChange?.(false);
262
292
  }}
263
- className="w-full"
293
+ className="w-full text-muted-foreground"
264
294
  >
265
295
  <X className="h-3 w-3 mr-1" /> Dismiss
266
296
  </Button>
@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Copy, Download, Maximize2, Minimize2 } from "lucide-react";
9
9
  import { toast } from "sonner";
10
+ import { PROSE_READER } from "@/lib/constants/prose-styles";
10
11
 
11
12
  interface ContentPreviewProps {
12
13
  content: string;
@@ -75,7 +76,7 @@ export function ContentPreview({ content, contentType }: ContentPreviewProps) {
75
76
  {content}
76
77
  </pre>
77
78
  ) : contentType === "markdown" ? (
78
- <div className={`prose prose-sm dark:prose-invert max-w-none overflow-auto ${heightClass}`}>
79
+ <div className={`${PROSE_READER} overflow-auto ${heightClass}`}>
79
80
  <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
80
81
  </div>
81
82
  ) : (
@@ -31,6 +31,7 @@ import { EmptyBoard } from "./empty-board";
31
31
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
32
32
  import { COLUMN_ORDER, isValidDragTransition, type TaskStatus } from "@/lib/constants/task-status";
33
33
  import { usePersistedState } from "@/hooks/use-persisted-state";
34
+ import type { WorkflowKanbanItem } from "@/components/workflows/workflow-kanban-card";
34
35
 
35
36
  type SortOrder = "priority" | "created-desc" | "created-asc" | "title-asc";
36
37
 
@@ -54,12 +55,34 @@ export function compareTasks(a: TaskItem, b: TaskItem, order: SortOrder): number
54
55
  }
55
56
  }
56
57
 
58
+ /** Map workflow status to kanban column */
59
+ function workflowStatusToColumn(status: string): TaskStatus {
60
+ switch (status) {
61
+ case "draft":
62
+ case "paused":
63
+ return "planned";
64
+ case "active":
65
+ return "running";
66
+ case "completed":
67
+ return "completed";
68
+ case "failed":
69
+ return "failed";
70
+ default:
71
+ return "planned";
72
+ }
73
+ }
74
+
75
+ export type KanbanItem =
76
+ | (TaskItem & { type?: "task" })
77
+ | WorkflowKanbanItem;
78
+
57
79
  interface KanbanBoardProps {
58
80
  initialTasks: TaskItem[];
81
+ initialWorkflows?: WorkflowKanbanItem[];
59
82
  projects: { id: string; name: string }[];
60
83
  }
61
84
 
62
- export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
85
+ export function KanbanBoard({ initialTasks, initialWorkflows = [], projects }: KanbanBoardProps) {
63
86
  const dndId = useId();
64
87
  const router = useRouter();
65
88
  const [tasks, setTasks] = useState<TaskItem[]>(() => {
@@ -74,13 +97,16 @@ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
74
97
  return initialTasks;
75
98
  }
76
99
  });
100
+ const [workflowItems] = useState<WorkflowKanbanItem[]>(initialWorkflows);
77
101
  const [exitingIds, setExitingIds] = useState<Set<string>>(new Set());
78
102
  const [activeTask, setActiveTask] = useState<TaskItem | null>(null);
79
103
  const [projectFilter, setProjectFilter] = usePersistedState("stagent-project-filter", "all");
80
104
  const [statusFilter, setStatusFilter] = useState("all");
81
105
  const [sortOrder, setSortOrder] = usePersistedState<SortOrder>("stagent-sort-order", "priority");
106
+
107
+ const totalItems = tasks.length + workflowItems.length;
82
108
  const [announcement, setAnnouncement] = useState(
83
- `Showing ${initialTasks.length} task${initialTasks.length === 1 ? "" : "s"} on the kanban board.`
109
+ `Showing ${totalItems} item${totalItems === 1 ? "" : "s"} on the kanban board.`
84
110
  );
85
111
  const hasAnnouncedFilters = useRef(false);
86
112
 
@@ -144,21 +170,34 @@ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
144
170
  return true;
145
171
  });
146
172
 
173
+ // Filter workflows by project and status (mapped to column)
174
+ const filteredWorkflows = workflowItems.filter((w) => {
175
+ if (projectFilter !== "all") {
176
+ // Workflows don't have projectId on the kanban item directly,
177
+ // but we pass projectName — filter by checking if name matches
178
+ // For now, skip project filter for workflows (they show in all)
179
+ // TODO: add projectId to WorkflowKanbanItem if needed
180
+ }
181
+ if (statusFilter !== "all" && workflowStatusToColumn(w.status) !== statusFilter) return false;
182
+ return true;
183
+ });
184
+
147
185
  useEffect(() => {
148
186
  if (!hasAnnouncedFilters.current) {
149
187
  hasAnnouncedFilters.current = true;
150
188
  return;
151
189
  }
152
190
 
191
+ const total = filteredTasks.length + filteredWorkflows.length;
153
192
  const projectName =
154
193
  projectFilter === "all"
155
194
  ? "all projects"
156
195
  : projects.find((project) => project.id === projectFilter)?.name ?? "the selected project";
157
196
  const statusName = statusFilter === "all" ? "all statuses" : statusFilter;
158
197
  setAnnouncement(
159
- `Showing ${filteredTasks.length} task${filteredTasks.length === 1 ? "" : "s"} for ${projectName} with ${statusName}.`
198
+ `Showing ${total} item${total === 1 ? "" : "s"} for ${projectName} with ${statusName}.`
160
199
  );
161
- }, [filteredTasks.length, projectFilter, projects, statusFilter]);
200
+ }, [filteredTasks.length, filteredWorkflows.length, projectFilter, projects, statusFilter]);
162
201
 
163
202
  const sensors = useSensors(
164
203
  useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
@@ -304,6 +343,7 @@ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
304
343
 
305
344
  const sortedTasks = [...filteredTasks].sort((a, b) => compareTasks(a, b, sortOrder));
306
345
 
346
+ // Group tasks by column
307
347
  const groupedTasks = COLUMN_ORDER.reduce(
308
348
  (acc, status) => {
309
349
  acc[status] = sortedTasks.filter((t) => t.status === status);
@@ -312,6 +352,17 @@ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
312
352
  {} as Record<TaskStatus, TaskItem[]>
313
353
  );
314
354
 
355
+ // Group workflows by mapped column
356
+ const groupedWorkflows = COLUMN_ORDER.reduce(
357
+ (acc, status) => {
358
+ acc[status] = filteredWorkflows.filter(
359
+ (w) => workflowStatusToColumn(w.status) === status
360
+ );
361
+ return acc;
362
+ },
363
+ {} as Record<TaskStatus, WorkflowKanbanItem[]>
364
+ );
365
+
315
366
  const filterBar = (
316
367
  <div className="flex items-center gap-2">
317
368
  {projects.length > 0 && (
@@ -365,7 +416,7 @@ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
365
416
  </Link>
366
417
  );
367
418
 
368
- if (tasks.length === 0) {
419
+ if (tasks.length === 0 && workflowItems.length === 0) {
369
420
  return (
370
421
  <div>
371
422
  <div className="flex items-center justify-between mb-6 gap-4 flex-wrap">
@@ -428,6 +479,7 @@ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
428
479
  key={status}
429
480
  status={status}
430
481
  tasks={groupedTasks[status]}
482
+ workflows={groupedWorkflows[status]}
431
483
  exitingIds={exitingIds}
432
484
  onTaskClick={handleTaskClick}
433
485
  onAddTask={status === "planned" ? () => router.push("/tasks/new") : undefined}
@@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Inbox, Plus, CheckSquare, Square, ArrowRight, Play, Trash2 } from "lucide-react";
9
9
  import { TaskCard, type TaskItem } from "./task-card";
10
+ import { WorkflowKanbanCard, type WorkflowKanbanItem } from "@/components/workflows/workflow-kanban-card";
10
11
  import type { TaskStatus } from "@/lib/constants/task-status";
11
12
 
12
13
  const columnLabels: Record<string, string> = {
@@ -20,6 +21,7 @@ const columnLabels: Record<string, string> = {
20
21
  export function KanbanColumn({
21
22
  status,
22
23
  tasks,
24
+ workflows = [],
23
25
  exitingIds,
24
26
  onTaskClick,
25
27
  onAddTask,
@@ -30,6 +32,7 @@ export function KanbanColumn({
30
32
  }: {
31
33
  status: TaskStatus;
32
34
  tasks: TaskItem[];
35
+ workflows?: WorkflowKanbanItem[];
33
36
  exitingIds?: Set<string>;
34
37
  onTaskClick: (task: TaskItem) => void;
35
38
  onAddTask?: () => void;
@@ -40,6 +43,7 @@ export function KanbanColumn({
40
43
  }) {
41
44
  const { setNodeRef, isOver } = useDroppable({ id: status });
42
45
  const label = columnLabels[status] ?? status;
46
+ const totalCount = tasks.length + workflows.length;
43
47
 
44
48
  const [selectMode, setSelectMode] = useState(false);
45
49
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@@ -120,12 +124,12 @@ export function KanbanColumn({
120
124
  ) : null;
121
125
 
122
126
  return (
123
- <div className="group/col flex flex-col min-w-64 max-w-80 flex-1 shrink-0" role="group" aria-label={`${label} column, ${tasks.length} tasks`}>
127
+ <div className="group/col flex flex-col min-w-64 max-w-80 flex-1 shrink-0" role="group" aria-label={`${label} column, ${totalCount} items`}>
124
128
  {/* Column header */}
125
129
  <div className="flex items-center gap-2 mb-3 px-1">
126
130
  <h3 className="text-sm font-medium">{label}</h3>
127
131
  <Badge variant="secondary" className="text-xs">
128
- {tasks.length}
132
+ {totalCount}
129
133
  </Badge>
130
134
  <div className="flex-1" />
131
135
  {canSelect && tasks.length > 0 && (
@@ -182,31 +186,38 @@ export function KanbanColumn({
182
186
  >
183
187
  <SortableContext items={tasks.map((t) => t.id)} strategy={verticalListSortingStrategy}>
184
188
  <div className="space-y-2">
185
- {tasks.length === 0 ? (
189
+ {totalCount === 0 ? (
186
190
  <div className="flex flex-col items-center justify-center h-full min-h-[120px] text-muted-foreground border-2 border-dashed border-border/50 rounded-lg bg-background/35">
187
191
  <Inbox className="h-5 w-5 mb-1 opacity-40" />
188
- <span className="text-xs">No tasks</span>
192
+ <span className="text-xs">No items</span>
189
193
  </div>
190
194
  ) : (
191
- tasks.map((task) => {
192
- const isExiting = exitingIds?.has(task.id);
193
- return (
194
- <div
195
- key={task.id}
196
- className={isExiting ? "animate-card-exit pointer-events-none" : ""}
197
- >
198
- <TaskCard
199
- task={task}
200
- onClick={onTaskClick}
201
- selectionMode={selectMode}
202
- selected={selectedIds.has(task.id)}
203
- onSelect={handleSelect}
204
- onDelete={onDeleteTask}
205
- onEdit={onEditTask}
206
- />
207
- </div>
208
- );
209
- })
195
+ <>
196
+ {/* Workflow cards first (not draggable) */}
197
+ {workflows.map((workflow) => (
198
+ <WorkflowKanbanCard key={workflow.id} workflow={workflow} />
199
+ ))}
200
+ {/* Task cards (draggable) */}
201
+ {tasks.map((task) => {
202
+ const isExiting = exitingIds?.has(task.id);
203
+ return (
204
+ <div
205
+ key={task.id}
206
+ className={isExiting ? "animate-card-exit pointer-events-none" : ""}
207
+ >
208
+ <TaskCard
209
+ task={task}
210
+ onClick={onTaskClick}
211
+ selectionMode={selectMode}
212
+ selected={selectedIds.has(task.id)}
213
+ onSelect={handleSelect}
214
+ onDelete={onDeleteTask}
215
+ onEdit={onEditTask}
216
+ />
217
+ </div>
218
+ );
219
+ })}
220
+ </>
210
221
  )}
211
222
  </div>
212
223
  </SortableContext>