@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.
Files changed (279) hide show
  1. package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
  2. package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
  3. package/.rush/temp/operation/_phase_build/all.log +18 -0
  4. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
  5. package/.rush/temp/operation/_phase_build/state.json +3 -0
  6. package/.rush/temp/operation/_phase_test/all.log +121 -0
  7. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
  8. package/.rush/temp/operation/_phase_test/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +938 -0
  10. package/.storybook/main.ts +22 -0
  11. package/.storybook/preview.tsx +30 -0
  12. package/config/rig.json +4 -0
  13. package/config/rush-project.json +12 -0
  14. package/dist/index.css +1 -0
  15. package/dist/index.js +39221 -0
  16. package/eslint.config.cjs +5 -0
  17. package/package.json +83 -0
  18. package/rush-logs/web-components._phase_build.cache.log +4 -0
  19. package/rush-logs/web-components._phase_test.cache.log +4 -0
  20. package/src/components/chat/ChatInput.module.scss +81 -0
  21. package/src/components/chat/ChatInput.stories.tsx +91 -0
  22. package/src/components/chat/ChatInput.tsx +168 -0
  23. package/src/components/chat/index.ts +6 -0
  24. package/src/components/dag/DagView.module.scss +149 -0
  25. package/src/components/dag/DagView.stories.tsx +125 -0
  26. package/src/components/dag/DagView.tsx +109 -0
  27. package/src/components/dag/TaskNode.stories.tsx +133 -0
  28. package/src/components/dag/TaskNode.tsx +40 -0
  29. package/src/components/dag/useDagLayout.ts +139 -0
  30. package/src/components/display/Breadcrumbs.module.scss +71 -0
  31. package/src/components/display/Breadcrumbs.stories.tsx +80 -0
  32. package/src/components/display/Breadcrumbs.tsx +46 -0
  33. package/src/components/display/Button.module.scss +110 -0
  34. package/src/components/display/Button.stories.tsx +88 -0
  35. package/src/components/display/Button.tsx +40 -0
  36. package/src/components/display/ConfirmDialog.module.scss +67 -0
  37. package/src/components/display/ConfirmDialog.stories.tsx +81 -0
  38. package/src/components/display/ConfirmDialog.tsx +88 -0
  39. package/src/components/display/CopyButton.module.scss +41 -0
  40. package/src/components/display/CopyButton.stories.tsx +78 -0
  41. package/src/components/display/CopyButton.tsx +64 -0
  42. package/src/components/display/DemoBanner.module.scss +37 -0
  43. package/src/components/display/DemoBanner.stories.tsx +40 -0
  44. package/src/components/display/DemoBanner.tsx +23 -0
  45. package/src/components/display/EventHoverRow.module.scss +102 -0
  46. package/src/components/display/EventHoverRow.stories.tsx +99 -0
  47. package/src/components/display/EventHoverRow.tsx +154 -0
  48. package/src/components/display/EventRenderer.module.scss +272 -0
  49. package/src/components/display/EventRenderer.stories.tsx +186 -0
  50. package/src/components/display/EventRenderer.tsx +271 -0
  51. package/src/components/display/EventStream.module.scss +93 -0
  52. package/src/components/display/EventStream.stories.tsx +249 -0
  53. package/src/components/display/EventStream.tsx +369 -0
  54. package/src/components/display/FloatingActionBar.module.scss +107 -0
  55. package/src/components/display/FloatingActionBar.stories.tsx +122 -0
  56. package/src/components/display/FloatingActionBar.tsx +119 -0
  57. package/src/components/display/SessionAttemptSelector.module.scss +50 -0
  58. package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
  59. package/src/components/display/SessionAttemptSelector.tsx +49 -0
  60. package/src/components/display/SessionPicker.module.scss +200 -0
  61. package/src/components/display/SessionPicker.stories.tsx +169 -0
  62. package/src/components/display/SessionPicker.tsx +214 -0
  63. package/src/components/display/Skeleton.module.scss +58 -0
  64. package/src/components/display/Skeleton.stories.tsx +94 -0
  65. package/src/components/display/Skeleton.tsx +127 -0
  66. package/src/components/display/Spinner.module.scss +41 -0
  67. package/src/components/display/Spinner.stories.tsx +66 -0
  68. package/src/components/display/Spinner.tsx +32 -0
  69. package/src/components/display/SplashScreen.module.scss +20 -0
  70. package/src/components/display/SplashScreen.stories.tsx +26 -0
  71. package/src/components/display/SplashScreen.tsx +16 -0
  72. package/src/components/display/SplitButton.module.scss +166 -0
  73. package/src/components/display/SplitButton.stories.tsx +95 -0
  74. package/src/components/display/SplitButton.tsx +128 -0
  75. package/src/components/display/Tooltip.module.scss +84 -0
  76. package/src/components/display/Tooltip.stories.tsx +240 -0
  77. package/src/components/display/Tooltip.tsx +184 -0
  78. package/src/components/display/extractText.test.tsx +48 -0
  79. package/src/components/display/index.ts +20 -0
  80. package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
  81. package/src/components/editable/EditableCheckbox.tsx +39 -0
  82. package/src/components/editable/EditableField.module.scss +135 -0
  83. package/src/components/editable/EditableSelect.tsx +164 -0
  84. package/src/components/editable/EditableTextArea.stories.tsx +50 -0
  85. package/src/components/editable/EditableTextArea.tsx +148 -0
  86. package/src/components/editable/EditableTextField.stories.tsx +62 -0
  87. package/src/components/editable/EditableTextField.tsx +153 -0
  88. package/src/components/editable/EnvironmentSelect.module.scss +17 -0
  89. package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
  90. package/src/components/editable/EnvironmentSelect.tsx +87 -0
  91. package/src/components/editable/index.ts +13 -0
  92. package/src/components/editable/useEditableField.test.tsx +233 -0
  93. package/src/components/editable/useEditableField.ts +173 -0
  94. package/src/components/index.ts +20 -0
  95. package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
  96. package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
  97. package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
  98. package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
  99. package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
  100. package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
  101. package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
  102. package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
  103. package/src/components/knowledge/KnowledgeNav.tsx +138 -0
  104. package/src/components/knowledge/index.ts +3 -0
  105. package/src/components/layout/AppNav.module.scss +82 -0
  106. package/src/components/layout/AppNav.stories.tsx +115 -0
  107. package/src/components/layout/AppNav.tsx +133 -0
  108. package/src/components/layout/BottomStatusBar.module.scss +58 -0
  109. package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
  110. package/src/components/layout/BottomStatusBar.tsx +206 -0
  111. package/src/components/layout/Sidebar.module.scss +60 -0
  112. package/src/components/layout/Sidebar.stories.tsx +46 -0
  113. package/src/components/layout/Sidebar.tsx +84 -0
  114. package/src/components/layout/StatusBar.module.scss +108 -0
  115. package/src/components/layout/StatusBar.stories.tsx +119 -0
  116. package/src/components/layout/StatusBar.tsx +70 -0
  117. package/src/components/layout/index.ts +9 -0
  118. package/src/components/lists/EnvironmentNav.module.scss +118 -0
  119. package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
  120. package/src/components/lists/EnvironmentNav.tsx +133 -0
  121. package/src/components/lists/FindingsNav.module.scss +126 -0
  122. package/src/components/lists/FindingsNav.tsx +146 -0
  123. package/src/components/lists/TaskList.module.scss +206 -0
  124. package/src/components/lists/TaskList.stories.tsx +401 -0
  125. package/src/components/lists/TaskList.tsx +509 -0
  126. package/src/components/lists/index.ts +6 -0
  127. package/src/components/lists/listHelpers.tsx +130 -0
  128. package/src/components/notifications/Callout.module.scss +83 -0
  129. package/src/components/notifications/Callout.stories.tsx +81 -0
  130. package/src/components/notifications/Callout.tsx +64 -0
  131. package/src/components/notifications/Toast.module.scss +86 -0
  132. package/src/components/notifications/Toast.stories.tsx +71 -0
  133. package/src/components/notifications/Toast.tsx +51 -0
  134. package/src/components/notifications/ToastContainer.module.scss +23 -0
  135. package/src/components/notifications/ToastContainer.stories.tsx +66 -0
  136. package/src/components/notifications/ToastContainer.tsx +29 -0
  137. package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
  138. package/src/components/notifications/UpdateBanner.test.tsx +64 -0
  139. package/src/components/notifications/UpdateBanner.tsx +44 -0
  140. package/src/components/notifications/index.ts +8 -0
  141. package/src/components/panels/AboutPanel.stories.tsx +70 -0
  142. package/src/components/panels/AboutPanel.tsx +66 -0
  143. package/src/components/panels/AppearancePanel.stories.tsx +45 -0
  144. package/src/components/panels/AppearancePanel.tsx +97 -0
  145. package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
  146. package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
  147. package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
  148. package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
  149. package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
  150. package/src/components/panels/FindingsPanel.module.scss +94 -0
  151. package/src/components/panels/FindingsPanel.stories.tsx +109 -0
  152. package/src/components/panels/FindingsPanel.tsx +76 -0
  153. package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
  154. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
  155. package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
  156. package/src/components/panels/PluginsPanel.tsx +77 -0
  157. package/src/components/panels/SettingsPanel.module.scss +336 -0
  158. package/src/components/panels/TaskActionButtons.module.scss +22 -0
  159. package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
  160. package/src/components/panels/TaskActionButtons.tsx +87 -0
  161. package/src/components/panels/TaskEditPanel.module.scss +202 -0
  162. package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
  163. package/src/components/panels/TaskEditPanel.tsx +328 -0
  164. package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
  165. package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
  166. package/src/components/panels/TaskOverviewPanel.tsx +270 -0
  167. package/src/components/panels/TokensPanel.stories.tsx +131 -0
  168. package/src/components/panels/TokensPanel.tsx +143 -0
  169. package/src/components/panels/WorkpadPanel.module.scss +39 -0
  170. package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
  171. package/src/components/panels/WorkpadPanel.tsx +63 -0
  172. package/src/components/panels/index.ts +13 -0
  173. package/src/components/personas/McpToolSelector.module.scss +109 -0
  174. package/src/components/personas/McpToolSelector.stories.tsx +129 -0
  175. package/src/components/personas/McpToolSelector.tsx +180 -0
  176. package/src/components/personas/PersonaManager.module.scss +233 -0
  177. package/src/components/personas/PersonaManager.stories.tsx +139 -0
  178. package/src/components/personas/PersonaManager.tsx +122 -0
  179. package/src/components/schedules/ScheduleManager.module.scss +98 -0
  180. package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
  181. package/src/components/schedules/ScheduleManager.tsx +160 -0
  182. package/src/components/settings/SettingsNav.module.scss +82 -0
  183. package/src/components/settings/SettingsNav.stories.tsx +83 -0
  184. package/src/components/settings/SettingsNav.tsx +104 -0
  185. package/src/components/streams/StreamDetailPanel.module.scss +206 -0
  186. package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
  187. package/src/components/streams/StreamDetailPanel.tsx +119 -0
  188. package/src/components/streams/StreamList.module.scss +92 -0
  189. package/src/components/streams/StreamList.stories.tsx +99 -0
  190. package/src/components/streams/StreamList.tsx +114 -0
  191. package/src/components/streams/index.ts +10 -0
  192. package/src/components/tools/AgentToolCard.module.scss +118 -0
  193. package/src/components/tools/AgentToolCard.stories.tsx +304 -0
  194. package/src/components/tools/AgentToolCard.tsx +247 -0
  195. package/src/components/tools/FileEditCard.stories.tsx +138 -0
  196. package/src/components/tools/FileEditCard.tsx +160 -0
  197. package/src/components/tools/FileReadCard.stories.tsx +120 -0
  198. package/src/components/tools/FileReadCard.tsx +106 -0
  199. package/src/components/tools/FindingCard.stories.tsx +124 -0
  200. package/src/components/tools/FindingCard.tsx +178 -0
  201. package/src/components/tools/GenericToolCard.stories.tsx +80 -0
  202. package/src/components/tools/GenericToolCard.tsx +111 -0
  203. package/src/components/tools/IpcCard.stories.tsx +129 -0
  204. package/src/components/tools/IpcCard.tsx +178 -0
  205. package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
  206. package/src/components/tools/KnowledgeCard.tsx +165 -0
  207. package/src/components/tools/MetadataCard.stories.tsx +32 -0
  208. package/src/components/tools/MetadataCard.tsx +39 -0
  209. package/src/components/tools/SearchCard.stories.tsx +74 -0
  210. package/src/components/tools/SearchCard.tsx +86 -0
  211. package/src/components/tools/ShellCard.stories.tsx +112 -0
  212. package/src/components/tools/ShellCard.tsx +106 -0
  213. package/src/components/tools/TaskCard.stories.tsx +123 -0
  214. package/src/components/tools/TaskCard.tsx +203 -0
  215. package/src/components/tools/TodoCard.module.scss +131 -0
  216. package/src/components/tools/TodoCard.stories.tsx +202 -0
  217. package/src/components/tools/TodoCard.tsx +200 -0
  218. package/src/components/tools/ToolCard.stories.tsx +177 -0
  219. package/src/components/tools/ToolCard.tsx +60 -0
  220. package/src/components/tools/ToolCardProps.ts +20 -0
  221. package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
  222. package/src/components/tools/ToolSearchCard.tsx +86 -0
  223. package/src/components/tools/WorkpadCard.stories.tsx +106 -0
  224. package/src/components/tools/WorkpadCard.tsx +125 -0
  225. package/src/components/tools/classifyTool.test.ts +44 -0
  226. package/src/components/tools/classifyTool.ts +134 -0
  227. package/src/components/tools/parseDiff.ts +95 -0
  228. package/src/components/tools/parseShellOutput.ts +28 -0
  229. package/src/components/tools/toolCardHelpers.test.ts +53 -0
  230. package/src/components/tools/toolCards.module.scss +234 -0
  231. package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
  232. package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
  233. package/src/components/workspace/WorkspaceBoard.tsx +232 -0
  234. package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
  235. package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
  236. package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
  237. package/src/context/GrackleContext.ts +28 -0
  238. package/src/context/GrackleContextTypes.ts +64 -0
  239. package/src/context/SidebarContext.tsx +53 -0
  240. package/src/context/ThemeContext.tsx +21 -0
  241. package/src/context/ToastContext.tsx +56 -0
  242. package/src/hooks/types.ts +864 -0
  243. package/src/hooks/useEventSelection.test.ts +204 -0
  244. package/src/hooks/useEventSelection.ts +158 -0
  245. package/src/hooks/useSmartScroll.ts +151 -0
  246. package/src/hooks/useTheme.ts +228 -0
  247. package/src/index.ts +210 -0
  248. package/src/mocks/MockGrackleProvider.tsx +1397 -0
  249. package/src/mocks/mockData.ts +1966 -0
  250. package/src/mocks/mockKnowledgeData.ts +294 -0
  251. package/src/scss.d.ts +12 -0
  252. package/src/styles/global.scss +244 -0
  253. package/src/styles/mixins.scss +278 -0
  254. package/src/styles/prism-theme.scss +148 -0
  255. package/src/styles/theme.scss +1102 -0
  256. package/src/test-utils/storybook-decorators.tsx +50 -0
  257. package/src/test-utils/storybook-helpers.ts +262 -0
  258. package/src/themes.ts +142 -0
  259. package/src/utils/boardColumns.ts +141 -0
  260. package/src/utils/breadcrumbs.test.ts +285 -0
  261. package/src/utils/breadcrumbs.ts +222 -0
  262. package/src/utils/dashboard.test.ts +156 -0
  263. package/src/utils/dashboard.ts +195 -0
  264. package/src/utils/eventContent.test.ts +353 -0
  265. package/src/utils/eventContent.ts +209 -0
  266. package/src/utils/findingCategory.ts +33 -0
  267. package/src/utils/format.ts +27 -0
  268. package/src/utils/iconSize.ts +18 -0
  269. package/src/utils/navigation.ts +205 -0
  270. package/src/utils/route-config.test.ts +128 -0
  271. package/src/utils/scrollUtils.test.ts +65 -0
  272. package/src/utils/scrollUtils.ts +49 -0
  273. package/src/utils/sessionEvents.test.ts +302 -0
  274. package/src/utils/sessionEvents.ts +233 -0
  275. package/src/utils/taskStatus.tsx +137 -0
  276. package/src/utils/time.ts +92 -0
  277. package/tsconfig.json +8 -0
  278. package/vite.config.ts +20 -0
  279. package/vitest.config.ts +10 -0
