diff --git a/src/features/workspace/IntakeFieldControl.test.tsx b/src/features/workspace/IntakeFieldControl.test.tsx
new file mode 100644
index 0000000..259fc25
--- /dev/null
+++ b/src/features/workspace/IntakeFieldControl.test.tsx
@@ -0,0 +1,54 @@
+// @vitest-environment jsdom
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { IntakeFieldControl } from "./IntakeFieldControl";
+
+describe("IntakeFieldControl", () => {
+ afterEach(() => cleanup());
+
+ it("renders a single-line text input when no rows prop is provided", () => {
+ render(
+ ,
+ );
+
+ const input = screen.getByLabelText("Issue summary") as HTMLInputElement;
+ expect(input.tagName).toBe("INPUT");
+ expect(input.type).toBe("text");
+ expect(input.value).toBe("VPN outage in west region");
+ });
+
+ it("renders a multi-line textarea when rows > 1 and reports new values through onChange", () => {
+ const onChange = vi.fn();
+ render(
+ ,
+ );
+
+ const textarea = screen.getByLabelText("Symptoms") as HTMLTextAreaElement;
+ expect(textarea.tagName).toBe("TEXTAREA");
+ expect(textarea.rows).toBe(3);
+
+ fireEvent.change(textarea, { target: { value: "updated symptoms" } });
+ expect(onChange).toHaveBeenCalledWith("updated symptoms");
+ });
+
+ it("fires onChange with the latest value on single-line edits", () => {
+ const onChange = vi.fn();
+ render(
+ ,
+ );
+
+ fireEvent.change(screen.getByLabelText("Affected user"), {
+ target: { value: "alice@example.com" },
+ });
+ expect(onChange).toHaveBeenCalledWith("alice@example.com");
+ });
+});
diff --git a/src/features/workspace/IntakeFieldControl.tsx b/src/features/workspace/IntakeFieldControl.tsx
new file mode 100644
index 0000000..bbf1dc4
--- /dev/null
+++ b/src/features/workspace/IntakeFieldControl.tsx
@@ -0,0 +1,74 @@
+import type { NoteAudience } from "../../types/workspace";
+
+export type IntakeField =
+ | "issue"
+ | "environment"
+ | "impact"
+ | "affected_user"
+ | "affected_system"
+ | "affected_site"
+ | "symptoms"
+ | "steps_tried"
+ | "blockers"
+ | "likely_category";
+
+export const NOTE_AUDIENCES: Array<{ id: NoteAudience; label: string }> = [
+ { id: "internal-note", label: "Internal note" },
+ { id: "customer-safe", label: "Customer-safe" },
+ { id: "escalation-note", label: "Escalation note" },
+];
+
+export const INTAKE_FIELDS: Array<{
+ key: IntakeField;
+ label: string;
+ rows?: number;
+}> = [
+ { key: "issue", label: "Issue summary" },
+ { key: "environment", label: "Environment" },
+ { key: "impact", label: "Impact", rows: 2 },
+ { key: "affected_user", label: "Affected user" },
+ { key: "affected_system", label: "Affected system" },
+ { key: "affected_site", label: "Affected site" },
+ { key: "symptoms", label: "Symptoms", rows: 3 },
+ { key: "steps_tried", label: "Steps already tried", rows: 3 },
+ { key: "blockers", label: "Current blocker", rows: 2 },
+ { key: "likely_category", label: "Likely category" },
+];
+
+interface IntakeFieldControlProps {
+ label: string;
+ value: string;
+ rows?: number;
+ onChange: (value: string) => void;
+}
+
+export function IntakeFieldControl({
+ label,
+ value,
+ rows,
+ onChange,
+}: IntakeFieldControlProps) {
+ if (rows && rows > 1) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/features/workspace/TicketWorkspaceRail.tsx b/src/features/workspace/TicketWorkspaceRail.tsx
index dc50b54..d135297 100644
--- a/src/features/workspace/TicketWorkspaceRail.tsx
+++ b/src/features/workspace/TicketWorkspaceRail.tsx
@@ -16,32 +16,15 @@ import type {
WorkspaceFavorite,
WorkspacePersonalization,
} from "../../types/workspace";
+import { markWorkspaceReady } from "./workspacePerformanceMetrics";
+import {
+ INTAKE_FIELDS,
+ IntakeFieldControl,
+ NOTE_AUDIENCES,
+ type IntakeField,
+} from "./IntakeFieldControl";
import "./TicketWorkspaceRail.css";
-const APP_BOOTSTRAP_START_MARK = "assistsupport:perf:bootstrap-start";
-const TICKET_WORKSPACE_READY_MARK = "assistsupport:perf:ticket-workspace-ready";
-const TICKET_WORKSPACE_READY_MEASURE =
- "assistsupport:perf:ticket-workspace-ready-ms";
-
-type PerfWindow = Window & {
- __assistsupportPerf?: {
- bootstrapStartedAt: number;
- ticketWorkspaceReadyMs?: number;
- };
-};
-
-type IntakeField =
- | "issue"
- | "environment"
- | "impact"
- | "affected_user"
- | "affected_system"
- | "affected_site"
- | "symptoms"
- | "steps_tried"
- | "blockers"
- | "likely_category";
-
interface TicketWorkspaceRailProps {
intake: CaseIntake;
onIntakeChange: (field: IntakeField, value: string) => void;
@@ -90,62 +73,6 @@ interface TicketWorkspaceRailProps {
currentResponse: string;
}
-const NOTE_AUDIENCES: Array<{ id: NoteAudience; label: string }> = [
- { id: "internal-note", label: "Internal note" },
- { id: "customer-safe", label: "Customer-safe" },
- { id: "escalation-note", label: "Escalation note" },
-];
-
-const INTAKE_FIELDS: Array<{ key: IntakeField; label: string; rows?: number }> =
- [
- { key: "issue", label: "Issue summary" },
- { key: "environment", label: "Environment" },
- { key: "impact", label: "Impact", rows: 2 },
- { key: "affected_user", label: "Affected user" },
- { key: "affected_system", label: "Affected system" },
- { key: "affected_site", label: "Affected site" },
- { key: "symptoms", label: "Symptoms", rows: 3 },
- { key: "steps_tried", label: "Steps already tried", rows: 3 },
- { key: "blockers", label: "Current blocker", rows: 2 },
- { key: "likely_category", label: "Likely category" },
- ];
-
-function IntakeFieldControl({
- label,
- value,
- rows,
- onChange,
-}: {
- label: string;
- value: string;
- rows?: number;
- onChange: (value: string) => void;
-}) {
- if (rows && rows > 1) {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
export function TicketWorkspaceRail({
intake,
onIntakeChange,
@@ -209,35 +136,7 @@ export function TicketWorkspaceRail({
}, [guidedRunbookSession]);
useEffect(() => {
- if (
- typeof window === "undefined" ||
- typeof window.performance === "undefined"
- ) {
- return;
- }
-
- const perfWindow = window as PerfWindow;
- const bootstrapStartedAt =
- perfWindow.__assistsupportPerf?.bootstrapStartedAt ?? 0;
- perfWindow.__assistsupportPerf = {
- ...(perfWindow.__assistsupportPerf ?? { bootstrapStartedAt }),
- bootstrapStartedAt,
- ticketWorkspaceReadyMs: Number(
- (window.performance.now() - bootstrapStartedAt).toFixed(2),
- ),
- };
- window.performance.clearMarks(TICKET_WORKSPACE_READY_MARK);
- window.performance.clearMeasures(TICKET_WORKSPACE_READY_MEASURE);
- window.performance.mark(TICKET_WORKSPACE_READY_MARK);
- try {
- window.performance.measure(
- TICKET_WORKSPACE_READY_MEASURE,
- APP_BOOTSTRAP_START_MARK,
- TICKET_WORKSPACE_READY_MARK,
- );
- } catch {
- // Keep the workspace usable even if performance marks are unavailable in the current runtime.
- }
+ markWorkspaceReady();
}, []);
return (
diff --git a/src/features/workspace/workspacePerformanceMetrics.ts b/src/features/workspace/workspacePerformanceMetrics.ts
new file mode 100644
index 0000000..e8da221
--- /dev/null
+++ b/src/features/workspace/workspacePerformanceMetrics.ts
@@ -0,0 +1,44 @@
+export const APP_BOOTSTRAP_START_MARK = "assistsupport:perf:bootstrap-start";
+export const TICKET_WORKSPACE_READY_MARK =
+ "assistsupport:perf:ticket-workspace-ready";
+export const TICKET_WORKSPACE_READY_MEASURE =
+ "assistsupport:perf:ticket-workspace-ready-ms";
+
+type PerfWindow = Window & {
+ __assistsupportPerf?: {
+ bootstrapStartedAt: number;
+ ticketWorkspaceReadyMs?: number;
+ };
+};
+
+export function markWorkspaceReady(): void {
+ if (
+ typeof window === "undefined" ||
+ typeof window.performance === "undefined"
+ ) {
+ return;
+ }
+
+ const perfWindow = window as PerfWindow;
+ const bootstrapStartedAt =
+ perfWindow.__assistsupportPerf?.bootstrapStartedAt ?? 0;
+ perfWindow.__assistsupportPerf = {
+ ...(perfWindow.__assistsupportPerf ?? { bootstrapStartedAt }),
+ bootstrapStartedAt,
+ ticketWorkspaceReadyMs: Number(
+ (window.performance.now() - bootstrapStartedAt).toFixed(2),
+ ),
+ };
+ window.performance.clearMarks(TICKET_WORKSPACE_READY_MARK);
+ window.performance.clearMeasures(TICKET_WORKSPACE_READY_MEASURE);
+ window.performance.mark(TICKET_WORKSPACE_READY_MARK);
+ try {
+ window.performance.measure(
+ TICKET_WORKSPACE_READY_MEASURE,
+ APP_BOOTSTRAP_START_MARK,
+ TICKET_WORKSPACE_READY_MARK,
+ );
+ } catch {
+ // Keep the workspace usable even if performance marks are unavailable in the current runtime.
+ }
+}