Claude Code's task list as a progress bar above the prompt: a pixel track in the Desktop app, dotted text in the terminal.

A Claude Code mod that draws the session's task list as a progress bar above the prompt: the task Claude is on, a track that fills as tasks finish with a pill naming the count, and the share done.
● Running tests ⣿⣿⣿⣿⣿⣿│⣿⣿⣿⣿⣿⣿ Tasks 3/5 ⣿⣿⣿⣿│⣿⣿⣿⣿⣿⣿ 40% ×
The pill names the task Claude is on and the figure is the share done, so two of five done reads Tasks 3/5 and 40%. The Desktop app draws the track as a pixel bar; the terminal draws it as dotted text, as above. Once every task in the batch is done the bar goes away, until Claude starts new tasks.
claude --version).~/.claude/settings.json, which covers the terminal and the Desktop app alike: { "env": { "CLAUDE_CODE_ENABLE_TODO_TOOLS": "1" } }
Merge the env key into your existing settings rather than replacing the file. Without the tools the bar says "Tasks off". The tool definitions take some context of their own; /context shows how much.
claude --plugin-dir ./super-bar
The folder is watched, so saving a file reloads the mod in place.
claude plugin marketplace add RakshitSathvara/rakshit-mods
claude plugin install super-bar@rakshit-mods --scope user
Run /reload-plugins in any open session. When a new version comes out, run claude plugin marketplace update rakshit-mods, then claude plugin update super-bar@rakshit-mods.
claude plugin validate ./super-bar lists every call: $.command.register, $.session.messages, $.state, $.tool.list and $.ui.resolve. It reads no files, starts no processes, makes no network requests and calls no model, so it spends no tokens.
/resume the tasks are rebuilt from the transcript's newest 4,096 entries.claude plugin validate ./super-bar
claude plugin test ./super-bar
hooks/tasks-bar.mjs handles the events, keeps state and lays out the band. hooks/track.mjs draws the Desktop app's pixel track as SVG, and hooks/bar.mjs the terminal's dotted bar. Claude Code writes type declarations for your build into .claude-plugin/types/ the first time it loads the mod.
The Desktop app's pixel track is adapted from plan-progress by Kirill Serditov, under the MIT License; LICENSE carries its notice.
hooks/tasks-bar.mjs 281 lines1// Super Bar: Claude Code's task list as a progress bar above the prompt.
2//
3// One bar for the batch of tasks Claude is working through: the task it's on, a
4// track that fills as tasks finish with a pill naming the count, and the share
5// done. It goes once every task in the batch is done. The desktop draws the
6// track as an SVG (track.mjs), the terminal as dotted text (bar.mjs). × hides
7// the bar until Claude creates the next task; /taskbar brings it back.
8//
9// Needs the task tools, which Claude Code leaves out on newer models unless you
10// start it with CLAUDE_CODE_ENABLE_TODO_TOOLS=1. Without them the bar says so.
11
12import { atom, read, update } from 'claude-code'
13import { textRow } from './bar.mjs'
14import { TRACK_H, textWidth, trackSvg } from './track.mjs'
15
16// The desktop track's accent, and the terminal bar's palette in the same violet.
17const ACCENT = '#8B7CF6'
18const VIOLET = { mark: '#a99cf7', fill: '#9a8af4', pillBg: '#7d6cf0', pillFg: '#ffffff', tick: '#e4dffd' }
19
20const TASK_TOOLS = ['TaskCreate', 'TaskUpdate', 'TodoWrite']
21const STATUSES = ['pending', 'in_progress', 'completed']
22const CLOSE = 2 // "×" and the gap before it, in cells
23// A space as wide as a digit, so ' 0%' and '100%' take the same room.
24const FIGURE_SPACE = String.fromCharCode(0x2007)
25
26// Held by the host, so they survive a hot reload of this file.
27const board = atom({ plugin: 'super-bar', key: 'board' }, { tasks: [], batch: 1, toolsOn: null })
28const view = atom({ plugin: 'super-bar', key: 'view' }, { hidden: false })
29
30export function register(on) {
31 on('session.start', async ($, e, next) => {
32 const result = await next(e)
33 await checkTools($)
34 if ((await read($, board)).tasks.length === 0) await replay($) // claude --resume / --continue
35 try {
36 await $.command.register({
37 name: 'taskbar',
38 description: 'Show the task bar again, or hide it',
39 argumentHint: '[hide]',
40 immediate: true,
41 })
42 } catch {
43 // The name is taken: the bar still works, only the command is missing.
44 }
45 return result
46 })
47
48 // /clear, /resume and /branch put $.state back to its defaults.
49 on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
50 const result = await next(e)
51 await checkTools($)
52 if (e.source !== 'clear') await replay($)
53 return result
54 })
55
56 // Watch the task tools: let each call run, then fold its result into the board.
57 on('tool.call', { tool: ['TaskCreate', 'TaskUpdate', 'TodoWrite'] }, async ($, e, next) => {
58 const r = await next(e)
59 try {
60 if (r && !r.deny && !r.isError && r.result != null) {
61 const { tool, tool_use_id, ...input } = e
62 await update($, board, (b) => reduce({ ...b, toolsOn: true }, tool, input, r.result))
63 if (tool === 'TaskCreate') await update($, view, (v) => (v.hidden ? { ...v, hidden: false } : v))
64 }
65 } catch {
66 // Never let the bar get in the way of the tool's result.
67 }
68 return r
69 })
70
71 // /model may have changed which tools exist.
72 on('turn.complete', async ($, e, next) => {
73 const result = await next(e)
74 if (!e.agentId) await checkTools($) // main-loop turns only, not subagents
75 return result
76 })
77
78 on('command.run', { command: 'taskbar' }, async ($, e) => {
79 const hidden = String(e.args ?? '').trim().toLowerCase() === 'hide'
80 await update($, view, (v) => ({ ...v, hidden }))
81 return {}
82 })
83
84 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
85 if (e.props.hasSurvey) return next(e)
86 if ((await read($, view)).hidden) return next(e)
87 const b = await read($, board)
88 const tasks = b.tasks.filter((t) => t.batch === b.batch)
89 // Nothing to show before Claude's first task (unless the tools are off), or
90 // once every task in the batch is done.
91 if (tasks.length === 0 && b.toolsOn !== false) return next(e)
92 if (tasks.length > 0 && tasks.every((t) => t.status === 'completed')) return next(e)
93 const theirs = await next(e) // keep what mods after this one draw in the band
94 return band($.ui.resolve(e), e, tasks, b.batch, theirs, $)
95 })
96}
97
98// ── Tasks ───────────────────────────────────────────────────────────────────
99
100// Fold one answered task-tool call into the board. Tasks created after every
101// task of the current batch is done start a new batch.
102function reduce(b, tool, input, result) {
103 const tasks = b.tasks.slice()
104 let batch = b.batch
105 const current = () => tasks.filter((t) => t.batch === batch)
106 const allDone = (list) => list.length > 0 && list.every((t) => t.status === 'completed')
107
108 if (tool === 'TaskCreate') {
109 if (allDone(current())) batch += 1
110 const id = String(result?.task?.id ?? nextId(tasks))
111 const subject = String(input.subject ?? result?.task?.subject ?? 'Task')
112 tasks.push({ id, subject, activeForm: text(input.activeForm), status: 'pending', batch })
113 return { ...b, tasks, batch }
114 }
115
116 if (tool === 'TaskUpdate') {
117 if (result?.success === false) return b
118 const i = tasks.findIndex((t) => t.id === String(input.taskId ?? result?.taskId ?? ''))
119 if (i < 0) return b
120 if (input.status === 'deleted') {
121 tasks.splice(i, 1)
122 return { ...b, tasks }
123 }
124 const t = { ...tasks[i] }
125 if (STATUSES.includes(input.status)) t.status = input.status
126 if (typeof input.subject === 'string') t.subject = input.subject
127 if (typeof input.activeForm === 'string') t.activeForm = input.activeForm
128 tasks[i] = t
129 return { ...b, tasks }
130 }
131
132 if (tool === 'TodoWrite') {
133 const todos = Array.isArray(result?.newTodos) ? result.newTodos : Array.isArray(input.todos) ? input.todos : null
134 if (!todos) return b
135 const known = new Set(current().map((t) => t.subject))
136 if (allDone(current()) && todos.some((t) => !known.has(String(t.content)))) batch += 1
137 const list = todos.map((t, k) => ({
138 id: `todo-${batch}-${k + 1}`,
139 subject: String(t.content ?? ''),
140 activeForm: text(t.activeForm),
141 status: STATUSES.includes(t.status) ? t.status : 'pending',
142 batch,
143 }))
144 return { ...b, tasks: [...tasks.filter((t) => t.batch !== batch), ...list], batch }
145 }
146
147 return b
148}
149
150// Rebuild the board from the transcript, after a resume or a fresh load.
151async function replay($) {
152 let messages
153 try {
154 messages = await $.session.messages()
155 } catch {
156 return
157 }
158 let b = { tasks: [], batch: 1, toolsOn: true }
159 let seen = false
160 for (const m of messages ?? []) {
161 for (const use of m?.toolUses ?? []) {
162 if (!TASK_TOOLS.includes(use.tool) || use.isError || use.result == null) continue
163 b = reduce(b, use.tool, use.input ?? {}, use.result)
164 seen = true
165 }
166 }
167 if (seen) await update($, board, () => b)
168}
169
170async function checkTools($) {
171 let on
172 try {
173 const tools = await $.tool.list()
174 on = tools.some((t) => t?.name === 'TaskCreate' || t?.name === 'TodoWrite')
175 } catch {
176 return
177 }
178 if ((await read($, board)).toolsOn !== on) await update($, board, (b) => ({ ...b, toolsOn: on }))
179}
180
181async function hide($) {
182 await update($, view, (x) => ({ ...x, hidden: true }))
183}
184
185// ── Drawing ─────────────────────────────────────────────────────────────────
186
187function band(ui, e, tasks, batch, theirs, $) {
188 const { Box, Button } = ui
189 // The band's padding takes a cell on each side, and the row stays two cells
190 // short of the edge, where the terminal draws its close mark.
191 const width = Math.max(24, Number(e.props.bodyColumns) || 80) - 4
192 const p = tasks.length > 0 ? progress(tasks) : null
193 const close = Button({ key: 'tb-close', label: '×', plain: true, role: 'dismiss', dimColor: true, onPress: () => hide($) })
194 // Every surface but the terminal draws Svg, so they get the pixel track and the
195 // terminal gets text. Ask the surface: the table holds every constructor.
196 const row =
197 p && e.surface !== 'terminal'
198 ? Box({ key: 'tb-active', flexDirection: 'row', alignItems: 'center', gap: 1, children: [...trackRow(ui, e, p, batch), close] })
199 : Box({
200 key: 'tb-active',
201 flexDirection: 'row',
202 width,
203 columnGap: 1,
204 children: [Box({ width: width - CLOSE, children: [textRow(ui, p ? textProps(p, width) : toolsOff(), width - CLOSE)] }), close],
205 })
206 return Box({ flexDirection: 'column', paddingX: 1, children: [row, theirs] })
207}
208
209// One batch as the bar shows it. The pill names the task you're on and the
210// figure is the share done: 2 of 5 done reads "Tasks 3/5" and 40%.
211function progress(tasks) {
212 const n = tasks.length
213 const done = tasks.filter((t) => t.status === 'completed').length
214 const cur =
215 tasks.find((t) => t.status === 'in_progress') ?? tasks.find((t) => t.status === 'pending') ?? tasks[0]
216 return {
217 n,
218 done,
219 at: Math.min(done + 1, n),
220 pct: Math.round((done / n) * 100),
221 label: cur.status === 'in_progress' && cur.activeForm ? cur.activeForm : cur.subject,
222 }
223}
224
225// The desktop row, as plan-progress lays it out: the state, the task, then the
226// track pinned right at a width that fits, and the share done. The desktop
227// reports about 8 CSS pixels per column.
228function trackRow({ Box, Text, Svg }, e, p, batch) {
229 const total = Math.max(320, (Number(e.props.bodyColumns) || 100) * 8)
230 const titleW = Math.min(Math.round(total * 0.3), Math.round(textWidth(p.label, 6.4)))
231 const trackW = Math.max(120, Math.min(1400, total - titleW - 140))
232 const alt = `${p.label}: task ${p.at} of ${p.n}, ${p.pct}% done`
233 const source = trackSvg({ id: String(batch), total: p.n, finished: p.done, color: ACCENT }, trackW)
234 return [
235 Text({ color: ACCENT, children: ['●'] }),
236 Text({ wrap: 'truncate', children: [p.label] }),
237 Box({ flexGrow: 1 }),
238 Svg({ source, alt, width: trackW, height: TRACK_H }),
239 Text({ dimColor: true, children: [`${String(p.pct).padStart(3, FIGURE_SPACE)}%`] }),
240 ]
241}
242
243// The terminal row's props for bar.mjs.
244function textProps(p, width) {
245 return {
246 mark: '●',
247 markColor: VIOLET.mark,
248 label: p.label,
249 labelWidth: width >= 70 ? Math.min(32, Math.round(width * 0.3)) : width >= 50 ? 14 : 0,
250 fraction: p.done / p.n,
251 ticks: Array.from({ length: p.n - 1 }, (_, k) => (k + 1) / p.n),
252 pill: `Tasks ${p.at}/${p.n}`,
253 pillShort: `${p.at}/${p.n}`,
254 fill: VIOLET.fill,
255 pillBg: VIOLET.pillBg,
256 pillFg: VIOLET.pillFg,
257 tickColor: VIOLET.tick,
258 right: `${p.pct}%`,
259 rightWidth: 4,
260 }
261}
262
263function toolsOff() {
264 return {
265 mark: '○',
266 markColor: 'gray',
267 label: 'Tasks off',
268 labelDim: true,
269 hint: 'set CLAUDE_CODE_ENABLE_TODO_TOOLS=1 and restart Claude Code',
270 right: '',
271 }
272}
273
274function nextId(tasks) {
275 return tasks.reduce((max, t) => Math.max(max, Number(t.id) || 0), 0) + 1
276}
277
278function text(value) {
279 return typeof value === 'string' && value.trim() ? value : null
280}
281hooks/bar.mjs 123 lines1// Super Bar's terminal bar.
2//
3// Draws one dotted progress row: a mark, a label, a braille-dot bar with tick
4// marks and a pill riding the fill's edge, then a figure on the right. The
5// hooks module draws it straight into the band with its own element table;
6// the desktop draws track.mjs's SVG instead.
7
8const DOT = '⣿'
9const TICK = '│'
10const TRACK = 'gray'
11
12// The row as Text runs in a Box, sized to `cols` cells.
13export function textRow({ Box, Text }, props, cols) {
14 return Box({ flexDirection: 'row', children: runs(Text, layout(props ?? {}, Math.max(8, cols))) })
15}
16
17// The row as styled segments, left to right, sized to `cols` cells.
18function layout(p, cols) {
19 const segs = []
20 const add = (text, style = {}) => {
21 if (text) segs.push({ text, ...style })
22 }
23
24 const right = String(p.right ?? '')
25 const rightW = Math.max(Number(p.rightWidth) || 0, right.length)
26 const tail = rightW > 0 ? rightW + 1 : 0
27 let pill = ` ${String(p.pill ?? '')} `
28 const spare = cols - 2 - tail // what's left after the mark and its space
29 const minTrack = pill.length + 8
30
31 // Give the bar room first: shorten the label, then drop it. A hint row keeps
32 // its short label only while the whole message fits beside it.
33 let labelW = Math.max(0, Number(p.labelWidth) || 0)
34 if (p.hint) {
35 const own = Array.from(String(p.label ?? '')).length
36 labelW = spare - (own + 1) >= Array.from(String(p.hint)).length ? own : 0
37 } else {
38 if (labelW > 0 && spare - (labelW + 1) < minTrack) labelW = spare - 1 - minTrack
39 if (labelW < 8) labelW = 0
40 }
41 const track = Math.max(0, spare - (labelW > 0 ? labelW + 1 : 0))
42 if (track < pill.length + 4 && p.pillShort) pill = ` ${String(p.pillShort)} `
43
44 add(`${String(p.mark ?? ' ')} `, { color: p.markColor })
45 if (labelW > 0) {
46 const text = fit(String(p.label ?? ''), labelW)
47 add(text, { color: p.labelColor, dim: Boolean(p.labelDim) })
48 add(' '.repeat(labelW - Array.from(text).length + 1))
49 }
50 if (p.hint) add(fit(String(p.hint), track).padEnd(track), { color: TRACK })
51 else bar(segs, p, track, pill)
52 if (rightW > 0) add(` ${right.padStart(rightW)}`, { color: p.rightColor, dim: !p.rightColor })
53 return segs
54}
55
56// Dots up to the fill's edge, dim dots after it, ticks at the boundaries, and the
57// pill centred on the edge so it sits where the work is (clamped at both ends).
58function bar(segs, p, T, pill) {
59 if (T <= 0) return
60 const P = Math.min(pill.length, T)
61 const filled = Math.round(clamp01(p.fraction) * T)
62 const start = Math.max(0, Math.min(T - P, Math.round(filled - P / 2)))
63 const ticks = tickCells(p.ticks, T)
64 for (let i = 0; i < T; i++) {
65 if (i === start) {
66 segs.push({ text: pill.slice(0, P), color: p.pillFg, bg: p.pillBg, bold: true })
67 i += P - 1
68 continue
69 }
70 const lit = i < filled
71 if (ticks.has(i)) segs.push({ text: TICK, color: lit ? p.tickColor : TRACK, dim: !lit })
72 else if (lit) segs.push({ text: DOT, color: p.fill, dim: speckle(i) })
73 else segs.push({ text: DOT, color: TRACK, dim: true })
74 }
75}
76
77// Tick positions in cells, or none when they'd crowd the bar.
78function tickCells(fractions, T) {
79 const out = new Set()
80 if (!Array.isArray(fractions) || fractions.length === 0) return out
81 if (T / (fractions.length + 1) < 3) return out
82 for (const f of fractions) {
83 const i = Math.round(f * T)
84 if (i > 0 && i < T) out.add(i)
85 }
86 return out
87}
88
89// About one lit dot in five drawn dim, the same ones every time: the dot-matrix texture.
90function speckle(i) {
91 return ((i * 2654435761) >>> 0) % 5 === 0
92}
93
94function clamp01(n) {
95 const x = Number(n)
96 return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : 0
97}
98
99function fit(text, w) {
100 const chars = Array.from(text.replace(/\s+/g, ' ').trim())
101 if (chars.length <= w) return chars.join('')
102 return `${chars.slice(0, Math.max(0, w - 1)).join('')}…`
103}
104
105// Adjacent segments with the same style become one Text.
106function runs(Text, segs) {
107 const merged = []
108 for (const s of segs) {
109 const key = `${s.color ?? ''}|${s.bg ?? ''}|${s.dim ? 1 : 0}|${s.bold ? 1 : 0}`
110 const last = merged[merged.length - 1]
111 if (last && last.key === key) last.text += s.text
112 else merged.push({ ...s, key })
113 }
114 return merged.map((s) => {
115 const props = { children: [s.text] }
116 if (s.color) props.color = s.color
117 if (s.bg) props.backgroundColor = s.bg
118 if (s.dim) props.dimColor = true
119 if (s.bold) props.bold = true
120 return Text(props)
121 })
122}
123hooks/track.mjs 113 lines1// Super Bar's desktop track.
2//
3// The batch as an SVG the band draws as an image, so it needs no Client: a
4// pixel fill that thickens towards its edge, a tick between tasks, and a pill
5// riding the edge with the count. Adapted from plan-progress by Kirill
6// Serditov (https://github.com/zycck/claude-mods), MIT License; see LICENSE.
7
8export const TRACK_H = 22
9const NARROW = 360 // below this the pill becomes a dot with the task number
10
11const hex = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16))
12const mix = (a, b, m) => a.map((v, i) => Math.round(v + ((b[i] ?? 0) - v) * m))
13const rgb = (c) => `rgb(${c.join(',')})`
14const hash = (a, b, k) => {
15 const x = Math.sin(a * 127.1 + b * 311.7 + k * 74.7) * 43758.5453
16 return x - Math.floor(x)
17}
18
19// A string's rough width in CSS pixels, in the desktop's 12px sans.
20export const textWidth = (s, px = 6.7) =>
21 [...s].reduce(
22 (w, ch) =>
23 w +
24 (/[ -鿿]/.test(ch) ? 12 : /[ilI.,:;'|!]/.test(ch) ? 3.4 : /[mwMWЖМШЩ]/.test(ch) ? 9.5 : px),
25 0,
26 )
27
28// Where each batch's head was last drawn, so a redraw glides from there.
29const lastHead = new Map()
30
31// One batch, W pixels wide: { id, total, finished, color }.
32export function trackSvg(t, W) {
33 const H = TRACK_H
34 const total = Math.max(1, t.total)
35 // The fill is exactly the finished share: a fresh batch starts empty.
36 const fx = Math.min(1, t.finished / total) * W
37 const from = lastHead.get(t.id) ?? fx
38 lastHead.set(t.id, fx)
39
40 const acc = hex(t.color)
41 const light = mix(acc, [255, 255, 255], 0.32)
42 const grey = [132, 130, 138]
43 const ease = 'calcMode="spline" keyTimes="0;1" keySplines=".2 .8 .2 1"'
44 const glide = Math.abs(from - fx) > 0.5
45
46 // Pixels on a 3px grid, 7 rows, denser and closer to the accent towards the head.
47 const buckets = [0, 1, 2, 3, 4].map((b) => {
48 const m = b / 4
49 const dense = 0.22 + 0.78 * Math.pow(m, 1.5)
50 return { color: rgb(mix(grey, light, m)), opacity: (0.35 + 0.65 * dense).toFixed(2) }
51 })
52 let px = ''
53 for (let col = 0; col * 3 < fx; col++) {
54 const x = col * 3
55 const u = Math.min(1, (x + 1.5) / fx)
56 const dense = 0.22 + 0.78 * Math.pow(u, 1.5)
57 const bucket = Math.min(4, Math.floor(Math.min(1, Math.pow(u, 0.9) * 1.1) * 4.99))
58 for (let r = 0; r < 7; r++) {
59 if (hash(col, r, 1) > dense + 0.1) continue
60 px += `<rect x="${x}" y="${1 + r * 3}" class="b${bucket} t${Math.floor(hash(col, r, 2) * 4)}"/>`
61 }
62 }
63
64 // A short tick between tasks, brighter once passed.
65 let marks = ''
66 for (let k = 1; k < total; k++) {
67 const x = (k / total) * W
68 const passed = x < fx - 1
69 const fill = passed ? rgb(mix(light, [255, 255, 255], 0.45)) : '#8A8984'
70 marks += `<rect x="${(x - 0.75).toFixed(1)}" y="${(H - 8) / 2}" width="1.5" height="8" rx=".75" fill="${fill}" opacity="${passed ? 0.6 : 0.45}"/>`
71 }
72
73 // The knob: a pill with "Tasks 3/5", or a round dot with the number when narrow.
74 const number = Math.min(total, t.finished + 1)
75 let knob
76 let kw = H
77 if (W < NARROW) {
78 knob = `<circle cx="0" cy="${H / 2}" r="${H / 2}" fill="${t.color}"/><text x="0" y="${H / 2 + 4.2}" text-anchor="middle" class="kt">${number}</text>`
79 } else {
80 const count = `${number}/${total}`
81 kw = Math.round(20 + textWidth('Tasks') + 6 + textWidth(count, 6.5))
82 knob = `<rect x="${-kw / 2}" y="0" width="${kw}" height="${H}" rx="${H / 2}" fill="${t.color}"/>`
83 knob += `<text x="${-kw / 2 + 10}" y="${H / 2 + 4.2}" class="kt">Tasks<tspan class="kc" dx="6">${count}</tspan></text>`
84 }
85 const clampX = (x) => Math.max(kw / 2, Math.min(W - kw / 2, x))
86 const kx = clampX(fx)
87 const kFrom = clampX(from)
88
89 const style = `<style>
90${buckets.map((b, i) => `.b${i}{fill:${b.color};fill-opacity:${b.opacity}}`).join('')}
91rect[class]{width:2px;height:2px}
92.t0,.t1,.t2,.t3{animation:tw 2.2s ease-in-out infinite}
93.t1{animation-duration:2.8s;animation-delay:-.7s}.t2{animation-duration:1.9s;animation-delay:-1.3s}.t3{animation-duration:3.3s;animation-delay:-.4s}
94@keyframes tw{0%,100%{opacity:1}50%{opacity:.45}}
95.kt{font:500 12px 'Anthropic Sans',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;fill:#fff}
96.kc{font-weight:400;fill-opacity:.75}
97@media (prefers-reduced-motion:reduce){.t0,.t1,.t2,.t3{animation:none}}
98</style>`
99 const glideFill = glide
100 ? `<animate attributeName="width" from="${from.toFixed(1)}" to="${fx.toFixed(1)}" dur=".45s" ${ease} fill="freeze"/>`
101 : ''
102 const glideKnob = glide
103 ? `<animateTransform attributeName="transform" type="translate" from="${kFrom.toFixed(1)} 0" to="${kx.toFixed(1)} 0" dur=".45s" ${ease} fill="freeze"/>`
104 : ''
105
106 return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${style}
107<defs><clipPath id="pill"><rect width="${W}" height="${H}" rx="${H / 2}"/></clipPath><clipPath id="fill"><rect width="${fx.toFixed(1)}" height="${H}">${glideFill}</rect></clipPath>
108<linearGradient id="base" x1="0" x2="${fx.toFixed(1)}" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="${rgb(acc)}" stop-opacity=".05"/><stop offset="1" stop-color="${rgb(acc)}" stop-opacity=".33"/></linearGradient></defs>
109<g clip-path="url(#pill)"><rect width="${W}" height="${H}" fill="#808080" fill-opacity=".16"/>
110<g clip-path="url(#fill)"><rect width="${fx.toFixed(1)}" height="${H}" fill="url(#base)"/>${px}</g>${marks}</g>
111<g transform="translate(${kx.toFixed(1)} 0)">${glideKnob}${knob}</g></svg>`
112}
113types/index.d.ts 35 lines1// What super-bar keeps in $.state for the length of a session. The host holds
2// these values, so they survive a hot reload; /clear, /resume and /branch reset them.
3
4export type SuperBarStatus = 'pending' | 'in_progress' | 'completed'
5
6export type SuperBarTask = {
7 id: string
8 subject: string
9 activeForm: string | null
10 status: SuperBarStatus
11 /** Tasks created after every earlier task was done start a new batch. */
12 batch: number
13}
14
15export type SuperBarBoard = {
16 tasks: SuperBarTask[]
17 batch: number
18 /** Whether this session's model has the task tools; null until checked. */
19 toolsOn: boolean | null
20}
21
22export type SuperBarView = {
23 /** Hidden with × until Claude creates the next task, or until /taskbar. */
24 hidden: boolean
25}
26
27declare module 'claude-code' {
28 interface PluginState {
29 'super-bar': {
30 board: SuperBarBoard
31 view: SuperBarView
32 }
33 }
34}
35