Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
332 lines
8.9 KiB
TypeScript
332 lines
8.9 KiB
TypeScript
import { extractAgentMcpToolTarget } from "../agentMcpToolTarget";
|
|
import { fileChangePathsFromChanges } from "../workspaceAgentFileChangePayload";
|
|
|
|
export interface PromptToolDetail {
|
|
kind:
|
|
| "command"
|
|
| "directory"
|
|
| "files"
|
|
| "mcp"
|
|
| "path"
|
|
| "prompt"
|
|
| "query"
|
|
| "reason"
|
|
| "url";
|
|
value: string;
|
|
meta?: string;
|
|
}
|
|
|
|
export function getPromptToolDetails(
|
|
input: Record<string, unknown> | null
|
|
): PromptToolDetail[] {
|
|
if (!input) {
|
|
return [];
|
|
}
|
|
const mcpTarget = extractAgentMcpToolTarget({ input });
|
|
const mcpDetails: PromptToolDetail[] = mcpTarget
|
|
? [
|
|
{
|
|
kind: "mcp",
|
|
value: mcpTarget.displayName,
|
|
...(mcpTarget.instruction ? { meta: mcpTarget.instruction } : {})
|
|
}
|
|
]
|
|
: [];
|
|
const detailInputs = resolveToolDetailInputs(input);
|
|
const details: PromptToolDetail[] = [...mcpDetails];
|
|
const fileChanges = fileChangePathsFromInputs(detailInputs);
|
|
const fileChangePresentation = presentFileChangePaths(fileChanges);
|
|
if (fileChanges.length > 0) {
|
|
details.push({
|
|
kind: "files",
|
|
value: fileChangePresentation.value
|
|
});
|
|
if (fileChangePresentation.directory) {
|
|
details.push({
|
|
kind: "directory",
|
|
value: fileChangePresentation.directory
|
|
});
|
|
}
|
|
}
|
|
const commandDetail = firstDetailEntry(
|
|
detailInputs,
|
|
(detailInput) =>
|
|
commandStringValue(detailInput.command) ??
|
|
commandStringValue(detailInput.cmd)
|
|
);
|
|
if (commandDetail) {
|
|
const description = stringValue(commandDetail.input.description);
|
|
details.push({
|
|
kind: "command",
|
|
value: commandDetail.value,
|
|
...(description ? { meta: description } : {})
|
|
});
|
|
}
|
|
const filePathDetail = firstDetailEntry(
|
|
detailInputs,
|
|
(detailInput) =>
|
|
stringValue(detailInput.grantRoot) ??
|
|
stringValue(detailInput.file_path) ??
|
|
stringValue(detailInput.filePath) ??
|
|
stringValue(detailInput.path) ??
|
|
stringValue(detailInput.notebook_path)
|
|
);
|
|
if (
|
|
filePathDetail &&
|
|
!isRedundantFileChangePath(filePathDetail.value, fileChanges)
|
|
) {
|
|
const lineRange = formatLineRange(filePathDetail.input);
|
|
details.push({
|
|
kind: "path",
|
|
value: filePathDetail.value,
|
|
...(lineRange ? { meta: lineRange } : {})
|
|
});
|
|
}
|
|
const query = firstDetailValue(
|
|
detailInputs,
|
|
(detailInput) =>
|
|
stringValue(detailInput.query) ??
|
|
stringValue(detailInput.search_query) ??
|
|
stringValue(detailInput.searchQuery) ??
|
|
stringValue(detailInput.pattern)
|
|
);
|
|
if (query) {
|
|
details.push({
|
|
kind: "query",
|
|
value: query
|
|
});
|
|
}
|
|
const url = firstDetailValue(
|
|
detailInputs,
|
|
(detailInput) =>
|
|
stringValue(detailInput.url) ?? stringValue(detailInput.uri)
|
|
);
|
|
if (url) {
|
|
details.push({
|
|
kind: "url",
|
|
value: url
|
|
});
|
|
}
|
|
const prompt = firstDetailValue(
|
|
detailInputs,
|
|
(detailInput) =>
|
|
stringValue(detailInput.prompt) ?? stringValue(detailInput.instruction)
|
|
);
|
|
if (prompt) {
|
|
details.push({
|
|
kind: "prompt",
|
|
value: prompt
|
|
});
|
|
}
|
|
const reason = firstDetailValue(detailInputs, (detailInput) =>
|
|
stringValue(detailInput.reason)
|
|
);
|
|
if (reason) {
|
|
details.push({
|
|
kind: "reason",
|
|
value: reason
|
|
});
|
|
}
|
|
return details;
|
|
}
|
|
|
|
export function isPromptRequestIdTitle(value: string): boolean {
|
|
return /^requestid\s*:/iu.test(value.trim());
|
|
}
|
|
|
|
function resolveToolDetailInputs(
|
|
input: Record<string, unknown>
|
|
): readonly Record<string, unknown>[] {
|
|
const toolCall = objectValue(input.toolCall);
|
|
const nestedInput =
|
|
firstObjectValue(toolCall, ["input", "arguments", "args"]) ?? toolCall;
|
|
if (!nestedInput) {
|
|
return [input];
|
|
}
|
|
// Canonical nested input is authoritative within each semantic alias group;
|
|
// root input remains a field-by-field compatibility fallback.
|
|
return [nestedInput, input];
|
|
}
|
|
|
|
function firstDetailValue<T>(
|
|
inputs: readonly Record<string, unknown>[],
|
|
resolve: (input: Record<string, unknown>) => T | null
|
|
): T | null {
|
|
return firstDetailEntry(inputs, resolve)?.value ?? null;
|
|
}
|
|
|
|
function firstDetailEntry<T>(
|
|
inputs: readonly Record<string, unknown>[],
|
|
resolve: (input: Record<string, unknown>) => T | null
|
|
): { input: Record<string, unknown>; value: T } | null {
|
|
for (const input of inputs) {
|
|
const value = resolve(input);
|
|
if (value !== null) {
|
|
return { input, value };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function firstObjectValue(
|
|
input: Record<string, unknown> | null,
|
|
keys: readonly string[]
|
|
): Record<string, unknown> | null {
|
|
if (!input) {
|
|
return null;
|
|
}
|
|
if (keys.some((key) => stringValue(input[key]))) {
|
|
return input;
|
|
}
|
|
for (const key of keys) {
|
|
const value = objectValue(input[key]);
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function formatLineRange(input: Record<string, unknown>): string | null {
|
|
const start = numericValue(input.startLine) ?? numericValue(input.start_line);
|
|
const end = numericValue(input.endLine) ?? numericValue(input.end_line);
|
|
if (start === null || end === null) {
|
|
return null;
|
|
}
|
|
return start === end ? `L${start}` : `L${start}-${end}`;
|
|
}
|
|
|
|
function stringValue(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function commandStringValue(value: unknown): string | null {
|
|
if (typeof value === "string") {
|
|
return stringValue(value);
|
|
}
|
|
if (!Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
const shellFlag = stringValue(value[value.length - 2]);
|
|
const shellCommand = stringValue(value[value.length - 1]);
|
|
if ((shellFlag === "-c" || shellFlag === "-lc") && shellCommand) {
|
|
return shellCommand;
|
|
}
|
|
const parts = value.flatMap((part) => {
|
|
const text = stringValue(part);
|
|
return text ? [text] : [];
|
|
});
|
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
}
|
|
|
|
function objectValue(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null;
|
|
}
|
|
|
|
function fileChangePaths(input: Record<string, unknown>): string[] {
|
|
const candidates = [
|
|
objectValue(input.fileChanges)?.files,
|
|
input.fileChanges,
|
|
input.files,
|
|
input.changes
|
|
];
|
|
for (const candidate of candidates) {
|
|
const paths = fileChangePathsFromChanges(candidate);
|
|
if (paths.length > 0) {
|
|
return paths;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function fileChangePathsFromInputs(
|
|
inputs: readonly Record<string, unknown>[]
|
|
): string[] {
|
|
for (const input of inputs) {
|
|
const paths = fileChangePaths(input);
|
|
if (paths.length > 0) {
|
|
return paths;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function presentFileChangePaths(paths: readonly string[]): {
|
|
directory: string | null;
|
|
value: string;
|
|
} {
|
|
const uniquePaths = Array.from(
|
|
new Set(paths.map((path) => path.trim()).filter((path) => path.length > 0))
|
|
);
|
|
const directory = commonAbsoluteFileDirectory(uniquePaths);
|
|
const displayPaths = directory
|
|
? uniquePaths.map((path) => relativePathFromDirectory(path, directory))
|
|
: uniquePaths;
|
|
if (displayPaths.length <= 3) {
|
|
return { directory, value: displayPaths.join(", ") };
|
|
}
|
|
return {
|
|
directory,
|
|
value: `${displayPaths.slice(0, 3).join(", ")} +${displayPaths.length - 3} more`
|
|
};
|
|
}
|
|
|
|
function commonAbsoluteFileDirectory(paths: readonly string[]): string | null {
|
|
if (paths.length === 0) {
|
|
return null;
|
|
}
|
|
const normalized = paths.map(normalizeDisplayPath);
|
|
if (!normalized.every(isAbsoluteDisplayPath)) {
|
|
return null;
|
|
}
|
|
const directories = normalized.map((path) =>
|
|
path.slice(0, path.lastIndexOf("/"))
|
|
);
|
|
const firstParts = directories[0]?.split("/") ?? [];
|
|
let sharedLength = firstParts.length;
|
|
for (const directory of directories.slice(1)) {
|
|
const parts = directory.split("/");
|
|
sharedLength = Math.min(sharedLength, parts.length);
|
|
while (
|
|
sharedLength > 0 &&
|
|
firstParts[sharedLength - 1] !== parts[sharedLength - 1]
|
|
) {
|
|
sharedLength -= 1;
|
|
}
|
|
}
|
|
const shared = firstParts.slice(0, sharedLength).join("/");
|
|
return shared && shared !== "/" && !/^[A-Za-z]:$/u.test(shared)
|
|
? shared
|
|
: null;
|
|
}
|
|
|
|
function relativePathFromDirectory(path: string, directory: string): string {
|
|
const normalized = normalizeDisplayPath(path);
|
|
const prefix = `${directory}/`;
|
|
return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : path;
|
|
}
|
|
|
|
function isRedundantFileChangePath(
|
|
path: string,
|
|
fileChanges: readonly string[]
|
|
): boolean {
|
|
const normalizedPath = normalizeDisplayPath(path);
|
|
return fileChanges.some(
|
|
(candidate) => normalizeDisplayPath(candidate) === normalizedPath
|
|
);
|
|
}
|
|
|
|
function normalizeDisplayPath(path: string): string {
|
|
return path.trim().replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
}
|
|
|
|
function isAbsoluteDisplayPath(path: string): boolean {
|
|
return path.startsWith("/") || /^[A-Za-z]:\//u.test(path);
|
|
}
|
|
|
|
function numericValue(value: unknown): number | null {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
}
|