Skip to content

Commit 52e883f

Browse files
author
Ian Philpot
committed
refactor: split appReducer into agentReducer + conversationReducer (H2)
Break 230-line monolithic appReducer (22 cases) into two focused sub-reducers: agentReducer (12 actions) and conversationReducer (11 actions). AppStateContext.tsx now delegates via a thin switch. Added 39 new unit tests covering 11 previously untested actions. All 227 tests pass. Patch bump to v0.10.6. Co-Authored-By: Claude <claude-sonnet-4>
1 parent be218bf commit 52e883f

8 files changed

Lines changed: 873 additions & 235 deletions

File tree

aidocs/code-review.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The codebase is generally well-structured with clear separation between main pro
1212

1313
| Severity | Count | Resolved |
1414
|----------|-------|----------|
15-
| High | 7 | 3 |
15+
| High | 7 | 4 |
1616
| Medium | 15 | 2 |
1717
| Low | 10 | 3 |
1818

@@ -44,11 +44,13 @@ The codebase is generally well-structured with clear separation between main pro
4444

4545
---
4646

47-
### H2. 248-Line Reducer in AppStateContext.tsx
47+
### H2. ~~248-Line Reducer in AppStateContext.tsx~~ ✅ RESOLVED
4848

49-
`src/renderer/contexts/AppStateContext.tsx`the `appReducer` function has 22 case branches in ~248 lines. Agent-filtering logic is duplicated between `REMOVE_AGENT` and `SET_ACTIVE_AGENT`. Several cases have 3-4 levels of nesting.
49+
**Resolved in v0.10.6**Split monolithic `appReducer` (22 cases, 230 lines) into two focused sub-reducers: `agentReducer` (12 actions) and `conversationReducer` (11 actions). Composed `appReducer` now delegates via a thin switch. Added 39 new unit tests covering 11 previously untested actions. All 227 tests pass.
5050

51-
**Fix:** Split into focused sub-reducers (`agentReducer`, `conversationReducer`, `viewReducer`) composed together.
51+
~~`src/renderer/contexts/AppStateContext.tsx` — the `appReducer` function has 22 case branches in ~248 lines. Agent-filtering logic is duplicated between `REMOVE_AGENT` and `SET_ACTIVE_AGENT`. Several cases have 3-4 levels of nesting.~~
52+
53+
~~**Fix:** Split into focused sub-reducers (`agentReducer`, `conversationReducer`, `viewReducer`) composed together.~~
5254

5355
---
5456

@@ -324,7 +326,7 @@ Several test files have unused imports:
324326
|----------|--------|--------|
325327
| 1 | ~~Delete duplicate type definitions (H1)~~ | ✅ Done (c67a116) |
326328
| 2 | ~~Delete unused methods and imports (M1, M2, H6, H7, L3-L5)~~ | ✅ Done (v0.10.5) |
327-
| 3 | Break up AppStateContext reducer (H2) | Most-edited file becomes maintainable |
329+
| 3 | ~~Break up AppStateContext reducer (H2)~~ | ✅ Done (v0.10.6) |
328330
| 4 | Extract ChatView into hooks (H3) | Largest component becomes testable |
329331
| 5 | Extract preload.ts listener helper (M3) | ~60 lines of repetition eliminated |
330332
| 6 | Break up CopilotService.sendMessage (H4) | Core service becomes testable |

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vibe-playground",
33
"productName": "Vibe Playground",
4-
"version": "0.10.5",
4+
"version": "0.10.6",
55
"description": "Terminal manager with integrated file browsing for multiple repositories",
66
"main": ".webpack/main",
77
"private": true,

src/renderer/contexts/AppStateContext.tsx

Lines changed: 29 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import { createContext, useContext, useReducer, ReactNode } from 'react';
3-
import { AppState, AppAction, Agent, AgentEvent, AgentStatus } from '../../shared/types';
3+
import { AppState, AppAction } from '../../shared/types';
4+
import { agentReducer } from './agentReducer';
5+
import { conversationReducer } from './conversationReducer';
46

