fix(optimize): label connector actions clearly

This commit is contained in:
Aditya Vikram Singh 2026-08-12 21:51:57 +05:30
parent ad9fa70587
commit 1ea3d68e71
7 changed files with 144 additions and 8 deletions

View file

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

View file

@ -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(<Optimize period="30days" provider="all" />)
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(<Optimize period="30days" provider="all" />)
await screen.findByText('Opus is doing your small talk')

View file

@ -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, '─')
}

View file

@ -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')
}
}

View file

@ -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)')

View file

@ -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') {

View file

@ -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)
}
}