Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions apps/web/src/auto-workbench-title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,30 @@
/** Placeholder title for an untitled / blank mint — never a prefab name. */
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 = 40;
/** Sidebar-friendly caps — a goal phrase that never truncates in the pill or sidebar. */
export const AUTO_WORKBENCH_TITLE_MAX = 24;
const AUTO_WORKBENCH_TITLE_MAX_WORDS = 4;

const ARTICLES = new Set(["the", "a", "an"]);
const TRAILING_STOPWORDS = new Set([
...ARTICLES,
"for",
"and",
"or",
"of",
"to",
"in",
"on",
"with",
"my",
"our",
]);

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.
// First sentence or clause, filler and articles stripped, first letter
// capitalised, cut to a few whole words — never an ellipsis.
export function titleFromFirstMessage(
message: string,
maxLength: number = AUTO_WORKBENCH_TITLE_MAX,
Expand All @@ -22,15 +38,22 @@ export function titleFromFirstMessage(
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 = title.slice(0, maxLength);
const lastSpace = sliced.lastIndexOf(" ");
const cut = lastSpace > Math.floor(maxLength / 2) ? sliced.slice(0, lastSpace) : sliced;
return `${cut.replace(/[\s.,;:!?]+$/u, "")}…`;
const words = clause
.replace(/[\s.,;:!?]+$/u, "")
.split(" ")
.filter((word) => word.length > 0 && !ARTICLES.has(word.toLowerCase()));

const kept: string[] = [];
for (const word of words.slice(0, AUTO_WORKBENCH_TITLE_MAX_WORDS)) {
if ([...kept, word].join(" ").length > maxLength) break;
kept.push(word);
}
if (kept.length === 0 && words[0] !== undefined) kept.push(words[0].slice(0, maxLength));
while (kept.length > 1 && TRAILING_STOPWORDS.has((kept.at(-1) ?? "").toLowerCase())) kept.pop();

const title = kept.join(" ").replace(/[\s.,;:!?]+$/u, "");
if (title.length === 0) return undefined;
return title.charAt(0).toLocaleUpperCase() + title.slice(1);
}

// `undefined` unless the workbench is still the generic placeholder, so
Expand Down
20 changes: 18 additions & 2 deletions apps/web/src/pages/workbench-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ import { createFetchStockHub } from "../needs-converge";
import { workbenchKeys } from "../chat-path";
import { tenantKeys } from "../query-client";
import { recordLastWorkbenchId } from "../last-workbench";
import { ProviderSkipBanner } from "../provider-skip-banner";
import { PROVIDER_SETTINGS_PATH, ProviderSkipBanner } from "../provider-skip-banner";
import { useNavigate } from "../navigation";
import { isClassifiedInferenceFailureText } from "@/chat/inference-failure";
import { redeployWorkbenchAgent } from "../workbench-create";
import { workbenchIdFromPath } from "../workbench-path";

// A textarea placeholder can't ellipsize, so a long bench name is dropped.
const PLACEHOLDER_NAME_MAX = 18;
function messagePlaceholder(name: string | undefined): string {
return name !== undefined && name.length <= PLACEHOLDER_NAME_MAX ? `Message ${name}` : "Message";
}

function errorText(cause: unknown): string {
return cause instanceof Error ? cause.message : String(cause);
}
Expand Down Expand Up @@ -86,9 +94,17 @@ function WorkbenchMessageRow({
const { pkg, renderedBody } = resolveMessagePackage(message.attachments, body);
const own = message.author === "me";
const time = new Date(message.at);
const navigate = useNavigate();
// The wire carries no error kind, so the existing preamble classifier decides.
const providerFailure = !own && isClassifiedInferenceFailureText(renderedBody);
const content = (
<>
<Markdown text={renderedBody} />
{providerFailure ? (
<Button variant="link" size="sm" onClick={() => navigate(PROVIDER_SETTINGS_PATH)}>
Fix provider
</Button>
) : null}
<MessageAttachments
tenantId={workbenchTenantId}
attachments={message.attachments}
Expand Down Expand Up @@ -320,7 +336,7 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string }
<Composer
placeholder={
startingAgent === undefined
? `Message ${tenant.data?.name ?? "this workbench"}`
? messagePlaceholder(tenant.data?.name)
: `${startingAgent.name} is starting…`
}
busy={send.isPending}
Expand Down
Loading