mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
200 lines
10 KiB
TypeScript
200 lines
10 KiB
TypeScript
import {
|
||
AlertTriangle,
|
||
BookOpen,
|
||
Bot,
|
||
Box,
|
||
Clock3,
|
||
Database,
|
||
FileCheck2,
|
||
FilePenLine,
|
||
FileText,
|
||
GitBranch,
|
||
Info,
|
||
ListChecks,
|
||
PackageCheck,
|
||
Search,
|
||
ShieldCheck,
|
||
TerminalSquare,
|
||
Users,
|
||
} from "lucide-react";
|
||
import type {
|
||
TaskChatMaterializedResourceItem,
|
||
TaskChatProtocolItem,
|
||
TaskChatProviderActivityItem,
|
||
TaskChatWorkspaceChangeItem,
|
||
TaskChatWorkspaceFileItem,
|
||
} from "./task-chat-model";
|
||
import {
|
||
humanizeToolName,
|
||
isGenericToolName,
|
||
mcpToolIdentity,
|
||
toolActivityPresentation,
|
||
type ToolIcon,
|
||
} from "./tool-taxonomy";
|
||
|
||
export interface TaskChatActivityPresentation {
|
||
icon: ToolIcon;
|
||
runningLabel: string;
|
||
completedLabel: string;
|
||
failedLabel?: string;
|
||
interruptedLabel?: string;
|
||
detail?: string;
|
||
}
|
||
|
||
export function providerNoticeSeverity(item: TaskChatProviderActivityItem): "info" | "warning" | "error" {
|
||
const severity = item.details.find((entry) => entry.label === "Severity")?.value;
|
||
if (item.status === "failed" || severity === "error") return "error";
|
||
return severity === "info" ? "info" : "warning";
|
||
}
|
||
|
||
function providerDetail(item: TaskChatProviderActivityItem, ...labels: string[]): string | undefined {
|
||
return item.details.find((entry) => labels.includes(entry.label))?.value;
|
||
}
|
||
|
||
function meaningfulToolDetail(value: string | undefined): string | undefined {
|
||
if (!value || isGenericToolName(value) || /^tool(?:\s+|_)call\b/i.test(value)) return undefined;
|
||
return value;
|
||
}
|
||
|
||
export function providerActivityPresentation(item: TaskChatProviderActivityItem): TaskChatActivityPresentation {
|
||
const detail = item.summary
|
||
?? providerDetail(item, "Query", "URL", "Target", "Name", "Reference", "Reason", "Summary");
|
||
switch (item.family) {
|
||
case "research": {
|
||
const action = providerDetail(item, "Action")?.toLowerCase();
|
||
if (action === "open_page") {
|
||
return { icon: Search, runningLabel: "Opening a web page", completedLabel: "Opened a web page", failedLabel: "Couldn’t open the web page", interruptedLabel: "Stopped opening the web page", detail };
|
||
}
|
||
if (action === "find_in_page") {
|
||
return { icon: Search, runningLabel: "Searching the page", completedLabel: "Searched the page", failedLabel: "Page search failed", interruptedLabel: "Page search stopped", detail };
|
||
}
|
||
return { icon: Search, runningLabel: "Searching the web", completedLabel: "Searched the web", failedLabel: "Web search failed", interruptedLabel: "Web search stopped", detail };
|
||
}
|
||
case "tool_execution": {
|
||
const name = providerDetail(item, "Name");
|
||
const target = providerDetail(item, "Target");
|
||
const progress = providerDetail(item, "Progress");
|
||
const tool = toolActivityPresentation({
|
||
name,
|
||
transport: providerDetail(item, "Transport"),
|
||
namespace: providerDetail(item, "Namespace"),
|
||
operation: providerDetail(item, "Operation"),
|
||
target,
|
||
progress,
|
||
});
|
||
const boundedActivity = meaningfulToolDetail(item.summary)
|
||
?? meaningfulToolDetail(progress)
|
||
?? meaningfulToolDetail(target);
|
||
const activityIsIdentity = Boolean(
|
||
boundedActivity && name && humanizeToolName(boundedActivity) === humanizeToolName(name),
|
||
);
|
||
const technicalName = name ? mcpToolIdentity(name)?.name ?? name : tool.displayName;
|
||
const source = tool.sourceLabel
|
||
? `${tool.sourceLabel} · ${technicalName}`
|
||
: name && !isGenericToolName(name) && humanizeToolName(name) !== tool.runningLabel.replace(/^Running /, "")
|
||
? name
|
||
: undefined;
|
||
return {
|
||
icon: tool.icon,
|
||
runningLabel: tool.runningLabel,
|
||
completedLabel: tool.completedLabel,
|
||
failedLabel: tool.failedLabel,
|
||
interruptedLabel: tool.interruptedLabel,
|
||
detail: [activityIsIdentity ? undefined : boundedActivity, source].filter(Boolean).join(" · ") || undefined,
|
||
};
|
||
}
|
||
case "plan":
|
||
return { icon: ListChecks, runningLabel: "Updating the plan", completedLabel: "Updated the plan", failedLabel: "Plan update failed", interruptedLabel: "Plan update stopped", detail };
|
||
case "delegation": {
|
||
const action = providerDetail(item, "Action")?.toLowerCase();
|
||
if (action === "message") return { icon: Users, runningLabel: "Messaging a subagent", completedLabel: "Messaged a subagent", failedLabel: "Subagent message failed", interruptedLabel: "Subagent message stopped", detail };
|
||
if (action === "resume") return { icon: Users, runningLabel: "Resuming a subagent", completedLabel: "Resumed a subagent", failedLabel: "Couldn’t resume the subagent", interruptedLabel: "Subagent resume stopped", detail };
|
||
if (action === "close") return { icon: Users, runningLabel: "Closing a subagent", completedLabel: "Closed a subagent", failedLabel: "Couldn’t close the subagent", interruptedLabel: "Subagent close stopped", detail };
|
||
if (action === "wait") return { icon: Users, runningLabel: "Waiting for subagents", completedLabel: "Finished waiting for subagents", failedLabel: "Subagent wait failed", interruptedLabel: "Stopped waiting for subagents", detail };
|
||
return { icon: Users, runningLabel: "Starting a subagent", completedLabel: "Started a subagent", failedLabel: "Subagent start failed", interruptedLabel: "Subagent start stopped", detail };
|
||
}
|
||
case "model_identity":
|
||
return item.eventType === "model.route.changed"
|
||
? { icon: Bot, runningLabel: "Switching models", completedLabel: "Switched models", failedLabel: "Model switch failed", interruptedLabel: "Model switch stopped", detail }
|
||
: { icon: Bot, runningLabel: "Verifying the model", completedLabel: "Verified the model", failedLabel: "Model verification failed", interruptedLabel: "Model verification stopped", detail };
|
||
case "context":
|
||
return { icon: Database, runningLabel: "Compacting context", completedLabel: "Compacted context", failedLabel: "Context compaction failed", interruptedLabel: "Context compaction stopped", detail };
|
||
case "artifact": {
|
||
const viewed = item.eventType === "artifact.viewed";
|
||
return viewed
|
||
? { icon: Box, runningLabel: "Viewing an artifact", completedLabel: "Viewed an artifact", failedLabel: "Couldn’t view the artifact", interruptedLabel: "Artifact view stopped", detail }
|
||
: { icon: Box, runningLabel: "Generating an artifact", completedLabel: "Generated an artifact", failedLabel: "Artifact generation failed", interruptedLabel: "Artifact generation stopped", detail };
|
||
}
|
||
case "review": {
|
||
const state = providerDetail(item, "State")?.toLowerCase();
|
||
return state === "exited"
|
||
? { icon: FileCheck2, runningLabel: "Leaving review mode", completedLabel: "Left review mode", failedLabel: "Couldn’t leave review mode", interruptedLabel: "Review-mode change stopped", detail }
|
||
: { icon: FileCheck2, runningLabel: "Entering review mode", completedLabel: "Entered review mode", failedLabel: "Couldn’t enter review mode", interruptedLabel: "Review-mode change stopped", detail };
|
||
}
|
||
case "hook":
|
||
return { icon: GitBranch, runningLabel: "Running a hook", completedLabel: "Ran a hook", failedLabel: "Hook failed", interruptedLabel: "Hook stopped", detail };
|
||
case "memory":
|
||
return { icon: BookOpen, runningLabel: "Checking memory", completedLabel: "Referenced memory", failedLabel: "Memory lookup failed", interruptedLabel: "Memory lookup stopped", detail };
|
||
case "safety":
|
||
return { icon: ShieldCheck, runningLabel: "Reviewing safety", completedLabel: "Reviewed safety", failedLabel: "Safety review failed", interruptedLabel: "Safety review stopped", detail };
|
||
case "terminal":
|
||
return { icon: TerminalSquare, runningLabel: "Sending terminal input", completedLabel: "Sent terminal input", failedLabel: "Terminal input failed", interruptedLabel: "Terminal input stopped", detail };
|
||
case "wait":
|
||
return { icon: Clock3, runningLabel: "Waiting", completedLabel: "Finished waiting", failedLabel: "Wait failed", interruptedLabel: "Wait stopped", detail };
|
||
case "provider_notice":
|
||
return { icon: providerNoticeSeverity(item) === "info" ? Info : AlertTriangle, runningLabel: "Provider notice", completedLabel: "Provider notice", failedLabel: "Provider error", interruptedLabel: "Provider notice", detail };
|
||
}
|
||
}
|
||
|
||
function workspaceChangePresentation(item: TaskChatWorkspaceChangeItem): TaskChatActivityPresentation {
|
||
const count = item.totals.files || item.files.length;
|
||
const detail = count > 0 ? `${count} ${count === 1 ? "file" : "files"}` : undefined;
|
||
return { icon: FilePenLine, runningLabel: "Editing files", completedLabel: "Edited files", detail };
|
||
}
|
||
|
||
function workspaceFilePresentation(item: TaskChatWorkspaceFileItem): TaskChatActivityPresentation {
|
||
return {
|
||
icon: FileText,
|
||
runningLabel: "Referencing a file",
|
||
completedLabel: "Referenced a file",
|
||
detail: item.line == null ? item.path : `${item.path}:${item.line}`,
|
||
};
|
||
}
|
||
|
||
function resourcePresentation(item: TaskChatMaterializedResourceItem): TaskChatActivityPresentation {
|
||
return {
|
||
icon: item.resourceKind === "document" ? FileText : PackageCheck,
|
||
runningLabel: "Saving a resource",
|
||
completedLabel: item.resourceKind === "document" ? "Added a document" : "Added a deliverable",
|
||
detail: item.title,
|
||
};
|
||
}
|
||
|
||
export function protocolActivityPresentation(item: TaskChatProtocolItem): TaskChatActivityPresentation | null {
|
||
switch (item.surface) {
|
||
case "provider_activity": return providerActivityPresentation(item);
|
||
case "workspace_change": return workspaceChangePresentation(item);
|
||
case "workspace_file": return workspaceFilePresentation(item);
|
||
case "resource": return resourcePresentation(item);
|
||
case "runtime_request":
|
||
case "run_result":
|
||
case "run_terminal":
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function protocolActivityIsRunning(item: TaskChatProtocolItem): boolean {
|
||
if (item.surface === "provider_activity") return item.status === "running";
|
||
if (item.surface === "workspace_change") return !item.complete;
|
||
return false;
|
||
}
|
||
|
||
export function protocolActivityLabel(item: TaskChatProtocolItem, presentation: TaskChatActivityPresentation): string {
|
||
if (item.surface !== "provider_activity") {
|
||
return protocolActivityIsRunning(item) ? presentation.runningLabel : presentation.completedLabel;
|
||
}
|
||
if (item.status === "failed") return presentation.failedLabel ?? `${presentation.completedLabel} · failed`;
|
||
if (item.status === "interrupted") return presentation.interruptedLabel ?? `${presentation.completedLabel} · interrupted`;
|
||
return item.status === "running" ? presentation.runningLabel : presentation.completedLabel;
|
||
}
|