@@ -0,0 +1,202 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // Task Edit Panel — full-panel create/edit form for tasks
5
+ // =============================================================================
6
+
7
+ .container {
8
+ flex: 1;
9
+ display: flex;
10
+ flex-direction: column;
11
+ overflow: hidden;
12
+ }
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Header
16
+ // ---------------------------------------------------------------------------
17
+
18
+ .header {
19
+ @include surface-panel;
20
+ padding: var(--space-sm) var(--space-md);
21
+ border-bottom: 1px solid var(--border-subtle);
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ gap: var(--space-md);
26
+ flex-shrink: 0;
27
+ }
28
+
29
+ .headerTitle {
30
+ font-size: var(--font-size-sm);
31
+ color: var(--text-secondary);
32
+ font-family: var(--font-mono);
33
+ display: flex;
34
+ align-items: center;
35
+ gap: var(--space-sm);
36
+ }
37
+
38
+ .badge {
39
+ background: var(--accent-green-dim);
40
+ color: var(--accent-green);
41
+ border: 1px solid var(--accent-green);
42
+ border-radius: var(--radius-full);
43
+ padding: 1px var(--space-sm);
44
+ font-size: var(--font-size-xs);
45
+ font-family: var(--font-mono);
46
+ }
47
+
48
+ .headerActions {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--space-sm);
52
+ flex-shrink: 0;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Form body
57
+ // ---------------------------------------------------------------------------
58
+
59
+ .body {
60
+ flex: 1;
61
+ overflow: auto;
62
+ padding: var(--space-lg);
63
+ width: 100%;
64
+
65
+ @include mobile {
66
+ padding: var(--space-md);
67
+ }
68
+ }
69
+
70
+ .formContent {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: var(--space-lg);
74
+ max-width: 680px;
75
+
76
+ @include mobile {
77
+ max-width: 100%;
78
+ }
79
+ }
80
+
81
+ .section {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: var(--space-sm);
85
+ }
86
+
87
+ .label {
88
+ font-size: 11px;
89
+ color: var(--text-tertiary);
90
+ text-transform: uppercase;
91
+ letter-spacing: 0.05em;
92
+ }
93
+
94
+ .titleInput {
95
+ @include input-field;
96
+ font-size: var(--font-size-lg);
97
+ font-weight: var(--font-weight-bold);
98
+ color: var(--text-primary);
99
+ padding: var(--space-sm) var(--space-md);
100
+ width: 100%;
101
+ }
102
+
103
+ .descriptionTextarea {
104
+ @include input-field;
105
+ font-size: var(--font-size-sm);
106
+ color: var(--text-secondary);
107
+ resize: vertical;
108
+ min-height: 160px;
109
+ padding: var(--space-sm) var(--space-md);
110
+ width: 100%;
111
+ font-family: var(--font-mono);
112
+ line-height: 1.5;
113
+ }
114
+
115
+ .parentContext {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: var(--space-sm);
119
+ font-size: var(--font-size-sm);
120
+ color: var(--text-tertiary);
121
+ }
122
+
123
+ .parentLabel {
124
+ color: var(--text-tertiary);
125
+ font-size: var(--font-size-xs);
126
+ text-transform: uppercase;
127
+ letter-spacing: 0.05em;
128
+ }
129
+
130
+ .parentName {
131
+ color: var(--text-secondary);
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Persona select
136
+ // ---------------------------------------------------------------------------
137
+
138
+ .personaSelect {
139
+ @include input-field;
140
+ font-size: var(--font-size-sm);
141
+ color: var(--text-secondary);
142
+ padding: var(--space-xs) var(--space-sm);
143
+ width: 100%;
144
+ max-width: 320px;
145
+ cursor: pointer;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Dependencies multi-select
150
+ // ---------------------------------------------------------------------------
151
+
152
+ .depList {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: var(--space-xs);
156
+ }
157
+
158
+ .depItem {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: var(--space-sm);
162
+ font-size: var(--font-size-sm);
163
+ cursor: pointer;
164
+ padding: var(--space-xs) var(--space-sm);
165
+ border-radius: var(--radius-sm);
166
+ transition: background var(--transition-fast);
167
+ color: var(--text-secondary);
168
+ user-select: none;
169
+
170
+ &:hover {
171
+ background: var(--bg-overlay);
172
+ }
173
+
174
+ input[type="checkbox"] {
175
+ accent-color: var(--accent-green);
176
+ width: 14px;
177
+ height: 14px;
178
+ flex-shrink: 0;
179
+ }
180
+ }
181
+
182
+ .depItemSelected {
183
+ color: var(--accent-green);
184
+ }
185
+
186
+ .noDeps {
187
+ font-size: var(--font-size-sm);
188
+ color: var(--text-tertiary);
189
+ font-style: italic;
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Buttons
194
+ // ---------------------------------------------------------------------------
195
+
196
+ .btnPrimary {
197
+ @include btn-primary;
198
+ }
199
+
200
+ .btnGhost {
201
+ @include btn-ghost(var(--text-secondary));
202
+ }
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn } from "@storybook/test";
3
+ import type { Workspace } from "../../hooks/types.js";
4
+ import { buildWorkspace, buildPersona } from "../../test-utils/storybook-helpers.js";
5
+ import { TaskEditPanel } from "./TaskEditPanel.js";
6
+
7
+ const defaultWorkspace: Workspace = buildWorkspace({ id: "ws-001", name: "Test Workspace" });
8
+
9
+ const meta: Meta<typeof TaskEditPanel> = {
10
+ title: "App/Panels/TaskEditPanel",
11
+ component: TaskEditPanel,
12
+ args: {
13
+ mode: "new",
14
+ workspaceId: "ws-001",
15
+ tasks: [],
16
+ workspaces: [defaultWorkspace],
17
+ personas: [buildPersona({ id: "p-1", name: "Coder" })],
18
+ onCreateTask: fn(),
19
+ onUpdateTask: fn(),
20
+ onShowToast: fn(),
21
+ },
22
+ };
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof TaskEditPanel>;
27
+
28
+ /** New task form shows title and description fields plus a disabled Create button. */
29
+ export const FormFieldsVisible: Story = {
30
+ play: async ({ canvas }) => {
31
+ // Title input
32
+ const titleInput = canvas.getByTestId("task-edit-title");
33
+ await expect(titleInput).toBeInTheDocument();
34
+
35
+ // Description textarea
36
+ const descriptionInput = canvas.getByTestId("task-edit-description");
37
+ await expect(descriptionInput).toBeInTheDocument();
38
+
39
+ // Save/Create button present and disabled (no title entered)
40
+ const saveButton = canvas.getByTestId("task-edit-save");
41
+ await expect(saveButton).toBeInTheDocument();
42
+ await expect(saveButton).toBeDisabled();
43
+ },
44
+ };
45
+
46
+ /** The Cancel button is visible in the new task form. */
47
+ export const CancelButtonVisible: Story = {
48
+ play: async ({ canvas }) => {
49
+ await expect(canvas.getByTestId("task-edit-title")).toBeInTheDocument();
50
+
51
+ const cancelButton = canvas.getByRole("button", { name: "Cancel" });
52
+ await expect(cancelButton).toBeInTheDocument();
53
+ },
54
+ };
55
+
56
+ /**
57
+ * The task creation form has no environment dropdown -- environment is
58
+ * assigned at start time, not creation time.
59
+ */
60
+ export const NoEnvironmentDropdown: Story = {
61
+ play: async ({ canvas }) => {
62
+ await expect(canvas.getByTestId("task-edit-title")).toBeInTheDocument();
63
+
64
+ // There should be no element mentioning "Default env" or "test-local"
65
+ // as environment options in the form.
66
+ const allSelects = canvas.getAllByRole("combobox");
67
+ for (const select of allSelects) {
68
+ // None of the selects should contain environment-like options
69
+ const options = select.querySelectorAll("option");
70
+ for (const option of options) {
71
+ await expect(option.textContent ?? "").not.toMatch(/Default env|test-local/);
72
+ }
73
+ }
74
+ },
75
+ };
@@ -0,0 +1,328 @@
1
+ import { useState, useEffect, useRef, type JSX } from "react";
2
+ import type { ToastVariant } from "../../context/ToastContext.js";
3
+ import type { TaskData, Workspace, PersonaData } from "../../hooks/types.js";
4
+ import { taskUrl, workspaceUrl, useAppNavigate } from "../../utils/navigation.js";
5
+ import styles from "./TaskEditPanel.module.scss";
6
+
7
+ /** Props for the TaskEditPanel component. */
8
+ interface Props {
9
+ mode: "new" | "edit";
10
+ /** Task ID — required in edit mode. */
11
+ taskId?: string;
12
+ /** Workspace ID — required in new mode. */
13
+ workspaceId?: string;
14
+ /** Parent task ID — optional in new mode. */
15
+ parentTaskId?: string;
16
+ /** Environment ID — used for scoped navigation. */
17
+ environmentId?: string;
18
+ /** All tasks (used for lookups and sibling dependency list). */
19
+ tasks: TaskData[];
20
+ /** All workspaces (used for workspace selector and environment resolution). */
21
+ workspaces: Workspace[];
22
+ /** All personas (used for persona dropdown). */
23
+ personas: PersonaData[];
24
+ /** Callback to create a new task. */
25
+ onCreateTask: (
26
+ workspaceId: string, title: string, description?: string,
27
+ dependsOn?: string[], parentTaskId?: string, defaultPersonaId?: string,
28
+ canDecompose?: boolean, onSuccess?: () => void, onError?: (message: string) => void,
29
+ ) => void;
30
+ /** Callback to update an existing task. */
31
+ onUpdateTask: (
32
+ taskId: string, title: string, description: string,
33
+ dependsOn: string[], defaultPersonaId?: string,
34
+ ) => void;
35
+ /** Optional callback invoked when inline edit completes (save or cancel). When provided, navigation is skipped. */
36
+ onEditDone?: () => void;
37
+ /** Display a toast notification. */
38
+ onShowToast?: (message: string, variant?: ToastVariant) => void;
39
+ }
40
+
41
+ /**
42
+ * Full-panel create/edit form for tasks.
43
+ *
44
+ * - new: blank form; calls createTask on save, then navigates back to
45
+ * the workspace view.
46
+ * - edit: pre-populated form; calls updateTask on save, then navigates
47
+ * back to the task overview.
48
+ */
49
+ export function TaskEditPanel({ mode, taskId, workspaceId: workspaceIdProp, parentTaskId: parentTaskIdProp, environmentId: environmentIdProp, tasks, workspaces, personas, onCreateTask, onUpdateTask, onEditDone, onShowToast }: Props): JSX.Element {
50
+ const navigate = useAppNavigate();
51
+
52
+ const isEdit = mode === "edit";
53
+ const existingTask = isEdit && taskId ? tasks.find((t) => t.id === taskId) : undefined;
54
+
55
+ const initialWorkspaceId = isEdit
56
+ ? (existingTask?.workspaceId ?? "")
57
+ : (workspaceIdProp ?? "");
58
+
59
+ const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(initialWorkspaceId);
60
+ const workspaceId = initialWorkspaceId || selectedWorkspaceId;
61
+
62
+ /** Resolve environmentId from prop, or from the workspace's first linked environment. */
63
+ const resolvedWorkspace = workspaces.find((w) => w.id === workspaceId);
64
+ const environmentId = environmentIdProp ?? resolvedWorkspace?.linkedEnvironmentIds[0];
65
+
66
+ /** Whether the workspace dropdown should be shown (new mode without pre-set workspace). */
67
+ const showWorkspaceSelector = !isEdit && !workspaceIdProp;
68
+
69
+ const parentTaskId = isEdit
70
+ ? (existingTask?.parentTaskId ?? "")
71
+ : (parentTaskIdProp ?? "");
72
+
73
+ const parentTask = parentTaskId ? tasks.find((t) => t.id === parentTaskId) : undefined;
74
+
75
+ const [title, setTitle] = useState(existingTask?.title ?? "");
76
+ const [description, setDescription] = useState(existingTask?.description ?? "");
77
+ const [selectedDeps, setSelectedDeps] = useState<string[]>(existingTask?.dependsOn ?? []);
78
+ const [defaultPersonaId, setDefaultPersonaId] = useState(existingTask?.defaultPersonaId ?? "");
79
+ const [canDecompose, setCanDecompose] = useState(existingTask?.canDecompose ?? false);
80
+ const [creating, setCreating] = useState(false);
81
+
82
+ // In edit mode, tasks may not have loaded yet at mount time. Sync form state
83
+ // the first time existingTask becomes available so the form is pre-populated,
84
+ // but do not re-apply on subsequent refreshes — that would discard in-progress
85
+ // user edits whenever a background update replaces the task object.
86
+ const hasHydratedRef = useRef(false);
87
+ useEffect(() => {
88
+ if (isEdit && existingTask && !hasHydratedRef.current) {
89
+ hasHydratedRef.current = true;
90
+ setTitle(existingTask.title);
91
+ setDescription(existingTask.description);
92
+ setSelectedDeps(existingTask.dependsOn);
93
+ setDefaultPersonaId(existingTask.defaultPersonaId);
94
+ setCanDecompose(existingTask.canDecompose);
95
+ }
96
+ }, [isEdit, existingTask]);
97
+
98
+ // All tasks in the same workspace, excluding the task being edited (self)
99
+ const siblingTasks = tasks.filter(
100
+ (t) =>
101
+ t.workspaceId === workspaceId &&
102
+ (!isEdit || t.id !== taskId) &&
103
+ t.id !== parentTaskId,
104
+ );
105
+
106
+ // In edit mode, also require that task data has loaded before allowing save
107
+ // to prevent overwriting server data with blank form values.
108
+ // Workspace is required: the user must pick one from the dropdown.
109
+ const canSave = title.trim().length > 0
110
+ && (!isEdit || existingTask !== undefined)
111
+ && workspaceId.length > 0;
112
+
113
+ const toggleDep = (depId: string): void => {
114
+ setSelectedDeps((prev) =>
115
+ prev.includes(depId) ? prev.filter((d) => d !== depId) : [...prev, depId],
116
+ );
117
+ };
118
+
119
+ const handleSave = (): void => {
120
+ if (!canSave || creating) {
121
+ return;
122
+ }
123
+ if (isEdit && existingTask === undefined) {
124
+ // Guard: task data not yet loaded — do not overwrite with blank values.
125
+ return;
126
+ }
127
+ if (isEdit && taskId) {
128
+ onUpdateTask(taskId, title.trim(), description, selectedDeps, defaultPersonaId);
129
+ onShowToast?.("Task updated", "success");
130
+ if (onEditDone) {
131
+ onEditDone();
132
+ } else {
133
+ navigate(taskUrl(taskId, undefined, workspaceId, environmentId), { replace: true });
134
+ }
135
+ } else {
136
+ setCreating(true);
137
+ onCreateTask(
138
+ workspaceId,
139
+ title.trim(),
140
+ description,
141
+ selectedDeps.length > 0 ? selectedDeps : undefined,
142
+ parentTaskId || undefined,
143
+ defaultPersonaId,
144
+ canDecompose,
145
+ () => {
146
+ onShowToast?.("Task created", "success");
147
+ navigate(workspaceIdProp ? workspaceUrl(workspaceIdProp, environmentId) : "/tasks", { replace: true });
148
+ },
149
+ (message: string) => {
150
+ onShowToast?.(message, "error");
151
+ setCreating(false);
152
+ },
153
+ );
154
+ }
155
+ };
156
+
157
+ const handleCancel = (): void => {
158
+ if (onEditDone) {
159
+ onEditDone();
160
+ return;
161
+ }
162
+ if (isEdit && taskId) {
163
+ navigate(taskUrl(taskId, undefined, workspaceId, environmentId));
164
+ } else {
165
+ navigate(workspaceIdProp ? workspaceUrl(workspaceIdProp, environmentId) : "/tasks");
166
+ }
167
+ };
168
+
169
+ const modeLabel = isEdit ? "edit task" : (parentTaskId ? "child task" : "new task");
170
+
171
+ return (
172
+ <div className={styles.container}>
173
+ {/* Header */}
174
+ <div className={styles.header}>
175
+ <div className={styles.headerTitle}>
176
+ <span className={styles.badge}>{modeLabel}</span>
177
+ {parentTask && (
178
+ <span className={styles.parentContext}>
179
+ <span className={styles.parentLabel}>Child of</span>
180
+ <span className={styles.parentName}>{parentTask.title}</span>
181
+ </span>
182
+ )}
183
+ </div>
184
+ <div className={styles.headerActions}>
185
+ <button
186
+ onClick={handleSave}
187
+ disabled={!canSave || creating}
188
+ className={styles.btnPrimary}
189
+ data-testid="task-edit-save"
190
+ >
191
+ {creating ? "Creating\u2026" : isEdit ? "Save Changes" : "Create"}
192
+ </button>
193
+ <button onClick={handleCancel} className={styles.btnGhost}>
194
+ Cancel
195
+ </button>
196
+ </div>
197
+ </div>
198
+
199
+ {/* Form body */}
200
+ <div className={styles.body}>
201
+ <div className={styles.formContent}>
202
+ {/* Workspace selector (shown when creating from global Tasks view) */}
203
+ {showWorkspaceSelector && (
204
+ <div className={styles.section}>
205
+ <label className={styles.label} htmlFor="task-edit-workspace">
206
+ Workspace
207
+ </label>
208
+ <select
209
+ id="task-edit-workspace"
210
+ value={selectedWorkspaceId}
211
+ onChange={(e) => setSelectedWorkspaceId(e.target.value)}
212
+ className={styles.personaSelect}
213
+ data-testid="task-edit-workspace"
214
+ >
215
+ <option value="">Select a workspace...</option>
216
+ {workspaces.map((w) => (
217
+ <option key={w.id} value={w.id}>{w.name}</option>
218
+ ))}
219
+ </select>
220
+ </div>
221
+ )}
222
+
223
+ {/* Title */}
224
+ <div className={styles.section}>
225
+ <label className={styles.label} htmlFor="task-edit-title">
226
+ Title
227
+ </label>
228
+ <input
229
+ id="task-edit-title"
230
+ type="text"
231
+ value={title}
232
+ onChange={(e) => setTitle(e.target.value)}
233
+ placeholder="Task title..."
234
+ autoFocus
235
+ className={styles.titleInput}
236
+ data-testid="task-edit-title"
237
+ onKeyDown={(e) => {
238
+ if (e.key === "Enter" && canSave) {
239
+ handleSave();
240
+ }
241
+ }}
242
+ />
243
+ </div>
244
+
245
+ {/* Description */}
246
+ <div className={styles.section}>
247
+ <label className={styles.label} htmlFor="task-edit-description">
248
+ Description
249
+ </label>
250
+ <textarea
251
+ id="task-edit-description"
252
+ value={description}
253
+ onChange={(e) => setDescription(e.target.value)}
254
+ placeholder="Describe the task... (markdown supported)"
255
+ className={styles.descriptionTextarea}
256
+ data-testid="task-edit-description"
257
+ rows={8}
258
+ />
259
+ </div>
260
+
261
+ {/* Default Persona */}
262
+ <div className={styles.section}>
263
+ <label className={styles.label} htmlFor="task-edit-persona">
264
+ Default Persona
265
+ </label>
266
+ <select
267
+ id="task-edit-persona"
268
+ value={defaultPersonaId}
269
+ onChange={(e) => setDefaultPersonaId(e.target.value)}
270
+ className={styles.personaSelect}
271
+ data-testid="task-edit-persona"
272
+ >
273
+ <option value="">(Inherit)</option>
274
+ {personas.map((p) => (
275
+ <option key={p.id} value={p.id}>{p.name}</option>
276
+ ))}
277
+ </select>
278
+ </div>
279
+
280
+ {/* Can Decompose */}
281
+ {!isEdit && (
282
+ <div className={styles.section}>
283
+ <label className={styles.depItem} data-testid="task-edit-can-decompose">
284
+ <input
285
+ type="checkbox"
286
+ checked={canDecompose}
287
+ onChange={(e) => setCanDecompose(e.target.checked)}
288
+ />
289
+ Can spawn subtasks
290
+ </label>
291
+ </div>
292
+ )}
293
+
294
+ {/* Dependencies */}
295
+ <div className={styles.section}>
296
+ <div className={styles.label}>Dependencies</div>
297
+ {siblingTasks.length === 0 ? (
298
+ <div className={styles.noDeps}>No other tasks in this workspace</div>
299
+ ) : (
300
+ <div className={styles.depList}>
301
+ {siblingTasks.map((t) => {
302
+ const isChecked = selectedDeps.includes(t.id);
303
+ return (
304
+ <label
305
+ key={t.id}
306
+ className={`${styles.depItem} ${isChecked ? styles.depItemSelected : ""}`}
307
+ data-testid={`dep-option-${t.id}`}
308
+ >
309
+ <input
310
+ type="checkbox"
311
+ checked={isChecked}
312
+ onChange={() => toggleDep(t.id)}
313
+ />
314
+ {t.title}
315
+ <span style={{ opacity: 0.5, fontSize: "11px", marginLeft: "4px" }}>
316
+ ({t.status})
317
+ </span>
318
+ </label>
319
+ );
320
+ })}
321
+ </div>
322
+ )}
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ );
328
+ }