@pennyfarthing/cyclist 10.3.1 → 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.
- package/dist/api/agent-load.d.ts +1 -2
- package/dist/api/agent-load.d.ts.map +1 -1
- package/dist/api/agent-load.js +2 -123
- package/dist/api/agent-load.js.map +1 -1
- package/dist/api/audit-log.d.ts +1 -17
- package/dist/api/audit-log.d.ts.map +1 -1
- package/dist/api/audit-log.js +2 -162
- package/dist/api/audit-log.js.map +1 -1
- package/dist/api/background-tasks.d.ts +1 -26
- package/dist/api/background-tasks.d.ts.map +1 -1
- package/dist/api/background-tasks.js +2 -55
- package/dist/api/background-tasks.js.map +1 -1
- package/dist/api/bell.d.ts +1 -18
- package/dist/api/bell.d.ts.map +1 -1
- package/dist/api/bell.js +2 -33
- package/dist/api/bell.js.map +1 -1
- package/dist/api/code-markers.d.ts +1 -8
- package/dist/api/code-markers.d.ts.map +1 -1
- package/dist/api/code-markers.js +2 -61
- package/dist/api/code-markers.js.map +1 -1
- package/dist/api/complexity.d.ts +1 -2
- package/dist/api/complexity.d.ts.map +1 -1
- package/dist/api/complexity.js +2 -46
- package/dist/api/complexity.js.map +1 -1
- package/dist/api/context.d.ts +1 -37
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js +2 -143
- package/dist/api/context.js.map +1 -1
- package/dist/api/dead-code.d.ts +1 -2
- package/dist/api/dead-code.d.ts.map +1 -1
- package/dist/api/dead-code.js +2 -69
- package/dist/api/dead-code.js.map +1 -1
- package/dist/api/dependencies.d.ts +1 -2
- package/dist/api/dependencies.d.ts.map +1 -1
- package/dist/api/dependencies.js +2 -42
- package/dist/api/dependencies.js.map +1 -1
- package/dist/api/evaluation.d.ts +1 -19
- package/dist/api/evaluation.d.ts.map +1 -1
- package/dist/api/evaluation.js +2 -127
- package/dist/api/evaluation.js.map +1 -1
- package/dist/api/file-browser.d.ts +1 -8
- package/dist/api/file-browser.d.ts.map +1 -1
- package/dist/api/file-browser.js +2 -114
- package/dist/api/file-browser.js.map +1 -1
- package/dist/api/git.d.ts +1 -46
- package/dist/api/git.d.ts.map +1 -1
- package/dist/api/git.js +2 -354
- package/dist/api/git.js.map +1 -1
- package/dist/api/health-score.d.ts +1 -2
- package/dist/api/health-score.d.ts.map +1 -1
- package/dist/api/health-score.js +2 -46
- package/dist/api/health-score.js.map +1 -1
- package/dist/api/hook-request.d.ts +1 -40
- package/dist/api/hook-request.d.ts.map +1 -1
- package/dist/api/hook-request.js +2 -277
- package/dist/api/hook-request.js.map +1 -1
- package/dist/api/hotspots.d.ts +1 -2
- package/dist/api/hotspots.d.ts.map +1 -1
- package/dist/api/hotspots.js +2 -61
- package/dist/api/hotspots.js.map +1 -1
- package/dist/api/identity.d.ts +1 -16
- package/dist/api/identity.d.ts.map +1 -1
- package/dist/api/identity.js +2 -78
- package/dist/api/identity.js.map +1 -1
- package/dist/api/index.d.ts +1 -34
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -44
- package/dist/api/index.js.map +1 -1
- package/dist/api/mode.d.ts +1 -22
- package/dist/api/mode.d.ts.map +1 -1
- package/dist/api/mode.js +2 -37
- package/dist/api/mode.js.map +1 -1
- package/dist/api/otlp.d.ts +1 -2
- package/dist/api/otlp.d.ts.map +1 -1
- package/dist/api/otlp.js +2 -46
- package/dist/api/otlp.js.map +1 -1
- package/dist/api/permissions.d.ts +1 -15
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +2 -66
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/persona.d.ts +1 -8
- package/dist/api/persona.d.ts.map +1 -1
- package/dist/api/persona.js +2 -67
- package/dist/api/persona.js.map +1 -1
- package/dist/api/portrait.d.ts +1 -5
- package/dist/api/portrait.d.ts.map +1 -1
- package/dist/api/portrait.js +2 -27
- package/dist/api/portrait.js.map +1 -1
- package/dist/api/settings.d.ts +1 -53
- package/dist/api/settings.d.ts.map +1 -1
- package/dist/api/settings.js +2 -464
- package/dist/api/settings.js.map +1 -1
- package/dist/api/spans.d.ts +1 -16
- package/dist/api/spans.d.ts.map +1 -1
- package/dist/api/spans.js +2 -244
- package/dist/api/spans.js.map +1 -1
- package/dist/api/stats.d.ts +1 -12
- package/dist/api/stats.d.ts.map +1 -1
- package/dist/api/stats.js +2 -84
- package/dist/api/stats.js.map +1 -1
- package/dist/api/story.d.ts +1 -2
- package/dist/api/story.d.ts.map +1 -1
- package/dist/api/story.js +2 -14
- package/dist/api/story.js.map +1 -1
- package/dist/api/telemetry.d.ts +1 -18
- package/dist/api/telemetry.d.ts.map +1 -1
- package/dist/api/telemetry.js +2 -164
- package/dist/api/telemetry.js.map +1 -1
- package/dist/api/theme-agents.d.ts +1 -60
- package/dist/api/theme-agents.d.ts.map +1 -1
- package/dist/api/theme-agents.js +2 -213
- package/dist/api/theme-agents.js.map +1 -1
- package/dist/api/todos.d.ts +1 -32
- package/dist/api/todos.d.ts.map +1 -1
- package/dist/api/todos.js +2 -43
- package/dist/api/todos.js.map +1 -1
- package/dist/api/token-stats.d.ts +1 -7
- package/dist/api/token-stats.d.ts.map +1 -1
- package/dist/api/token-stats.js +2 -35
- package/dist/api/token-stats.js.map +1 -1
- package/dist/api/welcome.d.ts +1 -21
- package/dist/api/welcome.d.ts.map +1 -1
- package/dist/api/welcome.js +2 -34
- package/dist/api/welcome.js.map +1 -1
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +10 -0
- package/dist/env.js.map +1 -0
- package/dist/focus.d.ts +53 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/focus.js +122 -0
- package/dist/focus.js.map +1 -0
- package/dist/menu-builder.d.ts.map +1 -1
- package/dist/menu-builder.js +0 -1
- package/dist/menu-builder.js.map +1 -1
- package/dist/public/css/react.css +1 -1
- package/dist/public/js/react/react.js +51 -59
- package/dist/server.d.ts +16 -85
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +38 -409
- package/dist/server.js.map +1 -1
- package/dist/sprint-data.d.ts +1 -1
- package/dist/sprint-data.d.ts.map +1 -1
- package/dist/sprint-data.js +29 -8
- package/dist/sprint-data.js.map +1 -1
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +45 -78
- package/dist/websocket.js.map +1 -1
- package/package.json +33 -35
- package/portraits/hogans-heroes/large/burkhalter-35312.png +0 -0
- package/portraits/hogans-heroes/large/carter-34352.png +0 -0
- package/portraits/hogans-heroes/large/hochstetter-45314.png +0 -0
- package/portraits/hogans-heroes/large/hogan-44541.png +0 -0
- package/portraits/hogans-heroes/large/kinch-35241.png +0 -0
- package/portraits/hogans-heroes/large/klink-23434.png +0 -0
- package/portraits/hogans-heroes/large/lebeau-45443.png +0 -0
- package/portraits/hogans-heroes/large/marya-53543.png +0 -0
- package/portraits/hogans-heroes/large/newkirk-54432.png +0 -0
- package/portraits/hogans-heroes/large/schultz-42453.png +0 -0
- package/portraits/hogans-heroes/large/underground-55131.png +0 -0
- package/portraits/hogans-heroes/medium/burkhalter-35312.png +0 -0
- package/portraits/hogans-heroes/medium/carter-34352.png +0 -0
- package/portraits/hogans-heroes/medium/hochstetter-45314.png +0 -0
- package/portraits/hogans-heroes/medium/hogan-44541.png +0 -0
- package/portraits/hogans-heroes/medium/kinch-35241.png +0 -0
- package/portraits/hogans-heroes/medium/klink-23434.png +0 -0
- package/portraits/hogans-heroes/medium/lebeau-45443.png +0 -0
- package/portraits/hogans-heroes/medium/marya-53543.png +0 -0
- package/portraits/hogans-heroes/medium/newkirk-54432.png +0 -0
- package/portraits/hogans-heroes/medium/schultz-42453.png +0 -0
- package/portraits/hogans-heroes/medium/underground-55131.png +0 -0
- package/portraits/monty-python/large/announcer-44441.png +0 -0
- package/portraits/monty-python/large/arguer-35412.png +0 -0
- package/portraits/monty-python/large/bicycle-repair-man-35241.png +0 -0
- package/portraits/monty-python/large/colonel-35423.png +0 -0
- package/portraits/monty-python/large/counsellor-45341.png +0 -0
- package/portraits/monty-python/large/gumbys-23524.png +0 -0
- package/portraits/monty-python/large/nudge-43533.png +0 -0
- package/portraits/monty-python/large/praline-45413.png +0 -0
- package/portraits/monty-python/large/silly-walks-55322.png +0 -0
- package/portraits/monty-python/large/wensleydale-54451.png +0 -0
- package/portraits/monty-python/large/xim-nez-43534.png +0 -0
- package/portraits/monty-python/medium/announcer-44441.png +0 -0
- package/portraits/monty-python/medium/arguer-35412.png +0 -0
- package/portraits/monty-python/medium/bicycle-repair-man-35241.png +0 -0
- package/portraits/monty-python/medium/colonel-35423.png +0 -0
- package/portraits/monty-python/medium/counsellor-45341.png +0 -0
- package/portraits/monty-python/medium/gumbys-23524.png +0 -0
- package/portraits/monty-python/medium/nudge-43533.png +0 -0
- package/portraits/monty-python/medium/praline-45413.png +0 -0
- package/portraits/monty-python/medium/silly-walks-55322.png +0 -0
- package/portraits/monty-python/medium/wensleydale-54451.png +0 -0
- package/portraits/monty-python/medium/xim-nez-43534.png +0 -0
- package/portraits/stephen-king/large/andy-55231.png +0 -0
- package/portraits/stephen-king/large/christine-25112.png +0 -0
- package/portraits/stephen-king/large/danny-53243.png +0 -0
- package/portraits/stephen-king/large/flagg-55311.png +0 -0
- package/portraits/stephen-king/large/gaunt-54421.png +0 -0
- package/portraits/stephen-king/large/jack-44224.png +0 -0
- package/portraits/stephen-king/large/johnny-44353.png +0 -0
- package/portraits/stephen-king/large/margaret-15415.png +0 -0
- package/portraits/stephen-king/large/paul-45233.png +0 -0
- package/portraits/stephen-king/large/pennywise-54411.png +0 -0
- package/portraits/stephen-king/large/roland-35121.png +0 -0
- package/portraits/stephen-king/medium/andy-55231.png +0 -0
- package/portraits/stephen-king/medium/christine-25112.png +0 -0
- package/portraits/stephen-king/medium/danny-53243.png +0 -0
- package/portraits/stephen-king/medium/flagg-55311.png +0 -0
- package/portraits/stephen-king/medium/gaunt-54421.png +0 -0
- package/portraits/stephen-king/medium/jack-44224.png +0 -0
- package/portraits/stephen-king/medium/johnny-44353.png +0 -0
- package/portraits/stephen-king/medium/margaret-15415.png +0 -0
- package/portraits/stephen-king/medium/paul-45233.png +0 -0
- package/portraits/stephen-king/medium/pennywise-54411.png +0 -0
- package/portraits/stephen-king/medium/roland-35121.png +0 -0
- package/src/public/App.tsx +21 -5
- package/src/public/components/BikeRackIndex.tsx +0 -1
- package/src/public/components/BikeRackWorkspace.tsx +86 -11
- package/src/public/components/DockviewWorkspace.tsx +19 -8
- package/src/public/components/StandalonePanel.tsx +1 -3
- package/src/public/components/panel-registry.ts +3 -1
- package/src/public/components/panels/AuditLogPanel.tsx +28 -4
- package/src/public/components/panels/GitPanel.tsx +1 -20
- package/src/public/components/panels/SettingsPanel.tsx +0 -1
- package/src/public/components/panels/SprintPanel.tsx +32 -1
- package/src/public/components/panels/index.ts +0 -2
- package/src/public/hooks/useFocusPanel.ts +137 -0
- package/src/public/hooks/useLayoutPersistence.ts +8 -5
- package/src/public/styles/dockview-theme.css +1 -84
- package/src/public/styles/tailwind.css +27 -32
- package/src/public/utils/slash-commands.ts +122 -98
- package/LICENSE +0 -14
- package/dist/hooks/cyclist-pretooluse-hook.d.ts +0 -60
- package/dist/hooks/cyclist-pretooluse-hook.d.ts.map +0 -1
- package/dist/hooks/cyclist-pretooluse-hook.js +0 -57
- package/dist/hooks/cyclist-pretooluse-hook.js.map +0 -1
- package/dist/hooks/pretooluse-hook.d.ts +0 -89
- package/dist/hooks/pretooluse-hook.d.ts.map +0 -1
- package/dist/hooks/pretooluse-hook.js +0 -235
- package/dist/hooks/pretooluse-hook.js.map +0 -1
- package/dist/notification-sound.d.ts +0 -59
- package/dist/notification-sound.d.ts.map +0 -1
- package/dist/notification-sound.js +0 -219
- package/dist/notification-sound.js.map +0 -1
- package/dist/plugin-loader.test.d.ts +0 -17
- package/dist/plugin-loader.test.d.ts.map +0 -1
- package/dist/plugin-loader.test.js +0 -407
- package/dist/plugin-loader.test.js.map +0 -1
- package/portraits/star-trek-tng/large/beverly-44352.png +0 -0
- package/portraits/star-trek-tng/large/data-55241.png +0 -0
- package/portraits/star-trek-tng/large/deanna-43353.png +0 -0
- package/portraits/star-trek-tng/large/geordi-54342.png +0 -0
- package/portraits/star-trek-tng/large/jean-luc-45342.png +0 -0
- package/portraits/star-trek-tng/large/kathryn-45332.png +0 -0
- package/portraits/star-trek-tng/large/miles-35342.png +0 -0
- package/portraits/star-trek-tng/large/q-53521.png +0 -0
- package/portraits/star-trek-tng/large/spock-45231.png +0 -0
- package/portraits/star-trek-tng/large/troi-44352.png +0 -0
- package/portraits/star-trek-tng/medium/beverly-44352.png +0 -0
- package/portraits/star-trek-tng/medium/data-55241.png +0 -0
- package/portraits/star-trek-tng/medium/deanna-43353.png +0 -0
- package/portraits/star-trek-tng/medium/geordi-54342.png +0 -0
- package/portraits/star-trek-tng/medium/jean-luc-45342.png +0 -0
- package/portraits/star-trek-tng/medium/kathryn-45332.png +0 -0
- package/portraits/star-trek-tng/medium/miles-35342.png +0 -0
- package/portraits/star-trek-tng/medium/q-53521.png +0 -0
- package/portraits/star-trek-tng/medium/spock-45231.png +0 -0
- package/portraits/star-trek-tng/medium/troi-44352.png +0 -0
- package/src/public/components/panels/TTYPanel.tsx +0 -299
- package/src/public/types/electron.d.ts +0 -18
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
))}
|
|
@@ -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';
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFocusPanel Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for handling panel focus events via /ws/focus WebSocket.
|
|
5
|
+
* Story MSSCI-14977 - BikeShow client layout stash/restore on panel focus
|
|
6
|
+
* Epic 104: /bc CLI Panel Focus
|
|
7
|
+
*
|
|
8
|
+
* Listens to /ws/focus WebSocket for focus events.
|
|
9
|
+
*
|
|
10
|
+
* Multi-group layouts (Cyclist): maximizeGroup/exitMaximizedGroup
|
|
11
|
+
* Single-group layouts (BikeRack): activate target tab, stash previous
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState, useEffect, useRef } from 'react';
|
|
15
|
+
import type { DockviewApi } from 'dockview-react';
|
|
16
|
+
|
|
17
|
+
export interface UseFocusPanelResult {
|
|
18
|
+
/** Currently focused panel ID, or null if not in focus mode */
|
|
19
|
+
focusedPanel: string | null;
|
|
20
|
+
/** Whether the workspace is currently in single-panel focus mode */
|
|
21
|
+
isInFocusMode: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** WebSocket message format from /ws/focus */
|
|
25
|
+
interface FocusMessage {
|
|
26
|
+
type: 'init' | 'update';
|
|
27
|
+
focus: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hook for managing panel focus mode.
|
|
32
|
+
*
|
|
33
|
+
* @param api - Dockview API instance for layout manipulation, or null if not ready
|
|
34
|
+
* @returns Focus state including current focused panel and mode
|
|
35
|
+
*/
|
|
36
|
+
export function useFocusPanel(api: DockviewApi | null): UseFocusPanelResult {
|
|
37
|
+
const [focusedPanel, setFocusedPanel] = useState<string | null>(null);
|
|
38
|
+
const [isInFocusMode, setIsInFocusMode] = useState(false);
|
|
39
|
+
|
|
40
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
41
|
+
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
42
|
+
const isMountedRef = useRef(true);
|
|
43
|
+
|
|
44
|
+
// Refs to access latest values inside WebSocket callbacks without re-creating the effect
|
|
45
|
+
const apiRef = useRef(api);
|
|
46
|
+
apiRef.current = api;
|
|
47
|
+
|
|
48
|
+
// Stash the previously active panel ID for single-group reset
|
|
49
|
+
const previousActivePanelRef = useRef<string | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
isMountedRef.current = true;
|
|
53
|
+
|
|
54
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
55
|
+
const wsUrl = `${protocol}//${window.location.host}/ws/focus`;
|
|
56
|
+
|
|
57
|
+
const handleFocusChange = (focus: string | null) => {
|
|
58
|
+
const currentApi = apiRef.current;
|
|
59
|
+
if (!currentApi) return;
|
|
60
|
+
|
|
61
|
+
if (focus !== null) {
|
|
62
|
+
const panel = currentApi.getPanel(focus);
|
|
63
|
+
if (!panel) return;
|
|
64
|
+
|
|
65
|
+
const isMultiGroup = currentApi.groups.length > 1;
|
|
66
|
+
|
|
67
|
+
if (isMultiGroup) {
|
|
68
|
+
// Multi-group (Cyclist): maximize the target panel's group
|
|
69
|
+
currentApi.maximizeGroup(panel);
|
|
70
|
+
} else {
|
|
71
|
+
// Single-group (BikeRack): stash current active, switch tab
|
|
72
|
+
if (!previousActivePanelRef.current) {
|
|
73
|
+
previousActivePanelRef.current = currentApi.activePanel?.id ?? null;
|
|
74
|
+
}
|
|
75
|
+
panel.api.setActive();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setIsInFocusMode(true);
|
|
79
|
+
setFocusedPanel(focus);
|
|
80
|
+
} else {
|
|
81
|
+
// Reset
|
|
82
|
+
const isMaximized = currentApi.hasMaximizedGroup();
|
|
83
|
+
|
|
84
|
+
if (isMaximized) {
|
|
85
|
+
// Multi-group: exit maximize
|
|
86
|
+
currentApi.exitMaximizedGroup();
|
|
87
|
+
} else if (previousActivePanelRef.current) {
|
|
88
|
+
// Single-group: restore previous active tab
|
|
89
|
+
const prev = currentApi.getPanel(previousActivePanelRef.current);
|
|
90
|
+
if (prev) prev.api.setActive();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
previousActivePanelRef.current = null;
|
|
94
|
+
setIsInFocusMode(false);
|
|
95
|
+
setFocusedPanel(null);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const connect = () => {
|
|
100
|
+
if (!isMountedRef.current) return;
|
|
101
|
+
|
|
102
|
+
wsRef.current = new WebSocket(wsUrl);
|
|
103
|
+
|
|
104
|
+
wsRef.current.onmessage = (event: MessageEvent) => {
|
|
105
|
+
try {
|
|
106
|
+
const msg = JSON.parse(event.data) as FocusMessage;
|
|
107
|
+
if (msg.type === 'update') {
|
|
108
|
+
// Live /bc commands — apply focus change immediately
|
|
109
|
+
handleFocusChange(msg.focus);
|
|
110
|
+
}
|
|
111
|
+
// 'init' messages are ignored — focus is ephemeral, not persistent.
|
|
112
|
+
// Stale focus values in config would destroy the layout on page load.
|
|
113
|
+
} catch {
|
|
114
|
+
// Ignore malformed messages
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
wsRef.current.onclose = () => {
|
|
119
|
+
reconnectTimeoutRef.current = setTimeout(connect, 2000);
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
connect();
|
|
124
|
+
|
|
125
|
+
return () => {
|
|
126
|
+
isMountedRef.current = false;
|
|
127
|
+
if (reconnectTimeoutRef.current) {
|
|
128
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
129
|
+
}
|
|
130
|
+
if (wsRef.current) {
|
|
131
|
+
wsRef.current.close();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
return { focusedPanel, isInFocusMode };
|
|
137
|
+
}
|
|
@@ -42,6 +42,9 @@ function isValidDockviewLayout(layout: unknown): layout is SerializedDockview {
|
|
|
42
42
|
if (!layoutObj.grid || typeof layoutObj.grid !== 'object') return false;
|
|
43
43
|
if (!layoutObj.panels || typeof layoutObj.panels !== 'object') return false;
|
|
44
44
|
|
|
45
|
+
// A layout with zero panels is empty — treat as invalid so default panels get created
|
|
46
|
+
if (Object.keys(layoutObj.panels as Record<string, unknown>).length === 0) return false;
|
|
47
|
+
|
|
45
48
|
const grid = layoutObj.grid as Record<string, unknown>;
|
|
46
49
|
// Grid should have root, width, height, orientation
|
|
47
50
|
if (!grid.root || typeof grid.width !== 'number' || typeof grid.height !== 'number') return false;
|
|
@@ -49,7 +52,7 @@ function isValidDockviewLayout(layout: unknown): layout is SerializedDockview {
|
|
|
49
52
|
return true;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
export function useLayoutPersistence(): UseLayoutPersistenceResult {
|
|
55
|
+
export function useLayoutPersistence(endpoint: string = '/api/settings/layout'): UseLayoutPersistenceResult {
|
|
53
56
|
const [layout, setLayout] = useState<SerializedDockview | null>(null);
|
|
54
57
|
const [isLoading, setIsLoading] = useState(true);
|
|
55
58
|
const [isSaving, setIsSaving] = useState(false);
|
|
@@ -62,7 +65,7 @@ export function useLayoutPersistence(): UseLayoutPersistenceResult {
|
|
|
62
65
|
useEffect(() => {
|
|
63
66
|
const loadLayout = async () => {
|
|
64
67
|
try {
|
|
65
|
-
const response = await fetch(
|
|
68
|
+
const response = await fetch(endpoint);
|
|
66
69
|
if (response.ok) {
|
|
67
70
|
const data = await response.json();
|
|
68
71
|
if (data.layout && isValidDockviewLayout(data.layout)) {
|
|
@@ -87,7 +90,7 @@ export function useLayoutPersistence(): UseLayoutPersistenceResult {
|
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
loadLayout();
|
|
90
|
-
}, []);
|
|
93
|
+
}, [endpoint]);
|
|
91
94
|
|
|
92
95
|
// Debounced save function via REST API - saves native Dockview format directly
|
|
93
96
|
const saveLayout = useCallback((newLayout: SerializedDockview) => {
|
|
@@ -106,7 +109,7 @@ export function useLayoutPersistence(): UseLayoutPersistenceResult {
|
|
|
106
109
|
setIsSaving(true);
|
|
107
110
|
try {
|
|
108
111
|
// Save native Dockview format directly - no conversion needed
|
|
109
|
-
const response = await fetch(
|
|
112
|
+
const response = await fetch(endpoint, {
|
|
110
113
|
method: 'PATCH',
|
|
111
114
|
headers: { 'Content-Type': 'application/json' },
|
|
112
115
|
body: JSON.stringify(layoutToSave),
|
|
@@ -123,7 +126,7 @@ export function useLayoutPersistence(): UseLayoutPersistenceResult {
|
|
|
123
126
|
setIsSaving(false);
|
|
124
127
|
}
|
|
125
128
|
}, DEBOUNCE_DELAY);
|
|
126
|
-
}, []);
|
|
129
|
+
}, [endpoint]);
|
|
127
130
|
|
|
128
131
|
// Cleanup debounce timer on unmount
|
|
129
132
|
useEffect(() => {
|
|
@@ -78,15 +78,7 @@
|
|
|
78
78
|
border-bottom: 2px solid var(--accent, #6366f1);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/* Tab close button */
|
|
82
|
-
.cyclist-dockview .dv-tab .dv-default-tab-content .dv-close-action {
|
|
83
|
-
opacity: 0;
|
|
84
|
-
transition: opacity 0.15s ease;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.cyclist-dockview .dv-tab:hover .dv-default-tab-content .dv-close-action {
|
|
88
|
-
opacity: 1;
|
|
89
|
-
}
|
|
81
|
+
/* Tab close button removed via defaultTabComponent with hideClose={true} */
|
|
90
82
|
|
|
91
83
|
/* =============================================================================
|
|
92
84
|
Group Styles
|
|
@@ -366,81 +358,6 @@
|
|
|
366
358
|
outline-offset: -2px;
|
|
367
359
|
}
|
|
368
360
|
|
|
369
|
-
/* =============================================================================
|
|
370
|
-
TTY Panel Styles (MSSCI-14211)
|
|
371
|
-
============================================================================= */
|
|
372
|
-
|
|
373
|
-
.tty-panel {
|
|
374
|
-
width: 100%;
|
|
375
|
-
height: 100%;
|
|
376
|
-
position: relative;
|
|
377
|
-
display: flex;
|
|
378
|
-
flex-direction: column;
|
|
379
|
-
background-color: var(--bg-primary, #0a0a0f);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
.tty-terminal-container {
|
|
383
|
-
flex: 1;
|
|
384
|
-
overflow: hidden;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/* Error/Exit overlay */
|
|
388
|
-
.tty-overlay {
|
|
389
|
-
position: absolute;
|
|
390
|
-
top: 0;
|
|
391
|
-
left: 0;
|
|
392
|
-
right: 0;
|
|
393
|
-
bottom: 0;
|
|
394
|
-
display: flex;
|
|
395
|
-
align-items: center;
|
|
396
|
-
justify-content: center;
|
|
397
|
-
background-color: color-mix(in srgb, var(--bg-primary, #0a0a0f) 90%, transparent);
|
|
398
|
-
z-index: 10;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.tty-overlay-content {
|
|
402
|
-
display: flex;
|
|
403
|
-
flex-direction: column;
|
|
404
|
-
align-items: center;
|
|
405
|
-
gap: 12px;
|
|
406
|
-
padding: 24px;
|
|
407
|
-
background-color: var(--bg-tertiary, #1a1a2e);
|
|
408
|
-
border: 1px solid var(--border, #27272a);
|
|
409
|
-
border-radius: 8px;
|
|
410
|
-
text-align: center;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
.tty-overlay-icon {
|
|
414
|
-
font-size: 32px;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
.tty-overlay-message {
|
|
418
|
-
color: var(--text-secondary, #a1a1aa);
|
|
419
|
-
font-size: 14px;
|
|
420
|
-
max-width: 300px;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.tty-restart-button {
|
|
424
|
-
padding: 8px 16px;
|
|
425
|
-
background-color: var(--accent, #6366f1);
|
|
426
|
-
border: none;
|
|
427
|
-
border-radius: 6px;
|
|
428
|
-
color: var(--text-primary, #ffffff);
|
|
429
|
-
font-size: 14px;
|
|
430
|
-
font-weight: 500;
|
|
431
|
-
cursor: pointer;
|
|
432
|
-
transition: background-color 0.15s ease;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
.tty-restart-button:hover {
|
|
436
|
-
background-color: var(--accent-hover, #4f46e5);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
.tty-restart-button:focus-visible {
|
|
440
|
-
outline: 2px solid var(--accent, #6366f1);
|
|
441
|
-
outline-offset: 2px;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
361
|
/* =============================================================================
|
|
445
362
|
Accessibility Utilities
|
|
446
363
|
============================================================================= */
|
|
@@ -1752,6 +1752,33 @@
|
|
|
1752
1752
|
text-decoration: line-through;
|
|
1753
1753
|
}
|
|
1754
1754
|
|
|
1755
|
+
/* Copy ID Button — show on hover, feedback on click */
|
|
1756
|
+
.enhanced-sprint-panel .copy-id-button {
|
|
1757
|
+
opacity: 0;
|
|
1758
|
+
background: none;
|
|
1759
|
+
border: none;
|
|
1760
|
+
color: var(--text-secondary, #8b8b8b);
|
|
1761
|
+
cursor: pointer;
|
|
1762
|
+
padding: 2px;
|
|
1763
|
+
line-height: 1;
|
|
1764
|
+
flex-shrink: 0;
|
|
1765
|
+
transition: opacity 0.15s, color 0.15s;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
.enhanced-sprint-panel .story-item:hover .copy-id-button,
|
|
1769
|
+
.enhanced-sprint-panel .epic-header:hover .copy-id-button {
|
|
1770
|
+
opacity: 1;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
.enhanced-sprint-panel .copy-id-button:hover {
|
|
1774
|
+
color: var(--text-primary, #d4d4d4);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.enhanced-sprint-panel .copy-id-button.copied {
|
|
1778
|
+
opacity: 1;
|
|
1779
|
+
color: var(--success-color, #4ec9b0);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1755
1782
|
/* Future Initiatives Section */
|
|
1756
1783
|
.enhanced-sprint-panel .future-epic {
|
|
1757
1784
|
display: flex;
|
|
@@ -2000,38 +2027,6 @@
|
|
|
2000
2027
|
color: var(--status-warning, #cca700);
|
|
2001
2028
|
}
|
|
2002
2029
|
|
|
2003
|
-
/* Sync all repos action bar */
|
|
2004
|
-
.git-panel .git-panel-actions {
|
|
2005
|
-
padding: 0 0 4px;
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
.git-panel .sync-all-btn {
|
|
2009
|
-
display: flex;
|
|
2010
|
-
align-items: center;
|
|
2011
|
-
justify-content: center;
|
|
2012
|
-
gap: 6px;
|
|
2013
|
-
width: 100%;
|
|
2014
|
-
padding: 6px 10px;
|
|
2015
|
-
border: 1px solid var(--border-secondary, rgba(255, 255, 255, 0.08));
|
|
2016
|
-
border-radius: 4px;
|
|
2017
|
-
background: var(--bg-tertiary, #2d2d2d);
|
|
2018
|
-
color: var(--text-secondary, #8b8b8b);
|
|
2019
|
-
font-size: 0.75rem;
|
|
2020
|
-
font-family: var(--font-mono, 'SF Mono', Monaco, monospace);
|
|
2021
|
-
cursor: pointer;
|
|
2022
|
-
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
.git-panel .sync-all-btn:hover {
|
|
2026
|
-
background: var(--bg-hover, #3d3d3d);
|
|
2027
|
-
color: var(--text-primary, #e0e0e0);
|
|
2028
|
-
border-color: var(--accent-color, #007acc);
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
.git-panel .sync-all-btn:active {
|
|
2032
|
-
background: var(--bg-active, #4d4d4d);
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
2030
|
.git-panel .repo-status .file-status {
|
|
2036
2031
|
gap: 6px;
|
|
2037
2032
|
}
|