@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,232 @@
|
|
|
1
|
+
import { useMemo, type JSX } from "react";
|
|
2
|
+
import type { TaskData, Session, PersonaData, Environment } from "../../hooks/types.js";
|
|
3
|
+
import { buildBoardColumns, type BoardTask } from "../../utils/boardColumns.js";
|
|
4
|
+
import { getStatusStyle } from "../../utils/taskStatus.js";
|
|
5
|
+
import { taskUrl, newTaskUrl, useAppNavigate } from "../../utils/navigation.js";
|
|
6
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
7
|
+
import styles from "./WorkspaceBoard.module.scss";
|
|
8
|
+
|
|
9
|
+
/** Props for the WorkspaceBoard component. */
|
|
10
|
+
interface WorkspaceBoardProps {
|
|
11
|
+
workspaceId: string;
|
|
12
|
+
environmentId: string;
|
|
13
|
+
/** All tasks — filtered internally by workspaceId. */
|
|
14
|
+
tasks: TaskData[];
|
|
15
|
+
/** All sessions (used for metadata lookup). */
|
|
16
|
+
sessions: Session[];
|
|
17
|
+
/** All personas (used for metadata lookup). */
|
|
18
|
+
personas: PersonaData[];
|
|
19
|
+
/** All environments (used for metadata lookup). */
|
|
20
|
+
environments: Environment[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Kanban-style board view with fixed columns for each task status. */
|
|
24
|
+
export function WorkspaceBoard({ workspaceId, environmentId, tasks, sessions, personas, environments }: WorkspaceBoardProps): JSX.Element {
|
|
25
|
+
const navigate = useAppNavigate();
|
|
26
|
+
|
|
27
|
+
const workspaceTasks = useMemo(
|
|
28
|
+
() => tasks.filter((t) => t.workspaceId === workspaceId),
|
|
29
|
+
[tasks, workspaceId],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const taskStatusById = useMemo(
|
|
33
|
+
() => new Map(tasks.map((t) => [t.id, t.status])),
|
|
34
|
+
[tasks],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const tasksById = useMemo(
|
|
38
|
+
() => new Map(workspaceTasks.map((t) => [t.id, t])),
|
|
39
|
+
[workspaceTasks],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const boardMetadataByTaskId = useMemo(() => {
|
|
43
|
+
const sessionById = new Map(sessions.map((s) => [s.id, s]));
|
|
44
|
+
const personaById = new Map(personas.map((p) => [p.id, p]));
|
|
45
|
+
const environmentById = new Map(environments.map((e) => [e.id, e]));
|
|
46
|
+
const sessionStatusByTaskId = new Map<string, string>();
|
|
47
|
+
const personaNameByTaskId = new Map<string, string>();
|
|
48
|
+
const environmentNameByTaskId = new Map<string, string>();
|
|
49
|
+
|
|
50
|
+
for (const task of workspaceTasks) {
|
|
51
|
+
if (task.latestSessionId) {
|
|
52
|
+
const session = sessionById.get(task.latestSessionId);
|
|
53
|
+
if (session) {
|
|
54
|
+
sessionStatusByTaskId.set(task.id, session.endReason || session.status);
|
|
55
|
+
|
|
56
|
+
if (session.personaId) {
|
|
57
|
+
const persona = personaById.get(session.personaId);
|
|
58
|
+
if (persona) {
|
|
59
|
+
personaNameByTaskId.set(task.id, persona.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (session.environmentId) {
|
|
64
|
+
const environment = environmentById.get(session.environmentId);
|
|
65
|
+
if (environment) {
|
|
66
|
+
environmentNameByTaskId.set(task.id, environment.displayName);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
sessionStatusByTaskId,
|
|
75
|
+
personaNameByTaskId,
|
|
76
|
+
environmentNameByTaskId,
|
|
77
|
+
};
|
|
78
|
+
}, [workspaceTasks, sessions, personas, environments]);
|
|
79
|
+
|
|
80
|
+
const columns = useMemo(
|
|
81
|
+
() => buildBoardColumns({
|
|
82
|
+
tasks: workspaceTasks,
|
|
83
|
+
taskStatusById,
|
|
84
|
+
sessionStatusByTaskId: boardMetadataByTaskId.sessionStatusByTaskId,
|
|
85
|
+
}),
|
|
86
|
+
[workspaceTasks, taskStatusById, boardMetadataByTaskId],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (workspaceTasks.length === 0) {
|
|
90
|
+
return (
|
|
91
|
+
<div className={styles.emptyCta} data-testid="board-empty-cta">
|
|
92
|
+
<button
|
|
93
|
+
className={styles.ctaButton}
|
|
94
|
+
onClick={() => navigate(newTaskUrl(workspaceId, undefined, environmentId))}
|
|
95
|
+
>
|
|
96
|
+
Create Task
|
|
97
|
+
</button>
|
|
98
|
+
<div className={styles.ctaDescription}>
|
|
99
|
+
Break your work into tasks and let agents tackle them
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className={styles.boardContainer} data-testid="board-container">
|
|
107
|
+
{columns.map((col) => (
|
|
108
|
+
<section
|
|
109
|
+
key={col.status}
|
|
110
|
+
className={styles.column}
|
|
111
|
+
data-testid={`board-column-${col.status}`}
|
|
112
|
+
aria-label={`${col.label}, ${col.tasks.length} ${col.tasks.length === 1 ? "task" : "tasks"}`}
|
|
113
|
+
>
|
|
114
|
+
<div className={styles.columnHeader}>
|
|
115
|
+
<span className={styles.columnIcon} style={{ color: col.style.color }}>
|
|
116
|
+
{col.style.icon}
|
|
117
|
+
</span>
|
|
118
|
+
<span className={styles.columnLabel}>{col.label}</span>
|
|
119
|
+
<span className={styles.columnCount} data-testid={`board-count-${col.status}`}>
|
|
120
|
+
{col.tasks.length}
|
|
121
|
+
</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div className={styles.cardList}>
|
|
124
|
+
{col.tasks.length === 0 ? (
|
|
125
|
+
<div className={styles.emptyPlaceholder}>No tasks</div>
|
|
126
|
+
) : (
|
|
127
|
+
<AnimatePresence mode="popLayout">
|
|
128
|
+
{col.tasks.map((bt) => (
|
|
129
|
+
<motion.div
|
|
130
|
+
key={bt.task.id}
|
|
131
|
+
layout
|
|
132
|
+
initial={{ opacity: 0, y: 4 }}
|
|
133
|
+
animate={{ opacity: 1, y: 0 }}
|
|
134
|
+
exit={{ opacity: 0, y: -4 }}
|
|
135
|
+
transition={{ duration: 0.15 }}
|
|
136
|
+
>
|
|
137
|
+
<BoardCard
|
|
138
|
+
boardTask={bt}
|
|
139
|
+
tasksById={tasksById}
|
|
140
|
+
personaName={boardMetadataByTaskId.personaNameByTaskId.get(bt.task.id)}
|
|
141
|
+
envName={boardMetadataByTaskId.environmentNameByTaskId.get(bt.task.id)}
|
|
142
|
+
onClick={() => navigate(taskUrl(bt.task.id, undefined, workspaceId, environmentId))}
|
|
143
|
+
/>
|
|
144
|
+
</motion.div>
|
|
145
|
+
))}
|
|
146
|
+
</AnimatePresence>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// BoardCard
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
interface BoardCardProps {
|
|
160
|
+
boardTask: BoardTask;
|
|
161
|
+
tasksById: Map<string, TaskData>;
|
|
162
|
+
personaName?: string;
|
|
163
|
+
envName?: string;
|
|
164
|
+
onClick: () => void;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Individual card rendered inside a board column. */
|
|
168
|
+
function BoardCard({ boardTask, tasksById, personaName, envName, onClick }: BoardCardProps): JSX.Element {
|
|
169
|
+
const { task, isBlocked, childCount, doneChildCount, pausedSubBadge } = boardTask;
|
|
170
|
+
const statusStyle = getStatusStyle(task.status);
|
|
171
|
+
const parentTask = task.parentTaskId ? tasksById.get(task.parentTaskId) : undefined;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div
|
|
175
|
+
className={styles.card}
|
|
176
|
+
tabIndex={0}
|
|
177
|
+
role="button"
|
|
178
|
+
data-testid={`board-card-${task.id}`}
|
|
179
|
+
onClick={onClick}
|
|
180
|
+
onKeyDown={(e) => {
|
|
181
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
onClick();
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
<div className={styles.cardHeader}>
|
|
188
|
+
<span className={styles.cardStatusIcon} style={{ color: statusStyle.color }}>
|
|
189
|
+
{statusStyle.icon}
|
|
190
|
+
</span>
|
|
191
|
+
<span className={styles.cardTitle}>{task.title}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className={styles.cardBadges}>
|
|
194
|
+
{parentTask && (
|
|
195
|
+
<span className={`${styles.badge} ${styles.parentBadge}`} title={parentTask.title}>
|
|
196
|
+
{parentTask.title}
|
|
197
|
+
</span>
|
|
198
|
+
)}
|
|
199
|
+
{childCount > 0 && (
|
|
200
|
+
<span className={`${styles.badge} ${styles.childBadge}`}>
|
|
201
|
+
{doneChildCount}/{childCount}
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
{isBlocked && (
|
|
205
|
+
<span className={`${styles.badge} ${styles.blockedBadge}`}>
|
|
206
|
+
blocked
|
|
207
|
+
</span>
|
|
208
|
+
)}
|
|
209
|
+
{task.dependsOn.length > 0 && !isBlocked && (
|
|
210
|
+
<span className={`${styles.badge} ${styles.depBadge}`}>
|
|
211
|
+
dep
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
{pausedSubBadge && (
|
|
215
|
+
<span className={`${styles.badge} ${styles.pausedSubBadge}`}>
|
|
216
|
+
{pausedSubBadge}
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
{personaName && (
|
|
220
|
+
<span className={`${styles.badge} ${styles.personaBadge}`}>
|
|
221
|
+
{personaName}
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
{envName && (
|
|
225
|
+
<span className={`${styles.badge} ${styles.envBadge}`}>
|
|
226
|
+
{envName}
|
|
227
|
+
</span>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@use '../../styles/mixins' as *;
|
|
2
|
+
|
|
3
|
+
.formContent {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--space-lg);
|
|
7
|
+
max-width: 680px;
|
|
8
|
+
|
|
9
|
+
@include mobile {
|
|
10
|
+
max-width: 100%;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.section {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--space-sm);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.label {
|
|
21
|
+
font-size: 11px;
|
|
22
|
+
color: var(--text-tertiary);
|
|
23
|
+
text-transform: uppercase;
|
|
24
|
+
letter-spacing: 0.05em;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.titleInput {
|
|
28
|
+
@include input-field;
|
|
29
|
+
font-size: var(--font-size-sm);
|
|
30
|
+
color: var(--text-primary);
|
|
31
|
+
padding: var(--space-sm) var(--space-md);
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.descriptionTextarea {
|
|
36
|
+
@include input-field;
|
|
37
|
+
font-size: var(--font-size-sm);
|
|
38
|
+
color: var(--text-secondary);
|
|
39
|
+
resize: vertical;
|
|
40
|
+
min-height: 120px;
|
|
41
|
+
padding: var(--space-sm) var(--space-md);
|
|
42
|
+
width: 100%;
|
|
43
|
+
font-family: var(--font-mono);
|
|
44
|
+
line-height: 1.5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.selectField {
|
|
48
|
+
@include input-field;
|
|
49
|
+
font-size: var(--font-size-sm);
|
|
50
|
+
color: var(--text-secondary);
|
|
51
|
+
padding: var(--space-xs) var(--space-sm);
|
|
52
|
+
width: 100%;
|
|
53
|
+
max-width: 320px;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.checkboxRow {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: var(--space-sm);
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
|
|
63
|
+
input[type="checkbox"] {
|
|
64
|
+
accent-color: var(--accent-green);
|
|
65
|
+
width: 14px;
|
|
66
|
+
height: 14px;
|
|
67
|
+
flex-shrink: 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.checkboxLabel {
|
|
72
|
+
font-size: var(--font-size-sm);
|
|
73
|
+
color: var(--text-secondary);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.fieldError {
|
|
77
|
+
font-size: var(--font-size-xs);
|
|
78
|
+
color: var(--accent-red);
|
|
79
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn } from "@storybook/test";
|
|
3
|
+
import { WorkspaceFormFields, defaultFormValues } from "./WorkspaceFormFields.js";
|
|
4
|
+
import type { Environment, PersonaData } from "../../hooks/types.js";
|
|
5
|
+
import { makeEnvironment, makePersona } from "../../test-utils/storybook-helpers.js";
|
|
6
|
+
|
|
7
|
+
const envLocal: Environment = makeEnvironment({
|
|
8
|
+
id: "env-1",
|
|
9
|
+
displayName: "Local Machine",
|
|
10
|
+
status: "connected",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const envSSH: Environment = makeEnvironment({
|
|
14
|
+
id: "env-2",
|
|
15
|
+
displayName: "Dev Server",
|
|
16
|
+
adapterType: "ssh",
|
|
17
|
+
status: "ready",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const personaDefault: PersonaData = makePersona({
|
|
21
|
+
id: "persona-1",
|
|
22
|
+
name: "Code Reviewer",
|
|
23
|
+
description: "Reviews pull requests",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const personaWriter: PersonaData = makePersona({
|
|
27
|
+
id: "persona-2",
|
|
28
|
+
name: "Tech Writer",
|
|
29
|
+
description: "Writes documentation",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const meta: Meta<typeof WorkspaceFormFields> = {
|
|
33
|
+
component: WorkspaceFormFields,
|
|
34
|
+
title: "Grackle/Workspace/WorkspaceFormFields",
|
|
35
|
+
tags: ["autodocs"],
|
|
36
|
+
args: {
|
|
37
|
+
values: defaultFormValues(),
|
|
38
|
+
onChange: fn(),
|
|
39
|
+
environments: [envLocal, envSSH],
|
|
40
|
+
personas: [personaDefault, personaWriter],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
|
|
46
|
+
/** Empty form with default values. */
|
|
47
|
+
export const EmptyForm: Story = {
|
|
48
|
+
play: async ({ canvas }) => {
|
|
49
|
+
const nameInput = canvas.getByTestId("workspace-form-name");
|
|
50
|
+
await expect(nameInput).toBeInTheDocument();
|
|
51
|
+
await expect(nameInput).toHaveValue("");
|
|
52
|
+
|
|
53
|
+
const descInput = canvas.getByTestId("workspace-form-description");
|
|
54
|
+
await expect(descInput).toBeInTheDocument();
|
|
55
|
+
await expect(descInput).toHaveValue("");
|
|
56
|
+
|
|
57
|
+
const repoInput = canvas.getByTestId("workspace-form-repo");
|
|
58
|
+
await expect(repoInput).toBeInTheDocument();
|
|
59
|
+
await expect(repoInput).toHaveValue("");
|
|
60
|
+
|
|
61
|
+
// Environment select should list environments
|
|
62
|
+
const envSelect = canvas.getByTestId("workspace-form-environment");
|
|
63
|
+
await expect(envSelect).toBeInTheDocument();
|
|
64
|
+
|
|
65
|
+
// Persona select should be present
|
|
66
|
+
const personaSelect = canvas.getByTestId("workspace-form-persona");
|
|
67
|
+
await expect(personaSelect).toBeInTheDocument();
|
|
68
|
+
|
|
69
|
+
// Worktree checkbox should be checked by default
|
|
70
|
+
const worktreeCheckbox = canvas.getByTestId("workspace-form-worktrees");
|
|
71
|
+
await expect(worktreeCheckbox).toBeChecked();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Form displaying validation errors. */
|
|
76
|
+
export const WithErrors: Story = {
|
|
77
|
+
args: {
|
|
78
|
+
values: defaultFormValues(),
|
|
79
|
+
errors: {
|
|
80
|
+
name: "Name is required",
|
|
81
|
+
environmentId: "Environment is required",
|
|
82
|
+
repoUrl: "Invalid URL",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
play: async ({ canvas }) => {
|
|
86
|
+
const nameError = canvas.getByTestId("workspace-form-error-name");
|
|
87
|
+
await expect(nameError).toBeInTheDocument();
|
|
88
|
+
await expect(nameError).toHaveTextContent("Name is required");
|
|
89
|
+
|
|
90
|
+
const envError = canvas.getByTestId("workspace-form-error-environmentId");
|
|
91
|
+
await expect(envError).toBeInTheDocument();
|
|
92
|
+
await expect(envError).toHaveTextContent("Environment is required");
|
|
93
|
+
|
|
94
|
+
const repoError = canvas.getByTestId("workspace-form-error-repoUrl");
|
|
95
|
+
await expect(repoError).toBeInTheDocument();
|
|
96
|
+
await expect(repoError).toHaveTextContent("Invalid URL");
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Disabled form prevents all interactions. */
|
|
101
|
+
export const Disabled: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
values: {
|
|
104
|
+
...defaultFormValues(),
|
|
105
|
+
name: "My Workspace",
|
|
106
|
+
environmentId: "env-1",
|
|
107
|
+
},
|
|
108
|
+
disabled: true,
|
|
109
|
+
},
|
|
110
|
+
play: async ({ canvas }) => {
|
|
111
|
+
const nameInput = canvas.getByTestId("workspace-form-name");
|
|
112
|
+
await expect(nameInput).toBeDisabled();
|
|
113
|
+
await expect(nameInput).toHaveValue("My Workspace");
|
|
114
|
+
|
|
115
|
+
const descInput = canvas.getByTestId("workspace-form-description");
|
|
116
|
+
await expect(descInput).toBeDisabled();
|
|
117
|
+
|
|
118
|
+
const repoInput = canvas.getByTestId("workspace-form-repo");
|
|
119
|
+
await expect(repoInput).toBeDisabled();
|
|
120
|
+
|
|
121
|
+
const envSelect = canvas.getByTestId("workspace-form-environment");
|
|
122
|
+
await expect(envSelect).toBeDisabled();
|
|
123
|
+
|
|
124
|
+
const personaSelect = canvas.getByTestId("workspace-form-persona");
|
|
125
|
+
await expect(personaSelect).toBeDisabled();
|
|
126
|
+
|
|
127
|
+
const worktreeCheckbox = canvas.getByTestId("workspace-form-worktrees");
|
|
128
|
+
await expect(worktreeCheckbox).toBeDisabled();
|
|
129
|
+
|
|
130
|
+
const workdirInput = canvas.getByTestId("workspace-form-workdir");
|
|
131
|
+
await expect(workdirInput).toBeDisabled();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared workspace form fields used by both the create page and the inline-edit
|
|
3
|
+
* detail page. Each field renders its own label + input/control.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX } from "react";
|
|
9
|
+
import type { Workspace, Environment, PersonaData } from "../../hooks/types.js";
|
|
10
|
+
import styles from "./WorkspaceFormFields.module.scss";
|
|
11
|
+
|
|
12
|
+
/** Fields managed by the workspace form. */
|
|
13
|
+
export interface WorkspaceFormValues {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
repoUrl: string;
|
|
17
|
+
environmentId: string;
|
|
18
|
+
defaultPersonaId: string;
|
|
19
|
+
useWorktrees: boolean;
|
|
20
|
+
workingDirectory: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build a blank set of defaults, optionally seeded from a workspace. */
|
|
24
|
+
export function defaultFormValues(ws?: Workspace, environmentId?: string): WorkspaceFormValues {
|
|
25
|
+
return {
|
|
26
|
+
name: ws?.name ?? "",
|
|
27
|
+
description: ws?.description ?? "",
|
|
28
|
+
repoUrl: ws?.repoUrl ?? "",
|
|
29
|
+
environmentId: ws?.linkedEnvironmentIds[0] ?? environmentId ?? "",
|
|
30
|
+
defaultPersonaId: ws?.defaultPersonaId ?? "",
|
|
31
|
+
useWorktrees: ws?.useWorktrees ?? true,
|
|
32
|
+
workingDirectory: ws?.workingDirectory ?? "",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Props for {@link WorkspaceFormFields}. */
|
|
37
|
+
interface WorkspaceFormFieldsProps {
|
|
38
|
+
values: WorkspaceFormValues;
|
|
39
|
+
onChange: (values: WorkspaceFormValues) => void;
|
|
40
|
+
environments: Environment[];
|
|
41
|
+
personas: PersonaData[];
|
|
42
|
+
/** Validation errors keyed by field name. */
|
|
43
|
+
errors?: Partial<Record<keyof WorkspaceFormValues, string>>;
|
|
44
|
+
/** Whether the form is in a submitting state. */
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
/** Whether the name field should receive autofocus on mount. */
|
|
47
|
+
autoFocusName?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MAX_NAME_LENGTH: number = 100;
|
|
51
|
+
|
|
52
|
+
/** Shared form fields for workspace create/edit. */
|
|
53
|
+
export function WorkspaceFormFields({
|
|
54
|
+
values,
|
|
55
|
+
onChange,
|
|
56
|
+
environments,
|
|
57
|
+
personas,
|
|
58
|
+
errors,
|
|
59
|
+
disabled,
|
|
60
|
+
autoFocusName,
|
|
61
|
+
}: WorkspaceFormFieldsProps): JSX.Element {
|
|
62
|
+
const set = <K extends keyof WorkspaceFormValues>(key: K, val: WorkspaceFormValues[K]): void => {
|
|
63
|
+
onChange({ ...values, [key]: val });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className={styles.formContent}>
|
|
68
|
+
{/* Name */}
|
|
69
|
+
<div className={styles.section}>
|
|
70
|
+
<label className={styles.label} htmlFor="ws-name">Name</label>
|
|
71
|
+
<input
|
|
72
|
+
id="ws-name"
|
|
73
|
+
className={styles.titleInput}
|
|
74
|
+
type="text"
|
|
75
|
+
value={values.name}
|
|
76
|
+
onChange={(e) => set("name", e.target.value)}
|
|
77
|
+
placeholder="Workspace name"
|
|
78
|
+
maxLength={MAX_NAME_LENGTH}
|
|
79
|
+
autoFocus={autoFocusName}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
data-testid="workspace-form-name"
|
|
82
|
+
/>
|
|
83
|
+
{errors?.name && <span className={styles.fieldError} data-testid="workspace-form-error-name">{errors.name}</span>}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Description */}
|
|
87
|
+
<div className={styles.section}>
|
|
88
|
+
<label className={styles.label} htmlFor="ws-description">Description</label>
|
|
89
|
+
<textarea
|
|
90
|
+
id="ws-description"
|
|
91
|
+
className={styles.descriptionTextarea}
|
|
92
|
+
value={values.description}
|
|
93
|
+
onChange={(e) => set("description", e.target.value)}
|
|
94
|
+
placeholder="Optional description (Markdown supported)"
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
data-testid="workspace-form-description"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Repository URL */}
|
|
101
|
+
<div className={styles.section}>
|
|
102
|
+
<label className={styles.label} htmlFor="ws-repo">Repository URL</label>
|
|
103
|
+
<input
|
|
104
|
+
id="ws-repo"
|
|
105
|
+
className={styles.titleInput}
|
|
106
|
+
type="text"
|
|
107
|
+
value={values.repoUrl}
|
|
108
|
+
onChange={(e) => set("repoUrl", e.target.value)}
|
|
109
|
+
placeholder="https://github.com/org/repo"
|
|
110
|
+
disabled={disabled}
|
|
111
|
+
data-testid="workspace-form-repo"
|
|
112
|
+
/>
|
|
113
|
+
{errors?.repoUrl && <span className={styles.fieldError} data-testid="workspace-form-error-repoUrl">{errors.repoUrl}</span>}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{/* Environment */}
|
|
117
|
+
<div className={styles.section}>
|
|
118
|
+
<label className={styles.label} htmlFor="ws-environment">Environment</label>
|
|
119
|
+
<select
|
|
120
|
+
id="ws-environment"
|
|
121
|
+
className={styles.selectField}
|
|
122
|
+
value={values.environmentId}
|
|
123
|
+
onChange={(e) => set("environmentId", e.target.value)}
|
|
124
|
+
disabled={disabled}
|
|
125
|
+
data-testid="workspace-form-environment"
|
|
126
|
+
>
|
|
127
|
+
<option value="">Select environment…</option>
|
|
128
|
+
{environments.map((env) => (
|
|
129
|
+
<option key={env.id} value={env.id}>
|
|
130
|
+
{env.displayName || env.id}
|
|
131
|
+
</option>
|
|
132
|
+
))}
|
|
133
|
+
</select>
|
|
134
|
+
{errors?.environmentId && <span className={styles.fieldError} data-testid="workspace-form-error-environmentId">{errors.environmentId}</span>}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Default Persona */}
|
|
138
|
+
<div className={styles.section}>
|
|
139
|
+
<label className={styles.label} htmlFor="ws-persona">Default Persona</label>
|
|
140
|
+
<select
|
|
141
|
+
id="ws-persona"
|
|
142
|
+
className={styles.selectField}
|
|
143
|
+
value={values.defaultPersonaId}
|
|
144
|
+
onChange={(e) => set("defaultPersonaId", e.target.value)}
|
|
145
|
+
disabled={disabled}
|
|
146
|
+
data-testid="workspace-form-persona"
|
|
147
|
+
>
|
|
148
|
+
<option value="">(Inherit)</option>
|
|
149
|
+
{personas.map((p) => (
|
|
150
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
151
|
+
))}
|
|
152
|
+
</select>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Worktree isolation */}
|
|
156
|
+
<div className={styles.section}>
|
|
157
|
+
<label className={styles.checkboxRow}>
|
|
158
|
+
<input
|
|
159
|
+
type="checkbox"
|
|
160
|
+
checked={values.useWorktrees}
|
|
161
|
+
onChange={(e) => set("useWorktrees", e.target.checked)}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
data-testid="workspace-form-worktrees"
|
|
164
|
+
/>
|
|
165
|
+
<span className={styles.checkboxLabel}>Enable worktree isolation</span>
|
|
166
|
+
</label>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Working directory */}
|
|
170
|
+
<div className={styles.section}>
|
|
171
|
+
<label className={styles.label} htmlFor="ws-workdir">Working Directory</label>
|
|
172
|
+
<input
|
|
173
|
+
id="ws-workdir"
|
|
174
|
+
className={styles.titleInput}
|
|
175
|
+
type="text"
|
|
176
|
+
value={values.workingDirectory}
|
|
177
|
+
onChange={(e) => set("workingDirectory", e.target.value)}
|
|
178
|
+
placeholder="Default (server default)"
|
|
179
|
+
disabled={disabled}
|
|
180
|
+
data-testid="workspace-form-workdir"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GrackleContext definition used by both the real GrackleProvider (in @grackle-ai/web)
|
|
3
|
+
* and MockGrackleProvider (in this package). Only the raw context + consumer hook live here;
|
|
4
|
+
* the actual provider with gRPC wiring lives in @grackle-ai/web.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { createContext, useContext } from "react";
|
|
10
|
+
import type { UseGrackleSocketResult } from "./GrackleContextTypes.js";
|
|
11
|
+
|
|
12
|
+
/** Re-export the socket result type so consumers can reference it. */
|
|
13
|
+
export type { UseGrackleSocketResult };
|
|
14
|
+
|
|
15
|
+
/** Alias for the context value type. */
|
|
16
|
+
export type GrackleContextType = UseGrackleSocketResult;
|
|
17
|
+
|
|
18
|
+
/** The raw React context for Grackle state. */
|
|
19
|
+
export const GrackleContext: React.Context<GrackleContextType | undefined> = createContext<GrackleContextType | undefined>(undefined);
|
|
20
|
+
|
|
21
|
+
/** Consumes the Grackle context; must be called within a GrackleProvider or MockGrackleProvider. */
|
|
22
|
+
export function useGrackle(): GrackleContextType {
|
|
23
|
+
const ctx = useContext(GrackleContext);
|
|
24
|
+
if (!ctx) {
|
|
25
|
+
throw new Error("useGrackle must be used within GrackleProvider");
|
|
26
|
+
}
|
|
27
|
+
return ctx;
|
|
28
|
+
}
|