HUD under the prompt / HUD под полем ввода: model, folder, git, context, 5h and weekly limits with resets, prompt-cache timer

A three-line HUD under the prompt: model, folder, git, context and 5-hour limit bars, weekly limit with reset times, and a prompt-cache timer.
●), ahead (↑) and behind (↓) counts. Click the folder to pick another one in an Explorer-style dialog: Claude Code opens there in a new Windows Terminal window (/open-project does the same). Click the branch to open /changes, the pane of the session's changed files from the changes-pane mod. With the shot mod installed, 📷 shot · window at the end of the line put a screenshot into the prompt without touching what you typed, so several fit in one message.Bars turn yellow past 50% and red past 80%. Refreshes after every turn and every 30 seconds. Sits where a status-line HUD such as claude-hud does, with Claude Code's own hint line kept beneath it.
Reads the session's usage figures (context, rate limits) and model. Every 30 seconds runs git status --porcelain --branch in the session folder (read-only). Only when you click the folder or run /open-project: shows the Windows folder dialog through PowerShell, then starts wt.exe -w new -d <folder> cmd /k claude (or powershell -NoExit -Command claude with shell: powershell; a plain console window without Windows Terminal). Every 30 seconds reads the list of commands, to show the shot buttons when that mod is installed. On a click on the branch: runs /changes when the changes-pane mod is installed, else shows how to install it. Adds the /open-project command.
/config)language: auto (default; follows LANG, else the Windows UI language or regional format), ru or en.position: below (default, under the prompt) or above (over the prompt).shell: cmd (default) or powershell — the shell of the window a click on the folder opens.cacheTtlMinutes: idle minutes until the prompt cache goes cold, 60 by default (a subscription); set 5 when using an API key./plugin install hud --marketplace ArtyaEXE/claude-mods
Part of claude-mods. MIT license.
HUD из трёх строк под полем ввода: модель, папка, git, полоски контекста и лимита 5ч, недельный лимит со временем сброса и таймер кэша.
●), опережением (↑) и отставанием (↓). Клик по папке открывает выбор папки в стиле Проводника, и Claude Code запускается в ней в новом окне Windows Terminal (/open-project делает то же самое). Клик по ветке открывает /changes, панель изменённых за сессию файлов из мода changes-pane. Если установлен мод shot, кнопки 📷 скрин · окно в конце строки вставляют скриншот в промпт, не трогая набранное, так что в одно сообщение помещается несколько снимков.Полоски желтеют после 50% и краснеют после 80%. Обновляется после каждого хода и раз в 30 секунд. Стоит там же, где HUD в статусной строке вроде claude-hud; строка подсказок Claude Code остаётся под ним.
Читает показатели сессии (контекст, лимиты) и модель. Раз в 30 секунд выполняет git status --porcelain --branch в папке сессии (только чтение). Только по клику на папку или /open-project: показывает диалог выбора папки Windows через PowerShell, затем запускает wt.exe -w new -d <папка> cmd /k claude (или powershell -NoExit -Command claude при shell: powershell; без Windows Terminal — обычное окно консоли). Раз в 30 секунд читает список команд, чтобы показать кнопки скриншотов, если установлен мод shot. По клику на ветку: выполняет /changes, если установлен мод changes-pane, иначе подсказывает, как его поставить. Добавляет команду /open-project.
/config)language: auto (по умолчанию; по LANG, иначе по языку интерфейса или региональному формату Windows), ru или en.position: below (по умолчанию, под полем ввода) или above (над полем).shell: cmd (по умолчанию) или powershell — оболочка окна, которое открывает клик по папке.cacheTtlMinutes: через сколько минут простоя кэш остывает, по умолчанию 60 (подписка); с API-ключом поставь 5./plugin install hud --marketplace ArtyaEXE/claude-mods
Часть claude-mods. Лицензия MIT.
hooks/register.tsx 387 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderElement, RenderInput } from 'claude-code'
3
4import type { HudLimit, HudSnap } from '../types'
5import { launchCommands, pickerCommand } from './open-project'
6import type { Shell } from './open-project'
7
8const TICK_MS = 30_000
9
10const snap = atom({ plugin: 'hud', key: 'snap' } as const, null as HudSnap | null, { shape: 'snap-2' })
11const lastTurnAt = atom({ plugin: 'hud', key: 'lastTurnAt' } as const, null as number | null)
12const now = atom({ plugin: 'hud', key: 'now' } as const, 0)
13
14let cacheTtlMs = 60 * 60_000
15
16// `claude-opus-5-5` -> `Opus 5.5`; anything else is shown as given.
17const prettyModel = (id: string) => {
18 const m = /claude-([a-z]+)-(\d+)(?:-(\d+))?/i.exec(id)
19 if (m === null) return id
20 const name = (m[1] ?? '').charAt(0).toUpperCase() + (m[1] ?? '').slice(1)
21
22 return m[3] === undefined || m[3].length > 2 ? `${name} ${m[2]}` : `${name} ${m[2]}.${m[3]}`
23}
24
25const kTokens = (n: number) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`)
26
27// The same bar claude-hud draws: █/░, 10 cells from 100 columns, 6 from 60, else 4.
28const barWidth = (columns: number) => (columns >= 100 ? 10 : columns >= 60 ? 6 : 4)
29
30const bar = (percent: number, cells: number) => {
31 const full = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
32
33 return '█'.repeat(full) + '░'.repeat(cells - full)
34}
35
36const levelColor = (percent: number) => (percent >= 80 ? 'red' : percent >= 50 ? 'yellow' : 'green')
37
38type Lang = 'ru' | 'en'
39
40const STRINGS = {
41 ru: {
42 context: 'Контекст',
43 cacheNone: 'кэш —',
44 cacheWorking: 'кэш ● ход идёт',
45 cacheCold: 'кэш остыл',
46 cacheLeft: (m: number) => `кэш ${m} мин`,
47 pickTitle: 'Открыть Claude Code в папке',
48 openCommand: 'Выбрать папку и открыть в ней Claude Code в новом окне',
49 opened: (dir: string) => `Открыл Claude Code в ${dir}`,
50 openFailed: 'Не получилось открыть новое окно терминала',
51 cancelled: 'Папка не выбрана',
52 shotRegion: 'скрин',
53 shotWindow: 'окно',
54 noChanges: 'hud: клик по ветке открывает /changes из мода changes-pane: /plugin install changes-pane --marketplace ArtyaEXE/claude-mods',
55 weekdays: ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'],
56 resetIn: (h: number, m: number, hm: string) => `через ${h > 0 ? `${h}ч ${m}м` : `${m}м`} · ${hm}`,
57 limits: { five_hour: 'Лимит 5ч', seven_day: 'Неделя', spend_limit: 'Бюджет' } as Record<string, string>,
58 },
59 en: {
60 context: 'Context',
61 cacheNone: 'cache —',
62 cacheWorking: 'cache ● turn running',
63 cacheCold: 'cache cold',
64 cacheLeft: (m: number) => `cache ${m}m`,
65 pickTitle: 'Open Claude Code in a folder',
66 openCommand: 'Pick a folder and open Claude Code there in a new window',
67 opened: (dir: string) => `Opened Claude Code in ${dir}`,
68 openFailed: 'Could not open a new terminal window',
69 cancelled: 'No folder picked',
70 shotRegion: 'shot',
71 shotWindow: 'window',
72 noChanges: 'hud: a click on the branch opens /changes from the changes-pane mod: /plugin install changes-pane --marketplace ArtyaEXE/claude-mods',
73 weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
74 resetIn: (h: number, m: number, hm: string) => `in ${h > 0 ? `${h}h ${m}m` : `${m}m`} · ${hm}`,
75 limits: { five_hour: 'Limit 5h', seven_day: 'Week', spend_limit: 'Budget' } as Record<string, string>,
76 },
77}
78
79let lang: Lang = 'en'
80
81// `auto`: the locale variables first, then Windows' UI language or regional format.
82async function resolveLang($: EngineInterface, setting: unknown): Promise<Lang> {
83 if (setting === 'ru' || setting === 'en') return setting
84 const env = (await $.env.get('LC_ALL')) || (await $.env.get('LC_MESSAGES')) || (await $.env.get('LANG'))
85 if (env) return env.toLowerCase().startsWith('ru') ? 'ru' : 'en'
86 try {
87 const ran = await $.process.run(
88 ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', '(Get-UICulture).Name; (Get-Culture).Name'],
89 { timeoutMs: 10_000 },
90 )
91 if (ran.exitCode === 0 && /^ru\b/im.test(ran.stdout)) return 'ru'
92 } catch {
93 // Not Windows: English.
94 }
95
96 return 'en'
97}
98
99// `через 1ч 12м · 20:59` / `in 1h 12m · 20:59` within a day, `пт 14:00` / `Fri 14:00` past it.
100const resetLabel = (iso: string | undefined, t: number) => {
101 if (iso === undefined) return ''
102 const at = new Date(iso)
103 if (Number.isNaN(at.getTime())) return ''
104 const L = STRINGS[lang]
105 const hm = `${String(at.getHours()).padStart(2, '0')}:${String(at.getMinutes()).padStart(2, '0')}`
106 const left = at.getTime() - t
107 if (left >= 24 * 3_600_000) return `${L.weekdays[at.getDay()]} ${hm}`
108 const minutes = Math.max(0, Math.ceil(left / 60_000))
109
110 return L.resetIn(Math.floor(minutes / 60), minutes % 60, hm)
111}
112
113async function readGit($: EngineInterface, cwd: string) {
114 try {
115 const ran = await $.process.run(['git', 'status', '--porcelain=v1', '--branch'], { cwd, timeoutMs: 5_000 })
116 if (ran.exitCode !== 0) return { branch: null, dirty: 0, ahead: 0, behind: 0 }
117 const [head = '', ...rest] = ran.stdout.split('\n')
118 const branch = /^## (?:No commits yet on )?([^.\s]+)/.exec(head)?.[1] ?? 'HEAD'
119
120 return {
121 branch,
122 dirty: rest.filter(line => line.trim() !== '').length,
123 ahead: Number(/ahead (\d+)/.exec(head)?.[1] ?? 0),
124 behind: Number(/behind (\d+)/.exec(head)?.[1] ?? 0),
125 }
126 } catch {
127 return { branch: null, dirty: 0, ahead: 0, behind: 0 }
128 }
129}
130
131// Whether another mod's command is installed; false when the list cannot be read.
132async function hasCommand($: EngineInterface, name: string): Promise<boolean> {
133 try {
134 return (await $.command.list()).some(c => c.name === name)
135 } catch {
136 return false
137 }
138}
139
140async function refresh($: EngineInterface) {
141 const [usage, model, cwd] = await Promise.all([$.session.usage(), $.session.model(), $.session.cwd()])
142 const git = await readGit($, cwd)
143 const hasShot = await hasCommand($, 'shot')
144 const limits: HudLimit[] = usage.rateLimits.map(l => ({ kind: l.kind, percent: l.percentUsed, resetsAt: l.resetsAt }))
145 const next: HudSnap = {
146 model: prettyModel(model),
147 cwd,
148 ...git,
149 ctxTokens: usage.context.tokens,
150 ctxWindow: usage.context.window,
151 ctxPercent: usage.context.percent,
152 costUsd: usage.cost?.usd,
153 limits,
154 hasShot,
155 }
156 await update($, snap, () => next)
157 await update($, now, () => Date.now())
158}
159
160async function safeRefresh($: EngineInterface) {
161 try {
162 await refresh($)
163 } catch {
164 // A failed read keeps the last figures on screen.
165 }
166}
167
168// The folder chosen in an Explorer-style dialog, or null when cancelled.
169async function pickFolder($: EngineInterface, start: string, title: string): Promise<string | null> {
170 const ran = await $.process.run(pickerCommand(start, title), { timeoutMs: 600_000 })
171 const chosen = ran.stdout.trim()
172
173 return ran.exitCode === 0 && chosen !== '' ? chosen : null
174}
175
176// A new window running `claude` in the folder: whichever launcher works first.
177async function launchClaude($: EngineInterface, folder: string, shell: Shell): Promise<boolean> {
178 for (const argv of launchCommands(folder, shell)) {
179 try {
180 const ran = await $.process.run(argv, { timeoutMs: 15_000 })
181 if (ran.exitCode === 0) return true
182 } catch {
183 // Not there: try the next.
184 }
185 }
186
187 return false
188}
189
190// Pick a folder in an Explorer-style dialog and start Claude Code there in a new window.
191async function openProject($: EngineInterface): Promise<string> {
192 const L = STRINGS[lang]
193 const folder = await pickFolder($, await $.session.cwd(), L.pickTitle)
194 if (folder === null) return L.cancelled
195 const text = (await launchClaude($, folder, shell)) ? L.opened(folder) : L.openFailed
196 $.ui.toast(text)
197
198 return text
199}
200
201// The changes-pane mod's /changes when it is installed; else how to get it.
202async function openChanges($: EngineInterface) {
203 if ((await $.command.list()).some(c => c.name === 'changes')) {
204 await $.command.run({ command: 'changes' })
205
206 return
207 }
208 $.ui.toast(STRINGS[lang].noChanges)
209}
210
211// The HUD's three rows, then `below`: whatever the site drew beneath it.
212async function drawHud($: EngineInterface, e: RenderInput, s: HudSnap, columns: number, isWorking: boolean, below: RenderElement) {
213 const { Box, Button, Text } = $.ui.resolve(e)
214 const L = STRINGS[lang]
215 const t = (await read($, now)) || Date.now()
216 const last = await read($, lastTurnAt)
217 const sep = <Text dimColor> │ </Text>
218
219 // The branch is a button (opening /changes); its counts stay coloured text beside it.
220 const gitMarks =
221 s.branch === null ? null : (
222 <Text>
223 {s.dirty > 0 && <Text color="yellow"> ●{s.dirty}</Text>}
224 {s.ahead > 0 && <Text color="cyan"> ↑{s.ahead}</Text>}
225 {s.behind > 0 && <Text color="cyan"> ↓{s.behind}</Text>}
226 </Text>
227 )
228
229 const ctxPercent = s.ctxPercent ?? (s.ctxTokens !== undefined && s.ctxWindow > 0 ? Math.round((s.ctxTokens / s.ctxWindow) * 100) : undefined)
230
231 let cache = <Text dimColor>{L.cacheNone}</Text>
232 if (isWorking) {
233 cache = <Text color="green">{L.cacheWorking}</Text>
234 } else if (last !== null) {
235 const left = cacheTtlMs - (t - last)
236 const minutes = Math.ceil(left / 60_000)
237 cache =
238 left <= 0 ? (
239 <Text color="red">{L.cacheCold}</Text>
240 ) : (
241 <Text color={minutes <= 3 ? 'red' : minutes <= 10 ? 'yellow' : 'green'}>{L.cacheLeft(minutes)}</Text>
242 )
243 }
244
245 const cells = barWidth(columns)
246 const order = (kind: string) => (kind === 'five_hour' ? 0 : kind === 'seven_day' ? 1 : 2)
247 const limits = [...s.limits].sort((a, b) => order(a.kind) - order(b.kind))
248 // The 5-hour bar shares the context bar's row, so the two bars never stack into one block.
249 const fiveHour = limits.find(l => l.kind === 'five_hour')
250 const others = limits.filter(l => l.kind !== 'five_hour')
251 const limitView = (l: HudLimit) => (
252 <Text>
253 <Text dimColor>{L.limits[l.kind] ?? l.kind} </Text>
254 {l.kind === 'five_hour' && (
255 <Text>
256 <Text color={levelColor(l.percent)}>{bar(l.percent, cells)}</Text>{' '}
257 </Text>
258 )}
259 <Text color={levelColor(l.percent)}>{Math.round(l.percent)}%</Text>
260 {l.resetsAt !== undefined && (
261 <Text>
262 {' '}
263 <Text color="cyan">↻ </Text>
264 <Text bold>{resetLabel(l.resetsAt, t)}</Text>
265 </Text>
266 )}
267 </Text>
268 )
269
270 const room = Math.max(10, columns - 40)
271 const cwd = s.cwd.length > room ? `…${s.cwd.slice(-(room - 1))}` : s.cwd
272
273 return (
274 <Box flexDirection="column">
275 <Box flexDirection="row">
276 <Text>
277 <Text bold color="cyan">{s.model}</Text>
278 {sep}
279 </Text>
280 {/* A click opens a folder picker, then Claude Code in a new window there. */}
281 <Button key="open-project" plain onPress={() => void openProject($)}>
282 {cwd}
283 </Button>
284 {s.branch !== null && sep}
285 {s.branch !== null && (
286 <Button key="changes" plain onPress={() => void openChanges($)}>
287 {`⎇ ${s.branch}`}
288 </Button>
289 )}
290 <Text wrap="truncate">{gitMarks}</Text>
291 {/* The shot mod's /shot as buttons: the draft stays, each picture lands at the cursor, so several go in one prompt. */}
292 {s.hasShot === true && (
293 <Box flexDirection="row">
294 {sep}
295 <Text>📷 </Text>
296 <Button key="shot" plain label={L.shotRegion} onPress={() => void $.command.run({ command: 'shot' })} />
297 <Text dimColor> · </Text>
298 <Button key="shot-window" plain label={L.shotWindow} onPress={() => void $.command.run({ command: 'shot', args: 'window' })} />
299 </Box>
300 )}
301 </Box>
302 <Text wrap="truncate">
303 <Text dimColor>{L.context} </Text>
304 {ctxPercent === undefined ? (
305 <Text dimColor>—</Text>
306 ) : (
307 <Text>
308 <Text color={levelColor(ctxPercent)}>{bar(ctxPercent, cells)}</Text>{' '}
309 <Text color={levelColor(ctxPercent)}>{ctxPercent}%</Text>
310 <Text dimColor>
311 {' '}
312 ({kTokens(s.ctxTokens ?? 0)}/{kTokens(s.ctxWindow)})
313 </Text>
314 </Text>
315 )}
316 {fiveHour !== undefined && sep}
317 {fiveHour !== undefined && limitView(fiveHour)}
318 </Text>
319 <Text wrap="truncate">
320 {others.map(l => (
321 <Text>
322 {limitView(l)}
323 {sep}
324 </Text>
325 ))}
326 {cache}
327 </Text>
328 {below}
329 </Box>
330 )
331}
332
333let position: 'above' | 'below' = 'below'
334let shell: Shell = 'cmd'
335
336export const register: Register = (on, options) => {
337 const ttl = Number((options as { cacheTtlMinutes?: unknown }).cacheTtlMinutes ?? 60)
338 cacheTtlMs = (Number.isFinite(ttl) && ttl > 0 ? ttl : 60) * 60_000
339 position = (options as { position?: unknown }).position === 'above' ? 'above' : 'below'
340 shell = (options as { shell?: unknown }).shell === 'powershell' ? 'powershell' : 'cmd'
341
342 on('session.start', async ($, e, next) => {
343 const started = await next(e)
344 lang = await resolveLang($, (options as { language?: unknown }).language)
345 await $.command.register({ name: 'open-project', description: STRINGS[lang].openCommand })
346 void safeRefresh($)
347 $.clock.every(TICK_MS, () => void safeRefresh($))
348
349 return started
350 })
351
352 on('command.run', { command: 'open-project' }, async $ => ({ text: await openProject($) }))
353
354 on('turn.complete', async ($, e, next) => {
355 const done = await next(e)
356 if (e.agentId === undefined) {
357 await update($, lastTurnAt, () => Date.now())
358 void safeRefresh($)
359 }
360
361 return done
362 })
363
364 on('session.end', async ($, e, next) => {
365 if (e.reason === 'clear') await update($, lastTurnAt, () => null)
366
367 return next(e)
368 })
369
370 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
371 const below = await next(e)
372 const s = await read($, snap)
373 if (position !== 'above' || s === null || e.props.hasSurvey || e.props.view.agentId !== undefined) return below
374
375 return drawHud($, e, s, e.props.bodyColumns, e.props.isWorking, below)
376 })
377
378 // Under the prompt, where a status-line HUD sits: the HUD, then the engine's own hint line.
379 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
380 const below = await next(e)
381 const s = await read($, snap)
382 if (position !== 'below' || s === null) return below
383
384 return drawHud($, e, s, Math.max(20, (e.viewport?.columns ?? 120) - 4), e.props.isWorking, below)
385 })
386}
387hooks/open-project.ts 81 lines1// The Explorer-style folder picker (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
2// window in front (the terminal that was clicked), so it opens on top of it. Prints the
3// chosen folder as UTF-8, or nothing when cancelled. `__START__` is the folder it opens in.
4const PICKER = String.raw`
5[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
6Add-Type -TypeDefinition @'
7using System;
8using System.Runtime.InteropServices;
9public static class FolderPicker {
10 [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] class FileOpenDialog {}
11 [ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
12 interface IFileDialog {
13 [PreserveSig] int Show(IntPtr parent);
14 void SetFileTypes(uint c, IntPtr f); void SetFileTypeIndex(uint i); void GetFileTypeIndex(out uint i);
15 void Advise(IntPtr p, out uint c); void Unadvise(uint c);
16 void SetOptions(uint fos); void GetOptions(out uint fos);
17 void SetDefaultFolder(IShellItem si); void SetFolder(IShellItem si);
18 void GetFolder(out IShellItem si); void GetCurrentSelection(out IShellItem si);
19 void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string n); void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string n);
20 void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string t); void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string t);
21 void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string t);
22 void GetResult(out IShellItem si);
23 }
24 [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
25 interface IShellItem {
26 void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); void GetParent(out IShellItem si);
27 void GetDisplayName(uint sigdn, [MarshalAs(UnmanagedType.LPWStr)] out string name);
28 }
29 [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
30 static extern void SHCreateItemFromParsingName(string path, IntPtr pbc, ref Guid riid, out IShellItem si);
31 [DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
32 public static string Pick(string start, string title) {
33 IFileDialog dialog = (IFileDialog)new FileOpenDialog();
34 dialog.SetOptions(0x20 | 0x40);
35 dialog.SetTitle(title);
36 try {
37 Guid shellItem = typeof(IShellItem).GUID;
38 IShellItem folder;
39 SHCreateItemFromParsingName(start, IntPtr.Zero, ref shellItem, out folder);
40 dialog.SetFolder(folder);
41 } catch { }
42 if (dialog.Show(GetForegroundWindow()) != 0) return "";
43 IShellItem result; dialog.GetResult(out result);
44 string path; result.GetDisplayName(0x80058000, out path);
45 return path;
46 }
47}
48'@
49[FolderPicker]::Pick('__START__', '__TITLE__')
50`
51
52// A PowerShell single-quoted string holds anything but a single quote, which it doubles.
53const psQuote = (s: string) => s.replace(/'/g, "''")
54
55// The PowerShell line that shows the picker opened on `start`.
56export function pickerCommand(start: string, title: string): string[] {
57 const script = PICKER.replace('__START__', psQuote(start.replace(/\//g, '\\'))).replace('__TITLE__', psQuote(title))
58
59 return ['powershell.exe', '-NoProfile', '-NonInteractive', '-STA', '-Command', script]
60}
61
62// A new Windows Terminal window running `claude` in the folder; a plain PowerShell window without it.
63export type Shell = 'cmd' | 'powershell'
64
65// The shell the new window runs `claude` in, kept open after it exits.
66const SHELLS: Record<Shell, string[]> = {
67 cmd: ['cmd.exe', '/k', 'claude'],
68 powershell: ['powershell.exe', '-NoExit', '-Command', 'claude'],
69}
70
71// Windows Terminal first, then a plain `start` window when there is none.
72export function launchCommands(folder: string, shell: Shell): string[][] {
73 // A trailing backslash would escape wt's closing quote: `C:\` becomes `C:\.`.
74 const dir = folder.endsWith('\\') ?`${folder}.` : folder
75
76 return [
77 ['wt.exe', '-w', 'new', '-d', dir, ...SHELLS[shell]],
78 ['cmd.exe', '/c', 'start', '""', '/D', dir, ...SHELLS[shell]],
79 ]
80}
81types/index.d.ts 29 lines1export type HudLimit = { kind: string; percent: number; resetsAt?: string }
2
3export type HudSnap = {
4 model: string
5 cwd: string
6 branch: string | null
7 dirty: number
8 ahead: number
9 behind: number
10 ctxTokens?: number
11 ctxWindow: number
12 ctxPercent?: number
13 costUsd?: number
14 limits: HudLimit[]
15 // The shot mod's /shot is there: the HUD shows its buttons.
16 hasShot?: boolean
17}
18
19declare module 'claude-code' {
20 interface PluginState {
21 hud: {
22 snap: Shaped<HudSnap | null>
23 // When the main loop's last turn ended: the cache's clock starts there.
24 lastTurnAt: number | null
25 now: number
26 }
27 }
28}
29