A tall deep-sea pane: a little submersible hangs in the dark while glowing jellyfish, an anglerfish and lanternfish drift by and something vast and unknowable…

<img src="images/hero-en.gif" alt="Claude Code on the left works through a task while the reef-tank pane on the right dims and a submarine comes down" width="720">
Hand Claude Code a task, and zone out at the fish while it works. (The chat window here is a mock-up; the tank is the real mod.)
Animated side panels for Claude Code that change when Claude is idle vs. working, plus two small prompt bands. I'm learning Claude Code mods by building things, and this is what came out so far.
Every scene is a hand-written SVG animation (no image files). The previews below are the real pictures, captured with the idle state on the left and the working state on the right (an 8-second loop).
한국어 설명은 아래에 있습니다.
This repository is a marketplace. Inside Claude Code:
/plugin marketplace add Gnosis333/mod_gallery
/plugin install night-train@mod-gallery
Or from a shell:
claude plugin marketplace add Gnosis333/mod_gallery
claude plugin install night-train@mod-gallery --scope user
Swap night-train for any mod below. Panels open only when you run their slash command (they don't open by themselves, to save CPU). After installing, run /reload-plugins or start a new session.
To try one mod without installing: claude --plugin-dir mods/night-train.
Labels (the status line, descriptions, replies) are English by default. Every mod also has a Korean (한국어) mode through its language option:
echo '{"language":"ko"}' | claude plugin configure night-train@mod-gallery --values-stdin
Then restart Claude Code. You can also run /plugin configure night-train@mod-gallery inside Claude Code, and claude plugin configure night-train@mod-gallery shows whether it is set. Use en to switch back.
| Mod | Command | What it is |
|---|---|---|
night-train | /train | A night-train window |
beach-day | /beach | A top-down beach |
snow-cabin | /snow | A snowy cabin, day and night |
deep-sub | /deep | A deep-sea submersible |
campfire | /fire | A campfire to stare at |
jungle-falls | /falls | A jungle waterfall with 24 kinds of animals |
rain-window | /rain | A rainy window |
moon-view | /moon | The universe seen from the Moon |
tower-cut | /tower | A building cross-section |
desk-garden | /garden | A desk plant and a workshop |
reef-tank | /reef | An aquarium |
usage-band | automatic | Context, limits and cost on one line |
haiku-warning | automatic | A warning while a Haiku model is selected |
<img src="images/night-train.gif" alt="night-train" width="460">
A tall train window. At rest the train waits at STATION under lit lamp posts; when work starts the station slides away and hills, trees and poles stream past at different speeds. Now and then it runs through a long tunnel, and flocks of migrating birds cross the sky. The steam from the cup blows sideways, and when the turn ends the train rolls back into the station.
<img src="images/beach-day.gif" alt="beach-day" width="460">
A beach seen from above. Gentle waves wash the shore, and people in sunglasses lie on sunbeds under parasols (bikinis, one-pieces and trunks). Out at sea swimmers float on all kinds of rings, a pink unicorn among them. While Claude works a jet ski and a banana boat go by, and crabs scuttle across the sand.
<img src="images/snow-cabin.gif" alt="snow-cabin" width="460">
Snow falls softly over a white forest, snowy peaks and a log cabin. It's daytime at rest; while Claude works night falls, the windows glow, the chimney smokes, and a snowman is built every 15 seconds. A white rabbit, a white fox, a magpie and a husky come by.
<img src="images/deep-sub.gif" alt="deep-sub" width="460">
A yellow submersible drifts through the dark among glowing jellyfish, an anglerfish, lanternfish, a dumbo octopus and a gulper eel, while something vast and unknowable stirs faintly far below. While Claude works the searchlight comes on, sonar pings, and now and then the shadow of a giant squid sweeps past.
<img src="images/campfire.gif" alt="campfire" width="460">
A campfire in a dense forest clearing under the stars. Flames lick, sparks crackle up, and a ring of logs and six kinds of flowers surround the fire. A fox, an owl (it swivels its head), a deer, a raccoon and a hedgehog drop by. The fire roars up while Claude works and dies back afterwards.
<img src="images/jungle-falls.gif" alt="jungle-falls" width="460">
A terraced waterfall winds down into a lily pool. 24 kinds of animals visit at random (birds, monkeys, a jaguar, a crocodile, frogs, otters and more) and fish leap up the falls. It's day at rest and night while Claude works. This is the heaviest mod on CPU.
<img src="images/rain-window.gif" alt="rain-window" width="460">
A rainy window: drops run down the glass and merge, city lights blur outside and a cat dozes on the sill. While Claude works a storm comes in, and when lightning strikes the cat opens its eyes and the desk lamp flickers. When the turn ends a rainbow comes out and fades away.
<img src="images/moon-view.gif" alt="moon-view" width="460">
The Earth seen from the Moon. The lander's "eyes" flash and an astronaut bounces about (and tumbles around while Claude works). Stars, supernovae, a black hole, rockets, satellites, UFOs, planets and a few Star Wars-like ships pass at random.
<img src="images/tower-cut.gif" alt="tower-cut" width="460">
A building cross-section. One floor goes up with every finished turn, and at 20 floors the tower tops out with fireworks. A new session starts over. While Claude works the crane moves and raises the frame of the next floor.
<img src="images/desk-garden.gif" alt="desk-garden" width="460">
Three pictures stacked: a flowerpot by a window (one more flower blooms with every finished turn), a dozing cat by a fish tank, and a workshop where Claude's character works. While Claude works the character types at the keyboard.
<img src="images/reef-tank.gif" alt="reef-tank" width="460">
A tall aquarium with fish, a seahorse and jellyfish. While Claude works the lights dim and a yellow submarine dives; every finished turn adds a pearl to the treasure chest.
<img src="images/usage-band.png" alt="usage-band" width="720">
One line above the prompt: context window fill, the 5-hour and 7-day plan limits, and session cost. It turns amber at 70% and brick red at 85%, and each limit shows when it resets (↻). The image is an illustration of the look.
<img src="images/haiku-warning.png" alt="haiku-warning" width="720">
Shows a red warning above the prompt while a Haiku model is selected, as a reminder to switch before complex work. The image is an illustration of the look.
jungle-falls, snow-cabin and campfire are larger pictures and relatively heavier.mods/<name>/ with .claude-plugin/plugin.json, hooks/register.tsx (the behaviour), a test file and a state type. The part of register.tsx between // ---scene-start and // ---scene-end is what draws the picture.language option (see Language). The pictures themselves contain no text, except a few English words such as STATION and the speech bubble in desk-garden.Built with Claude Code.
MIT. Use, change and share freely.
<img src="images/hero-ko.gif" alt="왼쪽 Claude Code가 일하는 동안 오른쪽 어항 패널이 어두워지고 잠수함이 내려오는 모습" width="720">
Claude Code에 일을 맡기고, 일하는 동안엔 어항 보며 멍~ (왼쪽 대화창은 비슷하게 그린 화면이고, 어항은 실제 모드입니다.)
Claude Code 옆에 띄워 두는 그림 패널과 입력창 위 상태 띠를 모았습니다. 패널은 Claude가 쉬고 있을 때와 일하고 있을 때 모습이 달라집니다. Claude Code의 모드를 공부하면서 이것저것 만들어 본 결과물입니다. 위 영어 부분의 그림(왼쪽 쉬는 중, 오른쪽 작업 중)이 그대로 해당합니다.
/plugin marketplace add Gnosis333/mod_gallery
/plugin install night-train@mod-gallery
night-train 자리에 위 표의 모드 이름을 넣으면 됩니다. 패널은 슬래시 명령으로 열 때만 열립니다(CPU를 아끼려고 자동으로 열지 않습니다). 설치 후 /reload-plugins를 입력하거나 새 세션을 여세요.
한글 라벨로 보려면 모드마다 language 옵션을 ko로 정하고 Claude Code를 다시 시작하세요(기본은 영어).
echo '{"language":"ko"}' | claude plugin configure night-train@mod-gallery --values-stdin
/plugin configure night-train@mod-gallery로도 정할 수 있습니다. 영어로 되돌리려면 en을 쓰세요.
/train): 야간열차 창가. 쉴 때는 STATION에 정차해 가로등이 켜져 있고, 작업 중에는 산·나무·전신주가 서로 다른 속도로 흘러가며 가끔 긴 터널을 지납니다. 하늘에는 철새떼가 날아가고, 턴이 끝나면 다시 역으로 들어옵니다./beach): 위에서 내려다본 해변. 파라솔 아래 썬베드에 선글라스를 낀 사람들이 누워 있고, 바다에는 핑크 유니콘 튜브를 비롯한 여러 튜브가 떠 있습니다. 작업 중에는 제트스키, 바나나보트, 꽃게가 나옵니다./snow): 눈이 소복소복 내리는 흰 숲과 설산, 통나무 오두막. 쉴 때는 낮, 작업 중에는 밤이 되어 창에 불이 켜지고 눈사람이 15초마다 하나씩 생깁니다. 흰 토끼, 흰 여우, 까치, 허스키가 찾아옵니다./deep): 어둠 속 노란 잠수정과 야광 해파리, 아귀, 덤보 문어, 풍선장어, 그리고 저 아래 희미하게 움직이는 정체불명의 거대한 것. 작업 중에는 탐조등과 소나가 켜지고 가끔 거대 오징어 그림자가 스칩니다./fire): 별이 가득한 밤 숲속 공터의 모닥불. 불꽃과 불티, 통나무 원, 꽃 여섯 종류, 여우·올빼미(머리를 돌립니다)·사슴·너구리·고슴도치. 작업 중에는 불이 커지고 끝나면 잦아듭니다./falls): 계단식 폭포와 연꽃 연못, 동물 24종이 무작위로 방문. 쉴 때는 낮, 작업 중에는 밤. CPU를 가장 많이 씁니다./rain): 비 오는 창가. 작업 중에는 폭풍, 번개가 치면 고양이가 눈을 뜨고 스탠드가 깜빡이며, 끝나면 무지개가 뜹니다./moon): 달에서 본 지구. 통통 뛰는 우주인(작업 중에는 빙빙 돕니다), 무작위로 지나가는 우주 물체와 스타워즈풍 우주선./tower): 빌딩 단면. 턴이 끝날 때마다 한 층씩 올라가고 20층이면 준공 불꽃놀이./garden): 창가 화분(턴마다 꽃이 핍니다), 졸고 있는 고양이와 수조, Claude가 일하는 작업실./reef): 수족관. 작업 중에는 조명이 어두워지고 노란 잠수함이 내려오며, 턴이 끝날 때마다 진주가 쌓입니다.↻ 포함), 비용을 보여줍니다.jungle-falls, snow-cabin, campfire는 그림이 커서 상대적으로 무겁습니다.language 옵션을 ko로 하면 한글입니다. 그림 속 글자는 STATION과 desk-garden의 말풍선 정도입니다(말풍선은 옵션을 따릅니다).모두 Claude Code로 만들었습니다.
hooks/register.tsx 455 lines1import { atom, read, update } from 'claude-code'
2import type { ElementTable, Register } from 'claude-code'
3
4import type { Dive } from '../types'
5
6const PANE = 'deep-sub'
7const STRINGS = {
8 en: {
9 title: 'Deep sea',
10 alt: 'A submersible in the deep sea. Glowing jellyfish, an anglerfish and lanternfish drift by while something vast and unknowable stirs faintly far below.',
11 working: (clock: string, tools: number) => `Working ${clock} · tools ${tools}`,
12 idle: 'Idle',
13 opened: 'Opened the deep-sea pane.',
14 note: 'The animation shows in the desktop app.',
15 },
16 ko: {
17 title: '심해',
18 alt: '깊은 바다 속 잠수정. 야광 해파리와 아귀, 샛비늘치 떼가 지나가고, 저 아래 어둠 속에서 알 수 없는 거대한 것이 희미하게 움직입니다.',
19 working: (clock: string, tools: number) => `작업 중 ${clock} · 도구 ${tools}회`,
20 idle: '쉬는 중',
21 opened: '심해를 열었습니다.',
22 note: '그림 움직임은 데스크톱 앱에서 보입니다.',
23 },
24}
25
26type Words = (typeof STRINGS)['en']
27
28// ---scene-start
29type Sub = { working: boolean }
30
31const W = 400
32const H = 1200
33// the submersible wanders the water on a slow loop, easing from one spot to the next
34const ROUTE: [number, number][] = [
35 [110, 520],
36 [230, 430],
37 [290, 600],
38 [210, 760],
39 [90, 700],
40 [70, 590],
41 [110, 520],
42]
43const ROUTE_DUR = 84
44
45const n = (v: number) => String(Math.round(v * 10) / 10)
46const EASE = '.45 0 .55 1'
47const rnd = (s: number) => {
48 const v = Math.sin(s * 127.1 + 311.7) * 43758.5453
49
50 return v - Math.floor(v)
51}
52
53// Looping motion shifts back by the time already elapsed, so redrawing the picture never makes it jump.
54const retime = (svg: string, elapsed: number) =>
55 svg.replace(/<(animate|animateTransform|animateMotion)\b[^>]*>/g, tag => {
56 const found = / begin="(-?[\d.]+)s"/.exec(tag)
57 const shifted = ` begin="${n((found ? parseFloat(found[1] ?? '0') : 0) - elapsed)}s"`
58
59 return found ? tag.replace(found[0], shifted) : tag.replace(/^<(\w+)/, `<$1${shifted}`)
60 })
61
62// a soft halo: rings of the same colour fading outward
63const halo = (r: number, color: string, strength = 1) =>
64 [1, 0.6, 0.3].map((k, i) => `<circle r="${n(r * (1 + i * 0.9))}" fill="${color}" opacity="${Math.round(16 * k * strength) / 100}"/>`).join('')
65
66// ---- the water: the dark, and marine snow sinking through it --------------------------------------------
67const SNOWFALL = Array.from({ length: 46 }, (_, k) => {
68 const x = rnd(k + 10) * 400
69 const dur = 30 + rnd(k + 20) * 30
70 const r = 0.6 + rnd(k + 30) * 1.1
71
72 return `<circle r="${n(r)}" fill="#9ab4c8" opacity="${n(0.25 + rnd(k + 40) * 0.35)}"><animateTransform attributeName="transform" type="translate" values="${n(x)} -10;${n(x + 14)} ${H / 2};${n(x - 6)} ${H + 10}" dur="${n(dur)}s" begin="${n(-rnd(k + 50) * dur)}s" repeatCount="indefinite"/></circle>`
73}).join('')
74
75// ---- far below: something vast, rising out of the floor, its tentacles reaching up the sides of the dark,
76// almost never quite seen
77const THING = (() => {
78 // feelers hanging from the face, then great arms climbing on either side
79 const angles = [-24, -8, 8, 24, -150, -128, -106, -84, 84, 106, 128, 150]
80 const arms = angles.map((a, k) => {
81 const len = (Math.abs(a) < 30 ? 70 : 170) + rnd(k + 60) * 80
82 const curl = (k % 2 ? 1 : -1) * (30 + rnd(k + 70) * 30)
83
84 return (
85 `<g transform="rotate(${n(a)})"><g><animateTransform attributeName="transform" type="rotate" values="-8;8;-8" calcMode="spline" keyTimes="0;.5;1" keySplines="${EASE};${EASE}" dur="${n(9 + rnd(k + 80) * 6)}s" begin="${n(-k * 1.7)}s" repeatCount="indefinite"/>` +
86 `<path d="M-9 0 Q${n(curl)} ${n(len * 0.5)} ${n(-curl * 0.4)} ${n(len)} Q${n(curl * 0.2)} ${n(len * 0.55)} 9 0Z" fill="#0f2634"/></g></g>`
87 )
88 }).join('')
89
90 return (
91 '<g transform="translate(200 1086) scale(2.6)" opacity="0"><animate attributeName="opacity" values="0;0;.5;.5;0;0" keyTimes="0;.25;.45;.6;.8;1" dur="80s" begin="-34s" repeatCount="indefinite"/>' +
92 // two vestigial wings, then the head and its hanging feelers
93 '<path d="M-60 -40 Q-170 -150 -150 -20 Q-120 -60 -60 -10Z M60 -40 Q170 -150 150 -20 Q120 -60 60 -10Z" fill="#0c1f2c"/>' +
94 `<g transform="translate(0 30)">${arms}</g>` +
95 '<path d="M-90 30 Q-100 -110 0 -120 Q100 -110 90 30 Q0 50 -90 30Z" fill="#0d2230"/>' +
96 '<path d="M-60 -60 Q-40 -90 0 -96 M30 -80 Q50 -70 60 -40" stroke="#14303e" stroke-width="3" fill="none"/>' +
97 // its eyes open, slowly, once in a long while
98 '<g opacity="0"><animate attributeName="opacity" values="0;0;.9;0;0" keyTimes="0;.42;.5;.58;1" dur="80s" begin="-34s" repeatCount="indefinite"/>' +
99 '<ellipse cx="-34" cy="-30" rx="9" ry="4" fill="#7dffb0"/><ellipse cx="34" cy="-30" rx="9" ry="4" fill="#7dffb0"/><ellipse cx="-34" cy="-30" rx="2" ry="4" fill="#062014"/><ellipse cx="34" cy="-30" rx="2" ry="4" fill="#062014"/>' +
100 '<circle cx="-34" cy="-30" r="22" fill="#7dffb0" opacity=".12"/><circle cx="34" cy="-30" r="22" fill="#7dffb0" opacity=".12"/></g></g>'
101 )
102})()
103
104// the sea floor: rock, and the glowing pens and worms living on it
105const FLOOR =
106 `<path d="M0 1120 Q40 1090 90 1104 T190 1096 Q240 1080 290 1100 T400 1090 V${H} H0Z" fill="#03070c"/>` +
107 `<path d="M0 1150 Q60 1130 120 1146 T260 1140 T400 1150 V${H} H0Z" fill="#020407"/>` +
108 Array.from({ length: 14 }, (_, k) => {
109 const x = 12 + k * 28 + rnd(k + 90) * 10
110 const y = 1110 + rnd(k + 91) * 30
111 const c = ['#5ff0ff', '#ff7ae0', '#a0ff7a'][k % 3]
112
113 return (
114 `<path d="M${n(x)} ${n(y)} q-2 -10 0 -20" stroke="${c}" stroke-width="1.6" opacity=".5" fill="none"/>` +
115 `<circle cx="${n(x)}" cy="${n(y - 21)}" r="2.2" fill="${c}"><animate attributeName="opacity" values=".3;1;.3" dur="${n(2 + rnd(k + 92) * 3)}s" begin="${n(-rnd(k + 93) * 4)}s" repeatCount="indefinite"/></circle>`
116 )
117 }).join('')
118
119// ---- glowing jellyfish, rising slowly, bells pulsing ------------------------------------------------------
120const jelly = (x: number, dx: number, color: string, dur: number, begin: number, s: number) => {
121 const tentacles = [-10, -4, 3, 9]
122 .map(
123 (tx, i) =>
124 `<path d="M${tx} 0 q-4 14 0 28 q4 14 0 28" fill="none" stroke="${color}" stroke-width="1.2" opacity=".6"><animate attributeName="d" values="M${tx} 0 q-4 14 0 28 q4 14 0 28;M${tx} 0 q4 14 0 28 q-4 14 0 28;M${tx} 0 q-4 14 0 28 q4 14 0 28" dur="${n(2.4 + i * 0.3)}s" repeatCount="indefinite"/></path>`,
125 )
126 .join('')
127
128 return (
129 `<g><animateTransform attributeName="transform" type="translate" values="${x} ${H + 80};${x + dx * 0.6} ${H / 2};${x + dx} -120" dur="${dur}s" begin="${begin}s" repeatCount="indefinite"/>` +
130 `<g transform="scale(${s})">${halo(18, color, 0.8)}${tentacles}` +
131 '<g><animateTransform attributeName="transform" type="scale" values="1 1;1.14 .84;1 1" dur="1.8s" repeatCount="indefinite"/>' +
132 `<path d="M-17 0 Q-17 -22 0 -24 Q17 -22 17 0 Q8 -4 0 0 Q-8 -4 -17 0Z" fill="${color}" fill-opacity=".28" stroke="${color}" stroke-width="1.4"/>` +
133 `<path d="M-10 -4 Q-9 -16 0 -17 Q9 -16 10 -4" fill="none" stroke="#fff" stroke-opacity=".5" stroke-width="1.2"/><circle cy="-10" r="3.4" fill="${color}"/>` +
134 '</g></g></g>'
135 )
136}
137
138const JELLIES = jelly(70, 40, '#5ff0ff', 70, -10, 1.2) + jelly(320, -50, '#ff7ae0', 84, -46, 1) + jelly(250, 30, '#a0ff7a', 96, -70, 0.8)
139
140// a comb jelly, its rows of cilia running through the colours
141const COMB =
142 '<g><animateTransform attributeName="transform" type="translate" values="-40 360;440 300" dur="60s" begin="-20s" repeatCount="indefinite"/>' +
143 '<ellipse rx="10" ry="16" fill="#cfe8ff" fill-opacity=".12" stroke="#cfe8ff" stroke-opacity=".35"/>' +
144 [-6, -2, 2, 6]
145 .map(
146 (x, i) =>
147 `<path d="M${x} -14 Q${x * 1.3} 0 ${x} 14" fill="none" stroke-width="1.6" stroke-dasharray="2 2"><animate attributeName="stroke" values="#ff6a6a;#ffd36a;#6aff9a;#6ac8ff;#c86aff;#ff6a6a" dur="2s" begin="${-i * 0.4}s" repeatCount="indefinite"/></path>`,
148 )
149 .join('') +
150 '</g>'
151
152// ---- an anglerfish, all mouth, its lure bobbing in the dark ----------------------------------------------
153// ---- a dumbo octopus, flapping its ear-fins, drifting up and down as it crosses ------------------------
154const DUMBO =
155 '<g><animateTransform attributeName="transform" type="translate" values="440 700;300 640;160 720;-60 660;-60 660" keyTimes="0;.2;.42;.66;1" calcMode="spline" keySplines="' + [EASE, EASE, EASE, EASE].join(';') + '" dur="70s" begin="-48s" repeatCount="indefinite"/>' +
156 halo(16, '#ffb0c8', 0.5) +
157 // the webbed arms below, pulsing as it swims
158 '<g><animateTransform attributeName="transform" type="scale" values="1 1;1.2 .8;1 1" dur="2.4s" repeatCount="indefinite"/>' +
159 '<path d="M-14 6 Q-18 20 -10 22 Q-6 14 -3 22 Q0 14 3 22 Q6 14 10 22 Q18 20 14 6Z" fill="#f2a6b8" opacity=".85"/></g>' +
160 '<ellipse cy="-4" rx="15" ry="14" fill="#f8bccb"/><ellipse cx="-4" cy="-10" rx="6" ry="4" fill="#fff" opacity=".35"/>' +
161 // the two ear-fins
162 [-1, 1]
163 .map(
164 s =>
165 `<g transform="translate(${s * 12} -12)"><g><animateTransform attributeName="transform" type="rotate" values="${s * -20};${s * 25};${s * -20}" dur="1.2s" repeatCount="indefinite"/><ellipse cx="${s * 8}" cy="-2" rx="9" ry="5" fill="#f2a6b8"/></g></g>`,
166 )
167 .join('') +
168 '<circle cx="-5" cy="-3" r="2.4" fill="#2a1420"/><circle cx="5" cy="-3" r="2.4" fill="#2a1420"/><circle cx="-4.4" cy="-3.8" r=".8" fill="#fff"/><circle cx="5.6" cy="-3.8" r=".8" fill="#fff"/>' +
169 '</g>'
170
171// ---- a gulper eel: a huge hinged mouth, a whip of a tail, and a red light at its tip ------------------
172const GULPER =
173 '<g><animateTransform attributeName="transform" type="translate" values="-140 470;-140 470;520 520;520 520" keyTimes="0;.3;.8;1" dur="58s" begin="-20s" repeatCount="indefinite"/>' +
174 // the tail whipping behind (to the left), its light flickering
175 '<path d="M-14 0 Q-50 -14 -80 0 T-150 0" fill="none" stroke="#1a1622" stroke-width="3" stroke-linecap="round"><animate attributeName="d" values="M-14 0 Q-50 -14 -80 0 T-150 0;M-14 0 Q-50 14 -80 0 T-150 0;M-14 0 Q-50 -14 -80 0 T-150 0" dur="1.8s" repeatCount="indefinite"/></path>' +
176 '<g><animateTransform attributeName="transform" type="translate" values="-150 0;-150 0;-150 0" dur="1.8s" repeatCount="indefinite"/>' + halo(5, '#ff4a6a', 1.6) + '<circle r="2.4" fill="#ffd0d8"><animate attributeName="opacity" values="1;.3;1;1" dur="1.4s" repeatCount="indefinite"/></circle></g>' +
177 '<path d="M-16 -4 Q0 -8 10 -6 L10 6 Q0 8 -16 4Z" fill="#1a1622" stroke="#2c3442"/>' +
178 // the mouth, opening and closing like a pelican's pouch
179 '<g><animateTransform attributeName="transform" type="rotate" values="0 8 0;-18 8 0;0 8 0" dur="5s" repeatCount="indefinite"/><path d="M8 -6 L46 -16 L44 -10 L8 0Z" fill="#1a1622" stroke="#2c3442"/></g>' +
180 '<g><animateTransform attributeName="transform" type="rotate" values="0 8 0;22 8 0;0 8 0" dur="5s" repeatCount="indefinite"/><path d="M8 0 Q30 22 46 12 L44 6 Q28 12 8 6Z" fill="#120e18" stroke="#2c3442"/></g>' +
181 '<circle cx="12" cy="-5" r="1.4" fill="#9ab4c8"/>' +
182 '</g>'
183
184// ---- far off, the ghost of a sperm whale gliding through the dark ------------------------------------
185const WHALE =
186 '<g opacity=".13"><animateTransform attributeName="transform" type="translate" values="-460 230;520 170" dur="150s" begin="-40s" repeatCount="indefinite"/>' +
187 '<g transform="scale(1.4)">' +
188 // the great square head, the body tapering to the flukes, which beat slowly
189 '<path d="M150 -30 Q172 -28 172 0 Q170 26 140 30 L-60 26 Q-140 18 -200 4 L-200 -4 Q-140 -18 -60 -30Z" fill="#7aa6c8"/>' +
190 '<path d="M60 26 Q90 50 110 40 Q96 30 90 22Z" fill="#7aa6c8"/>' +
191 '<g><animateTransform attributeName="transform" type="rotate" values="-10 -200 0;10 -200 0;-10 -200 0" dur="6s" repeatCount="indefinite"/><path d="M-196 0 L-250 -30 Q-238 0 -250 30Z" fill="#7aa6c8"/></g>' +
192 '<path d="M140 20 Q100 26 40 24" stroke="#4a6a88" stroke-width="2" fill="none"/>' +
193 '</g></g>'
194
195const ANGLER =
196 '<g><animateTransform attributeName="transform" type="translate" values="470 820;470 820;180 800;-120 830;-120 830" keyTimes="0;.2;.55;.85;1" dur="52s" begin="-6s" repeatCount="indefinite"/>' +
197 '<g><animateTransform attributeName="transform" type="translate" values="0 0;0 -5;0 0" dur="4s" repeatCount="indefinite"/>' +
198 // body, tail and fins, barely darker than the water, with a faint edge
199 '<path d="M-30 -6 Q-28 -34 4 -36 Q40 -34 48 -6 Q50 18 30 26 Q0 36 -26 20 Q-36 10 -30 -6Z" fill="#100c16" stroke="#26323f" stroke-width="1.2"/>' +
200 '<path d="M46 -4 L72 -22 L68 4 L74 24 L46 10Z" fill="#100c16" stroke="#26323f" stroke-width="1.2"><animateTransform attributeName="transform" type="rotate" values="-8 46 4;8 46 4;-8 46 4" dur="1.6s" repeatCount="indefinite"/></path>' +
201 '<path d="M10 14 L22 30 L28 14Z" fill="#16121e" stroke="#26323f"/>' +
202 // the gaping jaw and its needle teeth
203 '<path d="M-30 -2 Q-44 4 -36 18 Q-26 26 -14 22 Q-24 12 -30 -2Z" fill="#05040a" stroke="#26323f"/>' +
204 '<path d="M-33 1 l2 6 l2 -6 l2 7 l2 -7 l2 6 l2 -5 M-34 19 l3 -6 l2 5 l3 -7 l2 6 l3 -6" stroke="#e8eef4" stroke-width="1" fill="none"/>' +
205 '<circle cx="-14" cy="-14" r="3" fill="#cfe8ff" opacity=".7"/><circle cx="-14" cy="-14" r="1.2" fill="#000"/>' +
206 // the lure on its rod
207 '<g><animateTransform attributeName="transform" type="rotate" values="-6 -6 -32;8 -6 -32;-6 -6 -32" calcMode="spline" keyTimes="0;.5;1" keySplines="' + EASE + ';' + EASE + '" dur="3s" repeatCount="indefinite"/>' +
208 '<path d="M-6 -32 Q-20 -64 -46 -52" fill="none" stroke="#26323f" stroke-width="1.6"/>' +
209 '<g transform="translate(-48 -48)">' + halo(9, '#a8fff0', 1.4) + '<circle r="4.4" fill="#e8fff8"><animate attributeName="opacity" values="1;.55;1" dur="2.2s" repeatCount="indefinite"/></circle></g></g>' +
210 '</g></g>'
211
212// ---- a school of lanternfish, each with its rows of blue lights --------------------------------------------
213const LANTERNS =
214 '<g><animateTransform attributeName="transform" type="translate" values="-160 250;-160 250;460 230;460 230" keyTimes="0;.1;.75;1" dur="34s" begin="-2s" repeatCount="indefinite"/>' +
215 Array.from({ length: 9 }, (_, k) => {
216 const x = (k % 3) * 34 + rnd(k + 300) * 12 - (k > 5 ? 50 : 0)
217 const y = Math.floor(k / 3) * 22 + rnd(k + 310) * 8
218
219 return (
220 `<g transform="translate(${n(x)} ${n(y)})"><g><animateTransform attributeName="transform" type="translate" values="0 0;0 ${k % 2 ? 3 : -3};0 0" dur="${n(1.4 + rnd(k + 320))}s" repeatCount="indefinite"/>` +
221 '<path d="M-10 0 Q0 -5 9 0 Q0 5 -10 0Z M-10 0 L-15 -4 L-15 4Z" fill="#1a2430" stroke="#2c3c4c" stroke-width=".8"/>' +
222 '<circle cx="-4" cy="2" r="1" fill="#6ad8ff"/><circle cx="0" cy="2.4" r="1" fill="#6ad8ff"/><circle cx="4" cy="2" r="1" fill="#6ad8ff"/><circle cx="6" cy="-1" r="1.6" fill="#6ad8ff" opacity=".8"/></g></g>'
223 )
224 }).join('') +
225 '</g>'
226
227// ---- the submersible ---------------------------------------------------------------------------------------
228const sub = (lit: boolean) => {
229 const glass = lit ? '#ffe7a0' : '#2c6a78'
230 const glow = lit ? '#ffe7a0' : '#3ac0d0'
231
232 return (
233 // bubbles trailing up from the stern
234 Array.from({ length: lit ? 6 : 3 }, (_, k) => {
235 const dur = lit ? 3 : 5
236
237 return `<circle r="${n(1.4 + (k % 3))}" fill="none" stroke="#bfe6ff" stroke-opacity=".6"><animateTransform attributeName="transform" type="translate" values="-54 ${-4 + (k % 2) * 6};${-70 - k * 4} -60;${-64 - k * 6} -130" dur="${dur}s" begin="${n(-k * (dur / (lit ? 6 : 3)))}s" repeatCount="indefinite"/><animate attributeName="opacity" values="1;.6;0" dur="${dur}s" begin="${n(-k * (dur / (lit ? 6 : 3)))}s" repeatCount="indefinite"/></circle>`
238 }).join('') +
239 // the propeller, a blur when it is busy
240 `<g transform="translate(-54 0)"><rect x="-4" y="-2" width="6" height="4" fill="#6a5a3a"/><g><animateTransform attributeName="transform" type="scale" values="1 1;1 -1;1 1" dur="${lit ? 0.18 : 0.8}s" repeatCount="indefinite"/><path d="M-6 0 L-10 -12 L-4 -12Z M-6 0 L-10 12 L-4 12Z" fill="#8a7a5a"/></g></g>` +
241 // hull, tower, rails and skids
242 '<ellipse cx="0" cy="0" rx="48" ry="25" fill="#e8b42e"/><path d="M-46 6 Q0 34 46 6 Q40 22 0 25 Q-40 22 -46 6Z" fill="#b8841a"/>' +
243 '<path d="M-14 -24 V-38 Q-14 -42 -10 -42 H12 Q16 -42 16 -38 V-22" fill="#d8a024"/><rect x="-6" y="-50" width="3" height="10" fill="#8a7a5a"/>' +
244 '<path d="M-36 28 H36 M-26 25 V28 M26 25 V28" stroke="#6a5a3a" stroke-width="3"/>' +
245 '<path d="M-40 -6 H30" stroke="#b8841a" stroke-width="1.4"/>' +
246 // a little arm folded under the bow
247 '<path d="M30 16 L44 24 L52 20" fill="none" stroke="#8a7a5a" stroke-width="3" stroke-linecap="round"/><path d="M52 20 l4 -4 M52 20 l5 2" stroke="#8a7a5a" stroke-width="2"/>' +
248 // portholes: the big bow window and two small ones
249 `<g transform="translate(26 -2)">${halo(12, glow, lit ? 1.2 : 0.6)}<circle r="12" fill="#5a4a2a"/><circle r="9" fill="${glass}"/><path d="M-5 -4 Q-2 -7 2 -7" stroke="#fff" stroke-opacity=".6" stroke-width="1.6" fill="none"/></g>` +
250 [-6, -22].map(x => `<circle cx="${x}" cy="-4" r="5" fill="#5a4a2a"/><circle cx="${x}" cy="-4" r="3.4" fill="${glass}" opacity="${lit ? 1 : 0.7}"/>`).join('') +
251 // the lamp on the bow
252 `<rect x="40" y="2" width="8" height="8" rx="2" fill="#5a4a2a"/><circle cx="48" cy="6" r="3" fill="${lit ? '#fffbe0' : '#4a5a60'}"/>`
253 )
254}
255
256// while Claude works: the searchlight sweeping ahead, and sonar pinging outward
257const BEAM =
258 `<g><animateTransform attributeName="transform" type="rotate" values="-7 48 6;9 48 6;-7 48 6" calcMode="spline" keyTimes="0;.5;1" keySplines="${EASE};${EASE}" dur="11s" repeatCount="indefinite"/>` +
259 '<polygon points="48,1 48,11 340,170 340,-140" fill="url(#beam)"><animate attributeName="opacity" values=".95;.8;.95;.9;.95" dur="3.4s" repeatCount="indefinite"/></polygon>' +
260 '<polygon points="48,4 48,8 340,70 340,-50" fill="url(#beam)" opacity=".6"/></g>'
261
262const SONAR = [0, 1]
263 .map(
264 k =>
265 `<circle r="20" fill="none" stroke="#5ff0ff" stroke-width="1.6" opacity="0"><animate attributeName="r" values="30;260" dur="4s" begin="${-k * 2}s" repeatCount="indefinite"/><animate attributeName="opacity" values=".5;0" dur="4s" begin="${-k * 2}s" repeatCount="indefinite"/></circle>`,
266 )
267 .join('')
268
269// ...and now and then the shadow of a giant squid sweeps across, mantle first, arms trailing
270const SQUID = (() => {
271 const arms = Array.from({ length: 8 }, (_, k) => {
272 const y = -18 + k * 5
273 const len = 150 + (k % 3) * 20
274 const a = `M-20 ${y} Q${-len * 0.5} ${y - 18} ${-len} ${y + (k - 3.5) * 6}`
275 const b = `M-20 ${y} Q${-len * 0.5} ${y + 18} ${-len} ${y + (k - 3.5) * 9}`
276
277 return `<path d="${a}" fill="none" stroke="#000" stroke-width="${7 - Math.abs(k - 3.5)}" stroke-linecap="round"><animate attributeName="d" values="${a};${b};${a}" dur="${n(1.6 + k * 0.1)}s" repeatCount="indefinite"/></path>`
278 }).join('')
279 const tentacles = [-1, 1]
280 .map(
281 s =>
282 `<path d="M-20 ${s * 4} Q-160 ${s * 40} -300 ${s * 10}" fill="none" stroke="#000" stroke-width="3"><animate attributeName="d" values="M-20 ${s * 4} Q-160 ${s * 40} -300 ${s * 10};M-20 ${s * 4} Q-160 ${-s * 20} -300 ${s * 30};M-20 ${s * 4} Q-160 ${s * 40} -300 ${s * 10}" dur="2.4s" repeatCount="indefinite"/></path><ellipse cx="-300" cy="${s * 12}" rx="14" ry="5" fill="#000"/>`,
283 )
284 .join('')
285
286 return (
287 '<g opacity=".72"><animateTransform attributeName="transform" type="translate" values="-560 300;-560 300;980 210;980 210" keyTimes="0;.62;.86;1" dur="32s" begin="-14s" repeatCount="indefinite"/>' +
288 `<g transform="scale(1.3)">${tentacles}${arms}` +
289 '<path d="M150 0 Q120 -30 40 -28 Q0 -26 -20 -14 L-20 14 Q0 26 40 28 Q120 30 150 0Z" fill="#000"/>' +
290 '<path d="M150 0 L110 -42 L96 -18Z M150 0 L110 42 L96 18Z" fill="#000"/>' +
291 '<circle cx="-8" cy="-8" r="5" fill="#0a1218"/></g></g>'
292 )
293})()
294
295// ---- the whole picture --------------------------------------------------------------------------------
296const buildScene = (w: Sub, elapsed: number, vh: number) => {
297 const view = Math.min(H, Math.max(vh, 240))
298 // a short pane trims the water above and below the submersible alike
299 const y0 = (H - view) / 2
300 const lit = w.working
301
302 const defs =
303 '<defs>' +
304 '<linearGradient id="deep" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0a2236"/><stop offset=".45" stop-color="#05121e"/><stop offset="1" stop-color="#010409"/></linearGradient>' +
305 `<linearGradient id="beam" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#fff6c8" stop-opacity=".55"/><stop offset=".6" stop-color="#cfefff" stop-opacity=".16"/><stop offset="1" stop-color="#cfefff" stop-opacity="0"/></linearGradient>` +
306 '</defs>'
307
308 const water =
309 `<rect x="0" y="0" width="${W}" height="${H}" fill="url(#deep)"/>` +
310 // the last of the light, far above
311 [1, 1.5, 2.1].map(k => `<ellipse cx="200" cy="-80" rx="${n(180 * k)}" ry="${n(110 * k)}" fill="#2a5a7a" opacity=".07"/>`).join('') +
312 WHALE +
313 THING +
314 FLOOR +
315 SNOWFALL +
316 COMB +
317 LANTERNS +
318 GULPER +
319 DUMBO +
320 JELLIES +
321 ANGLER
322
323 const craft =
324 `<g><animateTransform attributeName="transform" type="translate" values="${ROUTE.map(([x, y]) => `${x} ${y}`).join(';')}" calcMode="spline" keyTimes="${ROUTE.map((_, k) => n(k / (ROUTE.length - 1))).join(';')}" keySplines="${ROUTE.slice(1).map(() => EASE).join(';')}" dur="${ROUTE_DUR}s" repeatCount="indefinite"/>` +
325 (lit ? SONAR : '') +
326 `<g><animateTransform attributeName="transform" type="translate" values="0 0;0 -7;0 0" calcMode="spline" keyTimes="0;.5;1" keySplines="${EASE};${EASE}" dur="6s" repeatCount="indefinite"/>` +
327 `<g><animateTransform attributeName="transform" type="rotate" values="-1.6;1.6;-1.6" dur="9s" repeatCount="indefinite"/>${lit ? BEAM : ''}${sub(lit)}</g></g></g>` +
328 (lit ? SQUID : '')
329
330 return (
331 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 ${n(y0)} ${W} ${n(view)}" width="${W}" height="${n(view)}" preserveAspectRatio="xMidYMid slice">${defs}` +
332 retime(water + craft, elapsed) +
333 '</svg>'
334 )
335}
336// ---scene-end
337
338const TERMINAL_ART: [string, string][] = [
339 [' · ◌ · ◌ ', '#5ff0ff'],
340 [' ▄▄█▄▄ ─ ─ ─ ─', '#e8b42e'],
341 [' ◖ ████◉█▶ ═══ ', '#e8b42e'],
342 [' · ◌ · ◌ ', '#ff7ae0'],
343 [' ▁▂▃▂▁▁▂▃▄▃▂▁▁▂▃▂▁ ', '#26323f'],
344]
345
346const clock = (ms: number) => {
347 const s = Math.max(0, Math.floor(ms / 1000))
348
349 return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
350}
351
352const label = (w: Dive, now: number, T: Words) => (w.working ? T.working(clock(now - w.startedAt), w.tools) : T.idle)
353
354const dive = atom({ plugin: 'deep-sub', key: 'dive' } as const, { working: false, tools: 0, startedAt: 0, endedAt: 0, tick: 0 } as Dive)
355
356// The picture is built again only when the state it draws changes; the same text keeps it running.
357let t0 = 0
358let cache = { key: '', svg: '' }
359
360export const register: Register = (on, options) => {
361 // `language` is a field of the plugin's userConfig: 'en' (default) or 'ko'
362 const T: Words = STRINGS[options.language === 'ko' ? 'ko' : 'en']
363
364 on('session.start', async ($, e, next) => {
365 t0 = await $.clock.now()
366 await $.command.register({
367 name: 'deep',
368 description: 'Open the deep-sea submersible pane',
369 })
370 // Wakes the pane each second while Claude works, for the stopwatch in its top line.
371 $.clock.every(1000, () => {
372 void read($, dive)
373 .then(r => (r.working ? update($, dive, v => ({ ...v, tick: v.tick + 1 })) : undefined))
374 .catch(() => {})
375 })
376
377 return next(e)
378 })
379
380 on('command.run', { command: 'deep' }, async $ => {
381 await $.ui.open({ id: PANE, title: T.title })
382
383 return { text: T.opened }
384 })
385
386 // The main loop's turns only: a subagent's run is not Claude at work.
387 on('turn.start', async ($, e, next) => {
388 if (e.agentId === undefined) {
389 const now = await $.clock.now()
390 await update($, dive, v => ({ ...v, working: true, tools: 0, startedAt: now }))
391 }
392
393 return next(e)
394 }).catch(($, e, next) => next(e))
395
396 on('turn.complete', async ($, e, next) => {
397 if (e.agentId === undefined) {
398 const now = await $.clock.now()
399 await update($, dive, v => ({ ...v, working: false, endedAt: now }))
400 }
401
402 return next(e)
403 }).catch(($, e, next) => next(e))
404
405 on('tool.call', async ($, e, next) => {
406 await update($, dive, v => ({ ...v, tools: v.tools + 1 }))
407
408 return next(e)
409 }).catch(($, e, next) => next(e))
410
411 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
412 const els = $.ui.resolve(e)
413 const w = await read($, dive)
414 const now = await $.clock.now()
415 if (t0 === 0) t0 = now
416 const cols = e.props?.bodyColumns ?? 40
417 const rows = e.props?.scroll?.bodyRows ?? 20
418 const line = label(w, now, T)
419
420 if (e.surface !== 'terminal') {
421 const { Box, Text, Svg } = els as ElementTable<'desktop'>
422 // The pane's px: its cells over the code font's advance (about 7.8 px) and line height (about 19.5 px).
423 const width = Math.max(160, Math.round(cols * 7.8))
424 const full = Math.round((width * H) / W)
425 const tall = e.props?.scroll?.bodyRows ? Math.round((rows - 1) * 19.5) : full
426 const height = Math.min(full, tall)
427 const key = `${w.working}|${height}|${width}`
428 if (cache.key !== key) cache = { key, svg: buildScene(w, (now - t0) / 1000, (height * W) / width) }
429
430 return (
431 <Box flexDirection="column" justifyContent="center" minHeight={rows}>
432 <Box justifyContent="center">
433 <Text bold={w.working} dimColor={!w.working}>
434 {line}
435 </Text>
436 </Box>
437 <Svg source={cache.svg} alt={T.alt} width={width} height={height} isInteractive />
438 </Box>
439 )
440 }
441
442 const { Box, Text } = els as ElementTable<'terminal'>
443
444 return (
445 <Box flexDirection="column">
446 {TERMINAL_ART.map(([row, color]) => (
447 <Text color={color}>{row}</Text>
448 ))}
449 <Text>{line}</Text>
450 <Text dimColor>{T.note}</Text>
451 </Box>
452 )
453 })
454}
455types/index.d.ts 14 lines1export type Dive = {
2 working: boolean
3 tools: number
4 startedAt: number
5 endedAt: number
6 tick: number
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'deep-sub': { dive: Dive }
12 }
13}
14