diff --git a/apps/web/src/auto-workbench-title.ts b/apps/web/src/auto-workbench-title.ts index 93346b49f..27ad38b77 100644 --- a/apps/web/src/auto-workbench-title.ts +++ b/apps/web/src/auto-workbench-title.ts @@ -6,22 +6,31 @@ export const NEW_WORKBENCH_TITLE = "New Workbench"; /** Sidebar-friendly cap — long enough for a goal phrase, short enough to scan. */ -export const AUTO_WORKBENCH_TITLE_MAX = 48; +export const AUTO_WORKBENCH_TITLE_MAX = 40; -// Truncates at a word boundary when the cut would land mid-word past -// halfway, appending an ellipsis. +const LEADING_FILLER = + /^(?:(?:hey|hi|hello|so|ok|okay)[,\s]+)?(?:(?:can|could|would|will)\s+you\s+|please\s+|pls\s+|i(?:'d| would)? (?:like|want|need) (?:you )?to\s+)+(?:please\s+)?/iu; + +// First sentence or clause, filler stripped, first letter capitalised, cut at +// a word boundary with an ellipsis when it still exceeds the cap. export function titleFromFirstMessage( message: string, maxLength: number = AUTO_WORKBENCH_TITLE_MAX, ): string | undefined { const collapsed = message.trim().replace(/\s+/g, " "); - if (collapsed.length === 0) return undefined; - if (collapsed.length <= maxLength) return collapsed; + const clause = (collapsed.split(/[.!?:;]+(?:\s|$)|\s[-—–]\s/u)[0] ?? "").replace( + LEADING_FILLER, + "", + ); + const cleaned = clause.replace(/[\s.,;:!?]+$/u, ""); + if (cleaned.length === 0) return undefined; + const title = cleaned.charAt(0).toLocaleUpperCase() + cleaned.slice(1); + if (title.length <= maxLength) return title; - const sliced = collapsed.slice(0, maxLength); + const sliced = title.slice(0, maxLength); const lastSpace = sliced.lastIndexOf(" "); const cut = lastSpace > Math.floor(maxLength / 2) ? sliced.slice(0, lastSpace) : sliced; - return `${cut.replace(/[.,;:!?]+$/u, "")}…`; + return `${cut.replace(/[\s.,;:!?]+$/u, "")}…`; } // `undefined` unless the workbench is still the generic placeholder, so diff --git a/apps/web/src/pages/new-workbench-picker.tsx b/apps/web/src/pages/new-workbench-picker.tsx index 73f4150ab..511e04bd9 100644 --- a/apps/web/src/pages/new-workbench-picker.tsx +++ b/apps/web/src/pages/new-workbench-picker.tsx @@ -9,6 +9,7 @@ import { reportError } from "@corbits/error-sink"; import { useBench } from "../bench-context"; import { workbenchKeys } from "../chat-path"; +import { NEW_WORKBENCH_TITLE, titleFromFirstMessage } from "@/auto-workbench-title"; import { createWorkbench, WorkbenchCreateError } from "../workbench-create"; import { useNavigate } from "../navigation"; import { ProviderSkipBanner } from "../provider-skip-banner"; @@ -29,12 +30,6 @@ export function describeWorkbenchCreateFailure(cause: unknown, refId?: string): return refId === undefined ? message : `${message} Reference: ${refId}`; } -/** A workbench's name is its opening ask, trimmed. */ -function workbenchName(prompt: string): string { - const trimmed = prompt.trim(); - return trimmed === "" ? "Workbench" : trimmed.slice(0, 60); -} - export function NewWorkbenchPickerRoute() { const navigate = useNavigate(); const queryClient = useQueryClient(); @@ -51,7 +46,7 @@ export function NewWorkbenchPickerRoute() { }) => createWorkbench({ benchTenantId, - name: workbenchName(openingMessage), + name: titleFromFirstMessage(openingMessage) ?? NEW_WORKBENCH_TITLE, openingMessage, }), onSuccess: (tenantId, variables) => {