From dc33131553119394f9f46503374917df981252df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 17 Apr 2026 19:54:16 -0400 Subject: [PATCH] refactor(selection): split selectedAgentId into focus vs edit-modal states; improve Edit modal title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit one piece of state (`selectedAgentId`) did three jobs: roster highlight, navigation destination, and Edit-modal open flag. The three jobs had incompatible lifecycles — highlight should persist across modal close, navigation should land on a screen without opening a modal, and the modal itself only wants to open on explicit intent. The conflation was the root cause of the "Edit: CEO auto-opens on first Team view" bug (commit 41132e2 patched the most visible symptom by not pre-selecting the lead agent, but the coupling remained). Split into two slice fields: - `selectedAgentId` — which agent is currently highlighted in the roster / OrgChart. Survives modal close. Set by: row click, card click, navigation from Inbox / Hiring (all of which pass through `openAgentEditor` which sets both so the highlight follows the last interaction). - `editingAgentId` — which agent's Edit modal is open, or NEW_ENTITY_ID for the "Request new hire" modal. Only set by explicit user action; cleared on modal close. Never set by navigation or onboarding — the pre-select pathway cannot accidentally open a modal again. Plumbing: - `selection.ts`: new `editingAgentId` + `setEditingAgentId`, plus clear on `clearCompanyScopedSelections`. - `useCompanyData.ts`: derive `editingAgent` record from the id. - `useCompanyController.ts`: forward `editingAgent` on the section context. - `OrganizationSections.tsx`: `agents` section modals read `editingAgentId` / `editingAgent`. "Request new hire" button uses `setEditingAgentId(NEW_ENTITY_ID)`. Single-click handlers go through `openAgentEditor(id)` which updates both states. Also improved Edit modal title (was "Edit: CEO", a machined English prefix on a Chinese UI with zero context about the role): - zh: "编辑 {name}({role})" - en: "Edit {name} ({role})" Renders as "编辑 CEO(首席执行官)" in the screenshot example. Covers J9 / S2. --- src/renderer/hooks/useCompanyController.ts | 1 + src/renderer/hooks/useCompanyData.ts | 7 ++++ src/renderer/i18n/en.ts | 1 + src/renderer/i18n/zh.ts | 1 + src/renderer/state/slices/selection.ts | 14 +++++++ .../views/sections/OrganizationSections.tsx | 38 +++++++++++++------ 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/renderer/hooks/useCompanyController.ts b/src/renderer/hooks/useCompanyController.ts index fd053463..9a79bdbd 100644 --- a/src/renderer/hooks/useCompanyController.ts +++ b/src/renderer/hooks/useCompanyController.ts @@ -181,6 +181,7 @@ export function useCompanyController() { selectedApproval: companyData.selectedApproval, selectedRun: companyData.selectedRun, selectedAgent: companyData.selectedAgent, + editingAgent: companyData.editingAgent, selectedGoal: companyData.selectedGoal, selectedProject: companyData.selectedProject, selectedWorkspace: companyData.selectedWorkspace, diff --git a/src/renderer/hooks/useCompanyData.ts b/src/renderer/hooks/useCompanyData.ts index c458fa26..ac825110 100644 --- a/src/renderer/hooks/useCompanyData.ts +++ b/src/renderer/hooks/useCompanyData.ts @@ -19,6 +19,7 @@ export function useCompanyData( const selectedTaskId = useAppStore((s) => s.selectedTaskId); const selectedApprovalId = useAppStore((s) => s.selectedApprovalId); const selectedAgentId = useAppStore((s) => s.selectedAgentId); + const editingAgentId = useAppStore((s) => s.editingAgentId); const selectedGoalId = useAppStore((s) => s.selectedGoalId); const selectedProjectId = useAppStore((s) => s.selectedProjectId); const selectedWorkspaceId = useAppStore((s) => s.selectedWorkspaceId); @@ -58,6 +59,10 @@ export function useCompanyData( () => companyAgents.find((a) => a.id === selectedAgentId) ?? null, [companyAgents, selectedAgentId], ); + const editingAgent = useMemo( + () => companyAgents.find((a) => a.id === editingAgentId) ?? null, + [companyAgents, editingAgentId], + ); const selectedGoal = useMemo( () => companyGoals.find((g) => g.id === selectedGoalId) ?? null, [companyGoals, selectedGoalId], @@ -104,6 +109,7 @@ export function useCompanyData( selectedApproval, selectedRun, selectedAgent, + editingAgent, selectedGoal, selectedProject, selectedWorkspace, @@ -128,6 +134,7 @@ export function useCompanyData( selectedApproval, selectedRun, selectedAgent, + editingAgent, selectedGoal, selectedProject, selectedWorkspace, diff --git a/src/renderer/i18n/en.ts b/src/renderer/i18n/en.ts index 70547af2..e6395b8c 100644 --- a/src/renderer/i18n/en.ts +++ b/src/renderer/i18n/en.ts @@ -342,6 +342,7 @@ export const en: Record = { "org.agentRoster": "Agent roster", "org.newHireRequest": "New hire request", "org.editAgent": "Edit", + "org.editAgentTitle": "Edit {name} ({role})", "org.requestNewHire": "Request new hire", "org.goalStack": "Goal stack", "org.newGoal": "New goal", diff --git a/src/renderer/i18n/zh.ts b/src/renderer/i18n/zh.ts index 29c398f5..9007e1d5 100644 --- a/src/renderer/i18n/zh.ts +++ b/src/renderer/i18n/zh.ts @@ -342,6 +342,7 @@ export const zh: Record = { "org.agentRoster": "智能体名册", "org.newHireRequest": "新招聘申请", "org.editAgent": "编辑", + "org.editAgentTitle": "编辑 {name}({role})", "org.requestNewHire": "申请新招聘", "org.goalStack": "目标栈", "org.newGoal": "新目标", diff --git a/src/renderer/state/slices/selection.ts b/src/renderer/state/slices/selection.ts index 5b78d614..b9d9dfb0 100644 --- a/src/renderer/state/slices/selection.ts +++ b/src/renderer/state/slices/selection.ts @@ -6,7 +6,14 @@ export interface SelectionSlice { selectedConnectorId: string | null; selectedApprovalId: string | null; selectedRunId: string | null; + /** Highlighted agent (roster row / orgchart card). Navigation target. + * Does NOT control the Edit modal — see editingAgentId. */ selectedAgentId: string | null; + /** Agent whose Edit modal is currently open, or NEW_ENTITY_ID for the + * new-hire request modal. Only set by explicit user action (row click, + * card click, "Request new hire" button); never by navigation or + * onboarding defaults. Cleared on modal close. */ + editingAgentId: string | null; selectedGoalId: string | null; selectedProjectId: string | null; selectedWorkspaceId: string | null; @@ -16,6 +23,7 @@ export interface SelectionSlice { setSelectedApprovalId: (id: string | null) => void; setSelectedRunId: (id: string | null) => void; setSelectedAgentId: (id: string | null) => void; + setEditingAgentId: (id: string | null) => void; setSelectedGoalId: (id: string | null) => void; setSelectedProjectId: (id: string | null) => void; setSelectedWorkspaceId: (id: string | null) => void; @@ -34,6 +42,7 @@ export const createSelectionSlice: StateCreator< selectedApprovalId: null, selectedRunId: null, selectedAgentId: null, + editingAgentId: null, selectedGoalId: null, selectedProjectId: null, selectedWorkspaceId: null, @@ -58,6 +67,10 @@ export const createSelectionSlice: StateCreator< set((state) => { state.selectedAgentId = id; }), + setEditingAgentId: (id) => + set((state) => { + state.editingAgentId = id; + }), setSelectedGoalId: (id) => set((state) => { state.selectedGoalId = id; @@ -80,6 +93,7 @@ export const createSelectionSlice: StateCreator< state.selectedApprovalId = null; state.selectedRunId = null; state.selectedAgentId = null; + state.editingAgentId = null; state.selectedGoalId = null; state.selectedProjectId = null; state.selectedWorkspaceId = null; diff --git a/src/renderer/views/sections/OrganizationSections.tsx b/src/renderer/views/sections/OrganizationSections.tsx index 8fa0b104..dd6cf631 100644 --- a/src/renderer/views/sections/OrganizationSections.tsx +++ b/src/renderer/views/sections/OrganizationSections.tsx @@ -35,7 +35,7 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) { companyGoals, companyProjects, companyWorkspaces, - selectedAgent, + editingAgent, selectedGoal, selectedProject, } = useSectionContext(); @@ -43,53 +43,67 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) { const selectedAgentId = useAppStore((s) => s.selectedAgentId); const setSelectedAgentId = useAppStore((s) => s.setSelectedAgentId); + const editingAgentId = useAppStore((s) => s.editingAgentId); + const setEditingAgentId = useAppStore((s) => s.setEditingAgentId); const selectedGoalId = useAppStore((s) => s.selectedGoalId); const setSelectedGoalId = useAppStore((s) => s.setSelectedGoalId); const selectedProjectId = useAppStore((s) => s.selectedProjectId); const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId); const setSection = useAppStore((s) => s.setSection); + // Open the Edit modal on row / card click. `selectedAgentId` is updated + // alongside so the roster highlight follows the user's last interaction + // even after the modal closes. + const openAgentEditor = (id: string) => { + setSelectedAgentId(id); + setEditingAgentId(id); + }; + if (!snapshot || !currentCompany) return null; return ( <> {section === "agents" ? ( <> - setSelectedAgentId(id)} /> - setSelectedAgentId(NEW_ENTITY_ID)} />}> + + setEditingAgentId(NEW_ENTITY_ID)} />}> - setSelectedAgentId(null)}> + setEditingAgentId(null)}> { - setSelectedAgentId(null); + setEditingAgentId(null); setSection("hiring"); await refresh(); }} /> - setSelectedAgentId(null)}> - {selectedAgent ? ( + setEditingAgentId(null)} + > + {editingAgent ? ( setSelectedAgentId(null)} - onDeleted={async () => { setSelectedAgentId(null); await refresh(); }} + onCancel={() => setEditingAgentId(null)} + onDeleted={async () => { setEditingAgentId(null); await refresh(); }} /> ) : null}