- All gsdDir/gsdRoot/gsdHome → sfDir/sfRootDir/sfHome - GSDWorkspace* → SFWorkspace* interfaces - bootstrapGsdProject → bootstrapProject - runGSDDoctor → runSFDoctor - GsdClient → SfClient, gsd-client.ts → sf-client.ts - .gsd/ → .sf/ in all tests, docs, docker, native, vscode - Auto-migration: headless detects .gsd/ → renames to .sf/ - Deleted gsd-phase-state.ts backward-compat re-export - Renamed bin/gsd-from-source → bin/sf-from-source - Updated mintlify docs, github workflows, docker configs
212 lines
5.9 KiB
TypeScript
212 lines
5.9 KiB
TypeScript
import * as vscode from "vscode";
|
|
import type { SfClient, AgentEvent } from "./sf-client.js";
|
|
|
|
interface ActivityItem {
|
|
id: number;
|
|
type: "tool" | "agent";
|
|
label: string;
|
|
detail: string;
|
|
icon: vscode.ThemeIcon;
|
|
timestamp: number;
|
|
duration?: number;
|
|
filePath?: string;
|
|
status: "running" | "success" | "error";
|
|
}
|
|
|
|
const TOOL_ICONS: Record<string, string> = {
|
|
Read: "file",
|
|
Write: "new-file",
|
|
Edit: "edit",
|
|
Bash: "terminal",
|
|
Grep: "search",
|
|
Glob: "file-directory",
|
|
Agent: "organization",
|
|
};
|
|
|
|
function toolSummary(toolName: string, toolInput: Record<string, unknown>): { label: string; filePath?: string } {
|
|
const name = toolName ?? "Unknown";
|
|
switch (name) {
|
|
case "Read": {
|
|
const p = String(toolInput?.file_path ?? toolInput?.path ?? "");
|
|
const short = p.split(/[\\/]/).pop() ?? p;
|
|
return { label: `Read ${short}`, filePath: p || undefined };
|
|
}
|
|
case "Write": {
|
|
const p = String(toolInput?.file_path ?? "");
|
|
const short = p.split(/[\\/]/).pop() ?? p;
|
|
return { label: `Write ${short}`, filePath: p || undefined };
|
|
}
|
|
case "Edit": {
|
|
const p = String(toolInput?.file_path ?? "");
|
|
const short = p.split(/[\\/]/).pop() ?? p;
|
|
return { label: `Edit ${short}`, filePath: p || undefined };
|
|
}
|
|
case "Bash": {
|
|
const cmd = String(toolInput?.command ?? "").slice(0, 60);
|
|
return { label: `Bash: ${cmd}` };
|
|
}
|
|
case "Grep": {
|
|
const pat = String(toolInput?.pattern ?? "").slice(0, 40);
|
|
return { label: `Grep: ${pat}` };
|
|
}
|
|
case "Glob": {
|
|
const pat = String(toolInput?.pattern ?? "").slice(0, 40);
|
|
return { label: `Glob: ${pat}` };
|
|
}
|
|
default:
|
|
return { label: name };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TreeDataProvider that shows real-time tool executions from the SF agent.
|
|
* Listens to tool_execution_start/end and agent_start/end events.
|
|
*/
|
|
export class GsdActivityFeedProvider implements vscode.TreeDataProvider<ActivityItem>, vscode.Disposable {
|
|
public static readonly viewId = "sf-activity";
|
|
|
|
private readonly _onDidChangeTreeData = new vscode.EventEmitter<void>();
|
|
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
|
|
|
private items: ActivityItem[] = [];
|
|
private nextId = 0;
|
|
private runningTools = new Map<string, number>(); // toolUseId -> item id
|
|
private maxItems: number;
|
|
private disposables: vscode.Disposable[] = [];
|
|
|
|
constructor(private readonly client: SfClient) {
|
|
this.maxItems = vscode.workspace.getConfiguration("sf").get<number>("activityFeedMaxItems", 100);
|
|
|
|
this.disposables.push(
|
|
this._onDidChangeTreeData,
|
|
client.onEvent((evt) => this.handleEvent(evt)),
|
|
client.onConnectionChange((connected) => {
|
|
if (!connected) {
|
|
this.runningTools.clear();
|
|
}
|
|
this._onDidChangeTreeData.fire();
|
|
}),
|
|
vscode.workspace.onDidChangeConfiguration((e) => {
|
|
if (e.affectsConfiguration("sf.activityFeedMaxItems")) {
|
|
this.maxItems = vscode.workspace.getConfiguration("sf").get<number>("activityFeedMaxItems", 100);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
getTreeItem(element: ActivityItem): vscode.TreeItem {
|
|
const item = new vscode.TreeItem(element.label, vscode.TreeItemCollapsibleState.None);
|
|
item.iconPath = element.icon;
|
|
item.description = element.duration !== undefined
|
|
? `${element.duration}ms`
|
|
: element.status === "running"
|
|
? "running..."
|
|
: "";
|
|
item.tooltip = `${element.detail}\n${new Date(element.timestamp).toLocaleTimeString()}`;
|
|
|
|
if (element.filePath) {
|
|
item.command = {
|
|
command: "vscode.open",
|
|
title: "Open File",
|
|
arguments: [vscode.Uri.file(element.filePath)],
|
|
};
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
getChildren(): ActivityItem[] {
|
|
// Show newest first
|
|
return [...this.items].reverse();
|
|
}
|
|
|
|
clear(): void {
|
|
this.items = [];
|
|
this.runningTools.clear();
|
|
this._onDidChangeTreeData.fire();
|
|
}
|
|
|
|
dispose(): void {
|
|
for (const d of this.disposables) {
|
|
d.dispose();
|
|
}
|
|
}
|
|
|
|
private handleEvent(evt: AgentEvent): void {
|
|
switch (evt.type) {
|
|
case "agent_start": {
|
|
this.addItem({
|
|
type: "agent",
|
|
label: "Agent started",
|
|
detail: "Agent began processing",
|
|
icon: new vscode.ThemeIcon("play", new vscode.ThemeColor("testing.iconPassed")),
|
|
status: "running",
|
|
});
|
|
break;
|
|
}
|
|
case "agent_end": {
|
|
this.addItem({
|
|
type: "agent",
|
|
label: "Agent finished",
|
|
detail: "Agent completed processing",
|
|
icon: new vscode.ThemeIcon("check", new vscode.ThemeColor("testing.iconPassed")),
|
|
status: "success",
|
|
});
|
|
break;
|
|
}
|
|
case "tool_execution_start": {
|
|
const toolName = String(evt.toolName ?? "");
|
|
const toolInput = (evt.toolInput ?? {}) as Record<string, unknown>;
|
|
const toolUseId = String(evt.toolUseId ?? "");
|
|
const { label, filePath } = toolSummary(toolName, toolInput);
|
|
const iconName = TOOL_ICONS[toolName] ?? "tools";
|
|
|
|
const id = this.addItem({
|
|
type: "tool",
|
|
label,
|
|
detail: `Tool: ${toolName}`,
|
|
icon: new vscode.ThemeIcon(iconName, new vscode.ThemeColor("charts.yellow")),
|
|
status: "running",
|
|
filePath,
|
|
});
|
|
|
|
if (toolUseId) {
|
|
this.runningTools.set(toolUseId, id);
|
|
}
|
|
break;
|
|
}
|
|
case "tool_execution_end": {
|
|
const toolUseId = String(evt.toolUseId ?? "");
|
|
const itemId = this.runningTools.get(toolUseId);
|
|
if (itemId !== undefined) {
|
|
this.runningTools.delete(toolUseId);
|
|
const item = this.items.find((i) => i.id === itemId);
|
|
if (item) {
|
|
const isError = evt.error === true || evt.isError === true;
|
|
item.status = isError ? "error" : "success";
|
|
item.duration = Date.now() - item.timestamp;
|
|
item.icon = new vscode.ThemeIcon(
|
|
isError ? "error" : "check",
|
|
new vscode.ThemeColor(isError ? "testing.iconFailed" : "testing.iconPassed"),
|
|
);
|
|
this._onDidChangeTreeData.fire();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private addItem(partial: Omit<ActivityItem, "id" | "timestamp">): number {
|
|
const id = this.nextId++;
|
|
this.items.push({ ...partial, id, timestamp: Date.now() });
|
|
|
|
// Evict old items
|
|
while (this.items.length > this.maxItems) {
|
|
this.items.shift();
|
|
}
|
|
|
|
this._onDidChangeTreeData.fire();
|
|
return id;
|
|
}
|
|
}
|