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
@@ -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, parentTask } = body;
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 + optional parent task atomically
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 parent task (no workflowId visible on dashboard)
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(defToStore),
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 }
@@ -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
- const [spendRows30, tokenRows30, monthBreakdown, filteredBreakdown, auditEntries, budgetSnapshot] =
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: runtimeId === "all" ? undefined : [runtimeId],
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(filteredBreakdown);
223
- const monthTokens = monthBreakdown.reduce(
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
- todaySpendMicros: findOverallSpend(budgetSnapshot.statuses, "daily"),
239
- monthSpendMicros: findOverallSpend(budgetSnapshot.statuses, "monthly"),
240
- todayTokens: tokenSeries30[tokenSeries30.length - 1] ?? 0,
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={filteredBreakdown}
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 for task cards
26
+ // Build project name lookup
25
27
  const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
26
28
 
27
- // Look up linked workflows for parent tasks (via sourceTaskId in definition JSON)
29
+ // Fetch all workflows for kanban display
28
30
  const allWorkflows = await db
29
- .select({ id: workflows.id, definition: workflows.definition, status: workflows.status })
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 Date objects for client component consumption
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
- return <KanbanBoard initialTasks={serializedTasks} projects={allProjects} />;
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-forest-dawn min-h-screen p-6">
37
- <Link href="/documents">
38
- <Button variant="ghost" size="sm" className="mb-4">
39
- <ArrowLeft className="h-4 w-4 mr-1" />
40
- Back to Documents
41
- </Button>
42
- </Link>
43
- <DocumentDetailView documentId={id} initialDocument={initialDoc} />
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
  }
@@ -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(5),
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={serializedPriorityTasks} />
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
+ }