redmine-context 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +449 -0
  3. package/dist/bundle/index.d.ts +5 -0
  4. package/dist/bundle/index.js +5 -0
  5. package/dist/bundle/json.d.ts +90 -0
  6. package/dist/bundle/json.js +266 -0
  7. package/dist/bundle/markdown.d.ts +75 -0
  8. package/dist/bundle/markdown.js +294 -0
  9. package/dist/bundle/search-list.d.ts +43 -0
  10. package/dist/bundle/search-list.js +53 -0
  11. package/dist/bundle/stable-stringify.d.ts +26 -0
  12. package/dist/bundle/stable-stringify.js +50 -0
  13. package/dist/cache/contract.d.ts +157 -0
  14. package/dist/cache/contract.js +0 -0
  15. package/dist/cache/disk-index.d.ts +82 -0
  16. package/dist/cache/disk-index.js +220 -0
  17. package/dist/cache/disk.d.ts +133 -0
  18. package/dist/cache/disk.js +313 -0
  19. package/dist/cache/gc.d.ts +78 -0
  20. package/dist/cache/gc.js +123 -0
  21. package/dist/cache/get-or-compute.d.ts +36 -0
  22. package/dist/cache/get-or-compute.js +52 -0
  23. package/dist/cache/index.d.ts +9 -0
  24. package/dist/cache/index.js +8 -0
  25. package/dist/cache/keys.d.ts +76 -0
  26. package/dist/cache/keys.js +78 -0
  27. package/dist/cache/memory.d.ts +48 -0
  28. package/dist/cache/memory.js +110 -0
  29. package/dist/cache-first.d.ts +127 -0
  30. package/dist/cache-first.js +227 -0
  31. package/dist/client/errors.d.ts +33 -0
  32. package/dist/client/errors.js +49 -0
  33. package/dist/client/http.d.ts +110 -0
  34. package/dist/client/http.js +207 -0
  35. package/dist/client/index.d.ts +5 -0
  36. package/dist/client/index.js +5 -0
  37. package/dist/client/issues.d.ts +71 -0
  38. package/dist/client/issues.js +100 -0
  39. package/dist/client/search.d.ts +58 -0
  40. package/dist/client/search.js +81 -0
  41. package/dist/config/credentials.d.ts +247 -0
  42. package/dist/config/credentials.js +427 -0
  43. package/dist/config/doctor.d.ts +123 -0
  44. package/dist/config/doctor.js +260 -0
  45. package/dist/config/index.d.ts +6 -0
  46. package/dist/config/index.js +6 -0
  47. package/dist/config/keyring.d.ts +96 -0
  48. package/dist/config/keyring.js +158 -0
  49. package/dist/config/login.d.ts +97 -0
  50. package/dist/config/login.js +189 -0
  51. package/dist/config/settings.d.ts +94 -0
  52. package/dist/config/settings.js +140 -0
  53. package/dist/contract.d.ts +173 -0
  54. package/dist/contract.js +27 -0
  55. package/dist/core.d.ts +1 -0
  56. package/dist/core.js +8 -0
  57. package/dist/extract/audio-extractor.d.ts +105 -0
  58. package/dist/extract/audio-extractor.js +156 -0
  59. package/dist/extract/audio.d.ts +126 -0
  60. package/dist/extract/audio.js +184 -0
  61. package/dist/extract/dispatcher.d.ts +132 -0
  62. package/dist/extract/dispatcher.js +115 -0
  63. package/dist/extract/download.d.ts +111 -0
  64. package/dist/extract/download.js +261 -0
  65. package/dist/extract/duration.d.ts +106 -0
  66. package/dist/extract/duration.js +148 -0
  67. package/dist/extract/ffmpeg.d.ts +56 -0
  68. package/dist/extract/ffmpeg.js +95 -0
  69. package/dist/extract/gguf.d.ts +137 -0
  70. package/dist/extract/gguf.js +215 -0
  71. package/dist/extract/index.d.ts +19 -0
  72. package/dist/extract/index.js +19 -0
  73. package/dist/extract/magic.d.ts +80 -0
  74. package/dist/extract/magic.js +282 -0
  75. package/dist/extract/ooxml.d.ts +131 -0
  76. package/dist/extract/ooxml.js +336 -0
  77. package/dist/extract/pdf.d.ts +147 -0
  78. package/dist/extract/pdf.js +322 -0
  79. package/dist/extract/queue.d.ts +167 -0
  80. package/dist/extract/queue.js +217 -0
  81. package/dist/extract/subprocess.d.ts +145 -0
  82. package/dist/extract/subprocess.js +181 -0
  83. package/dist/extract/tesseract.d.ts +153 -0
  84. package/dist/extract/tesseract.js +321 -0
  85. package/dist/extract/video-extractor.d.ts +84 -0
  86. package/dist/extract/video-extractor.js +89 -0
  87. package/dist/extract/video.d.ts +198 -0
  88. package/dist/extract/video.js +418 -0
  89. package/dist/extract/which.d.ts +63 -0
  90. package/dist/extract/which.js +72 -0
  91. package/dist/extract/whisper-extract.d.ts +211 -0
  92. package/dist/extract/whisper-extract.js +323 -0
  93. package/dist/extract/whisper.d.ts +50 -0
  94. package/dist/extract/whisper.js +67 -0
  95. package/dist/extract/zip.d.ts +50 -0
  96. package/dist/extract/zip.js +165 -0
  97. package/dist/extract-issue-attachments.d.ts +82 -0
  98. package/dist/extract-issue-attachments.js +156 -0
  99. package/dist/fetch-attachment-text.d.ts +113 -0
  100. package/dist/fetch-attachment-text.js +155 -0
  101. package/dist/fetch-issue-bundle.d.ts +81 -0
  102. package/dist/fetch-issue-bundle.js +93 -0
  103. package/dist/fetch-issue-search.d.ts +74 -0
  104. package/dist/fetch-issue-search.js +120 -0
  105. package/dist/index.d.ts +21 -0
  106. package/dist/index.js +67 -0
  107. package/dist/normalize/collections.d.ts +52 -0
  108. package/dist/normalize/collections.js +170 -0
  109. package/dist/normalize/helpers.d.ts +35 -0
  110. package/dist/normalize/helpers.js +59 -0
  111. package/dist/normalize/index.d.ts +2 -0
  112. package/dist/normalize/index.js +2 -0
  113. package/dist/normalize/issue.d.ts +34 -0
  114. package/dist/normalize/issue.js +154 -0
  115. package/dist/surfaces/cli/commands.d.ts +61 -0
  116. package/dist/surfaces/cli/commands.js +262 -0
  117. package/dist/surfaces/cli/main.d.ts +32 -0
  118. package/dist/surfaces/cli/main.js +210 -0
  119. package/dist/surfaces/cli/prompts.d.ts +66 -0
  120. package/dist/surfaces/cli/prompts.js +147 -0
  121. package/dist/surfaces/cli/tty.d.ts +27 -0
  122. package/dist/surfaces/cli/tty.js +35 -0
  123. package/dist/surfaces/cli/types.d.ts +39 -0
  124. package/dist/surfaces/cli/types.js +7 -0
  125. package/dist/surfaces/mcp/server.d.ts +171 -0
  126. package/dist/surfaces/mcp/server.js +427 -0
  127. package/dist/surfaces/tui/app.d.ts +55 -0
  128. package/dist/surfaces/tui/app.js +180 -0
  129. package/dist/surfaces/tui/attachment-status.d.ts +79 -0
  130. package/dist/surfaces/tui/attachment-status.js +113 -0
  131. package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
  132. package/dist/surfaces/tui/components/breadcrumb.js +31 -0
  133. package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
  134. package/dist/surfaces/tui/components/gradient-text.js +75 -0
  135. package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
  136. package/dist/surfaces/tui/components/scroll-view.js +77 -0
  137. package/dist/surfaces/tui/components/spinner.d.ts +12 -0
  138. package/dist/surfaces/tui/components/spinner.js +39 -0
  139. package/dist/surfaces/tui/components/text-input.d.ts +45 -0
  140. package/dist/surfaces/tui/components/text-input.js +114 -0
  141. package/dist/surfaces/tui/format-file-size.d.ts +24 -0
  142. package/dist/surfaces/tui/format-file-size.js +43 -0
  143. package/dist/surfaces/tui/glyphs.d.ts +47 -0
  144. package/dist/surfaces/tui/glyphs.js +84 -0
  145. package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
  146. package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
  147. package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
  148. package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
  149. package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
  150. package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
  151. package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
  152. package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
  153. package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
  154. package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
  155. package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
  156. package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
  157. package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
  158. package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
  159. package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
  160. package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
  161. package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
  162. package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
  163. package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
  164. package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
  165. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
  166. package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
  167. package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
  168. package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
  169. package/dist/surfaces/tui/index.d.ts +50 -0
  170. package/dist/surfaces/tui/index.js +158 -0
  171. package/dist/surfaces/tui/instance.d.ts +37 -0
  172. package/dist/surfaces/tui/instance.js +36 -0
  173. package/dist/surfaces/tui/job-registry.d.ts +113 -0
  174. package/dist/surfaces/tui/job-registry.js +123 -0
  175. package/dist/surfaces/tui/job-status.d.ts +45 -0
  176. package/dist/surfaces/tui/job-status.js +81 -0
  177. package/dist/surfaces/tui/navigation.d.ts +74 -0
  178. package/dist/surfaces/tui/navigation.js +87 -0
  179. package/dist/surfaces/tui/palettes.d.ts +38 -0
  180. package/dist/surfaces/tui/palettes.js +244 -0
  181. package/dist/surfaces/tui/screen.d.ts +30 -0
  182. package/dist/surfaces/tui/screen.js +51 -0
  183. package/dist/surfaces/tui/screens/about.d.ts +2 -0
  184. package/dist/surfaces/tui/screens/about.js +35 -0
  185. package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
  186. package/dist/surfaces/tui/screens/appearance.js +74 -0
  187. package/dist/surfaces/tui/screens/config.d.ts +2 -0
  188. package/dist/surfaces/tui/screens/config.js +82 -0
  189. package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
  190. package/dist/surfaces/tui/screens/doctor.js +109 -0
  191. package/dist/surfaces/tui/screens/export.d.ts +2 -0
  192. package/dist/surfaces/tui/screens/export.js +168 -0
  193. package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
  194. package/dist/surfaces/tui/screens/home-selection.js +80 -0
  195. package/dist/surfaces/tui/screens/home.d.ts +2 -0
  196. package/dist/surfaces/tui/screens/home.js +200 -0
  197. package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
  198. package/dist/surfaces/tui/screens/issue-detail.js +182 -0
  199. package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
  200. package/dist/surfaces/tui/screens/jobs.js +89 -0
  201. package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
  202. package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
  203. package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
  204. package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
  205. package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
  206. package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
  207. package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
  208. package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
  209. package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
  210. package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
  211. package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
  212. package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
  213. package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
  214. package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
  215. package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
  216. package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
  217. package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
  218. package/dist/surfaces/tui/screens/welcome.js +95 -0
  219. package/dist/surfaces/tui/status-color.d.ts +21 -0
  220. package/dist/surfaces/tui/status-color.js +23 -0
  221. package/dist/surfaces/tui/symbols.d.ts +231 -0
  222. package/dist/surfaces/tui/symbols.js +14 -0
  223. package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
  224. package/dist/surfaces/tui/terminal-colors.js +39 -0
  225. package/dist/surfaces/tui/theme.d.ts +156 -0
  226. package/dist/surfaces/tui/theme.js +86 -0
  227. package/dist/surfaces/tui/truncate.d.ts +31 -0
  228. package/dist/surfaces/tui/truncate.js +81 -0
  229. package/package.json +93 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Callbacks REAIS do onboarding (#28) — fecha o wiring entre as telas de
3
+ * onboarding (`../screens/onboarding/`) e o core: login por senha
4
+ * (`loginWithPassword`), validação de api_key colada (`validateApiKey`, o
5
+ * fallback de 2FA do ADR-003) e persistência via a cascata de credenciais M2
6
+ * (`createCredentialCascade` — keychain preferido, com migração automática).
7
+ *
8
+ * Único ponto de import do core para o fluxo de onboarding (ADR-005): as
9
+ * telas em si (`../screens/onboarding/*.tsx`) só recebem os callbacks
10
+ * injetados via `<OnboardingProvider callbacks={...}>` (`../app.tsx`) — nunca
11
+ * importam `../../../index.js` diretamente.
12
+ *
13
+ * Mantém uma cópia local da URL (via o callback `onUrlSubmit`, atualizado a
14
+ * cada submissão da tela de URL) porque o `OnboardingContext` só existe
15
+ * DEPOIS deste hook rodar (`<OnboardingProvider callbacks={...}>` recebe os
16
+ * callbacks como prop) — não há como este hook consumir `useOnboarding()`
17
+ * sem um ciclo de dependência.
18
+ */
19
+ import { useRef } from 'react';
20
+ import { createCredentialCascade, defaultSettingsStore, loginWithPassword, validateApiKey, RedmineAuthError, } from '../../../index.js';
21
+ /**
22
+ * Interpreta `REDMINE_INSECURE` (`1`/`true`, case-insensitive) — mesma
23
+ * convenção usada por `../screens/onboarding/url.tsx` e pelo MCP
24
+ * (`../../mcp/server.ts`).
25
+ */
26
+ function isInsecureEnvSet(env) {
27
+ const raw = env.REDMINE_INSECURE;
28
+ return raw !== undefined && /^(1|true)$/i.test(raw.trim());
29
+ }
30
+ /** Extrai uma mensagem segura de uma falha desconhecida — o core já redige segredos antes de lançar. */
31
+ function messageOf(cause) {
32
+ return cause instanceof Error ? cause.message : String(cause);
33
+ }
34
+ /**
35
+ * Persiste a instância autenticada (#187) — simétrico ao `login` da CLL. Falha ao
36
+ * salvar a URL NÃO invalida o onboarding (a credencial já foi salva): swallow.
37
+ */
38
+ async function persistInstance(settings, url) {
39
+ try {
40
+ await settings.setInstanceUrl(url);
41
+ }
42
+ catch {
43
+ // Sem persistência a TUI ainda funciona nesta sessão (REDMINE_URL/prompt).
44
+ }
45
+ }
46
+ /**
47
+ * Monta os callbacks reais de onboarding — injetados em
48
+ * `<OnboardingProvider callbacks={...}>` por `../app.tsx`.
49
+ *
50
+ * @example
51
+ * const callbacks = useOnboardingCallbacks();
52
+ * return <OnboardingProvider callbacks={callbacks}>...</OnboardingProvider>;
53
+ */
54
+ export function useOnboardingCallbacks(settings = defaultSettingsStore()) {
55
+ // Ref (não state): a URL só é lida dentro dos callbacks de login/api_key,
56
+ // nunca precisa disparar um re-render por si só.
57
+ const urlRef = useRef('');
58
+ const onUrlSubmit = (url) => {
59
+ urlRef.current = url;
60
+ };
61
+ const onModeSelect = () => {
62
+ // Reason: o modo escolhido só direciona a NAVEGAÇÃO entre telas
63
+ // (`../screens/onboarding/mode.tsx`) — nenhuma chamada ao core depende
64
+ // dele, então não há estado a guardar aqui.
65
+ };
66
+ const onLoginSubmit = async (credentials) => {
67
+ try {
68
+ const result = await loginWithPassword({
69
+ baseUrl: urlRef.current,
70
+ username: credentials.username,
71
+ password: credentials.password,
72
+ insecure: isInsecureEnvSet(process.env),
73
+ });
74
+ await createCredentialCascade().set(urlRef.current, result.apiKey);
75
+ await persistInstance(settings, urlRef.current);
76
+ return { kind: 'success', user: result.user };
77
+ }
78
+ catch (cause) {
79
+ if (cause instanceof RedmineAuthError) {
80
+ return { kind: 'auth-error', message: cause.message };
81
+ }
82
+ return { kind: 'network-error', message: messageOf(cause) };
83
+ }
84
+ };
85
+ const onApiKeySubmit = async (apiKey) => {
86
+ try {
87
+ const result = await validateApiKey({
88
+ baseUrl: urlRef.current,
89
+ apiKey,
90
+ insecure: isInsecureEnvSet(process.env),
91
+ });
92
+ await createCredentialCascade().set(urlRef.current, result.apiKey);
93
+ await persistInstance(settings, urlRef.current);
94
+ return { kind: 'success', user: result.user };
95
+ }
96
+ catch (cause) {
97
+ if (cause instanceof RedmineAuthError) {
98
+ return { kind: 'auth-error', message: cause.message };
99
+ }
100
+ return { kind: 'network-error', message: messageOf(cause) };
101
+ }
102
+ };
103
+ return { onUrlSubmit, onModeSelect, onLoginSubmit, onApiKeySubmit };
104
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Hook de largura do terminal (M2-16, #39): lê `process.stdout.columns` com
3
+ * fallback, escuta o evento `resize` do stdout REAL para atualizar em
4
+ * runtime (redimensionamento da janela do terminal), e é injetável via
5
+ * {@link TerminalWidthProvider} para testes.
6
+ *
7
+ * A injeção via contexto existe porque `ink-testing-library` FIXA
8
+ * `stdout.columns` em `100` (não expõe opção de configurar — ver
9
+ * `node_modules/ink-testing-library/build/index.js`), então nenhum teste
10
+ * usando o `render()` daquela lib consegue simular 80/60 colunas só ajustando
11
+ * `process.stdout.columns`. Testes de layout responsivo usam
12
+ * `tests/surfaces/tui/render-at-width.tsx`, que injeta a largura por este
13
+ * provider (e mocka o `stdout` passado ao Ink real com o mesmo valor, para
14
+ * que o PRÓPRIO wrap do Ink também reflita a largura simulada).
15
+ */
16
+ import { type ReactNode } from 'react';
17
+ /** Largura assumida quando `process.stdout.columns` está indisponível (ex.: saída não-TTY). */
18
+ export declare const DEFAULT_TERMINAL_WIDTH = 80;
19
+ /** Altura assumida quando `process.stdout.rows` está indisponível. */
20
+ export declare const DEFAULT_TERMINAL_HEIGHT = 24;
21
+ /**
22
+ * Provider de largura FIXA — usado pelos testes (injeta 80/60 sem depender do
23
+ * `process.stdout` real). Fora de testes, a árvore da TUI (`../app.tsx`) usa o
24
+ * {@link TerminalSizeProvider} (uma única assinatura de `resize`).
25
+ */
26
+ export declare function TerminalWidthProvider({ width, children }: {
27
+ width: number;
28
+ children: ReactNode;
29
+ }): import("react").JSX.Element;
30
+ /**
31
+ * Provider de TAMANHO do terminal (largura + altura) com UMA ÚNICA assinatura do
32
+ * evento `resize` do stdout (#190 fix). Montado no topo da TUI (`../app.tsx`) para
33
+ * que `useTerminalWidth`/`useTerminalHeight` leiam do contexto em vez de cada
34
+ * consumidor (ex.: cada linha da lista) assinar o próprio `resize` — o que
35
+ * estourava o limite de 10 listeners do EventEmitter (MaxListenersExceededWarning).
36
+ */
37
+ export declare function TerminalSizeProvider({ children }: {
38
+ children: ReactNode;
39
+ }): import("react").JSX.Element;
40
+ /**
41
+ * Largura atual do terminal, em colunas.
42
+ *
43
+ * Usa o provider mais próximo se houver ({@link TerminalSizeProvider} em produção
44
+ * ou {@link TerminalWidthProvider} nos testes); senão (sem provider) lê o stdout
45
+ * real e assina o `resize` — caminho de fallback para uso isolado.
46
+ */
47
+ export declare function useTerminalWidth(): number;
48
+ /**
49
+ * Altura atual do terminal, em linhas. Lê do {@link TerminalSizeProvider}; sem ele
50
+ * (uso isolado/testes) cai no fallback {@link DEFAULT_TERMINAL_HEIGHT}.
51
+ */
52
+ export declare function useTerminalHeight(): number;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Hook de largura do terminal (M2-16, #39): lê `process.stdout.columns` com
4
+ * fallback, escuta o evento `resize` do stdout REAL para atualizar em
5
+ * runtime (redimensionamento da janela do terminal), e é injetável via
6
+ * {@link TerminalWidthProvider} para testes.
7
+ *
8
+ * A injeção via contexto existe porque `ink-testing-library` FIXA
9
+ * `stdout.columns` em `100` (não expõe opção de configurar — ver
10
+ * `node_modules/ink-testing-library/build/index.js`), então nenhum teste
11
+ * usando o `render()` daquela lib consegue simular 80/60 colunas só ajustando
12
+ * `process.stdout.columns`. Testes de layout responsivo usam
13
+ * `tests/surfaces/tui/render-at-width.tsx`, que injeta a largura por este
14
+ * provider (e mocka o `stdout` passado ao Ink real com o mesmo valor, para
15
+ * que o PRÓPRIO wrap do Ink também reflita a largura simulada).
16
+ */
17
+ import { createContext, useContext, useEffect, useState } from 'react';
18
+ /** Largura assumida quando `process.stdout.columns` está indisponível (ex.: saída não-TTY). */
19
+ export const DEFAULT_TERMINAL_WIDTH = 80;
20
+ /** Altura assumida quando `process.stdout.rows` está indisponível. */
21
+ export const DEFAULT_TERMINAL_HEIGHT = 24;
22
+ const TerminalWidthContext = createContext(undefined);
23
+ /** Altura atual (linhas), provida pelo {@link TerminalSizeProvider}; `undefined` fora dele. */
24
+ const TerminalHeightContext = createContext(undefined);
25
+ /** Lê a largura atual do stdout do processo, com fallback para {@link DEFAULT_TERMINAL_WIDTH}. */
26
+ function readProcessColumns() {
27
+ const columns = process.stdout.columns;
28
+ return typeof columns === 'number' && columns > 0 ? columns : DEFAULT_TERMINAL_WIDTH;
29
+ }
30
+ /** Lê a altura atual do stdout do processo, com fallback para {@link DEFAULT_TERMINAL_HEIGHT}. */
31
+ function readProcessRows() {
32
+ const rows = process.stdout.rows;
33
+ return typeof rows === 'number' && rows > 0 ? rows : DEFAULT_TERMINAL_HEIGHT;
34
+ }
35
+ /**
36
+ * Provider de largura FIXA — usado pelos testes (injeta 80/60 sem depender do
37
+ * `process.stdout` real). Fora de testes, a árvore da TUI (`../app.tsx`) usa o
38
+ * {@link TerminalSizeProvider} (uma única assinatura de `resize`).
39
+ */
40
+ export function TerminalWidthProvider({ width, children }) {
41
+ return _jsx(TerminalWidthContext.Provider, { value: width, children: children });
42
+ }
43
+ /**
44
+ * Provider de TAMANHO do terminal (largura + altura) com UMA ÚNICA assinatura do
45
+ * evento `resize` do stdout (#190 fix). Montado no topo da TUI (`../app.tsx`) para
46
+ * que `useTerminalWidth`/`useTerminalHeight` leiam do contexto em vez de cada
47
+ * consumidor (ex.: cada linha da lista) assinar o próprio `resize` — o que
48
+ * estourava o limite de 10 listeners do EventEmitter (MaxListenersExceededWarning).
49
+ */
50
+ export function TerminalSizeProvider({ children }) {
51
+ const [size, setSize] = useState(() => ({ width: readProcessColumns(), height: readProcessRows() }));
52
+ useEffect(() => {
53
+ const onResize = () => setSize({ width: readProcessColumns(), height: readProcessRows() });
54
+ process.stdout.on('resize', onResize);
55
+ return () => {
56
+ process.stdout.off('resize', onResize);
57
+ };
58
+ }, []);
59
+ return (_jsx(TerminalWidthContext.Provider, { value: size.width, children: _jsx(TerminalHeightContext.Provider, { value: size.height, children: children }) }));
60
+ }
61
+ /**
62
+ * Largura atual do terminal, em colunas.
63
+ *
64
+ * Usa o provider mais próximo se houver ({@link TerminalSizeProvider} em produção
65
+ * ou {@link TerminalWidthProvider} nos testes); senão (sem provider) lê o stdout
66
+ * real e assina o `resize` — caminho de fallback para uso isolado.
67
+ */
68
+ export function useTerminalWidth() {
69
+ const injected = useContext(TerminalWidthContext);
70
+ const [width, setWidth] = useState(readProcessColumns);
71
+ useEffect(() => {
72
+ // Com um provider (produção via TerminalSizeProvider, ou testes), a largura é
73
+ // a fonte da verdade — não assina o stdout real por consumidor.
74
+ if (injected !== undefined)
75
+ return;
76
+ const handleResize = () => setWidth(readProcessColumns());
77
+ process.stdout.on('resize', handleResize);
78
+ return () => {
79
+ process.stdout.off('resize', handleResize);
80
+ };
81
+ }, [injected]);
82
+ return injected ?? width;
83
+ }
84
+ /**
85
+ * Altura atual do terminal, em linhas. Lê do {@link TerminalSizeProvider}; sem ele
86
+ * (uso isolado/testes) cai no fallback {@link DEFAULT_TERMINAL_HEIGHT}.
87
+ */
88
+ export function useTerminalHeight() {
89
+ return useContext(TerminalHeightContext) ?? DEFAULT_TERMINAL_HEIGHT;
90
+ }
@@ -0,0 +1,50 @@
1
+ import { type SettingsStore } from '../../index.js';
2
+ import { type InstanceInfo } from './instance.js';
3
+ import type { Theme } from './theme.js';
4
+ /** Dependências injetáveis do `runTui` (default: processo real). */
5
+ export interface RunTuiDeps {
6
+ /** Ambiente (para `REDMINE_URL`); default `process.env`. */
7
+ env?: NodeJS.ProcessEnv;
8
+ /** Store da URL persistida + paleta; default {@link defaultSettingsStore}. */
9
+ settings?: SettingsStore;
10
+ /** Stream de saída (para o alt-screen); default `process.stdout`. Injetável em testes. */
11
+ stdout?: NodeJS.WriteStream;
12
+ /** `process` para os handlers de sinal do alt-screen; default `process`. Injetável em testes. */
13
+ proc?: AltScreenProc;
14
+ }
15
+ /**
16
+ * Resolve a instância da TUI no boot (#187) e POPULA `env.REDMINE_URL` para os
17
+ * hooks de dados (que leem o ambiente) — `REDMINE_URL` vence; sem ela, usa a URL
18
+ * persistida no `login`. Extraída para teste (o `runTui` bloqueia em `waitUntilExit`).
19
+ *
20
+ * @param env - Ambiente a consultar/popular (o `runTui` passa `process.env`).
21
+ * @param settings - Store da URL persistida.
22
+ * @returns Metadados da instância + ação de limpar a persistida, para o contexto.
23
+ */
24
+ export declare function resolveTuiInstance(env: NodeJS.ProcessEnv, settings: SettingsStore): Promise<InstanceInfo>;
25
+ /**
26
+ * Sobe a TUI e aguarda o app sair.
27
+ *
28
+ * @param deps - Ver {@link RunTuiDeps} (env/settings/stdout); default: processo real.
29
+ * @returns Exit code `0` (sair da TUI é sempre encerramento normal).
30
+ */
31
+ export declare function runTui(deps?: RunTuiDeps): Promise<number>;
32
+ /** Subconjunto do `process` que `installAltScreen` usa (injetável em testes). */
33
+ export interface AltScreenProc {
34
+ on(event: string, listener: (...args: unknown[]) => void): unknown;
35
+ off(event: string, listener: (...args: unknown[]) => void): unknown;
36
+ exit(code?: number): never;
37
+ }
38
+ /**
39
+ * Entra no alt-screen e instala a restauração à prova de saída não cooperativa
40
+ * (#190, B1). Restaura em: saída normal (via o `dispose` retornado, chamado no
41
+ * `finally`), `process.exit`/fim natural (`'exit'`) e sinais de término
42
+ * (SIGINT/SIGTERM/SIGHUP) — nesses, restaura SÍNCRONO e então encerra com o código
43
+ * convencional. A restauração é IDEMPOTENTE (só escreve a sequência de saída uma vez).
44
+ *
45
+ * @param out - Stream do terminal.
46
+ * @param proc - `process` (ou um fake em testes).
47
+ * @param theme - Paleta ativa no boot (aplica fundo/texto default via OSC).
48
+ * @returns `dispose()` — restaura + remove os listeners (saída normal).
49
+ */
50
+ export declare function installAltScreen(out: NodeJS.WriteStream, proc: AltScreenProc, theme?: Theme): () => void;
@@ -0,0 +1,158 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Ponto de entrada da TUI (M2-01).
4
+ *
5
+ * Renderiza o roteador (`app.tsx`) via Ink e resolve quando o usuário sai
6
+ * (`q`, ver `app.tsx`). Consumido pelo CLI quando `redmine-context` roda sem
7
+ * argumentos e o stdout é um TTY (`../cli/main.ts`).
8
+ *
9
+ * `exitOnCtrlC: false` (M2-04): o app implementa seu próprio padrão de saída
10
+ * (duplo Ctrl+C, ver `hooks/use-exit-guard.ts`) — sem essa flag, o Ink sai do
11
+ * processo na primeira pressão de Ctrl+C antes do handler global rodar.
12
+ *
13
+ * Full-screen + paletas (#190): entra no buffer de tela alternativo (alt-screen)
14
+ * em TTY e passa a paleta persistida + o flag full-screen ao `App`.
15
+ */
16
+ import { render } from 'ink';
17
+ import { defaultSettingsStore } from '../../index.js';
18
+ import { App } from './app.js';
19
+ import { InstanceProvider } from './instance.js';
20
+ import { DEFAULT_PALETTE_ID, resolvePalette } from './palettes.js';
21
+ import { applyTerminalColors, resetTerminalColors } from './terminal-colors.js';
22
+ /**
23
+ * Resolve a instância da TUI no boot (#187) e POPULA `env.REDMINE_URL` para os
24
+ * hooks de dados (que leem o ambiente) — `REDMINE_URL` vence; sem ela, usa a URL
25
+ * persistida no `login`. Extraída para teste (o `runTui` bloqueia em `waitUntilExit`).
26
+ *
27
+ * @param env - Ambiente a consultar/popular (o `runTui` passa `process.env`).
28
+ * @param settings - Store da URL persistida.
29
+ * @returns Metadados da instância + ação de limpar a persistida, para o contexto.
30
+ */
31
+ export async function resolveTuiInstance(env, settings) {
32
+ let url = nonEmpty(env.REDMINE_URL);
33
+ let origin = url !== undefined ? 'env' : 'none';
34
+ if (url === undefined) {
35
+ const persisted = await settings.getInstanceUrl().catch(() => undefined);
36
+ if (persisted !== undefined && persisted.length > 0) {
37
+ env.REDMINE_URL = persisted;
38
+ url = persisted;
39
+ origin = 'config';
40
+ }
41
+ }
42
+ return {
43
+ ...(url !== undefined ? { url } : {}),
44
+ origin,
45
+ clearPersisted: async () => {
46
+ await settings.clearInstanceUrl();
47
+ // Se a instância desta sessão veio da persistida, ela foi escrita em
48
+ // `env.REDMINE_URL` no boot — limpa também, senão os hooks de dados
49
+ // continuariam vendo a instância "logada" após o logout (só um restart
50
+ // corrigiria). Não toca `env` quando a origem foi a própria env real.
51
+ if (origin === 'config') {
52
+ delete env.REDMINE_URL;
53
+ }
54
+ },
55
+ };
56
+ }
57
+ /**
58
+ * Sobe a TUI e aguarda o app sair.
59
+ *
60
+ * @param deps - Ver {@link RunTuiDeps} (env/settings/stdout); default: processo real.
61
+ * @returns Exit code `0` (sair da TUI é sempre encerramento normal).
62
+ */
63
+ export async function runTui(deps = {}) {
64
+ const env = deps.env ?? process.env;
65
+ const settings = deps.settings ?? defaultSettingsStore();
66
+ const instance = await resolveTuiInstance(env, settings);
67
+ // Paleta persistida (#190) — default (Catppuccin Mocha) se nenhuma escolhida.
68
+ const paletteId = (await settings.getPaletteId().catch(() => undefined)) ?? DEFAULT_PALETTE_ID;
69
+ // Full-screen (#190): alt-screen buffer (como vim/htop) só em TTY real — a TUI
70
+ // ocupa a tela toda e o conteúdo anterior do terminal é restaurado ao sair.
71
+ // Fora de TTY o `shouldRenderTui` (M2-03) já nem chega aqui.
72
+ const out = deps.stdout ?? process.stdout;
73
+ const fullScreen = out.isTTY === true;
74
+ // `dispose` restaura o terminal na saída NORMAL (finally). Os handlers de sinal
75
+ // instalados por `installAltScreen` cobrem a saída NÃO cooperativa (SIGTERM/
76
+ // SIGHUP/SIGINT externos, fechar a aba) — sem eles o `finally` de uma Promise
77
+ // não roda e o terminal fica preso no alt-screen (achado B1 da review).
78
+ const bootTheme = resolvePalette(paletteId).theme;
79
+ const dispose = fullScreen ? installAltScreen(out, deps.proc ?? process, bootTheme) : () => undefined;
80
+ try {
81
+ const { waitUntilExit } = render(_jsx(InstanceProvider, { value: instance, children: _jsx(App, { initialPaletteId: paletteId, settings: settings }) }), { exitOnCtrlC: false });
82
+ await waitUntilExit();
83
+ }
84
+ finally {
85
+ dispose();
86
+ }
87
+ return 0;
88
+ }
89
+ /**
90
+ * Entra no alt-screen, aplica as cores da paleta como default do terminal (OSC,
91
+ * ANTES do clear — a tela é limpa já com o fundo temático), limpa e esconde o cursor.
92
+ */
93
+ function enterAltScreen(out, theme) {
94
+ out.write('\x1b[?1049h');
95
+ if (theme !== undefined)
96
+ applyTerminalColors(out, theme);
97
+ out.write('\x1b[H\x1b[2J\x1b[?25l');
98
+ }
99
+ /** Restaura cursor + cores default do terminal + buffer principal (ao sair). */
100
+ function leaveAltScreen(out, theme) {
101
+ out.write('\x1b[?25h');
102
+ if (theme !== undefined)
103
+ resetTerminalColors(out);
104
+ out.write('\x1b[?1049l');
105
+ }
106
+ /** Sinais que terminam o processo e para os quais restauramos o terminal. */
107
+ const RESTORE_SIGNALS = [
108
+ ['SIGINT', 130],
109
+ ['SIGTERM', 143],
110
+ ['SIGHUP', 129],
111
+ ];
112
+ /**
113
+ * Entra no alt-screen e instala a restauração à prova de saída não cooperativa
114
+ * (#190, B1). Restaura em: saída normal (via o `dispose` retornado, chamado no
115
+ * `finally`), `process.exit`/fim natural (`'exit'`) e sinais de término
116
+ * (SIGINT/SIGTERM/SIGHUP) — nesses, restaura SÍNCRONO e então encerra com o código
117
+ * convencional. A restauração é IDEMPOTENTE (só escreve a sequência de saída uma vez).
118
+ *
119
+ * @param out - Stream do terminal.
120
+ * @param proc - `process` (ou um fake em testes).
121
+ * @param theme - Paleta ativa no boot (aplica fundo/texto default via OSC).
122
+ * @returns `dispose()` — restaura + remove os listeners (saída normal).
123
+ */
124
+ export function installAltScreen(out, proc, theme) {
125
+ enterAltScreen(out, theme);
126
+ let restored = false;
127
+ const restore = () => {
128
+ if (restored)
129
+ return;
130
+ restored = true;
131
+ leaveAltScreen(out, theme);
132
+ };
133
+ const onExit = () => restore();
134
+ const signalHandlers = RESTORE_SIGNALS.map(([signal, code]) => {
135
+ const handler = () => {
136
+ restore();
137
+ cleanup();
138
+ proc.exit(code);
139
+ };
140
+ proc.on(signal, handler);
141
+ return [signal, handler];
142
+ });
143
+ proc.on('exit', onExit);
144
+ function cleanup() {
145
+ proc.off('exit', onExit);
146
+ for (const [signal, handler] of signalHandlers)
147
+ proc.off(signal, handler);
148
+ }
149
+ return () => {
150
+ restore();
151
+ cleanup();
152
+ };
153
+ }
154
+ /** Retorna a string aparada se não-vazia, senão `undefined`. */
155
+ function nonEmpty(value) {
156
+ const trimmed = value?.trim();
157
+ return trimmed !== undefined && trimmed.length > 0 ? trimmed : undefined;
158
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Contexto da INSTÂNCIA resolvida da TUI (#187).
3
+ *
4
+ * O `runTui` resolve a instância uma vez no boot — `REDMINE_URL` (env) tem
5
+ * precedência; sem ela, usa a URL persistida no `login` — e a expõe aqui para as
6
+ * telas que precisam saber a ORIGEM (`config`/`doctor`) e para o `logout` também
7
+ * limpar a URL persistida. Os hooks de dados continuam lendo `process.env`
8
+ * (populado no boot), então este contexto carrega só metadados + a ação de limpar.
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ /** Origem da instância em uso na TUI. */
12
+ export type InstanceOrigin = 'env' | 'config' | 'none';
13
+ /** Metadados da instância resolvida + ação para limpar a URL persistida. */
14
+ export interface InstanceInfo {
15
+ /** URL da instância em uso, ou `undefined` se nenhuma. */
16
+ readonly url?: string;
17
+ /** De onde veio: `env` (REDMINE_URL), `config` (persistida) ou `none`. */
18
+ readonly origin: InstanceOrigin;
19
+ /** Remove a URL persistida (usado no logout); no-op se não houver store. */
20
+ readonly clearPersisted: () => Promise<void>;
21
+ }
22
+ /** Provider da instância resolvida (montado pelo `runTui`). */
23
+ export declare function InstanceProvider({ value, children, }: {
24
+ value: InstanceInfo;
25
+ children: ReactNode;
26
+ }): ReactNode;
27
+ /** Lê a instância resolvida no contexto (default seguro fora do provider). */
28
+ export declare function useInstance(): InstanceInfo;
29
+ /**
30
+ * `true` se a credencial de AMBIENTE (`REDMINE_API_KEY`) pode ser usada nesta
31
+ * sessão. SEGURANÇA (#187): quando a instância veio de fonte MUTÁVEL (`settings.json`
32
+ * persistido, origem `config`), retorna `false` — a env-key é instance-agnóstica e
33
+ * não pode ser enviada a uma URL controlável por arquivo. Espelha o `allowEnvFallback`
34
+ * já aplicado na CLI (`issue`) e no MCP; deve ser passado no `CredentialCascadeOptions`
35
+ * de TODA resolução de credencial da TUI.
36
+ */
37
+ export declare function useEnvFallbackAllowed(): boolean;
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Contexto da INSTÂNCIA resolvida da TUI (#187).
4
+ *
5
+ * O `runTui` resolve a instância uma vez no boot — `REDMINE_URL` (env) tem
6
+ * precedência; sem ela, usa a URL persistida no `login` — e a expõe aqui para as
7
+ * telas que precisam saber a ORIGEM (`config`/`doctor`) e para o `logout` também
8
+ * limpar a URL persistida. Os hooks de dados continuam lendo `process.env`
9
+ * (populado no boot), então este contexto carrega só metadados + a ação de limpar.
10
+ */
11
+ import { createContext, useContext } from 'react';
12
+ /** Default seguro: sem instância e limpeza no-op (usado fora do provider, ex.: testes). */
13
+ const DEFAULT = {
14
+ origin: 'none',
15
+ clearPersisted: async () => undefined,
16
+ };
17
+ const InstanceContext = createContext(DEFAULT);
18
+ /** Provider da instância resolvida (montado pelo `runTui`). */
19
+ export function InstanceProvider({ value, children, }) {
20
+ return _jsx(InstanceContext.Provider, { value: value, children: children });
21
+ }
22
+ /** Lê a instância resolvida no contexto (default seguro fora do provider). */
23
+ export function useInstance() {
24
+ return useContext(InstanceContext);
25
+ }
26
+ /**
27
+ * `true` se a credencial de AMBIENTE (`REDMINE_API_KEY`) pode ser usada nesta
28
+ * sessão. SEGURANÇA (#187): quando a instância veio de fonte MUTÁVEL (`settings.json`
29
+ * persistido, origem `config`), retorna `false` — a env-key é instance-agnóstica e
30
+ * não pode ser enviada a uma URL controlável por arquivo. Espelha o `allowEnvFallback`
31
+ * já aplicado na CLI (`issue`) e no MCP; deve ser passado no `CredentialCascadeOptions`
32
+ * de TODA resolução de credencial da TUI.
33
+ */
34
+ export function useEnvFallbackAllowed() {
35
+ return useInstance().origin !== 'config';
36
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Registro leve de jobs da sessão (#34/M2-11) — fonte de dados do painel de
3
+ * jobs (`./screens/jobs.tsx`, tecla `t` na home/detalhe).
4
+ *
5
+ * Mesmo padrão de `./screens/home-selection.tsx`/`./screens/loaded-issue-context.tsx`
6
+ * (ver o JSDoc de qualquer um dos dois para as alternativas descartadas —
7
+ * singleton de módulo vazaria estado entre testes; guardar isto dentro de
8
+ * `NavigationValue` acoplaria o roteador a um detalhe de produtores
9
+ * específicos): instanciado uma vez em `./app.tsx`, ACIMA da pilha de
10
+ * navegação, para que jobs sobrevivam ao push/pop de telas — iniciar uma
11
+ * exportação (`./screens/export.tsx`), navegar para outra tela e voltar ainda
12
+ * deve mostrar o job (em andamento ou já concluído) no painel.
13
+ *
14
+ * ## Contrato para produtores futuros (M3/M4 — extração de anexos)
15
+ *
16
+ * Qualquer operação assíncrona da sessão que o usuário deva acompanhar
17
+ * (exportação hoje; extração de imagem/PDF no M3, áudio/vídeo no M4) é um
18
+ * "produtor": chama {@link JobRegistryValue.registerJob} uma vez ao iniciar
19
+ * e {@link JobRegistryValue.updateJobStatus} a cada transição de estado.
20
+ * Regras do contrato:
21
+ *
22
+ * 1. `id` é escolhido pelo PRODUTOR, não pelo registro — deve ser único por
23
+ * EXECUÇÃO (ex.: `extract-${attachmentId}-${Date.now()}`), permitindo que
24
+ * tentativas repetidas do mesmo tipo de job apareçam como entradas
25
+ * distintas no painel (histórico da sessão, não um contador único por
26
+ * recurso). `registerJob` faz upsert por `id`: registrar de novo com um
27
+ * `id` já existente substitui a entrada em vez de duplicá-la.
28
+ * 2. Transições de `status` esperadas: `pending` (enfileirado, ainda não
29
+ * iniciado) → `processing` (em execução) → `done` | `failed` (estado
30
+ * terminal). Produtores sem fase de fila (ex.: exportação, #33 — dispara
31
+ * e já começa a gravar) podem registrar direto em `processing`.
32
+ * 3. `startedAt` (epoch ms, `Date.now()`) é OPCIONAL — quando presente, é o
33
+ * instante em que o job entrou em `processing` (não o momento do
34
+ * registro em si, que pode ser antes se o job nasceu `pending`). Hoje o
35
+ * painel (`./screens/jobs.tsx`) não renderiza duração a partir disso —
36
+ * campo já capturado para uma UI futura sem exigir novo campo no
37
+ * contrato.
38
+ * 4. `cancelable`/`onCancel`: só produtores que sabem interromper a
39
+ * operação de fato devem marcar `cancelable: true` E fornecer
40
+ * `onCancel` (chamado pelo painel ao usuário pressionar Ctrl+C com o job
41
+ * selecionado). Exportação (#33) NÃO é cancelável — nada a abortar no
42
+ * meio de um `writeFile` — o painel mostra "(não cancelável)" para ela.
43
+ * A infraestrutura de cancelamento REAL (abortar uma extração em
44
+ * andamento) é do M4; até lá, nenhum produtor real marca `cancelable`.
45
+ * 5. Jobs terminais (`done`/`failed`) permanecem no registro até o fim da
46
+ * sessão (histórico "concluídas da sessão" da AC) — nenhuma limpeza
47
+ * automática hoje.
48
+ */
49
+ import { type ReactNode } from 'react';
50
+ /** Estados possíveis de um job (ver a regra 2 do contrato no JSDoc do módulo). */
51
+ export type JobStatus = 'pending' | 'processing' | 'done' | 'failed';
52
+ /**
53
+ * Um job da sessão, exibido no painel (`./screens/jobs.tsx`). Ver o JSDoc do
54
+ * módulo para o contrato completo consumido por produtores futuros (M3/M4).
55
+ */
56
+ export interface Job {
57
+ /** Identificador único da EXECUÇÃO (não do recurso) — escolhido pelo produtor, ver regra 1. */
58
+ id: string;
59
+ /** Rótulo legível exibido no painel (ex.: `Exportar #88 (md)`). */
60
+ label: string;
61
+ /** Estado atual — ver as transições esperadas na regra 2. */
62
+ status: JobStatus;
63
+ /** Instante (epoch ms) em que o job entrou em `processing` — ver regra 3. Opcional. */
64
+ startedAt?: number;
65
+ /** `true` só quando o produtor também fornece {@link onCancel} — ver regra 4. Default `false`. */
66
+ cancelable?: boolean;
67
+ /** Chamado pelo painel ao usuário pressionar Ctrl+C com este job selecionado, quando `cancelable` é `true`. */
68
+ onCancel?: () => void;
69
+ /** Progresso incremental 0–100 (extrações M3/M4). Omitido: indeterminado. */
70
+ progress?: number;
71
+ /** Detalhe curto da fase atual ou mensagem de erro (ex.: "transcrevendo áudio"). */
72
+ detail?: string;
73
+ }
74
+ /** Campos atualizáveis de um job em andamento — ver {@link JobRegistryValue.updateJob}. */
75
+ export interface JobPatch {
76
+ status?: JobStatus;
77
+ progress?: number;
78
+ detail?: string;
79
+ label?: string;
80
+ }
81
+ /** Valor exposto pelo contexto do registro de jobs. */
82
+ export interface JobRegistryValue {
83
+ /** Jobs da sessão, na ordem de registro (mais antigo primeiro). */
84
+ jobs: readonly Job[];
85
+ /**
86
+ * Registra um job novo, ou substitui (upsert por `id`) um já existente —
87
+ * ver regra 1 do contrato.
88
+ */
89
+ registerJob: (job: Job) => void;
90
+ /**
91
+ * Atualiza campos do job (`status`/`progress`/`detail`/`label`). Sem efeito
92
+ * se `id` não existir. Transições a partir de estado TERMINAL (`done`/
93
+ * `failed`) são ignoradas — bug de produtor não regride o painel.
94
+ */
95
+ updateJob: (id: string, patch: JobPatch) => void;
96
+ /** Atalho de compatibilidade: `updateJob(id, { status })`. */
97
+ updateJobStatus: (id: string, status: JobStatus) => void;
98
+ }
99
+ /**
100
+ * Provider do registro de jobs — instanciado uma vez em `./app.tsx`,
101
+ * envolvendo toda a árvore de telas (ver o JSDoc do módulo).
102
+ */
103
+ export declare function JobRegistryProvider({ children }: {
104
+ children: ReactNode;
105
+ }): import("react").JSX.Element;
106
+ /**
107
+ * Hook do registro de jobs, consumido tanto por produtores (`./screens/export.tsx`,
108
+ * futuramente extração no M3/M4) quanto pelo painel (`./screens/jobs.tsx`).
109
+ *
110
+ * @throws {Error} Se chamado fora de um `<JobRegistryProvider>` — indica erro
111
+ * de composição (tela renderizada sem passar pelo roteador).
112
+ */
113
+ export declare function useJobRegistry(): JobRegistryValue;