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,168 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Tela de exportação do bundle (#33) — aberta a partir do detalhe da issue
4
+ * (`e`, ver `./issue-detail.tsx`), SOMENTE quando a issue já terminou de
5
+ * carregar (`state.status === 'loaded'` naquela tela).
6
+ *
7
+ * Reusa a issue já normalizada em memória via `./loaded-issue-context.js`
8
+ * (ver o JSDoc daquele módulo para a escolha de reuso vs. refetch) e chama
9
+ * `../hooks/use-export-bundle.js`, que empacota com `buildMarkdownBundle`/
10
+ * `buildJsonBundle` (fronteira do core, `../../../index.js`) e grava
11
+ * `<id>.md`/`<id>.json` no destino informado.
12
+ *
13
+ * Dois campos alternados por `Tab` (mesmo padrão de
14
+ * `./onboarding/login.tsx`): formato — lista navegável (↑/↓,
15
+ * `../hooks/use-list-navigation.js`) entre `md`/`json`/`both` — e destino —
16
+ * `../components/text-input.js`, com `process.cwd()` como valor inicial
17
+ * (`~` é expandido pelo hook antes de gravar). `Enter` em qualquer um dos
18
+ * dois campos confirma a exportação.
19
+ *
20
+ * #34 (M2-11): PRIMEIRO produtor real do registro de jobs da sessão
21
+ * (`../job-registry.js`, ver o JSDoc daquele módulo para o contrato) — ao
22
+ * confirmar (Enter), registra um job em `processing` (exportação não tem fase
23
+ * de fila: começa a gravar assim que confirmada) e o transiciona para
24
+ * `done`/`failed` conforme `useExportBundle` resolve. `useExportBundle` em si
25
+ * permanece intocado (fronteira do core, ADR-005) — a integração vive só
26
+ * aqui, na tela, mantendo o hook puro. Exportação NÃO é cancelável
27
+ * (`cancelable` fica `false`/omitido) — o painel (`../screens/jobs.js`)
28
+ * mostra "(não cancelável)" para ela.
29
+ *
30
+ * 4 estados visuais: formulário (`idle`), progresso (`Spinner` existente,
31
+ * igual a `./issue-detail.tsx`), sucesso (caminho(s) absoluto(s) gravado(s) +
32
+ * `b` para voltar) e erro (`theme.danger` + ação corretiva — diretório
33
+ * inexistente ou sem permissão de escrita, ver o hook). `b` (ou `Esc`, atalho
34
+ * global de `../app.tsx`) volta ao detalhe em qualquer estado que não seja
35
+ * `exporting` — a pilha já preserva a navegação (a issue é buscada de novo
36
+ * pelo detalhe ao remontar, mesmo comportamento já existente ao voltar da
37
+ * home para o detalhe).
38
+ *
39
+ * M2-16 (#39): o campo de destino usa `maxWidth` do `TextInput`
40
+ * (`../components/text-input.js`) — o valor exibido é truncado a partir do
41
+ * INÍCIO (preserva o fim, onde o usuário está digitando) quando o caminho
42
+ * excede o espaço restante do terminal, o mesmo `Box` em linha que sofreria
43
+ * sobreposição sem isso (ver o JSDoc de `../truncate.ts`). Os caminhos
44
+ * gravados exibidos em `success`/`error` truncam do FIM (`../truncate.js`,
45
+ * `truncate`) — são texto estático, não um campo em edição.
46
+ */
47
+ import { Box, Text, useInput } from 'ink';
48
+ import { useCallback, useEffect, useRef, useState } from 'react';
49
+ import { Spinner } from '../components/spinner.js';
50
+ import { TextInput } from '../components/text-input.js';
51
+ import { glyphs } from '../glyphs.js';
52
+ import { useExportBundle } from '../hooks/use-export-bundle.js';
53
+ import { useListNavigation } from '../hooks/use-list-navigation.js';
54
+ import { useJobRegistry } from '../job-registry.js';
55
+ import { useTerminalWidth } from '../hooks/use-terminal-width.js';
56
+ import { useNavigation } from '../navigation.js';
57
+ import { symbols } from '../symbols.js';
58
+ import { useTheme } from '../theme.js';
59
+ import { truncate } from '../truncate.js';
60
+ import { useLoadedIssue } from './loaded-issue-context.js';
61
+ /** `paddingX={1}` dos dois lados do `Box` raiz da tela. */
62
+ const SCREEN_PADDING_X = 2;
63
+ /** Recuo (`{' '}`) na frente de cada caminho gravado, na tela de sucesso. */
64
+ const SUCCESS_PATH_INDENT_WIDTH = 2;
65
+ /** Largura mínima garantida a um caminho exibido, mesmo em terminais muito estreitos. */
66
+ const MIN_PATH_WIDTH = 8;
67
+ /** Alterna entre os dois campos do formulário. */
68
+ function otherField(field) {
69
+ return field === 'format' ? 'destination' : 'format';
70
+ }
71
+ /** As 3 opções de formato exigidas pela AC: MD, JSON, ou ambos. */
72
+ const FORMAT_OPTIONS = [
73
+ { value: 'md', label: 'Markdown (.md)' },
74
+ { value: 'json', label: 'JSON (.json)' },
75
+ { value: 'both', label: 'Ambos (.md + .json)' },
76
+ ];
77
+ /** Uma linha da lista de formato: ponteiro quando é a linha selecionada, negrito quando o campo está focado. */
78
+ function FormatRow({ label, selected, focused }) {
79
+ const theme = useTheme();
80
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: selected ? `${symbols.pointerSmall} ` : ' ' }), _jsx(Text, { color: selected && focused ? theme.primary : theme.muted, children: label })] }));
81
+ }
82
+ /** Rótulo fixo à esquerda do campo de destino: ponteiro/espaço (1) + `" Destino: "` (10). */
83
+ const DESTINATION_LABEL_WIDTH = 11;
84
+ /** Largura mínima garantida ao campo de destino/caminhos exibidos, mesmo em terminais muito estreitos. */
85
+ const MIN_DESTINATION_WIDTH = 8;
86
+ /** Tela de exportação: escolhe formato + destino, grava via o core, mostra progresso/sucesso/erro. */
87
+ export function ExportScreen() {
88
+ const theme = useTheme();
89
+ const { pop } = useNavigation();
90
+ const { issue } = useLoadedIssue();
91
+ const { state, runExport, reset } = useExportBundle(issue);
92
+ // #34 (M2-11): registro de jobs da sessão — exportação é o PRIMEIRO
93
+ // produtor real (ver o JSDoc do módulo para o contrato completo).
94
+ const { registerJob, updateJobStatus } = useJobRegistry();
95
+ const terminalWidth = useTerminalWidth();
96
+ const destinationMaxWidth = Math.max(terminalWidth - SCREEN_PADDING_X - DESTINATION_LABEL_WIDTH, MIN_DESTINATION_WIDTH);
97
+ const pathMaxWidth = Math.max(terminalWidth - SCREEN_PADDING_X - SUCCESS_PATH_INDENT_WIDTH, MIN_PATH_WIDTH);
98
+ const [field, setField] = useState('format');
99
+ const [destination, setDestination] = useState(() => process.cwd());
100
+ const isFormActive = state.status === 'idle';
101
+ // Id do job da tentativa de exportação EM ANDAMENTO (ou da última, após
102
+ // terminar) — `undefined` antes da primeira confirmação. Um `ref` (não
103
+ // estado) porque só é lido pelo efeito abaixo, nunca pelo JSX.
104
+ const currentJobIdRef = useRef(undefined);
105
+ const handleSubmit = useCallback((formatIndex) => {
106
+ const format = FORMAT_OPTIONS[formatIndex]?.value ?? 'md';
107
+ if (issue !== undefined) {
108
+ // Novo `id` a cada confirmação (mesmo após um erro + "r" de retry) —
109
+ // cada tentativa vira uma entrada distinta no painel (histórico da
110
+ // sessão), nunca uma reescrita silenciosa da anterior.
111
+ const jobId = `export-${issue.id}-${Date.now()}`;
112
+ currentJobIdRef.current = jobId;
113
+ registerJob({
114
+ id: jobId,
115
+ label: `Exportar #${issue.id} (${format})`,
116
+ status: 'processing',
117
+ startedAt: Date.now(),
118
+ // Exportação não é cancelável — grava direto no disco, sem uma
119
+ // etapa segura para abortar no meio (ver o JSDoc do módulo).
120
+ cancelable: false,
121
+ });
122
+ }
123
+ void runExport(format, destination);
124
+ }, [runExport, destination, issue, registerJob]);
125
+ // Espelha o resultado da exportação no job registrado acima — SEM tocar em
126
+ // `useExportBundle` (fronteira do core intocada, ver o JSDoc do módulo).
127
+ useEffect(() => {
128
+ const jobId = currentJobIdRef.current;
129
+ if (jobId === undefined) {
130
+ return;
131
+ }
132
+ if (state.status === 'success') {
133
+ updateJobStatus(jobId, 'done');
134
+ }
135
+ else if (state.status === 'error') {
136
+ updateJobStatus(jobId, 'failed');
137
+ }
138
+ }, [state.status, updateJobStatus]);
139
+ const { selectedIndex: formatIndex } = useListNavigation(FORMAT_OPTIONS.length, {
140
+ isActive: isFormActive && field === 'format',
141
+ onSelect: handleSubmit,
142
+ });
143
+ // Handlers ESTÁVEIS (refs + useCallback, padrão do repo): identidade nova a
144
+ // cada render des/re-subscreve o useInput e pode perder uma tecla rápida.
145
+ const stateStatusRef = useRef(state.status);
146
+ stateStatusRef.current = state.status;
147
+ const popRef = useRef(pop);
148
+ popRef.current = pop;
149
+ const resetRef = useRef(reset);
150
+ resetRef.current = reset;
151
+ const handleControlInput = useCallback((input, key) => {
152
+ if (key.tab && stateStatusRef.current === 'idle') {
153
+ setField(otherField);
154
+ return;
155
+ }
156
+ // "b" não interrompe uma exportação em andamento — evita voltar no meio
157
+ // de uma gravação em disco.
158
+ if (input === 'b' && stateStatusRef.current !== 'exporting') {
159
+ popRef.current();
160
+ return;
161
+ }
162
+ if (input === 'r' && stateStatusRef.current === 'error') {
163
+ resetRef.current();
164
+ }
165
+ }, []);
166
+ useInput(handleControlInput);
167
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.primary, children: "Exportar bundle" }), issue === undefined ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Nenhuma issue carregada \u2014 volte ao detalhe e tente de novo." }) })) : null, issue !== undefined && state.status === 'idle' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: ["Issue #", issue.id] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: field === 'format' ? theme.primary : theme.muted, children: "Formato:" }), FORMAT_OPTIONS.map((option, index) => (_jsx(FormatRow, { label: option.label, selected: index === formatIndex, focused: field === 'format' }, option.value)))] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: field === 'destination' ? theme.primary : theme.muted, children: [field === 'destination' ? symbols.pointer : ' ', " Destino:", ' '] }), _jsx(TextInput, { value: destination, onChange: setDestination, onSubmit: () => handleSubmit(formatIndex), isActive: field === 'destination', maxWidth: destinationMaxWidth })] })] })) : null, state.status === 'exporting' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Exportando..."] }) })) : null, state.status === 'success' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.success, children: [symbols.tick, " Bundle gravado:"] }), state.files.map((file) => (_jsxs(Text, { color: theme.muted, children: [' ', truncate(file.path, pathMaxWidth)] }, file.path)))] })) : null, state.status === 'error' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.danger, children: [symbols.cross, " Falha ao gravar o bundle: ", state.message] }), _jsx(Text, { color: theme.muted, children: "Verifique se o diret\u00F3rio existe e se voc\u00EA tem permiss\u00E3o de escrita nele, ou informe outro destino." })] })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [state.status === 'idle' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), ' ', "escolhe o formato,", ' ', _jsx(Text, { color: theme.accent, children: "Tab" }), ' ', "alterna formato/destino,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "exporta,", ' '] })) : null, state.status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: "r" }), ' ', "tenta de novo,", ' '] })) : null, _jsx(Text, { color: theme.accent, children: "b" }), ' ', "volta ao detalhe."] }) })] }));
168
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Estado de seleção da home (#31), compartilhado entre `./home.tsx` e
3
+ * `./issue-detail.tsx` sem prop-drilling.
4
+ *
5
+ * O roteador (`../screen.ts`/`../app.tsx`) renderiza cada tela SEM props e só
6
+ * mantém montado o TOPO da pilha — ao empilhar `issue-detail` (Enter sobre
7
+ * uma issue na home), `HomeScreen` é desmontada. Isso cria dois problemas que
8
+ * este módulo resolve com um único contexto:
9
+ *
10
+ * 1. **Qual issue abrir**: `issue-detail` precisa saber o `id` da issue
11
+ * selecionada para buscá-la via `../hooks/use-issue-detail.js` — sem
12
+ * props entre telas, o único jeito de repassar essa informação é um
13
+ * estado compartilhado escrito pela home (`setSelectedIssueId`) e lido
14
+ * pelo detalhe (`selectedIssueId`).
15
+ * 2. **Posição da lista ao voltar**: `selectedIndex` de
16
+ * `../hooks/use-list-navigation.js` vive num `useState` LOCAL ao
17
+ * componente — ele reseta para `0` a cada remount. Sem preservá-lo aqui,
18
+ * Esc/`b` a partir do detalhe devolveria o usuário ao topo da lista,
19
+ * mesmo que a issue aberta fosse a 10ª.
20
+ *
21
+ * Instanciado uma vez em `../app.tsx`, envolvendo toda a árvore de telas —
22
+ * mesmo padrão do `OnboardingProvider` (`./onboarding/onboarding-context.tsx`),
23
+ * que resolve o mesmo tipo de problema (estado compartilhado entre telas sem
24
+ * props) para o fluxo de onboarding.
25
+ *
26
+ * ## Alternativas descartadas
27
+ *
28
+ * - **Adicionar parâmetros às telas** (`SCREENS` passaria a aceitar props):
29
+ * afetaria as 12 telas do registro (`../screen.ts`) e o contrato do
30
+ * roteador inteiro por uma necessidade de só duas telas — desproporcional.
31
+ * - **Singleton de módulo** (uma variável fora de qualquer componente React,
32
+ * sem `Context`): funciona em produção, mas vaza estado entre execuções de
33
+ * teste (`render()` de um teste não reresetaria o singleton do teste
34
+ * seguinte) — um `Context` reinstanciado a cada `render()` evita esse
35
+ * acoplamento oculto.
36
+ * - **Guardar no próprio `NavigationValue`** (`../navigation.tsx`): esse
37
+ * contrato é sobre QUAIS telas existem na pilha, não sobre o estado interno
38
+ * de uma tela específica — misturar os dois tornaria `navigation.tsx`
39
+ * acoplado a detalhes da home/detalhe de issue.
40
+ */
41
+ import { type ReactNode } from 'react';
42
+ /** Valor exposto pelo contexto de seleção da home. */
43
+ export interface HomeSelectionValue {
44
+ /**
45
+ * Índice da linha destacada na lista da home — sobrevive ao push/pop de
46
+ * `issue-detail` (ver o JSDoc do módulo). Semente lida por `initialIndex`
47
+ * de `../hooks/use-list-navigation.js`.
48
+ */
49
+ selectedIndex: number;
50
+ /** Atualiza o índice destacado — chamado pela home a cada mudança do cursor. */
51
+ setSelectedIndex: (index: number) => void;
52
+ /** Id da issue aberta em `issue-detail`, ou `undefined` antes de qualquer seleção. */
53
+ selectedIssueId: number | undefined;
54
+ /** Registra a issue selecionada — chamado pela home ao empilhar `issue-detail` (Enter). */
55
+ setSelectedIssueId: (id: number) => void;
56
+ }
57
+ /**
58
+ * Provider do contexto de seleção da home — instanciado uma vez em
59
+ * `../app.tsx`, envolvendo toda a árvore de telas (ver o JSDoc do módulo).
60
+ */
61
+ export declare function HomeSelectionProvider({ children }: {
62
+ children: ReactNode;
63
+ }): import("react").JSX.Element;
64
+ /**
65
+ * Hook de seleção da home consumido por `./home.tsx`/`./issue-detail.tsx`.
66
+ *
67
+ * @throws {Error} Se chamado fora de um `<HomeSelectionProvider>` — indica
68
+ * erro de composição (tela renderizada sem passar pelo roteador).
69
+ */
70
+ export declare function useHomeSelection(): HomeSelectionValue;
@@ -0,0 +1,80 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Estado de seleção da home (#31), compartilhado entre `./home.tsx` e
4
+ * `./issue-detail.tsx` sem prop-drilling.
5
+ *
6
+ * O roteador (`../screen.ts`/`../app.tsx`) renderiza cada tela SEM props e só
7
+ * mantém montado o TOPO da pilha — ao empilhar `issue-detail` (Enter sobre
8
+ * uma issue na home), `HomeScreen` é desmontada. Isso cria dois problemas que
9
+ * este módulo resolve com um único contexto:
10
+ *
11
+ * 1. **Qual issue abrir**: `issue-detail` precisa saber o `id` da issue
12
+ * selecionada para buscá-la via `../hooks/use-issue-detail.js` — sem
13
+ * props entre telas, o único jeito de repassar essa informação é um
14
+ * estado compartilhado escrito pela home (`setSelectedIssueId`) e lido
15
+ * pelo detalhe (`selectedIssueId`).
16
+ * 2. **Posição da lista ao voltar**: `selectedIndex` de
17
+ * `../hooks/use-list-navigation.js` vive num `useState` LOCAL ao
18
+ * componente — ele reseta para `0` a cada remount. Sem preservá-lo aqui,
19
+ * Esc/`b` a partir do detalhe devolveria o usuário ao topo da lista,
20
+ * mesmo que a issue aberta fosse a 10ª.
21
+ *
22
+ * Instanciado uma vez em `../app.tsx`, envolvendo toda a árvore de telas —
23
+ * mesmo padrão do `OnboardingProvider` (`./onboarding/onboarding-context.tsx`),
24
+ * que resolve o mesmo tipo de problema (estado compartilhado entre telas sem
25
+ * props) para o fluxo de onboarding.
26
+ *
27
+ * ## Alternativas descartadas
28
+ *
29
+ * - **Adicionar parâmetros às telas** (`SCREENS` passaria a aceitar props):
30
+ * afetaria as 12 telas do registro (`../screen.ts`) e o contrato do
31
+ * roteador inteiro por uma necessidade de só duas telas — desproporcional.
32
+ * - **Singleton de módulo** (uma variável fora de qualquer componente React,
33
+ * sem `Context`): funciona em produção, mas vaza estado entre execuções de
34
+ * teste (`render()` de um teste não reresetaria o singleton do teste
35
+ * seguinte) — um `Context` reinstanciado a cada `render()` evita esse
36
+ * acoplamento oculto.
37
+ * - **Guardar no próprio `NavigationValue`** (`../navigation.tsx`): esse
38
+ * contrato é sobre QUAIS telas existem na pilha, não sobre o estado interno
39
+ * de uma tela específica — misturar os dois tornaria `navigation.tsx`
40
+ * acoplado a detalhes da home/detalhe de issue.
41
+ */
42
+ import { createContext, useCallback, useContext, useState } from 'react';
43
+ const HomeSelectionContext = createContext(undefined);
44
+ /**
45
+ * Provider do contexto de seleção da home — instanciado uma vez em
46
+ * `../app.tsx`, envolvendo toda a árvore de telas (ver o JSDoc do módulo).
47
+ */
48
+ export function HomeSelectionProvider({ children }) {
49
+ const [selectedIndex, setSelectedIndexState] = useState(0);
50
+ const [selectedIssueId, setSelectedIssueIdState] = useState(undefined);
51
+ // Identidades ESTÁVEIS (useCallback sem deps, padrão do repo) — evita
52
+ // resubscrições desnecessárias em quem os usa como dependência de efeito
53
+ // (ex.: `../screens/home.tsx`).
54
+ const setSelectedIndex = useCallback((index) => {
55
+ setSelectedIndexState(index);
56
+ }, []);
57
+ const setSelectedIssueId = useCallback((id) => {
58
+ setSelectedIssueIdState(id);
59
+ }, []);
60
+ const value = {
61
+ selectedIndex,
62
+ setSelectedIndex,
63
+ selectedIssueId,
64
+ setSelectedIssueId,
65
+ };
66
+ return _jsx(HomeSelectionContext.Provider, { value: value, children: children });
67
+ }
68
+ /**
69
+ * Hook de seleção da home consumido por `./home.tsx`/`./issue-detail.tsx`.
70
+ *
71
+ * @throws {Error} Se chamado fora de um `<HomeSelectionProvider>` — indica
72
+ * erro de composição (tela renderizada sem passar pelo roteador).
73
+ */
74
+ export function useHomeSelection() {
75
+ const value = useContext(HomeSelectionContext);
76
+ if (value === undefined) {
77
+ throw new Error('useHomeSelection() usado fora de <HomeSelectionProvider>.');
78
+ }
79
+ return value;
80
+ }
@@ -0,0 +1,2 @@
1
+ /** Tela home: lista "minhas issues", `↑/↓`/`j`/`k` navegam, `Enter` abre o detalhe. */
2
+ export declare function HomeScreen(): import("react").JSX.Element;
@@ -0,0 +1,200 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Tela home da TUI (M2-06, #29): "minhas issues" (`assigned_to_id=me`) via o
4
+ * core, com seleção por teclado (`../hooks/use-list-navigation.js`) e os 4
5
+ * estados visuais exigidos pela AC — carregando (`Spinner`), vazio, erro de
6
+ * rede (banner `theme.danger` + tecla `r` para tentar de novo) e 403
7
+ * (mensagem específica de permissão). Vira o destino pós-onboarding (ver
8
+ * `onboarding/success.tsx`, que faz `resetTo('home')`) e o Enter da
9
+ * `welcome.tsx` quando já há credencial salva na cascata.
10
+ *
11
+ * A busca em si vive em `../hooks/use-my-issues.js` (deps injetáveis,
12
+ * fronteira do core via `../../../index.js`) — esta tela só formata os
13
+ * estados. `Enter` sobre a issue selecionada empilha `issue-detail`
14
+ * (`./issue-detail.js`, detalhe real desde a #31).
15
+ *
16
+ * Fix do review do PR #120: `useMyIssues` agora envolve a busca com
17
+ * `useAuthGuard` (#36) — 401 relogina e retoma a busca sozinho (o estado
18
+ * fica `loading` até lá, nenhuma tela nova aqui). Se o re-login for
19
+ * abandonado (Esc), o hook expõe o 5º estado `auth-aborted` (neutro, não um
20
+ * erro) — renderizado aqui igual aos banners de erro, com o mesmo `r` de
21
+ * retry.
22
+ *
23
+ * M2-07 (#30) acrescenta a busca/filtros inline: `/` abre um `TextInput`
24
+ * QUANDO a home está ativa; `f` cicla o filtro com a busca FECHADA (badge no
25
+ * cabeçalho); Esc fecha a busca sem refetch, interceptado via
26
+ * `../hooks/use-escape-interceptor.ts` para não desempilhar a home.
27
+ *
28
+ * #31 (detalhe) acrescenta `./home-selection.js`: a tela é DESMONTADA ao
29
+ * empilhar `issue-detail`, então seleção (`selectedIndex` + issue escolhida)
30
+ * vive num contexto acima da pilha (padrão do `OnboardingProvider`) — ver o
31
+ * JSDoc de `./home-selection.js`.
32
+ *
33
+ * M2-16 (#39): o subject de cada linha é truncado (`../truncate.js`) com um
34
+ * orçamento de largura CALCULADO por linha (`../hooks/use-terminal-width.js`
35
+ * menos o espaço fixo de ponteiro/`#id`/badge de status, ver
36
+ * `fixedRowOverhead` abaixo) — sem isso, um subject comprido sobrepõe o badge
37
+ * de status da mesma linha em terminais estreitos (o Ink não reflui `<Text>`
38
+ * IRMÃS dentro de um `<Box>` em linha; ver o JSDoc de `../truncate.ts`).
39
+ */
40
+ import { useCallback, useEffect, useRef, useState } from 'react';
41
+ import { Box, Text, useInput } from 'ink';
42
+ import { Spinner } from '../components/spinner.js';
43
+ import { TextInput } from '../components/text-input.js';
44
+ import { glyphs } from '../glyphs.js';
45
+ import { useEscapeInterceptor } from '../hooks/use-escape-interceptor.js';
46
+ import { useIssueSearch } from '../hooks/use-issue-search.js';
47
+ import { useListNavigation } from '../hooks/use-list-navigation.js';
48
+ import { useMyIssues } from '../hooks/use-my-issues.js';
49
+ import { useTerminalWidth } from '../hooks/use-terminal-width.js';
50
+ import { useNavigation } from '../navigation.js';
51
+ import { statusColor } from '../status-color.js';
52
+ import { symbols } from '../symbols.js';
53
+ import { useTheme } from '../theme.js';
54
+ import { truncate } from '../truncate.js';
55
+ import { useHomeSelection } from './home-selection.js';
56
+ /** Largura mínima garantida ao subject, mesmo em terminais muito estreitos. */
57
+ const MIN_SUBJECT_WIDTH = 8;
58
+ /** `paddingX={1}` dos dois lados do `Box` raiz da tela (ver o JSX abaixo). */
59
+ const SCREEN_PADDING_X = 2;
60
+ // Nit do review #120: referência ESTÁVEL para os estados sem lista — um
61
+ // literal `[]` inline em cada render seria recriado a cada chamada,
62
+ // invalidando memoizações a jusante (`useListNavigation`) sem necessidade.
63
+ const EMPTY_ISSUES = [];
64
+ /** Rótulo do badge de cada filtro rápido de status (tecla `f` cicla, M2-07/#30). */
65
+ const STATUS_FILTER_LABELS = {
66
+ open: 'aberta',
67
+ closed: 'fechada',
68
+ all: 'todas',
69
+ };
70
+ /** Próximo filtro no ciclo `aberta → fechada → todas → aberta` (tecla `f`). */
71
+ function nextStatusFilter(current) {
72
+ if (current === 'open')
73
+ return 'closed';
74
+ if (current === 'closed')
75
+ return 'all';
76
+ return 'open';
77
+ }
78
+ /**
79
+ * Espaço fixo ocupado pela linha FORA do subject (M2-16, #39): ponteiro (2),
80
+ * `#id ` (id + `#`/espaço), 1 espaço final após o subject, `[status]`
81
+ * (colchetes + nome) e o `paddingX` da tela. O que sobrar da largura do
82
+ * terminal é o orçamento do subject.
83
+ */
84
+ function fixedRowOverhead(idLength, statusLength) {
85
+ const POINTER_WIDTH = 2;
86
+ const ID_PREFIX_WIDTH = 1 + idLength + 1; // "#" + dígitos + espaço
87
+ const SUBJECT_TRAILING_SPACE = 1;
88
+ const STATUS_BRACKETS_WIDTH = 2 + statusLength; // "[" + nome + "]"
89
+ return POINTER_WIDTH + ID_PREFIX_WIDTH + SUBJECT_TRAILING_SPACE + STATUS_BRACKETS_WIDTH + SCREEN_PADDING_X;
90
+ }
91
+ /** Uma linha da lista: `#id` em `theme.muted`, subject truncado ao orçamento de largura, badge de status. */
92
+ function IssueRow({ issue, selected }) {
93
+ const theme = useTheme();
94
+ const terminalWidth = useTerminalWidth();
95
+ const overhead = fixedRowOverhead(String(issue.id).length, issue.statusName.length);
96
+ const subjectBudget = Math.max(terminalWidth - overhead, MIN_SUBJECT_WIDTH);
97
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: selected ? `${symbols.pointerSmall} ` : ' ' }), _jsxs(Text, { color: theme.muted, children: ["#", issue.id, " "] }), _jsxs(Text, { ...(selected ? { color: theme.primary } : {}), children: [truncate(issue.subject, subjectBudget), ' '] }), _jsxs(Text, { color: statusColor(theme, issue.statusName), children: ["[", issue.statusName, "]"] })] }));
98
+ }
99
+ /** Tela home: lista "minhas issues", `↑/↓`/`j`/`k` navegam, `Enter` abre o detalhe. */
100
+ export function HomeScreen() {
101
+ const theme = useTheme();
102
+ const { push } = useNavigation();
103
+ const { state, retry } = useMyIssues();
104
+ // #31: índice preservado entre remounts + registro de qual issue foi aberta
105
+ // (ver o JSDoc do módulo e de `./home-selection.js`).
106
+ const { selectedIndex: persistedIndex, setSelectedIndex: persistIndex, setSelectedIssueId } = useHomeSelection();
107
+ // --- Busca/filtros inline (M2-07, #30) ---
108
+ const [isSearching, setIsSearching] = useState(false);
109
+ const [query, setQuery] = useState('');
110
+ const [statusFilter, setStatusFilter] = useState('all');
111
+ const search = useIssueSearch(query, statusFilter);
112
+ // Handler ESTÁVEL: `search.clear` é a única dependência mutável (mas já é
113
+ // estável por construção, ver `use-issue-search.ts`) — fecha a busca e
114
+ // restaura os 3 estados neutros de uma vez (query/filtro/hook de busca).
115
+ const closeSearch = useCallback(() => {
116
+ setIsSearching(false);
117
+ setQuery('');
118
+ setStatusFilter('all');
119
+ search.clear();
120
+ }, [search.clear]);
121
+ // Desvia o Esc GLOBAL (`../app.tsx`) enquanto a busca está aberta — sem
122
+ // isso, Esc desempilharia a home inteira em vez de só fechar a busca.
123
+ useEscapeInterceptor(isSearching, closeSearch);
124
+ const issues = state.status === 'loaded' ? state.issues : EMPTY_ISSUES;
125
+ // Handlers ESTÁVEIS (useCallback + refs, padrão do repo): identidade nova a
126
+ // cada render des/re-subscreve o useInput e pode perder uma tecla rápida.
127
+ const pushRef = useRef(push);
128
+ pushRef.current = push;
129
+ const setSelectedIssueIdRef = useRef(setSelectedIssueId);
130
+ setSelectedIssueIdRef.current = setSelectedIssueId;
131
+ const handleSelect = useCallback((index) => {
132
+ const issue = issues[index];
133
+ if (issue !== undefined) {
134
+ setSelectedIssueIdRef.current(issue.id);
135
+ pushRef.current('issue-detail');
136
+ }
137
+ }, [issues]);
138
+ // #30: navegação desligada com a busca aberta; #31: cursor semeado com a
139
+ // última posição persistida (sobrevive ao unmount via home-selection).
140
+ const { selectedIndex } = useListNavigation(issues.length, {
141
+ onSelect: handleSelect,
142
+ isActive: !isSearching,
143
+ initialIndex: persistedIndex,
144
+ });
145
+ // Espelha `selectedIndex` ao contexto — a cópia externa sobrevive ao
146
+ // unmount desta tela (ver ./home-selection.js).
147
+ const persistIndexRef = useRef(persistIndex);
148
+ persistIndexRef.current = persistIndex;
149
+ useEffect(() => {
150
+ persistIndexRef.current(selectedIndex);
151
+ }, [selectedIndex]);
152
+ const retryRef = useRef(retry);
153
+ retryRef.current = retry;
154
+ const statusRef = useRef(state.status);
155
+ statusRef.current = state.status;
156
+ const isSearchingRef = useRef(isSearching);
157
+ isSearchingRef.current = isSearching;
158
+ const handleRetryInput = useCallback((input) => {
159
+ // M2-07 (#30): "r" digitado como texto de busca não deve disparar retry.
160
+ if (isSearchingRef.current)
161
+ return;
162
+ if (input === 'r' &&
163
+ (statusRef.current === 'error-network' ||
164
+ statusRef.current === 'error-forbidden' ||
165
+ // Fix do review #120: abandono do re-login (Esc) também tem retry.
166
+ statusRef.current === 'auth-aborted')) {
167
+ retryRef.current();
168
+ }
169
+ }, []);
170
+ useInput(handleRetryInput);
171
+ // "/" abre a busca; "f" cicla o filtro rápido de status (só com a busca já
172
+ // aberta). Reason: dentro do campo de texto, "f" digitado NÃO vira
173
+ // caractere da query — é a tecla de controle do filtro, trade-off
174
+ // Com a busca ABERTA, toda letra pertence à query (buscar "workflow" exige
175
+ // digitar "f") — o ciclo de filtro por "f" só vale com a busca fechada.
176
+ const handleSearchControlInput = useCallback((input) => {
177
+ if (input === '/' && !isSearchingRef.current) {
178
+ setIsSearching(true);
179
+ return;
180
+ }
181
+ if (input === 'f' && !isSearchingRef.current) {
182
+ setStatusFilter((current) => nextStatusFilter(current));
183
+ }
184
+ }, []);
185
+ useInput(handleSearchControlInput);
186
+ // #34 (M2-11): "t" abre o painel de jobs da sessão (`./jobs.js`) — só fora
187
+ // da busca (mesma guarda de "/"/"f" acima: dentro do campo, "t" é texto da
188
+ // query, não um atalho).
189
+ const handleJobsShortcut = useCallback((input) => {
190
+ if (input === 't' && !isSearchingRef.current) {
191
+ pushRef.current('jobs');
192
+ }
193
+ }, []);
194
+ useInput(handleJobsShortcut);
195
+ const searchState = search.state;
196
+ return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "Minhas issues" }), _jsxs(Text, { color: theme.muted, children: [" [", STATUS_FILTER_LABELS[statusFilter], "]"] }), !isSearching ? (_jsx(Text, { color: theme.muted, children: ` / busca ${glyphs.middleDot} f filtro ${glyphs.middleDot} t jobs` })) : null] }), isSearching ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "Buscar: " }), _jsx(TextInput, { value: query, onChange: setQuery, placeholder: `digite para buscar${glyphs.ellipsis}`, isActive: isSearching }), _jsxs(Text, { color: theme.muted, children: [" [", STATUS_FILTER_LABELS[statusFilter], "]"] })] }), searchState.status === 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Digite para buscar ou pressione f para ciclar o filtro de status." }) })) : null, searchState.status === 'loading' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Buscando..."] }) })) : null, searchState.status === 'loaded' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [searchState.degraded ? (_jsxs(Text, { color: theme.warning, children: [symbols.warning, " ", searchState.warnings.join(' ')] })) : null, _jsx(Text, { children: searchState.content })] })) : null, searchState.status === 'error-network' || searchState.status === 'error-forbidden' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", searchState.message] }) })) : null, searchState.status === 'auth-aborted' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: searchState.message }) })) : null] })) : (_jsxs(_Fragment, { children: [state.status === 'loading' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Carregando issues..."] }) })) : null, state.status === 'empty' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "nenhuma issue atribu\u00EDda" }) })) : null, state.status === 'error-network' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.danger, children: [symbols.cross, " Falha ao carregar suas issues: ", state.message] }), _jsxs(Text, { color: theme.muted, children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "r" }), ' ', "para tentar de novo."] })] })) : null, state.status === 'error-forbidden' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", state.message] }), _jsxs(Text, { color: theme.muted, children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "r" }), ' ', "para tentar de novo."] })] })) : null, state.status === 'auth-aborted' ? (
197
+ // Fix do review #120: estado NEUTRO (abandono consciente do re-login,
198
+ // Esc) — `theme.muted`, não `theme.danger` (não é uma falha).
199
+ _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { color: theme.muted, children: state.message }) })) : null, state.status === 'loaded' ? (_jsx(Box, { marginTop: 1, flexDirection: "column", children: state.issues.map((issue, index) => (_jsx(IssueRow, { issue: issue, selected: index === selectedIndex }, issue.id))) })) : null] })), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: isSearching ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "fecha a busca,", ' ', _jsx(Text, { color: theme.accent, children: "f" }), ' ', "cicla o filtro."] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), ' ', "navega,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "abre a issue,", ' ', _jsx(Text, { color: theme.accent, children: "/" }), ' ', "busca,", ' ', _jsx(Text, { color: theme.accent, children: "t" }), ' ', "jobs,", ' ', _jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "volta."] })) }) })] }));
200
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Tela de detalhe: metadados fixos + descrição/journals em viewport rolável.
3
+ * `b` (ou `Esc`, atalho global) volta para a home — `selectedIndex` da lista
4
+ * é preservado via `./home-selection.js`.
5
+ */
6
+ export declare function IssueDetailScreen(): import("react").JSX.Element;