@pennyfarthing/cyclist 10.4.0 → 11.0.0-alpha.0

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 (270) hide show
  1. package/dist/api/agent-load.d.ts +1 -2
  2. package/dist/api/agent-load.d.ts.map +1 -1
  3. package/dist/api/agent-load.js +2 -123
  4. package/dist/api/agent-load.js.map +1 -1
  5. package/dist/api/audit-log.d.ts +1 -17
  6. package/dist/api/audit-log.d.ts.map +1 -1
  7. package/dist/api/audit-log.js +2 -162
  8. package/dist/api/audit-log.js.map +1 -1
  9. package/dist/api/background-tasks.d.ts +1 -26
  10. package/dist/api/background-tasks.d.ts.map +1 -1
  11. package/dist/api/background-tasks.js +2 -55
  12. package/dist/api/background-tasks.js.map +1 -1
  13. package/dist/api/bell.d.ts +1 -18
  14. package/dist/api/bell.d.ts.map +1 -1
  15. package/dist/api/bell.js +2 -33
  16. package/dist/api/bell.js.map +1 -1
  17. package/dist/api/code-markers.d.ts +1 -8
  18. package/dist/api/code-markers.d.ts.map +1 -1
  19. package/dist/api/code-markers.js +2 -61
  20. package/dist/api/code-markers.js.map +1 -1
  21. package/dist/api/complexity.d.ts +1 -2
  22. package/dist/api/complexity.d.ts.map +1 -1
  23. package/dist/api/complexity.js +2 -46
  24. package/dist/api/complexity.js.map +1 -1
  25. package/dist/api/context.d.ts +1 -37
  26. package/dist/api/context.d.ts.map +1 -1
  27. package/dist/api/context.js +2 -143
  28. package/dist/api/context.js.map +1 -1
  29. package/dist/api/dead-code.d.ts +1 -2
  30. package/dist/api/dead-code.d.ts.map +1 -1
  31. package/dist/api/dead-code.js +2 -69
  32. package/dist/api/dead-code.js.map +1 -1
  33. package/dist/api/dependencies.d.ts +1 -2
  34. package/dist/api/dependencies.d.ts.map +1 -1
  35. package/dist/api/dependencies.js +2 -42
  36. package/dist/api/dependencies.js.map +1 -1
  37. package/dist/api/evaluation.d.ts +1 -19
  38. package/dist/api/evaluation.d.ts.map +1 -1
  39. package/dist/api/evaluation.js +2 -127
  40. package/dist/api/evaluation.js.map +1 -1
  41. package/dist/api/file-browser.d.ts +1 -8
  42. package/dist/api/file-browser.d.ts.map +1 -1
  43. package/dist/api/file-browser.js +2 -114
  44. package/dist/api/file-browser.js.map +1 -1
  45. package/dist/api/git.d.ts +1 -46
  46. package/dist/api/git.d.ts.map +1 -1
  47. package/dist/api/git.js +2 -354
  48. package/dist/api/git.js.map +1 -1
  49. package/dist/api/health-score.d.ts +1 -2
  50. package/dist/api/health-score.d.ts.map +1 -1
  51. package/dist/api/health-score.js +2 -46
  52. package/dist/api/health-score.js.map +1 -1
  53. package/dist/api/hook-request.d.ts +1 -40
  54. package/dist/api/hook-request.d.ts.map +1 -1
  55. package/dist/api/hook-request.js +2 -277
  56. package/dist/api/hook-request.js.map +1 -1
  57. package/dist/api/hotspots.d.ts +1 -2
  58. package/dist/api/hotspots.d.ts.map +1 -1
  59. package/dist/api/hotspots.js +2 -61
  60. package/dist/api/hotspots.js.map +1 -1
  61. package/dist/api/identity.d.ts +1 -16
  62. package/dist/api/identity.d.ts.map +1 -1
  63. package/dist/api/identity.js +2 -78
  64. package/dist/api/identity.js.map +1 -1
  65. package/dist/api/index.d.ts +1 -34
  66. package/dist/api/index.d.ts.map +1 -1
  67. package/dist/api/index.js +2 -44
  68. package/dist/api/index.js.map +1 -1
  69. package/dist/api/mode.d.ts +1 -22
  70. package/dist/api/mode.d.ts.map +1 -1
  71. package/dist/api/mode.js +2 -37
  72. package/dist/api/mode.js.map +1 -1
  73. package/dist/api/otlp.d.ts +1 -2
  74. package/dist/api/otlp.d.ts.map +1 -1
  75. package/dist/api/otlp.js +2 -46
  76. package/dist/api/otlp.js.map +1 -1
  77. package/dist/api/permissions.d.ts +1 -15
  78. package/dist/api/permissions.d.ts.map +1 -1
  79. package/dist/api/permissions.js +2 -66
  80. package/dist/api/permissions.js.map +1 -1
  81. package/dist/api/persona.d.ts +1 -8
  82. package/dist/api/persona.d.ts.map +1 -1
  83. package/dist/api/persona.js +2 -67
  84. package/dist/api/persona.js.map +1 -1
  85. package/dist/api/portrait.d.ts +1 -5
  86. package/dist/api/portrait.d.ts.map +1 -1
  87. package/dist/api/portrait.js +2 -27
  88. package/dist/api/portrait.js.map +1 -1
  89. package/dist/api/settings.d.ts +1 -53
  90. package/dist/api/settings.d.ts.map +1 -1
  91. package/dist/api/settings.js +2 -464
  92. package/dist/api/settings.js.map +1 -1
  93. package/dist/api/spans.d.ts +1 -16
  94. package/dist/api/spans.d.ts.map +1 -1
  95. package/dist/api/spans.js +2 -244
  96. package/dist/api/spans.js.map +1 -1
  97. package/dist/api/stats.d.ts +1 -12
  98. package/dist/api/stats.d.ts.map +1 -1
  99. package/dist/api/stats.js +2 -84
  100. package/dist/api/stats.js.map +1 -1
  101. package/dist/api/story.d.ts +1 -2
  102. package/dist/api/story.d.ts.map +1 -1
  103. package/dist/api/story.js +2 -14
  104. package/dist/api/story.js.map +1 -1
  105. package/dist/api/telemetry.d.ts +1 -18
  106. package/dist/api/telemetry.d.ts.map +1 -1
  107. package/dist/api/telemetry.js +2 -164
  108. package/dist/api/telemetry.js.map +1 -1
  109. package/dist/api/theme-agents.d.ts +1 -60
  110. package/dist/api/theme-agents.d.ts.map +1 -1
  111. package/dist/api/theme-agents.js +2 -213
  112. package/dist/api/theme-agents.js.map +1 -1
  113. package/dist/api/todos.d.ts +1 -32
  114. package/dist/api/todos.d.ts.map +1 -1
  115. package/dist/api/todos.js +2 -43
  116. package/dist/api/todos.js.map +1 -1
  117. package/dist/api/token-stats.d.ts +1 -7
  118. package/dist/api/token-stats.d.ts.map +1 -1
  119. package/dist/api/token-stats.js +2 -35
  120. package/dist/api/token-stats.js.map +1 -1
  121. package/dist/api/welcome.d.ts +1 -21
  122. package/dist/api/welcome.d.ts.map +1 -1
  123. package/dist/api/welcome.js +2 -34
  124. package/dist/api/welcome.js.map +1 -1
  125. package/dist/env.d.ts +6 -0
  126. package/dist/env.d.ts.map +1 -0
  127. package/dist/env.js +10 -0
  128. package/dist/env.js.map +1 -0
  129. package/dist/focus.d.ts +53 -0
  130. package/dist/focus.d.ts.map +1 -0
  131. package/dist/focus.js +122 -0
  132. package/dist/focus.js.map +1 -0
  133. package/dist/menu-builder.d.ts.map +1 -1
  134. package/dist/menu-builder.js +0 -1
  135. package/dist/menu-builder.js.map +1 -1
  136. package/dist/public/css/react.css +1 -1
  137. package/dist/public/js/react/react.js +51 -59
  138. package/dist/server.d.ts +16 -85
  139. package/dist/server.d.ts.map +1 -1
  140. package/dist/server.js +38 -409
  141. package/dist/server.js.map +1 -1
  142. package/dist/sprint-data.d.ts +1 -1
  143. package/dist/sprint-data.d.ts.map +1 -1
  144. package/dist/sprint-data.js +2 -2
  145. package/dist/sprint-data.js.map +1 -1
  146. package/dist/websocket.d.ts +2 -0
  147. package/dist/websocket.d.ts.map +1 -1
  148. package/dist/websocket.js +42 -75
  149. package/dist/websocket.js.map +1 -1
  150. package/package.json +3 -6
  151. package/portraits/hogans-heroes/large/burkhalter-35312.png +0 -0
  152. package/portraits/hogans-heroes/large/carter-34352.png +0 -0
  153. package/portraits/hogans-heroes/large/hochstetter-45314.png +0 -0
  154. package/portraits/hogans-heroes/large/hogan-44541.png +0 -0
  155. package/portraits/hogans-heroes/large/kinch-35241.png +0 -0
  156. package/portraits/hogans-heroes/large/klink-23434.png +0 -0
  157. package/portraits/hogans-heroes/large/lebeau-45443.png +0 -0
  158. package/portraits/hogans-heroes/large/marya-53543.png +0 -0
  159. package/portraits/hogans-heroes/large/newkirk-54432.png +0 -0
  160. package/portraits/hogans-heroes/large/schultz-42453.png +0 -0
  161. package/portraits/hogans-heroes/large/underground-55131.png +0 -0
  162. package/portraits/hogans-heroes/medium/burkhalter-35312.png +0 -0
  163. package/portraits/hogans-heroes/medium/carter-34352.png +0 -0
  164. package/portraits/hogans-heroes/medium/hochstetter-45314.png +0 -0
  165. package/portraits/hogans-heroes/medium/hogan-44541.png +0 -0
  166. package/portraits/hogans-heroes/medium/kinch-35241.png +0 -0
  167. package/portraits/hogans-heroes/medium/klink-23434.png +0 -0
  168. package/portraits/hogans-heroes/medium/lebeau-45443.png +0 -0
  169. package/portraits/hogans-heroes/medium/marya-53543.png +0 -0
  170. package/portraits/hogans-heroes/medium/newkirk-54432.png +0 -0
  171. package/portraits/hogans-heroes/medium/schultz-42453.png +0 -0
  172. package/portraits/hogans-heroes/medium/underground-55131.png +0 -0
  173. package/portraits/monty-python/large/announcer-44441.png +0 -0
  174. package/portraits/monty-python/large/arguer-35412.png +0 -0
  175. package/portraits/monty-python/large/bicycle-repair-man-35241.png +0 -0
  176. package/portraits/monty-python/large/colonel-35423.png +0 -0
  177. package/portraits/monty-python/large/counsellor-45341.png +0 -0
  178. package/portraits/monty-python/large/gumbys-23524.png +0 -0
  179. package/portraits/monty-python/large/nudge-43533.png +0 -0
  180. package/portraits/monty-python/large/praline-45413.png +0 -0
  181. package/portraits/monty-python/large/silly-walks-55322.png +0 -0
  182. package/portraits/monty-python/large/wensleydale-54451.png +0 -0
  183. package/portraits/monty-python/large/xim-nez-43534.png +0 -0
  184. package/portraits/monty-python/medium/announcer-44441.png +0 -0
  185. package/portraits/monty-python/medium/arguer-35412.png +0 -0
  186. package/portraits/monty-python/medium/bicycle-repair-man-35241.png +0 -0
  187. package/portraits/monty-python/medium/colonel-35423.png +0 -0
  188. package/portraits/monty-python/medium/counsellor-45341.png +0 -0
  189. package/portraits/monty-python/medium/gumbys-23524.png +0 -0
  190. package/portraits/monty-python/medium/nudge-43533.png +0 -0
  191. package/portraits/monty-python/medium/praline-45413.png +0 -0
  192. package/portraits/monty-python/medium/silly-walks-55322.png +0 -0
  193. package/portraits/monty-python/medium/wensleydale-54451.png +0 -0
  194. package/portraits/monty-python/medium/xim-nez-43534.png +0 -0
  195. package/portraits/stephen-king/large/andy-55231.png +0 -0
  196. package/portraits/stephen-king/large/christine-25112.png +0 -0
  197. package/portraits/stephen-king/large/danny-53243.png +0 -0
  198. package/portraits/stephen-king/large/flagg-55311.png +0 -0
  199. package/portraits/stephen-king/large/gaunt-54421.png +0 -0
  200. package/portraits/stephen-king/large/jack-44224.png +0 -0
  201. package/portraits/stephen-king/large/johnny-44353.png +0 -0
  202. package/portraits/stephen-king/large/margaret-15415.png +0 -0
  203. package/portraits/stephen-king/large/paul-45233.png +0 -0
  204. package/portraits/stephen-king/large/pennywise-54411.png +0 -0
  205. package/portraits/stephen-king/large/roland-35121.png +0 -0
  206. package/portraits/stephen-king/medium/andy-55231.png +0 -0
  207. package/portraits/stephen-king/medium/christine-25112.png +0 -0
  208. package/portraits/stephen-king/medium/danny-53243.png +0 -0
  209. package/portraits/stephen-king/medium/flagg-55311.png +0 -0
  210. package/portraits/stephen-king/medium/gaunt-54421.png +0 -0
  211. package/portraits/stephen-king/medium/jack-44224.png +0 -0
  212. package/portraits/stephen-king/medium/johnny-44353.png +0 -0
  213. package/portraits/stephen-king/medium/margaret-15415.png +0 -0
  214. package/portraits/stephen-king/medium/paul-45233.png +0 -0
  215. package/portraits/stephen-king/medium/pennywise-54411.png +0 -0
  216. package/portraits/stephen-king/medium/roland-35121.png +0 -0
  217. package/src/public/App.tsx +21 -5
  218. package/src/public/components/BikeRackIndex.tsx +0 -1
  219. package/src/public/components/BikeRackWorkspace.tsx +86 -11
  220. package/src/public/components/DockviewWorkspace.tsx +19 -8
  221. package/src/public/components/StandalonePanel.tsx +1 -3
  222. package/src/public/components/panel-registry.ts +3 -1
  223. package/src/public/components/panels/AuditLogPanel.tsx +28 -4
  224. package/src/public/components/panels/GitPanel.tsx +1 -20
  225. package/src/public/components/panels/SettingsPanel.tsx +0 -1
  226. package/src/public/components/panels/SprintPanel.tsx +32 -1
  227. package/src/public/components/panels/index.ts +0 -2
  228. package/src/public/hooks/useFocusPanel.ts +137 -0
  229. package/src/public/hooks/useLayoutPersistence.ts +8 -5
  230. package/src/public/styles/dockview-theme.css +1 -84
  231. package/src/public/styles/tailwind.css +27 -32
  232. package/src/public/utils/slash-commands.ts +122 -98
  233. package/dist/hooks/cyclist-pretooluse-hook.d.ts +0 -60
  234. package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +0 -1
  235. package/dist/hooks/cyclist-pretooluse-hook.js +0 -57
  236. package/dist/hooks/cyclist-pretooluse-hook.js.map +0 -1
  237. package/dist/hooks/pretooluse-hook.d.ts +0 -89
  238. package/dist/hooks/pretooluse-hook.d.ts.map +0 -1
  239. package/dist/hooks/pretooluse-hook.js +0 -235
  240. package/dist/hooks/pretooluse-hook.js.map +0 -1
  241. package/dist/notification-sound.d.ts +0 -59
  242. package/dist/notification-sound.d.ts.map +0 -1
  243. package/dist/notification-sound.js +0 -219
  244. package/dist/notification-sound.js.map +0 -1
  245. package/dist/plugin-loader.test.d.ts +0 -17
  246. package/dist/plugin-loader.test.d.ts.map +0 -1
  247. package/dist/plugin-loader.test.js +0 -407
  248. package/dist/plugin-loader.test.js.map +0 -1
  249. package/portraits/star-trek-tng/large/beverly-44352.png +0 -0
  250. package/portraits/star-trek-tng/large/data-55241.png +0 -0
  251. package/portraits/star-trek-tng/large/deanna-43353.png +0 -0
  252. package/portraits/star-trek-tng/large/geordi-54342.png +0 -0
  253. package/portraits/star-trek-tng/large/jean-luc-45342.png +0 -0
  254. package/portraits/star-trek-tng/large/kathryn-45332.png +0 -0
  255. package/portraits/star-trek-tng/large/miles-35342.png +0 -0
  256. package/portraits/star-trek-tng/large/q-53521.png +0 -0
  257. package/portraits/star-trek-tng/large/spock-45231.png +0 -0
  258. package/portraits/star-trek-tng/large/troi-44352.png +0 -0
  259. package/portraits/star-trek-tng/medium/beverly-44352.png +0 -0
  260. package/portraits/star-trek-tng/medium/data-55241.png +0 -0
  261. package/portraits/star-trek-tng/medium/deanna-43353.png +0 -0
  262. package/portraits/star-trek-tng/medium/geordi-54342.png +0 -0
  263. package/portraits/star-trek-tng/medium/jean-luc-45342.png +0 -0
  264. package/portraits/star-trek-tng/medium/kathryn-45332.png +0 -0
  265. package/portraits/star-trek-tng/medium/miles-35342.png +0 -0
  266. package/portraits/star-trek-tng/medium/q-53521.png +0 -0
  267. package/portraits/star-trek-tng/medium/spock-45231.png +0 -0
  268. package/portraits/star-trek-tng/medium/troi-44352.png +0 -0
  269. package/src/public/components/panels/TTYPanel.tsx +0 -299
  270. package/src/public/types/electron.d.ts +0 -18
