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.
- package/LICENSE +21 -0
- package/README.md +449 -0
- package/dist/bundle/index.d.ts +5 -0
- package/dist/bundle/index.js +5 -0
- package/dist/bundle/json.d.ts +90 -0
- package/dist/bundle/json.js +266 -0
- package/dist/bundle/markdown.d.ts +75 -0
- package/dist/bundle/markdown.js +294 -0
- package/dist/bundle/search-list.d.ts +43 -0
- package/dist/bundle/search-list.js +53 -0
- package/dist/bundle/stable-stringify.d.ts +26 -0
- package/dist/bundle/stable-stringify.js +50 -0
- package/dist/cache/contract.d.ts +157 -0
- package/dist/cache/contract.js +0 -0
- package/dist/cache/disk-index.d.ts +82 -0
- package/dist/cache/disk-index.js +220 -0
- package/dist/cache/disk.d.ts +133 -0
- package/dist/cache/disk.js +313 -0
- package/dist/cache/gc.d.ts +78 -0
- package/dist/cache/gc.js +123 -0
- package/dist/cache/get-or-compute.d.ts +36 -0
- package/dist/cache/get-or-compute.js +52 -0
- package/dist/cache/index.d.ts +9 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/keys.d.ts +76 -0
- package/dist/cache/keys.js +78 -0
- package/dist/cache/memory.d.ts +48 -0
- package/dist/cache/memory.js +110 -0
- package/dist/cache-first.d.ts +127 -0
- package/dist/cache-first.js +227 -0
- package/dist/client/errors.d.ts +33 -0
- package/dist/client/errors.js +49 -0
- package/dist/client/http.d.ts +110 -0
- package/dist/client/http.js +207 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +5 -0
- package/dist/client/issues.d.ts +71 -0
- package/dist/client/issues.js +100 -0
- package/dist/client/search.d.ts +58 -0
- package/dist/client/search.js +81 -0
- package/dist/config/credentials.d.ts +247 -0
- package/dist/config/credentials.js +427 -0
- package/dist/config/doctor.d.ts +123 -0
- package/dist/config/doctor.js +260 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +6 -0
- package/dist/config/keyring.d.ts +96 -0
- package/dist/config/keyring.js +158 -0
- package/dist/config/login.d.ts +97 -0
- package/dist/config/login.js +189 -0
- package/dist/config/settings.d.ts +94 -0
- package/dist/config/settings.js +140 -0
- package/dist/contract.d.ts +173 -0
- package/dist/contract.js +27 -0
- package/dist/core.d.ts +1 -0
- package/dist/core.js +8 -0
- package/dist/extract/audio-extractor.d.ts +105 -0
- package/dist/extract/audio-extractor.js +156 -0
- package/dist/extract/audio.d.ts +126 -0
- package/dist/extract/audio.js +184 -0
- package/dist/extract/dispatcher.d.ts +132 -0
- package/dist/extract/dispatcher.js +115 -0
- package/dist/extract/download.d.ts +111 -0
- package/dist/extract/download.js +261 -0
- package/dist/extract/duration.d.ts +106 -0
- package/dist/extract/duration.js +148 -0
- package/dist/extract/ffmpeg.d.ts +56 -0
- package/dist/extract/ffmpeg.js +95 -0
- package/dist/extract/gguf.d.ts +137 -0
- package/dist/extract/gguf.js +215 -0
- package/dist/extract/index.d.ts +19 -0
- package/dist/extract/index.js +19 -0
- package/dist/extract/magic.d.ts +80 -0
- package/dist/extract/magic.js +282 -0
- package/dist/extract/ooxml.d.ts +131 -0
- package/dist/extract/ooxml.js +336 -0
- package/dist/extract/pdf.d.ts +147 -0
- package/dist/extract/pdf.js +322 -0
- package/dist/extract/queue.d.ts +167 -0
- package/dist/extract/queue.js +217 -0
- package/dist/extract/subprocess.d.ts +145 -0
- package/dist/extract/subprocess.js +181 -0
- package/dist/extract/tesseract.d.ts +153 -0
- package/dist/extract/tesseract.js +321 -0
- package/dist/extract/video-extractor.d.ts +84 -0
- package/dist/extract/video-extractor.js +89 -0
- package/dist/extract/video.d.ts +198 -0
- package/dist/extract/video.js +418 -0
- package/dist/extract/which.d.ts +63 -0
- package/dist/extract/which.js +72 -0
- package/dist/extract/whisper-extract.d.ts +211 -0
- package/dist/extract/whisper-extract.js +323 -0
- package/dist/extract/whisper.d.ts +50 -0
- package/dist/extract/whisper.js +67 -0
- package/dist/extract/zip.d.ts +50 -0
- package/dist/extract/zip.js +165 -0
- package/dist/extract-issue-attachments.d.ts +82 -0
- package/dist/extract-issue-attachments.js +156 -0
- package/dist/fetch-attachment-text.d.ts +113 -0
- package/dist/fetch-attachment-text.js +155 -0
- package/dist/fetch-issue-bundle.d.ts +81 -0
- package/dist/fetch-issue-bundle.js +93 -0
- package/dist/fetch-issue-search.d.ts +74 -0
- package/dist/fetch-issue-search.js +120 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +67 -0
- package/dist/normalize/collections.d.ts +52 -0
- package/dist/normalize/collections.js +170 -0
- package/dist/normalize/helpers.d.ts +35 -0
- package/dist/normalize/helpers.js +59 -0
- package/dist/normalize/index.d.ts +2 -0
- package/dist/normalize/index.js +2 -0
- package/dist/normalize/issue.d.ts +34 -0
- package/dist/normalize/issue.js +154 -0
- package/dist/surfaces/cli/commands.d.ts +61 -0
- package/dist/surfaces/cli/commands.js +262 -0
- package/dist/surfaces/cli/main.d.ts +32 -0
- package/dist/surfaces/cli/main.js +210 -0
- package/dist/surfaces/cli/prompts.d.ts +66 -0
- package/dist/surfaces/cli/prompts.js +147 -0
- package/dist/surfaces/cli/tty.d.ts +27 -0
- package/dist/surfaces/cli/tty.js +35 -0
- package/dist/surfaces/cli/types.d.ts +39 -0
- package/dist/surfaces/cli/types.js +7 -0
- package/dist/surfaces/mcp/server.d.ts +171 -0
- package/dist/surfaces/mcp/server.js +427 -0
- package/dist/surfaces/tui/app.d.ts +55 -0
- package/dist/surfaces/tui/app.js +180 -0
- package/dist/surfaces/tui/attachment-status.d.ts +79 -0
- package/dist/surfaces/tui/attachment-status.js +113 -0
- package/dist/surfaces/tui/components/breadcrumb.d.ts +7 -0
- package/dist/surfaces/tui/components/breadcrumb.js +31 -0
- package/dist/surfaces/tui/components/gradient-text.d.ts +23 -0
- package/dist/surfaces/tui/components/gradient-text.js +75 -0
- package/dist/surfaces/tui/components/scroll-view.d.ts +25 -0
- package/dist/surfaces/tui/components/scroll-view.js +77 -0
- package/dist/surfaces/tui/components/spinner.d.ts +12 -0
- package/dist/surfaces/tui/components/spinner.js +39 -0
- package/dist/surfaces/tui/components/text-input.d.ts +45 -0
- package/dist/surfaces/tui/components/text-input.js +114 -0
- package/dist/surfaces/tui/format-file-size.d.ts +24 -0
- package/dist/surfaces/tui/format-file-size.js +43 -0
- package/dist/surfaces/tui/glyphs.d.ts +47 -0
- package/dist/surfaces/tui/glyphs.js +84 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.d.ts +39 -0
- package/dist/surfaces/tui/hooks/use-auth-guard.js +135 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.d.ts +64 -0
- package/dist/surfaces/tui/hooks/use-doctor-status.js +123 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.d.ts +25 -0
- package/dist/surfaces/tui/hooks/use-escape-interceptor.js +65 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.d.ts +18 -0
- package/dist/surfaces/tui/hooks/use-exit-guard.js +66 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.d.ts +62 -0
- package/dist/surfaces/tui/hooks/use-export-bundle.js +100 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.d.ts +61 -0
- package/dist/surfaces/tui/hooks/use-issue-detail.js +132 -0
- package/dist/surfaces/tui/hooks/use-issue-search.d.ts +71 -0
- package/dist/surfaces/tui/hooks/use-issue-search.js +168 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.d.ts +44 -0
- package/dist/surfaces/tui/hooks/use-list-navigation.js +82 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.d.ts +24 -0
- package/dist/surfaces/tui/hooks/use-media-binaries.js +44 -0
- package/dist/surfaces/tui/hooks/use-my-issues.d.ts +66 -0
- package/dist/surfaces/tui/hooks/use-my-issues.js +151 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.d.ts +11 -0
- package/dist/surfaces/tui/hooks/use-onboarding-callbacks.js +104 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.d.ts +52 -0
- package/dist/surfaces/tui/hooks/use-terminal-width.js +90 -0
- package/dist/surfaces/tui/index.d.ts +50 -0
- package/dist/surfaces/tui/index.js +158 -0
- package/dist/surfaces/tui/instance.d.ts +37 -0
- package/dist/surfaces/tui/instance.js +36 -0
- package/dist/surfaces/tui/job-registry.d.ts +113 -0
- package/dist/surfaces/tui/job-registry.js +123 -0
- package/dist/surfaces/tui/job-status.d.ts +45 -0
- package/dist/surfaces/tui/job-status.js +81 -0
- package/dist/surfaces/tui/navigation.d.ts +74 -0
- package/dist/surfaces/tui/navigation.js +87 -0
- package/dist/surfaces/tui/palettes.d.ts +38 -0
- package/dist/surfaces/tui/palettes.js +244 -0
- package/dist/surfaces/tui/screen.d.ts +30 -0
- package/dist/surfaces/tui/screen.js +51 -0
- package/dist/surfaces/tui/screens/about.d.ts +2 -0
- package/dist/surfaces/tui/screens/about.js +35 -0
- package/dist/surfaces/tui/screens/appearance.d.ts +2 -0
- package/dist/surfaces/tui/screens/appearance.js +74 -0
- package/dist/surfaces/tui/screens/config.d.ts +2 -0
- package/dist/surfaces/tui/screens/config.js +82 -0
- package/dist/surfaces/tui/screens/doctor.d.ts +2 -0
- package/dist/surfaces/tui/screens/doctor.js +109 -0
- package/dist/surfaces/tui/screens/export.d.ts +2 -0
- package/dist/surfaces/tui/screens/export.js +168 -0
- package/dist/surfaces/tui/screens/home-selection.d.ts +70 -0
- package/dist/surfaces/tui/screens/home-selection.js +80 -0
- package/dist/surfaces/tui/screens/home.d.ts +2 -0
- package/dist/surfaces/tui/screens/home.js +200 -0
- package/dist/surfaces/tui/screens/issue-detail.d.ts +6 -0
- package/dist/surfaces/tui/screens/issue-detail.js +182 -0
- package/dist/surfaces/tui/screens/jobs.d.ts +7 -0
- package/dist/surfaces/tui/screens/jobs.js +89 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.d.ts +49 -0
- package/dist/surfaces/tui/screens/loaded-issue-context.js +57 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/api-key.js +69 -0
- package/dist/surfaces/tui/screens/onboarding/login.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/login.js +50 -0
- package/dist/surfaces/tui/screens/onboarding/mode.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/mode.js +42 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.d.ts +221 -0
- package/dist/surfaces/tui/screens/onboarding/onboarding-context.js +131 -0
- package/dist/surfaces/tui/screens/onboarding/success.d.ts +2 -0
- package/dist/surfaces/tui/screens/onboarding/success.js +41 -0
- package/dist/surfaces/tui/screens/onboarding/url.d.ts +24 -0
- package/dist/surfaces/tui/screens/onboarding/url.js +84 -0
- package/dist/surfaces/tui/screens/onboarding/validating.d.ts +1 -0
- package/dist/surfaces/tui/screens/onboarding/validating.js +86 -0
- package/dist/surfaces/tui/screens/welcome.d.ts +6 -0
- package/dist/surfaces/tui/screens/welcome.js +95 -0
- package/dist/surfaces/tui/status-color.d.ts +21 -0
- package/dist/surfaces/tui/status-color.js +23 -0
- package/dist/surfaces/tui/symbols.d.ts +231 -0
- package/dist/surfaces/tui/symbols.js +14 -0
- package/dist/surfaces/tui/terminal-colors.d.ts +29 -0
- package/dist/surfaces/tui/terminal-colors.js +39 -0
- package/dist/surfaces/tui/theme.d.ts +156 -0
- package/dist/surfaces/tui/theme.js +86 -0
- package/dist/surfaces/tui/truncate.d.ts +31 -0
- package/dist/surfaces/tui/truncate.js +81 -0
- package/package.json +93 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tela de detalhe de issue (#31) — substitui o placeholder mínimo da #29.
|
|
4
|
+
*
|
|
5
|
+
* Carrega a issue completa via `../hooks/use-issue-detail.js`
|
|
6
|
+
* (`getIssue`+`normalizeIssue` do core, DIRETO — sem passar pelo bundle
|
|
7
|
+
* Markdown/JSON, que é o formato de saída da CLI/MCP, não o modelo em
|
|
8
|
+
* memória que esta tela precisa para paginar/rolar o conteúdo). A issue a
|
|
9
|
+
* buscar vem de `./home-selection.js` (`selectedIssueId`, escrito pela home
|
|
10
|
+
* ao empilhar esta tela — ver o JSDoc daquele módulo para o porquê de um
|
|
11
|
+
* contexto compartilhado em vez de props).
|
|
12
|
+
*
|
|
13
|
+
* Layout: metadados (status/prioridade/autor/responsável/datas) FIXOS no
|
|
14
|
+
* topo — nunca rolam — e descrição + histórico de journals dentro de um
|
|
15
|
+
* `../components/scroll-view.tsx` (viewport de altura fixa,
|
|
16
|
+
* `j`/`k`/setas/PgUp/PgDn), para que a tela não quebre o layout mesmo com uma
|
|
17
|
+
* issue com dezenas de journals.
|
|
18
|
+
*
|
|
19
|
+
* `Esc` (atalho global, `../app.tsx`) desempilha normalmente — a home é
|
|
20
|
+
* remontada, mas `selectedIndex` é preservado via `./home-selection.js`
|
|
21
|
+
* (documentado ali). `b` é o atalho local, mesmo padrão de
|
|
22
|
+
* `doctor.tsx`/`config.tsx`.
|
|
23
|
+
*
|
|
24
|
+
* 401 é responsabilidade de `useAuthGuard`, já embutido em
|
|
25
|
+
* `use-issue-detail.js` (mandatório, M2-13/#36) — esta tela só formata o
|
|
26
|
+
* estado `auth-aborted` (neutro) igual aos demais banners de erro.
|
|
27
|
+
*
|
|
28
|
+
* Anexos (#32): seção "Anexos" ao FIM do mesmo `ScrollView` — nome, tamanho
|
|
29
|
+
* humanizado (`../format-file-size.js`) e badge de status
|
|
30
|
+
* (`../attachment-status.js`); mesma rolagem, sem bloquear navegação.
|
|
31
|
+
*
|
|
32
|
+
* `e` (issue carregada) empilha a tela de exportação (#33, `./export.js`):
|
|
33
|
+
* a issue normalizada é espelhada em `./loaded-issue-context.js` para a
|
|
34
|
+
* exportação REUSAR a issue em memória (ver JSDoc daquele módulo).
|
|
35
|
+
*/
|
|
36
|
+
import { Box, Text, useInput } from 'ink';
|
|
37
|
+
import { useEffect, useRef } from 'react';
|
|
38
|
+
import { attachmentStatusColor, attachmentStatusLabel, deriveAttachmentExtractionStatus, } from '../attachment-status.js';
|
|
39
|
+
import { Spinner } from '../components/spinner.js';
|
|
40
|
+
import { ScrollView } from '../components/scroll-view.js';
|
|
41
|
+
import { glyphs } from '../glyphs.js';
|
|
42
|
+
import { humanizeFileSize } from '../format-file-size.js';
|
|
43
|
+
import { useIssueDetail } from '../hooks/use-issue-detail.js';
|
|
44
|
+
import { useTerminalHeight } from '../hooks/use-terminal-width.js';
|
|
45
|
+
import { useNavigation } from '../navigation.js';
|
|
46
|
+
import { statusColor } from '../status-color.js';
|
|
47
|
+
import { symbols } from '../symbols.js';
|
|
48
|
+
import { useTheme } from '../theme.js';
|
|
49
|
+
import { useHomeSelection } from './home-selection.js';
|
|
50
|
+
import { useLoadedIssue } from './loaded-issue-context.js';
|
|
51
|
+
/**
|
|
52
|
+
* Overhead de linhas FORA da viewport de descrição (breadcrumb + título + meta +
|
|
53
|
+
* rodapé + paddings). A viewport cresce com o terminal: `altura - OVERHEAD`
|
|
54
|
+
* (#190). Com a altura default dos testes (24) dá 10 — o valor histórico — então
|
|
55
|
+
* os snapshots/asserções de rolagem seguem iguais; em terminal real a descrição
|
|
56
|
+
* usa quase toda a tela e só rola quando passa DE VERDADE.
|
|
57
|
+
*/
|
|
58
|
+
const CONTENT_OVERHEAD_ROWS = 14;
|
|
59
|
+
/** Piso da viewport (terminais muito baixos). */
|
|
60
|
+
const CONTENT_MIN_HEIGHT = 6;
|
|
61
|
+
/** Placeholder discreto para campos ausentes (assignee, autor/data de journal, old/new value). ASCII no Windows legado (#84). */
|
|
62
|
+
const EMPTY_PLACEHOLDER = glyphs.emptyPlaceholder;
|
|
63
|
+
/** Metadados fixos no topo: id/subject, status/prioridade, autor/responsável, datas. */
|
|
64
|
+
function IssueMeta({ issue, theme }) {
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.primary, children: ["#", issue.id, " ", issue.subject] }), _jsxs(Box, { children: [_jsxs(Text, { color: statusColor(theme, issue.status.name), children: ["[", issue.status.name, "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: theme.accent, children: issue.priority.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Autor: " }), _jsx(Text, { children: issue.author.name }), _jsx(Text, { color: theme.muted, children: " Respons\u00E1vel: " }), _jsx(Text, { children: issue.assigned_to?.name ?? EMPTY_PLACEHOLDER })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "Criada em: " }), _jsx(Text, { children: issue.created_on }), _jsx(Text, { color: theme.muted, children: " Atualizada em: " }), _jsx(Text, { children: issue.updated_on })] })] }));
|
|
66
|
+
}
|
|
67
|
+
/** Constrói a linha de um único anexo: nome (texto derivado, puro) · tamanho humanizado · content_type · badge de status. */
|
|
68
|
+
function buildAttachmentRow(attachment, theme) {
|
|
69
|
+
const status = deriveAttachmentExtractionStatus(attachment);
|
|
70
|
+
return (_jsxs(Text, { children: [attachment.filename, ' ', _jsxs(Text, { color: theme.muted, children: ["(", humanizeFileSize(attachment.filesize), " ", glyphs.middleDot, " ", attachment.content_type ?? EMPTY_PLACEHOLDER, ")"] }), ' ', _jsxs(Text, { color: attachmentStatusColor(theme, status), children: ["[", attachmentStatusLabel(status), "]"] })] }, `attachment-${attachment.id}`));
|
|
71
|
+
}
|
|
72
|
+
/** Constrói as linhas da seção de anexos (após os journals): cabeçalho + 1 linha por anexo, ou "(nenhum)". */
|
|
73
|
+
function buildAttachmentRows(issue, theme) {
|
|
74
|
+
const rows = [
|
|
75
|
+
_jsx(Text, { color: theme.primary, children: "Anexos" }, "attachments-header"),
|
|
76
|
+
];
|
|
77
|
+
if (issue.attachments.length === 0) {
|
|
78
|
+
rows.push(_jsx(Text, { color: theme.muted, children: "(nenhum)" }, "attachments-empty"));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
issue.attachments.forEach((attachment) => {
|
|
82
|
+
rows.push(buildAttachmentRow(attachment, theme));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return rows;
|
|
86
|
+
}
|
|
87
|
+
/** Constrói as linhas da viewport rolável: bloco de descrição + histórico cronológico de journals. */
|
|
88
|
+
function buildContentRows(issue, theme) {
|
|
89
|
+
const rows = [];
|
|
90
|
+
rows.push(_jsx(Text, { color: theme.primary, children: "Descri\u00E7\u00E3o" }, "desc-header"));
|
|
91
|
+
if (issue.description === undefined || issue.description === '') {
|
|
92
|
+
rows.push(_jsx(Text, { color: theme.muted, children: "(sem descri\u00E7\u00E3o)" }, "desc-empty"));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
issue.description.split('\n').forEach((line, index) => {
|
|
96
|
+
rows.push(_jsx(Text, { children: line.length > 0 ? line : ' ' }, `desc-${index}`));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
rows.push(_jsx(Text, { children: " " }, "desc-spacer"));
|
|
100
|
+
rows.push(_jsx(Text, { color: theme.primary, children: "Hist\u00F3rico" }, "journals-header"));
|
|
101
|
+
if (issue.journals.length === 0) {
|
|
102
|
+
rows.push(_jsx(Text, { color: theme.muted, children: "(nenhuma atualiza\u00E7\u00E3o registrada)" }, "journals-empty"));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Cronológico: preserva a ordem já entregue por `getIssue`/`normalizeIssue`.
|
|
106
|
+
issue.journals.forEach((journal) => {
|
|
107
|
+
rows.push(_jsxs(Text, { color: theme.muted, children: [journal.user?.name ?? EMPTY_PLACEHOLDER, " ", glyphs.middleDot, " ", journal.created_on] }, `journal-${journal.id}-head`));
|
|
108
|
+
if (journal.notes !== undefined && journal.notes !== '') {
|
|
109
|
+
journal.notes.split('\n').forEach((line, index) => {
|
|
110
|
+
rows.push(_jsx(Text, { children: line }, `journal-${journal.id}-note-${index}`));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Details estruturais resumidos: `campo: antigo → novo`, sempre em muted.
|
|
114
|
+
journal.details.forEach((detail, index) => {
|
|
115
|
+
rows.push(_jsxs(Text, { color: theme.muted, children: [' ', detail.name, ": ", detail.old_value ?? EMPTY_PLACEHOLDER, " ", symbols.arrowRight, ' ', detail.new_value ?? EMPTY_PLACEHOLDER] }, `journal-${journal.id}-detail-${index}`));
|
|
116
|
+
});
|
|
117
|
+
rows.push(_jsx(Text, { children: " " }, `journal-${journal.id}-spacer`));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
rows.push(_jsx(Text, { children: " " }, "journals-attachments-spacer"));
|
|
121
|
+
// Anexos (#32) SEMPRE por último — após journals, dentro do mesmo ScrollView.
|
|
122
|
+
rows.push(...buildAttachmentRows(issue, theme));
|
|
123
|
+
return rows;
|
|
124
|
+
}
|
|
125
|
+
/** Rodapé com os atalhos disponíveis nesta tela. */
|
|
126
|
+
function Footer({ showRetry, showExport }) {
|
|
127
|
+
const theme = useTheme();
|
|
128
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [showRetry ? (_jsxs(_Fragment, { children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "r" }), ' ', "para tentar de novo,", ' '] })) : null, showExport ? (_jsxs(_Fragment, { children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "e" }), ' ', "para exportar,", ' '] })) : null, "Pressione", ' ', _jsx(Text, { color: theme.accent, children: "t" }), ' ', "para ver os jobs,", ' ', _jsx(Text, { color: theme.accent, children: "b" }), ' ', "para voltar."] }) }));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Tela de detalhe: metadados fixos + descrição/journals em viewport rolável.
|
|
132
|
+
* `b` (ou `Esc`, atalho global) volta para a home — `selectedIndex` da lista
|
|
133
|
+
* é preservado via `./home-selection.js`.
|
|
134
|
+
*/
|
|
135
|
+
export function IssueDetailScreen() {
|
|
136
|
+
const { pop, push } = useNavigation();
|
|
137
|
+
const theme = useTheme();
|
|
138
|
+
const { selectedIssueId } = useHomeSelection();
|
|
139
|
+
const { state, retry } = useIssueDetail(selectedIssueId);
|
|
140
|
+
const { setIssue } = useLoadedIssue();
|
|
141
|
+
// Viewport de descrição CRESCE com o terminal (#190) — só rola quando o conteúdo
|
|
142
|
+
// passa da tela de verdade, em vez de uma janelinha fixa com espaço vazio embaixo.
|
|
143
|
+
const contentHeight = Math.max(CONTENT_MIN_HEIGHT, useTerminalHeight() - CONTENT_OVERHEAD_ROWS);
|
|
144
|
+
// Espelha a issue carregada no contexto leve consumido por `./export.js`
|
|
145
|
+
// (#33) — ver o JSDoc do módulo e de `./loaded-issue-context.js` para a
|
|
146
|
+
// escolha de reuso em memória em vez de refetch.
|
|
147
|
+
const setIssueRef = useRef(setIssue);
|
|
148
|
+
setIssueRef.current = setIssue;
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (state.status === 'loaded') {
|
|
151
|
+
setIssueRef.current(state.issue);
|
|
152
|
+
}
|
|
153
|
+
}, [state]);
|
|
154
|
+
useInput((input) => {
|
|
155
|
+
if (input === 'b') {
|
|
156
|
+
pop();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (input === 'e' && state.status === 'loaded') {
|
|
160
|
+
push('export');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// #34 (M2-11): "t" abre o painel de jobs da sessão (`./jobs.js`) — em
|
|
164
|
+
// qualquer estado (loading/erro/loaded), sem restrição, ao contrário de
|
|
165
|
+
// "e" (exportar), que só faz sentido com a issue já carregada.
|
|
166
|
+
if (input === 't') {
|
|
167
|
+
push('jobs');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (input === 'r' &&
|
|
171
|
+
(state.status === 'error-network' ||
|
|
172
|
+
state.status === 'error-forbidden' ||
|
|
173
|
+
state.status === 'error-not-found' ||
|
|
174
|
+
state.status === 'auth-aborted')) {
|
|
175
|
+
retry();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.primary, children: "Detalhe da issue" }), state.status === 'no-selection' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Nenhuma issue selecionada." }) })) : null, state.status === 'loading' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, {}), " Carregando issue..."] }) })) : null, state.status === 'error-network' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.danger, children: [symbols.cross, " Falha ao carregar a issue: ", state.message] }) })) : null, state.status === 'error-forbidden' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", state.message] }) })) : null, state.status === 'error-not-found' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", state.message] }) })) : null, state.status === 'auth-aborted' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: state.message }) })) : null, state.status === 'loaded' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(IssueMeta, { issue: state.issue, theme: theme }), _jsx(Box, { marginTop: 1, children: _jsx(ScrollView, { lines: buildContentRows(state.issue, theme), height: contentHeight }) })] })) : null, _jsx(Box, { flexGrow: 1 }), _jsx(Footer, { showRetry: state.status === 'error-network' ||
|
|
179
|
+
state.status === 'error-forbidden' ||
|
|
180
|
+
state.status === 'error-not-found' ||
|
|
181
|
+
state.status === 'auth-aborted', showExport: state.status === 'loaded' })] }));
|
|
182
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tela do painel de jobs: lista as operações da sessão (`../job-registry.tsx`),
|
|
3
|
+
* `↑/↓`/`j`/`k` navegam a seleção, `Ctrl+C` cancela o job selecionado (só se
|
|
4
|
+
* `cancelable`, ver o JSDoc de `../job-registry.tsx`), `b` (ou `Esc`, atalho
|
|
5
|
+
* global) volta.
|
|
6
|
+
*/
|
|
7
|
+
export declare function JobsScreen(): import("react").JSX.Element;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Painel de jobs da sessão (#34/M2-11) — empilhado a partir da home
|
|
4
|
+
* (`./home.tsx`) ou do detalhe (`./issue-detail.tsx`) com a tecla `t`
|
|
5
|
+
* ("tarefas"), disponível em qualquer estado das duas telas.
|
|
6
|
+
*
|
|
7
|
+
* Lista as operações registradas em `../job-registry.tsx` (ver o JSDoc
|
|
8
|
+
* daquele módulo para o contrato completo) — hoje só a exportação de bundle
|
|
9
|
+
* (`./export.tsx`, #33) é um produtor real; a tela já está pronta para
|
|
10
|
+
* qualquer job futuro do M3/M4 sem mudança de contrato.
|
|
11
|
+
*
|
|
12
|
+
* ## `ink-task-list`: incompatibilidade verificada, componente próprio
|
|
13
|
+
*
|
|
14
|
+
* A AC pedia `ink-task-list` (já na dependência do projeto) OU um componente
|
|
15
|
+
* próprio "se a lib conflitar com ink@6". Testado manualmente antes de
|
|
16
|
+
* qualquer implementação (`ink-testing-library` + `ink-task-list@2.0.0`):
|
|
17
|
+
*
|
|
18
|
+
* 1. **Sem export default** — só exporta `{ Task, TaskList }` nomeados (o
|
|
19
|
+
* típico `import TaskList, { Task } from 'ink-task-list'` sugerido pela
|
|
20
|
+
* doc da lib quebra com `ink@6`/ESM estrito).
|
|
21
|
+
* 2. **Cores hardcoded internas** (amarelo/vermelho/verde/cinza fixos no
|
|
22
|
+
* próprio bundle da lib) — incompatível com a AC "cores do tema": a
|
|
23
|
+
* varredura anti-hardcode do repo (`tests/surfaces/tui/no-hardcoded-colors.test.ts`)
|
|
24
|
+
* só cobre `src/surfaces/tui`, então isso não quebraria o CI, mas o
|
|
25
|
+
* resultado visual NUNCA respeitaria um tema customizado — justamente o
|
|
26
|
+
* problema que `theme.ts` existe para resolver.
|
|
27
|
+
* 3. O estado `'loading'` da lib **exige** um prop `spinner` explícito
|
|
28
|
+
* (`{ frames, interval }`, sem default) — omiti-lo quebra o render com
|
|
29
|
+
* `Cannot read properties of undefined (reading 'frames')`. Contornável
|
|
30
|
+
* passando um objeto de frames à mão, mas é mais um sinal de uma lib
|
|
31
|
+
* parada na era do ink@3 (suas `devDependencies` fixam `ink@^3.2.0` e
|
|
32
|
+
* `react@^17.0.2`).
|
|
33
|
+
*
|
|
34
|
+
* Dado o conflito real com a AC de tema (item 2) e a fragilidade do item 3,
|
|
35
|
+
* a escolha foi um componente próprio, reaproveitando peças já existentes:
|
|
36
|
+
* `../symbols.ts` (ícones, com fallback ASCII automático) para os estados
|
|
37
|
+
* estáticos e o `Spinner` (`../components/spinner.tsx`) já usado por
|
|
38
|
+
* `./export.tsx`/`./home.tsx` para `processing` — ver `../job-status.ts` para
|
|
39
|
+
* o mapeamento ícone/cor por status.
|
|
40
|
+
*/
|
|
41
|
+
import { Box, Text, useInput } from 'ink';
|
|
42
|
+
import { useRef } from 'react';
|
|
43
|
+
import { Spinner } from '../components/spinner.js';
|
|
44
|
+
import { glyphs } from '../glyphs.js';
|
|
45
|
+
import { useListNavigation } from '../hooks/use-list-navigation.js';
|
|
46
|
+
import { useJobRegistry } from '../job-registry.js';
|
|
47
|
+
import { jobStatusColor, jobStatusIcon, jobStatusLabel } from '../job-status.js';
|
|
48
|
+
import { useNavigation } from '../navigation.js';
|
|
49
|
+
import { symbols } from '../symbols.js';
|
|
50
|
+
import { useTheme } from '../theme.js';
|
|
51
|
+
/** Uma linha do painel: ícone/spinner de status, rótulo, badge textual e, quando selecionada, o hint de cancelamento. */
|
|
52
|
+
function JobRow({ job, selected, theme }) {
|
|
53
|
+
const icon = jobStatusIcon(job.status);
|
|
54
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: selected ? `${symbols.pointerSmall} ` : ' ' }), job.status === 'processing' ? (_jsx(Spinner, {})) : (_jsx(Text, { color: jobStatusColor(theme, job.status), children: icon })), _jsx(Text, { children: " " }), _jsx(Text, { ...(selected ? { color: theme.primary } : {}), children: job.label }), _jsxs(Text, { color: theme.muted, children: [" [", jobStatusLabel(job.status), "]"] }), selected ? (_jsxs(Text, { color: theme.muted, children: [' ', job.cancelable === true ? '(Ctrl+C cancela)' : '(não cancelável)'] })) : null] }));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Tela do painel de jobs: lista as operações da sessão (`../job-registry.tsx`),
|
|
58
|
+
* `↑/↓`/`j`/`k` navegam a seleção, `Ctrl+C` cancela o job selecionado (só se
|
|
59
|
+
* `cancelable`, ver o JSDoc de `../job-registry.tsx`), `b` (ou `Esc`, atalho
|
|
60
|
+
* global) volta.
|
|
61
|
+
*/
|
|
62
|
+
export function JobsScreen() {
|
|
63
|
+
const theme = useTheme();
|
|
64
|
+
const { pop } = useNavigation();
|
|
65
|
+
const { jobs } = useJobRegistry();
|
|
66
|
+
const { selectedIndex } = useListNavigation(jobs.length);
|
|
67
|
+
// Handlers ESTÁVEIS (refs + useCallback via closures simples aqui, padrão
|
|
68
|
+
// do repo): identidade nova a cada render des/re-subscreve o useInput e
|
|
69
|
+
// pode perder uma tecla rápida entre pressões próximas.
|
|
70
|
+
const popRef = useRef(pop);
|
|
71
|
+
popRef.current = pop;
|
|
72
|
+
const jobsRef = useRef(jobs);
|
|
73
|
+
jobsRef.current = jobs;
|
|
74
|
+
const selectedIndexRef = useRef(selectedIndex);
|
|
75
|
+
selectedIndexRef.current = selectedIndex;
|
|
76
|
+
useInput((input, key) => {
|
|
77
|
+
if (input === 'b') {
|
|
78
|
+
popRef.current();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.ctrl && input === 'c') {
|
|
82
|
+
const selectedJob = jobsRef.current[selectedIndexRef.current];
|
|
83
|
+
if (selectedJob?.cancelable === true) {
|
|
84
|
+
selectedJob.onCancel?.();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: theme.primary, children: "Jobs da sess\u00E3o" }), jobs.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Nenhum job nesta sess\u00E3o ainda." }) })) : (_jsx(Box, { marginTop: 1, flexDirection: "column", children: jobs.map((job, index) => (_jsx(JobRow, { job: job, selected: index === selectedIndex, theme: theme }, job.id))) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}/${glyphs.arrowDown}` }), ' ', "navega,", ' ', _jsx(Text, { color: theme.accent, children: "Ctrl+C" }), ' ', "cancela o job selecionado (se cancel\u00E1vel),", ' ', _jsx(Text, { color: theme.accent, children: "b" }), ' ', "volta."] }) })] }));
|
|
89
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue em memória compartilhada entre `./issue-detail.tsx` e `./export.tsx`
|
|
3
|
+
* (#33), mesmo padrão de `./home-selection.tsx` (contexto leve, instanciado
|
|
4
|
+
* uma vez em `../app.tsx`, sobrevivendo ao unmount de cada tela).
|
|
5
|
+
*
|
|
6
|
+
* ## Por que este contexto existe (escolha de reuso documentada, AC da #33)
|
|
7
|
+
*
|
|
8
|
+
* A tela de exportação precisa da MESMA {@link Issue} já carregada pelo
|
|
9
|
+
* detalhe para empacotar via `buildMarkdownBundle`/`buildJsonBundle`
|
|
10
|
+
* (`../../../index.js`). Duas opções foram consideradas:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Reusar a issue em memória** (escolhida): `./issue-detail.tsx` grava a
|
|
13
|
+
* issue aqui assim que `useIssueDetail` resolve para `loaded`; `./export.tsx`
|
|
14
|
+
* lê daqui e chama os builders DIRETO — sem HTTP nem normalização de novo.
|
|
15
|
+
* 2. **Refetch simples**: `./export.tsx` chamaria `fetchIssueBundle` (ou o
|
|
16
|
+
* par `getIssue`+`normalizeIssue`) de novo, ignorando que o detalhe acabou
|
|
17
|
+
* de buscar a mesma issue segundos antes.
|
|
18
|
+
*
|
|
19
|
+
* (1) evita uma segunda chamada de rede redundante (a issue não muda entre o
|
|
20
|
+
* usuário abrir o detalhe e pressionar `e`) e é consistente com a decisão já
|
|
21
|
+
* tomada pela #31 de manter o modelo `Issue` em memória em vez de só um bundle
|
|
22
|
+
* serializado. O roteador (`../app.tsx`) só mantém montado o TOPO da pilha —
|
|
23
|
+
* ao empilhar `export` a partir do detalhe, `IssueDetailScreen` é desmontada,
|
|
24
|
+
* então a issue precisa sobreviver em algo ACIMA da pilha, daí o contexto
|
|
25
|
+
* (mesmo raciocínio do `HomeSelectionProvider`, ver o JSDoc daquele módulo).
|
|
26
|
+
*/
|
|
27
|
+
import { type ReactNode } from 'react';
|
|
28
|
+
import type { Issue } from '../../../index.js';
|
|
29
|
+
/** Valor exposto pelo contexto da issue em memória. */
|
|
30
|
+
export interface LoadedIssueValue {
|
|
31
|
+
/** Última issue carregada por `./issue-detail.tsx`, ou `undefined` antes de qualquer carga. */
|
|
32
|
+
issue: Issue | undefined;
|
|
33
|
+
/** Registra a issue carregada — chamado pelo detalhe assim que `useIssueDetail` resolve para `loaded`. */
|
|
34
|
+
setIssue: (issue: Issue) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Provider do contexto de issue em memória — instanciado uma vez em
|
|
38
|
+
* `../app.tsx`, envolvendo toda a árvore de telas (ver o JSDoc do módulo).
|
|
39
|
+
*/
|
|
40
|
+
export declare function LoadedIssueProvider({ children }: {
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* Hook de issue em memória consumido por `./issue-detail.tsx`/`./export.tsx`.
|
|
45
|
+
*
|
|
46
|
+
* @throws {Error} Se chamado fora de um `<LoadedIssueProvider>` — indica erro
|
|
47
|
+
* de composição (tela renderizada sem passar pelo roteador).
|
|
48
|
+
*/
|
|
49
|
+
export declare function useLoadedIssue(): LoadedIssueValue;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Issue em memória compartilhada entre `./issue-detail.tsx` e `./export.tsx`
|
|
4
|
+
* (#33), mesmo padrão de `./home-selection.tsx` (contexto leve, instanciado
|
|
5
|
+
* uma vez em `../app.tsx`, sobrevivendo ao unmount de cada tela).
|
|
6
|
+
*
|
|
7
|
+
* ## Por que este contexto existe (escolha de reuso documentada, AC da #33)
|
|
8
|
+
*
|
|
9
|
+
* A tela de exportação precisa da MESMA {@link Issue} já carregada pelo
|
|
10
|
+
* detalhe para empacotar via `buildMarkdownBundle`/`buildJsonBundle`
|
|
11
|
+
* (`../../../index.js`). Duas opções foram consideradas:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Reusar a issue em memória** (escolhida): `./issue-detail.tsx` grava a
|
|
14
|
+
* issue aqui assim que `useIssueDetail` resolve para `loaded`; `./export.tsx`
|
|
15
|
+
* lê daqui e chama os builders DIRETO — sem HTTP nem normalização de novo.
|
|
16
|
+
* 2. **Refetch simples**: `./export.tsx` chamaria `fetchIssueBundle` (ou o
|
|
17
|
+
* par `getIssue`+`normalizeIssue`) de novo, ignorando que o detalhe acabou
|
|
18
|
+
* de buscar a mesma issue segundos antes.
|
|
19
|
+
*
|
|
20
|
+
* (1) evita uma segunda chamada de rede redundante (a issue não muda entre o
|
|
21
|
+
* usuário abrir o detalhe e pressionar `e`) e é consistente com a decisão já
|
|
22
|
+
* tomada pela #31 de manter o modelo `Issue` em memória em vez de só um bundle
|
|
23
|
+
* serializado. O roteador (`../app.tsx`) só mantém montado o TOPO da pilha —
|
|
24
|
+
* ao empilhar `export` a partir do detalhe, `IssueDetailScreen` é desmontada,
|
|
25
|
+
* então a issue precisa sobreviver em algo ACIMA da pilha, daí o contexto
|
|
26
|
+
* (mesmo raciocínio do `HomeSelectionProvider`, ver o JSDoc daquele módulo).
|
|
27
|
+
*/
|
|
28
|
+
import { createContext, useCallback, useContext, useState } from 'react';
|
|
29
|
+
const LoadedIssueContext = createContext(undefined);
|
|
30
|
+
/**
|
|
31
|
+
* Provider do contexto de issue em memória — instanciado uma vez em
|
|
32
|
+
* `../app.tsx`, envolvendo toda a árvore de telas (ver o JSDoc do módulo).
|
|
33
|
+
*/
|
|
34
|
+
export function LoadedIssueProvider({ children }) {
|
|
35
|
+
const [issue, setIssueState] = useState(undefined);
|
|
36
|
+
// Identidade ESTÁVEL (useCallback sem deps, padrão do repo) — evita
|
|
37
|
+
// resubscrições desnecessárias em quem a usa como dependência de efeito
|
|
38
|
+
// (ex.: `./issue-detail.tsx`).
|
|
39
|
+
const setIssue = useCallback((next) => {
|
|
40
|
+
setIssueState(next);
|
|
41
|
+
}, []);
|
|
42
|
+
const value = { issue, setIssue };
|
|
43
|
+
return _jsx(LoadedIssueContext.Provider, { value: value, children: children });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Hook de issue em memória consumido por `./issue-detail.tsx`/`./export.tsx`.
|
|
47
|
+
*
|
|
48
|
+
* @throws {Error} Se chamado fora de um `<LoadedIssueProvider>` — indica erro
|
|
49
|
+
* de composição (tela renderizada sem passar pelo roteador).
|
|
50
|
+
*/
|
|
51
|
+
export function useLoadedIssue() {
|
|
52
|
+
const value = useContext(LoadedIssueContext);
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
throw new Error('useLoadedIssue() usado fora de <LoadedIssueProvider>.');
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tela de onboarding: colar api_key diretamente (#28) — alcançável de dois
|
|
4
|
+
* jeitos: (1) escolha direta na tela de modo (`mode.tsx`, opção "Colar uma
|
|
5
|
+
* api_key"), ou (2) fallback automático quando o login por senha falha com
|
|
6
|
+
* 401/2FA (`validating.tsx` faz `replace('onboarding-api-key')`, ADR-003).
|
|
7
|
+
*
|
|
8
|
+
* Campo único mascarado (mesmo `TextInput` do login por senha, `mask="•"` —
|
|
9
|
+
* a key nunca é ecoada em texto). Ao confirmar, a key é VALIDADA contra
|
|
10
|
+
* `GET /users/current.json` (via `callbacks.onApiKeySubmit`, wiring real em
|
|
11
|
+
* `../../hooks/use-onboarding-callbacks.ts`) ANTES de ser persistida —
|
|
12
|
+
* mesma disciplina do login por senha (nunca salva sem confirmar que a
|
|
13
|
+
* credencial funciona). Estado assíncrono gerenciado localmente (mesmo
|
|
14
|
+
* padrão de `../config.tsx` no fluxo de logout): idle → validating → error,
|
|
15
|
+
* com spinner próprio (`../../components/spinner.tsx`) durante a validação.
|
|
16
|
+
*
|
|
17
|
+
* M2-13 (issue #36): esta tela também é o fallback de 2FA (`validating.tsx`,
|
|
18
|
+
* `replace('onboarding-api-key')`) quando o login por senha de um re-auth
|
|
19
|
+
* falha com 401 — então o sucesso aqui também precisa checar
|
|
20
|
+
* `OnboardingContext.reAuth` e, se definido, voltar à tela de origem
|
|
21
|
+
* (`popTo`) e retomar a operação original (`resolveReAuth()`) em vez de
|
|
22
|
+
* seguir para a splash.
|
|
23
|
+
*/
|
|
24
|
+
import { Box, Text } from 'ink';
|
|
25
|
+
import { useState } from 'react';
|
|
26
|
+
import { Spinner } from '../../components/spinner.js';
|
|
27
|
+
import { TextInput } from '../../components/text-input.js';
|
|
28
|
+
import { glyphs } from '../../glyphs.js';
|
|
29
|
+
import { useNavigation } from '../../navigation.js';
|
|
30
|
+
import { symbols } from '../../symbols.js';
|
|
31
|
+
import { useTheme } from '../../theme.js';
|
|
32
|
+
import { useOnboarding } from './onboarding-context.js';
|
|
33
|
+
/** Tela de onboarding: colar api_key — Enter valida e confirma, Esc volta (atalho global, ver `../../app.tsx`). */
|
|
34
|
+
export function OnboardingApiKeyScreen() {
|
|
35
|
+
const theme = useTheme();
|
|
36
|
+
const { replace, popTo } = useNavigation();
|
|
37
|
+
const { callbacks, setUser, reAuth, resolveReAuth } = useOnboarding();
|
|
38
|
+
const [apiKey, setApiKey] = useState('');
|
|
39
|
+
const [status, setStatus] = useState('idle');
|
|
40
|
+
const [errorMessage, setErrorMessage] = useState(undefined);
|
|
41
|
+
function handleSubmit(value) {
|
|
42
|
+
setStatus('validating');
|
|
43
|
+
setErrorMessage(undefined);
|
|
44
|
+
callbacks.onApiKeySubmit(value).then((outcome) => {
|
|
45
|
+
if (outcome.kind === 'success') {
|
|
46
|
+
setUser(outcome.user);
|
|
47
|
+
if (reAuth !== undefined) {
|
|
48
|
+
// M2-13 (#36): volta à tela de origem e retoma a operação
|
|
49
|
+
// original — ver o mesmo desvio em `validating.tsx`.
|
|
50
|
+
popTo(reAuth.origin);
|
|
51
|
+
resolveReAuth();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
replace('onboarding-success');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setStatus('error');
|
|
58
|
+
setErrorMessage(outcome.message);
|
|
59
|
+
}, (cause) => {
|
|
60
|
+
// Reason: defesa em profundidade — `onApiKeySubmit` (ver
|
|
61
|
+
// `../../hooks/use-onboarding-callbacks.ts`) já converte qualquer
|
|
62
|
+
// falha num `OnboardingLoginOutcome` resolvido; isso só protege
|
|
63
|
+
// contra um callback injetado incorretamente (ex.: em teste).
|
|
64
|
+
setStatus('error');
|
|
65
|
+
setErrorMessage(cause instanceof Error ? cause.message : String(cause));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, borderStyle: "round", borderColor: theme.border, children: [_jsx(Text, { color: theme.primary, children: "Colar api_key" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.muted, children: [symbols.pointer, " api_key: "] }), _jsx(TextInput, { value: apiKey, onChange: setApiKey, onSubmit: handleSubmit, mask: glyphs.maskBullet, isActive: status !== 'validating' })] }), status === 'validating' ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.primary, children: [_jsx(Spinner, {}), " Validando api_key..."] }) })) : null, status === 'error' && errorMessage !== undefined ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.danger, children: [symbols.cross, " ", errorMessage] }) })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: ["Pressione", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "para confirmar,", ' ', _jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "para voltar."] }) })] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tela de onboarding 3/3: login por usuário e senha (M2-05.1, issue #27).
|
|
4
|
+
*
|
|
5
|
+
* Dois campos (`TextInput`, `../../components/text-input.tsx`): usuário com
|
|
6
|
+
* eco normal, senha mascarada (`mask="•"`, nunca ecoada em texto — ver
|
|
7
|
+
* `text-input.tsx`). `Tab`/setas alternam o campo ativo — o `useInput()` do
|
|
8
|
+
* Ink não tem noção de foco, então só um `TextInput` fica `isActive` por
|
|
9
|
+
* vez, senão a mesma tecla digitaria nos dois campos ao mesmo tempo. Enter
|
|
10
|
+
* no campo de usuário avança para a senha; Enter na senha confirma o
|
|
11
|
+
* formulário: dispara `callbacks.onLoginSubmit` (wiring real a
|
|
12
|
+
* `loginWithPassword` do core em `../../hooks/use-onboarding-callbacks.ts`,
|
|
13
|
+
* #28), guarda a `Promise` resultante em `pendingLogin` (`OnboardingContext`)
|
|
14
|
+
* e navega para `onboarding-validating`, que consome essa promise e decide a
|
|
15
|
+
* próxima tela (splash de sucesso, fallback de api_key em 401/2FA, ou
|
|
16
|
+
* mensagem de erro de rede).
|
|
17
|
+
*/
|
|
18
|
+
import { Box, Text, useInput } from 'ink';
|
|
19
|
+
import { useState, useCallback } from 'react';
|
|
20
|
+
import { TextInput } from '../../components/text-input.js';
|
|
21
|
+
import { glyphs } from '../../glyphs.js';
|
|
22
|
+
import { useNavigation } from '../../navigation.js';
|
|
23
|
+
import { symbols } from '../../symbols.js';
|
|
24
|
+
import { useTheme } from '../../theme.js';
|
|
25
|
+
import { useOnboarding } from './onboarding-context.js';
|
|
26
|
+
/** Alterna entre os dois campos do formulário. */
|
|
27
|
+
function otherField(field) {
|
|
28
|
+
return field === 'username' ? 'password' : 'username';
|
|
29
|
+
}
|
|
30
|
+
/** Tela de onboarding: login por usuário/senha — Esc volta (atalho global, ver `../../app.tsx`). */
|
|
31
|
+
export function OnboardingLoginScreen() {
|
|
32
|
+
const theme = useTheme();
|
|
33
|
+
const { push } = useNavigation();
|
|
34
|
+
const { callbacks, setPendingLogin } = useOnboarding();
|
|
35
|
+
const [username, setUsername] = useState('');
|
|
36
|
+
const [password, setPassword] = useState('');
|
|
37
|
+
const [field, setField] = useState('username');
|
|
38
|
+
// Handler estável — setField (setState) já tem identidade fixa; o useCallback
|
|
39
|
+
// garante que o useInput subscreva uma única vez (padrão do repo).
|
|
40
|
+
const handleFieldSwitch = useCallback((_input, key) => {
|
|
41
|
+
if (key.tab || key.downArrow || key.upArrow) {
|
|
42
|
+
setField(otherField);
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
useInput(handleFieldSwitch);
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, borderStyle: "round", borderColor: theme.border, children: [_jsx(Text, { color: theme.primary, children: "Login no Redmine" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: field === 'username' ? theme.primary : theme.muted, children: [field === 'username' ? symbols.pointer : ' ', " Usu\u00E1rio:", ' '] }), _jsx(TextInput, { value: username, onChange: setUsername, onSubmit: () => setField('password'), isActive: field === 'username' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: field === 'password' ? theme.primary : theme.muted, children: [field === 'password' ? symbols.pointer : ' ', " Senha:", ' '] }), _jsx(TextInput, { value: password, onChange: setPassword, onSubmit: () => {
|
|
47
|
+
setPendingLogin(callbacks.onLoginSubmit({ username, password }));
|
|
48
|
+
push('onboarding-validating');
|
|
49
|
+
}, mask: glyphs.maskBullet, isActive: field === 'password' })] })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: "Tab" }), ' ', "alterna o campo,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "confirma,", ' ', _jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "volta."] }) })] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Tela de onboarding 2/3: modo de autenticação (M2-05.1, issue #27).
|
|
4
|
+
*
|
|
5
|
+
* Seleção entre login por usuário/senha (avança para `onboarding-login`) e
|
|
6
|
+
* colar uma api_key diretamente (avança para `onboarding-api-key`, #28).
|
|
7
|
+
* `useListNavigation` (M2-04) dá o teclado padrão de lista (setas/`j`/`k`/
|
|
8
|
+
* Enter) de graça — nenhuma reimplementação aqui.
|
|
9
|
+
*/
|
|
10
|
+
import { Box, Text } from 'ink';
|
|
11
|
+
import { glyphs } from '../../glyphs.js';
|
|
12
|
+
import { useListNavigation } from '../../hooks/use-list-navigation.js';
|
|
13
|
+
import { useNavigation } from '../../navigation.js';
|
|
14
|
+
import { symbols } from '../../symbols.js';
|
|
15
|
+
import { useTheme } from '../../theme.js';
|
|
16
|
+
import { useOnboarding } from './onboarding-context.js';
|
|
17
|
+
/** As duas formas de autenticação suportadas pelo onboarding. */
|
|
18
|
+
const MODE_OPTIONS = [
|
|
19
|
+
{ mode: 'password', label: 'Entrar com usuário e senha' },
|
|
20
|
+
{ mode: 'api-key', label: 'Colar uma api_key' },
|
|
21
|
+
];
|
|
22
|
+
/** Tela de onboarding: modo de autenticação — setas/`j`/`k` navegam, Enter confirma, Esc volta (atalho global, ver `../../app.tsx`). */
|
|
23
|
+
export function OnboardingModeScreen() {
|
|
24
|
+
const { push } = useNavigation();
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const { setMode, callbacks } = useOnboarding();
|
|
27
|
+
const { selectedIndex } = useListNavigation(MODE_OPTIONS.length, {
|
|
28
|
+
onSelect: (index) => {
|
|
29
|
+
const option = MODE_OPTIONS[index];
|
|
30
|
+
if (option === undefined) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setMode(option.mode);
|
|
34
|
+
callbacks.onModeSelect(option.mode);
|
|
35
|
+
push(option.mode === 'password' ? 'onboarding-login' : 'onboarding-api-key');
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, borderStyle: "round", borderColor: theme.border, children: [_jsx(Text, { color: theme.primary, children: "Como voc\u00EA quer entrar?" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: MODE_OPTIONS.map((option, index) => {
|
|
39
|
+
const isSelected = index === selectedIndex;
|
|
40
|
+
return (_jsxs(Text, { color: isSelected ? theme.primary : theme.muted, children: [isSelected ? symbols.pointer : ' ', " ", option.label] }, option.mode));
|
|
41
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, children: `${glyphs.arrowUp}${glyphs.arrowDown}/j/k` }), ' ', "navegam,", ' ', _jsx(Text, { color: theme.accent, children: "Enter" }), ' ', "confirma,", ' ', _jsx(Text, { color: theme.accent, children: "Esc" }), ' ', "volta."] }) })] }));
|
|
42
|
+
}
|