Skip to content

Commit ce0555a

Browse files
saagar210claude
andcommitted
refactor(components): extract handleAcceptNextAction to new hook
The inline handleAcceptNextAction callback in DraftTab was a ~90-line switch on NextActionRecommendation.kind (answer, clarify, approval, runbook, escalate, and a default KB-draft-copy fallback) — each branch routed to different workspace side effects through a dense set of state writers and sibling handlers. Extracts to a sibling hook useNextActionHandler with an explicit options interface: state reads, state writers, delegated cross-hook handlers, logEvent, and onShowSuccess. DraftTab's call site collapses from a ~90-line useCallback to a ~17-line hook invocation with named bindings, and the useCallback's full dep array now lives next to the body it memoizes. Removes the last two DraftTab-only imports made dead by the extraction (compactLines, NextActionRecommendation) and keeps parseCaseIntake on its own import line. DraftTab.tsx: 1278 -> 1207 lines (-71). Branch total vs master: 1412 -> 1207 (-205, -14.5%). Also drops one react-hooks/exhaustive-deps warning in DraftTab — the dep array that lint was flagging now lives inside the new hook where it is complete. No behavior change. Typecheck + vitest (241/241) + eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4e1c9d5 commit ce0555a

2 files changed

Lines changed: 175 additions & 91 deletions

File tree

src/components/Draft/DraftTab.tsx

