Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/electron/main/ipc/register-main-ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export function registerMainIpcHandlers(options: RegisterMainIpcHandlersOptions)
async (_event, sessionId: string) =>
withRuntime((runtimeController) => runtimeController.getTelegramSetupSession(sessionId)),
);
ipcMain.handle(
ipcChannels.getToolUsageSummary,
async () => withRuntime((runtimeController) => runtimeController.getToolUsageSummary()),
);
ipcMain.handle(
ipcChannels.createAgent,
async (_event, input: CreateAgentInput) =>
Expand Down
52 changes: 52 additions & 0 deletions src/electron/main/runtime/agent-runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ import type {
AgentServiceListener,
AgentServiceSnapshot,
} from '@/shared/agents/agent-runtime';
import type {
ToolUsageSummaryResult,
ToolUsageSummaryRow,
} from '@/shared/agents/tool-analytics';

export { resolveAgentLiteRuntimeRoot } from '@/electron/main/dune-paths';
export type {
Expand Down Expand Up @@ -656,6 +660,54 @@ export class AgentRuntime implements AgentRuntimeContract {
};
}

/** Returns aggregated AgentLite tool usage analytics for active Dune agents. */
async getToolUsageSummary(): Promise<ToolUsageSummaryResult> {
await this.ensureAgentLiteReady();
const since = new Date(Date.now() - 3600_000);

const byTool = new Map<string, {
callCount: number;
durationTotalMs: number;
successCount: number;
toolName: string;
}>();

for (const [, runtime] of this.lifecycle.allRuntimes()) {
const rows = await runtime.getToolUsageSummary(since);

for (const row of rows) {
const existing = byTool.get(row.toolName) ?? {
callCount: 0,
durationTotalMs: 0,
successCount: 0,
toolName: row.toolName,
};

existing.callCount += row.callCount;
existing.successCount += row.successCount;
existing.durationTotalMs += row.avgDurationMs * row.callCount;
byTool.set(row.toolName, existing);
}
}

const rows: ToolUsageSummaryRow[] = [...byTool.values()]
.map((row) => ({
avgDurationMs: row.callCount > 0 ? row.durationTotalMs / row.callCount : 0,
callCount: row.callCount,
successCount: row.successCount,
successRate: row.callCount > 0 ? row.successCount / row.callCount : 0,
toolName: row.toolName,
}))
.sort((left, right) =>
right.callCount - left.callCount || left.toolName.localeCompare(right.toolName));

return {
generatedAt: new Date().toISOString(),
rows,
windowHours: 1,
};
}

/** Starts agent. */
async start() {
seedArtifacts(this.homeDir, this.bundledAgentDir);
Expand Down
11 changes: 11 additions & 0 deletions src/electron/main/runtime/desktop-runtime-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
AgentServiceListener,
AgentServiceSnapshot,
} from '@/shared/agents/agent-runtime';
import type { ToolUsageSummaryResult } from '@/shared/agents/tool-analytics';
import { createMockAgentRuntime } from '@/renderer/features/agents/services/mock-agent-service';
import type {
AgentDefinition,
Expand All @@ -21,6 +22,7 @@ import {

/** Active runtime shape. */
type ActiveRuntime = AgentRuntimeContract & {
getToolUsageSummary?: () => Promise<ToolUsageSummaryResult>;
reloadExternalChannels?: () => Promise<void>;
shutdown?: () => Promise<void>;
};
Expand Down Expand Up @@ -139,6 +141,15 @@ export class DesktopRuntimeController {
return this.activeRuntime.service.getTranscriptPage(agentId, options);
}

/** Returns AgentLite tool usage analytics. */
async getToolUsageSummary() {
return this.activeRuntime.getToolUsageSummary?.() ?? {
generatedAt: new Date().toISOString(),
rows: [],
windowHours: 1,
};
}

/** Reloads external channels. */
async reloadExternalChannels() {
await this.activeRuntime.reloadExternalChannels?.();
Expand Down
41 changes: 39 additions & 2 deletions src/electron/main/runtime/dune-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
RegisterGroupOptions,
} from '@boxlite-ai/agentlite';

import type { ToolUsageSummaryRow } from '@/shared/agents/tool-analytics';

import { DuneChannel } from './dune-channel';

/** ACP peer config understood by newer AgentLite runtimes. */
Expand Down Expand Up @@ -151,16 +153,51 @@ export class DuneAgent {
return this.agent;
}

/** Calls AgentLite's built-in tool_usage_summary action for this agent. */
async getToolUsageSummary(since: Date): Promise<ToolUsageSummaryRow[]> {
const agentWithActions = this.agent as unknown as {
actions?: Map<string, {
handler: (payload: Record<string, unknown>) => Promise<unknown> | unknown;
}>;
db?: {
getToolUsageSummary?: (opts?: {
since?: Date;
toolName?: string;
}) => Promise<ToolUsageSummaryRow[]>;
};
};
const action = agentWithActions.actions?.get('tool_usage_summary');

if (action) {
const result = await action.handler({ since: since.toISOString() });
const summary = (result as { summary?: unknown }).summary;

if (!Array.isArray(summary)) {
throw new Error('AgentLite action "tool_usage_summary" returned an invalid summary.');
}

return summary as ToolUsageSummaryRow[];
}

const fallback = agentWithActions.db?.getToolUsageSummary;

if (!fallback) {
throw new Error('AgentLite action "tool_usage_summary" is unavailable.');
}

return fallback.call(agentWithActions.db, { since });
}

/** Pushes user message. */
async pushUserMessage(chatJid: string, text: string, senderName: string = 'You') {
async pushUserMessage(chatJid: string, text: string, senderName = 'You') {
await this.duneChannel.pushInboundMessage(chatJid, text, senderName);
}

/** Pushes control message. */
async pushControlMessage(
chatJid: string,
text: string,
senderName: string = 'Dune Control',
senderName = 'Dune Control',
) {
await this.duneChannel.pushInboundMessage(chatJid, text, senderName);
}
Expand Down
1 change: 1 addition & 0 deletions src/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const bridge: DesktopBridge = {
getRuntimeSnapshot: () => ipcRenderer.invoke(ipcChannels.getRuntimeSnapshot),
getTelegramSetupSession: (sessionId) =>
ipcRenderer.invoke(ipcChannels.getTelegramSetupSession, sessionId),
getToolUsageSummary: () => ipcRenderer.invoke(ipcChannels.getToolUsageSummary),
listProjectArtifactEntries: (rootPath, artifactFolderName) =>
ipcRenderer.invoke(ipcChannels.listProjectArtifactEntries, rootPath, artifactFolderName),
openExternal: (url) => ipcRenderer.invoke(ipcChannels.openExternal, url),
Expand Down
13 changes: 13 additions & 0 deletions src/renderer/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { AgentWorkspace } from '@/renderer/app/workspaces/AgentWorkspace';
import { SettingsWorkspace } from '@/renderer/app/workspaces/SettingsWorkspace';
import { WorkflowWorkspace } from '@/renderer/app/workspaces/WorkflowWorkspace';
import { PluginsWorkspace } from '@/renderer/app/workspaces/PluginsWorkspace';
import { ToolAnalyticsWorkspace } from '@/renderer/app/workspaces/ToolAnalyticsWorkspace';
import { useDesktopPlatform } from '@/renderer/shared/lib/use-desktop-platform';
import { cn } from '@/renderer/shared/lib/utils';
import { Button } from '@/renderer/shared/ui/button';
Expand Down Expand Up @@ -214,6 +215,10 @@ export default function AppShell() {
controller.handleSidebarDrawerOpenChange(false);
setCreateProjectOpen(true);
},
onOpenAnalytics: () => {
controller.handleSidebarDrawerOpenChange(false);
commands.openToolAnalytics();
},
onOpenPlugins: () => {
controller.handleSidebarDrawerOpenChange(false);
commands.openPlugins();
Expand Down Expand Up @@ -535,6 +540,13 @@ export default function AppShell() {
onToggleSidebar={controller.handleToggleSidebar}
showCompactSidebarToggle={showCompactSidebarToggle}
/>
) : route === 'analytics' ? (
<ToolAnalyticsWorkspace
isCompactShell={isCompactShell}
isSidebarOpen={controller.isSidebarDrawerOpen}
onToggleSidebar={controller.handleToggleSidebar}
showCompactSidebarToggle={showCompactSidebarToggle}
/>
) : (
<SettingsWorkspace
agents={agents}
Expand Down Expand Up @@ -594,6 +606,7 @@ export default function AppShell() {
onOpenChange={commands.setCommandOpen}
onOpenBoard={commands.openWorkflow}
onOpenSettings={controller.handleOpenSettings}
onOpenToolAnalytics={commands.openToolAnalytics}
onSelectAgent={controller.handleSelectAgent}
onSelectItem={commands.openItem}
onSelectProject={(projectId) => {
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/app/shell/AppSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// App sidebar UI.

import {
BarChart3,
Blocks,
Command,
Plus,
Expand All @@ -24,6 +25,7 @@ import {
/** Workflow sidebar state. */
interface WorkflowSidebarState {
onCreateProject: () => void;
onOpenAnalytics: () => void;
onOpenPlugins: () => void;
onOpenSettings: () => void;
onSelectProject: (projectId: string) => void;
Expand Down Expand Up @@ -95,6 +97,21 @@ export function AppSidebar({
</div>

<div className="mt-5 space-y-1">
<button
aria-current={route === 'analytics' ? 'page' : undefined}
className={cn(
'flex w-full items-center gap-3 rounded-[14px] px-3 py-2.5 text-left text-sm font-medium transition-colors',
route === 'analytics'
? 'bg-app-accent-soft text-app-text'
: 'text-app-text hover:bg-app-card',
)}
onClick={workflow.onOpenAnalytics}
type="button"
>
<BarChart3 className="h-4 w-4 shrink-0 text-app-muted" />
<span>Tool Analytics</span>
</button>

<button
aria-current={route === 'plugins' ? 'page' : undefined}
className={cn(
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/app/shell/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { startTransition } from 'react';
import {
BarChart3,
Bot,
PanelRight,
Plus,
Expand Down Expand Up @@ -58,6 +59,7 @@ interface CommandMenuProps {
onOpenBoard: () => void;
onOpenChange: (open: boolean) => void;
onOpenSettings: () => void;
onOpenToolAnalytics: () => void;
onSelectAgent: (agentId: string) => void;
onSelectItem: (itemId: string) => void;
onSelectProject: (projectId: string) => void;
Expand All @@ -77,6 +79,7 @@ export function CommandMenu({
onOpenBoard,
onOpenChange,
onOpenSettings,
onOpenToolAnalytics,
onSelectAgent,
onSelectItem,
onSelectProject,
Expand Down Expand Up @@ -120,6 +123,11 @@ export function CommandMenu({
<span className="flex-1 truncate">Project board</span>
</CommandItem>

<CommandItem onSelect={() => closeAndRun(onOpenToolAnalytics)}>
<BarChart3 className="h-4 w-4 text-app-muted" />
<span className="flex-1 truncate">Tool Analytics</span>
</CommandItem>

<CommandItem onSelect={() => closeAndRun(onOpenSettings)}>
<Settings2 className="h-4 w-4 text-app-muted" />
<span className="flex-1 truncate">Settings</span>
Expand Down
11 changes: 11 additions & 0 deletions src/renderer/app/store/app-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ export function openPlugins() {
});
}

/** Opens tool analytics. */
export function openToolAnalytics() {
withNavigationChange(() => {
const state = useAppStore.getState();

state.setCommandOpen(false);
state.setRoute('analytics');
});
}

/** Opens workflow. */
export function openWorkflow(projectId?: string | null) {
openProjectView('board', projectId);
Expand Down Expand Up @@ -372,6 +382,7 @@ export function useAppCommands() {
openProjectActivity,
openProjectSettings,
openSettings,
openToolAnalytics,
openWorkflow,
setCommandOpen,
setDraft,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/app/store/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import type {
} from '@/renderer/features/workflow/types';

/** App route shape. */
export type AppRoute = 'agent' | 'plugins' | 'settings' | 'workflow';
export type AppRoute = 'agent' | 'analytics' | 'plugins' | 'settings' | 'workflow';

/** Navigation snapshot. */
export interface NavigationSnapshot {
Expand Down
Loading