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
|
@@ -23,7 +23,7 @@ interface FromAssistBody {
|
|
|
23
23
|
|
|
24
24
|
export async function POST(req: NextRequest) {
|
|
25
25
|
const body = (await req.json()) as FromAssistBody;
|
|
26
|
-
const { name, projectId, definition, priority, assignedAgent, executeImmediately
|
|
26
|
+
const { name, projectId, definition, priority, assignedAgent, executeImmediately } = body;
|
|
27
27
|
|
|
28
28
|
if (!name?.trim()) {
|
|
29
29
|
return NextResponse.json({ error: "Name is required" }, { status: 400 });
|
|
@@ -46,53 +46,27 @@ export async function POST(req: NextRequest) {
|
|
|
46
46
|
return NextResponse.json({ error: compatibilityError }, { status: 400 });
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Transaction: create workflow + tasks
|
|
49
|
+
// Transaction: create workflow + step tasks atomically (no phantom parent task)
|
|
50
50
|
const workflowId = crypto.randomUUID();
|
|
51
51
|
const now = new Date();
|
|
52
52
|
const taskIds: string[] = [];
|
|
53
|
-
let parentTaskId: string | null = null;
|
|
54
53
|
|
|
55
54
|
try {
|
|
56
55
|
db.transaction((tx) => {
|
|
57
|
-
// Create
|
|
58
|
-
if (parentTask?.title) {
|
|
59
|
-
parentTaskId = crypto.randomUUID();
|
|
60
|
-
tx.insert(tasks)
|
|
61
|
-
.values({
|
|
62
|
-
id: parentTaskId,
|
|
63
|
-
title: parentTask.title,
|
|
64
|
-
description: parentTask.description || null,
|
|
65
|
-
projectId: projectId || null,
|
|
66
|
-
workflowId: null,
|
|
67
|
-
status: executeImmediately ? "running" : "planned",
|
|
68
|
-
assignedAgent: assignedAgent ?? null,
|
|
69
|
-
agentProfile: parentTask.agentProfile ?? null,
|
|
70
|
-
priority: priority ?? 2,
|
|
71
|
-
createdAt: now,
|
|
72
|
-
updatedAt: now,
|
|
73
|
-
})
|
|
74
|
-
.run();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Store sourceTaskId in definition for parent↔workflow linkage
|
|
78
|
-
const defToStore = parentTaskId
|
|
79
|
-
? { ...definition, sourceTaskId: parentTaskId }
|
|
80
|
-
: definition;
|
|
81
|
-
|
|
82
|
-
// Create workflow
|
|
56
|
+
// Create workflow — no sourceTaskId needed since there's no parent task
|
|
83
57
|
tx.insert(workflows)
|
|
84
58
|
.values({
|
|
85
59
|
id: workflowId,
|
|
86
60
|
name: name.trim(),
|
|
87
61
|
projectId: projectId || null,
|
|
88
|
-
definition: JSON.stringify(
|
|
62
|
+
definition: JSON.stringify(definition),
|
|
89
63
|
status: executeImmediately ? "active" : "draft",
|
|
90
64
|
createdAt: now,
|
|
91
65
|
updatedAt: now,
|
|
92
66
|
})
|
|
93
67
|
.run();
|
|
94
68
|
|
|
95
|
-
// Create tasks for each step (with workflowId — hidden from dashboard)
|
|
69
|
+
// Create tasks for each step (with workflowId — hidden from dashboard kanban)
|
|
96
70
|
for (const step of definition.steps) {
|
|
97
71
|
const taskId = crypto.randomUUID();
|
|
98
72
|
taskIds.push(taskId);
|
|
@@ -135,7 +109,7 @@ export async function POST(req: NextRequest) {
|
|
|
135
109
|
{
|
|
136
110
|
workflow: created,
|
|
137
111
|
taskIds,
|
|
138
|
-
parentTaskId,
|
|
112
|
+
parentTaskId: null,
|
|
139
113
|
status: executeImmediately ? "started" : "created",
|
|
140
114
|
},
|
|
141
115
|
{ status: 201 }
|
package/src/app/costs/page.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { listRuntimeCatalog } from "@/lib/agents/runtime/catalog";
|
|
2
1
|
import { CostDashboard } from "@/components/costs/cost-dashboard";
|
|
3
2
|
import { getBudgetGuardrailSnapshot } from "@/lib/settings/budget-guardrails";
|
|
4
3
|
import {
|
|
@@ -13,8 +12,6 @@ import {
|
|
|
13
12
|
|
|
14
13
|
export const dynamic = "force-dynamic";
|
|
15
14
|
|
|
16
|
-
const runtimeCatalog = listRuntimeCatalog();
|
|
17
|
-
const validRuntimeIds = new Set<string>(runtimeCatalog.map((runtime) => runtime.id));
|
|
18
15
|
const validDateRanges = new Set(["7d", "30d", "90d", "all"]);
|
|
19
16
|
const validStatuses = new Set<UsageLedgerStatus>([
|
|
20
17
|
"completed",
|
|
@@ -40,10 +37,6 @@ function resolveDateRange(value: string | undefined) {
|
|
|
40
37
|
return value && validDateRanges.has(value) ? value : "30d";
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
function resolveRuntime(value: string | undefined) {
|
|
44
|
-
return value && validRuntimeIds.has(value) ? value : "all";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
40
|
function resolveStatus(value: string | undefined) {
|
|
48
41
|
return value && validStatuses.has(value as UsageLedgerStatus) ? value : "all";
|
|
49
42
|
}
|
|
@@ -102,25 +95,6 @@ function fillSeries<T extends { day: string }>(
|
|
|
102
95
|
return keys.map((key) => values.get(key) ?? 0);
|
|
103
96
|
}
|
|
104
97
|
|
|
105
|
-
function findOverallSpend(
|
|
106
|
-
statuses: Array<{
|
|
107
|
-
scopeId: string;
|
|
108
|
-
window: string;
|
|
109
|
-
metric: string;
|
|
110
|
-
currentValue: number;
|
|
111
|
-
}>,
|
|
112
|
-
window: "daily" | "monthly"
|
|
113
|
-
) {
|
|
114
|
-
return (
|
|
115
|
-
statuses.find(
|
|
116
|
-
(status) =>
|
|
117
|
-
status.scopeId === "overall" &&
|
|
118
|
-
status.window === window &&
|
|
119
|
-
status.metric === "spend"
|
|
120
|
-
)?.currentValue ?? 0
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
98
|
function buildRuntimeBreakdown(
|
|
125
99
|
rows: ProviderModelBreakdownEntry[]
|
|
126
100
|
): Array<{
|
|
@@ -149,9 +123,7 @@ function buildRuntimeBreakdown(
|
|
|
149
123
|
for (const row of rows) {
|
|
150
124
|
const current = totals.get(row.runtimeId) ?? {
|
|
151
125
|
runtimeId: row.runtimeId,
|
|
152
|
-
label:
|
|
153
|
-
runtimeCatalog.find((runtime) => runtime.id === row.runtimeId)?.label ??
|
|
154
|
-
row.runtimeId,
|
|
126
|
+
label: row.runtimeId,
|
|
155
127
|
providerId: row.providerId,
|
|
156
128
|
costMicros: 0,
|
|
157
129
|
totalTokens: 0,
|
|
@@ -191,22 +163,34 @@ export default async function CostsPage({
|
|
|
191
163
|
}) {
|
|
192
164
|
const params = await searchParams;
|
|
193
165
|
const dateRange = resolveDateRange(toScalar(params.range));
|
|
194
|
-
const runtimeId = resolveRuntime(toScalar(params.runtime));
|
|
195
166
|
const status = resolveStatus(toScalar(params.status));
|
|
196
167
|
const activityType = resolveActivityType(toScalar(params.activity));
|
|
197
|
-
|
|
198
168
|
const rangeStart = getRangeStart(dateRange);
|
|
199
|
-
|
|
169
|
+
|
|
170
|
+
const budgetSnapshot = await getBudgetGuardrailSnapshot();
|
|
171
|
+
const configuredRuntimeIds = Object.values(budgetSnapshot.runtimeStates)
|
|
172
|
+
.filter((runtime) => runtime.configured)
|
|
173
|
+
.map((runtime) => runtime.runtimeId);
|
|
174
|
+
const requestedRuntime = toScalar(params.runtime);
|
|
175
|
+
const runtimeId =
|
|
176
|
+
requestedRuntime && configuredRuntimeIds.includes(requestedRuntime as never)
|
|
177
|
+
? requestedRuntime
|
|
178
|
+
: "all";
|
|
179
|
+
|
|
180
|
+
const [spendRows30, tokenRows30, monthBreakdown, filteredBreakdown, auditEntries] =
|
|
200
181
|
await Promise.all([
|
|
201
182
|
getDailySpendTotals(30),
|
|
202
183
|
getDailyTokenTotals(30),
|
|
203
184
|
getProviderModelBreakdown({ startedAt: startOfCurrentMonth() }),
|
|
204
|
-
getProviderModelBreakdown(
|
|
205
|
-
rangeStart ? { startedAt: rangeStart } : undefined
|
|
206
|
-
),
|
|
185
|
+
getProviderModelBreakdown(rangeStart ? { startedAt: rangeStart } : undefined),
|
|
207
186
|
listUsageAuditEntries({
|
|
208
187
|
limit: 100,
|
|
209
|
-
runtimeIds:
|
|
188
|
+
runtimeIds:
|
|
189
|
+
runtimeId === "all"
|
|
190
|
+
? configuredRuntimeIds.length > 0
|
|
191
|
+
? configuredRuntimeIds
|
|
192
|
+
: undefined
|
|
193
|
+
: [runtimeId],
|
|
210
194
|
statuses: status === "all" ? undefined : [status as UsageLedgerStatus],
|
|
211
195
|
activityTypes:
|
|
212
196
|
activityType === "all"
|
|
@@ -214,17 +198,38 @@ export default async function CostsPage({
|
|
|
214
198
|
: [activityType as UsageActivityType],
|
|
215
199
|
startedAt: rangeStart,
|
|
216
200
|
}),
|
|
217
|
-
getBudgetGuardrailSnapshot(),
|
|
218
201
|
]);
|
|
219
202
|
|
|
203
|
+
const configuredBreakdown = filteredBreakdown.filter((row) =>
|
|
204
|
+
configuredRuntimeIds.length > 0
|
|
205
|
+
? configuredRuntimeIds.includes(row.runtimeId as never)
|
|
206
|
+
: true
|
|
207
|
+
);
|
|
208
|
+
const configuredMonthBreakdown = monthBreakdown.filter((row) =>
|
|
209
|
+
configuredRuntimeIds.length > 0
|
|
210
|
+
? configuredRuntimeIds.includes(row.runtimeId as never)
|
|
211
|
+
: true
|
|
212
|
+
);
|
|
213
|
+
|
|
220
214
|
const spendSeries30 = fillSeries(30, spendRows30, (row) => row.costMicros);
|
|
221
215
|
const tokenSeries30 = fillSeries(30, tokenRows30, (row) => row.totalTokens);
|
|
222
|
-
const runtimeBreakdown = buildRuntimeBreakdown(
|
|
223
|
-
|
|
216
|
+
const runtimeBreakdown = buildRuntimeBreakdown(configuredBreakdown).map((row) => ({
|
|
217
|
+
...row,
|
|
218
|
+
label: budgetSnapshot.runtimeStates[row.runtimeId as keyof typeof budgetSnapshot.runtimeStates]
|
|
219
|
+
?.label ?? row.runtimeId,
|
|
220
|
+
}));
|
|
221
|
+
const monthTokens = configuredMonthBreakdown.reduce(
|
|
224
222
|
(sum, row) => sum + row.totalTokens,
|
|
225
223
|
0
|
|
226
224
|
);
|
|
227
225
|
|
|
226
|
+
const overallDaily = budgetSnapshot.statuses.find(
|
|
227
|
+
(status) => status.scopeId === "overall" && status.window === "daily"
|
|
228
|
+
);
|
|
229
|
+
const overallMonthly = budgetSnapshot.statuses.find(
|
|
230
|
+
(status) => status.scopeId === "overall" && status.window === "monthly"
|
|
231
|
+
);
|
|
232
|
+
|
|
228
233
|
return (
|
|
229
234
|
<div className="gradient-neutral min-h-screen p-6">
|
|
230
235
|
<CostDashboard
|
|
@@ -235,9 +240,12 @@ export default async function CostsPage({
|
|
|
235
240
|
activityType,
|
|
236
241
|
}}
|
|
237
242
|
summary={{
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
monthSpendMicros: overallMonthly?.currentValue ?? 0,
|
|
244
|
+
derivedDailyBudgetMicros: overallDaily?.limitValue ?? 0,
|
|
245
|
+
remainingMonthlyHeadroomMicros: Math.max(
|
|
246
|
+
(overallMonthly?.limitValue ?? 0) - (overallMonthly?.currentValue ?? 0),
|
|
247
|
+
0
|
|
248
|
+
),
|
|
241
249
|
monthTokens,
|
|
242
250
|
}}
|
|
243
251
|
trendSeries={{
|
|
@@ -247,8 +255,10 @@ export default async function CostsPage({
|
|
|
247
255
|
tokens30: tokenSeries30,
|
|
248
256
|
}}
|
|
249
257
|
budgetStatuses={budgetSnapshot.statuses}
|
|
258
|
+
runtimeStates={budgetSnapshot.runtimeStates}
|
|
259
|
+
pricing={budgetSnapshot.pricing}
|
|
250
260
|
runtimeBreakdown={runtimeBreakdown}
|
|
251
|
-
modelBreakdown={
|
|
261
|
+
modelBreakdown={configuredBreakdown}
|
|
252
262
|
auditEntries={auditEntries}
|
|
253
263
|
/>
|
|
254
264
|
</div>
|
|
@@ -2,9 +2,11 @@ import { Suspense } from "react";
|
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { tasks, projects, workflows } from "@/lib/db/schema";
|
|
4
4
|
import { desc, isNull } from "drizzle-orm";
|
|
5
|
+
import { parseWorkflowState } from "@/lib/workflows/engine";
|
|
5
6
|
import { KanbanBoard } from "@/components/tasks/kanban-board";
|
|
6
7
|
import { SkeletonBoard } from "@/components/tasks/skeleton-board";
|
|
7
8
|
import type { TaskItem } from "@/components/tasks/task-card";
|
|
9
|
+
import type { WorkflowKanbanItem } from "@/components/workflows/workflow-kanban-card";
|
|
8
10
|
|
|
9
11
|
export const dynamic = "force-dynamic";
|
|
10
12
|
|
|
@@ -21,38 +23,74 @@ async function BoardContent() {
|
|
|
21
23
|
.from(projects)
|
|
22
24
|
.orderBy(projects.name);
|
|
23
25
|
|
|
24
|
-
// Build project name lookup
|
|
26
|
+
// Build project name lookup
|
|
25
27
|
const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
|
|
26
28
|
|
|
27
|
-
//
|
|
29
|
+
// Fetch all workflows for kanban display
|
|
28
30
|
const allWorkflows = await db
|
|
29
|
-
.select(
|
|
30
|
-
.from(workflows)
|
|
31
|
-
|
|
32
|
-
const linkedWorkflowMap = new Map<string, { workflowId: string; workflowStatus: string }>();
|
|
33
|
-
for (const w of allWorkflows) {
|
|
34
|
-
try {
|
|
35
|
-
const def = JSON.parse(w.definition);
|
|
36
|
-
if (def.sourceTaskId) {
|
|
37
|
-
linkedWorkflowMap.set(def.sourceTaskId, {
|
|
38
|
-
workflowId: w.id,
|
|
39
|
-
workflowStatus: w.status,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
} catch { /* skip invalid JSON */ }
|
|
43
|
-
}
|
|
31
|
+
.select()
|
|
32
|
+
.from(workflows)
|
|
33
|
+
.orderBy(desc(workflows.updatedAt));
|
|
44
34
|
|
|
45
|
-
// Serialize
|
|
35
|
+
// Serialize tasks (no more linkedWorkflow fields)
|
|
46
36
|
const serializedTasks: TaskItem[] = allTasks.map((t) => ({
|
|
47
37
|
...t,
|
|
48
38
|
projectName: t.projectId ? projectMap.get(t.projectId) ?? undefined : undefined,
|
|
49
|
-
linkedWorkflowId: linkedWorkflowMap.get(t.id)?.workflowId,
|
|
50
|
-
linkedWorkflowStatus: linkedWorkflowMap.get(t.id)?.workflowStatus,
|
|
51
39
|
createdAt: t.createdAt.toISOString(),
|
|
52
40
|
updatedAt: t.updatedAt.toISOString(),
|
|
53
41
|
}));
|
|
54
42
|
|
|
55
|
-
|
|
43
|
+
// Build workflow kanban items with step progress
|
|
44
|
+
const serializedWorkflows: WorkflowKanbanItem[] = allWorkflows.map((w) => {
|
|
45
|
+
let stepProgress = { current: 0, total: 0 };
|
|
46
|
+
let currentStepName: string | undefined;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { definition, state } = parseWorkflowState(w.definition);
|
|
50
|
+
if (definition.steps) {
|
|
51
|
+
stepProgress.total = definition.steps.length;
|
|
52
|
+
if (state) {
|
|
53
|
+
stepProgress.current = state.stepStates.filter(
|
|
54
|
+
(s) => s.status === "completed"
|
|
55
|
+
).length;
|
|
56
|
+
const running = state.stepStates.find((s) => s.status === "running");
|
|
57
|
+
if (running) {
|
|
58
|
+
currentStepName = definition.steps.find(
|
|
59
|
+
(step) => step.id === running.stepId
|
|
60
|
+
)?.name;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
/* skip parse errors */
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
type: "workflow" as const,
|
|
70
|
+
id: w.id,
|
|
71
|
+
name: w.name,
|
|
72
|
+
status: w.status,
|
|
73
|
+
pattern: (() => {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(w.definition).pattern ?? "sequence";
|
|
76
|
+
} catch {
|
|
77
|
+
return "sequence";
|
|
78
|
+
}
|
|
79
|
+
})(),
|
|
80
|
+
projectName: w.projectId ? projectMap.get(w.projectId) ?? undefined : undefined,
|
|
81
|
+
stepProgress,
|
|
82
|
+
currentStepName,
|
|
83
|
+
createdAt: w.createdAt.toISOString(),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<KanbanBoard
|
|
89
|
+
initialTasks={serializedTasks}
|
|
90
|
+
initialWorkflows={serializedWorkflows}
|
|
91
|
+
projects={allProjects}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
export default function DashboardPage() {
|
|
@@ -33,14 +33,16 @@ export default async function DocumentDetailPage({
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
|
-
<div className="gradient-
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
<div className="gradient-twilight min-h-screen p-6">
|
|
37
|
+
<div className="surface-page-shell rounded-xl p-6 max-w-5xl mx-auto">
|
|
38
|
+
<Link href="/documents">
|
|
39
|
+
<Button variant="ghost" size="sm" className="mb-4">
|
|
40
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
41
|
+
Back to Documents
|
|
42
|
+
</Button>
|
|
43
|
+
</Link>
|
|
44
|
+
<DocumentDetailView documentId={id} initialDocument={initialDoc} />
|
|
45
|
+
</div>
|
|
44
46
|
</div>
|
|
45
47
|
);
|
|
46
48
|
}
|
package/src/app/globals.css
CHANGED
|
@@ -654,6 +654,17 @@
|
|
|
654
654
|
border: 1px solid color-mix(in oklab, var(--border) 75%, transparent);
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
/* Document reader surface — white bg for readability */
|
|
658
|
+
.prose-reader-surface {
|
|
659
|
+
background: oklch(1 0 0);
|
|
660
|
+
border-radius: var(--radius-lg);
|
|
661
|
+
padding: 1.5rem;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.dark .prose-reader-surface {
|
|
665
|
+
background: oklch(0.18 0.015 265);
|
|
666
|
+
}
|
|
667
|
+
|
|
657
668
|
/* --- Progress slide animation (AI Assist) --- */
|
|
658
669
|
@keyframes progress-slide {
|
|
659
670
|
0% { transform: translateX(-100%); }
|
package/src/app/page.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { db } from "@/lib/db";
|
|
2
|
-
import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
|
|
2
|
+
import { tasks, projects, agentLogs, notifications, workflows } from "@/lib/db/schema";
|
|
3
3
|
import { eq, count, gte, and, desc, sql, inArray } from "drizzle-orm";
|
|
4
|
+
import { parseWorkflowState } from "@/lib/workflows/engine";
|
|
4
5
|
import { Greeting } from "@/components/dashboard/greeting";
|
|
5
6
|
import { StatsCards } from "@/components/dashboard/stats-cards";
|
|
6
7
|
import { PriorityQueue } from "@/components/dashboard/priority-queue";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
getActiveProjectActivityByDay,
|
|
17
18
|
getAgentActivityByHour,
|
|
18
19
|
getNotificationsByDay,
|
|
20
|
+
getWorkflowActivityByDay,
|
|
19
21
|
} from "@/lib/queries/chart-data";
|
|
20
22
|
|
|
21
23
|
export const dynamic = "force-dynamic";
|
|
@@ -33,6 +35,8 @@ export default async function HomePage() {
|
|
|
33
35
|
[awaitingResult],
|
|
34
36
|
[activeProjectsResult],
|
|
35
37
|
priorityTasks,
|
|
38
|
+
activeWorkflows,
|
|
39
|
+
[activeWorkflowCountResult],
|
|
36
40
|
recentLogs,
|
|
37
41
|
allProjects,
|
|
38
42
|
recentActiveProjects,
|
|
@@ -41,6 +45,7 @@ export default async function HomePage() {
|
|
|
41
45
|
projectCreationsByDay,
|
|
42
46
|
agentActivityByHour,
|
|
43
47
|
notificationsByDay,
|
|
48
|
+
workflowsByDay,
|
|
44
49
|
] = await Promise.all([
|
|
45
50
|
db.select({ count: count() }).from(tasks).where(eq(tasks.status, "running")),
|
|
46
51
|
db.select({ count: count() }).from(tasks).where(eq(tasks.status, "failed")),
|
|
@@ -62,7 +67,11 @@ export default async function HomePage() {
|
|
|
62
67
|
// Priority queue: failed + running tasks, sorted by priority
|
|
63
68
|
db.select().from(tasks).where(
|
|
64
69
|
inArray(tasks.status, ["failed", "running", "queued"])
|
|
65
|
-
).orderBy(tasks.priority, desc(tasks.updatedAt)).limit(
|
|
70
|
+
).orderBy(tasks.priority, desc(tasks.updatedAt)).limit(8),
|
|
71
|
+
// All workflows for priority queue (match kanban board behavior)
|
|
72
|
+
db.select().from(workflows).orderBy(desc(workflows.updatedAt)).limit(8),
|
|
73
|
+
// Count active workflows for stats
|
|
74
|
+
db.select({ count: count() }).from(workflows).where(eq(workflows.status, "active")),
|
|
66
75
|
// Recent agent logs
|
|
67
76
|
db.select().from(agentLogs).orderBy(desc(agentLogs.timestamp)).limit(6),
|
|
68
77
|
// All projects for quick actions
|
|
@@ -81,11 +90,13 @@ export default async function HomePage() {
|
|
|
81
90
|
getActiveProjectActivityByDay(7),
|
|
82
91
|
getAgentActivityByHour(),
|
|
83
92
|
getNotificationsByDay(7),
|
|
93
|
+
getWorkflowActivityByDay(7),
|
|
84
94
|
]);
|
|
85
95
|
|
|
86
96
|
// Build project name lookup for priority tasks
|
|
87
97
|
const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
|
|
88
98
|
|
|
99
|
+
// Serialize priority tasks (no more workflow linkage via parent task)
|
|
89
100
|
const serializedPriorityTasks: PriorityTask[] = priorityTasks.map((t) => ({
|
|
90
101
|
id: t.id,
|
|
91
102
|
title: t.title,
|
|
@@ -94,6 +105,49 @@ export default async function HomePage() {
|
|
|
94
105
|
projectName: t.projectId ? projectMap.get(t.projectId) ?? undefined : undefined,
|
|
95
106
|
}));
|
|
96
107
|
|
|
108
|
+
// Build workflow priority items directly
|
|
109
|
+
const workflowPriorityItems: PriorityTask[] = activeWorkflows.map((w) => {
|
|
110
|
+
let workflowProgress: PriorityTask["workflowProgress"];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const { definition: def, state } = parseWorkflowState(w.definition);
|
|
114
|
+
if (state && def.steps) {
|
|
115
|
+
const completed = state.stepStates.filter((s) => s.status === "completed").length;
|
|
116
|
+
const running = state.stepStates.find((s) => s.status === "running");
|
|
117
|
+
const runningStep = running
|
|
118
|
+
? def.steps.find((step) => step.id === running.stepId)
|
|
119
|
+
: undefined;
|
|
120
|
+
workflowProgress = {
|
|
121
|
+
current: completed,
|
|
122
|
+
total: def.steps.length,
|
|
123
|
+
currentStepName: runningStep?.name,
|
|
124
|
+
workflowId: w.id,
|
|
125
|
+
workflowStatus: w.status,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
} catch { /* skip parse errors */ }
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
id: w.id,
|
|
132
|
+
title: w.name,
|
|
133
|
+
status: w.status,
|
|
134
|
+
priority: 1, // Workflows always high priority in the attention queue
|
|
135
|
+
projectName: w.projectId ? projectMap.get(w.projectId) ?? undefined : undefined,
|
|
136
|
+
workflowProgress,
|
|
137
|
+
isWorkflow: true,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Urgency ranking: actionable items surface first
|
|
142
|
+
const urgencyRank: Record<string, number> = {
|
|
143
|
+
failed: 0, running: 1, active: 1, queued: 2, paused: 3, draft: 4, completed: 5,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Merge, sort by urgency, and limit to 8 items
|
|
147
|
+
const allPriorityItems = [...workflowPriorityItems, ...serializedPriorityTasks]
|
|
148
|
+
.sort((a, b) => (urgencyRank[a.status] ?? 6) - (urgencyRank[b.status] ?? 6))
|
|
149
|
+
.slice(0, 8);
|
|
150
|
+
|
|
97
151
|
// Get task titles for log entries
|
|
98
152
|
const logTaskIds = [...new Set(recentLogs.filter((l) => l.taskId).map((l) => l.taskId!))];
|
|
99
153
|
const logTasks = logTaskIds.length > 0
|
|
@@ -132,6 +186,7 @@ export default async function HomePage() {
|
|
|
132
186
|
runningCount={runningResult.count}
|
|
133
187
|
awaitingCount={awaitingResult.count}
|
|
134
188
|
failedCount={failedResult.count}
|
|
189
|
+
activeWorkflows={activeWorkflowCountResult.count}
|
|
135
190
|
/>
|
|
136
191
|
<StatsCards
|
|
137
192
|
runningCount={runningResult.count}
|
|
@@ -139,16 +194,18 @@ export default async function HomePage() {
|
|
|
139
194
|
completedAllTime={completedAllTimeResult.count}
|
|
140
195
|
awaitingReview={awaitingResult.count}
|
|
141
196
|
activeProjects={activeProjectsResult.count}
|
|
197
|
+
activeWorkflows={activeWorkflowCountResult.count}
|
|
142
198
|
sparklines={{
|
|
143
199
|
completions: completionsByDay,
|
|
144
200
|
creations: taskCreationsByDay,
|
|
145
201
|
projects: projectCreationsByDay,
|
|
146
202
|
notifications: notificationsByDay,
|
|
203
|
+
workflows: workflowsByDay,
|
|
147
204
|
}}
|
|
148
205
|
/>
|
|
149
206
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5 mb-6">
|
|
150
207
|
<div className="lg:col-span-3">
|
|
151
|
-
<PriorityQueue tasks={
|
|
208
|
+
<PriorityQueue tasks={allPriorityItems} />
|
|
152
209
|
</div>
|
|
153
210
|
<div className="lg:col-span-2">
|
|
154
211
|
<ActivityFeed entries={serializedLogs} hourlyActivity={agentActivityByHour} />
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { getDocBySlug, getManifest } from "@/lib/docs/reader";
|
|
3
|
+
import { getAdoptionMap } from "@/lib/docs/adoption";
|
|
4
|
+
import { PlaybookDetailView } from "@/components/playbook/playbook-detail-view";
|
|
5
|
+
|
|
6
|
+
export const dynamic = "force-dynamic";
|
|
7
|
+
|
|
8
|
+
interface PlaybookDetailProps {
|
|
9
|
+
params: Promise<{ slug: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function generateMetadata({ params }: PlaybookDetailProps) {
|
|
13
|
+
const { slug } = await params;
|
|
14
|
+
const doc = getDocBySlug(slug);
|
|
15
|
+
return {
|
|
16
|
+
title: doc
|
|
17
|
+
? `${(doc.frontmatter.title as string) || slug} | Playbook | Stagent`
|
|
18
|
+
: "Not Found | Playbook",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default async function PlaybookDetailPage({
|
|
23
|
+
params,
|
|
24
|
+
}: PlaybookDetailProps) {
|
|
25
|
+
const { slug } = await params;
|
|
26
|
+
const [doc, manifest, adoptionMap] = await Promise.all([
|
|
27
|
+
getDocBySlug(slug),
|
|
28
|
+
getManifest(),
|
|
29
|
+
getAdoptionMap(),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
if (!doc) notFound();
|
|
33
|
+
|
|
34
|
+
// Find related sections from manifest
|
|
35
|
+
const allSections = [...manifest.sections, ...manifest.journeys];
|
|
36
|
+
const currentSection = allSections.find((s) => s.slug === slug);
|
|
37
|
+
|
|
38
|
+
// Find related docs by shared tags
|
|
39
|
+
const currentTags = new Set(
|
|
40
|
+
(currentSection && "tags" in currentSection
|
|
41
|
+
? (currentSection as { tags: string[] }).tags
|
|
42
|
+
: (doc.frontmatter.tags as string[]) || []
|
|
43
|
+
).map((t) => t.toLowerCase())
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const relatedSections = manifest.sections
|
|
47
|
+
.filter(
|
|
48
|
+
(s) =>
|
|
49
|
+
s.slug !== slug &&
|
|
50
|
+
s.tags.some((t) => currentTags.has(t.toLowerCase()))
|
|
51
|
+
)
|
|
52
|
+
.slice(0, 4);
|
|
53
|
+
|
|
54
|
+
const adoption = Object.fromEntries(adoptionMap);
|
|
55
|
+
|
|
56
|
+
// Collect all known doc slugs so markdown links resolve correctly
|
|
57
|
+
const allSlugs = [
|
|
58
|
+
...manifest.sections.map((s) => s.slug),
|
|
59
|
+
...manifest.journeys.map((j) => j.slug),
|
|
60
|
+
"getting-started",
|
|
61
|
+
"index",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="gradient-twilight min-h-[100dvh] p-4 sm:p-6">
|
|
66
|
+
<div className="surface-page rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
|
|
67
|
+
<PlaybookDetailView
|
|
68
|
+
doc={doc}
|
|
69
|
+
relatedSections={relatedSections}
|
|
70
|
+
adoption={adoption}
|
|
71
|
+
allSlugs={allSlugs}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getManifest, getDocsLastGenerated } from "@/lib/docs/reader";
|
|
2
|
+
import { getUsageStage } from "@/lib/docs/usage-stage";
|
|
3
|
+
import { getAdoptionMap } from "@/lib/docs/adoption";
|
|
4
|
+
import { getJourneyCompletions } from "@/lib/docs/journey-tracker";
|
|
5
|
+
import { getSetting, setSetting } from "@/lib/settings/helpers";
|
|
6
|
+
import { PlaybookHomepage } from "@/components/playbook/playbook-homepage";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
title: "Playbook | Stagent",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default async function PlaybookPage() {
|
|
15
|
+
const [manifest, stage, adoptionMap, lastGenerated, lastVisit] =
|
|
16
|
+
await Promise.all([
|
|
17
|
+
getManifest(),
|
|
18
|
+
getUsageStage(),
|
|
19
|
+
getAdoptionMap(),
|
|
20
|
+
getDocsLastGenerated(),
|
|
21
|
+
getSetting("lastPlaybookVisit"),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const journeyCompletions = getJourneyCompletions(
|
|
25
|
+
manifest.journeys,
|
|
26
|
+
adoptionMap
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Update last visit timestamp
|
|
30
|
+
await setSetting("lastPlaybookVisit", new Date().toISOString());
|
|
31
|
+
|
|
32
|
+
// Serialize maps for client component
|
|
33
|
+
const adoption = Object.fromEntries(adoptionMap);
|
|
34
|
+
const completions = Object.fromEntries(journeyCompletions);
|
|
35
|
+
|
|
36
|
+
const hasUpdates =
|
|
37
|
+
lastGenerated != null &&
|
|
38
|
+
lastVisit != null &&
|
|
39
|
+
new Date(lastGenerated) > new Date(lastVisit);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="gradient-twilight min-h-[100dvh] p-4 sm:p-6">
|
|
43
|
+
<div className="surface-page rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
|
|
44
|
+
<PlaybookHomepage
|
|
45
|
+
manifest={manifest}
|
|
46
|
+
stage={stage}
|
|
47
|
+
adoption={adoption}
|
|
48
|
+
journeyCompletions={completions}
|
|
49
|
+
hasUpdates={hasUpdates}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|