Lines changed: 20 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { useDraftIntake } from "./useDraftIntake";
2828
import { useDraftLifecycle } from "./useDraftLifecycle";
2929
import { useDraftPersistence } from "./useDraftPersistence";
3030
import { useGuidedRunbook } from "./useGuidedRunbook";
31+
import { useNextActionHandler } from "./useNextActionHandler";
3132
import { useResponseActions } from "./useResponseActions";
3233
import { useWorkspaceArtifacts } from "./useWorkspaceArtifacts";
3334
import { useWorkspaceClipboardPacks } from "./useWorkspaceClipboardPacks";
@@ -53,10 +54,7 @@ import { useWorkspaceCatalog } from "../../features/workspace/useWorkspaceCatalo
5354
import { useWorkspaceDerivedArtifacts } from "../../features/workspace/useWorkspaceDerivedArtifacts";
5455
import { useWorkspaceCommandBridge } from "../../features/workspace/useWorkspaceCommandBridge";
5556
import { useWorkspaceDraftState } from "../../features/workspace/useWorkspaceDraftState";
56-
import {
57-
compactLines,
58-
parseCaseIntake,
59-
} from "../../features/workspace/workspaceAssistant";
57+
import { parseCaseIntake } from "../../features/workspace/workspaceAssistant";
6058
import { countWords } from "../../features/analytics/qualityMetrics";
6159
import type { JiraTicket } from "../../hooks/useJira";
6260
import type {
@@ -66,7 +64,6 @@ import type {
6664
} from "../../types/llm";
6765
import type { ContextSource } from "../../types/knowledge";
6866
import type {
69-
NextActionRecommendation,
7067
ResponseLength,
7168
SavedDraft,
7269
SimilarCase,
@@ -813,92 +810,24 @@ export const DraftTab = forwardRef<DraftTabHandle, DraftTabProps>(
813810
],
814811
);
815812

816-
const handleAcceptNextAction = useCallback(
817-
(action: NextActionRecommendation) => {
818-
void logEvent("workspace_next_action_accepted", {
819-
ticket_id: currentTicketId,
820-
action_kind: action.kind,
821-
action_id: action.id,
822-
});
823-
824-
if (action.kind === "answer") {
825-
void handleGenerate();
826-
return;
827-
}
828-
829-
if (action.kind === "clarify") {
830-
const clarifyPrompt = compactLines([
831-
diagnosticNotes,
832-
"Clarifying questions to ask:",
833-
...missingQuestions.map((question) => `- ${question.question}`),
834-
]);
835-
setDiagnosticNotes(clarifyPrompt);
836-
setPanelDensityMode("focus-intake");
837-
showSuccess("Added clarifying questions to the diagnostic notes");
838-
return;
839-
}
840-
841-
if (action.kind === "approval") {
842-
const querySeed = compactLines([
843-
caseIntake.issue,
844-
currentTicket?.summary,
845-
input,
846-
]);
847-
setApprovalQuery(`${querySeed || "support request"} policy approval`);
848-
setPanelDensityMode("focus-intake");
849-
showSuccess("Primed the approval search query");
850-
return;
851-
}
852-
853-
if (action.kind === "runbook") {
854-
setPanelDensityMode("focus-intake");
855-
setDiagnosticNotes((prev) =>
856-
compactLines([
857-
prev,
858-
"Runbook kickoff:",
859-
`- ${action.rationale}`,
860-
...action.prerequisites.map((item) => `- ${item}`),
861-
]),
862-
);
863-
const incidentTemplate = runbookTemplates.find((template) =>
864-
/incident|security/i.test(`${template.name} ${template.scenario}`),
865-
);
866-
if (incidentTemplate) {
867-
void handleStartGuidedRunbook(incidentTemplate.id);
868-
}
869-
showSuccess("Prepared the workspace for guided runbook steps");
870-
return;
871-
}
872-
873-
if (action.kind === "escalate") {
874-
setCaseIntake((prev) => ({
875-
...prev,
876-
note_audience: "escalation-note",
877-
}));
878-
setDiagnosticNotes((prev) =>
879-
compactLines([prev, "Escalation focus:", `- ${action.rationale}`]),
880-
);
881-
showSuccess("Switched the workspace into escalation-note mode");
882-
return;
883-
}
884-
885-
void handleCopyKbDraft();
886-
},
887-
[
888-
logEvent,
889-
currentTicketId,
890-
handleGenerate,
891-
diagnosticNotes,
892-
missingQuestions,
893-
showSuccess,
894-
caseIntake.issue,
895-
currentTicket?.summary,
896-
input,
897-
runbookTemplates,
898-
handleStartGuidedRunbook,
899-
handleCopyKbDraft,
900-
],
901-
);
813+
const handleAcceptNextAction = useNextActionHandler({
814+
currentTicketId,
815+
currentTicketSummary: currentTicket?.summary,
816+
diagnosticNotes,
817+
input,
818+
caseIntakeIssue: caseIntake.issue,
819+
missingQuestions,
820+
runbookTemplates,
821+
setDiagnosticNotes,
822+
setPanelDensityMode,
823+
setApprovalQuery,
824+
setCaseIntake,
825+
handleGenerate,
826+
handleStartGuidedRunbook,
827+
handleCopyKbDraft,
828+
logEvent,
829+
onShowSuccess: showSuccess,
830+
});
902831

903832
useWorkspaceCommandBridge({
904833
enabled: workspaceRailEnabled,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { useCallback } from "react";
2+
import type { Dispatch, SetStateAction } from "react";
3+
import { compactLines } from "../../features/workspace/workspaceAssistant";
4+
import type {
5+
CaseIntake,
6+
GuidedRunbookTemplate,
7+
MissingQuestion,
8+
NextActionRecommendation,
9+
} from "../../types/workspace";
10+
import type { DraftPanelDensityMode } from "./draftTabDefaults";
11+
12+
export interface UseNextActionHandlerOptions {
13+
// State reads
14+
currentTicketId: string | null;
15+
currentTicketSummary: string | undefined;
16+
diagnosticNotes: string;
17+
input: string;
18+
caseIntakeIssue: string | null | undefined;
19+
missingQuestions: MissingQuestion[];
20+
runbookTemplates: GuidedRunbookTemplate[];
21+
22+
// State writers
23+
setDiagnosticNotes: Dispatch<SetStateAction<string>>;
24+
setPanelDensityMode: (mode: DraftPanelDensityMode) => void;
25+
setApprovalQuery: (value: string) => void;
26+
setCaseIntake: Dispatch<SetStateAction<CaseIntake>>;
27+
28+
// Cross-hook handlers we delegate to
29+
handleGenerate: () => unknown;
30+
handleStartGuidedRunbook: (templateId: string) => unknown;
31+
handleCopyKbDraft: () => unknown;
32+
33+
// Observability + UX
34+
logEvent: (event: string, payload?: Record<string, unknown>) => unknown;
35+
onShowSuccess: (message: string) => void;
36+
}
37+
38+
/**
39+
* Handles the user's acceptance of a next-best-action recommendation. Each
40+
* action kind routes to a different workspace side effect — triggering
41+
* generation, primer notes, approval search seed, guided runbook kickoff,
42+
* escalation-note mode, or a default KB draft copy.
43+
*
44+
* Extracted from DraftTab so the ~90-line switch lives next to the hook
45+
* family instead of inflating the orchestrator component.
46+
*/
47+
export function useNextActionHandler({
48+
currentTicketId,
49+
currentTicketSummary,
50+
diagnosticNotes,
51+
input,
52+
caseIntakeIssue,
53+
missingQuestions,
54+
runbookTemplates,
55+
setDiagnosticNotes,
56+
setPanelDensityMode,
57+
setApprovalQuery,
58+
setCaseIntake,
59+
handleGenerate,
60+
handleStartGuidedRunbook,
61+
handleCopyKbDraft,
62+
logEvent,
63+
onShowSuccess,
64+
}: UseNextActionHandlerOptions) {
65+
return useCallback(
66+
(action: NextActionRecommendation) => {
67+
void logEvent("workspace_next_action_accepted", {
68+
ticket_id: currentTicketId,
69+
action_kind: action.kind,
70+
action_id: action.id,
71+
});
72+
73+
if (action.kind === "answer") {
74+
void handleGenerate();
75+
return;
76+
}
77+
78+
if (action.kind === "clarify") {
79+
const clarifyPrompt = compactLines([
80+
diagnosticNotes,
81+
"Clarifying questions to ask:",
82+
...missingQuestions.map((question) => `- ${question.question}`),
83+
]);
84+
setDiagnosticNotes(clarifyPrompt);
85+
setPanelDensityMode("focus-intake");
86+
onShowSuccess("Added clarifying questions to the diagnostic notes");
87+
return;
88+
}
89+
90+
if (action.kind === "approval") {
91+
const querySeed = compactLines([
92+
caseIntakeIssue,
93+
currentTicketSummary,
94+
input,
95+
]);
96+
setApprovalQuery(`${querySeed || "support request"} policy approval`);
97+
setPanelDensityMode("focus-intake");
98+
onShowSuccess("Primed the approval search query");
99+
return;
100+
}
101+
102+
if (action.kind === "runbook") {
103+
setPanelDensityMode("focus-intake");
104+
setDiagnosticNotes((prev) =>
105+
compactLines([
106+
prev,
107+
"Runbook kickoff:",
108+
`- ${action.rationale}`,
109+
...action.prerequisites.map((item) => `- ${item}`),
110+
]),
111+
);
112+
const incidentTemplate = runbookTemplates.find((template) =>
113+
/incident|security/i.test(`${template.name} ${template.scenario}`),
114+
);
115+
if (incidentTemplate) {
116+
void handleStartGuidedRunbook(incidentTemplate.id);
117+
}
118+
onShowSuccess("Prepared the workspace for guided runbook steps");
119+
return;
120+
}
121+
122+
if (action.kind === "escalate") {
123+
setCaseIntake((prev) => ({
124+
...prev,
125+
note_audience: "escalation-note",
126+
}));
127+
setDiagnosticNotes((prev) =>
128+
compactLines([prev, "Escalation focus:", `- ${action.rationale}`]),
129+
);
130+
onShowSuccess("Switched the workspace into escalation-note mode");
131+
return;
132+
}
133+
134+
void handleCopyKbDraft();
135+
},
136+
[
137+
logEvent,
138+
currentTicketId,
139+
handleGenerate,
140+
diagnosticNotes,
141+
missingQuestions,
142+
onShowSuccess,
143+
caseIntakeIssue,
144+
currentTicketSummary,
145+
input,
146+
runbookTemplates,
147+
handleStartGuidedRunbook,
148+
handleCopyKbDraft,
149+
setDiagnosticNotes,
150+
setPanelDensityMode,
151+
setApprovalQuery,
152+
setCaseIntake,
153+
],
154+
);
155+
}

0 commit comments

Comments
 (0)