@grackle-ai/web-components 0.107.2
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/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
- package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
- package/.rush/temp/operation/_phase_build/all.log +18 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/operation/_phase_test/all.log +121 -0
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
- package/.rush/temp/operation/_phase_test/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +938 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.tsx +30 -0
- package/config/rig.json +4 -0
- package/config/rush-project.json +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +39221 -0
- package/eslint.config.cjs +5 -0
- package/package.json +83 -0
- package/rush-logs/web-components._phase_build.cache.log +4 -0
- package/rush-logs/web-components._phase_test.cache.log +4 -0
- package/src/components/chat/ChatInput.module.scss +81 -0
- package/src/components/chat/ChatInput.stories.tsx +91 -0
- package/src/components/chat/ChatInput.tsx +168 -0
- package/src/components/chat/index.ts +6 -0
- package/src/components/dag/DagView.module.scss +149 -0
- package/src/components/dag/DagView.stories.tsx +125 -0
- package/src/components/dag/DagView.tsx +109 -0
- package/src/components/dag/TaskNode.stories.tsx +133 -0
- package/src/components/dag/TaskNode.tsx +40 -0
- package/src/components/dag/useDagLayout.ts +139 -0
- package/src/components/display/Breadcrumbs.module.scss +71 -0
- package/src/components/display/Breadcrumbs.stories.tsx +80 -0
- package/src/components/display/Breadcrumbs.tsx +46 -0
- package/src/components/display/Button.module.scss +110 -0
- package/src/components/display/Button.stories.tsx +88 -0
- package/src/components/display/Button.tsx +40 -0
- package/src/components/display/ConfirmDialog.module.scss +67 -0
- package/src/components/display/ConfirmDialog.stories.tsx +81 -0
- package/src/components/display/ConfirmDialog.tsx +88 -0
- package/src/components/display/CopyButton.module.scss +41 -0
- package/src/components/display/CopyButton.stories.tsx +78 -0
- package/src/components/display/CopyButton.tsx +64 -0
- package/src/components/display/DemoBanner.module.scss +37 -0
- package/src/components/display/DemoBanner.stories.tsx +40 -0
- package/src/components/display/DemoBanner.tsx +23 -0
- package/src/components/display/EventHoverRow.module.scss +102 -0
- package/src/components/display/EventHoverRow.stories.tsx +99 -0
- package/src/components/display/EventHoverRow.tsx +154 -0
- package/src/components/display/EventRenderer.module.scss +272 -0
- package/src/components/display/EventRenderer.stories.tsx +186 -0
- package/src/components/display/EventRenderer.tsx +271 -0
- package/src/components/display/EventStream.module.scss +93 -0
- package/src/components/display/EventStream.stories.tsx +249 -0
- package/src/components/display/EventStream.tsx +369 -0
- package/src/components/display/FloatingActionBar.module.scss +107 -0
- package/src/components/display/FloatingActionBar.stories.tsx +122 -0
- package/src/components/display/FloatingActionBar.tsx +119 -0
- package/src/components/display/SessionAttemptSelector.module.scss +50 -0
- package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
- package/src/components/display/SessionAttemptSelector.tsx +49 -0
- package/src/components/display/SessionPicker.module.scss +200 -0
- package/src/components/display/SessionPicker.stories.tsx +169 -0
- package/src/components/display/SessionPicker.tsx +214 -0
- package/src/components/display/Skeleton.module.scss +58 -0
- package/src/components/display/Skeleton.stories.tsx +94 -0
- package/src/components/display/Skeleton.tsx +127 -0
- package/src/components/display/Spinner.module.scss +41 -0
- package/src/components/display/Spinner.stories.tsx +66 -0
- package/src/components/display/Spinner.tsx +32 -0
- package/src/components/display/SplashScreen.module.scss +20 -0
- package/src/components/display/SplashScreen.stories.tsx +26 -0
- package/src/components/display/SplashScreen.tsx +16 -0
- package/src/components/display/SplitButton.module.scss +166 -0
- package/src/components/display/SplitButton.stories.tsx +95 -0
- package/src/components/display/SplitButton.tsx +128 -0
- package/src/components/display/Tooltip.module.scss +84 -0
- package/src/components/display/Tooltip.stories.tsx +240 -0
- package/src/components/display/Tooltip.tsx +184 -0
- package/src/components/display/extractText.test.tsx +48 -0
- package/src/components/display/index.ts +20 -0
- package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
- package/src/components/editable/EditableCheckbox.tsx +39 -0
- package/src/components/editable/EditableField.module.scss +135 -0
- package/src/components/editable/EditableSelect.tsx +164 -0
- package/src/components/editable/EditableTextArea.stories.tsx +50 -0
- package/src/components/editable/EditableTextArea.tsx +148 -0
- package/src/components/editable/EditableTextField.stories.tsx +62 -0
- package/src/components/editable/EditableTextField.tsx +153 -0
- package/src/components/editable/EnvironmentSelect.module.scss +17 -0
- package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
- package/src/components/editable/EnvironmentSelect.tsx +87 -0
- package/src/components/editable/index.ts +13 -0
- package/src/components/editable/useEditableField.test.tsx +233 -0
- package/src/components/editable/useEditableField.ts +173 -0
- package/src/components/index.ts +20 -0
- package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
- package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
- package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
- package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
- package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
- package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
- package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
- package/src/components/knowledge/KnowledgeNav.tsx +138 -0
- package/src/components/knowledge/index.ts +3 -0
- package/src/components/layout/AppNav.module.scss +82 -0
- package/src/components/layout/AppNav.stories.tsx +115 -0
- package/src/components/layout/AppNav.tsx +133 -0
- package/src/components/layout/BottomStatusBar.module.scss +58 -0
- package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
- package/src/components/layout/BottomStatusBar.tsx +206 -0
- package/src/components/layout/Sidebar.module.scss +60 -0
- package/src/components/layout/Sidebar.stories.tsx +46 -0
- package/src/components/layout/Sidebar.tsx +84 -0
- package/src/components/layout/StatusBar.module.scss +108 -0
- package/src/components/layout/StatusBar.stories.tsx +119 -0
- package/src/components/layout/StatusBar.tsx +70 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/lists/EnvironmentNav.module.scss +118 -0
- package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
- package/src/components/lists/EnvironmentNav.tsx +133 -0
- package/src/components/lists/FindingsNav.module.scss +126 -0
- package/src/components/lists/FindingsNav.tsx +146 -0
- package/src/components/lists/TaskList.module.scss +206 -0
- package/src/components/lists/TaskList.stories.tsx +401 -0
- package/src/components/lists/TaskList.tsx +509 -0
- package/src/components/lists/index.ts +6 -0
- package/src/components/lists/listHelpers.tsx +130 -0
- package/src/components/notifications/Callout.module.scss +83 -0
- package/src/components/notifications/Callout.stories.tsx +81 -0
- package/src/components/notifications/Callout.tsx +64 -0
- package/src/components/notifications/Toast.module.scss +86 -0
- package/src/components/notifications/Toast.stories.tsx +71 -0
- package/src/components/notifications/Toast.tsx +51 -0
- package/src/components/notifications/ToastContainer.module.scss +23 -0
- package/src/components/notifications/ToastContainer.stories.tsx +66 -0
- package/src/components/notifications/ToastContainer.tsx +29 -0
- package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
- package/src/components/notifications/UpdateBanner.test.tsx +64 -0
- package/src/components/notifications/UpdateBanner.tsx +44 -0
- package/src/components/notifications/index.ts +8 -0
- package/src/components/panels/AboutPanel.stories.tsx +70 -0
- package/src/components/panels/AboutPanel.tsx +66 -0
- package/src/components/panels/AppearancePanel.stories.tsx +45 -0
- package/src/components/panels/AppearancePanel.tsx +97 -0
- package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
- package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
- package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
- package/src/components/panels/FindingsPanel.module.scss +94 -0
- package/src/components/panels/FindingsPanel.stories.tsx +109 -0
- package/src/components/panels/FindingsPanel.tsx +76 -0
- package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
- package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
- package/src/components/panels/PluginsPanel.tsx +77 -0
- package/src/components/panels/SettingsPanel.module.scss +336 -0
- package/src/components/panels/TaskActionButtons.module.scss +22 -0
- package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
- package/src/components/panels/TaskActionButtons.tsx +87 -0
- package/src/components/panels/TaskEditPanel.module.scss +202 -0
- package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
- package/src/components/panels/TaskEditPanel.tsx +328 -0
- package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
- package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
- package/src/components/panels/TaskOverviewPanel.tsx +270 -0
- package/src/components/panels/TokensPanel.stories.tsx +131 -0
- package/src/components/panels/TokensPanel.tsx +143 -0
- package/src/components/panels/WorkpadPanel.module.scss +39 -0
- package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
- package/src/components/panels/WorkpadPanel.tsx +63 -0
- package/src/components/panels/index.ts +13 -0
- package/src/components/personas/McpToolSelector.module.scss +109 -0
- package/src/components/personas/McpToolSelector.stories.tsx +129 -0
- package/src/components/personas/McpToolSelector.tsx +180 -0
- package/src/components/personas/PersonaManager.module.scss +233 -0
- package/src/components/personas/PersonaManager.stories.tsx +139 -0
- package/src/components/personas/PersonaManager.tsx +122 -0
- package/src/components/schedules/ScheduleManager.module.scss +98 -0
- package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
- package/src/components/schedules/ScheduleManager.tsx +160 -0
- package/src/components/settings/SettingsNav.module.scss +82 -0
- package/src/components/settings/SettingsNav.stories.tsx +83 -0
- package/src/components/settings/SettingsNav.tsx +104 -0
- package/src/components/streams/StreamDetailPanel.module.scss +206 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
- package/src/components/streams/StreamDetailPanel.tsx +119 -0
- package/src/components/streams/StreamList.module.scss +92 -0
- package/src/components/streams/StreamList.stories.tsx +99 -0
- package/src/components/streams/StreamList.tsx +114 -0
- package/src/components/streams/index.ts +10 -0
- package/src/components/tools/AgentToolCard.module.scss +118 -0
- package/src/components/tools/AgentToolCard.stories.tsx +304 -0
- package/src/components/tools/AgentToolCard.tsx +247 -0
- package/src/components/tools/FileEditCard.stories.tsx +138 -0
- package/src/components/tools/FileEditCard.tsx +160 -0
- package/src/components/tools/FileReadCard.stories.tsx +120 -0
- package/src/components/tools/FileReadCard.tsx +106 -0
- package/src/components/tools/FindingCard.stories.tsx +124 -0
- package/src/components/tools/FindingCard.tsx +178 -0
- package/src/components/tools/GenericToolCard.stories.tsx +80 -0
- package/src/components/tools/GenericToolCard.tsx +111 -0
- package/src/components/tools/IpcCard.stories.tsx +129 -0
- package/src/components/tools/IpcCard.tsx +178 -0
- package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
- package/src/components/tools/KnowledgeCard.tsx +165 -0
- package/src/components/tools/MetadataCard.stories.tsx +32 -0
- package/src/components/tools/MetadataCard.tsx +39 -0
- package/src/components/tools/SearchCard.stories.tsx +74 -0
- package/src/components/tools/SearchCard.tsx +86 -0
- package/src/components/tools/ShellCard.stories.tsx +112 -0
- package/src/components/tools/ShellCard.tsx +106 -0
- package/src/components/tools/TaskCard.stories.tsx +123 -0
- package/src/components/tools/TaskCard.tsx +203 -0
- package/src/components/tools/TodoCard.module.scss +131 -0
- package/src/components/tools/TodoCard.stories.tsx +202 -0
- package/src/components/tools/TodoCard.tsx +200 -0
- package/src/components/tools/ToolCard.stories.tsx +177 -0
- package/src/components/tools/ToolCard.tsx +60 -0
- package/src/components/tools/ToolCardProps.ts +20 -0
- package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
- package/src/components/tools/ToolSearchCard.tsx +86 -0
- package/src/components/tools/WorkpadCard.stories.tsx +106 -0
- package/src/components/tools/WorkpadCard.tsx +125 -0
- package/src/components/tools/classifyTool.test.ts +44 -0
- package/src/components/tools/classifyTool.ts +134 -0
- package/src/components/tools/parseDiff.ts +95 -0
- package/src/components/tools/parseShellOutput.ts +28 -0
- package/src/components/tools/toolCardHelpers.test.ts +53 -0
- package/src/components/tools/toolCards.module.scss +234 -0
- package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
- package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
- package/src/components/workspace/WorkspaceBoard.tsx +232 -0
- package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
- package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
- package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
- package/src/context/GrackleContext.ts +28 -0
- package/src/context/GrackleContextTypes.ts +64 -0
- package/src/context/SidebarContext.tsx +53 -0
- package/src/context/ThemeContext.tsx +21 -0
- package/src/context/ToastContext.tsx +56 -0
- package/src/hooks/types.ts +864 -0
- package/src/hooks/useEventSelection.test.ts +204 -0
- package/src/hooks/useEventSelection.ts +158 -0
- package/src/hooks/useSmartScroll.ts +151 -0
- package/src/hooks/useTheme.ts +228 -0
- package/src/index.ts +210 -0
- package/src/mocks/MockGrackleProvider.tsx +1397 -0
- package/src/mocks/mockData.ts +1966 -0
- package/src/mocks/mockKnowledgeData.ts +294 -0
- package/src/scss.d.ts +12 -0
- package/src/styles/global.scss +244 -0
- package/src/styles/mixins.scss +278 -0
- package/src/styles/prism-theme.scss +148 -0
- package/src/styles/theme.scss +1102 -0
- package/src/test-utils/storybook-decorators.tsx +50 -0
- package/src/test-utils/storybook-helpers.ts +262 -0
- package/src/themes.ts +142 -0
- package/src/utils/boardColumns.ts +141 -0
- package/src/utils/breadcrumbs.test.ts +285 -0
- package/src/utils/breadcrumbs.ts +222 -0
- package/src/utils/dashboard.test.ts +156 -0
- package/src/utils/dashboard.ts +195 -0
- package/src/utils/eventContent.test.ts +353 -0
- package/src/utils/eventContent.ts +209 -0
- package/src/utils/findingCategory.ts +33 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/iconSize.ts +18 -0
- package/src/utils/navigation.ts +205 -0
- package/src/utils/route-config.test.ts +128 -0
- package/src/utils/scrollUtils.test.ts +65 -0
- package/src/utils/scrollUtils.ts +49 -0
- package/src/utils/sessionEvents.test.ts +302 -0
- package/src/utils/sessionEvents.ts +233 -0
- package/src/utils/taskStatus.tsx +137 -0
- package/src/utils/time.ts +92 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard data selectors — derive KPIs, attention lists, and rollups
|
|
3
|
+
* from the live Grackle state (sessions, tasks, environments, workspaces).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Environment, Session, TaskData, Workspace } from "../hooks/types.js";
|
|
7
|
+
|
|
8
|
+
// ─── KPI computation ────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/** Summary KPIs surfaced across the top of the dashboard. */
|
|
11
|
+
export interface DashboardKpis {
|
|
12
|
+
activeSessions: number;
|
|
13
|
+
blockedTasks: number;
|
|
14
|
+
attentionTasks: number;
|
|
15
|
+
unhealthyEnvironments: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Compute dashboard KPI counts from live state. */
|
|
19
|
+
export function computeKpis(
|
|
20
|
+
sessions: Session[],
|
|
21
|
+
tasks: TaskData[],
|
|
22
|
+
environments: Environment[],
|
|
23
|
+
): DashboardKpis {
|
|
24
|
+
const activeSessions = sessions.filter(
|
|
25
|
+
(s) => s.status === "running" || s.status === "idle" || s.status === "waiting",
|
|
26
|
+
).length;
|
|
27
|
+
|
|
28
|
+
const taskStatusById = buildTaskStatusMap(tasks);
|
|
29
|
+
|
|
30
|
+
const blockedTasks = tasks.filter((t) => isTaskBlocked(t, taskStatusById)).length;
|
|
31
|
+
|
|
32
|
+
const attentionTasks = tasks.filter(
|
|
33
|
+
(t) =>
|
|
34
|
+
t.status === "paused" ||
|
|
35
|
+
t.status === "failed" ||
|
|
36
|
+
isTaskBlocked(t, taskStatusById),
|
|
37
|
+
).length;
|
|
38
|
+
|
|
39
|
+
const unhealthyEnvironments = environments.filter(
|
|
40
|
+
(e) => e.status === "disconnected" || e.status === "error",
|
|
41
|
+
).length;
|
|
42
|
+
|
|
43
|
+
return { activeSessions, blockedTasks, attentionTasks, unhealthyEnvironments };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Task helpers ───────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** Build a lookup map of task-id → status. */
|
|
49
|
+
function buildTaskStatusMap(tasks: TaskData[]): Map<string, string> {
|
|
50
|
+
const map = new Map<string, string>();
|
|
51
|
+
for (const t of tasks) {
|
|
52
|
+
map.set(t.id, t.status);
|
|
53
|
+
}
|
|
54
|
+
return map;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns true if the task has unresolved (non-complete) dependencies. */
|
|
58
|
+
function isTaskBlocked(task: TaskData, statusMap: Map<string, string>): boolean {
|
|
59
|
+
return task.dependsOn.some((depId) => statusMap.get(depId) !== "complete");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A task that needs operator attention (blocked, paused, or failed). */
|
|
63
|
+
export interface AttentionTask {
|
|
64
|
+
task: TaskData;
|
|
65
|
+
reason: "blocked" | "paused" | "failed";
|
|
66
|
+
workspaceName: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Collect tasks requiring attention, ordered: failed → blocked → paused. */
|
|
70
|
+
export function getAttentionTasks(
|
|
71
|
+
tasks: TaskData[],
|
|
72
|
+
workspaces: Workspace[],
|
|
73
|
+
): AttentionTask[] {
|
|
74
|
+
const wsMap = new Map<string, Workspace>();
|
|
75
|
+
for (const ws of workspaces) {
|
|
76
|
+
wsMap.set(ws.id, ws);
|
|
77
|
+
}
|
|
78
|
+
const taskStatusMap = buildTaskStatusMap(tasks);
|
|
79
|
+
|
|
80
|
+
const result: AttentionTask[] = [];
|
|
81
|
+
|
|
82
|
+
for (const task of tasks) {
|
|
83
|
+
const workspaceName = task.workspaceId
|
|
84
|
+
? (wsMap.get(task.workspaceId)?.name ?? "Unknown")
|
|
85
|
+
: "Unknown";
|
|
86
|
+
|
|
87
|
+
if (task.status === "failed") {
|
|
88
|
+
result.push({ task, reason: "failed", workspaceName });
|
|
89
|
+
} else if (isTaskBlocked(task, taskStatusMap)) {
|
|
90
|
+
result.push({ task, reason: "blocked", workspaceName });
|
|
91
|
+
} else if (task.status === "paused") {
|
|
92
|
+
result.push({ task, reason: "paused", workspaceName });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Sort: failed first, then blocked, then paused
|
|
97
|
+
const ORDER: Record<string, number> = { failed: 0, blocked: 1, paused: 2 };
|
|
98
|
+
result.sort((a, b) => (ORDER[a.reason] ?? 3) - (ORDER[b.reason] ?? 3));
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Active sessions with context ───────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** A session enriched with display context. */
|
|
106
|
+
export interface ActiveSession {
|
|
107
|
+
session: Session;
|
|
108
|
+
environmentName: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Get active sessions (running/idle/waiting) with resolved environment names. */
|
|
112
|
+
export function getActiveSessions(
|
|
113
|
+
sessions: Session[],
|
|
114
|
+
environments: Environment[],
|
|
115
|
+
): ActiveSession[] {
|
|
116
|
+
const envMap = new Map<string, Environment>();
|
|
117
|
+
for (const e of environments) {
|
|
118
|
+
envMap.set(e.id, e);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return sessions
|
|
122
|
+
.filter((s) => s.status === "running" || s.status === "idle" || s.status === "waiting")
|
|
123
|
+
.map((session) => ({
|
|
124
|
+
session,
|
|
125
|
+
environmentName: envMap.get(session.environmentId)?.displayName ?? "Unknown",
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Workspace snapshots ────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/** Progress rollup for a single workspace. */
|
|
132
|
+
export interface WorkspaceSnapshot {
|
|
133
|
+
workspace: Workspace;
|
|
134
|
+
totalTasks: number;
|
|
135
|
+
completedTasks: number;
|
|
136
|
+
workingTasks: number;
|
|
137
|
+
failedTasks: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Build progress snapshots for each workspace. */
|
|
141
|
+
export function getWorkspaceSnapshots(
|
|
142
|
+
workspaces: Workspace[],
|
|
143
|
+
tasks: TaskData[],
|
|
144
|
+
_environments: Environment[],
|
|
145
|
+
): WorkspaceSnapshot[] {
|
|
146
|
+
const statsByWorkspace = new Map<string, {
|
|
147
|
+
totalTasks: number;
|
|
148
|
+
completedTasks: number;
|
|
149
|
+
workingTasks: number;
|
|
150
|
+
failedTasks: number;
|
|
151
|
+
}>();
|
|
152
|
+
|
|
153
|
+
for (const task of tasks) {
|
|
154
|
+
if (!task.workspaceId) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let stats = statsByWorkspace.get(task.workspaceId);
|
|
159
|
+
if (!stats) {
|
|
160
|
+
stats = {
|
|
161
|
+
totalTasks: 0,
|
|
162
|
+
completedTasks: 0,
|
|
163
|
+
workingTasks: 0,
|
|
164
|
+
failedTasks: 0,
|
|
165
|
+
};
|
|
166
|
+
statsByWorkspace.set(task.workspaceId, stats);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
stats.totalTasks += 1;
|
|
170
|
+
if (task.status === "complete") {
|
|
171
|
+
stats.completedTasks += 1;
|
|
172
|
+
} else if (task.status === "working") {
|
|
173
|
+
stats.workingTasks += 1;
|
|
174
|
+
} else if (task.status === "failed") {
|
|
175
|
+
stats.failedTasks += 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return workspaces.map((workspace) => {
|
|
180
|
+
const stats = statsByWorkspace.get(workspace.id) ?? {
|
|
181
|
+
totalTasks: 0,
|
|
182
|
+
completedTasks: 0,
|
|
183
|
+
workingTasks: 0,
|
|
184
|
+
failedTasks: 0,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
workspace,
|
|
189
|
+
totalTasks: stats.totalTasks,
|
|
190
|
+
completedTasks: stats.completedTasks,
|
|
191
|
+
workingTasks: stats.workingTasks,
|
|
192
|
+
failedTasks: stats.failedTasks,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isContentBearingEvent,
|
|
4
|
+
getEventCopyText,
|
|
5
|
+
formatEventsAsMarkdown,
|
|
6
|
+
formatForwardEnvelope,
|
|
7
|
+
} from "./eventContent.js";
|
|
8
|
+
import type { SessionEvent } from "../hooks/types.js";
|
|
9
|
+
import type { DisplayEvent } from "./sessionEvents.js";
|
|
10
|
+
|
|
11
|
+
/** Helper to build a minimal SessionEvent. */
|
|
12
|
+
function makeEvent(overrides: Partial<SessionEvent> & { eventType: string }): SessionEvent {
|
|
13
|
+
return {
|
|
14
|
+
sessionId: "sess-1",
|
|
15
|
+
timestamp: "2026-01-15T14:34:00Z",
|
|
16
|
+
content: "",
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Helper to build a DisplayEvent with optional toolUseCtx. */
|
|
22
|
+
function makeDisplayEvent(
|
|
23
|
+
overrides: Partial<DisplayEvent> & { eventType: string },
|
|
24
|
+
): DisplayEvent {
|
|
25
|
+
return {
|
|
26
|
+
sessionId: "sess-1",
|
|
27
|
+
timestamp: "2026-01-15T14:34:00Z",
|
|
28
|
+
content: "",
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// isContentBearingEvent
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
describe("isContentBearingEvent", () => {
|
|
38
|
+
it("returns true for text events", () => {
|
|
39
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "text" }))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns true for output events", () => {
|
|
43
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "output" }))).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns true for user_input events", () => {
|
|
47
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "user_input" }))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns true for tool_use events", () => {
|
|
51
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "tool_use" }))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns true for tool_result events", () => {
|
|
55
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "tool_result" }))).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns true for error events", () => {
|
|
59
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "error" }))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns false for status events", () => {
|
|
63
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "status" }))).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns false for signal events", () => {
|
|
67
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "signal" }))).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns false for usage events", () => {
|
|
71
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "usage" }))).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("returns false for system events", () => {
|
|
75
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "system" }))).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns false for unknown event types", () => {
|
|
79
|
+
expect(isContentBearingEvent(makeEvent({ eventType: "unknown_future_type" }))).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// getEventCopyText
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
describe("getEventCopyText", () => {
|
|
88
|
+
it("returns content for text events", () => {
|
|
89
|
+
const event = makeDisplayEvent({ eventType: "text", content: "Hello world" });
|
|
90
|
+
expect(getEventCopyText(event)).toBe("Hello world");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns content for user_input events", () => {
|
|
94
|
+
const event = makeDisplayEvent({ eventType: "user_input", content: "Fix the bug" });
|
|
95
|
+
expect(getEventCopyText(event)).toBe("Fix the bug");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns content for error events", () => {
|
|
99
|
+
const event = makeDisplayEvent({ eventType: "error", content: "Something broke" });
|
|
100
|
+
expect(getEventCopyText(event)).toBe("Something broke");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("extracts content from JSON-wrapped tool_result", () => {
|
|
104
|
+
const event = makeDisplayEvent({
|
|
105
|
+
eventType: "tool_result",
|
|
106
|
+
content: JSON.stringify({ content: "file contents here" }),
|
|
107
|
+
});
|
|
108
|
+
expect(getEventCopyText(event)).toBe("file contents here");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns raw content for plain-text tool_result", () => {
|
|
112
|
+
const event = makeDisplayEvent({
|
|
113
|
+
eventType: "tool_result",
|
|
114
|
+
content: "plain result text",
|
|
115
|
+
});
|
|
116
|
+
expect(getEventCopyText(event)).toBe("plain result text");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("formats tool_use as tool name + args", () => {
|
|
120
|
+
const event = makeDisplayEvent({
|
|
121
|
+
eventType: "tool_use",
|
|
122
|
+
content: JSON.stringify({ tool: "Read", args: { file_path: "src/index.ts" } }),
|
|
123
|
+
});
|
|
124
|
+
const result = getEventCopyText(event);
|
|
125
|
+
expect(result).toContain("Read");
|
|
126
|
+
expect(result).toContain("src/index.ts");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns raw content for unparseable tool_use", () => {
|
|
130
|
+
const event = makeDisplayEvent({
|
|
131
|
+
eventType: "tool_use",
|
|
132
|
+
content: "not json",
|
|
133
|
+
});
|
|
134
|
+
expect(getEventCopyText(event)).toBe("not json");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// formatEventsAsMarkdown
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
describe("formatEventsAsMarkdown", () => {
|
|
143
|
+
it("returns empty string for empty array", () => {
|
|
144
|
+
expect(formatEventsAsMarkdown([])).toBe("");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("formats a single text event with Assistant label and timestamp", () => {
|
|
148
|
+
const event = makeDisplayEvent({
|
|
149
|
+
eventType: "text",
|
|
150
|
+
content: "I found the bug.",
|
|
151
|
+
timestamp: "2026-01-15T14:34:00Z",
|
|
152
|
+
});
|
|
153
|
+
const md = formatEventsAsMarkdown([event]);
|
|
154
|
+
expect(md).toContain("**Assistant**");
|
|
155
|
+
expect(md).toContain("I found the bug.");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("formats a user_input event with User label", () => {
|
|
159
|
+
const event = makeDisplayEvent({
|
|
160
|
+
eventType: "user_input",
|
|
161
|
+
content: "Fix the login bug",
|
|
162
|
+
});
|
|
163
|
+
const md = formatEventsAsMarkdown([event]);
|
|
164
|
+
expect(md).toContain("**User**");
|
|
165
|
+
expect(md).toContain("Fix the login bug");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("formats a tool_result event with paired tool_use context", () => {
|
|
169
|
+
const event = makeDisplayEvent({
|
|
170
|
+
eventType: "tool_result",
|
|
171
|
+
content: "const x = 42;",
|
|
172
|
+
toolUseCtx: { tool: "Read", args: { file_path: "src/index.ts" } },
|
|
173
|
+
});
|
|
174
|
+
const md = formatEventsAsMarkdown([event]);
|
|
175
|
+
expect(md).toContain("**Tool: Read**");
|
|
176
|
+
expect(md).toContain("`src/index.ts`");
|
|
177
|
+
expect(md).toContain("const x = 42;");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("formats an unpaired tool_result with generic label", () => {
|
|
181
|
+
const event = makeDisplayEvent({
|
|
182
|
+
eventType: "tool_result",
|
|
183
|
+
content: "some result",
|
|
184
|
+
});
|
|
185
|
+
const md = formatEventsAsMarkdown([event]);
|
|
186
|
+
expect(md).toContain("**Tool output**");
|
|
187
|
+
expect(md).toContain("some result");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("formats an error event", () => {
|
|
191
|
+
const event = makeDisplayEvent({
|
|
192
|
+
eventType: "error",
|
|
193
|
+
content: "Connection refused",
|
|
194
|
+
});
|
|
195
|
+
const md = formatEventsAsMarkdown([event]);
|
|
196
|
+
expect(md).toContain("**Error**");
|
|
197
|
+
expect(md).toContain("Connection refused");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("preserves code fences within content", () => {
|
|
201
|
+
const event = makeDisplayEvent({
|
|
202
|
+
eventType: "text",
|
|
203
|
+
content: "Here is code:\n```ts\nconst x = 42;\n```",
|
|
204
|
+
});
|
|
205
|
+
const md = formatEventsAsMarkdown([event]);
|
|
206
|
+
expect(md).toContain("```ts");
|
|
207
|
+
expect(md).toContain("const x = 42;");
|
|
208
|
+
expect(md).toContain("```");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("formats multiple events in order with blank line separation", () => {
|
|
212
|
+
const events: DisplayEvent[] = [
|
|
213
|
+
makeDisplayEvent({
|
|
214
|
+
eventType: "user_input",
|
|
215
|
+
content: "Fix the bug",
|
|
216
|
+
timestamp: "2026-01-15T14:34:00Z",
|
|
217
|
+
}),
|
|
218
|
+
makeDisplayEvent({
|
|
219
|
+
eventType: "text",
|
|
220
|
+
content: "Looking into it.",
|
|
221
|
+
timestamp: "2026-01-15T14:34:05Z",
|
|
222
|
+
}),
|
|
223
|
+
];
|
|
224
|
+
const md = formatEventsAsMarkdown(events);
|
|
225
|
+
const userIdx = md.indexOf("**User**");
|
|
226
|
+
const assistantIdx = md.indexOf("**Assistant**");
|
|
227
|
+
expect(userIdx).toBeLessThan(assistantIdx);
|
|
228
|
+
// Blank line separation
|
|
229
|
+
expect(md).toContain("\n\n");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("skips non-content-bearing events", () => {
|
|
233
|
+
const events: DisplayEvent[] = [
|
|
234
|
+
makeDisplayEvent({ eventType: "text", content: "Hello" }),
|
|
235
|
+
makeDisplayEvent({ eventType: "status", content: "running" }),
|
|
236
|
+
makeDisplayEvent({ eventType: "text", content: "World" }),
|
|
237
|
+
];
|
|
238
|
+
const md = formatEventsAsMarkdown(events);
|
|
239
|
+
expect(md).not.toContain("running");
|
|
240
|
+
expect(md).toContain("Hello");
|
|
241
|
+
expect(md).toContain("World");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("formats a tool_use event with tool name, args summary, and JSON body", () => {
|
|
245
|
+
const event = makeDisplayEvent({
|
|
246
|
+
eventType: "tool_use",
|
|
247
|
+
content: JSON.stringify({ tool: "Bash", args: { command: "npm test" } }),
|
|
248
|
+
});
|
|
249
|
+
const md = formatEventsAsMarkdown([event]);
|
|
250
|
+
expect(md).toContain("**Tool: Bash**");
|
|
251
|
+
expect(md).toContain("`npm test`");
|
|
252
|
+
expect(md).toContain("```json");
|
|
253
|
+
expect(md).toContain("npm test");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("prefers detailedResult for tool_result in getEventCopyText", () => {
|
|
257
|
+
const event = makeDisplayEvent({
|
|
258
|
+
eventType: "tool_result",
|
|
259
|
+
content: "short result",
|
|
260
|
+
toolUseCtx: { tool: "Edit", args: {}, detailedResult: "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new" },
|
|
261
|
+
});
|
|
262
|
+
expect(getEventCopyText(event)).toContain("--- a/file");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("prefers detailedResult for tool_result in formatEventsAsMarkdown", () => {
|
|
266
|
+
const event = makeDisplayEvent({
|
|
267
|
+
eventType: "tool_result",
|
|
268
|
+
content: "short result",
|
|
269
|
+
toolUseCtx: { tool: "Edit", args: {}, detailedResult: "full diff content here" },
|
|
270
|
+
});
|
|
271
|
+
const md = formatEventsAsMarkdown([event]);
|
|
272
|
+
expect(md).toContain("full diff content here");
|
|
273
|
+
expect(md).not.toContain("short result");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// formatForwardEnvelope
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
describe("formatForwardEnvelope", () => {
|
|
282
|
+
it("wraps content in forwarding envelope markers", () => {
|
|
283
|
+
const events: DisplayEvent[] = [
|
|
284
|
+
makeDisplayEvent({ eventType: "text", content: "Hello" }),
|
|
285
|
+
];
|
|
286
|
+
const result = formatForwardEnvelope("my-env", events);
|
|
287
|
+
expect(result).toContain("--- Forwarded from my-env ---");
|
|
288
|
+
expect(result).toContain("--- End forwarded ---");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("includes the formatted event markdown in the body", () => {
|
|
292
|
+
const events: DisplayEvent[] = [
|
|
293
|
+
makeDisplayEvent({ eventType: "user_input", content: "Fix the bug" }),
|
|
294
|
+
];
|
|
295
|
+
const result = formatForwardEnvelope("staging", events);
|
|
296
|
+
expect(result).toContain("**User**");
|
|
297
|
+
expect(result).toContain("Fix the bug");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("includes the sourceLabel in the header", () => {
|
|
301
|
+
const result = formatForwardEnvelope("prod-env / main", [
|
|
302
|
+
makeDisplayEvent({ eventType: "text", content: "done" }),
|
|
303
|
+
]);
|
|
304
|
+
expect(result).toContain("--- Forwarded from prod-env / main ---");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("sanitizes newlines in sourceLabel to spaces", () => {
|
|
308
|
+
const result = formatForwardEnvelope("env\nwith\nnewlines", [
|
|
309
|
+
makeDisplayEvent({ eventType: "text", content: "x" }),
|
|
310
|
+
]);
|
|
311
|
+
expect(result).toContain("--- Forwarded from env with newlines ---");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("sanitizes --- sequences in sourceLabel to em-dashes", () => {
|
|
315
|
+
const result = formatForwardEnvelope("env---name", [
|
|
316
|
+
makeDisplayEvent({ eventType: "text", content: "x" }),
|
|
317
|
+
]);
|
|
318
|
+
expect(result).toContain("--- Forwarded from env\u2014name ---");
|
|
319
|
+
expect(result).not.toContain("env---name");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("envelope body starts after header and ends before footer", () => {
|
|
323
|
+
const events: DisplayEvent[] = [
|
|
324
|
+
makeDisplayEvent({ eventType: "text", content: "line one" }),
|
|
325
|
+
];
|
|
326
|
+
const result = formatForwardEnvelope("env-1", events);
|
|
327
|
+
const headerEnd = result.indexOf("--- Forwarded from env-1 ---") + "--- Forwarded from env-1 ---".length;
|
|
328
|
+
const footerStart = result.indexOf("--- End forwarded ---");
|
|
329
|
+
const body = result.slice(headerEnd, footerStart).trim();
|
|
330
|
+
expect(body).toContain("line one");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("skips non-content-bearing events in the body", () => {
|
|
334
|
+
const events: DisplayEvent[] = [
|
|
335
|
+
makeDisplayEvent({ eventType: "text", content: "visible" }),
|
|
336
|
+
makeDisplayEvent({ eventType: "status", content: "running" }),
|
|
337
|
+
];
|
|
338
|
+
const result = formatForwardEnvelope("env", events);
|
|
339
|
+
expect(result).toContain("visible");
|
|
340
|
+
expect(result).not.toContain("running");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("formats multiple events separated by blank lines within the envelope", () => {
|
|
344
|
+
const events: DisplayEvent[] = [
|
|
345
|
+
makeDisplayEvent({ eventType: "user_input", content: "first" }),
|
|
346
|
+
makeDisplayEvent({ eventType: "text", content: "second" }),
|
|
347
|
+
];
|
|
348
|
+
const result = formatForwardEnvelope("env", events);
|
|
349
|
+
expect(result).toContain("\n\n");
|
|
350
|
+
expect(result).toContain("first");
|
|
351
|
+
expect(result).toContain("second");
|
|
352
|
+
});
|
|
353
|
+
});
|