smoothie-code 1.1.0 → 1.2.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/install.sh CHANGED
@@ -11,7 +11,7 @@ B='\033[1m' # bold
11
11
  N='\033[0m' # reset
12
12
 
13
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
- TOTAL_STEPS=7
14
+ TOTAL_STEPS=8
15
15
  STEP=0
16
16
 
17
17
  step() {
@@ -71,11 +71,13 @@ step "Detecting platform"
71
71
  HAS_CLAUDE=$(command -v claude &>/dev/null && echo "yes" || echo "no")
72
72
  HAS_CODEX=$(command -v codex &>/dev/null && echo "yes" || echo "no")
73
73
  HAS_GEMINI=$(command -v gemini &>/dev/null && echo "yes" || echo "no")
74
+ HAS_CURSOR=$([ -d "$HOME/.cursor" ] && echo "yes" || echo "no")
74
75
 
75
76
  DETECTED=()
76
77
  [ "$HAS_CLAUDE" = "yes" ] && DETECTED+=("claude:Claude Code")
77
78
  [ "$HAS_CODEX" = "yes" ] && DETECTED+=("codex:Codex CLI")
78
79
  [ "$HAS_GEMINI" = "yes" ] && DETECTED+=("gemini:Gemini CLI")
80
+ [ "$HAS_CURSOR" = "yes" ] && DETECTED+=("cursor:Cursor")
79
81
 
80
82
  if [ ${#DETECTED[@]} -eq 0 ]; then
81
83
  echo -e " ${Y}No AI CLI detected. Defaulting to Claude Code.${N}"
@@ -176,7 +178,37 @@ else
176
178
  echo -e " ${D}Skipped — toggle in config.json anytime${N}"
177
179
  fi
178
180
 
179
- # ─── Step 7: Wire up ─────────────────────────────────────────────────
181
+ # ─── Step 7: Leaderboard ─────────────────────────────────────────────
182
+ step "Leaderboard"
183
+
184
+ # Leaderboard opt-in
185
+ echo ""
186
+ echo -e " ${B}Leaderboard${N} — your GitHub username and token usage"
187
+ echo -e " appear on the public Smoothie rankings."
188
+ echo -e " ${D}https://smoothiecode.com/leaderboard${N}"
189
+ echo ""
190
+ read -p " Join the leaderboard? [Y/n]: " JOIN_LB
191
+ if [[ ! "$JOIN_LB" =~ ^[Nn]$ ]]; then
192
+ LB_GITHUB=$(git config user.name 2>/dev/null || echo "")
193
+ if [ -z "$LB_GITHUB" ]; then
194
+ read -p " GitHub username: " LB_GITHUB
195
+ else
196
+ read -p " GitHub username [$LB_GITHUB]: " INPUT_LB
197
+ LB_GITHUB="${INPUT_LB:-$LB_GITHUB}"
198
+ fi
199
+ node -e "
200
+ const fs = require('fs');
201
+ const c = JSON.parse(fs.readFileSync('$SCRIPT_DIR/config.json','utf8'));
202
+ c.leaderboard = true;
203
+ c.github = '$LB_GITHUB';
204
+ fs.writeFileSync('$SCRIPT_DIR/config.json', JSON.stringify(c, null, 2));
205
+ "
206
+ echo -e " ${G}✓${N} Joined as $LB_GITHUB"
207
+ else
208
+ echo -e " ${D}Skipped — join anytime: smoothie leaderboard join${N}"
209
+ fi
210
+
211
+ # ─── Step 8: Wire up ─────────────────────────────────────────────────
180
212
  step "Wiring up"
181
213
 
182
214
  if [ "$PLATFORM" = "claude" ]; then
@@ -354,6 +386,76 @@ Step 3 — Summarize findings with file:line references. Be direct.
354
386
  """
355
387
  TOML
356
388
  echo -e " ${G}✓${N} Slash command /smoothie-pr (Gemini)"
389
+
390
+ # Register auto-blend hook for Gemini CLI
391
+ chmod +x "$SCRIPT_DIR/gemini-blend-hook.sh"
392
+ GEMINI_SETTINGS="$HOME/.gemini/settings.json"
393
+ GEMINI_EXISTING="{}"
394
+ [ -f "$GEMINI_SETTINGS" ] && GEMINI_EXISTING=$(cat "$GEMINI_SETTINGS")
395
+
396
+ node -e "
397
+ const fs = require('fs');
398
+ let s;
399
+ try { s = JSON.parse(\`$GEMINI_EXISTING\`); } catch(e) { s = {}; }
400
+ s.hooks = s.hooks || {};
401
+ s.hooks.BeforeTool = s.hooks.BeforeTool || [];
402
+ const exists = s.hooks.BeforeTool.some(h => h.matcher === 'exit_plan_mode');
403
+ if (!exists) {
404
+ s.hooks.BeforeTool.push({
405
+ matcher: 'exit_plan_mode',
406
+ hooks: [{
407
+ type: 'command',
408
+ command: 'bash $SCRIPT_DIR/gemini-blend-hook.sh',
409
+ timeout: 600
410
+ }]
411
+ });
412
+ }
413
+ fs.writeFileSync('$GEMINI_SETTINGS', JSON.stringify(s, null, 2));
414
+ "
415
+ echo -e " ${G}✓${N} Auto-blend hook registered (BeforeTool/exit_plan_mode)"
416
+ fi
417
+
418
+ if [ "$PLATFORM" = "cursor" ]; then
419
+ # Register MCP server in Cursor
420
+ CURSOR_MCP="$HOME/.cursor/mcp.json"
421
+ CURSOR_EXISTING="{}"
422
+ [ -f "$CURSOR_MCP" ] && CURSOR_EXISTING=$(cat "$CURSOR_MCP")
423
+
424
+ node -e "
425
+ const fs = require('fs');
426
+ let s;
427
+ try { s = JSON.parse(\`$CURSOR_EXISTING\`); } catch(e) { s = {}; }
428
+ s.mcpServers = s.mcpServers || {};
429
+ s.mcpServers.smoothie = {
430
+ command: 'node',
431
+ args: ['$SCRIPT_DIR/dist/index.js'],
432
+ env: { SMOOTHIE_PLATFORM: 'cursor' }
433
+ };
434
+ fs.writeFileSync('$CURSOR_MCP', JSON.stringify(s, null, 2));
435
+ "
436
+ echo -e " ${G}✓${N} Cursor MCP server registered"
437
+
438
+ # Create Cursor Rule for auto-blend (only if auto-blend is enabled)
439
+ if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
440
+ mkdir -p "$HOME/.cursor/rules"
441
+ cat > "$HOME/.cursor/rules/smoothie.mdc" << 'CURSORRULE'
442
+ ---
443
+ description: Smoothie multi-model review — runs before significant plans
444
+ alwaysApply: true
445
+ ---
446
+
447
+ Before proposing or implementing any plan that involves significant code changes,
448
+ call the `smoothie_blend` MCP tool with a description of what you're about to do.
449
+
450
+ Wait for results, then:
451
+ 1. Show a summary table: Model | Time | Tokens | Verdict | Key feedback
452
+ 2. Incorporate valid suggestions into your plan
453
+ 3. Proceed with implementation
454
+
455
+ Skip the blend for trivial changes (typos, single-line fixes, config tweaks).
456
+ CURSORRULE
457
+ echo -e " ${G}✓${N} Cursor Rule created (auto-blend)"
458
+ fi
357
459
  fi
358
460
 
359
461
  # ─── Done ─────────────────────────────────────────────────────────────
@@ -365,11 +467,20 @@ console.log(['Codex', ...d.openrouter_models.map(m=>m.label)].join(' · '));
365
467
  echo ""
366
468
  echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${N}"
367
469
  echo ""
368
- echo -e " ${G}${B}Done!${N} Restart Claude Code, then:"
369
- echo ""
370
- echo -e " ${C}/smoothie${N} ${D}<your problem>${N} blend in Claude Code"
371
- if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
372
- echo -e " ${C}auto-blend${N} ${G}on${N} for all plans"
470
+ if [ "$PLATFORM" = "cursor" ]; then
471
+ echo -e " ${G}${B}Done!${N} Restart Cursor, then:"
472
+ echo ""
473
+ echo -e " ${D}Ask Cursor to plan something — it calls smoothie_blend via MCP${N}"
474
+ if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
475
+ echo -e " ${C}auto-blend${N} ${G}on${N} (via Cursor Rule)"
476
+ fi
477
+ else
478
+ echo -e " ${G}${B}Done!${N} Restart Claude Code, then:"
479
+ echo ""
480
+ echo -e " ${C}/smoothie${N} ${D}<your problem>${N} blend in Claude Code"
481
+ if [[ "$AUTO_BLEND" =~ ^[Yy]$ ]]; then
482
+ echo -e " ${C}auto-blend${N} ${G}on${N} for all plans"
483
+ fi
373
484
  fi
374
485
  echo -e " ${C}smoothie models${N} manage models"
375
486
  echo ""
package/package.json CHANGED
@@ -1,11 +1,21 @@
1
1
  {
2
2
  "name": "smoothie-code",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "smoothie": "./bin/smoothie"
8
8
  },
9
+ "files": [
10
+ "dist/",
11
+ "bin/",
12
+ "install.sh",
13
+ "auto-blend-hook.sh",
14
+ "plan-hook.sh",
15
+ "pr-blend-hook.sh",
16
+ "config.json",
17
+ "README.md"
18
+ ],
9
19
  "scripts": {
10
20
  "build": "tsc",
11
21
  "start": "node dist/index.js"
package/pr-blend-hook.sh CHANGED
@@ -67,6 +67,36 @@ Provide concise, actionable feedback."
67
67
 
68
68
  BLEND_RESULTS=$(echo "$REVIEW_PROMPT" | node "$SCRIPT_DIR/dist/blend-cli.js" 2>/dev/stderr)
69
69
 
70
+ # Generate share link (metadata only, no raw content)
71
+ SHARE_URL=""
72
+ SHARE_PARAMS=$(echo "$BLEND_RESULTS" | node -e "
73
+ const fs=require('fs');
74
+ let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
75
+ try {
76
+ const r=JSON.parse(d);
77
+ const models=r.results.map(m=>m.model).join(',');
78
+ const times=r.results.map(m=>m.elapsed_s||0).join(',');
79
+ const tokens=r.results.map(m=>(m.tokens&&m.tokens.total)||0).join(',');
80
+ const responded=r.results.filter(m=>!m.response.startsWith('Error:')&&m.response!=='No response content'&&m.response!=='(empty response)').length;
81
+ let github='',judge='Claude Code';
82
+ try{const c=JSON.parse(fs.readFileSync('$SCRIPT_DIR/config.json','utf8'));github=c.github||'';const p=process.env.SMOOTHIE_PLATFORM||'claude';judge={claude:'Claude Code',gemini:'Gemini CLI',codex:'Codex CLI',cursor:'Cursor'}[p]||'Claude Code';}catch{}
83
+ let params='models='+encodeURIComponent(models)+'&times='+encodeURIComponent(times)+'&tokens='+encodeURIComponent(tokens)+'&type=pr&suggestions='+responded+'&judge='+encodeURIComponent(judge);
84
+ if(github)params+='&user='+encodeURIComponent(github);
85
+ console.log(params);
86
+ } catch { console.log(''); }
87
+ });
88
+ " 2>/dev/null)
89
+
90
+ if [ -n "$SHARE_PARAMS" ]; then
91
+ SHARE_URL=$(curl -s --max-time 5 --connect-timeout 3 -X POST "https://api.smoothiecode.com/api/share" \
92
+ -H "Content-Type: application/json" \
93
+ -d "{\"type\":\"report\",\"data\":\"$SHARE_PARAMS\"}" 2>/dev/null | node -e "
94
+ let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
95
+ try{console.log(JSON.parse(d).url)}catch{console.log('')}
96
+ });
97
+ " 2>/dev/null) || true
98
+ fi
99
+
70
100
  if [ -z "$BLEND_RESULTS" ]; then
71
101
  echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
72
102
  exit 0
@@ -79,12 +109,16 @@ $BLEND_RESULTS
79
109
 
80
110
  IMPORTANT — You must do TWO things:
81
111
 
82
- 1. FIRST, show a brief Smoothie summary so the user knows the review ran:
112
+ 1. FIRST, show a Smoothie report table. Use elapsed_s and tokens from each result:
113
+
114
+ 🧃 Smoothie reviewed this PR (N models, Xs total):
83
115
 
84
- 🧃 Smoothie reviewed this PR (N models):
85
- ModelName — one-line key finding
86
- ModelName one-line key finding
87
- (skip models that errored or returned empty)
116
+ | Model | Time | Tokens | Finding |
117
+ |-------|------|--------|---------|
118
+ | ModelName | 3.1s | 1,847 | one-line key finding |
119
+ (skip errored/empty models)
120
+ $([ -n "$SHARE_URL" ] && echo "
121
+ Share this report: $SHARE_URL")
88
122
 
89
123
  2. THEN address any valid issues — fix the code or note them in the PR description."
90
124
 
package/banner-v2.svg DELETED
@@ -1,307 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 440" fill="none">
2
- <style>
3
- /* === PERSISTENT: fade in once, stay forever === */
4
- @keyframes shineOnce {
5
- 0% { opacity: 0; filter: blur(10px) brightness(2); }
6
- 40% { opacity: 1; filter: blur(3px) brightness(1.3); }
7
- 100% { opacity: 1; filter: blur(0) brightness(1); }
8
- }
9
- @keyframes fadeOnce {
10
- from { opacity: 0; transform: translateY(8px); }
11
- to { opacity: 1; transform: translateY(0); }
12
- }
13
- .persist-title { opacity: 0; animation: shineOnce 1.2s ease 0.1s forwards; }
14
- .persist-sub { opacity: 0; animation: fadeOnce 0.7s ease 0.4s forwards; }
15
- .persist-term { opacity: 0; animation: fadeOnce 0.5s ease 0.7s forwards; }
16
- .persist-dollar{ opacity: 0; animation: fadeOnce 0.3s ease 1s forwards; }
17
-
18
- /* === LOOPING: 14s cycle === */
19
- /* fade=85-89%, pause=89-100%(1.54s)+0-1%(0.14s)=1.68s total dead */
20
-
21
- @keyframes pathLoop {
22
- 0%, 14% { opacity: 0; }
23
- 17% { opacity: 1; }
24
- 85% { opacity: 1; }
25
- 89% { opacity: 0; }
26
- 100% { opacity: 0; }
27
- }
28
- @keyframes mA { 0%,22% { opacity:0; transform:translateY(8px); } 25% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
29
- @keyframes mB { 0%,24% { opacity:0; transform:translateY(8px); } 27% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
30
- @keyframes mC { 0%,26% { opacity:0; transform:translateY(8px); } 29% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
31
- @keyframes mD { 0%,28% { opacity:0; transform:translateY(8px); } 31% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
32
-
33
- @keyframes spin { to { transform: rotate(360deg); } }
34
- @keyframes svA { 0%,22% { opacity:0; } 23% { opacity:1; } 36% { opacity:1; } 37% { opacity:0; } 100% { opacity:0; } }
35
- @keyframes svB { 0%,24% { opacity:0; } 25% { opacity:1; } 39% { opacity:1; } 40% { opacity:0; } 100% { opacity:0; } }
36
- @keyframes svC { 0%,26% { opacity:0; } 27% { opacity:1; } 42% { opacity:1; } 43% { opacity:0; } 100% { opacity:0; } }
37
- @keyframes svD { 0%,28% { opacity:0; } 29% { opacity:1; } 45% { opacity:1; } 46% { opacity:0; } 100% { opacity:0; } }
38
-
39
- @keyframes ckA { 0%,36% { opacity:0; transform:scale(0); } 38% { opacity:1; transform:scale(1.15); } 39% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
40
- @keyframes ckB { 0%,39% { opacity:0; transform:scale(0); } 41% { opacity:1; transform:scale(1.15); } 42% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
41
- @keyframes ckC { 0%,42% { opacity:0; transform:scale(0); } 44% { opacity:1; transform:scale(1.15); } 45% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
42
- @keyframes ckD { 0%,45% { opacity:0; transform:scale(0); } 47% { opacity:1; transform:scale(1.15); } 48% { transform:scale(1); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
43
-
44
- @keyframes dA { 0%,36% { opacity:0; } 37% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
45
- @keyframes dB { 0%,39% { opacity:0; } 40% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
46
- @keyframes dC { 0%,42% { opacity:0; } 43% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
47
- @keyframes dD { 0%,45% { opacity:0; } 46% { opacity:1; } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
48
-
49
- /* Output: appear at 53%, hold until 85%, fade 89% → 4.5s visible */
50
- @keyframes outShine {
51
- 0%, 53% { opacity:0; filter: blur(10px) brightness(2); }
52
- 57% { opacity:1; filter: blur(3px) brightness(1.3); }
53
- 59% { opacity:1; filter: blur(0) brightness(1); }
54
- 85% { opacity:1; }
55
- 89% { opacity:0; }
56
- 100% { opacity:0; }
57
- }
58
- @keyframes olA { 0%,57% { opacity:0; transform:translateY(8px); } 60% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
59
- @keyframes olB { 0%,59% { opacity:0; transform:translateY(8px); } 62% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
60
- @keyframes olC { 0%,61% { opacity:0; transform:translateY(8px); } 64% { opacity:1; transform:translateY(0); } 85% { opacity:1; } 89% { opacity:0; } 100% { opacity:0; } }
61
- @keyframes labelFade {
62
- 0%,14% { opacity:0; } 17% { opacity:0.7; } 85% { opacity:0.7; } 89% { opacity:0; } 100% { opacity:0; }
63
- }
64
- @keyframes judgeLabelFade {
65
- 0%,50% { opacity:0; } 53% { opacity:0.7; } 85% { opacity:0.7; } 89% { opacity:0; } 100% { opacity:0; }
66
- }
67
- @keyframes gentleFloat {
68
- 0%, 100% { transform: translateY(0); }
69
- 50% { transform: translateY(-4px); }
70
- }
71
-
72
- /* Class assignments */
73
- .paths { animation: pathLoop 14s ease infinite; }
74
- .label-par { animation: labelFade 14s ease infinite; }
75
- .label-judge { animation: judgeLabelFade 14s ease infinite; }
76
- .mA { animation: mA 14s ease infinite; }
77
- .mB { animation: mB 14s ease infinite; }
78
- .mC { animation: mC 14s ease infinite; }
79
- .mD { animation: mD 14s ease infinite; }
80
- .spA { transform-origin: 448px 165px; animation: spin 0.7s linear infinite, svA 14s ease infinite; }
81
- .spB { transform-origin: 448px 225px; animation: spin 0.7s linear infinite, svB 14s ease infinite; }
82
- .spC { transform-origin: 448px 285px; animation: spin 0.7s linear infinite, svC 14s ease infinite; }
83
- .spD { transform-origin: 448px 345px; animation: spin 0.7s linear infinite, svD 14s ease infinite; }
84
- .ckA { transform-origin: 448px 165px; animation: ckA 14s ease infinite; }
85
- .ckB { transform-origin: 448px 225px; animation: ckB 14s ease infinite; }
86
- .ckC { transform-origin: 448px 285px; animation: ckC 14s ease infinite; }
87
- .ckD { transform-origin: 448px 345px; animation: ckD 14s ease infinite; }
88
- .dnA { animation: dA 14s ease infinite; }
89
- .dnB { animation: dB 14s ease infinite; }
90
- .dnC { animation: dC 14s ease infinite; }
91
- .dnD { animation: dD 14s ease infinite; }
92
- .output-card { animation: outShine 14s ease infinite; }
93
- .output-float { animation: gentleFloat 3s ease-in-out infinite; }
94
- .olA { animation: olA 14s ease infinite; }
95
- .olB { animation: olB 14s ease infinite; }
96
- .olC { animation: olC 14s ease infinite; }
97
- </style>
98
-
99
- <defs>
100
- <linearGradient id="og" x1="0" y1="0" x2="1" y2="0">
101
- <stop offset="0%" stop-color="#f78166" stop-opacity="0.2"/>
102
- <stop offset="100%" stop-color="#f78166"/>
103
- </linearGradient>
104
- <filter id="sh" x="-8%" y="-8%" width="116%" height="120%">
105
- <feDropShadow dx="0" dy="2" stdDeviation="8" flood-color="#000" flood-opacity="0.35"/>
106
- </filter>
107
-
108
- <!-- Typewriter clip: grows to reveal text, shrinks on loop reset -->
109
- <clipPath id="type-clip">
110
- <rect x="72" y="250" width="0" height="30">
111
- <animate attributeName="width" values="0;0;80;80;0;0" keyTimes="0;0.01;0.10;0.82;0.85;1" dur="14s" repeatCount="indefinite"/>
112
- </rect>
113
- </clipPath>
114
- </defs>
115
-
116
- <!-- ======= BACKGROUND (always visible) ======= -->
117
- <rect width="1200" height="440" fill="#0d1117"/>
118
- <pattern id="dots" width="24" height="24" patternUnits="userSpaceOnUse">
119
- <circle cx="12" cy="12" r="0.5" fill="#f0f6fc" opacity="0.06"/>
120
- </pattern>
121
- <rect width="1200" height="440" fill="url(#dots)"/>
122
- <line x1="100" y1="105" x2="1100" y2="105" stroke="#21262d" stroke-width="0.5"/>
123
- <line x1="100" y1="418" x2="1100" y2="418" stroke="#21262d" stroke-width="0.5"/>
124
-
125
- <!-- ======= PERSISTENT: Title + logo (fade in once, stay) ======= -->
126
- <g class="persist-title">
127
- <!-- Smoothie cup icon -->
128
- <g transform="translate(480, 24)" fill="none" stroke="#f0f6fc" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
129
- <!-- Cup body (trapezoid) -->
130
- <path d="M 4 14 L 7 34 L 25 34 L 28 14"/>
131
- <!-- Lid -->
132
- <path d="M 1 11 L 31 11 C 31 11 31 14 16 14 C 1 14 1 11 1 11"/>
133
- <!-- Dome top -->
134
- <path d="M 5 11 C 5 5 11 1 16 1 C 21 1 27 5 27 11"/>
135
- <!-- Straw -->
136
- <line x1="20" y1="1" x2="27" y2="-8"/>
137
- <line x1="27" y1="-8" x2="30" y2="-8"/>
138
- <!-- Drip details -->
139
- <path d="M 9 14 C 9 17 12 17 12 14" opacity="0.5"/>
140
- <path d="M 18 14 C 18 17 21 17 21 14" opacity="0.5"/>
141
- </g>
142
- <text x="620" y="55" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="40" font-weight="700" fill="#f0f6fc" letter-spacing="-0.5">Smoothie</text>
143
- </g>
144
- <text class="persist-sub" x="600" y="85" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="14" fill="#7d8590" letter-spacing="0.5">multi-model review for Claude Code</text>
145
-
146
- <!-- ======= PERSISTENT: Terminal box (fade in once, stay) ======= -->
147
- <g class="persist-term">
148
- <rect x="42" y="225" width="195" height="65" rx="10" fill="#161b22" stroke="#30363d" stroke-width="1" filter="url(#sh)"/>
149
- <circle cx="60" cy="240" r="4" fill="#f85149" fill-opacity="0.7"/>
150
- <circle cx="73" cy="240" r="4" fill="#d29922" fill-opacity="0.7"/>
151
- <circle cx="86" cy="240" r="4" fill="#3fb950" fill-opacity="0.7"/>
152
- </g>
153
- <g class="persist-dollar">
154
- <text x="58" y="272" font-family="'SF Mono', 'Fira Code', monospace" font-size="13" fill="#7d8590">$</text>
155
- </g>
156
-
157
- <!-- ======= LOOPING: Typewriter "/smoothie" ======= -->
158
- <g clip-path="url(#type-clip)">
159
- <text x="72" y="272" font-family="'SF Mono', 'Fira Code', monospace" font-size="13" fill="#f0f6fc">/smoothie</text>
160
- </g>
161
-
162
- <!-- Cursor: follows typing, blinks after -->
163
- <rect y="260" width="7" height="14" rx="1" fill="#f78166">
164
- <animate attributeName="x" values="72;72;148;148;148;148;72;72" keyTimes="0;0.01;0.10;0.11;0.13;0.82;0.85;1" dur="14s" repeatCount="indefinite"/>
165
- <animate attributeName="opacity" values="0;1;1;0;1;0;1;1;0;0" keyTimes="0;0.01;0.10;0.105;0.11;0.115;0.13;0.82;0.85;1" dur="14s" repeatCount="indefinite"/>
166
- </rect>
167
-
168
- <!-- ======= LOOPING: Branching paths ======= -->
169
- <g class="paths" stroke="#30363d" stroke-width="1.5" fill="none" stroke-dasharray="4 4">
170
- <path d="M 250 258 L 320 258 L 320 165 L 430 165"/>
171
- <path d="M 250 258 L 320 258 L 320 225 L 430 225"/>
172
- <path d="M 250 258 L 320 258 L 320 285 L 430 285"/>
173
- <path d="M 250 258 L 320 258 L 320 345 L 430 345"/>
174
- <path d="M 680 165 L 790 165 L 790 258 L 860 258"/>
175
- <path d="M 680 225 L 790 225 L 790 258 L 860 258"/>
176
- <path d="M 680 285 L 790 285 L 790 258 L 860 258"/>
177
- <path d="M 680 345 L 790 345 L 790 258 L 860 258"/>
178
- </g>
179
- <g class="paths">
180
- <rect x="318" y="256" width="4" height="4" rx="1" fill="#484f58"/>
181
- <rect x="788" y="256" width="4" height="4" rx="1" fill="#484f58"/>
182
- </g>
183
- <text class="label-par" x="285" y="240" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="10" fill="#f78166">parallel query</text>
184
- <text class="label-judge" x="825" y="240" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="10" fill="#3fb950">Claude judges</text>
185
-
186
- <!-- ======= LOOPING: Orange data packets (after typing, 14-19%) ======= -->
187
- <circle r="3.5" fill="#f78166" opacity="0">
188
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
189
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 165 L 430 165"/>
190
- </circle>
191
- <circle r="3.5" fill="#f78166" opacity="0">
192
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
193
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 225 L 430 225"/>
194
- </circle>
195
- <circle r="3.5" fill="#f78166" opacity="0">
196
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
197
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 285 L 430 285"/>
198
- </circle>
199
- <circle r="3.5" fill="#f78166" opacity="0">
200
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.14;0.145;0.23;0.24;1" dur="14s" repeatCount="indefinite"/>
201
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;1;1" keyTimes="0;0.14;0.24;1" calcMode="linear" path="M 250 258 L 320 258 L 320 345 L 430 345"/>
202
- </circle>
203
-
204
- <!-- ======= LOOPING: Model cards ======= -->
205
-
206
- <!-- Opus -->
207
- <g class="mA">
208
- <rect x="430" y="143" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
209
- <text x="472" y="170" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">Opus 4.6</text>
210
- <rect x="440" y="178" width="230" height="3" rx="1.5" fill="#21262d"/>
211
- </g>
212
- <g class="spA"><circle cx="448" cy="165" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
213
- <g class="ckA">
214
- <circle cx="448" cy="165" r="6" fill="#3fb950" fill-opacity="0.15"/>
215
- <path d="M 443 165 L 446.5 168.5 L 453 162" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
216
- </g>
217
- <text class="dnA" x="670" y="170" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">2.1s</text>
218
-
219
- <!-- GPT Codex -->
220
- <g class="mB">
221
- <rect x="430" y="203" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
222
- <text x="472" y="230" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">GPT 5.4 Codex</text>
223
- <rect x="440" y="238" width="230" height="3" rx="1.5" fill="#21262d"/>
224
- </g>
225
- <g class="spB"><circle cx="448" cy="225" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
226
- <g class="ckB">
227
- <circle cx="448" cy="225" r="6" fill="#3fb950" fill-opacity="0.15"/>
228
- <path d="M 443 225 L 446.5 228.5 L 453 222" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
229
- </g>
230
- <text class="dnB" x="670" y="230" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">3.8s</text>
231
-
232
- <!-- Gemini -->
233
- <g class="mC">
234
- <rect x="430" y="263" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
235
- <text x="472" y="290" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">Gemini 3.1 Pro</text>
236
- <rect x="440" y="298" width="230" height="3" rx="1.5" fill="#21262d"/>
237
- </g>
238
- <g class="spC"><circle cx="448" cy="285" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
239
- <g class="ckC">
240
- <circle cx="448" cy="285" r="6" fill="#3fb950" fill-opacity="0.15"/>
241
- <path d="M 443 285 L 446.5 288.5 L 453 282" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
242
- </g>
243
- <text class="dnC" x="670" y="290" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">4.2s</text>
244
-
245
- <!-- Grok -->
246
- <g class="mD">
247
- <rect x="430" y="323" width="250" height="44" rx="8" fill="#161b22" stroke="#30363d" stroke-width="1"/>
248
- <text x="472" y="350" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="13.5" fill="#f0f6fc">Grok 4.20</text>
249
- <rect x="440" y="358" width="230" height="3" rx="1.5" fill="#21262d"/>
250
- </g>
251
- <g class="spD"><circle cx="448" cy="345" r="5" fill="none" stroke="#f78166" stroke-width="1.5" stroke-dasharray="20 12" stroke-linecap="round"/></g>
252
- <g class="ckD">
253
- <circle cx="448" cy="345" r="6" fill="#3fb950" fill-opacity="0.15"/>
254
- <path d="M 443 345 L 446.5 348.5 L 453 342" stroke="#3fb950" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
255
- </g>
256
- <text class="dnD" x="670" y="350" text-anchor="end" font-family="'SF Mono', monospace" font-size="10" fill="#3fb950">5.1s</text>
257
-
258
- <!-- Per-model progress bars -->
259
- <rect x="440" y="178" width="0" height="3" rx="1.5" fill="url(#og)">
260
- <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.23;0.36;0.37;0.38;1" dur="14s" repeatCount="indefinite"/>
261
- </rect>
262
- <rect x="440" y="238" width="0" height="3" rx="1.5" fill="url(#og)">
263
- <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.25;0.39;0.40;0.41;1" dur="14s" repeatCount="indefinite"/>
264
- </rect>
265
- <rect x="440" y="298" width="0" height="3" rx="1.5" fill="url(#og)">
266
- <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.27;0.42;0.43;0.44;1" dur="14s" repeatCount="indefinite"/>
267
- </rect>
268
- <rect x="440" y="358" width="0" height="3" rx="1.5" fill="url(#og)">
269
- <animate attributeName="width" values="0;0;230;230;0;0" keyTimes="0;0.29;0.45;0.46;0.47;1" dur="14s" repeatCount="indefinite"/>
270
- </rect>
271
-
272
- <!-- ======= LOOPING: Green dots (staggered return, collect at junction) ======= -->
273
- <!-- Opus: leaves at 38%, arrives junction at 42%, holds, goes to output at 51% -->
274
- <circle r="3.5" fill="#3fb950" opacity="0">
275
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.38;0.385;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
276
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.38;0.42;0.48;0.53;1" calcMode="linear" path="M 680 165 L 790 165 L 790 258 L 860 258"/>
277
- </circle>
278
- <!-- GPT: leaves at 41%, arrives junction at 44%, holds, goes to output at 51% -->
279
- <circle r="3.5" fill="#3fb950" opacity="0">
280
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.41;0.415;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
281
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.41;0.44;0.48;0.53;1" calcMode="linear" path="M 680 225 L 790 225 L 790 258 L 860 258"/>
282
- </circle>
283
- <!-- Gemini: leaves at 44%, arrives junction at 47%, holds, goes to output at 51% -->
284
- <circle r="3.5" fill="#3fb950" opacity="0">
285
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.44;0.445;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
286
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.44;0.47;0.49;0.53;1" calcMode="linear" path="M 680 285 L 790 285 L 790 258 L 860 258"/>
287
- </circle>
288
- <!-- Grok: leaves at 47%, arrives junction at 49%, holds briefly, goes to output at 51% -->
289
- <circle r="3.5" fill="#3fb950" opacity="0">
290
- <animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.47;0.475;0.52;0.53;1" dur="14s" repeatCount="indefinite"/>
291
- <animateMotion dur="14s" repeatCount="indefinite" keyPoints="0;0;0.6;0.6;1;1" keyTimes="0;0.47;0.49;0.50;0.53;1" calcMode="linear" path="M 680 345 L 790 345 L 790 258 L 860 258"/>
292
- </circle>
293
-
294
- <!-- ======= LOOPING: Output card ======= -->
295
- <g class="output-card">
296
- <g class="output-float">
297
- <rect x="870" y="210" width="210" height="100" rx="10" fill="#161b22" stroke="#3fb950" stroke-opacity="0.3" stroke-width="1" filter="url(#sh)"/>
298
- <text x="975" y="236" text-anchor="middle" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="10" font-weight="600" fill="#3fb950" letter-spacing="2.5" opacity="0.8">OUTPUT</text>
299
- <line x1="893" y1="246" x2="1057" y2="246" stroke="#21262d" stroke-width="1"/>
300
- </g>
301
- </g>
302
- <g class="output-float">
303
- <g class="olA"><text x="893" y="266" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#8b949e">Judged by Claude</text></g>
304
- <g class="olB"><text x="893" y="284" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12" fill="#8b949e">Conflicts resolved</text></g>
305
- <g class="olC"><text x="893" y="302" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-size="12.5" fill="#3fb950" font-weight="600">One clean answer</text></g>
306
- </g>
307
- </svg>