tokengolf 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +4 -0
- package/.prettierignore +2 -0
- package/.prettierrc +6 -0
- package/.vscode/settings.json +15 -0
- package/CHANGELOG.md +254 -0
- package/CLAUDE.md +136 -10
- package/README.md +89 -47
- package/assets/demo-hud.png +0 -0
- package/assets/scorecard.png +0 -0
- package/dist/cli.js +790 -103
- package/docs/assets/demo-hud.png +0 -0
- package/docs/assets/scorecard.png +0 -0
- package/docs/assets/tokengolf-bg-min.jpg +0 -0
- package/docs/index.html +1080 -0
- package/eslint.config.js +39 -0
- package/hooks/post-tool-use-failure.js +27 -0
- package/hooks/post-tool-use.js +11 -7
- package/hooks/pre-compact.js +9 -3
- package/hooks/session-end.js +168 -42
- package/hooks/session-start.js +31 -11
- package/hooks/session-stop.js +6 -2
- package/hooks/statusline.sh +16 -7
- package/hooks/stop.js +27 -0
- package/hooks/subagent-start.js +27 -0
- package/hooks/user-prompt-submit.js +8 -6
- package/package.json +16 -3
- package/src/cli.js +23 -6
- package/src/components/ActiveRun.js +76 -24
- package/src/components/ScoreCard.js +132 -37
- package/src/components/StartRun.js +156 -53
- package/src/components/StatsView.js +89 -37
- package/src/lib/__tests__/score.test.js +596 -0
- package/src/lib/cost.js +84 -21
- package/src/lib/demo.js +186 -0
- package/src/lib/install.js +92 -62
- package/src/lib/score.js +433 -136
- package/src/lib/store.js +11 -11
- package/.claude/settings.local.json +0 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokengolf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Gamify your Claude Code sessions. Flow mode tracks you. Roguelike mode trains you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "esbuild src/cli.js --bundle --platform=node --format=esm --loader:.js=jsx --packages=external --outfile=dist/cli.js && chmod +x dist/cli.js",
|
|
11
|
-
"prepare": "npm run build",
|
|
11
|
+
"prepare": "npm run build && husky",
|
|
12
12
|
"dev": "node --import tsx/esm src/cli.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"lint": "eslint src/ hooks/",
|
|
16
|
+
"lint:fix": "eslint src/ hooks/ --fix",
|
|
17
|
+
"format": "prettier --write src/ hooks/",
|
|
18
|
+
"format:check": "prettier --check src/ hooks/",
|
|
19
|
+
"version": "git add CHANGELOG.md",
|
|
13
20
|
"postversion": "git push && git push --tags"
|
|
14
21
|
},
|
|
15
22
|
"dependencies": {
|
|
@@ -19,7 +26,13 @@
|
|
|
19
26
|
"react": "^18.0.0"
|
|
20
27
|
},
|
|
21
28
|
"devDependencies": {
|
|
22
|
-
"
|
|
29
|
+
"@eslint/js": "^10.0.1",
|
|
30
|
+
"esbuild": "^0.25.0",
|
|
31
|
+
"eslint": "^10.0.3",
|
|
32
|
+
"eslint-config-prettier": "^10.1.8",
|
|
33
|
+
"husky": "^9.1.7",
|
|
34
|
+
"prettier": "^3.8.1",
|
|
35
|
+
"vitest": "^4.0.18"
|
|
23
36
|
},
|
|
24
37
|
"engines": {
|
|
25
38
|
"node": ">=18.0.0"
|
package/src/cli.js
CHANGED
|
@@ -11,10 +11,7 @@ import { ActiveRun } from './components/ActiveRun.js';
|
|
|
11
11
|
import { ScoreCard } from './components/ScoreCard.js';
|
|
12
12
|
import { StatsView } from './components/StatsView.js';
|
|
13
13
|
|
|
14
|
-
program
|
|
15
|
-
.name('tokengolf')
|
|
16
|
-
.description('⛳ Gamify your Claude Code sessions')
|
|
17
|
-
.version('0.1.0');
|
|
14
|
+
program.name('tokengolf').description('⛳ Gamify your Claude Code sessions').version('0.1.0');
|
|
18
15
|
|
|
19
16
|
program
|
|
20
17
|
.command('start')
|
|
@@ -47,7 +44,13 @@ program
|
|
|
47
44
|
}
|
|
48
45
|
const detected = opts.spent ? null : autoDetectCost(run);
|
|
49
46
|
const spent = opts.spent ? parseFloat(opts.spent) : (detected?.spent ?? run.spent);
|
|
50
|
-
const completed = {
|
|
47
|
+
const completed = {
|
|
48
|
+
...run,
|
|
49
|
+
spent,
|
|
50
|
+
status: 'won',
|
|
51
|
+
modelBreakdown: detected?.modelBreakdown ?? run.modelBreakdown ?? null,
|
|
52
|
+
endedAt: new Date().toISOString(),
|
|
53
|
+
};
|
|
51
54
|
const saved = saveRun(completed);
|
|
52
55
|
clearCurrentRun();
|
|
53
56
|
render(React.createElement(ScoreCard, { run: saved }));
|
|
@@ -65,7 +68,13 @@ program
|
|
|
65
68
|
}
|
|
66
69
|
const detected = opts.spent ? null : autoDetectCost(run);
|
|
67
70
|
const spent = opts.spent ? parseFloat(opts.spent) : (detected?.spent ?? run.budget + 0.01);
|
|
68
|
-
const died = {
|
|
71
|
+
const died = {
|
|
72
|
+
...run,
|
|
73
|
+
spent,
|
|
74
|
+
status: 'died',
|
|
75
|
+
modelBreakdown: detected?.modelBreakdown ?? run.modelBreakdown ?? null,
|
|
76
|
+
endedAt: new Date().toISOString(),
|
|
77
|
+
};
|
|
69
78
|
const saved = saveRun(died);
|
|
70
79
|
clearCurrentRun();
|
|
71
80
|
render(React.createElement(ScoreCard, { run: saved }));
|
|
@@ -104,6 +113,14 @@ program
|
|
|
104
113
|
render(React.createElement(StatsView, { stats: getStats() }));
|
|
105
114
|
});
|
|
106
115
|
|
|
116
|
+
program
|
|
117
|
+
.command('demo')
|
|
118
|
+
.description('Show HUD examples for all game states (great for screenshots)')
|
|
119
|
+
.action(async () => {
|
|
120
|
+
const { runDemo } = await import('./lib/demo.js');
|
|
121
|
+
runDemo();
|
|
122
|
+
});
|
|
123
|
+
|
|
107
124
|
program
|
|
108
125
|
.command('install')
|
|
109
126
|
.description('Install Claude Code hooks into ~/.claude/settings.json')
|
|
@@ -2,7 +2,14 @@ import React, { useState, useEffect } from 'react';
|
|
|
2
2
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
3
|
import { ProgressBar } from '@inkjs/ui';
|
|
4
4
|
import { getCurrentRun } from '../lib/state.js';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getModelClass,
|
|
7
|
+
getEfficiencyRating,
|
|
8
|
+
getBudgetPct,
|
|
9
|
+
formatCost,
|
|
10
|
+
formatElapsed,
|
|
11
|
+
FLOORS,
|
|
12
|
+
} from '../lib/score.js';
|
|
6
13
|
|
|
7
14
|
export function ActiveRun({ run: initialRun }) {
|
|
8
15
|
const { exit } = useApp();
|
|
@@ -13,41 +20,75 @@ export function ActiveRun({ run: initialRun }) {
|
|
|
13
20
|
const interval = setInterval(() => {
|
|
14
21
|
const latest = getCurrentRun();
|
|
15
22
|
if (latest) setRun(latest);
|
|
16
|
-
setTick(t => t + 1);
|
|
23
|
+
setTick((t) => t + 1);
|
|
17
24
|
}, 2000);
|
|
18
25
|
return () => clearInterval(interval);
|
|
19
26
|
}, []);
|
|
20
27
|
|
|
21
|
-
useInput((input) => {
|
|
28
|
+
useInput((input) => {
|
|
29
|
+
if (input === 'q') exit();
|
|
30
|
+
});
|
|
22
31
|
|
|
32
|
+
const flowMode = !run.budget;
|
|
23
33
|
const mc = getModelClass(run.model);
|
|
24
|
-
const pct = getBudgetPct(run.spent, run.budget);
|
|
25
|
-
const efficiency = getEfficiencyRating(run.spent, run.budget);
|
|
26
|
-
const barColor = pct >= 80 ? 'red' : pct >= 50 ? 'yellow' : 'green';
|
|
34
|
+
const pct = flowMode ? null : getBudgetPct(run.spent, run.budget);
|
|
35
|
+
const efficiency = flowMode ? null : getEfficiencyRating(run.spent, run.budget);
|
|
36
|
+
const barColor = !pct ? 'green' : pct >= 80 ? 'red' : pct >= 50 ? 'yellow' : 'green';
|
|
27
37
|
|
|
28
38
|
return (
|
|
29
39
|
<Box flexDirection="column" gap={1} paddingX={1} paddingY={1}>
|
|
30
40
|
<Box gap={2}>
|
|
31
|
-
<Text bold color="yellow"
|
|
41
|
+
<Text bold color="yellow">
|
|
42
|
+
⛳ TokenGolf
|
|
43
|
+
</Text>
|
|
32
44
|
<Text color="gray">Active Run</Text>
|
|
33
|
-
<Text color="gray" dimColor>
|
|
45
|
+
<Text color="gray" dimColor>
|
|
46
|
+
{formatElapsed(run.startedAt)}
|
|
47
|
+
</Text>
|
|
34
48
|
</Box>
|
|
35
49
|
|
|
36
|
-
<Box
|
|
37
|
-
|
|
50
|
+
<Box
|
|
51
|
+
borderStyle="round"
|
|
52
|
+
borderColor="yellow"
|
|
53
|
+
paddingX={1}
|
|
54
|
+
paddingY={1}
|
|
55
|
+
flexDirection="column"
|
|
56
|
+
gap={1}
|
|
57
|
+
>
|
|
58
|
+
<Text bold color="white">
|
|
59
|
+
{run.quest}
|
|
60
|
+
</Text>
|
|
38
61
|
|
|
39
62
|
<Box gap={3}>
|
|
40
|
-
<Text>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
<Text>
|
|
64
|
+
{mc.emoji} <Text color="cyan">{mc.name}</Text>
|
|
65
|
+
</Text>
|
|
66
|
+
{flowMode ? (
|
|
67
|
+
<Text color="gray">Flow Mode</Text>
|
|
68
|
+
) : (
|
|
69
|
+
<Text color="gray">
|
|
70
|
+
Budget <Text color="green">${run.budget.toFixed(2)}</Text>
|
|
71
|
+
</Text>
|
|
72
|
+
)}
|
|
73
|
+
<Text color="gray">
|
|
74
|
+
Spent <Text color={barColor}>{formatCost(run.spent)}</Text>
|
|
75
|
+
</Text>
|
|
76
|
+
{!flowMode && (
|
|
77
|
+
<Text color={efficiency.color}>
|
|
78
|
+
{efficiency.emoji} {efficiency.label}
|
|
79
|
+
</Text>
|
|
80
|
+
)}
|
|
44
81
|
</Box>
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
{!flowMode && (
|
|
84
|
+
<Box gap={1} alignItems="center">
|
|
85
|
+
<Text color="gray">💰 </Text>
|
|
86
|
+
<Box width={24}>
|
|
87
|
+
<ProgressBar value={Math.min(pct, 100)} />
|
|
88
|
+
</Box>
|
|
89
|
+
<Text color={barColor}> {pct}%</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
)}
|
|
51
92
|
|
|
52
93
|
<Box flexDirection="column" gap={0} marginTop={1}>
|
|
53
94
|
{FLOORS.map((floor, i) => {
|
|
@@ -59,7 +100,10 @@ export function ActiveRun({ run: initialRun }) {
|
|
|
59
100
|
<Text color={done ? 'green' : active ? 'yellow' : 'gray'}>
|
|
60
101
|
{done ? '✓' : active ? '▶' : '○'}
|
|
61
102
|
</Text>
|
|
62
|
-
<Text
|
|
103
|
+
<Text
|
|
104
|
+
color={done ? 'green' : active ? 'white' : 'gray'}
|
|
105
|
+
dimColor={!done && !active}
|
|
106
|
+
>
|
|
63
107
|
Floor {n}: {floor}
|
|
64
108
|
</Text>
|
|
65
109
|
</Box>
|
|
@@ -68,18 +112,26 @@ export function ActiveRun({ run: initialRun }) {
|
|
|
68
112
|
</Box>
|
|
69
113
|
|
|
70
114
|
<Box gap={3} marginTop={1}>
|
|
71
|
-
<Text color="gray">
|
|
72
|
-
|
|
115
|
+
<Text color="gray">
|
|
116
|
+
Prompts <Text color="white">{run.promptCount || 0}</Text>
|
|
117
|
+
</Text>
|
|
118
|
+
<Text color="gray">
|
|
119
|
+
Tools <Text color="white">{run.totalToolCalls || 0}</Text>
|
|
120
|
+
</Text>
|
|
73
121
|
</Box>
|
|
74
122
|
|
|
75
123
|
{pct >= 80 && pct < 100 && (
|
|
76
124
|
<Box borderStyle="single" borderColor="red" paddingX={1}>
|
|
77
|
-
<Text color="red" bold
|
|
125
|
+
<Text color="red" bold>
|
|
126
|
+
⚠️ BUDGET WARNING — {formatCost(run.budget - run.spent)} left
|
|
127
|
+
</Text>
|
|
78
128
|
</Box>
|
|
79
129
|
)}
|
|
80
130
|
</Box>
|
|
81
131
|
|
|
82
|
-
<Text color="gray" dimColor>
|
|
132
|
+
<Text color="gray" dimColor>
|
|
133
|
+
tokengolf win [--spent 0.18] · tokengolf bust · q to close
|
|
134
|
+
</Text>
|
|
83
135
|
</Box>
|
|
84
136
|
);
|
|
85
137
|
}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import React, { useEffect } from 'react';
|
|
2
2
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getTier,
|
|
5
|
+
getModelClass,
|
|
6
|
+
getEffortLevel,
|
|
7
|
+
getEfficiencyRating,
|
|
8
|
+
getBudgetPct,
|
|
9
|
+
formatCost,
|
|
10
|
+
getHaikuPct,
|
|
11
|
+
getOpusPct,
|
|
12
|
+
MODEL_CLASSES,
|
|
13
|
+
} from '../lib/score.js';
|
|
4
14
|
|
|
5
15
|
export function ScoreCard({ run }) {
|
|
6
16
|
const { exit } = useApp();
|
|
7
17
|
const won = run.status === 'won';
|
|
8
18
|
|
|
9
|
-
useInput((input) => {
|
|
19
|
+
useInput((input) => {
|
|
20
|
+
if (input === 'q') exit();
|
|
21
|
+
});
|
|
10
22
|
|
|
11
23
|
useEffect(() => {
|
|
12
24
|
const t = setTimeout(() => exit(), 60000);
|
|
@@ -19,74 +31,116 @@ export function ScoreCard({ run }) {
|
|
|
19
31
|
const efficiency = flowMode ? null : getEfficiencyRating(run.spent, run.budget);
|
|
20
32
|
const pct = flowMode ? null : getBudgetPct(run.spent, run.budget);
|
|
21
33
|
const haikuPct = getHaikuPct(run.modelBreakdown, run.spent);
|
|
34
|
+
const opusPct = getOpusPct(run.modelBreakdown, run.spent);
|
|
22
35
|
|
|
23
36
|
return (
|
|
24
37
|
<Box flexDirection="column" paddingX={1} paddingY={1} gap={1}>
|
|
25
|
-
|
|
26
38
|
{/* Big status header */}
|
|
27
|
-
<Box
|
|
28
|
-
|
|
39
|
+
<Box
|
|
40
|
+
borderStyle="double"
|
|
41
|
+
borderColor={won ? 'yellow' : 'red'}
|
|
42
|
+
paddingX={2}
|
|
43
|
+
paddingY={1}
|
|
44
|
+
flexDirection="column"
|
|
45
|
+
gap={1}
|
|
46
|
+
>
|
|
47
|
+
<Text bold color={won ? 'yellow' : 'red'}>
|
|
29
48
|
{won ? '🏆 SESSION COMPLETE' : '💀 BUDGET BUSTED'}
|
|
30
49
|
</Text>
|
|
31
50
|
|
|
32
|
-
<Text color="white" bold>
|
|
51
|
+
<Text color="white" bold>
|
|
52
|
+
{run.quest ?? <Text color="gray">Flow Mode</Text>}
|
|
53
|
+
</Text>
|
|
33
54
|
|
|
34
55
|
{/* Score row */}
|
|
35
56
|
<Box gap={4} flexWrap="wrap" marginTop={1}>
|
|
36
57
|
<Box flexDirection="column">
|
|
37
|
-
<Text color="gray" dimColor>
|
|
38
|
-
|
|
58
|
+
<Text color="gray" dimColor>
|
|
59
|
+
SPENT
|
|
60
|
+
</Text>
|
|
61
|
+
<Text bold color={won ? 'green' : 'red'}>
|
|
62
|
+
{formatCost(run.spent)}
|
|
63
|
+
</Text>
|
|
39
64
|
</Box>
|
|
40
65
|
{!flowMode && (
|
|
41
66
|
<>
|
|
42
67
|
<Box flexDirection="column">
|
|
43
|
-
<Text color="gray" dimColor>
|
|
68
|
+
<Text color="gray" dimColor>
|
|
69
|
+
BUDGET
|
|
70
|
+
</Text>
|
|
44
71
|
<Text color="white">${run.budget.toFixed(2)}</Text>
|
|
45
72
|
</Box>
|
|
46
73
|
<Box flexDirection="column">
|
|
47
|
-
<Text color="gray" dimColor>
|
|
74
|
+
<Text color="gray" dimColor>
|
|
75
|
+
USED
|
|
76
|
+
</Text>
|
|
48
77
|
<Text color={pct > 100 ? 'red' : pct > 80 ? 'yellow' : 'green'}>{pct}%</Text>
|
|
49
78
|
</Box>
|
|
50
79
|
</>
|
|
51
80
|
)}
|
|
52
81
|
<Box flexDirection="column">
|
|
53
|
-
<Text color="gray" dimColor>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
<Text color="gray" dimColor>
|
|
83
|
+
MODEL
|
|
84
|
+
</Text>
|
|
85
|
+
<Text color="cyan">
|
|
86
|
+
{mc.emoji} {mc.name}
|
|
87
|
+
{[
|
|
88
|
+
run.effort && run.effort !== 'medium' ? getEffortLevel(run.effort)?.label : null,
|
|
89
|
+
run.fastMode ? 'Fast' : null,
|
|
90
|
+
]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map((s) => `·${s}`)
|
|
93
|
+
.join('')}
|
|
94
|
+
</Text>
|
|
58
95
|
</Box>
|
|
59
96
|
{run.effort && (
|
|
60
97
|
<Box flexDirection="column">
|
|
61
|
-
<Text color="gray" dimColor>
|
|
62
|
-
|
|
98
|
+
<Text color="gray" dimColor>
|
|
99
|
+
EFFORT
|
|
100
|
+
</Text>
|
|
101
|
+
<Text color={getEffortLevel(run.effort)?.color}>
|
|
102
|
+
{getEffortLevel(run.effort)?.emoji} {getEffortLevel(run.effort)?.label}
|
|
103
|
+
</Text>
|
|
63
104
|
</Box>
|
|
64
105
|
)}
|
|
65
106
|
{run.fastMode && (
|
|
66
107
|
<Box flexDirection="column">
|
|
67
|
-
<Text color="gray" dimColor>
|
|
108
|
+
<Text color="gray" dimColor>
|
|
109
|
+
MODE
|
|
110
|
+
</Text>
|
|
68
111
|
<Text color="yellow">↯ Fast</Text>
|
|
69
112
|
</Box>
|
|
70
113
|
)}
|
|
71
114
|
<Box flexDirection="column">
|
|
72
|
-
<Text color="gray" dimColor>
|
|
73
|
-
|
|
115
|
+
<Text color="gray" dimColor>
|
|
116
|
+
TIER
|
|
117
|
+
</Text>
|
|
118
|
+
<Text color={tier.color}>
|
|
119
|
+
{tier.emoji} {tier.label}
|
|
120
|
+
</Text>
|
|
74
121
|
</Box>
|
|
75
122
|
</Box>
|
|
76
123
|
|
|
77
124
|
{/* Efficiency (roguelike mode only) */}
|
|
78
125
|
{efficiency && (
|
|
79
126
|
<Box gap={2}>
|
|
80
|
-
<Text bold color={efficiency.color}>
|
|
127
|
+
<Text bold color={efficiency.color}>
|
|
128
|
+
{efficiency.emoji} {efficiency.label}
|
|
129
|
+
</Text>
|
|
81
130
|
</Box>
|
|
82
131
|
)}
|
|
83
132
|
|
|
84
133
|
{/* Achievements */}
|
|
85
134
|
{run.achievements?.length > 0 && (
|
|
86
135
|
<Box flexDirection="column" gap={0} marginTop={1}>
|
|
87
|
-
<Text color="gray" dimColor>
|
|
136
|
+
<Text color="gray" dimColor>
|
|
137
|
+
Achievements unlocked:
|
|
138
|
+
</Text>
|
|
88
139
|
{run.achievements.map((a, i) => (
|
|
89
|
-
<Text key={i} color="yellow">
|
|
140
|
+
<Text key={i} color="yellow">
|
|
141
|
+
{' '}
|
|
142
|
+
{a.emoji} {a.label}
|
|
143
|
+
</Text>
|
|
90
144
|
))}
|
|
91
145
|
</Box>
|
|
92
146
|
)}
|
|
@@ -95,7 +149,9 @@ export function ScoreCard({ run }) {
|
|
|
95
149
|
{run.thinkingInvocations > 0 && (
|
|
96
150
|
<Box flexDirection="column" gap={0} marginTop={1}>
|
|
97
151
|
<Box gap={3} alignItems="center">
|
|
98
|
-
<Text color="gray" dimColor>
|
|
152
|
+
<Text color="gray" dimColor>
|
|
153
|
+
Extended thinking:
|
|
154
|
+
</Text>
|
|
99
155
|
<Text color="magenta">🔮 {run.thinkingInvocations}× invoked</Text>
|
|
100
156
|
</Box>
|
|
101
157
|
</Box>
|
|
@@ -105,19 +161,34 @@ export function ScoreCard({ run }) {
|
|
|
105
161
|
{run.modelBreakdown && Object.keys(run.modelBreakdown).length > 0 && (
|
|
106
162
|
<Box flexDirection="column" gap={0} marginTop={1}>
|
|
107
163
|
<Box gap={2} alignItems="center">
|
|
108
|
-
<Text color="gray" dimColor>
|
|
164
|
+
<Text color="gray" dimColor>
|
|
165
|
+
Model usage:
|
|
166
|
+
</Text>
|
|
109
167
|
{haikuPct !== null && (
|
|
110
168
|
<Text color={haikuPct >= 75 ? 'magenta' : haikuPct >= 50 ? 'cyan' : 'yellow'}>
|
|
111
169
|
🏹 {haikuPct}% Haiku
|
|
112
170
|
</Text>
|
|
113
171
|
)}
|
|
172
|
+
{mc === MODEL_CLASSES.opusplan && opusPct !== null && (
|
|
173
|
+
<Text color="yellow">⚜️ {opusPct}% Opus (planning)</Text>
|
|
174
|
+
)}
|
|
114
175
|
</Box>
|
|
115
176
|
<Box gap={3} flexWrap="wrap">
|
|
116
177
|
{Object.entries(run.modelBreakdown).map(([model, cost]) => {
|
|
117
|
-
const
|
|
178
|
+
const m = model.toLowerCase();
|
|
179
|
+
const short = m.includes('haiku')
|
|
180
|
+
? 'Haiku'
|
|
181
|
+
: m.includes('sonnet')
|
|
182
|
+
? 'Sonnet'
|
|
183
|
+
: m.includes('opusplan') || m.includes('paladin')
|
|
184
|
+
? 'Paladin'
|
|
185
|
+
: 'Opus';
|
|
118
186
|
const pctOfTotal = Math.round((cost / run.spent) * 100);
|
|
119
187
|
return (
|
|
120
|
-
<Text key={model} color="gray">
|
|
188
|
+
<Text key={model} color="gray">
|
|
189
|
+
{short} <Text color="white">{pctOfTotal}%</Text>{' '}
|
|
190
|
+
<Text dimColor>{formatCost(cost)}</Text>
|
|
191
|
+
</Text>
|
|
121
192
|
);
|
|
122
193
|
})}
|
|
123
194
|
</Box>
|
|
@@ -127,10 +198,14 @@ export function ScoreCard({ run }) {
|
|
|
127
198
|
{/* Tool breakdown */}
|
|
128
199
|
{run.toolCalls && Object.keys(run.toolCalls).length > 0 && (
|
|
129
200
|
<Box flexDirection="column" gap={0} marginTop={1}>
|
|
130
|
-
<Text color="gray" dimColor>
|
|
201
|
+
<Text color="gray" dimColor>
|
|
202
|
+
Tool calls:
|
|
203
|
+
</Text>
|
|
131
204
|
<Box gap={2} flexWrap="wrap">
|
|
132
205
|
{Object.entries(run.toolCalls).map(([tool, count]) => (
|
|
133
|
-
<Text key={tool} color="gray"
|
|
206
|
+
<Text key={tool} color="gray">
|
|
207
|
+
<Text color="white">{tool}</Text> ×{count}
|
|
208
|
+
</Text>
|
|
134
209
|
))}
|
|
135
210
|
</Box>
|
|
136
211
|
</Box>
|
|
@@ -138,19 +213,39 @@ export function ScoreCard({ run }) {
|
|
|
138
213
|
|
|
139
214
|
{/* Death tip */}
|
|
140
215
|
{!won && run.budget && (
|
|
141
|
-
<Box
|
|
142
|
-
|
|
143
|
-
|
|
216
|
+
<Box
|
|
217
|
+
borderStyle="single"
|
|
218
|
+
borderColor="red"
|
|
219
|
+
paddingX={1}
|
|
220
|
+
marginTop={1}
|
|
221
|
+
flexDirection="column"
|
|
222
|
+
>
|
|
223
|
+
<Text color="red" bold>
|
|
224
|
+
Cause of death: Budget exceeded by {formatCost(run.spent - run.budget)}
|
|
225
|
+
</Text>
|
|
226
|
+
<Text color="gray" dimColor>
|
|
227
|
+
Tip: Use Read with line ranges instead of full file reads.
|
|
228
|
+
</Text>
|
|
144
229
|
</Box>
|
|
145
230
|
)}
|
|
146
231
|
</Box>
|
|
147
232
|
|
|
148
233
|
<Box gap={2}>
|
|
149
|
-
<Text color="gray" dimColor>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<Text color="gray" dimColor
|
|
153
|
-
|
|
234
|
+
<Text color="gray" dimColor>
|
|
235
|
+
tokengolf start — run again
|
|
236
|
+
</Text>
|
|
237
|
+
<Text color="gray" dimColor>
|
|
238
|
+
·
|
|
239
|
+
</Text>
|
|
240
|
+
<Text color="gray" dimColor>
|
|
241
|
+
tokengolf stats — career stats
|
|
242
|
+
</Text>
|
|
243
|
+
<Text color="gray" dimColor>
|
|
244
|
+
·
|
|
245
|
+
</Text>
|
|
246
|
+
<Text color="gray" dimColor>
|
|
247
|
+
q to exit
|
|
248
|
+
</Text>
|
|
154
249
|
</Box>
|
|
155
250
|
</Box>
|
|
156
251
|
);
|