diff --git a/src/renderer/src/app/App.tsx b/src/renderer/src/app/App.tsx index 9b28536a..d2c00674 100644 --- a/src/renderer/src/app/App.tsx +++ b/src/renderer/src/app/App.tsx @@ -18,15 +18,21 @@ export function App() { const { snapshot, actions } = useSnapshot(); const [route, setRoute] = useState("home"); const avatarDirectory = useMemo(() => { + // Scope the directory to the active organization: names are only unique + // within one org, and a same-named principal in another org must not + // hijack the avatar shown here. const directory = new Map(); + const orgId = snapshot?.activeOrgId ?? null; for (const agent of snapshot?.agents ?? []) { + if (agent.orgId !== orgId) continue; if (agent.avatarEmoji || agent.avatarColor) directory.set(agent.name, { emoji: agent.avatarEmoji, color: agent.avatarColor }); } for (const employee of snapshot?.employees ?? []) { + if (employee.orgId !== orgId) continue; if (employee.avatarEmoji || employee.avatarColor) directory.set(employee.displayName, { emoji: employee.avatarEmoji, color: employee.avatarColor }); } return directory; - }, [snapshot?.agents, snapshot?.employees]); + }, [snapshot?.agents, snapshot?.employees, snapshot?.activeOrgId]); const [creatingOrg, setCreatingOrg] = useState(false); // Keeps the wizard mounted through its post-bootstrap connectors step: the // organization exists from Launch onward, so the org-count check alone diff --git a/src/renderer/src/app/useSnapshot.ts b/src/renderer/src/app/useSnapshot.ts index 8f736466..de20e5f3 100644 --- a/src/renderer/src/app/useSnapshot.ts +++ b/src/renderer/src/app/useSnapshot.ts @@ -31,7 +31,20 @@ export function useSnapshot(): { useEffect(() => { let disposed = false; let timer: ReturnType | null = null; + let retryTimer: ReturnType | null = null; let lastLoadAt = 0; + // A failed load must not strand the app on a blank shell: log it loudly + // and retry until the main process answers (IPC hiccups are transient). + const guardedLoad = () => { + load().catch((error) => { + console.error(`[snapshot] load failed, retrying: ${error instanceof Error ? error.message : String(error)}`); + if (disposed || retryTimer) return; + retryTimer = setTimeout(() => { + retryTimer = null; + guardedLoad(); + }, 1_500); + }); + }; // Change events arrive in bursts while agents run; reload at most every // 800ms with a trailing load so the UI converges without render storms. const scheduleReload = () => { @@ -40,12 +53,12 @@ export function useSnapshot(): { timer = setTimeout(() => { timer = null; lastLoadAt = Date.now(); - void load(); + guardedLoad(); }, wait); }; void Promise.resolve().then(() => { lastLoadAt = Date.now(); - return load(); + guardedLoad(); }); const unsubscribe = window.agentCompany.subscribe((event) => { if (event.type === "snapshot-changed") scheduleReload(); @@ -53,6 +66,7 @@ export function useSnapshot(): { return () => { disposed = true; if (timer) clearTimeout(timer); + if (retryTimer) clearTimeout(retryTimer); unsubscribe(); }; }, [load]); diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index 8f2e1e44..68911c59 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -124,6 +124,7 @@ export const zh: Record = { "Edit task": "编辑任务", "Edit {name}": "编辑 {name}", "Edit profile": "编辑个人资料", + "Former member": "已离职成员", "Whip": "鞭子", "Office tools": "办公室工具", "Crack the whip: slackers in the snack bar or restroom hustle back to their desks. Working or meeting agents are unaffected.": "挥动鞭子:在茶水间或洗手间摸鱼的成员会赶回工位;正在工作或开会的成员不受影响。", diff --git a/src/renderer/src/pages/ChatPage.tsx b/src/renderer/src/pages/ChatPage.tsx index 5e3a349e..c6d4fc53 100644 --- a/src/renderer/src/pages/ChatPage.tsx +++ b/src/renderer/src/pages/ChatPage.tsx @@ -61,6 +61,11 @@ export function ChatPage({ snapshot, actions }: PageProps) { const activeSpaceId = selectedSpaceId && spaces.some((space) => space.id === selectedSpaceId) ? selectedSpaceId : firstAvailableSpaceId; + // Pin the implicit default as soon as one exists (render-time state + // adjustment): the conversation list re-sorts by recency on every + // snapshot, and an unpinned default would follow it - yanking the view to + // whichever space just got a message and marking it read unseen. + if (!selectedSpaceId && firstAvailableSpaceId) setSelectedSpaceId(firstAvailableSpaceId); const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? null; const latestActiveMessageId = useMemo(() => { if (!activeSpaceId) return null; @@ -181,6 +186,7 @@ export function ChatPage({ snapshot, actions }: PageProps) { /> item.id === mention.targetId); - if (!employee) throw new Error("Message mention employee target is missing."); - return employee.displayName; + return employee ? employee.displayName : t("Former member"); } if (mention.targetType === "agent") { const agent = context.agents.find((item) => item.id === mention.targetId); - if (!agent) throw new Error("Message mention agent target is missing."); - return agent.name; + return agent ? agent.name : t("Former member"); } if (mention.targetType === "board") return t("Board"); if (mention.targetType === "org_group" && mention.targetId.endsWith(":oncall")) return t("OnCall"); - throw new Error("Message mention target type is unsupported."); + return t("Former member"); } function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRecord; handoff?: boolean; onOpen: (() => void) | null }) { diff --git a/src/renderer/src/pages/chat/chat-helpers.ts b/src/renderer/src/pages/chat/chat-helpers.ts index 0bf8cf50..84e68b58 100644 --- a/src/renderer/src/pages/chat/chat-helpers.ts +++ b/src/renderer/src/pages/chat/chat-helpers.ts @@ -17,15 +17,16 @@ export interface MessageContext { } export function senderName(message: ChatMessageRecord, context: MessageContext): string { + // A message whose principal no longer resolves must degrade to a visible + // placeholder, not throw: one orphaned row would otherwise blank the whole + // Messages page mid-render. if (message.senderType === "employee") { const employee = context.employees.find((item) => item.id === message.senderId); - if (!employee) throw new Error("Message employee sender is missing."); - return employee.displayName; + return employee ? employee.displayName : t("Former member"); } if (message.senderType === "agent") { const agent = context.agents.find((item) => item.id === message.senderId); - if (!agent) throw new Error("Message agent sender is missing."); - return agent.name; + return agent ? agent.name : t("Former member"); } if (message.senderType === "board") return t("Board"); return t("System");