Skip to content

Commit 46e43a0

Browse files
authored
Label optimize suggestions by destination (#281)
Closes #277. Every paste-style fix now declares an explicit `destination` so users can tell at a glance whether a suggestion belongs in CLAUDE.md as a permanent rule, in a one-time session opener, in the current chat as an ask, or in a shell config file. Previously the prompts had no labeled home and users were dropping one-time session openers into CLAUDE.md as permanent rules. Type changes: - New `PasteDestination` union: `claude-md` / `session-opener` / `prompt` / `shell-config` - `WasteAction.paste` gains `destination?: PasteDestination` Renderer changes: - CLI `optimize` command (renderOptimize → renderFinding) prints a section header above each fix block: -- Suggested CLAUDE.md addition (permanent rule) ─── -- One-time session opener (do NOT add to CLAUDE.md) ─── -- Ask Claude in the current session ─── -- Add to your shell config ─── -- Run this command ─── - Interactive dashboard (FindingAction in dashboard.tsx) gets the same treatment so the in-popover findings list reads identically. Existing fixes retagged appropriately. Two existing prompts that lacked destination context altogether ("Set a delivery checkpoint at the start of the next expensive thread", "Start the next expensive thread with a fresh-context constraint") now read as one-time session openers with a clear "do not add to CLAUDE.md" hint — the exact failure mode the reporter described. Tests: - Existing `detectJunkReads` test extended to assert the destination tag. - New regression block walks every detector that emits a paste-style fix and asserts each one declares a destination — future detectors that ship without one get caught here.
1 parent 8208cf8 commit 46e43a0

5 files changed

Lines changed: 155 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## Unreleased
44

5+
### Changed (CLI)
6+
- **`optimize` suggestions now declare their destination.** Every paste-style
7+
fix carries an explicit destination — `claude-md` (permanent project rule),
8+
`session-opener` (one-time paste at the start of a future session),
9+
`prompt` (one-time ask in the current chat), or `shell-config` (append to
10+
`~/.zshrc` / `~/.bashrc`). Output renders a clearly-labeled section header
11+
per destination so users no longer accidentally bake one-time session
12+
openers into their CLAUDE.md as permanent rules. Closes #277.
13+
514
## 0.9.7 - 2026-05-07
615

716
### Added (CLI)

src/dashboard.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,9 +535,43 @@ function PeriodTabs({ active, providerName, showProvider }: { active: Period; pr
535535
)
536536
}
537537

538+
/// Header for an action's intended destination. Helps users distinguish a
539+
/// permanent CLAUDE.md rule from a one-time session opener so they don't
540+
/// accidentally bake a single-run constraint into their project's permanent
541+
/// instructions. Issue #277.
542+
function actionDestinationHeader(action: WasteAction): string {
543+
switch (action.type) {
544+
case 'file-content':
545+
return `── Suggested ${action.path} addition `.padEnd(64, '─')
546+
case 'command':
547+
return '── Run this command '.padEnd(64, '─')
548+
case 'paste': {
549+
switch (action.destination) {
550+
case 'claude-md':
551+
return '── Suggested CLAUDE.md addition (permanent rule) '.padEnd(64, '─')
552+
case 'session-opener':
553+
return '── One-time session opener (do not add to CLAUDE.md) '.padEnd(64, '─')
554+
case 'prompt':
555+
return '── Ask Claude in the current session '.padEnd(64, '─')
556+
case 'shell-config':
557+
return '── Add to your shell config '.padEnd(64, '─')
558+
default:
559+
return '── Suggested action '.padEnd(64, '─')
560+
}
561+
}
562+
}
563+
}
564+
538565
function FindingAction({ action }: { action: WasteAction }) {
539566
const lines = action.type === 'file-content' ? action.content.split('\n') : action.type === 'command' ? action.text.split('\n') : [action.text]
540-
return (<><Text dimColor>{action.label}</Text>{lines.map((line, i) => <Text key={i} color="#5BF5E0"> {line}</Text>)}</>)
567+
const header = actionDestinationHeader(action)
568+
return (
569+
<>
570+
<Text color={ORANGE}>{header}</Text>
571+
<Text dimColor>{action.label}</Text>
572+
{lines.map((line, i) => <Text key={i} color="#5BF5E0"> {line}</Text>)}
573+
</>
574+
)
541575
}
542576

543577
function FindingPanel({ index, finding, costRate, width }: { index: number; finding: WasteFinding; costRate: number; width: number }) {

src/data/litellm-snapshot.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/optimize.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,20 @@ const GHOST_CLEANUP_COMMANDS_LIMIT = 10
149149
export type Impact = 'high' | 'medium' | 'low'
150150
export type HealthGrade = 'A' | 'B' | 'C' | 'D' | 'F'
151151

152+
/// Where a paste-style suggestion belongs. Without this, users couldn't tell
153+
/// whether a prompt should go into CLAUDE.md (permanent rule), be pasted at
154+
/// the start of a future session (one-time constraint), be asked of Claude
155+
/// in the current chat (one-time prompt), or be added to a shell config file.
156+
/// Issue #277 — users were dropping one-time session openers into CLAUDE.md
157+
/// permanently because the destination wasn't clearly stated.
158+
export type PasteDestination =
159+
| 'claude-md' // permanent project rule, append to CLAUDE.md
160+
| 'session-opener' // one-time paste at the start of a NEW session
161+
| 'prompt' // one-time ask in the current Claude conversation
162+
| 'shell-config' // append to ~/.zshrc / ~/.bashrc
163+
152164
export type WasteAction =
153-
| { type: 'paste'; label: string; text: string }
165+
| { type: 'paste'; label: string; text: string; destination?: PasteDestination }
154166
| { type: 'command'; label: string; text: string }
155167
| { type: 'file-content'; label: string; path: string; content: string }
156168

@@ -454,6 +466,7 @@ export function detectJunkReads(calls: ToolCall[], dateRange?: DateRange): Waste
454466
tokensSaved,
455467
fix: {
456468
type: 'paste',
469+
destination: 'claude-md',
457470
label: 'Append to your project CLAUDE.md:',
458471
text: `Do not read or search files under these directories unless I explicitly ask: ${dirsToAvoid}.`,
459472
},
@@ -513,6 +526,7 @@ export function detectDuplicateReads(calls: ToolCall[], dateRange?: DateRange):
513526
tokensSaved,
514527
fix: {
515528
type: 'paste',
529+
destination: 'prompt',
516530
label: 'Point Claude at exact locations in your prompt, for example:',
517531
text: 'In <file> lines <start>-<end>, look at the <function> function.',
518532
},
@@ -960,7 +974,8 @@ export function detectBloatedClaudeMd(projectCwds: Set<string>): WasteFinding |
960974
tokensSaved,
961975
fix: {
962976
type: 'paste',
963-
label: 'Ask Claude to trim it:',
977+
destination: 'prompt',
978+
label: 'Ask Claude in the current session to trim it:',
964979
text: `Review CLAUDE.md and all @-imported files. Cut total expanded content to under ${CLAUDEMD_HEALTHY_LINES} lines. Remove anything Claude can figure out from the code itself. Keep only rules, gotchas, and non-obvious conventions.`,
965980
},
966981
}
@@ -1007,6 +1022,7 @@ export function detectLowReadEditRatio(calls: ToolCall[]): WasteFinding | null {
10071022
tokensSaved,
10081023
fix: {
10091024
type: 'paste',
1025+
destination: 'claude-md',
10101026
label: 'Add to your CLAUDE.md:',
10111027
text: 'Before editing any file, read it first. Before modifying a function, grep for all callers. Research before you edit.',
10121028
},
@@ -1077,7 +1093,8 @@ export function detectCacheBloat(apiCalls: ApiCallMeta[], projects: ProjectSumma
10771093
tokensSaved,
10781094
fix: {
10791095
type: 'paste',
1080-
label: 'Check for recent Claude Code updates or heavy MCP/skill additions. As a workaround (not officially supported):',
1096+
destination: 'shell-config',
1097+
label: 'Check for recent Claude Code updates or heavy MCP/skill additions. As a workaround (not officially supported), add to ~/.zshrc or ~/.bashrc:',
10811098
text: 'export ANTHROPIC_CUSTOM_HEADERS=\'User-Agent: claude-cli/2.1.98 (external, sdk-cli)\'',
10821099
},
10831100
trend,
@@ -1226,6 +1243,7 @@ export function detectBashBloat(): WasteFinding | null {
12261243
tokensSaved,
12271244
fix: {
12281245
type: 'paste',
1246+
destination: 'shell-config',
12291247
label: 'Add to ~/.zshrc or ~/.bashrc:',
12301248
text: `export BASH_MAX_OUTPUT_LENGTH=${BASH_RECOMMENDED_LIMIT}`,
12311249
},
@@ -1417,7 +1435,8 @@ export function detectLowWorthSessions(projects: ProjectSummary[]): WasteFinding
14171435
tokensSaved,
14181436
fix: {
14191437
type: 'paste',
1420-
label: 'Set a delivery checkpoint at the start of the next expensive thread:',
1438+
destination: 'session-opener',
1439+
label: 'Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):',
14211440
text: 'Before continuing, name the deliverable in one sentence (PR title, file changed, command output you expect). Stop and check with me if (a) you spend more than 10 minutes without an edit, or (b) the same approach fails twice. Do not retry past two attempts on any single fix.',
14221441
},
14231442
}
@@ -1529,7 +1548,8 @@ export function detectContextBloat(projects: ProjectSummary[], excludedSessionId
15291548
tokensSaved,
15301549
fix: {
15311550
type: 'paste',
1532-
label: 'Start the next expensive thread with a fresh-context constraint:',
1551+
destination: 'session-opener',
1552+
label: 'Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):',
15331553
text: 'Start fresh before continuing. Use only the current goal, the relevant files, the failing command/output, and the constraints below. Restate the working context in under 10 bullets before editing.',
15341554
},
15351555
}
@@ -1598,7 +1618,8 @@ export function detectSessionOutliers(projects: ProjectSummary[], excludedSessio
15981618
tokensSaved,
15991619
fix: {
16001620
type: 'paste',
1601-
label: 'For expensive work, start with a tighter operating constraint:',
1621+
destination: 'session-opener',
1622+
label: 'Paste at the start of your NEXT expensive thread (one-time, do not add to CLAUDE.md):',
16021623
text: 'Before making changes, summarize the smallest viable plan. Keep context narrow, avoid broad searches, and stop after the first working patch so I can review before continuing.',
16031624
},
16041625
}
@@ -1786,6 +1807,33 @@ function wrap(text: string, width: number, indent: string): string {
17861807
return lines.join('\n')
17871808
}
17881809

1810+
/// Section header for a finding's fix block, declaring its intended
1811+
/// destination. Issue #277: users were dropping one-time session openers
1812+
/// into CLAUDE.md as permanent rules because the prompts had no labeled
1813+
/// home in the output.
1814+
function renderActionHeader(action: WasteAction): string {
1815+
const headerWidth = PANEL_WIDTH - 4
1816+
const fillTo = (label: string): string => {
1817+
const inner = ` ${label} `
1818+
const trailing = Math.max(2, headerWidth - inner.length - 4)
1819+
return `--${inner}${SEP.repeat(trailing)}`.padEnd(headerWidth)
1820+
}
1821+
switch (action.type) {
1822+
case 'file-content':
1823+
return fillTo(`Suggested ${action.path} addition`)
1824+
case 'command':
1825+
return fillTo('Run this command')
1826+
case 'paste':
1827+
switch (action.destination) {
1828+
case 'claude-md': return fillTo('Suggested CLAUDE.md addition (permanent rule)')
1829+
case 'session-opener': return fillTo('One-time session opener (do NOT add to CLAUDE.md)')
1830+
case 'prompt': return fillTo('Ask Claude in the current session')
1831+
case 'shell-config': return fillTo('Add to your shell config')
1832+
default: return fillTo('Suggested action')
1833+
}
1834+
}
1835+
}
1836+
17891837
function renderFinding(n: number, f: WasteFinding, costRate: number): string[] {
17901838
const lines: string[] = []
17911839
const costSaved = f.tokensSaved * costRate
@@ -1807,16 +1855,19 @@ function renderFinding(n: number, f: WasteFinding, costRate: number): string[] {
18071855
lines.push(chalk.hex(GOLD)(` Potential savings: ${savings}`))
18081856
lines.push('')
18091857

1858+
// Destination header — issue #277. Tells the user where each suggestion
1859+
// belongs (CLAUDE.md / session opener / current chat / shell config) so
1860+
// permanent rules and one-time prompts are no longer interchangeable in
1861+
// the output.
18101862
const a = f.fix
1863+
lines.push(chalk.hex(ORANGE)(` ${renderActionHeader(a)}`))
1864+
lines.push(chalk.hex(DIM)(` ${a.label}`))
18111865
if (a.type === 'file-content') {
1812-
lines.push(chalk.hex(DIM)(` ${a.label}`))
18131866
for (const line of a.content.split('\n')) lines.push(chalk.hex(CYAN)(` ${line}`))
18141867
} else if (a.type === 'command') {
1815-
lines.push(chalk.hex(DIM)(` ${a.label}`))
18161868
for (const line of a.text.split('\n')) lines.push(chalk.hex(CYAN)(` ${line}`))
18171869
} else {
1818-
lines.push(chalk.hex(DIM)(` ${a.label}`))
1819-
lines.push(chalk.hex(CYAN)(` ${a.text}`))
1870+
for (const line of a.text.split('\n')) lines.push(chalk.hex(CYAN)(` ${line}`))
18201871
}
18211872
lines.push('')
18221873
return lines

tests/optimize.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ describe('detectJunkReads', () => {
157157
expect(finding.fix.type).toBe('paste')
158158
if (finding.fix.type === 'paste') {
159159
expect(finding.fix.text).toContain('node_modules')
160+
// Issue #277: every paste-style fix should declare its destination so
161+
// users can tell a permanent CLAUDE.md rule from a one-time session
162+
// opener at a glance.
163+
expect(finding.fix.destination).toBe('claude-md')
160164
}
161165
expect(finding.fix.label).toContain('CLAUDE.md')
162166
})
@@ -915,3 +919,48 @@ describe('computeTrend', () => {
915919
expect(trend).toBe('active')
916920
})
917921
})
922+
923+
describe('paste-fix destination tagging (issue #277)', () => {
924+
// Walks every emitted finding's fix and asserts that `paste`-type actions
925+
// declare a destination. Future detectors that ship a paste fix without a
926+
// destination get caught here so users never see an unlabeled "here's a
927+
// suggestion" block again.
928+
function checkAllPasteFixesHaveDestination(findings: WasteFinding[]) {
929+
for (const f of findings) {
930+
if (f.fix.type === 'paste') {
931+
expect(
932+
f.fix.destination,
933+
`finding "${f.title}" has paste fix without destination — pick one of: claude-md / session-opener / prompt / shell-config`
934+
).toBeDefined()
935+
expect(['claude-md', 'session-opener', 'prompt', 'shell-config'])
936+
.toContain(f.fix.destination)
937+
}
938+
}
939+
}
940+
941+
it('detectJunkReads emits a tagged paste fix', () => {
942+
const calls = Array.from({ length: 5 }, () => call('Read', { file_path: '/x/node_modules/a.js' }))
943+
checkAllPasteFixesHaveDestination([detectJunkReads(calls)!])
944+
})
945+
946+
it('detectDuplicateReads emits a tagged paste fix', () => {
947+
const calls = [
948+
...Array.from({ length: 6 }, () => call('Read', { file_path: '/src/a.ts' }, 's1')),
949+
...Array.from({ length: 6 }, () => call('Read', { file_path: '/src/b.ts' }, 's1')),
950+
...Array.from({ length: 6 }, () => call('Read', { file_path: '/src/c.ts' }, 's1')),
951+
]
952+
checkAllPasteFixesHaveDestination([detectDuplicateReads(calls)!])
953+
})
954+
955+
it('detectLowReadEditRatio emits a tagged paste fix', () => {
956+
const calls = [
957+
...Array.from({ length: 5 }, () => call('Edit', { file_path: '/src/a.ts' })),
958+
...Array.from({ length: 5 }, () => call('Edit', { file_path: '/src/b.ts' })),
959+
...Array.from({ length: 5 }, () => call('Edit', { file_path: '/src/c.ts' })),
960+
...Array.from({ length: 5 }, () => call('Edit', { file_path: '/src/d.ts' })),
961+
...Array.from({ length: 5 }, () => call('Edit', { file_path: '/src/e.ts' })),
962+
]
963+
const finding = detectLowReadEditRatio(calls)
964+
if (finding) checkAllPasteFixesHaveDestination([finding])
965+
})
966+
})

0 commit comments

Comments
 (0)