@aion0/forge 0.10.32 → 0.10.34
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/RELEASE_NOTES.md +8 -5
- package/app/api/activity/summary/route.ts +135 -0
- package/components/ActivityPanel.tsx +288 -0
- package/components/Dashboard.tsx +48 -29
- package/components/PipelineView.tsx +74 -3
- package/components/SkillsPanel.tsx +64 -29
- package/lib/chat/agent-loop.ts +68 -11
- package/lib/chat/build-memory-context.ts +36 -4
- package/lib/chat/llm/anthropic.ts +30 -1
- package/lib/chat/llm/openai.ts +12 -1
- package/lib/chat/llm/types.ts +11 -0
- package/lib/chat/session-store.ts +52 -1
- package/lib/help-docs/00-overview.md +14 -0
- package/lib/help-docs/06-skills.md +11 -1
- package/lib/help-docs/12-usage.md +1 -1
- package/lib/help-docs/24-watch.md +36 -11
- package/lib/watch/watch-runner.ts +76 -1
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.34
|
|
2
2
|
|
|
3
|
-
Released: 2026-06-
|
|
3
|
+
Released: 2026-06-03
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.33
|
|
6
6
|
|
|
7
7
|
### Other
|
|
8
|
-
- fix(watch):
|
|
8
|
+
- fix(watch): heuristic terminal detection for all connector pollers
|
|
9
|
+
- ui(activity): segmented pill — running/upcoming/failed each their own color
|
|
10
|
+
- fix(watch): honor poll result's terminal: true regardless of done_match
|
|
11
|
+
- fix(marketplace): scrollbar on long project list in install dropdown
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
14
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.33...v0.10.34
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/activity/summary
|
|
3
|
+
*
|
|
4
|
+
* Small snapshot for the Activity pill / dashboard. Three sections:
|
|
5
|
+
* - running: pipelines currently in flight (status queued|running)
|
|
6
|
+
* - upcoming: enabled schedules sorted by next_run_at ASC
|
|
7
|
+
* - recent: last 8 finished pipelines (succeeded|failed|cancelled)
|
|
8
|
+
*
|
|
9
|
+
* Aim <5KB response, <30ms. No expensive joins, no per-pipeline detail.
|
|
10
|
+
* Front-end refetches every 5s when ActivityPanel is open.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NextResponse } from 'next/server';
|
|
14
|
+
import { listPipelinesSummary } from '@/lib/pipeline';
|
|
15
|
+
import { listSchedules } from '@/lib/schedules/store';
|
|
16
|
+
|
|
17
|
+
interface RunningRow {
|
|
18
|
+
id: string;
|
|
19
|
+
workflowName: string;
|
|
20
|
+
status: string;
|
|
21
|
+
currentNode: string | null;
|
|
22
|
+
progress: { done: number; total: number };
|
|
23
|
+
createdAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface UpcomingRow {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
body_kind: string;
|
|
30
|
+
body_ref: string;
|
|
31
|
+
next_run_at: string | null;
|
|
32
|
+
schedule_kind: string;
|
|
33
|
+
schedule_summary: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RecentRow {
|
|
37
|
+
id: string;
|
|
38
|
+
workflowName: string;
|
|
39
|
+
status: string;
|
|
40
|
+
completedAt: string | null;
|
|
41
|
+
durationMs: number | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Summary {
|
|
45
|
+
running: RunningRow[];
|
|
46
|
+
upcoming: UpcomingRow[];
|
|
47
|
+
recent: RecentRow[];
|
|
48
|
+
generated_at: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scheduleSummary(s: {
|
|
52
|
+
schedule_kind: string;
|
|
53
|
+
schedule_interval_minutes: number;
|
|
54
|
+
schedule_at: string | null;
|
|
55
|
+
schedule_cron: string | null;
|
|
56
|
+
}): string {
|
|
57
|
+
switch (s.schedule_kind) {
|
|
58
|
+
case 'period':
|
|
59
|
+
return s.schedule_interval_minutes >= 60
|
|
60
|
+
? `every ${Math.round(s.schedule_interval_minutes / 60)}h`
|
|
61
|
+
: `every ${s.schedule_interval_minutes}m`;
|
|
62
|
+
case 'cron':
|
|
63
|
+
return `cron: ${s.schedule_cron || '?'}`;
|
|
64
|
+
case 'once':
|
|
65
|
+
return s.schedule_at ? `at ${s.schedule_at}` : 'one-shot';
|
|
66
|
+
case 'manual':
|
|
67
|
+
return 'manual only';
|
|
68
|
+
default:
|
|
69
|
+
return s.schedule_kind;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function GET() {
|
|
74
|
+
// Single listPipelinesSummary call — uses the parse cache, sorted desc.
|
|
75
|
+
// 200 runs cap is plenty; we'll filter into running + recent and discard
|
|
76
|
+
// the rest.
|
|
77
|
+
const all = listPipelinesSummary({ limit: 200 });
|
|
78
|
+
|
|
79
|
+
const running: RunningRow[] = [];
|
|
80
|
+
const recent: RecentRow[] = [];
|
|
81
|
+
for (const p of all) {
|
|
82
|
+
const isActive = p.status === 'running';
|
|
83
|
+
if (isActive && running.length < 30) {
|
|
84
|
+
// Compute progress + current node from the in-memory nodes map.
|
|
85
|
+
let done = 0;
|
|
86
|
+
let current: string | null = null;
|
|
87
|
+
for (const name of p.nodeOrder) {
|
|
88
|
+
const st = p.nodes[name]?.status;
|
|
89
|
+
if (st === 'done' || st === 'skipped') done++;
|
|
90
|
+
else if (!current && st === 'running') current = name;
|
|
91
|
+
}
|
|
92
|
+
running.push({
|
|
93
|
+
id: p.id,
|
|
94
|
+
workflowName: p.workflowName,
|
|
95
|
+
status: p.status,
|
|
96
|
+
currentNode: current,
|
|
97
|
+
progress: { done, total: p.nodeOrder.length },
|
|
98
|
+
createdAt: p.createdAt,
|
|
99
|
+
});
|
|
100
|
+
} else if (!isActive && recent.length < 8) {
|
|
101
|
+
const startedMs = Date.parse(p.createdAt);
|
|
102
|
+
const endedMs = p.completedAt ? Date.parse(p.completedAt) : NaN;
|
|
103
|
+
recent.push({
|
|
104
|
+
id: p.id,
|
|
105
|
+
workflowName: p.workflowName,
|
|
106
|
+
status: p.status,
|
|
107
|
+
completedAt: p.completedAt || null,
|
|
108
|
+
durationMs: isFinite(startedMs) && isFinite(endedMs) ? endedMs - startedMs : null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Upcoming = enabled schedules sorted by next_run_at ASC, top 10.
|
|
114
|
+
const schedules = listSchedules()
|
|
115
|
+
.filter((s) => s.enabled && s.next_run_at)
|
|
116
|
+
.sort((a, b) => String(a.next_run_at).localeCompare(String(b.next_run_at)))
|
|
117
|
+
.slice(0, 10);
|
|
118
|
+
const upcoming: UpcomingRow[] = schedules.map((s) => ({
|
|
119
|
+
id: s.id,
|
|
120
|
+
name: s.name,
|
|
121
|
+
body_kind: s.body_kind,
|
|
122
|
+
body_ref: s.body_ref,
|
|
123
|
+
next_run_at: s.next_run_at,
|
|
124
|
+
schedule_kind: s.schedule_kind,
|
|
125
|
+
schedule_summary: scheduleSummary(s),
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
const summary: Summary = {
|
|
129
|
+
running,
|
|
130
|
+
upcoming,
|
|
131
|
+
recent,
|
|
132
|
+
generated_at: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
return NextResponse.json(summary);
|
|
135
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface RunningRow {
|
|
6
|
+
id: string;
|
|
7
|
+
workflowName: string;
|
|
8
|
+
status: string;
|
|
9
|
+
currentNode: string | null;
|
|
10
|
+
progress: { done: number; total: number };
|
|
11
|
+
createdAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UpcomingRow {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
body_kind: string;
|
|
18
|
+
body_ref: string;
|
|
19
|
+
next_run_at: string | null;
|
|
20
|
+
schedule_kind: string;
|
|
21
|
+
schedule_summary: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RecentRow {
|
|
25
|
+
id: string;
|
|
26
|
+
workflowName: string;
|
|
27
|
+
status: string;
|
|
28
|
+
completedAt: string | null;
|
|
29
|
+
durationMs: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Summary {
|
|
33
|
+
running: RunningRow[];
|
|
34
|
+
upcoming: UpcomingRow[];
|
|
35
|
+
recent: RecentRow[];
|
|
36
|
+
generated_at: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const POLL_OPEN_MS = 5000; // active polling while open
|
|
40
|
+
const POLL_BADGE_MS = 30000; // background polling while closed (just for counts)
|
|
41
|
+
|
|
42
|
+
function timeAgo(iso: string | null): string {
|
|
43
|
+
if (!iso) return '';
|
|
44
|
+
const s = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
|
45
|
+
if (s < 60) return `${s}s ago`;
|
|
46
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
47
|
+
if (s < 86400) return `${Math.round(s / 3600)}h ago`;
|
|
48
|
+
return `${Math.round(s / 86400)}d ago`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function timeUntil(iso: string | null): string {
|
|
52
|
+
if (!iso) return '';
|
|
53
|
+
const s = Math.round((Date.parse(iso) - Date.now()) / 1000);
|
|
54
|
+
if (s < 0) return 'overdue';
|
|
55
|
+
if (s < 60) return `in ${s}s`;
|
|
56
|
+
if (s < 3600) return `in ${Math.round(s / 60)}m`;
|
|
57
|
+
if (s < 86400) return `in ${Math.round(s / 3600)}h`;
|
|
58
|
+
return `in ${Math.round(s / 86400)}d`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fmtDuration(ms: number | null): string {
|
|
62
|
+
if (ms == null) return '';
|
|
63
|
+
if (ms < 1000) return `${ms}ms`;
|
|
64
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
65
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function statusGlyph(s: string): string {
|
|
69
|
+
if (s === 'done') return '✓';
|
|
70
|
+
if (s === 'failed') return '✗';
|
|
71
|
+
if (s === 'cancelled') return '⊘';
|
|
72
|
+
if (s === 'running') return '▶';
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function statusColor(s: string): string {
|
|
77
|
+
if (s === 'done') return 'text-green-500';
|
|
78
|
+
if (s === 'failed') return 'text-red-400';
|
|
79
|
+
if (s === 'cancelled') return 'text-gray-400';
|
|
80
|
+
if (s === 'running') return 'text-blue-400';
|
|
81
|
+
return 'text-[var(--text-secondary)]';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Activity pill — top-right glanceable status for running pipelines +
|
|
86
|
+
* upcoming schedules. Click to expand a 3-section dropdown. Polls every
|
|
87
|
+
* 5s while open, 30s in background for badge counts.
|
|
88
|
+
*/
|
|
89
|
+
export default function ActivityPanel() {
|
|
90
|
+
const [summary, setSummary] = useState<Summary | null>(null);
|
|
91
|
+
const [open, setOpen] = useState(false);
|
|
92
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
93
|
+
|
|
94
|
+
const refetch = useCallback(async () => {
|
|
95
|
+
try {
|
|
96
|
+
const r = await fetch('/api/activity/summary');
|
|
97
|
+
if (!r.ok) return;
|
|
98
|
+
const j: Summary = await r.json();
|
|
99
|
+
setSummary(j);
|
|
100
|
+
} catch { /* network blip — silent */ }
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
// Initial fetch + adaptive polling cadence
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
refetch();
|
|
106
|
+
const ms = open ? POLL_OPEN_MS : POLL_BADGE_MS;
|
|
107
|
+
const id = setInterval(refetch, ms);
|
|
108
|
+
return () => clearInterval(id);
|
|
109
|
+
}, [open, refetch]);
|
|
110
|
+
|
|
111
|
+
// Click-outside dismisses
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!open) return;
|
|
114
|
+
const onClick = (e: MouseEvent) => {
|
|
115
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
window.addEventListener('mousedown', onClick);
|
|
120
|
+
return () => window.removeEventListener('mousedown', onClick);
|
|
121
|
+
}, [open]);
|
|
122
|
+
|
|
123
|
+
const runningCount = summary?.running.length ?? 0;
|
|
124
|
+
const upcomingCount = summary?.upcoming.length ?? 0;
|
|
125
|
+
const recentFailed = (summary?.recent ?? []).filter((r) => r.status === 'failed').length;
|
|
126
|
+
const hasAny = runningCount + upcomingCount + recentFailed > 0;
|
|
127
|
+
|
|
128
|
+
// Pill border tint picks the most urgent state:
|
|
129
|
+
// failed (red) > running (blue) > else dim.
|
|
130
|
+
const borderTint = recentFailed > 0
|
|
131
|
+
? 'border-red-500/50'
|
|
132
|
+
: runningCount > 0
|
|
133
|
+
? 'border-blue-500/50'
|
|
134
|
+
: 'border-[var(--border)]';
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="relative" ref={panelRef}>
|
|
138
|
+
<button
|
|
139
|
+
onClick={() => setOpen((o) => !o)}
|
|
140
|
+
className={`text-[10px] px-2 py-0.5 rounded border ${borderTint} flex items-center gap-2.5
|
|
141
|
+
text-[var(--text-secondary)] hover:text-[var(--text-primary)]`}
|
|
142
|
+
title="Activity — running pipelines · upcoming schedules · recent failures"
|
|
143
|
+
>
|
|
144
|
+
{!hasAny ? (
|
|
145
|
+
<span className="text-[var(--text-secondary)]">✓</span>
|
|
146
|
+
) : (
|
|
147
|
+
<>
|
|
148
|
+
{runningCount > 0 && (
|
|
149
|
+
<span className="inline-flex items-baseline text-blue-400" title={`${runningCount} running`}>
|
|
150
|
+
<span className="text-[7px] mr-0.5">●</span>
|
|
151
|
+
<span className="font-semibold tabular-nums">{runningCount}</span>
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
{upcomingCount > 0 && (
|
|
155
|
+
<span className="inline-flex items-baseline text-[var(--text-secondary)]" title={`${upcomingCount} upcoming`}>
|
|
156
|
+
<span className="text-[8px] mr-0.5">◷</span>
|
|
157
|
+
<span className="font-semibold tabular-nums">{upcomingCount}</span>
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
{recentFailed > 0 && (
|
|
161
|
+
<span className="inline-flex items-baseline text-red-400" title={`${recentFailed} recently failed`}>
|
|
162
|
+
<span className="text-[8px] mr-0.5">✕</span>
|
|
163
|
+
<span className="font-semibold tabular-nums">{recentFailed}</span>
|
|
164
|
+
</span>
|
|
165
|
+
)}
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
</button>
|
|
169
|
+
|
|
170
|
+
{open && (
|
|
171
|
+
<div
|
|
172
|
+
className="absolute right-0 mt-1 w-[440px] max-h-[70vh] overflow-y-auto bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50"
|
|
173
|
+
onClick={(e) => e.stopPropagation()}
|
|
174
|
+
>
|
|
175
|
+
{summary === null ? (
|
|
176
|
+
<div className="p-6 text-center text-xs text-[var(--text-secondary)]">Loading…</div>
|
|
177
|
+
) : (
|
|
178
|
+
<div className="p-3 space-y-4">
|
|
179
|
+
{/* Running */}
|
|
180
|
+
<Section title="Running" count={runningCount}>
|
|
181
|
+
{runningCount === 0 ? (
|
|
182
|
+
<Empty>Nothing running.</Empty>
|
|
183
|
+
) : (
|
|
184
|
+
summary.running.map((r) => (
|
|
185
|
+
<div key={r.id} className="flex items-start gap-2 text-xs py-1">
|
|
186
|
+
<span className={`${statusColor(r.status)} mt-0.5`}>{statusGlyph(r.status)}</span>
|
|
187
|
+
<div className="flex-1 min-w-0">
|
|
188
|
+
<div className="flex items-center gap-2">
|
|
189
|
+
<span className="text-[var(--text-primary)] font-medium truncate">{r.workflowName}</span>
|
|
190
|
+
<span className="text-[10px] text-[var(--text-secondary)]">
|
|
191
|
+
{r.progress.done}/{r.progress.total}
|
|
192
|
+
</span>
|
|
193
|
+
<span className="text-[10px] text-gray-500">{timeAgo(r.createdAt)}</span>
|
|
194
|
+
</div>
|
|
195
|
+
{r.currentNode && (
|
|
196
|
+
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
|
|
197
|
+
current: {r.currentNode}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
<button
|
|
202
|
+
onClick={() => {
|
|
203
|
+
setOpen(false);
|
|
204
|
+
window.dispatchEvent(new CustomEvent('forge:navigate', { detail: { view: 'pipelines', pipelineId: r.id } }));
|
|
205
|
+
}}
|
|
206
|
+
className="text-[10px] px-1.5 py-0.5 rounded border border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
207
|
+
>
|
|
208
|
+
view
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
))
|
|
212
|
+
)}
|
|
213
|
+
</Section>
|
|
214
|
+
|
|
215
|
+
{/* Upcoming */}
|
|
216
|
+
<Section title="Next up" count={upcomingCount}>
|
|
217
|
+
{upcomingCount === 0 ? (
|
|
218
|
+
<Empty>No enabled schedules.</Empty>
|
|
219
|
+
) : (
|
|
220
|
+
summary.upcoming.map((u) => (
|
|
221
|
+
<div key={u.id} className="flex items-start gap-2 text-xs py-1">
|
|
222
|
+
<span className="text-[var(--text-secondary)] mt-0.5">⏰</span>
|
|
223
|
+
<div className="flex-1 min-w-0">
|
|
224
|
+
<div className="flex items-center gap-2">
|
|
225
|
+
<span className="text-[var(--text-primary)] font-medium truncate">{u.name}</span>
|
|
226
|
+
<span className="text-[10px] text-blue-400">{timeUntil(u.next_run_at)}</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5 truncate">
|
|
229
|
+
{u.body_kind}: {u.body_ref} · {u.schedule_summary}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
))
|
|
234
|
+
)}
|
|
235
|
+
</Section>
|
|
236
|
+
|
|
237
|
+
{/* Recent */}
|
|
238
|
+
<Section title="Recent" count={summary.recent.length}>
|
|
239
|
+
{summary.recent.length === 0 ? (
|
|
240
|
+
<Empty>No recent runs.</Empty>
|
|
241
|
+
) : (
|
|
242
|
+
summary.recent.map((r) => (
|
|
243
|
+
<div key={r.id} className="flex items-start gap-2 text-xs py-1">
|
|
244
|
+
<span className={`${statusColor(r.status)} mt-0.5`}>{statusGlyph(r.status)}</span>
|
|
245
|
+
<div className="flex-1 min-w-0">
|
|
246
|
+
<div className="flex items-center gap-2">
|
|
247
|
+
<span className="text-[var(--text-primary)] truncate">{r.workflowName}</span>
|
|
248
|
+
<span className="text-[10px] text-[var(--text-secondary)]">
|
|
249
|
+
{fmtDuration(r.durationMs)} · {timeAgo(r.completedAt)}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
<button
|
|
254
|
+
onClick={() => {
|
|
255
|
+
setOpen(false);
|
|
256
|
+
window.dispatchEvent(new CustomEvent('forge:navigate', { detail: { view: 'pipelines', pipelineId: r.id } }));
|
|
257
|
+
}}
|
|
258
|
+
className="text-[10px] px-1.5 py-0.5 rounded border border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
259
|
+
>
|
|
260
|
+
view
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
))
|
|
264
|
+
)}
|
|
265
|
+
</Section>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function Section({ title, count, children }: { title: string; count: number; children: React.ReactNode }) {
|
|
275
|
+
return (
|
|
276
|
+
<div>
|
|
277
|
+
<h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-1 flex items-center gap-2">
|
|
278
|
+
{title}
|
|
279
|
+
<span className="text-gray-500 font-normal">({count})</span>
|
|
280
|
+
</h3>
|
|
281
|
+
<div className="space-y-0.5">{children}</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function Empty({ children }: { children: React.ReactNode }) {
|
|
287
|
+
return <div className="text-[10px] text-gray-500 italic py-0.5">{children}</div>;
|
|
288
|
+
}
|
package/components/Dashboard.tsx
CHANGED
|
@@ -25,6 +25,7 @@ const NewTaskModal = lazy(() => import('./NewTaskModal'));
|
|
|
25
25
|
const SettingsModal = lazy(() => import('./SettingsModal'));
|
|
26
26
|
const MonitorPanel = lazy(() => import('./MonitorPanel'));
|
|
27
27
|
const LoginStatusPanel = lazy(() => import('./LoginStatusPanel'));
|
|
28
|
+
const ActivityPanel = lazy(() => import('./ActivityPanel'));
|
|
28
29
|
const WorkspaceView = lazy(() => import('./WorkspaceView'));
|
|
29
30
|
// WorkspaceTree moved into ProjectDetail — no longer needed at Dashboard level
|
|
30
31
|
|
|
@@ -398,6 +399,11 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
398
399
|
>
|
|
399
400
|
Automation
|
|
400
401
|
</button>
|
|
402
|
+
{/* Activity sub-pill — sits next to Automation since its content
|
|
403
|
+
(running pipelines + upcoming schedules + recent runs) is the
|
|
404
|
+
live read-side of Automation. Click anywhere → dropdown with
|
|
405
|
+
3 sections + a "view" jump to the run. */}
|
|
406
|
+
<Suspense fallback={null}><ActivityPanel /></Suspense>
|
|
401
407
|
<span className="w-[2px] h-4 bg-[var(--text-secondary)]/30 mx-1.5" />
|
|
402
408
|
{/* Marketplace */}
|
|
403
409
|
<button
|
|
@@ -465,14 +471,6 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
465
471
|
</>
|
|
466
472
|
)}
|
|
467
473
|
</div>
|
|
468
|
-
<button
|
|
469
|
-
onClick={() => setViewMode('usage')}
|
|
470
|
-
className={`text-[10px] px-2 py-0.5 border rounded transition-colors ${
|
|
471
|
-
viewMode === 'usage'
|
|
472
|
-
? 'border-[var(--accent)] text-[var(--accent)]'
|
|
473
|
-
: 'border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-secondary)]'
|
|
474
|
-
}`}
|
|
475
|
-
>Usage</button>
|
|
476
474
|
<Suspense fallback={null}><TunnelToggle /></Suspense>
|
|
477
475
|
{onlineCount.total > 0 && (
|
|
478
476
|
<span className="text-[10px] text-[var(--text-secondary)] flex items-center gap-1" title={`${onlineCount.total} online${onlineCount.remote > 0 ? `, ${onlineCount.remote} remote` : ''}`}>
|
|
@@ -618,52 +616,73 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
618
616
|
{showUserMenu && (
|
|
619
617
|
<>
|
|
620
618
|
<div className="fixed inset-0 z-40" onClick={() => setShowUserMenu(false)} />
|
|
621
|
-
<div className="absolute right-0 top-8 w-[
|
|
619
|
+
<div className="absolute right-0 top-8 w-[170px] bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1">
|
|
620
|
+
{/* Settings promoted to the top — most-reached item in this
|
|
621
|
+
menu. Each row leads with a glyph so it's scannable. */}
|
|
622
|
+
<button
|
|
623
|
+
onClick={() => { setShowSettings(true); setShowUserMenu(false); }}
|
|
624
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
625
|
+
>
|
|
626
|
+
<span className="w-3 text-center">⚙</span><span>Settings</span>
|
|
627
|
+
</button>
|
|
628
|
+
{/* Chat (web) is a separate route — open in a new tab so
|
|
629
|
+
the dashboard isn't replaced. Promoted up here so it's
|
|
630
|
+
one of the prominent items, not buried below Logs. */}
|
|
631
|
+
<a
|
|
632
|
+
href="/chat"
|
|
633
|
+
target="_blank"
|
|
634
|
+
rel="noopener noreferrer"
|
|
635
|
+
className="block w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
636
|
+
>
|
|
637
|
+
<span className="w-3 text-center">💬</span>
|
|
638
|
+
<span className="flex-1">Chat (web)</span>
|
|
639
|
+
<span className="text-[9px] text-[var(--text-secondary)]">↗</span>
|
|
640
|
+
</a>
|
|
641
|
+
<div className="border-t border-[var(--border)] my-1" />
|
|
622
642
|
<button
|
|
623
643
|
onClick={() => { setShowMonitor(true); setShowUserMenu(false); }}
|
|
624
|
-
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
|
644
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
625
645
|
>
|
|
626
|
-
Monitor
|
|
646
|
+
<span className="w-3 text-center">📊</span><span>Monitor</span>
|
|
627
647
|
</button>
|
|
628
648
|
<button
|
|
629
649
|
onClick={() => { setShowLoginStatus(true); setShowUserMenu(false); }}
|
|
630
|
-
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center
|
|
650
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
631
651
|
>
|
|
632
|
-
<span
|
|
652
|
+
<span className="w-3 text-center">🔐</span>
|
|
653
|
+
<span className="flex-1">Login Status</span>
|
|
633
654
|
{loginBadge && loginBadge.broken > 0 && (
|
|
634
|
-
<span className="text-[9px] text-red-400"
|
|
655
|
+
<span className="text-[9px] text-red-400">{loginBadge.broken}/{loginBadge.total}</span>
|
|
635
656
|
)}
|
|
636
657
|
</button>
|
|
637
658
|
<button
|
|
638
|
-
onClick={() => {
|
|
639
|
-
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
|
659
|
+
onClick={() => { setViewMode('usage'); setShowUserMenu(false); }}
|
|
660
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
640
661
|
>
|
|
641
|
-
|
|
662
|
+
<span className="w-3 text-center">💰</span><span>Usage</span>
|
|
642
663
|
</button>
|
|
643
664
|
<button
|
|
644
665
|
onClick={() => { setViewMode('logs'); setShowUserMenu(false); }}
|
|
645
|
-
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
|
666
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
646
667
|
>
|
|
647
|
-
Logs
|
|
668
|
+
<span className="w-3 text-center">📜</span><span>Logs</span>
|
|
648
669
|
</button>
|
|
649
|
-
<a
|
|
650
|
-
href="/chat"
|
|
651
|
-
className="block w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
|
652
|
-
>
|
|
653
|
-
Chat (web)
|
|
654
|
-
</a>
|
|
655
670
|
<a
|
|
656
671
|
href="/mobile"
|
|
657
|
-
|
|
672
|
+
target="_blank"
|
|
673
|
+
rel="noopener noreferrer"
|
|
674
|
+
className="block w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
658
675
|
>
|
|
659
|
-
|
|
676
|
+
<span className="w-3 text-center">📱</span>
|
|
677
|
+
<span className="flex-1">Mobile View</span>
|
|
678
|
+
<span className="text-[9px] text-[var(--text-secondary)]">↗</span>
|
|
660
679
|
</a>
|
|
661
680
|
<div className="border-t border-[var(--border)] my-1" />
|
|
662
681
|
<button
|
|
663
682
|
onClick={() => signOut({ callbackUrl: '/login' })}
|
|
664
|
-
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--red)] hover:bg-[var(--bg-tertiary)]"
|
|
683
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--red)] hover:bg-[var(--bg-tertiary)] flex items-center gap-2"
|
|
665
684
|
>
|
|
666
|
-
Logout
|
|
685
|
+
<span className="w-3 text-center">⏻</span><span>Logout</span>
|
|
667
686
|
</button>
|
|
668
687
|
</div>
|
|
669
688
|
</>
|