@@ -29,6 +29,13 @@ import ApprovalModal, { useApprovalModal } from './components/ApprovalModal';
29
29
  import { subscribeToPermissionRequests, sendPermissionResponse, createApprovalResponse } from './components/ApprovalModal';
30
30
  import type { ApprovalRequest, GrantScope } from './components/ApprovalModal';
31
31
 
32
+ // Environment discriminator injected by server.ts (ADR-0024)
33
+ declare global {
34
+ interface Window {
35
+ __CYCLIST_MODE__?: 'cyclist' | 'bikerack';
36
+ }
37
+ }
38
+
32
39
  // Import all panel components
33
40
  // Note: ProgressPanel split into Workflow/AC/Todo panels (MSSCI-14188)
34
41
  import {
@@ -44,7 +51,6 @@ import {
44
51
  DebugPanel,
45
52
  SettingsPanel,
46
53
  AuditLogPanel,
47
- TTYPanel,
48
54
  } from './components/panels';
49
55
 
50
56
  // =============================================================================
@@ -60,7 +66,6 @@ registerPanelComponent(PANEL_INVENTORY.CHANGED, ChangedPanel);
60
66
  registerPanelComponent(PANEL_INVENTORY.DIFFS, DiffsPanel);
61
67
  registerPanelComponent(PANEL_INVENTORY.DEBUG, DebugPanel);
62
68
  registerPanelComponent(PANEL_INVENTORY.AUDIT_LOG, AuditLogPanel);
63
- registerPanelComponent(PANEL_INVENTORY.TTY, TTYPanel);
64
69
 
65
70
  // Right sidebar panels
66
71
  // Note: ProgressPanel split into Workflow/AC/Todo panels (MSSCI-14188)
@@ -202,12 +207,13 @@ function RootErrorFallback(): React.ReactElement {
202
207
 
203
208
  export default function App(): React.ReactElement {
204
209
  // Detect route mode (computed before hooks, used after)
205
- const isBikeRackIndex = window.location.pathname === '/bikerack';
210
+ const isBikeRackIndex = window.__CYCLIST_MODE__ === 'bikerack';
206
211
  const standalonePanelName = getStandalonePanelName();
207
212
 
208
213
  // --- All hooks called unconditionally (React rules of hooks) ---
209
214
 
210
- const { layout, isLoading, saveLayout } = useLayoutPersistence();
215
+ const layoutEndpoint = isBikeRackIndex ? '/api/settings/bikerack-layout' : '/api/settings/layout';
216
+ const { layout, isLoading, saveLayout } = useLayoutPersistence(layoutEndpoint);
211
217
 
212
218
  // Set up reduced motion support
213
219
  useReducedMotion();
@@ -268,6 +274,13 @@ export default function App(): React.ReactElement {
268
274
  // BikeRack Dockview workspace (MSSCI-14877) — /bikerack renders Dockview layout
269
275
  // No-op ClaudeContext: BikeRack has no Claude CLI subprocess, skip WebSocket
270
276
  if (isBikeRackIndex) {
277
+ if (isLoading) {
278
+ return (
279
+ <div className="cyclist-loading" style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
280
+ <div className="loading-spinner" aria-label="Loading layout..." />
281
+ </div>
282
+ );
283
+ }
271
284
  const noop = () => () => {};
272
285
  return (
273
286
  <ClaudeContext.Provider value={{
@@ -277,7 +290,10 @@ export default function App(): React.ReactElement {
277
290
  onMessage: noop, onComplete: noop, onError: noop,
278
291
  onUserMessage: noop, onClear: noop,
279
292
  }}>
280
- <BikeRackWorkspace />
293
+ <BikeRackWorkspace
294
+ initialLayout={layout ?? undefined}
295
+ onLayoutChange={saveLayout}
296
+ />
281
297
  </ClaudeContext.Provider>
282
298
  );
283
299
  }
@@ -25,7 +25,6 @@ const PANELS = [
25
25
  { id: 'audit', label: 'Audit', description: 'OTEL spans and logs' },
26
26
  { id: 'changed', label: 'Changed', description: 'Changed files' },
27
27
  { id: 'ac', label: 'AC', description: 'Acceptance criteria detail' },
28
- { id: 'tty', label: 'TTY', description: 'Terminal output' },
29
28
  { id: 'debug', label: 'Debug', description: 'Debug information' },
30
29
  { id: 'bikelane', label: 'BikeLane', description: 'Workflow visualization' },
31
30
  { id: 'settings', label: 'Settings', description: 'Theme, fonts, and display preferences' },
@@ -9,17 +9,20 @@
9
9
  * Single Dockview group — users can freely rearrange panels.
10
10
  */
11
11
 
12
- import React, { useCallback, useRef } from 'react';
12
+ import React, { useCallback, useRef, useEffect, useState } from 'react';
13
13
  import {
14
14
  DockviewReact,
15
15
  DockviewReadyEvent,
16
16
  DockviewApi,
17
17
  IDockviewPanelProps,
18
+ SerializedDockview,
19
+ DockviewDefaultTab,
18
20
  } from 'dockview-react';
19
21
  import 'dockview-react/dist/styles/dockview.css';
20
22
  import { ErrorBoundary } from './ErrorBoundary';
21
23
  import { panelRegistry } from './panel-registry';
22
24
  import PersonaHeader from './PersonaHeader.js';
25
+ import { useFocusPanel } from '../hooks/useFocusPanel.js';
23
26
  import '../styles/dockview-theme.css';
24
27
 
25
28
  // =============================================================================
@@ -94,12 +97,41 @@ function PanelAdapter({ params }: IDockviewPanelProps<PanelAdapterParams>): Reac
94
97
  // BikeRackWorkspace Component
95
98
  // =============================================================================
96
99
 
97
- export function BikeRackWorkspace(): React.ReactElement {
100
+ export interface BikeRackWorkspaceProps {
101
+ initialLayout?: SerializedDockview;
102
+ onLayoutChange?: (layout: SerializedDockview) => void;
103
+ }
104
+
105
+ export function BikeRackWorkspace({
106
+ initialLayout,
107
+ onLayoutChange,
108
+ }: BikeRackWorkspaceProps): React.ReactElement {
98
109
  const apiRef = useRef<DockviewApi | null>(null);
110
+ const [dockviewApi, setDockviewApi] = useState<DockviewApi | null>(null);
111
+ const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
112
+
113
+ // Panel focus mode — stash/restore layout on /bc CLI events
114
+ useFocusPanel(dockviewApi);
99
115
 
100
116
  const onReady = useCallback((event: DockviewReadyEvent) => {
101
117
  const api = event.api;
102
118
  apiRef.current = api;
119
+ setDockviewApi(api);
120
+
121
+ // Restore saved layout if available (must have actual panels, not just empty {})
122
+ if (initialLayout && initialLayout.grid && initialLayout.panels
123
+ && Object.keys(initialLayout.panels).length > 0) {
124
+ try {
125
+ api.fromJSON(initialLayout);
126
+ // Verify panels were actually created
127
+ if (api.panels.length > 0) {
128
+ return;
129
+ }
130
+ console.warn('[BikeRackWorkspace] Restored layout produced no panels, building default');
131
+ } catch (err) {
132
+ console.warn('[BikeRackWorkspace] Failed to restore layout, building default:', err);
133
+ }
134
+ }
103
135
 
104
136
  // Single group — all panels as tabs, user can rearrange freely
105
137
  const first = api.addPanel({
@@ -118,23 +150,66 @@ export function BikeRackWorkspace(): React.ReactElement {
118
150
  title: PANEL_TITLES[BIKERACK_PANELS[i]],
119
151
  });
120
152
  }
153
+ }, [initialLayout]);
154
+
155
+ // Subscribe to layout changes for persistence
156
+ const handleLayoutChange = useCallback(() => {
157
+ const api = apiRef.current;
158
+ if (!api || !onLayoutChange) return;
159
+
160
+ // Never save empty layouts — prevents corruption loop
161
+ if (api.panels.length === 0) return;
162
+
163
+ if (saveTimeoutRef.current) {
164
+ clearTimeout(saveTimeoutRef.current);
165
+ }
166
+
167
+ saveTimeoutRef.current = setTimeout(() => {
168
+ const serialized = api.toJSON();
169
+ // Double-check: don't persist if serialization produced empty panels
170
+ if (serialized.panels && Object.keys(serialized.panels).length > 0) {
171
+ onLayoutChange(serialized);
172
+ }
173
+ }, 300);
174
+ }, [onLayoutChange]);
175
+
176
+ useEffect(() => {
177
+ const api = apiRef.current;
178
+ if (!api) return;
179
+
180
+ const disposables = [
181
+ api.onDidLayoutChange(() => handleLayoutChange()),
182
+ api.onDidAddPanel(() => handleLayoutChange()),
183
+ api.onDidRemovePanel(() => handleLayoutChange()),
184
+ ];
185
+
186
+ return () => disposables.forEach(d => d.dispose());
187
+ }, [handleLayoutChange]);
188
+
189
+ // Cleanup on unmount
190
+ useEffect(() => {
191
+ return () => {
192
+ if (saveTimeoutRef.current) {
193
+ clearTimeout(saveTimeoutRef.current);
194
+ }
195
+ };
121
196
  }, []);
122
197
 
123
- const components = { PanelAdapter };
198
+ // Memoize to prevent DockviewReact from reinitializing on re-render
199
+ const components = React.useMemo(() => ({ PanelAdapter }), []);
124
200
 
125
201
  return (
126
202
  <div className="cyclist-app cyclist-dockview" style={{ height: '100vh', width: '100vw', display: 'flex', flexDirection: 'column' }}>
127
203
  <div data-testid="bikerack-portrait-anchor" style={{ flexShrink: 0 }}>
128
204
  <PersonaHeader />
129
205
  </div>
130
- <div className="flex-1" style={{ flexGrow: 1, minHeight: 0 }}>
131
- <DockviewReact
132
- className="dockview-container"
133
- onReady={onReady}
134
- components={components}
135
- watermarkComponent={() => null}
136
- />
137
- </div>
206
+ <DockviewReact
207
+ className="dockview-container"
208
+ onReady={onReady}
209
+ components={components}
210
+ defaultTabComponent={(props) => <DockviewDefaultTab {...props} hideClose />}
211
+ watermarkComponent={() => null}
212
+ />
138
213
  </div>
139
214
  );
140
215
  }
@@ -13,7 +13,7 @@
13
13
  * - Theme integration via CSS custom properties
14
14
  */
15
15
 
16
- import React, { useEffect, useRef, useCallback, useState, ComponentType } from 'react';
16
+ import React, { useEffect, useRef, useCallback, useState } from 'react';
17
17
  import { Button } from '@/components/ui/button';
18
18
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
19
19
  import {
@@ -23,11 +23,13 @@ import {
23
23
  DockviewApi,
24
24
  IDockviewPanel,
25
25
  SerializedDockview,
26
+ DockviewDefaultTab,
26
27
  } from 'dockview-react';
27
28
  import 'dockview-react/dist/styles/dockview.css';
28
29
  import { ErrorBoundary } from './ErrorBoundary';
29
- import { panelRegistry } from './panel-registry';
30
+ import { panelRegistry, type PanelComponent } from './panel-registry';
30
31
  import { useResponsiveLayout, MIN_DIMENSIONS, SIDEBAR_WIDTHS } from '../hooks/useResponsiveLayout';
32
+ import { useFocusPanel } from '../hooks/useFocusPanel.js';
31
33
  import '../styles/dockview-theme.css';
32
34
 
33
35
  // =============================================================================
@@ -40,7 +42,6 @@ export const PANEL_INVENTORY = {
40
42
  DIFFS: 'diffs',
41
43
  DEBUG: 'debug',
42
44
  AUDIT_LOG: 'audit-log',
43
- TTY: 'tty',
44
45
  // Center panel (sacred)
45
46
  MESSAGE: 'message',
46
47
  // Right sidebar panels
@@ -62,7 +63,7 @@ export type PanelId = typeof PANEL_INVENTORY[keyof typeof PANEL_INVENTORY];
62
63
  /**
63
64
  * Register a panel component by ID
64
65
  */
65
- export function registerPanelComponent(id: string, component: ComponentType): void {
66
+ export function registerPanelComponent(id: string, component: PanelComponent): void {
66
67
  panelRegistry.set(id, component);
67
68
  }
68
69
 
@@ -81,7 +82,7 @@ export function getDockviewApi(): DockviewApi | null {
81
82
 
82
83
  // Panel group definitions (needed for restore logic)
83
84
  // Exported so layout persistence can merge missing panels
84
- export const LEFT_SIDEBAR_PANELS = [PANEL_INVENTORY.CHANGED, PANEL_INVENTORY.DIFFS, PANEL_INVENTORY.DEBUG, PANEL_INVENTORY.AUDIT_LOG, PANEL_INVENTORY.TTY] as const;
85
+ export const LEFT_SIDEBAR_PANELS = [PANEL_INVENTORY.CHANGED, PANEL_INVENTORY.DIFFS, PANEL_INVENTORY.DEBUG, PANEL_INVENTORY.AUDIT_LOG] as const;
85
86
  export const RIGHT_SIDEBAR_PANELS = [
86
87
  PANEL_INVENTORY.SPRINT,
87
88
  PANEL_INVENTORY.WORKFLOW,
@@ -98,7 +99,6 @@ const PANEL_TITLES: Record<string, string> = {
98
99
  diffs: 'Diffs',
99
100
  debug: 'Debug',
100
101
  'audit-log': 'Audit Log',
101
- tty: 'Terminal',
102
102
  message: 'Message',
103
103
  sprint: 'Sprint',
104
104
  workflow: 'Workflow',
@@ -404,11 +404,15 @@ export function DockviewWorkspace({
404
404
  onLayoutChange,
405
405
  }: DockviewWorkspaceProps): React.ReactElement {
406
406
  const apiRef = useRef<DockviewApi | null>(null);
407
+ const [dockviewApi, setDockviewApi] = useState<DockviewApi | null>(null);
407
408
  const { isSmall, isBelowMinimum, sidebarWidth } = useResponsiveLayout();
408
409
  const [isReady, setIsReady] = useState(false);
409
410
  const [closedPanelsList, setClosedPanelsList] = useState<string[]>([]);
410
411
  const [showRestoreMenu, setShowRestoreMenu] = useState(false);
411
412
  const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
413
+ // Panel focus mode — stash/restore layout on /bc CLI events
414
+ useFocusPanel(dockviewApi);
415
+
412
416
  // Track if responsive effect should apply - skip on initial load to respect saved collapsed state
413
417
  const hasAppliedInitialLayout = useRef(false);
414
418
  const previousIsSmall = useRef<boolean | null>(null);
@@ -423,6 +427,7 @@ export function DockviewWorkspace({
423
427
  const api = event.api;
424
428
  apiRef.current = api;
425
429
  dockviewApiRef = api;
430
+ setDockviewApi(api);
426
431
 
427
432
  // Use native fromJSON if we have a saved layout, otherwise build default
428
433
  if (initialLayout && initialLayout.grid && initialLayout.panels) {
@@ -526,6 +531,9 @@ export function DockviewWorkspace({
526
531
  const api = apiRef.current;
527
532
  if (!api || !onLayoutChange) return;
528
533
 
534
+ // Never save empty layouts — prevents corruption loop
535
+ if (api.panels.length === 0) return;
536
+
529
537
  // Debounce saves
530
538
  if (saveTimeoutRef.current) {
531
539
  clearTimeout(saveTimeoutRef.current);
@@ -534,7 +542,10 @@ export function DockviewWorkspace({
534
542
  saveTimeoutRef.current = setTimeout(() => {
535
543
  // Use native Dockview toJSON for complete layout serialization
536
544
  const serializedLayout = api.toJSON();
537
- onLayoutChange(serializedLayout);
545
+ // Double-check: don't persist if serialization produced empty panels
546
+ if (serializedLayout.panels && Object.keys(serializedLayout.panels).length > 0) {
547
+ onLayoutChange(serializedLayout);
548
+ }
538
549
  }, 300);
539
550
  }, [onLayoutChange]);
540
551
 
@@ -659,7 +670,6 @@ export function DockviewWorkspace({
659
670
  diffs: 'Diffs',
660
671
  debug: 'Debug',
661
672
  'audit-log': 'Audit Log',
662
- tty: 'Terminal',
663
673
  sprint: 'Sprint',
664
674
  workflow: 'Workflow',
665
675
  ac: 'AC',
@@ -728,6 +738,7 @@ export function DockviewWorkspace({
728
738
  className="dockview-container"
729
739
  onReady={onReady}
730
740
  components={components}
741
+ defaultTabComponent={(props) => <DockviewDefaultTab {...props} hideClose />}
731
742
  watermarkComponent={() => null}
732
743
  />
733
744
  </div>
@@ -24,7 +24,6 @@ import {
24
24
  AuditLogPanel,
25
25
  ChangedPanel,
26
26
  ACPanel,
27
- TTYPanel,
28
27
  DebugPanel,
29
28
  BikeLanePanel,
30
29
  SettingsPanel,
@@ -44,7 +43,6 @@ export const PANEL_REGISTRY: Record<string, React.ComponentType> = {
44
43
  audit: AuditLogPanel,
45
44
  changed: ChangedPanel,
46
45
  ac: ACPanel,
47
- tty: TTYPanel,
48
46
  debug: DebugPanel,
49
47
  bikelane: BikeLanePanel,
50
48
  settings: SettingsPanel,
@@ -70,7 +68,7 @@ export function StandalonePanel(): React.ReactElement {
70
68
  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh', width: '100vw', backgroundColor: 'var(--bg-primary, #1a1a2e)', color: 'var(--text-primary, #e4e4e7)' }}>
71
69
  <h1>Panel not found</h1>
72
70
  <p>
73
- <a href="/bikerack" style={{ color: 'var(--accent, #818cf8)' }}>Back to BikeRack</a>
71
+ <a href="/" style={{ color: 'var(--accent, #818cf8)' }}>Back to BikeRack</a>
74
72
  </p>
75
73
  </div>
76
74
  );
@@ -8,4 +8,6 @@
8
8
 
9
9
  import type { ComponentType } from 'react';
10
10
 
11
- export const panelRegistry: Map<string, ComponentType> = new Map();
11
+ export type PanelComponent = ComponentType;
12
+
13
+ export const panelRegistry: Map<string, PanelComponent> = new Map();
@@ -8,12 +8,11 @@
8
8
  * - Statistics display
9
9
  */
10
10
 
11
- import React, { useState, useEffect, useCallback } from 'react';
11
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
12
12
  import { Button } from '@/components/ui/button';
13
13
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
14
14
  import { Separator } from '@/components/ui/separator';
15
15
  import { Skeleton } from '@/components/ui/skeleton';
16
- import { ScrollArea } from '@/components/ui/scroll-area';
17
16
 
18
17
  // =============================================================================
19
18
  // Types
@@ -125,6 +124,30 @@ export function AuditLogPanel(): React.ReactElement {
125
124
  const [error, setError] = useState<string | null>(null);
126
125
  const [expandedEntry, setExpandedEntry] = useState<number | null>(null);
127
126
 
127
+ // Auto-scroll refs (using ref instead of state for synchronous updates)
128
+ const autoScrollRef = useRef(true);
129
+ const topRef = useRef<HTMLDivElement>(null);
130
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
131
+
132
+ // Detect manual scroll to pause/resume auto-scroll
133
+ // Re-runs when loading changes so the listener attaches after the scroll container renders
134
+ useEffect(() => {
135
+ const el = scrollContainerRef.current;
136
+ if (!el) return;
137
+ const handler = () => {
138
+ autoScrollRef.current = el.scrollTop === 0;
139
+ };
140
+ el.addEventListener('scroll', handler);
141
+ return () => el.removeEventListener('scroll', handler);
142
+ }, [loading]);
143
+
144
+ // Auto-scroll to newest entry when entries change
145
+ useEffect(() => {
146
+ if (autoScrollRef.current && topRef.current && entries.length > 0) {
147
+ topRef.current.scrollIntoView();
148
+ }
149
+ }, [entries]);
150
+
128
151
  // Fetch entries
129
152
  const fetchEntries = useCallback(async () => {
130
153
  try {
@@ -326,7 +349,8 @@ export function AuditLogPanel(): React.ReactElement {
326
349
  </div>
327
350
 
328
351
  {/* Entries List */}
329
- <ScrollArea className="audit-log-entries flex-1">
352
+ <div ref={scrollContainerRef} className="audit-log-entries flex-1" style={{ overflow: 'auto' }}>
353
+ <div ref={topRef} />
330
354
  {entries.length === 0 ? (
331
355
  <div className="p-4 text-muted text-center">No entries</div>
332
356
  ) : (
@@ -456,7 +480,7 @@ export function AuditLogPanel(): React.ReactElement {
456
480
  </tbody>
457
481
  </table>
458
482
  )}
459
- </ScrollArea>
483
+ </div>
460
484
  </TooltipProvider>
461
485
  </div>
462
486
  );
@@ -6,13 +6,12 @@
6
6
  */
7
7
 
8
8
  import React, { useState } from 'react';
9
- import { RefreshCw } from 'lucide-react';
10
9
  import { Button } from '@/components/ui/button';
11
10
  import { Badge } from '@/components/ui/badge';
12
11
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
13
12
  import { Skeleton } from '@/components/ui/skeleton';
14
13
  import { useGitStatus, RepoStatusData, DirtyFile } from '../../hooks/useGitStatus';
15
- import { useClaudeContext } from '../../contexts/ClaudeContext';
14
+
16
15
 
17
16
  /** Get CSS class for file status */
18
17
  function getFileStatusClass(status: string): string {
@@ -152,11 +151,6 @@ function RepoStatus({ repo }: RepoStatusProps): React.ReactElement {
152
151
 
153
152
  export function GitPanel(): React.ReactElement {
154
153
  const { repos, isLoading, error } = useGitStatus();
155
- const { send } = useClaudeContext();
156
-
157
- const handleSyncAll = () => {
158
- send('Sync all repos');
159
- };
160
154
 
161
155
  if (isLoading) {
162
156
  return (
@@ -193,19 +187,6 @@ export function GitPanel(): React.ReactElement {
193
187
 
194
188
  return (
195
189
  <div className="git-panel stacked" data-testid="git-panel">
196
- <div className="git-panel-actions">
197
- <TooltipProvider delayDuration={300}>
198
- <Tooltip>
199
- <TooltipTrigger asChild>
200
- <button className="sync-all-btn" onClick={handleSyncAll} aria-label="Sync all repos">
201
- <RefreshCw size={14} />
202
- <span>Sync all repos</span>
203
- </button>
204
- </TooltipTrigger>
205
- <TooltipContent>Pull latest changes for all repos</TooltipContent>
206
- </Tooltip>
207
- </TooltipProvider>
208
- </div>
209
190
  {repos.map(repo => (
210
191
  <RepoStatus key={repo.name} repo={repo} />
211
192
  ))}
@@ -65,7 +65,6 @@ const PANEL_DISPLAY_NAMES: Record<string, string> = {
65
65
  diffs: 'Diffs',
66
66
  debug: 'Debug',
67
67
  'audit-log': 'Audit Log',
68
- tty: 'Terminal',
69
68
  message: 'Message',
70
69
  sprint: 'Sprint',
71
70
  workflow: 'Workflow',
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import React, { useState, useEffect, useCallback, useRef } from 'react';
9
- import { Check, Loader, Circle, AlertTriangle } from 'lucide-react';
9
+ import { Check, Copy, Loader, Circle, AlertTriangle } from 'lucide-react';
10
10
  import { Button } from '@/components/ui/button';
11
11
  import { Badge } from '@/components/ui/badge';
12
12
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
@@ -240,6 +240,35 @@ function JiraLink({ jiraKey, storyId }: { jiraKey: string; storyId: string }): R
240
240
  );
241
241
  }
242
242
 
243
+ /**
244
+ * CopyButton - Copy ID + title to clipboard on click
245
+ */
246
+ function CopyButton({ text }: { text: string }): React.ReactElement {
247
+ const [copied, setCopied] = useState(false);
248
+
249
+ const handleCopy = async (e: React.MouseEvent) => {
250
+ e.stopPropagation();
251
+ try {
252
+ await navigator.clipboard.writeText(text);
253
+ setCopied(true);
254
+ setTimeout(() => setCopied(false), 2000);
255
+ } catch {
256
+ // clipboard API not available
257
+ }
258
+ };
259
+
260
+ return (
261
+ <button
262
+ className={`copy-id-button ${copied ? 'copied' : ''}`}
263
+ onClick={handleCopy}
264
+ aria-label={`Copy ${text}`}
265
+ title="Copy ID + title"
266
+ >
267
+ {copied ? <Check size={12} /> : <Copy size={12} />}
268
+ </button>
269
+ );
270
+ }
271
+
243
272
  /**
244
273
  * EpicGroup - Renders a single epic with its stories
245
274
  */
@@ -281,6 +310,7 @@ function EpicGroup({
281
310
  </Button>
282
311
  <span className="epic-title">{epic.title}</span>
283
312
  {epic.jiraKey && <span className="epic-jira">{epic.jiraKey}</span>}
313
+ <CopyButton text={`${epic.id} ${epic.title}`} />
284
314
  <ContextIndicator hasContext={epic.hasContext ?? false} testIdPrefix="epic" id={epic.id} />
285
315
  {completed && epic.hasContext && (
286
316
  <Badge variant="default" className="epic-ready-badge" data-testid={`epic-ready-badge-${epic.id}`}>
@@ -347,6 +377,7 @@ function EpicGroup({
347
377
  <PriorityDot priority={story.priority} storyId={story.id} />
348
378
  <StatusBadge status={story.status} storyId={story.id} />
349
379
  {story.jiraKey && <JiraLink jiraKey={story.jiraKey} storyId={story.id} />}
380
+ <CopyButton text={`${story.id} ${story.title}`} />
350
381
  <div className="story-info">
351
382
  <span className="story-title">{story.title}</span>
352
383
  <span className="story-meta">
@@ -17,8 +17,6 @@ export { DiffsPanel } from './DiffsPanel';
17
17
  export { DebugPanel } from './DebugPanel';
18
18
  export { SettingsPanel } from './SettingsPanel';
19
19
  export { AuditLogPanel } from './AuditLogPanel';
20
- export { TTYPanel } from './TTYPanel';
21
-
22
20
  // Legacy exports - kept for backwards compatibility and tests
23
21
  export { AcceptanceCriteriaPanel, ConnectedAcceptanceCriteriaPanel } from './AcceptanceCriteriaPanel';
24
22
  export { BikeLanePanel, ConnectedBikeLanePanel } from './BikeLanePanel';