From 1ea3d68e7167760f0d3b5019981db638bb73d519 Mon Sep 17 00:00:00 2001 From: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:51:57 +0530 Subject: [PATCH] fix(optimize): label connector actions clearly --- app/renderer/lib/types.ts | 2 +- app/renderer/sections/Optimize.test.tsx | 23 +++++++++ src/dashboard.tsx | 2 + src/optimize.ts | 14 ++++-- tests/dashboard.test.ts | 41 +++++++++++++++ tests/mcp-coverage.test.ts | 66 ++++++++++++++++++++++++- tests/optimize.test.ts | 4 +- 7 files changed, 144 insertions(+), 8 deletions(-) diff --git a/app/renderer/lib/types.ts b/app/renderer/lib/types.ts index 25394087..19a652c0 100644 --- a/app/renderer/lib/types.ts +++ b/app/renderer/lib/types.ts @@ -403,7 +403,7 @@ export type SpendFlow = { // ————— src/optimize.ts ————— export type WasteAction = - | { type: 'paste'; label: string; text: string; destination?: 'claude-md' | 'session-opener' | 'prompt' | 'shell-config' } + | { type: 'paste'; label: string; text: string; destination?: 'claude-md' | 'session-opener' | 'prompt' | 'shell-config' | 'manual' } | { type: 'command'; label: string; text: string } | { type: 'file-content'; label: string; path: string; content: string } diff --git a/app/renderer/sections/Optimize.test.tsx b/app/renderer/sections/Optimize.test.tsx index 5219afaf..f0ef0449 100644 --- a/app/renderer/sections/Optimize.test.tsx +++ b/app/renderer/sections/Optimize.test.tsx @@ -160,6 +160,29 @@ describe('Optimize', () => { expect(screen.getByText('{"batch":true}')).toBeInTheDocument() }) + it('renders and copies connector guidance as a manual action', async () => { + const report = makeOptimizeReport() + report.findings.push({ + id: 'mcp-low-coverage', title: 'Underused claude.ai connector', + explanation: 'The connector loads unused tools.', severity: 'medium', + trend: null, tokensSaved: 2_000, estimatedSavingsUSD: 1, + fix: { + type: 'paste', destination: 'manual', label: 'Manage the connector where it loads:', + text: 'Open /mcp and disable claude.ai Google Calendar.', + }, + }) + getOptimizeReport.mockResolvedValue(report) + render() + const row = await screen.findByRole('button', { name: /Underused claude.ai connector/ }) + fireEvent.click(row) + + expect(screen.getByText('Manage the connector where it loads:')).toBeInTheDocument() + expect(screen.getByText('Open /mcp and disable claude.ai Google Calendar.')).toBeInTheDocument() + expect(row.parentElement?.querySelector('.opt-fix')).toHaveClass('opt-fix-paste') + fireEvent.click(screen.getByRole('button', { name: 'Copy' })) + await waitFor(() => expect(writeText).toHaveBeenCalledWith('Open /mcp and disable claude.ai Google Calendar.')) + }) + it('switches to Reverts and Abandoned and shows only the matching yield details', async () => { render() await screen.findByText('Opus is doing your small talk') diff --git a/src/dashboard.tsx b/src/dashboard.tsx index b66b41cf..4c24579a 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -1046,6 +1046,8 @@ function actionDestinationHeader(action: WasteAction): string { return '── Ask Claude in the current session '.padEnd(64, '─') case 'shell-config': return '── Add to your shell config '.padEnd(64, '─') + case 'manual': + return '── Manual action '.padEnd(64, '─') default: return '── Suggested action '.padEnd(64, '─') } diff --git a/src/optimize.ts b/src/optimize.ts index d1b04769..b5284cb9 100644 --- a/src/optimize.ts +++ b/src/optimize.ts @@ -232,6 +232,7 @@ export type PasteDestination = | 'session-opener' // one-time paste at the start of a NEW session | 'prompt' // one-time ask in the current Claude conversation | 'shell-config' // append to ~/.zshrc / ~/.bashrc + | 'manual' // instructions the user carries out directly export type WasteAction = | { type: 'paste'; label: string; text: string; destination?: PasteDestination } @@ -1083,8 +1084,14 @@ export function detectMcpToolCoverage( // so `claude mcp remove` and the file-editing apply plan cannot own them. // Coverage is aggregate here; project-level config attribution is deliberately // out of scope, hence the instruction to inspect /mcp per affected project. + const connectorLabels = connectorServers.map(server => + `claude.ai ${server.slice('claude_ai_'.length).replaceAll('_', ' ')}`, + ) + const connectorEvidence = connectorServers.map((server, index) => + `${connectorLabels[index]} (${server})`, + ) const connectorGuidance = connectorServers.length > 0 - ? ` ${connectorServers.join(', ')} ${connectorServers.length === 1 ? 'is a claude.ai connector namespace' : 'are claude.ai connector namespaces'}, separate from any similarly named local MCP server. Transcript inventory is aggregated across the selected projects; use /mcp in each project where ${connectorServers.length === 1 ? 'it loads' : 'they load'}, or manage ${connectorServers.length === 1 ? 'it' : 'them'} in claude.ai Settings > Connectors.` + ? ` ${connectorEvidence.join(', ')} ${connectorServers.length === 1 ? 'is a claude.ai connector namespace' : 'are claude.ai connector namespaces'}, separate from any similarly named local MCP server. Transcript inventory is aggregated across the selected projects; use /mcp in each project where ${connectorServers.length === 1 ? 'it loads' : 'they load'}, or manage ${connectorServers.length === 1 ? 'it' : 'them'} in claude.ai Settings > Connectors.` : '' const fix: WasteAction = localServers.length > 0 ? { @@ -1096,11 +1103,11 @@ export function detectMcpToolCoverage( } : { type: 'paste', - destination: 'prompt', + destination: 'manual', label: connectorServers.length === 1 ? 'Manage the underused claude.ai connector where it loads:' : 'Manage the underused claude.ai connectors where they load:', - text: `Open /mcp in each affected project and disable ${connectorServers.join(', ')}, or manage ${connectorServers.length === 1 ? 'it' : 'them'} in claude.ai Settings > Connectors.`, + text: `Open /mcp in each affected project and disable ${connectorLabels.join(', ')}, or manage ${connectorServers.length === 1 ? 'it' : 'them'} in claude.ai Settings > Connectors.`, } return { @@ -3152,6 +3159,7 @@ function renderActionHeader(action: WasteAction): string { case 'session-opener': return fillTo('One-time session opener (do NOT add to CLAUDE.md)') case 'prompt': return fillTo('Ask Claude in the current session') case 'shell-config': return fillTo('Add to your shell config') + case 'manual': return fillTo('Manual action') default: return fillTo('Suggested action') } } diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts index 9879c8a1..d121a2cb 100644 --- a/tests/dashboard.test.ts +++ b/tests/dashboard.test.ts @@ -395,6 +395,47 @@ describe('interactive terminal rendering', () => { expect(INTERACTIVE_RENDER_OPTIONS).toMatchObject({ alternateScreen: true }) }) + it('labels claude.ai connector remediation as a manual action', async () => { + const stdin = new PassThrough() as PassThrough & NodeJS.ReadStream + const stdout = new PassThrough() as PassThrough & NodeJS.WriteStream + stdin.isTTY = true + stdin.setRawMode = () => stdin + stdin.ref = () => stdin + stdin.unref = () => stdin + stdout.isTTY = true + stdout.columns = 120 + stdout.rows = 50 + const frames: string[] = [] + stdout.on('data', chunk => frames.push(stripAnsi(String(chunk)))) + + const inventory = Array.from({ length: 20 }, (_, i) => `mcp__claude_ai_Google_Calendar__t${i}`) + const sessions = ['connector-a', 'connector-b'].map((id, index) => { + const session = makeSession(id, 91.337 + index) + session.mcpInventory = inventory + return session + }) + const app = render(React.createElement(InteractiveDashboard, { + initialProjects: [makeProject('connector-manual-action', sessions)], + initialPeriod: 'today', + initialProvider: 'all', + refreshSeconds: 0, + windowColumns: 120, + }), { stdin, stdout, debug: true, interactive: true, patchConsole: false }) + onTestFinished(() => app.unmount()) + + await app.waitUntilRenderFlush() + stdin.write('o') + let frame = '' + for (let i = 0; i < 100 && !frame.includes('Manual action'); i++) { + await new Promise(resolve => setTimeout(resolve, 10)) + frame = frames.filter(value => value.trim()).at(-1) ?? '' + } + + expect(frame).toContain('Manual action') + expect(frame).toContain('claude.ai Google Calendar') + expect(frame).not.toContain('Ask Claude in the current session') + }) + it('leaves resize frame synchronization entirely to Ink', () => { const source = readFileSync(new URL('../src/dashboard.tsx', import.meta.url), 'utf8') expect(source).not.toContain('process.stdout.write(BSU)') diff --git a/tests/mcp-coverage.test.ts b/tests/mcp-coverage.test.ts index c036301b..6a18c325 100644 --- a/tests/mcp-coverage.test.ts +++ b/tests/mcp-coverage.test.ts @@ -1,10 +1,12 @@ -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { aggregateMcpCoverage, + buildOptimizeJsonReport, detectMcpProfileAdvisor, detectMcpToolCoverage, estimateMcpSchemaCost, + runOptimize, } from '../src/optimize.js' import type { ClassifiedTurn, @@ -346,19 +348,78 @@ describe('detectMcpToolCoverage', () => { expect(finding).not.toBeNull() expect(finding!.tokensSaved).toBe(20_000) + // Keep the transcript namespace as evidence, but name the connector the + // way users actually see it in /mcp and claude.ai Settings. expect(finding!.explanation).toContain(server) + expect(finding!.explanation).toContain('claude.ai Netlify') expect(finding!.explanation).toContain('/mcp') expect(finding!.explanation).toContain('claude.ai Settings > Connectors') expect(finding!.fix.type).toBe('paste') if (finding!.fix.type === 'paste') { - expect(finding!.fix.destination).toBe('prompt') + expect(finding!.fix.destination).toBe('manual') expect(finding!.fix.text).toContain('/mcp') + expect(finding!.fix.text).toContain('claude.ai Netlify') + expect(finding!.fix.text).not.toContain(server) expect(finding!.fix.text).toContain('claude.ai Settings > Connectors') } expect(JSON.stringify(finding)).not.toContain('claude mcp remove') expect(finding!.apply).toBeUndefined() }) + it('renders connector-only remediation as a manual action, never an Ask Claude prompt', async () => { + const server = 'claude_ai_Google_Calendar' + const inventory = Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`) + const turns = [makeTurn([makeCall({ cacheCreation: 50_000 })])] + const projects = [project([ + makeSession({ sessionId: 'a', inventory, turns }), + makeSession({ sessionId: 'b', inventory, turns }), + ])] + const log = vi.spyOn(console, 'log').mockImplementation(() => undefined) + + try { + await runOptimize(projects, 'Test period') + const output = log.mock.calls.map(args => args.join(' ')).join('\n') + expect(output).toContain('Manual action') + expect(output).toContain('claude.ai Google Calendar') + expect(output).not.toContain('Ask Claude in the current session') + } finally { + log.mockRestore() + } + }) + + it('keeps the public optimize JSON envelope while marking connector guidance manual', () => { + const server = 'claude_ai_Slack' + const coverage = [{ + server, + toolsAvailable: 20, + toolsInvoked: 0, + unusedTools: Array.from({ length: 20 }, (_, i) => `mcp__${server}__t${i}`), + invocations: 0, + loadedSessions: 2, + coverageRatio: 0, + }] + const finding = detectMcpToolCoverage([], coverage)! + + const report = buildOptimizeJsonReport([], 'Test period', { + findings: [finding], + costRate: 0, + healthScore: 90, + healthGrade: 'A', + }) + + expect(report.findings[0]).toMatchObject({ + id: 'mcp-low-coverage', + tokensSaved: 0, + fix: { + type: 'paste', + destination: 'manual', + text: expect.stringContaining('claude.ai Slack'), + }, + }) + expect(report.findings[0]).not.toHaveProperty('apply') + expect(report.findings[0]).not.toHaveProperty('applyTokensSaved') + }) + it('pluralises manual guidance when only claude.ai connectors are flagged', () => { const coverage = ['claude_ai_Slack', 'claude_ai_Google_Calendar'].map(server => ({ server, @@ -375,6 +436,7 @@ describe('detectMcpToolCoverage', () => { expect(finding).not.toBeNull() expect(finding!.fix).toMatchObject({ type: 'paste', + destination: 'manual', label: 'Manage the underused claude.ai connectors where they load:', }) if (finding!.fix.type === 'paste') { diff --git a/tests/optimize.test.ts b/tests/optimize.test.ts index bc72dd88..db14c94d 100644 --- a/tests/optimize.test.ts +++ b/tests/optimize.test.ts @@ -1197,9 +1197,9 @@ describe('paste-fix destination tagging (issue #277)', () => { if (f.fix.type === 'paste') { expect( f.fix.destination, - `finding "${f.title}" has paste fix without destination — pick one of: claude-md / session-opener / prompt / shell-config` + `finding "${f.title}" has paste fix without destination — pick one of: claude-md / session-opener / prompt / shell-config / manual` ).toBeDefined() - expect(['claude-md', 'session-opener', 'prompt', 'shell-config']) + expect(['claude-md', 'session-opener', 'prompt', 'shell-config', 'manual']) .toContain(f.fix.destination) } }