Skip to content

Commit 96071af

Browse files
author
Agent
committed
Fix Forkara tool title parsing and ForkDenial markup assertions
1 parent 4652a95 commit 96071af

2 files changed

Lines changed: 90 additions & 55 deletions

File tree

apps/web/src/components/ForkDenialStage.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ describe("ForkDenialStage", () => {
88
it("renders every stage label in provided order", () => {
99
const stages = getForkDenialStages({ includeFinalForkState: true });
1010
const markup = renderToStaticMarkup(<ForkDenialStageList stages={stages} />);
11-
const indexes = stages.map((stage) => markup.indexOf(stage.label));
11+
const normalizedMarkup = markup
12+
.replace(/&#39;|&apos;/g, "'")
13+
.replace(/&quot;/g, '"')
14+
.replace(/&amp;/g, "&");
15+
const indexes = stages.map((stage) => normalizedMarkup.indexOf(stage.label));
1216

1317
for (const index of indexes) {
1418
expect(index).toBeGreaterThanOrEqual(0);

apps/web/src/lib/toolCallLabel.ts

Lines changed: 85 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ const SYNARA_BROWSER_TOOL_NAME_BY_PRESENTATION = new Map<string, SynaraBrowserTo
287287
]),
288288
);
289289

290+
const SYNARA_TOOL_NAME_STEMS = new Set<string>(
291+
Object.keys(SYNARA_MCP_TOOL_PRESENTATIONS).map((toolName) =>
292+
toolName.replace(/^synara_/, ""),
293+
),
294+
);
295+
290296
const SYNARA_MCP_TOOL_PRESENTATION_ENTRIES = Object.entries(SYNARA_MCP_TOOL_PRESENTATIONS).map(
291297
([toolName, presentation]) => ({
292298
toolName,
@@ -301,6 +307,9 @@ function extractSynaraMcpToolName(normalizedCandidate: string): string | null {
301307
if (BROWSER_TOOL_NAME_SET.has(normalizedCandidate)) {
302308
return `synara_${normalizedCandidate}`;
303309
}
310+
if (SYNARA_TOOL_NAME_STEMS.has(normalizedCandidate)) {
311+
return `synara_${normalizedCandidate}`;
312+
}
304313
if (normalizedCandidate.startsWith("mcp_synara_synara_")) {
305314
return normalizedCandidate.slice("mcp_synara_".length);
306315
}
@@ -316,19 +325,42 @@ function extractSynaraMcpToolName(normalizedCandidate: string): string | null {
316325
return null;
317326
}
318327

328+
function expandSynaraMcpCandidateInputs(
329+
candidate: string | null | undefined,
330+
): ReadonlyArray<string> {
331+
if (!candidate) {
332+
return [];
333+
}
334+
const trimmed = candidate.trim().replace(/\s+/g, " ");
335+
if (!trimmed) {
336+
return [];
337+
}
338+
const variants = new Set<string>([trimmed]);
339+
const withPrefixedRemoved = trimmed.replace(/^forkara:\s*/i, "").trim();
340+
if (withPrefixedRemoved && withPrefixedRemoved !== trimmed) {
341+
variants.add(withPrefixedRemoved);
342+
}
343+
const withRepeatedPrefixRemoved = trimmed.replace(/^forkara:\s*forkara\s+/i, "").trim();
344+
if (withRepeatedPrefixRemoved) {
345+
variants.add(withRepeatedPrefixRemoved);
346+
}
347+
return [...variants];
348+
}
349+
319350
function resolveSynaraBrowserToolName(
320351
candidates: ReadonlyArray<string | null | undefined>,
321352
): SynaraBrowserToolName | null {
322353
for (const candidate of candidates) {
323-
if (!candidate) continue;
324-
const normalizedCandidate = normalizeSynaraMcpIdentifier(candidate);
325-
const extractedToolName = extractSynaraMcpToolName(normalizedCandidate);
326-
const candidateToolName =
327-
extractedToolName ??
328-
SYNARA_BROWSER_TOOL_NAME_BY_PRESENTATION.get(normalizedCandidate) ??
329-
normalizedCandidate;
330-
if (candidateToolName in SYNARA_BROWSER_TOOL_PRESENTATIONS) {
331-
return candidateToolName as SynaraBrowserToolName;
354+
for (const expandedCandidate of expandSynaraMcpCandidateInputs(candidate)) {
355+
const normalizedCandidate = normalizeSynaraMcpIdentifier(expandedCandidate);
356+
const extractedToolName = extractSynaraMcpToolName(normalizedCandidate);
357+
const candidateToolName =
358+
extractedToolName ??
359+
SYNARA_BROWSER_TOOL_NAME_BY_PRESENTATION.get(normalizedCandidate) ??
360+
normalizedCandidate;
361+
if (candidateToolName in SYNARA_BROWSER_TOOL_PRESENTATIONS) {
362+
return candidateToolName as SynaraBrowserToolName;
363+
}
332364
}
333365
}
334366
return null;
@@ -351,53 +383,52 @@ function resolveSynaraMcpToolPresentation(
351383
candidates: ReadonlyArray<string | null | undefined>,
352384
): SynaraMcpToolPresentation | null {
353385
for (const candidate of candidates) {
354-
if (!candidate) {
355-
continue;
356-
}
357-
const normalizedCandidate = normalizeSynaraMcpIdentifier(candidate);
358-
for (const entry of SYNARA_MCP_TOOL_PRESENTATION_ENTRIES) {
359-
if (
360-
normalizedCandidate === entry.normalizedRunning ||
361-
normalizedCandidate === entry.normalizedCompleted ||
362-
normalizedCandidate === entry.normalizedFailed
363-
) {
364-
return entry.presentation;
386+
for (const expandedCandidate of expandSynaraMcpCandidateInputs(candidate)) {
387+
const normalizedCandidate = normalizeSynaraMcpIdentifier(expandedCandidate);
388+
for (const entry of SYNARA_MCP_TOOL_PRESENTATION_ENTRIES) {
389+
if (
390+
normalizedCandidate === entry.normalizedRunning ||
391+
normalizedCandidate === entry.normalizedCompleted ||
392+
normalizedCandidate === entry.normalizedFailed
393+
) {
394+
return entry.presentation;
395+
}
365396
}
397+
const toolName = extractSynaraMcpToolName(normalizedCandidate);
398+
const knownPresentation = toolName
399+
? (SYNARA_MCP_TOOL_PRESENTATIONS[toolName as keyof typeof SYNARA_MCP_TOOL_PRESENTATIONS] as
400+
| SynaraMcpToolPresentation
401+
| undefined)
402+
: undefined;
403+
if (knownPresentation) {
404+
return knownPresentation;
405+
}
406+
// Free-text summaries (e.g. reconciler activity lines) can begin with the
407+
// word "Forkara" and normalize into a fake tool identifier; only
408+
// identifier-shaped candidates may take an invented fallback presentation.
409+
if (/\s/.test(expandedCandidate.trim())) {
410+
continue;
411+
}
412+
if (normalizedCandidate.startsWith("synara_is_handling_")) {
413+
return fallbackSynaraMcpToolPresentation(
414+
`synara_${normalizedCandidate.slice("synara_is_handling_".length)}`,
415+
);
416+
}
417+
if (normalizedCandidate.startsWith("synara_handled_")) {
418+
return fallbackSynaraMcpToolPresentation(
419+
`synara_${normalizedCandidate.slice("synara_handled_".length)}`,
420+
);
421+
}
422+
if (normalizedCandidate.startsWith("synara_couldn_t_handle_")) {
423+
return fallbackSynaraMcpToolPresentation(
424+
`synara_${normalizedCandidate.slice("synara_couldn_t_handle_".length)}`,
425+
);
426+
}
427+
if (!toolName) {
428+
continue;
429+
}
430+
return fallbackSynaraMcpToolPresentation(toolName);
366431
}
367-
const toolName = extractSynaraMcpToolName(normalizedCandidate);
368-
const knownPresentation = toolName
369-
? (SYNARA_MCP_TOOL_PRESENTATIONS[toolName as keyof typeof SYNARA_MCP_TOOL_PRESENTATIONS] as
370-
| SynaraMcpToolPresentation
371-
| undefined)
372-
: undefined;
373-
if (knownPresentation) {
374-
return knownPresentation;
375-
}
376-
// Free-text summaries (e.g. reconciler activity lines) can begin with the
377-
// word "Forkara" and normalize into a fake tool identifier; only
378-
// identifier-shaped candidates may take an invented fallback presentation.
379-
if (/\s/.test(candidate.trim())) {
380-
continue;
381-
}
382-
if (normalizedCandidate.startsWith("synara_is_handling_")) {
383-
return fallbackSynaraMcpToolPresentation(
384-
`synara_${normalizedCandidate.slice("synara_is_handling_".length)}`,
385-
);
386-
}
387-
if (normalizedCandidate.startsWith("synara_handled_")) {
388-
return fallbackSynaraMcpToolPresentation(
389-
`synara_${normalizedCandidate.slice("synara_handled_".length)}`,
390-
);
391-
}
392-
if (normalizedCandidate.startsWith("synara_couldn_t_handle_")) {
393-
return fallbackSynaraMcpToolPresentation(
394-
`synara_${normalizedCandidate.slice("synara_couldn_t_handle_".length)}`,
395-
);
396-
}
397-
if (!toolName) {
398-
continue;
399-
}
400-
return fallbackSynaraMcpToolPresentation(toolName);
401432
}
402433
return null;
403434
}

0 commit comments

Comments
 (0)