Skip to content

Commit 43d7155

Browse files
nunocoracaoclaude
andcommitted
refactor: full codebase restructure for maintainability
Split oversized monolith files into focused, single-responsibility modules: - services.go (1354 lines) → 9 per-service files with shared helpers - model.go (1167 lines) → 5 files (model, keyhandler, mousehandler, msghandler, actions) - tray.go handleClicks: replace 20 copy-paste blocks with loop-based goroutines - agent.go: decompose runAgentAttach into focused helper functions - Extract magic numbers to named constants in process.go and manager.go - GUI: extract usePanelResize hook and agentStatusEqual utility Behavior-preserving — no API changes, no new features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c0b1958 commit 43d7155

24 files changed

+2624
-2597
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useState, useCallback, useRef, useEffect } from 'react'
2+
3+
interface PanelResizeOptions {
4+
storageKey: string
5+
defaultWidth?: number
6+
minWidth?: number
7+
maxWidth?: number
8+
}
9+
10+
/**
11+
* Hook for drag-resizable panels with localStorage persistence.
12+
*/
13+
export function usePanelResize({
14+
storageKey,
15+
defaultWidth = 520,
16+
minWidth = 350,
17+
maxWidth = 800
18+
}: PanelResizeOptions) {
19+
const [width, setWidth] = useState(() => {
20+
const saved = localStorage.getItem(storageKey)
21+
return saved ? Number(saved) : defaultWidth
22+
})
23+
const isDragging = useRef(false)
24+
25+
const handleDragStart = useCallback(
26+
(e: React.MouseEvent) => {
27+
e.preventDefault()
28+
isDragging.current = true
29+
const startX = e.clientX
30+
const startWidth = width
31+
32+
const onMouseMove = (ev: MouseEvent) => {
33+
const delta = startX - ev.clientX
34+
const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidth + delta))
35+
setWidth(newWidth)
36+
}
37+
38+
const onMouseUp = () => {
39+
isDragging.current = false
40+
document.removeEventListener('mousemove', onMouseMove)
41+
document.removeEventListener('mouseup', onMouseUp)
42+
document.body.style.cursor = ''
43+
document.body.style.userSelect = ''
44+
}
45+
46+
document.body.style.cursor = 'col-resize'
47+
document.body.style.userSelect = 'none'
48+
document.addEventListener('mousemove', onMouseMove)
49+
document.addEventListener('mouseup', onMouseUp)
50+
},
51+
[width, minWidth, maxWidth]
52+
)
53+
54+
useEffect(() => {
55+
localStorage.setItem(storageKey, String(width))
56+
}, [width, storageKey])
57+
58+
return { width, handleDragStart, isDragging }
59+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { AgentStatus } from '../generated/watchfire_pb'
2+
3+
/**
4+
* Returns true if two agent statuses are equivalent (no meaningful change).
5+
* Used to skip redundant state updates in stores.
6+
*/
7+
export function agentStatusEqual(a: AgentStatus | undefined, b: AgentStatus): boolean {
8+
if (!a) return false
9+
return (
10+
a.isRunning === b.isRunning &&
11+
a.mode === b.mode &&
12+
a.taskNumber === b.taskNumber &&
13+
a.taskTitle === b.taskTitle &&
14+
a.wildfirePhase === b.wildfirePhase
15+
)
16+
}

gui/src/renderer/src/stores/agent-store.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { create } from 'zustand'
22
import type { AgentStatus, AgentIssue } from '../generated/watchfire_pb'
33
import { getAgentClient } from '../lib/grpc-client'
4+
import { agentStatusEqual } from '../lib/agent-utils'
45

