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.
- package/README.md +74 -49
- package/package.json +3 -2
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/playbook-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -20
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/api/tasks/[id]/route.ts +54 -3
- package/src/app/api/workflows/[id]/route.ts +43 -4
- package/src/app/api/workflows/[id]/status/route.ts +70 -2
- package/src/app/api/workflows/from-assist/route.ts +6 -32
- package/src/app/costs/page.tsx +53 -43
- package/src/app/dashboard/page.tsx +59 -21
- package/src/app/documents/[id]/page.tsx +10 -8
- package/src/app/globals.css +11 -0
- package/src/app/page.tsx +60 -3
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/[id]/page.tsx +22 -2
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/dashboard/greeting.tsx +3 -1
- package/src/components/dashboard/priority-queue.tsx +58 -9
- package/src/components/dashboard/stats-cards.tsx +16 -2
- package/src/components/documents/document-chip-bar.tsx +183 -0
- package/src/components/documents/document-content-renderer.tsx +146 -0
- package/src/components/documents/document-detail-view.tsx +16 -239
- package/src/components/documents/image-zoom-view.tsx +60 -0
- package/src/components/documents/smart-extracted-text.tsx +47 -0
- package/src/components/documents/utils.ts +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/inbox-list.tsx +4 -5
- package/src/components/notifications/notification-item.tsx +73 -6
- package/src/components/notifications/pending-approval-host.tsx +63 -14
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +225 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-browser.tsx +1 -0
- package/src/components/profiles/profile-card.tsx +16 -8
- package/src/components/profiles/profile-detail-view.tsx +12 -4
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +4 -2
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
- package/src/components/tasks/ai-assist-panel.tsx +108 -78
- package/src/components/tasks/content-preview.tsx +2 -1
- package/src/components/tasks/kanban-board.tsx +57 -5
- package/src/components/tasks/kanban-column.tsx +34 -23
- package/src/components/tasks/task-bento-cell.tsx +50 -0
- package/src/components/tasks/task-bento-grid.tsx +155 -0
- package/src/components/tasks/task-card.tsx +14 -16
- package/src/components/tasks/task-chip-bar.tsx +207 -0
- package/src/components/tasks/task-detail-view.tsx +42 -190
- package/src/components/tasks/task-result-renderer.tsx +33 -0
- package/src/components/workflows/blueprint-gallery.tsx +19 -12
- package/src/components/workflows/blueprint-preview.tsx +8 -1
- package/src/components/workflows/loop-status-view.tsx +2 -4
- package/src/components/workflows/swarm-dashboard.tsx +2 -3
- package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
- package/src/components/workflows/workflow-full-output.tsx +80 -0
- package/src/components/workflows/workflow-kanban-card.tsx +121 -0
- package/src/components/workflows/workflow-list.tsx +47 -42
- package/src/components/workflows/workflow-status-view.tsx +163 -16
- package/src/lib/agents/learned-context.ts +27 -15
- package/src/lib/agents/learning-session.ts +354 -0
- package/src/lib/agents/pattern-extractor.ts +19 -0
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/constants/card-icons.tsx +202 -0
- package/src/lib/constants/prose-styles.ts +7 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +3 -0
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +107 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/documents/context-builder.ts +41 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/queries/chart-data.ts +20 -1
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +2 -2
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +1 -1
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -95
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/engine.ts +75 -61
- package/src/lib/workflows/types.ts +2 -0
- package/tsconfig.json +2 -1
- 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
|
|
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="
|
|
159
|
-
{/* Header row
|
|
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
|
-
{/*
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
</CardContent>
|
|
203
|
-
</Card>
|
|
257
|
+
)}
|
|
204
258
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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="
|
|
214
|
-
{
|
|
215
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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={
|
|
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 ${
|
|
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 ${
|
|
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, ${
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
192
|
+
<span className="text-xs">No items</span>
|
|
189
193
|
</div>
|
|
190
194
|
) : (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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>
|