From be0ff5a04131c5a6ba3f4fd352298194b86f1ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 22:54:20 -0400 Subject: [PATCH] lib(renderer): add safeJson helper; migrate 6 JSON.parse try/catch call sites --- .../components/communication/MessageItem.tsx | 8 ++----- .../components/company-operations/helpers.ts | 20 +++++----------- .../execution/SocialDraftPreview.tsx | 24 +++++++++---------- .../approval-inspector/useApprovalPayload.ts | 13 ++++------ .../components/execution/execution-utils.tsx | 7 +++--- .../components/hiring/HiringPipeline.tsx | 4 +++- .../components/organization/AgentEditor.tsx | 10 ++++++-- src/renderer/lib/safe-json.ts | 8 +++++++ 8 files changed, 46 insertions(+), 48 deletions(-) create mode 100644 src/renderer/lib/safe-json.ts diff --git a/src/renderer/components/communication/MessageItem.tsx b/src/renderer/components/communication/MessageItem.tsx index c859e292..2e85bf98 100644 --- a/src/renderer/components/communication/MessageItem.tsx +++ b/src/renderer/components/communication/MessageItem.tsx @@ -11,6 +11,7 @@ import { } from "@phosphor-icons/react" import type { AgentMessageRecord, AgentRecord, CommentRecord, DepartmentId } from "@shared/types" import { getAgentInitials, formatMessageTime } from "../../lib/formatters" +import { safeJson } from "../../lib/safe-json" import { DEPARTMENT_COLORS } from "@shared/constants" const HUMAN_SENDER_ID = "__board__"; @@ -196,12 +197,7 @@ export function TaskCommentBubble({ } function AttachmentList({ json }: { json: string }) { - let items: Array<{ type: string; id: string; title: string }> = [] - try { - items = JSON.parse(json) - } catch { - return null - } + const items = safeJson>(json, []) if (!items.length) return null return ( diff --git a/src/renderer/components/company-operations/helpers.ts b/src/renderer/components/company-operations/helpers.ts index bbd91e01..9c3f5f3e 100644 --- a/src/renderer/components/company-operations/helpers.ts +++ b/src/renderer/components/company-operations/helpers.ts @@ -60,13 +60,10 @@ export function splitTags(value: string) { return JSON.stringify(value.split(",").map((item) => item.trim()).filter(Boolean)); } +import { safeJson } from "../../lib/safe-json"; + export function parseTags(value: string) { - try { - const tags = JSON.parse(value) as string[]; - return tags.join(", "); - } catch { - return ""; - } + return safeJson(value, []).join(", "); } export function parseParticipantIds(value: string) { @@ -79,12 +76,7 @@ export function parseParticipantIds(value: string) { import type { AgentRecord } from "@shared/types"; export function formatParticipantIds(raw: string, agents: AgentRecord[]) { - try { - const ids = JSON.parse(raw) as string[]; - return ids - .map((id) => agents.find((agent) => agent.id === id)?.name ?? id.slice(0, 8)) - .join(", "); - } catch { - return ""; - } + return safeJson(raw, []) + .map((id) => agents.find((agent) => agent.id === id)?.name ?? id.slice(0, 8)) + .join(", "); } diff --git a/src/renderer/components/execution/SocialDraftPreview.tsx b/src/renderer/components/execution/SocialDraftPreview.tsx index 6a7da96c..30219d7f 100644 --- a/src/renderer/components/execution/SocialDraftPreview.tsx +++ b/src/renderer/components/execution/SocialDraftPreview.tsx @@ -1,6 +1,7 @@ import { Clock, Link as LinkIcon } from "@phosphor-icons/react" import { platformConfig } from "../social/social-config" import type { SocialPlatform } from "@shared/types" +import { safeJson } from "../../lib/safe-json" const PLATFORM_CHAR_LIMITS: Record = { twitter: 280, @@ -26,19 +27,16 @@ interface DraftPayload { } function parseDraft(payloadJson: string): DraftPayload | null { - try { - const parsed = JSON.parse(payloadJson) - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - return { - content: typeof parsed.content === "string" ? parsed.content : "", - mediaUrls: Array.isArray(parsed.mediaUrls) ? parsed.mediaUrls : [], - scheduledAt: typeof parsed.scheduledAt === "string" ? parsed.scheduledAt : null, - socialAccountId: typeof parsed.socialAccountId === "string" ? parsed.socialAccountId : "", - platform: typeof parsed.platform === "string" ? parsed.platform : "other", - } - } - } catch { /* invalid JSON */ } - return null + const parsed = safeJson(payloadJson, null) + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null + const obj = parsed as Record + return { + content: typeof obj.content === "string" ? obj.content : "", + mediaUrls: Array.isArray(obj.mediaUrls) ? (obj.mediaUrls as string[]) : [], + scheduledAt: typeof obj.scheduledAt === "string" ? obj.scheduledAt : null, + socialAccountId: typeof obj.socialAccountId === "string" ? obj.socialAccountId : "", + platform: typeof obj.platform === "string" ? obj.platform : "other", + } } function formatScheduledDate(isoDate: string): string { diff --git a/src/renderer/components/execution/approval-inspector/useApprovalPayload.ts b/src/renderer/components/execution/approval-inspector/useApprovalPayload.ts index d7d0c6ce..3253b893 100644 --- a/src/renderer/components/execution/approval-inspector/useApprovalPayload.ts +++ b/src/renderer/components/execution/approval-inspector/useApprovalPayload.ts @@ -1,18 +1,15 @@ import { useMemo } from "react" import type { ApprovalRecord } from "@shared/types" +import { safeJson } from "../../../lib/safe-json" export function useApprovalPayload(approval: ApprovalRecord): Record | null { return useMemo(() => { if (!approval.payloadJson) return null - try { - const parsed = JSON.parse(approval.payloadJson) - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { - return parsed as Record - } - return null - } catch { - return null + const parsed = safeJson(approval.payloadJson, null) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record } + return null }, [approval.payloadJson]) } diff --git a/src/renderer/components/execution/execution-utils.tsx b/src/renderer/components/execution/execution-utils.tsx index 4ed07c42..fbea6d66 100644 --- a/src/renderer/components/execution/execution-utils.tsx +++ b/src/renderer/components/execution/execution-utils.tsx @@ -2,6 +2,7 @@ import type { ApprovalRecord } from "@shared/types" import { Robot, CheckSquare, Play, ShieldCheck, Gear, FolderOpen, Key, Target, Briefcase, Buildings, } from "@phosphor-icons/react" +import { safeJson } from "../../lib/safe-json" export function describeActionError(error: unknown) { return error instanceof Error ? error.message : "The action could not be completed." @@ -9,10 +10,8 @@ export function describeActionError(error: unknown) { export function parseApprovalPayloadJson(approval: ApprovalRecord): Record | null { if (!approval.payloadJson) return null - try { - const parsed = JSON.parse(approval.payloadJson) - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record - } catch { return null } + const parsed = safeJson(approval.payloadJson, null) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record return null } diff --git a/src/renderer/components/hiring/HiringPipeline.tsx b/src/renderer/components/hiring/HiringPipeline.tsx index 841dbdcc..73f19a75 100644 --- a/src/renderer/components/hiring/HiringPipeline.tsx +++ b/src/renderer/components/hiring/HiringPipeline.tsx @@ -9,6 +9,7 @@ import type { AgentRecord, ApprovalRecord } from "@shared/types"; import { useT } from "../../i18n"; import { getAgentInitials } from "../../lib/formatters"; import { unwrap } from "../../lib/desktop"; +import { safeJson } from "../../lib/safe-json"; import { ActionButton, EmptyState, Panel, StatusPill } from "../ui"; import { type HiringStage, @@ -44,7 +45,8 @@ export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent const approval = hireApprovals.find(a => { if (a.relatedAgentId === agent.id) return true; if (!a.payloadJson) return false; - try { return JSON.parse(a.payloadJson).agentId === agent.id; } catch { return false; } + const payload = safeJson<{ agentId?: string }>(a.payloadJson, {}); + return payload.agentId === agent.id; }) ?? null; return { agent, diff --git a/src/renderer/components/organization/AgentEditor.tsx b/src/renderer/components/organization/AgentEditor.tsx index 53752ded..423c1a01 100644 --- a/src/renderer/components/organization/AgentEditor.tsx +++ b/src/renderer/components/organization/AgentEditor.tsx @@ -4,6 +4,7 @@ import { DEPARTMENT_OPTIONS } from "@shared/constants"; import type { AgentRecord, BudgetPolicyRecord, ConnectorRecord, DepartmentId, WorkspaceRecord } from "@shared/types"; import { getConnectorExecutionReadinessIssue, isConnectorExecutionReady } from "@shared/connector-policy"; import { unwrap } from "../../lib/desktop"; +import { safeJson } from "../../lib/safe-json"; import { buildBudgetPolicyInput, getBudgetPolicyAmountDollars } from "../../lib/budget-policy-input"; import { useAppStore } from "../../state/store"; import { useT } from "../../i18n"; @@ -43,8 +44,13 @@ export function AgentEditor({ const [reportsTo, setReportsTo] = useState(agent.reportsTo ?? ""); const [confirmingDelete, setConfirmingDelete] = useState(false); const [saveError, setSaveError] = useState(null); - const existingMeta = useMemo(() => { try { return JSON.parse(agent.metadataJson || "{}"); } catch { return {}; } }, [agent.metadataJson]); - const [thinkingEffort, setThinkingEffort] = useState(existingMeta.thinkingEffort ?? ""); + const existingMeta = useMemo( + () => safeJson>(agent.metadataJson, {}), + [agent.metadataJson], + ); + const [thinkingEffort, setThinkingEffort] = useState( + typeof existingMeta.thinkingEffort === "string" ? existingMeta.thinkingEffort : "", + ); const supportsEffort = connectorId === "claude_local" || connectorId === "codex_local"; const pendingApproval = agent.status === "pending_approval"; const executionReadyConnectors = useMemo( diff --git a/src/renderer/lib/safe-json.ts b/src/renderer/lib/safe-json.ts new file mode 100644 index 00000000..153cb536 --- /dev/null +++ b/src/renderer/lib/safe-json.ts @@ -0,0 +1,8 @@ +export function safeJson(text: string | null | undefined, fallback: T): T { + if (text == null || text === "") return fallback; + try { + return JSON.parse(text) as T; + } catch { + return fallback; + } +}