57
export const initialState: AppState = {
68
agents: [],
@@ -18,232 +20,32 @@ export const initialState: AppState = {
1820

1921
export function appReducer(state: AppState, action: AppAction): AppState {
2022
switch (action.type) {
21-
case 'ADD_AGENT': {
22-
const newAgent: Agent = {
23-
id: action.payload.id,
24-
label: action.payload.label,
25-
cwd: action.payload.cwd,
26-
openFiles: [],
27-
isWorktree: action.payload.isWorktree,
28-
hasSession: action.payload.hasSession,
29-
status: action.payload.hasSession ? 'idle' : undefined,
30-
};
31-
return {
32-
...state,
33-
agents: [...state.agents, newAgent],
34-
activeItemId: newAgent.id,
35-
activeAgentId: newAgent.id,
36-
};
37-
}
38-
39-
case 'REMOVE_AGENT': {
40-
const filteredAgents = state.agents.filter(a => a.id !== action.payload.id);
41-
const wasActive = state.activeAgentId === action.payload.id;
42-
const newActiveAgentId = wasActive
43-
? filteredAgents[0]?.id ?? null
44-
: state.activeAgentId;
45-
const newActiveItemId = wasActive ? newActiveAgentId : state.activeItemId;
46-
const { [action.payload.id]: _, ...remainingEvents } = state.agentEvents;
47-
48-
return {
49-
...state,
50-
agents: filteredAgents,
51-
activeItemId: newActiveItemId,
52-
activeAgentId: newActiveAgentId,
53-
agentEvents: remainingEvents,
54-
};
55-
}
56-
57-
case 'SET_ACTIVE_AGENT': {
58-
return {
59-
...state,
60-
activeItemId: action.payload.id,
61-
activeAgentId: action.payload.id,
62-
viewMode: 'agents',
63-
};
64-
}
65-
66-
case 'SET_ACTIVE_ITEM': {
67-
return {
68-
...state,
69-
activeItemId: action.payload.id,
70-
activeAgentId: action.payload.agentId ?? state.activeAgentId,
71-
viewMode: 'agents',
72-
};
73-
}
74-
75-
case 'ADD_FILE': {
76-
return {
77-
...state,
78-
agents: state.agents.map(a =>
79-
a.id === action.payload.agentId
80-
? { ...a, openFiles: [...a.openFiles, action.payload.file] }
81-
: a
82-
),
83-
activeItemId: action.payload.file.id,
84-
};
85-
}
86-
87-
case 'REMOVE_FILE': {
88-
const wasActive = state.activeItemId === action.payload.fileId;
89-
90-
return {
91-
...state,
92-
agents: state.agents.map(a =>
93-
a.id === action.payload.agentId
94-
? { ...a, openFiles: a.openFiles.filter(f => f.id !== action.payload.fileId) }
95-
: a
96-
),
97-
activeItemId: wasActive ? action.payload.agentId : state.activeItemId,
98-
};
99-
}
100-
101-
case 'RENAME_AGENT': {
102-
return {
103-
...state,
104-
agents: state.agents.map(a =>
105-
a.id === action.payload.id ? { ...a, label: action.payload.label } : a
106-
),
107-
};
108-
}
109-
110-
case 'SET_VIEW_MODE': {
111-
return {
112-
...state,
113-
viewMode: action.payload.mode,
114-
};
115-
}
116-
117-
case 'ADD_CHAT_MESSAGE': {
118-
return {
119-
...state,
120-
chatMessages: [...state.chatMessages, action.payload.message],
121-
};
122-
}
123-
124-
case 'APPEND_CHAT_CHUNK': {
125-
return {
126-
...state,
127-
chatMessages: state.chatMessages.map(m =>
128-
m.id === action.payload.messageId
129-
? { ...m, content: m.content + action.payload.content }
130-
: m
131-
),
132-
};
133-
}
134-
135-
case 'SET_CHAT_LOADING': {
136-
return {
137-
...state,
138-
chatLoading: action.payload.loading,
139-
};
140-
}
141-
142-
case 'SET_CONVERSATIONS': {
143-
return {
144-
...state,
145-
conversations: action.payload.conversations,
146-
};
147-
}
148-
149-
case 'ADD_CONVERSATION': {
150-
return {
151-
...state,
152-
conversations: [action.payload.conversation, ...state.conversations],
153-
activeConversationId: action.payload.conversation.id,
154-
chatMessages: [],
155-
};
156-
}
157-
158-
case 'REMOVE_CONVERSATION': {
159-
const filtered = state.conversations.filter(c => c.id !== action.payload.id);
160-
const wasActive = state.activeConversationId === action.payload.id;
161-
return {
162-
...state,
163-
conversations: filtered,
164-
activeConversationId: wasActive ? (filtered[0]?.id ?? null) : state.activeConversationId,
165-
chatMessages: wasActive ? [] : state.chatMessages,
166-
};
167-
}
168-
169-
case 'RENAME_CONVERSATION': {
170-
return {
171-
...state,
172-
conversations: state.conversations.map(c =>
173-
c.id === action.payload.id ? { ...c, title: action.payload.title } : c
174-
),
175-
};
176-
}
177-
178-
case 'SET_ACTIVE_CONVERSATION': {
179-
return {
180-
...state,
181-
activeConversationId: action.payload.id,
182-
chatMessages: [],
183-
};
184-
}
185-
186-
case 'SET_CHAT_MESSAGES': {
187-
return {
188-
...state,
189-
chatMessages: action.payload.messages,
190-
};
191-
}
192-
193-
case 'SET_AVAILABLE_MODELS': {
194-
return {
195-
...state,
196-
availableModels: action.payload.models,
197-
};
198-
}
199-
200-
case 'SET_SELECTED_MODEL': {
201-
return {
202-
...state,
203-
selectedModel: action.payload.model,
204-
};
205-
}
206-
207-
case 'ADD_AGENT_EVENT': {
208-
const existing = state.agentEvents[action.payload.agentId] ?? [];
209-
return {
210-
...state,
211-
agentEvents: {
212-
...state.agentEvents,
213-
[action.payload.agentId]: [...existing, action.payload.event],
214-
},
215-
};
216-
}
217-
218-
case 'SET_AGENT_STATUS': {
219-
return {
220-
...state,
221-
agents: state.agents.map(a =>
222-
a.id === action.payload.agentId ? { ...a, status: action.payload.status } : a
223-
),
224-
};
225-
}
226-
227-
case 'CLEAR_AGENT_EVENTS': {
228-
return {
229-
...state,
230-
agentEvents: {
231-
...state.agentEvents,
232-
[action.payload.agentId]: [],
233-
},
234-
};
235-
}
236-
237-
case 'SET_AGENT_HAS_SESSION': {
238-
return {
239-
...state,
240-
agents: state.agents.map(a =>
241-
a.id === action.payload.agentId
242-
? { ...a, hasSession: action.payload.hasSession, status: action.payload.hasSession ? 'idle' : undefined }
243-
: a
244-
),
245-
};
246-
}
23+
case 'ADD_AGENT':
24+
case 'REMOVE_AGENT':
25+
case 'SET_ACTIVE_AGENT':
26+
case 'SET_ACTIVE_ITEM':
27+
case 'ADD_FILE':
28+
case 'REMOVE_FILE':
29+
case 'RENAME_AGENT':
30+
case 'SET_VIEW_MODE':
31+
case 'SET_AGENT_STATUS':
32+
case 'SET_AGENT_HAS_SESSION':
33+
case 'ADD_AGENT_EVENT':
34+
case 'CLEAR_AGENT_EVENTS':
35+
return agentReducer(state, action);
36+
37+
case 'ADD_CHAT_MESSAGE':
38+
case 'APPEND_CHAT_CHUNK':
39+
case 'SET_CHAT_LOADING':
40+
case 'SET_CONVERSATIONS':
41+
case 'ADD_CONVERSATION':
42+
case 'REMOVE_CONVERSATION':
43+
case 'RENAME_CONVERSATION':
44+
case 'SET_ACTIVE_CONVERSATION':
45+
case 'SET_CHAT_MESSAGES':
46+
case 'SET_AVAILABLE_MODELS':
47+
case 'SET_SELECTED_MODEL':
48+
return conversationReducer(state, action);
24749

24850
default:
24951
return state;

0 commit comments

Comments
 (0)