56
interface AgentState {
67
statuses: Record<string, AgentStatus>
@@ -48,14 +49,7 @@ export const useAgentStore = create<AgentState>((set, get) => ({
4849
const client = getAgentClient()
4950
const status = await client.getAgentStatus({ projectId })
5051
const existing = get().statuses[projectId]
51-
if (
52-
existing &&
53-
existing.isRunning === status.isRunning &&
54-
existing.mode === status.mode &&
55-
existing.taskNumber === status.taskNumber &&
56-
existing.taskTitle === status.taskTitle &&
57-
existing.wildfirePhase === status.wildfirePhase
58-
) return
52+
if (agentStatusEqual(existing, status)) return
5953
set((s) => ({ statuses: { ...s.statuses, [projectId]: status } }))
6054
} catch {
6155
// not running — set explicit idle status so consumers know the fetch completed

gui/src/renderer/src/stores/projects-store.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { create } from 'zustand'
22
import type { Project, AgentStatus } from '../generated/watchfire_pb'
33
import { getProjectClient, getAgentClient } from '../lib/grpc-client'
4+
import { agentStatusEqual } from '../lib/agent-utils'
45

56
interface ProjectsState {
67
projects: Project[]
@@ -38,14 +39,7 @@ export const useProjectsStore = create<ProjectsState>((set, get) => ({
3839
const client = getAgentClient()
3940
const status = await client.getAgentStatus({ projectId })
4041
const existing = get().agentStatuses[projectId]
41-
if (
42-
existing &&
43-
existing.isRunning === status.isRunning &&
44-
existing.mode === status.mode &&
45-
existing.taskNumber === status.taskNumber &&
46-
existing.taskTitle === status.taskTitle &&
47-
existing.wildfirePhase === status.wildfirePhase
48-
) return
42+
if (agentStatusEqual(existing, status)) return
4943
set((s) => ({
5044
agentStatuses: { ...s.agentStatuses, [projectId]: status }
5145
}))

gui/src/renderer/src/views/ProjectView/ProjectView.tsx

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback, useRef } from 'react'
1+
import { useState, useEffect } from 'react'
22
import { ListTodo, FileText, Trash2, Settings, GitBranch, Globe, Circle, PanelRightClose, PanelRight, Square, Flame, Play, Sparkles, KeyRound } from 'lucide-react'
33
import { useAppStore } from '../../stores/app-store'
44
import { useProjectsStore } from '../../stores/projects-store'
@@ -10,6 +10,7 @@ import { AgentBadge } from '../../components/AgentBadge'
1010
import { Button } from '../../components/ui/Button'
1111
import { useToast } from '../../components/ui/Toast'
1212
import { cn } from '../../lib/utils'
13+
import { usePanelResize } from '../../hooks/usePanelResize'
1314
import { TasksTab } from './TasksTab/TasksTab'
1415
import { DefinitionTab } from './DefinitionTab'
1516
import { SecretsTab } from './SecretsTab'
@@ -41,41 +42,12 @@ export function ProjectView() {
4142

4243
const [centerTab, setCenterTab] = useState<CenterTab>('tasks')
4344
const [rightPanelOpen, setRightPanelOpen] = useState(true)
44-
const [rightPanelWidth, setRightPanelWidth] = useState(() => {
45-
const saved = localStorage.getItem('wf-right-panel-width')
46-
return saved ? Number(saved) : 520
45+
const { width: rightPanelWidth, handleDragStart } = usePanelResize({
46+
storageKey: 'wf-right-panel-width',
47+
defaultWidth: 520,
48+
minWidth: 350,
49+
maxWidth: 800
4750
})
48-
const isDragging = useRef(false)
49-
50-
const handleDragStart = useCallback((e: React.MouseEvent) => {
51-
e.preventDefault()
52-
isDragging.current = true
53-
const startX = e.clientX
54-
const startWidth = rightPanelWidth
55-
56-
const onMouseMove = (ev: MouseEvent) => {
57-
const delta = startX - ev.clientX
58-
const newWidth = Math.min(800, Math.max(350, startWidth + delta))
59-
setRightPanelWidth(newWidth)
60-
}
61-
62-
const onMouseUp = () => {
63-
isDragging.current = false
64-
document.removeEventListener('mousemove', onMouseMove)
65-
document.removeEventListener('mouseup', onMouseUp)
66-
document.body.style.cursor = ''
67-
document.body.style.userSelect = ''
68-
}
69-
70-
document.body.style.cursor = 'col-resize'
71-
document.body.style.userSelect = 'none'
72-
document.addEventListener('mousemove', onMouseMove)
73-
document.addEventListener('mouseup', onMouseUp)
74-
}, [rightPanelWidth])
75-
76-
useEffect(() => {
77-
localStorage.setItem('wf-right-panel-width', String(rightPanelWidth))
78-
}, [rightPanelWidth])
7951

8052
const project = projects.find((p) => p.projectId === projectId)
8153
const isAgentRunning = agentStatus?.isRunning

0 commit comments

Comments
 (0)