Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

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;
}