mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-21 14:34:32 +00:00
fix(optimize): label connector actions clearly
This commit is contained in:
parent
ad9fa70587
commit
1ea3d68e71
7 changed files with 144 additions and 8 deletions
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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, '─')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)')
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue