Mantém um servidor de prévia no ar para o HTML editado e mostra a URL acima do prompt.

Cinco mods para o Claude Code, escritos como plugins de function hooks. Desenham barras e faixas acima do prompt, avisam sobre cache e uso do plano, sobem um servidor de prévia para HTML e lembram o modelo de usar o seu design system.
Funcionam no terminal e na aba Code do app desktop. Textos e comandos estão em português.

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 no bloco env do ~/.claude/settings.json. Sem ela nenhum dos mods carrega.{
"env": {
"CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
}
}
Se o arquivo já tem um bloco env, acrescente só a linha da variável.
Num terminal com claude aberto, instale cada mod que quiser:
/plugin install progresso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install barra-uso --marketplace Kelvi-Maycon/claude-code-mods
/plugin install cache-frio --marketplace Kelvi-Maycon/claude-code-mods
/plugin install previa --marketplace Kelvi-Maycon/claude-code-mods
/plugin install ds-primeiro --marketplace Kelvi-Maycon/claude-code-mods
Na primeira vez o Claude Code pergunta se quer adicionar o marketplace: responda y. Depois escolha o escopo do usuário (o primeiro da lista). O mod já roda na sessão atual e nas próximas.
Instalado no escopo do usuário pelo terminal, o mod carrega também na aba Code do app desktop. O comando /plugin install só existe no terminal.

Barras de progresso acima do prompt. O modelo recebe uma ferramenta (progresso) e uma regra curta no system prompt: em tarefa com vários passos, cria uma barra com o plano e a atualiza conforme avança. Cada barra mostra o passo ativo, a etapa, o percentual e o estado (andamento, esperando você, falhou, concluída).
Comandos:
/progresso: mostra ou esconde as barras. Aceita on, off ou painel (abre o painel lateral)./progresso-demo: demonstração de 30 segundos com todos os estados, faixas, painel e sons./progresso-limpar: remove todas as barras e faixas.Ícones por tipo de agente: executor-leve, executor, executor-pesado, executor-design, investigador e leitor têm robôs próprios. Explore usa a lupa do investigador. general-purpose, Plan, claude, fork e qualquer outro nome usam o robô padrão, com o nome do tipo em cinza. Modelo e esforço vêm do frontmatter (name, model, effort) dos arquivos em ~/.claude/agents/.
Um passo cujo título termina com o tipo do agente entre parênteses, como Revisar tudo (executor-pesado), aparece no painel como agente planejado.
![]()
Uma linha de pílulas acima do campo:
5H e 7D: uso do plano nas janelas de 5 horas e 7 dias, com o tempo até renovar.CTX: contexto usado até a compactação automática, pelo limite que o Claude Code informa (275k quando ele não informa nenhum).CACHE: tempo restante do cache de prompt (TTL de 60 minutos, fixo) e taxa de acerto.DS on/off: só aparece com o ds-primeiro configurado. Clicar alterna o DS nesta sessão.Comando: /uso, com on ou off.
Quando a sessão ficou parada além do TTL do cache e o contexto passa de 100k tokens, o próximo envio regrava o cache inteiro e custa mais. O mod segura esse envio uma vez, devolve o texto ao campo e avisa. Enviar de novo dentro de 3 minutos passa direto. Comandos de barra (/compact, /clear) nunca são segurados.
Comando: /cache-frio mostra contexto e tempo de cache. /cache-frio ttl <minutos> ajusta o TTL (padrão 60), /cache-frio off e /cache-frio on desligam e ligam o aviso.
Quando o modelo grava ou edita um .html, o mod sobe um python3 -m http.server na pasta do arquivo, a partir da porta 8765, e mostra a URL acima do prompt com botões para abrir, copiar e parar. O modelo recebe uma linha dizendo que o servidor já está no ar, para não abrir outro. Comandos Bash que matariam o servidor (kill, pkill, killall no processo dele) são negados.
Arquivos em /tmp e /var/folders não viram prévia.
Comando: /previa <caminho> serve uma pasta, /previa status lista os servidores, /previa parar encerra.
Requisitos: python3 e lsof no PATH. O botão Abrir usa o comando open do macOS; em Linux ele não faz nada e o botão de copiar continua funcionando.
No primeiro pedido de peça visual da sessão (página, landing, dashboard, slide, tela, componente...), anexa ao contexto uma linha pedindo que o modelo carregue a skill do seu design system antes de criar. Antes do envio aparece uma faixa avisando, com o botão "Tirar deste envio". A detecção é por verbos e alvos em português.
Não anexa quando o pedido já cita um design system, uma identidade visual, a sua skill, o rótulo ou um dos termos extras. Depois de compactar ou de /clear, volta a anexar no próximo pedido visual.
Sem configuração o mod não faz nada: não anexa, não mostra faixa e não registra comando. Para ligar, configure as opções do plugin. A tela de opções aparece na instalação, e depois fica em /config:
| Opção | O que é | Exemplo |
|---|---|---|
skill | Nome da skill do design system. Vazio desliga o mod. | meu-design-system |
rotulo | Nome exibido na faixa, no toast e na linha anexada. Vazio usa o nome da skill. | Minha Marca |
ignorar | Termos extras, separados por vírgula, que dispensam a linha quando citados. | marca do cliente, sem ds |
As opções ficam salvas no ~/.claude/settings.json, em pluginConfigs.
Comando: /ds mostra o estado. /ds on e /ds off valem para a sessão; /ds sempre on e /ds sempre off mudam o padrão.
claude plugin update <mod>@claude-code-mods
Depois, numa sessão aberta, rode /reload-plugins.
claude plugin uninstall <mod>@claude-code-mods
Cada plugin fica em plugins/<nome>/, com os hooks em hooks/ e os testes em tests/.
claude plugin validate plugins/progresso
claude plugin test plugins/progresso
O tsconfig.json de cada plugin estende .claude-plugin/types/tsconfig.json, que o Claude Code gera no primeiro carregamento do plugin. Num clone novo o editor e o tsc acusam tipos ausentes até esse primeiro carregamento (ou um claude plugin validate). A pasta gerada fica fora do git.
Os sons do progresso são gerados por plugins/progresso/sounds/gerar.py.
A estrutura das barras do progresso foi portada do plan-progress (zycck/claude-mods), de Kirill Serditov, sob licença MIT. O texto da licença está em plugins/progresso/licenses/plan-progress-LICENSE.
MIT. Veja LICENSE.
hooks/register.tsx 550 lines1import { atom, read, update } from 'claude-code'
2import type {
3 EngineInterface,
4 HookStream,
5 ProcessSpawnChunk,
6 ProcessSpawnResult,
7 Register,
8 Timer,
9} from 'claude-code'
10
11import type { Servidor } from '../types'
12
13type Filho = HookStream<ProcessSpawnChunk, ProcessSpawnResult>
14
15const servidores = atom({ plugin: 'previa', key: 'servidores' } as const, [])
16
17const PRIMEIRA_PORTA = 8765
18const MAXIMO = 3
19const FIM: ProcessSpawnResult = { code: null, signal: null }
20
21// Processos que este carregamento do mod abriu, por raiz. Um reload zera o mapa
22// e a engine mata os filhos; o session.start seguinte os levanta de novo.
23const vivos = new Map<string, Filho>()
24const falhas = new Map<string, { vezes: number; erro: string }>()
25const caidos = new Set<string>()
26let fila: Promise<void> = Promise.resolve()
27let gravado = '[]'
28let ultimoHtml: string | undefined
29let batida: Timer | undefined
30let avisado = ''
31let restaurar = false
32
33const pai = (caminho: string) =>
34 caminho.slice(0, caminho.lastIndexOf('/')) || '/'
35
36const nome = (caminho: string) =>
37 caminho.slice(caminho.lastIndexOf('/') + 1) || '/'
38
39const urlDe = (s: Servidor) =>
40 `http://localhost:${s.porta}/${s.arquivo.split('/').map(encodeURIComponent).join('/')}`
41
42// argv em lista: a raiz pode ter espaço e nunca passa por shell.
43const argvDe = (s: Servidor) => [
44 'python3',
45 '-m',
46 'http.server',
47 String(s.porta),
48 '--bind',
49 '127.0.0.1',
50 '--directory',
51 s.raiz,
52]
53
54const comLimite = <T,>(
55 mapa: Record<string, T>,
56 chave: string,
57 valor: T,
58 limite: number,
59) => {
60 const outras = Object.entries(mapa).filter(([k]) => k !== chave)
61
62 return Object.fromEntries([...outras, [chave, valor] as const].slice(-limite))
63}
64
65const pidNa = async ($: EngineInterface, porta: number) => {
66 const { stdout } = await $.process.run([
67 'lsof',
68 '-nP',
69 `-iTCP:${porta}`,
70 '-sTCP:LISTEN',
71 '-t',
72 ])
73
74 return stdout.trim().split('\n')[0] ?? ''
75}
76
77// Quem escuta na porta do servidor, e se é um http.server idêntico ao nosso
78// (outra sessão do usuário servindo a mesma raiz, ou um órfão de sessão anterior).
79const ocupante = async ($: EngineInterface, s: Servidor) => {
80 const pid = await pidNa($, s.porta)
81
82 if (pid === '') {
83 return { pid, igual: false }
84 }
85
86 const { stdout } = await $.process.run(['ps', '-ww', '-p', pid, '-o', 'command='])
87
88 return { pid, igual: stdout.trim().endsWith(argvDe(s).slice(1).join(' ')) }
89}
90
91const portaLivre = async ($: EngineInterface, portas: Record<string, number>) => {
92 const reservadas = new Set(Object.values(portas))
93
94 for (let porta = PRIMEIRA_PORTA; porta < PRIMEIRA_PORTA + 100; porta++) {
95 if (!reservadas.has(porta) && (await pidNa($, porta)) === '') {
96 return porta
97 }
98 }
99
100 return 0
101}
102
103const subir = ($: EngineInterface, s: Servidor) => {
104 const filho = $.process.spawn({ argv: argvDe(s) })
105 vivos.set(s.raiz, filho)
106
107 void (async () => {
108 const inicio = await $.clock.now()
109 let erro = ''
110
111 try {
112 for await (const { stream, text } of filho) {
113 if (stream === 'stderr') {
114 erro = text.trim().split('\n').pop() ?? ''
115 }
116 }
117 } catch (falha) {
118 erro = String(falha)
119 }
120
121 if (vivos.get(s.raiz) !== filho) {
122 return
123 }
124
125 vivos.delete(s.raiz)
126
127 if ((await $.clock.now()) - inicio < 5000) {
128 const vezes = (falhas.get(s.raiz)?.vezes ?? 0) + 1
129 falhas.set(s.raiz, { vezes, erro: erro.slice(0, 80) })
130 } else {
131 falhas.delete(s.raiz)
132 caidos.add(s.raiz)
133 }
134 })()
135}
136
137// Deixa um servidor da lista no ar. Devolve o motivo quando ele deve sair dela.
138const levantar = async ($: EngineInterface, s: Servidor) => {
139 const falha = falhas.get(s.raiz)
140
141 if (falha !== undefined && falha.vezes >= 3) {
142 falhas.delete(s.raiz)
143
144 return falha.erro || 'o servidor não subiu'
145 }
146
147 const pasta = await $.fs.stat(s.raiz).catch(() => undefined)
148
149 if (pasta?.kind !== 'dir') {
150 return 'a pasta não existe mais'
151 }
152
153 const portas = ((await $.store.get('portas')) ?? {}) as Record<string, number>
154 const preferida = s.porta || portas[s.raiz] || 0
155 const dono =
156 preferida > 0 ? await ocupante($, { ...s, porta: preferida }) : undefined
157 const serve = dono !== undefined && (dono.igual || dono.pid === '')
158 const porta = serve ? preferida : await portaLivre($, portas)
159
160 if (porta === 0) {
161 return `sem porta livre a partir de ${PRIMEIRA_PORTA}`
162 }
163
164 const pronto = { ...s, porta }
165
166 if (porta !== s.porta) {
167 await update($, servidores, lista =>
168 lista.map(um => (um.raiz === s.raiz ? { ...um, porta } : um)),
169 )
170 }
171
172 if (portas[s.raiz] !== porta) {
173 await $.store.set('portas', comLimite(portas, s.raiz, porta, 60))
174 }
175
176 // Porta já atendida por um http.server idêntico: usa esse, sem duplicar.
177 if (dono?.igual !== true) {
178 subir($, pronto)
179 }
180
181 if (caidos.delete(s.raiz)) {
182 $.ui.toast(`Prévia de ${nome(s.raiz)} caiu e voltou em ${urlDe(pronto)}`)
183 }
184
185 return undefined
186}
187
188const reconciliar = async ($: EngineInterface) => {
189 const lista = await read($, servidores)
190
191 for (const [raiz, filho] of vivos) {
192 if (!lista.some(s => s.raiz === raiz)) {
193 vivos.delete(raiz)
194 void filho.return(FIM)
195 }
196 }
197
198 for (const s of lista) {
199 if (vivos.has(s.raiz)) {
200 continue
201 }
202
203 const motivo = await levantar($, s)
204
205 if (motivo !== undefined) {
206 await update($, servidores, atual => atual.filter(um => um.raiz !== s.raiz))
207 $.ui.toast(`Prévia de ${nome(s.raiz)} saiu: ${motivo}`)
208 }
209 }
210
211 const final = await read($, servidores)
212 const texto = JSON.stringify(final)
213
214 if (texto !== gravado) {
215 const sessoes = ((await $.store.get('sessoes')) ?? {}) as Record<string, Servidor[]>
216 await $.store.set(
217 'sessoes',
218 comLimite(sessoes, await $.session.id(), final, 20),
219 )
220 gravado = texto
221 }
222}
223
224const rodar = ($: EngineInterface) => {
225 fila = fila
226 .then(() => reconciliar($))
227 .catch(erro => $.ui.log(`previa: ${String(erro)}`, { to: 'debug' }))
228
229 return fila
230}
231
232// O spawn sai de um timer, fora do dispatch do hook que pediu: assim o processo
233// não fica preso ao sinal de um tool.call ou de um comando.
234const acertar = ($: EngineInterface) =>
235 new Promise<void>(feito => {
236 $.clock.after(0, () => void rodar($).then(feito))
237 })
238
239// Raiz a servir: o diretório mais próximo com index.html, sem passar do
240// diretório da sessão; fora dele, sem subir até a pasta do usuário.
241const raizDe = async ($: EngineInterface, arquivo: string) => {
242 const sessao = await $.fs
243 .stat(await $.session.cwd(), { resolve: true })
244 .catch(() => undefined)
245 const cwd = sessao?.realPath
246 const dentro = cwd !== undefined && arquivo.startsWith(`${cwd}/`)
247
248 for (
249 let dir = pai(arquivo);
250 dentro ? dir.length >= cwd.length : dir.split('/').length > 4;
251 dir = pai(dir)
252 ) {
253 if (await $.fs.exists(`${dir}/index.html`)) {
254 return dir
255 }
256 }
257
258 return pai(arquivo)
259}
260
261// Põe a pasta (ou a raiz do arquivo) na frente da lista. Devolve a raiz, ou
262// undefined quando o caminho não existe.
263const servir = async ($: EngineInterface, caminho: string) => {
264 const alvo = await $.fs.stat(caminho, { resolve: true }).catch(() => undefined)
265 const real = alvo?.realPath
266
267 if (alvo === undefined || real === undefined || alvo.kind === 'other') {
268 return undefined
269 }
270
271 const raiz = alvo.kind === 'dir' ? real : await raizDe($, real)
272 const arquivo = alvo.kind === 'dir' ? '' : real.slice(raiz.length + 1)
273 const [primeiro] = await read($, servidores)
274 const mudou = primeiro?.raiz !== raiz || (arquivo !== '' && primeiro.arquivo !== arquivo)
275
276 if (mudou) {
277 await update($, servidores, lista => {
278 const antigo = lista.find(s => s.raiz === raiz)
279 const novo = {
280 raiz,
281 porta: antigo?.porta ?? 0,
282 arquivo: arquivo || antigo?.arquivo || '',
283 }
284
285 return [novo, ...lista.filter(s => s !== antigo)].slice(0, MAXIMO)
286 })
287 }
288
289 return raiz
290}
291
292// Tira da lista um servidor, ou todos. Devolve quantos saíram.
293const desligar = async ($: EngineInterface, raiz?: string) => {
294 const lista = await read($, servidores)
295 const saem = lista.filter(s => raiz === undefined || s.raiz === raiz)
296
297 await update($, servidores, atual =>
298 atual.filter(s => !saem.some(um => um.raiz === s.raiz)),
299 )
300
301 // Servidor idêntico que não é filho deste carregamento (órfão de sessão
302 // anterior): o stream não existe, então o pedido de parar vai por PID.
303 for (const s of saem) {
304 const dono = vivos.has(s.raiz) || s.porta === 0 ? undefined : await ocupante($, s)
305
306 if (dono?.igual === true) {
307 await $.process.run(['kill', dono.pid])
308 }
309 }
310
311 await acertar($)
312
313 return saem.length
314}
315
316// A lista que o store guarda para a sessão em curso, quando a tela está vazia.
317const restaurarSessao = async ($: EngineInterface) => {
318 if ((await read($, servidores)).length > 0) {
319 return
320 }
321
322 const sessoes = ((await $.store.get('sessoes')) ?? {}) as Record<string, Servidor[]>
323 const salvos = sessoes[await $.session.id()] ?? []
324
325 if (salvos.length > 0) {
326 await update($, servidores, () => salvos)
327 }
328}
329
330export const register: Register = on => {
331 on('session.start', async ($, e, next) => {
332 await $.command.register({
333 name: 'previa',
334 description: 'Liga, lista ou para o servidor de prévia',
335 argumentHint: '[caminho | status | parar]',
336 immediate: true,
337 })
338 const iniciada = await next(e)
339
340 restaurar = false
341 await restaurarSessao($)
342
343 batida?.cancel()
344 batida = $.clock.every(15000, () => void rodar($))
345 void acertar($)
346
347 return iniciada
348 })
349
350 // /clear e /resume seguem no mesmo processo com outro id e sem session.start: os
351 // servidores da sessão que acabou param (o store fica com a lista dela) e os da
352 // seguinte voltam no primeiro envio.
353 on('session.end', async ($, e, next) => {
354 for (const filho of vivos.values()) {
355 void filho.return(FIM)
356 }
357
358 vivos.clear()
359 gravado = '[]'
360 avisado = ''
361 restaurar = true
362 await update($, servidores, () => [])
363
364 return next(e)
365 })
366
367 on('tool.call', { tool: ['Write', 'Edit'] }, async ($, e, next) => {
368 const feito = await next(e)
369 const gravou = feito.deny === undefined && feito.isError !== true
370
371 // Rascunho em pasta temporária (scratchpad de agente) não vira prévia.
372 const temporario = /^\/(private\/)?(tmp|var\/folders)\//.test(e.file_path)
373
374 if (gravou && !temporario && /\.html$/i.test(e.file_path)) {
375 ultimoHtml = e.file_path
376 await servir($, e.file_path)
377 .then(() => void acertar($))
378 .catch(erro => $.ui.log(`previa: ${String(erro)}`, { to: 'debug' }))
379 }
380
381 return feito
382 })
383
384 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
385 const ativos = (await read($, servidores)).filter(s => s.porta > 0)
386
387 if (ativos.length === 0 || !/\b(pkill|killall|kill)\b/.test(e.command)) {
388 return next(e)
389 }
390
391 // "pkill -f 'python3 render.py'" mira outro processo e passa.
392 const emMassa =
393 /\b(pkill|killall)\b[^|;&\n]*(http\.server|\bpython3?["']?\s*(?:$|[|;&\n]))/i
394 const mata = async (s: Servidor) => {
395 const porPorta = new RegExp(`(lsof\\b[^|;&\\n]*:|kill-port\\s+)${s.porta}\\b`)
396
397 if (porPorta.test(e.command)) {
398 return true
399 }
400
401 const { pid } = await ocupante($, s)
402
403 return pid !== '' && new RegExp(`\\bkill\\b[^|;&\\n]*\\s${pid}\\b`).test(e.command)
404 }
405 let alvo = emMassa.test(e.command) ? ativos[0] : undefined
406
407 for (const s of ativos) {
408 alvo ??= (await mata(s)) ? s : undefined
409 }
410
411 return alvo === undefined
412 ? next(e)
413 : {
414 deny: `previa: esse comando derruba o servidor de prévia do usuário em http://localhost:${alvo.porta}/ (pasta ${alvo.raiz}). Ele não foi aberto pela tarefa. Quem encerra é o usuário, com /previa parar.`,
415 }
416 })
417
418 on('prompt.submit', async ($, e, next) => {
419 if (restaurar) {
420 restaurar = false
421 await restaurarSessao($)
422 // Levanta antes do aviso, para a linha dizer a porta em que o servidor ficou.
423 await acertar($)
424 }
425
426 const ativos = (await read($, servidores)).filter(s => s.porta > 0)
427
428 const onde = ativos
429 .map(s => `http://localhost:${s.porta}/ (pasta ${s.raiz})`)
430 .join(', ')
431
432 // A linha entra uma vez por conjunto de servidores, não a cada pedido. No app desktop o envio do
433 // usuário chega como `sdk`; no terminal, como `composer`.
434 const doUsuario = e.origin.kind === 'composer' || e.origin.kind === 'sdk'
435 if (!doUsuario || onde === avisado || onde === '') {
436 return next(e)
437 }
438
439 avisado = onde
440 const linha = `previa: servidor de prévia do usuário ativo em ${onde}. Não iniciar outro servidor para essa pasta nem encerrar este; ele não foi aberto pela tarefa.`
441
442 return next({ ...e, context: [...(e.context ?? []), linha] })
443 })
444
445 // O resumo da compactação não guarda a linha: o próximo pedido avisa de novo.
446 on('session.compact', ($, e, next) => {
447 avisado = ''
448
449 return next(e)
450 })
451
452 on('command.run', { command: 'previa' }, async ($, e) => {
453 const pedido = e.args.trim().replace(/^(['"])(.*)\1$/, '$2')
454
455 if (pedido === 'parar') {
456 const quantos = await desligar($)
457
458 return {
459 text:
460 quantos === 0
461 ? 'Nenhum servidor ativo.'
462 : `${quantos} ${quantos === 1 ? 'servidor encerrado' : 'servidores encerrados'}.`,
463 }
464 }
465
466 if (pedido === 'status') {
467 const ativos = (await read($, servidores)).filter(s => s.porta > 0)
468
469 return {
470 text:
471 ativos.length === 0
472 ? 'Nenhum servidor ativo.'
473 : ativos.map(s => `${nome(s.raiz)} ${urlDe(s)}`).join(' | '),
474 }
475 }
476
477 const cwd = await $.session.cwd()
478 const home = await $.env.get('HOME')
479 const caminho =
480 pedido === ''
481 ? (ultimoHtml ?? cwd)
482 : pedido.startsWith('~/') && home !== undefined
483 ? `${home}${pedido.slice(1)}`
484 : pedido.startsWith('/')
485 ? pedido
486 : `${cwd}/${pedido}`
487 const raiz = await servir($, caminho)
488
489 if (raiz === undefined) {
490 return { text: `Caminho não encontrado: ${caminho}.` }
491 }
492
493 await acertar($)
494 const servidor = (await read($, servidores)).find(s => s.raiz === raiz)
495
496 return {
497 text:
498 servidor === undefined || servidor.porta === 0
499 ? `O servidor não subiu em ${raiz}.`
500 : `No ar: ${urlDe(servidor)}`,
501 }
502 })
503
504 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
505 const ativos = (await read($, servidores)).filter(s => s.porta > 0)
506
507 if (e.props.hasSurvey || ativos.length === 0) {
508 return next(e)
509 }
510
511 const { Box, Button, Text } = $.ui.resolve(e)
512 // O que os outros mods desenham na faixa continua acima das linhas da prévia.
513 const abaixo = await next(e)
514
515 return (
516 <Box flexDirection="column">
517 {abaixo}
518 {ativos.map(s => {
519 const url = urlDe(s)
520
521 return (
522 <Box gap={2}>
523 <Text dimColor>Prévia</Text>
524 <Text bold>{nome(s.raiz)}</Text>
525 <Text wrap="truncate-middle">{url}</Text>
526 <Box gap={1}>
527 <Button
528 key={`abrir-${s.porta}`}
529 label="Abrir"
530 onPress={() => void $.process.run(['open', url])}
531 />
532 <Button
533 key={`copiar-${s.porta}`}
534 label="Copiar"
535 onPress={press => void $.ui.copy({ text: url, surface: press.surface })}
536 />
537 <Button
538 key={`parar-${s.porta}`}
539 label="Parar"
540 onPress={() => void desligar($, s.raiz)}
541 />
542 </Box>
543 </Box>
544 )
545 })}
546 </Box>
547 )
548 })
549}
550types/index.d.ts 16 lines1export type Servidor = {
2 /** Pasta servida, caminho absoluto resolvido. */
3 raiz: string
4 /** Porta em 127.0.0.1; 0 enquanto o servidor ainda não foi levantado. */
5 porta: number
6 /** Último arquivo editado, relativo à raiz; vazio quando a pasta foi ligada sem arquivo. */
7 arquivo: string
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 /** Servidores desta sessão, do mais recente para o mais antigo. */
13 previa: { servidores: Servidor[] }
14 }
15}
16