@gomjellie/lazyapi 0.0.4 → 0.0.5

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.
@@ -2,9 +2,9 @@
2
2
  * API 상세 화면
3
3
  */
4
4
  import React from 'react';
5
- import { Box, Text, useStdout } from 'ink';
5
+ import { Box, Text, useStdout, useStdin } from 'ink';
6
6
  import { useAppSelector, useAppDispatch } from '../../store/hooks.js';
7
- import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, detailSetBody, } from '../../store/appSlice.js';
7
+ import { setMode, detailSetFocus, detailSetPanel, detailSetRequestTab, detailSetResponseTab, detailSetResponse, detailSetParam, detailSetBody, detailSetHeader, } from '../../store/appSlice.js';
8
8
  import { navigateUp, navigateDown, navigateLeft, navigateRight, } from '../../store/navigationActions.js';
9
9
  import { useNavigation } from '../../hooks/useNavigation.js';
10
10
  import { useKeyPress } from '../../hooks/useKeyPress.js';
@@ -15,6 +15,10 @@ import LeftPanel from './LeftPanel.js';
15
15
  import RightPanel from './RightPanel.js';
16
16
  import { getServerUrl } from '../../types/openapi.js';
17
17
  import { setCommandInput } from '../../store/appSlice.js';
18
+ import { spawnSync } from 'child_process';
19
+ import fs from 'fs';
20
+ import os from 'os';
21
+ import path from 'path';
18
22
  export default function DetailView() {
19
23
  const detail = useAppSelector((state) => state.app.detail);
20
24
  const document = useAppSelector((state) => state.app.document);
@@ -24,11 +28,151 @@ export default function DetailView() {
24
28
  const { goBack } = useNavigation();
25
29
  const { executeRequest, loading } = useApiClient();
26
30
  const { stdout } = useStdout();
31
+ const { isRawModeSupported, setRawMode } = useStdin();
27
32
  // detail이 없는 경우를 대비한 기본값
28
33
  const endpoint = detail?.endpoint;
29
34
  const activePanel = detail?.activePanel || 'left';
30
35
  const activeRequestTab = detail?.activeRequestTab || 'headers';
31
36
  const activeResponseTab = detail?.activeResponseTab || 'body';
37
+ const handleOpenEditor = () => {
38
+ if (!detail || !endpoint)
39
+ return;
40
+ let content = '';
41
+ let fileName = `lazyapi-edit-${Date.now()}`;
42
+ let extension = '.json';
43
+ let isEditable = false;
44
+ let updateType = 'NONE';
45
+ // 1. Determine content and metadata based on active panel and tab
46
+ if (activePanel === 'request') {
47
+ if (activeRequestTab === 'body') {
48
+ content = detail.requestBody || '';
49
+ isEditable = true;
50
+ updateType = 'BODY';
51
+ }
52
+ else if (activeRequestTab === 'headers') {
53
+ content = JSON.stringify(detail.headers, null, 2);
54
+ isEditable = true; // Later could support bulk edit
55
+ updateType = 'HEADERS';
56
+ }
57
+ else if (activeRequestTab === 'query') {
58
+ content = JSON.stringify(detail.parameterValues, null, 2);
59
+ isEditable = true;
60
+ updateType = 'PARAMS';
61
+ }
62
+ }
63
+ else if (activePanel === 'response' && detail.response) {
64
+ if (activeResponseTab === 'body') {
65
+ content =
66
+ typeof detail.response.body === 'string'
67
+ ? detail.response.body
68
+ : JSON.stringify(detail.response.body, null, 2);
69
+ }
70
+ else if (activeResponseTab === 'headers') {
71
+ content = JSON.stringify(detail.response.headers, null, 2);
72
+ }
73
+ else if (activeResponseTab === 'cookies') {
74
+ content = JSON.stringify(detail.response.cookies || {}, null, 2);
75
+ }
76
+ else if (activeResponseTab === 'curl') {
77
+ extension = '.sh';
78
+ // Calculate curl command
79
+ let pathStr = endpoint.path;
80
+ endpoint.parameters
81
+ .filter((p) => p.in === 'path')
82
+ .forEach((param) => {
83
+ const value = detail.parameterValues[param.name] || '';
84
+ pathStr = pathStr.replace(`{${param.name}}`, value);
85
+ });
86
+ const queryParams = endpoint.parameters
87
+ .filter((p) => p.in === 'query')
88
+ .filter((param) => detail.parameterValues[param.name])
89
+ .map((param) => `${param.name}=${encodeURIComponent(detail.parameterValues[param.name])}`)
90
+ .join('&');
91
+ const url = queryParams ? `${baseURL}${pathStr}?${queryParams}` : `${baseURL}${pathStr}`;
92
+ let headerStr = '';
93
+ Object.entries(detail.headers).forEach(([key, val]) => {
94
+ headerStr += ` -H '${key}: ${val}'`;
95
+ });
96
+ let bodyStr = '';
97
+ if (detail.requestBody) {
98
+ bodyStr = ` -d '${detail.requestBody.replace(/'/g, "'\\''")}'`;
99
+ }
100
+ const method = endpoint.method.toUpperCase();
101
+ content = `curl -X ${method} '${url}'${headerStr}${bodyStr}`;
102
+ }
103
+ }
104
+ else if (activePanel === 'left') {
105
+ // LeftPanel handles body params or requestBody
106
+ content = detail.requestBody || '';
107
+ isEditable = true;
108
+ updateType = 'BODY';
109
+ }
110
+ if (!content && activePanel === 'response' && activeResponseTab === 'curl') {
111
+ // Re-calculate curl if needed or just show placeholder
112
+ content = 'Curl command calculation not available in this view';
113
+ }
114
+ try {
115
+ const tempFile = path.join(os.tmpdir(), `${fileName}${extension}`);
116
+ fs.writeFileSync(tempFile, content);
117
+ const editor = process.env.EDITOR || 'vim';
118
+ if (isRawModeSupported) {
119
+ setRawMode(false);
120
+ }
121
+ process.stdin.pause();
122
+ try {
123
+ spawnSync(editor, [tempFile], {
124
+ stdio: 'inherit',
125
+ });
126
+ if (fs.existsSync(tempFile) && isEditable) {
127
+ const newContent = fs.readFileSync(tempFile, 'utf-8');
128
+ if (newContent !== content) {
129
+ if (updateType === 'BODY') {
130
+ dispatch(detailSetBody(newContent));
131
+ }
132
+ else if (updateType === 'HEADERS') {
133
+ try {
134
+ const parsed = JSON.parse(newContent);
135
+ Object.entries(parsed).forEach(([key, value]) => {
136
+ dispatch(detailSetHeader({ key, value: String(value) }));
137
+ });
138
+ }
139
+ catch {
140
+ // Ignore parse errors for bulk edit for now
141
+ }
142
+ }
143
+ else if (updateType === 'PARAMS') {
144
+ try {
145
+ const parsed = JSON.parse(newContent);
146
+ Object.entries(parsed).forEach(([key, value]) => {
147
+ dispatch(detailSetParam({ key, value: String(value) }));
148
+ });
149
+ }
150
+ catch {
151
+ // Ignore
152
+ }
153
+ }
154
+ }
155
+ }
156
+ if (fs.existsSync(tempFile)) {
157
+ fs.unlinkSync(tempFile);
158
+ }
159
+ }
160
+ catch (innerError) {
161
+ console.error('Error executing editor:', innerError);
162
+ }
163
+ finally {
164
+ process.stdin.resume();
165
+ if (isRawModeSupported) {
166
+ setRawMode(true);
167
+ }
168
+ dispatch(setMode('NORMAL'));
169
+ }
170
+ }
171
+ catch (e) {
172
+ console.error('Failed to open editor', e);
173
+ dispatch(setMode('NORMAL'));
174
+ }
175
+ };
32
176
  // 파라미터 필드 개수 계산
33
177
  const pathParams = endpoint?.parameters.filter((p) => p.in === 'path') || [];
34
178
  const queryParams = endpoint?.parameters.filter((p) => p.in === 'query') || [];
@@ -42,6 +186,23 @@ export default function DetailView() {
42
186
  formDataParams.length +
43
187
  (endpoint?.requestBody && bodyParams.length === 0 ? 1 : 0) +
44
188
  (endpoint?.responses?.length || 0);
189
+ // Body 영역 인덱스 계산
190
+ const bodyStartIndex = pathParams.length + queryParams.length + headerParams.length;
191
+ const bodyEndIndex = bodyStartIndex + bodyParams.length;
192
+ // OA3 Request Body 인덱스 (Body Params가 없을 때만 존재)
193
+ const requestBodyIndex = endpoint?.requestBody && bodyParams.length === 0 ? bodyEndIndex + formDataParams.length : -1;
194
+ // 'e' 키 (Edit) 표시 여부 결정
195
+ const showEditKey =
196
+ // 1. Request or Response Panel > TABS area
197
+ ((activePanel === 'request' || activePanel === 'response') &&
198
+ detail?.subFocus === 'TABS') ||
199
+ // 2. Left Panel > Body Parameter (in: body)
200
+ (activePanel === 'left' &&
201
+ detail &&
202
+ detail.focusedFieldIndex >= bodyStartIndex &&
203
+ detail.focusedFieldIndex < bodyEndIndex) ||
204
+ // 3. Left Panel > Request Body (OA3)
205
+ (activePanel === 'left' && detail && detail.focusedFieldIndex === requestBodyIndex);
45
206
  const handleExecuteRequest = async () => {
46
207
  if (!detail || !document)
47
208
  return;
@@ -63,8 +224,8 @@ export default function DetailView() {
63
224
  if (cmd === 'q' || cmd === 'quit' || cmd === 'exit') {
64
225
  process.exit(0);
65
226
  }
66
- // 2. 접두사 있는 명령어 처리 (pp1, qp1, h1, rqb1, rs1, rq1, rb1...)
67
- const match = cmd.match(/^(pp|qp|h|rqb|rs|rq|rb)(\d+)$/);
227
+ // 2. 접두사 있는 명령어 처리 (pp1, qp1, h1, bt1, rs1, rq1, rt1...)
228
+ const match = cmd.match(/^(pp|qp|h|bt|rs|rq|rt)(\d+)$/);
68
229
  if (match) {
69
230
  const type = match[1];
70
231
  const index = parseInt(match[2], 10);
@@ -100,8 +261,8 @@ export default function DetailView() {
100
261
  dispatch(detailSetPanel('request'));
101
262
  }
102
263
  }
103
- else if (type === 'rqb') {
104
- // Request Body (기존 rb -> rqb)
264
+ else if (type === 'bt') {
265
+ // Request Body
105
266
  const bodyStartIndex = pathParams.length + queryParams.length + headerParams.length;
106
267
  if (bodyParams.length > 0) {
107
268
  if (targetIndex >= 0 && targetIndex < bodyParams.length) {
@@ -117,8 +278,8 @@ export default function DetailView() {
117
278
  }
118
279
  }
119
280
  }
120
- else if (type === 'rb') {
121
- // Responses (사용자 요청: rb)
281
+ else if (type === 'rt') {
282
+ // Responses
122
283
  const responsesStartIndex = pathParams.length +
123
284
  queryParams.length +
124
285
  headerParams.length +
@@ -235,6 +396,7 @@ export default function DetailView() {
235
396
  { key: ':', description: 'Command' },
236
397
  { key: 'Tab', description: 'Cycle Panel' },
237
398
  { key: 'i', description: 'Insert' },
399
+ ...(showEditKey ? [{ key: 'e', description: 'Editor' }] : []),
238
400
  { key: 'Enter', description: 'Execute' },
239
401
  { key: 'u', description: 'Back' },
240
402
  ]
@@ -260,7 +422,7 @@ export default function DetailView() {
260
422
  dispatch(detailSetBody(value));
261
423
  dispatch(detailSetPanel('request'));
262
424
  dispatch(detailSetRequestTab('body'));
263
- }, onSetMode: (m) => dispatch(setMode(m)) }),
264
- React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, subFocus: detail.subFocus, response: detail.response, endpoint: endpoint, document: document, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, mode: mode, isLoading: loading, commandMode: mode === 'COMMAND' })),
425
+ }, onSetMode: (m) => dispatch(setMode(m)), onOpenEditor: handleOpenEditor }),
426
+ React.createElement(RightPanel, { activeRequestTab: activeRequestTab, activeResponseTab: activeResponseTab, activePanel: activePanel, subFocus: detail.subFocus, response: detail.response, endpoint: endpoint, parameterValues: detail.parameterValues, requestBody: detail.requestBody, requestHeaders: detail.headers, baseURL: baseURL, isLoading: loading, commandMode: mode === 'COMMAND', onOpenEditor: handleOpenEditor })),
265
427
  React.createElement(KeybindingFooter, { bindings: keyBindings, mode: mode, commandInput: commandInput, onCommandChange: (value) => dispatch(setCommandInput(value)), onCommandSubmit: handleCommandSubmit })));
266
428
  }
@@ -17,6 +17,7 @@ interface LeftPanelProps {
17
17
  onExitInsertMode?: () => void;
18
18
  onRequestBodyEdit?: (value: string) => void;
19
19
  onSetMode?: (mode: AppMode) => void;
20
+ onOpenEditor?: () => void;
20
21
  }
21
- export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }: LeftPanelProps): React.JSX.Element;
22
+ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, onOpenEditor, }: LeftPanelProps): React.JSX.Element;
22
23
  export {};
@@ -35,7 +35,7 @@ function getStatusColor(status) {
35
35
  return 'red';
36
36
  return 'gray';
37
37
  }
38
- export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, }) {
38
+ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue, focusedFieldIndex, mode, isFocused, document, commandMode = false, onParameterChange, onExitInsertMode, onRequestBodyEdit, onSetMode, onOpenEditor, }) {
39
39
  const { stdout } = useStdout();
40
40
  const [localValue, setLocalValue] = useState('');
41
41
  const [collapsedSchemas, setCollapsedSchemas] = useState(() => {
@@ -429,12 +429,23 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
429
429
  }
430
430
  }
431
431
  };
432
+ const handleOpenRequestBodyEditor = () => {
433
+ if (!isFocused || mode !== 'NORMAL' || !onOpenEditor)
434
+ return;
435
+ const flatIndex = paramIndexToFlatIndex[focusedFieldIndex];
436
+ const item = flatItems[flatIndex];
437
+ const isBodyParam = item?.type === 'requestBody' || (item?.type === 'param' && item.param.in === 'body');
438
+ if (isBodyParam) {
439
+ onOpenEditor();
440
+ }
441
+ };
432
442
  useKeyPress({
433
443
  '>': handleExpandSchema,
434
444
  '<': handleCollapseSchema,
435
445
  j: handleSchemaScrollDown,
436
446
  k: handleSchemaScrollUp,
437
447
  i: handleEnterInsertMode,
448
+ e: handleOpenRequestBodyEditor,
438
449
  }, mode === 'NORMAL' && isFocused);
439
450
  // Render Helpers
440
451
  const getSchemaName = (schema) => {
@@ -530,7 +541,7 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
530
541
  if (item.param.schema) {
531
542
  if (isReferenceObject(item.param.schema)) {
532
543
  refInfo = item.param.schema.$ref.split('/').pop() || item.param.schema.$ref;
533
- typeInfo = 'object';
544
+ typeInfo = refInfo;
534
545
  }
535
546
  else {
536
547
  const schemaType = item.param.schema.type;
@@ -557,8 +568,8 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
557
568
  commandHint = `h${hIndex}`;
558
569
  }
559
570
  else if (item.param.in === 'body') {
560
- const rbIndex = item.globalIndex - bodyStartIndex + 1;
561
- commandHint = `rb${rbIndex}`;
571
+ const btIndex = item.globalIndex - bodyStartIndex + 1;
572
+ commandHint = `bt${btIndex}`;
562
573
  }
563
574
  }
564
575
  return (React.createElement(Box, { key: key, flexDirection: "column" },
@@ -581,7 +592,11 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
581
592
  React.createElement(Box, { marginLeft: 2, flexGrow: 1 }, mode === 'INSERT' && isFieldFocused ? (React.createElement(SearchInput, { value: localValue, onChange: (val) => {
582
593
  setLocalValue(val);
583
594
  onParameterChange(item.param.name, val);
584
- }, suggestions: [], placeholder: "", focus: true, onSubmit: onExitInsertMode })) : (React.createElement(Text, { color: value ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !value }, value || (isFieldFocused ? ' (empty) ' : '(empty)'))))),
595
+ }, suggestions: [], placeholder: "", focus: true, onSubmit: onExitInsertMode })) : (React.createElement(Text, { color: value ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !value }, item.param.in === 'body' && value
596
+ ? isFieldFocused
597
+ ? ' (Press e to edit) '
598
+ : '(Value set)'
599
+ : value || (isFieldFocused ? ' (empty) ' : '(empty)'))))),
585
600
  item.param.description && (React.createElement(Box, { marginLeft: 2, marginTop: 0 },
586
601
  React.createElement(Text, { color: "gray", dimColor: true }, item.param.description))),
587
602
  (() => {
@@ -625,15 +640,24 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
625
640
  const isFieldFocused = item.globalIndex === focusedFieldIndex && isFocused;
626
641
  const isExpanded = !collapsedSchemas.has('__requestBody__');
627
642
  const hasSchema = !!endpoint.requestBody?.schema;
628
- const commandHint = commandMode ? 'rqb1' : '';
643
+ const commandHint = commandMode ? 'bt1' : '';
629
644
  return (React.createElement(Box, { key: key, flexDirection: "column" },
630
- React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
631
- commandMode && (React.createElement(Box, { width: 8 },
632
- React.createElement(Text, { color: "yellow" },
633
- "[",
634
- commandHint,
635
- "]"))),
636
- React.createElement(Text, { color: isFieldFocused ? 'black' : 'gray' }, item.endpoint.requestBody?.description || 'Request Body Content')),
645
+ React.createElement(Box, { flexDirection: "row", marginLeft: 1 },
646
+ React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined },
647
+ commandMode && (React.createElement(Box, { width: 8 },
648
+ React.createElement(Text, { color: "yellow" },
649
+ "[",
650
+ commandHint,
651
+ "]"))),
652
+ React.createElement(Text, { color: isFieldFocused ? 'black' : 'gray' }, item.endpoint.requestBody?.description || 'Request Body Content')),
653
+ React.createElement(Box, { marginLeft: 2 },
654
+ React.createElement(Text, { color: requestBodyValue ? 'white' : 'gray', backgroundColor: isFieldFocused ? 'green' : undefined, bold: isFieldFocused && !requestBodyValue }, requestBodyValue
655
+ ? isFieldFocused
656
+ ? ' (Press e to edit) '
657
+ : '(Value set)'
658
+ : isFieldFocused
659
+ ? ' (empty) '
660
+ : '(empty)'))),
637
661
  hasSchema && !isExpanded && (React.createElement(Box, { marginLeft: 1, marginTop: 0 },
638
662
  React.createElement(Text, { color: "yellow", dimColor: true },
639
663
  "Press > to view ",
@@ -661,13 +685,13 @@ export default function LeftPanel({ endpoint, parameterValues, requestBodyValue,
661
685
  (endpoint.requestBody && endpoint.parameters.filter((p) => p.in === 'body').length === 0
662
686
  ? 1
663
687
  : 0);
664
- const rbIndex = item.globalIndex - responsesStartIndex + 1;
688
+ const rtIndex = item.globalIndex - responsesStartIndex + 1;
665
689
  return (React.createElement(Box, { key: key, flexDirection: "column" },
666
690
  React.createElement(Box, { flexDirection: "row", backgroundColor: isFieldFocused ? 'green' : undefined, marginLeft: 1 },
667
691
  commandMode && (React.createElement(Box, { width: 8 },
668
692
  React.createElement(Text, { color: "yellow" },
669
693
  "[",
670
- `rb${rbIndex}`,
694
+ `rt${rtIndex}`,
671
695
  "]"))),
672
696
  React.createElement(Text, { color: isFieldFocused ? 'black' : getStatusColor(item.response.status), bold: true }, item.response.status),
673
697
  React.createElement(Text, { color: isFieldFocused ? 'black' : 'white' },
@@ -2,23 +2,22 @@
2
2
  * API Detail 우측 패널 - Request/Response 분할
3
3
  */
4
4
  import React from 'react';
5
- import { ApiResponse, AppMode } from '../../types/app-state.js';
6
- import { Endpoint, OpenAPIDocument } from '../../types/openapi.js';
5
+ import { ApiResponse } from '../../types/app-state.js';
6
+ import { Endpoint } from '../../types/openapi.js';
7
7
  interface RightPanelProps {
8
8
  activeRequestTab: 'headers' | 'body' | 'query';
9
9
  activeResponseTab: 'body' | 'headers' | 'cookies' | 'curl';
10
10
  activePanel: 'left' | 'request' | 'response';
11
- subFocus: 'TITLE' | 'TABS' | 'CONTENT';
11
+ subFocus: 'TITLE' | 'TABS';
12
12
  response: ApiResponse | null;
13
13
  endpoint: Endpoint;
14
- document?: OpenAPIDocument;
15
14
  parameterValues: Record<string, string>;
16
15
  requestBody: string;
17
16
  requestHeaders: Record<string, string>;
18
17
  baseURL: string;
19
- mode: AppMode;
20
18
  isLoading: boolean;
21
19
  commandMode?: boolean;
20
+ onOpenEditor: () => void;
22
21
  }
23
- export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode, }: RightPanelProps): React.JSX.Element;
22
+ export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, parameterValues, requestBody, requestHeaders, baseURL, isLoading, commandMode, onOpenEditor, }: RightPanelProps): React.JSX.Element;
24
23
  export {};
@@ -1,16 +1,9 @@
1
1
  /**
2
2
  * API Detail 우측 패널 - Request/Response 분할
3
3
  */
4
- import React, { useState, useEffect, useMemo } from 'react';
5
- import { Box, Text, useStdout, useStdin } from 'ink';
4
+ import React, { useMemo } from 'react';
5
+ import { Box, Text, useStdout } from 'ink';
6
6
  import { useKeyPress } from '../../hooks/useKeyPress.js';
7
- import { useAppDispatch } from '../../store/hooks.js';
8
- import { detailSetPanel, detailSetBody, detailSetSubFocus, setMode } from '../../store/appSlice.js';
9
- import { spawnSync } from 'child_process';
10
- import fs from 'fs';
11
- import os from 'os';
12
- import path from 'path';
13
- import { scaffoldSchema } from '../../utils/schema-scaffolder.js';
14
7
  import Spinner from 'ink-spinner';
15
8
  const TabHeader = ({ tabs, active, isFocused, shortcutPrefix, commandMode }) => (React.createElement(Box, { marginBottom: 0, borderStyle: "single", borderBottom: false, borderTop: false, borderLeft: false, borderRight: false, gap: 1 }, tabs.map((t, index) => {
16
9
  let showHint = false;
@@ -34,129 +27,18 @@ const TabHeader = ({ tabs, active, isFocused, shortcutPrefix, commandMode }) =>
34
27
  React.createElement(Text, { color: "yellow" }, showIndicator ? '>' : ' '),
35
28
  React.createElement(Text, { bold: isActive, underline: showIndicator, color: isActive ? 'black' : 'gray', backgroundColor: isActive ? 'green' : undefined }, t.label + ' ')));
36
29
  })));
37
- export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, document, parameterValues, requestBody, requestHeaders, baseURL, mode, isLoading, commandMode = false, }) {
30
+ export default function RightPanel({ activeRequestTab, activeResponseTab, activePanel, subFocus, response, endpoint, parameterValues, requestBody, requestHeaders, baseURL, isLoading, commandMode = false, onOpenEditor, }) {
38
31
  const { stdout } = useStdout();
39
- const { isRawModeSupported, setRawMode } = useStdin();
40
- const dispatch = useAppDispatch();
41
32
  // 높이 계산
42
33
  const totalHeight = (stdout?.rows || 24) - 4;
43
34
  const requestBoxHeight = Math.floor(totalHeight / 2);
44
35
  const responseBoxHeight = totalHeight - requestBoxHeight; // 나머지 높이를 Response 박스에 할당
45
- const contentHeight = Math.max(requestBoxHeight - 4, 1);
46
- // -- Request Box State --
47
- const [reqScroll, setReqScroll] = useState(0);
48
- useEffect(() => {
49
- setReqScroll(0);
50
- }, [activeRequestTab, endpoint]);
51
- // Request Body 자동 스캐폴딩
52
- useEffect(() => {
53
- if (activeRequestTab === 'body' && activePanel === 'request' && !requestBody) {
54
- // 1. requestBody 필드 (OpenAPI 3.0)
55
- if (endpoint.requestBody?.schema) {
56
- const scaffolded = scaffoldSchema(endpoint.requestBody.schema, document);
57
- const value = JSON.stringify(scaffolded, null, 2);
58
- dispatch(detailSetBody(value));
59
- return;
60
- }
61
- // 2. in: body 파라미터 (Swagger 2.0)
62
- const bodyParam = endpoint.parameters.find(p => p.in === 'body');
63
- if (bodyParam && bodyParam.schema) {
64
- const scaffolded = scaffoldSchema(bodyParam.schema, document);
65
- const value = JSON.stringify(scaffolded, null, 2);
66
- dispatch(detailSetBody(value));
67
- return;
68
- }
69
- }
70
- }, [activeRequestTab, activePanel, requestBody, endpoint, document, dispatch]);
71
- // Request Content Lines 계산
72
- const getRequestLines = () => {
73
- if (activeRequestTab === 'headers') {
74
- const keys = Object.keys(requestHeaders);
75
- return keys.length === 0 ? 1 : keys.length;
76
- }
77
- if (activeRequestTab === 'body') {
78
- return requestBody ? requestBody.split('\n').length : 1;
79
- }
80
- if (activeRequestTab === 'query') {
81
- const queries = endpoint.parameters.filter((p) => p.in === 'query');
82
- return queries.length === 0 ? 1 : queries.length;
83
- }
84
- return 0;
85
- };
86
- const reqTotalLines = getRequestLines();
87
- // Request Scroll Handler (j/k)
88
- useKeyPress({
89
- j: () => {
90
- setReqScroll((prev) => {
91
- const maxScroll = Math.max(0, reqTotalLines - contentHeight);
92
- if (prev < maxScroll) {
93
- return Math.min(prev + 1, maxScroll);
94
- }
95
- else {
96
- // 스크롤이 끝에 도달하면 Response 패널로 이동
97
- dispatch(detailSetPanel('response'));
98
- dispatch(detailSetSubFocus('TITLE'));
99
- return prev;
100
- }
101
- });
102
- },
103
- k: () => {
104
- setReqScroll((prev) => {
105
- if (prev > 0)
106
- return prev - 1;
107
- dispatch(detailSetSubFocus('TABS'));
108
- return 0;
109
- });
110
- },
111
- }, mode === 'NORMAL' && activePanel === 'request' && subFocus === 'CONTENT');
112
- const openExternalEditor = () => {
113
- try {
114
- const tempFile = path.join(os.tmpdir(), `lazyapi-body-${Date.now()}.json`);
115
- fs.writeFileSync(tempFile, requestBody || '');
116
- const editor = process.env.EDITOR || 'vim';
117
- // Ink의 Raw 모드를 해제하고 stdin을 일시 중지하여 외부 에디터에 제어권 이양
118
- if (isRawModeSupported) {
119
- setRawMode(false);
120
- }
121
- process.stdin.pause();
122
- try {
123
- // 동기적으로 실행하여 에디터 종료까지 대기
124
- spawnSync(editor, [tempFile], {
125
- stdio: 'inherit',
126
- });
127
- if (fs.existsSync(tempFile)) {
128
- const content = fs.readFileSync(tempFile, 'utf-8');
129
- dispatch(detailSetBody(content));
130
- fs.unlinkSync(tempFile);
131
- }
132
- }
133
- catch (innerError) {
134
- console.error('Error executing editor:', innerError);
135
- }
136
- finally {
137
- // 에디터 종료 후 stdin 재개 및 Raw 모드 복구
138
- process.stdin.resume();
139
- if (isRawModeSupported) {
140
- setRawMode(true);
141
- }
142
- dispatch(setMode('NORMAL'));
143
- }
144
- }
145
- catch (e) {
146
- console.error('Failed to open editor', e);
147
- dispatch(setMode('NORMAL'));
148
- }
149
- };
150
- // e 키: Request Body 탭이 활성화되어 있으면 항상 에디터 오픈 (NORMAL/INSERT 모두)
36
+ // 콘텐츠 가용 높이 계산 (박스 테두리 2 + 제목 1 + 탭 1 + 하단 안내 1 = 5줄 제외)
37
+ const contentHeight = Math.max(requestBoxHeight - 5, 1);
38
+ // e 키: 해당 패널의 TABS에 포커스가 있을 때만 동작
151
39
  useKeyPress({
152
- e: openExternalEditor,
153
- }, activePanel === 'request' && activeRequestTab === 'body');
154
- // -- Response Box State --
155
- const [resScroll, setResScroll] = useState(0);
156
- useEffect(() => {
157
- // 탭이 변경될 때만 스크롤 리셋 (response 변경 시에는 리셋하지 않음)
158
- setResScroll(0);
159
- }, [activeResponseTab]);
40
+ e: onOpenEditor,
41
+ }, activePanel !== 'left' && subFocus === 'TABS');
160
42
  // Curl Command 생성 (메모이제이션)
161
43
  const curlCommand = useMemo(() => {
162
44
  let pathStr = endpoint.path;
@@ -183,103 +65,41 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
183
65
  const method = endpoint.method.toUpperCase();
184
66
  return `curl -X ${method} '${url}'${headerStr}${bodyStr}`;
185
67
  }, [endpoint, parameterValues, requestHeaders, requestBody, baseURL]);
186
- // Response Content Lines 계산 (메모이제이션)
187
- const resTotalLines = useMemo(() => {
188
- if (activeResponseTab === 'curl') {
189
- return curlCommand.split('\n').length;
190
- }
191
- if (!response)
192
- return 1;
193
- if (activeResponseTab === 'body') {
194
- const bodyStr = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2);
195
- return bodyStr.split('\n').length;
196
- }
197
- if (activeResponseTab === 'headers') {
198
- return Object.keys(response.headers).length || 1;
199
- }
200
- if (activeResponseTab === 'cookies') {
201
- return response.cookies ? Object.keys(response.cookies).length || 1 : 1;
202
- }
203
- return 0;
204
- }, [activeResponseTab, response, curlCommand]);
205
- // Response Scroll Handler (j/k)
206
- useKeyPress({
207
- j: () => {
208
- setResScroll((prev) => {
209
- const maxScroll = Math.max(0, resTotalLines - contentHeight);
210
- if (prev < maxScroll) {
211
- return Math.min(prev + 1, maxScroll);
212
- }
213
- return prev;
214
- });
215
- },
216
- k: () => {
217
- setResScroll((prev) => {
218
- if (prev > 0)
219
- return prev - 1;
220
- dispatch(detailSetSubFocus('TABS'));
221
- return 0;
222
- });
223
- },
224
- }, mode === 'NORMAL' && activePanel === 'response' && subFocus === 'CONTENT');
225
- // 렌더링 헬퍼
226
- const renderLines = (lines, offset, limit) => {
227
- const visible = lines.slice(offset, offset + limit);
228
- return visible.map((line, i) => React.createElement(Text, { key: offset + i }, line));
229
- };
230
- const renderKeyValue = (data, offset, limit) => {
231
- const entries = Object.entries(data);
232
- if (entries.length === 0)
233
- return React.createElement(Text, { color: "gray" }, "No data");
234
- const visible = entries.slice(offset, offset + limit);
235
- return visible.map(([k, v], i) => (React.createElement(Box, { key: offset + i },
236
- React.createElement(Text, { color: "green" },
237
- k,
238
- ": "),
239
- React.createElement(Text, null, v))));
68
+ // 렌더링 헬퍼 (스크롤 제거, 단순히 잘라서 표시)
69
+ const renderLines = (lines, limit) => {
70
+ const visible = lines.slice(0, limit);
71
+ return visible.map((line, i) => (React.createElement(Text, { key: i, wrap: "truncate" },
72
+ ' ',
73
+ line)));
240
74
  };
241
- // Request Box Render
75
+ // Request Box Content Render
242
76
  const renderRequestContent = () => {
243
77
  if (activeRequestTab === 'headers') {
244
78
  if (Object.keys(requestHeaders).length === 0)
245
- return React.createElement(Text, { color: "gray" }, "No headers");
246
- return renderKeyValue(requestHeaders, reqScroll, contentHeight);
79
+ return React.createElement(Text, { color: "gray" }, " No headers");
80
+ const json = JSON.stringify(requestHeaders, null, 2);
81
+ return renderLines(json.split('\n'), contentHeight);
247
82
  }
248
83
  if (activeRequestTab === 'body') {
249
- let content = null;
250
- if (!requestBody) {
251
- content = React.createElement(Text, { color: "gray" }, "No body");
252
- }
253
- else {
254
- const lines = requestBody.split('\n');
255
- content = renderLines(lines, reqScroll, contentHeight);
256
- }
257
- // Body 탭이고 Request 패널 활성 상태면 항상 안내 문구 표시
258
- if (activePanel === 'request') {
259
- return (React.createElement(Box, { flexDirection: "column" },
260
- content,
261
- React.createElement(Box, { borderStyle: "single", borderColor: "yellow", marginTop: 0 },
262
- React.createElement(Text, { color: "yellow" }, "Press 'e' key to open external editor."))));
263
- }
264
- return content;
84
+ if (!requestBody)
85
+ return React.createElement(Text, { color: "gray" }, " No body");
86
+ const lines = requestBody.split('\n');
87
+ return renderLines(lines, contentHeight);
265
88
  }
266
89
  if (activeRequestTab === 'query') {
267
90
  const queries = endpoint.parameters.filter((p) => p.in === 'query');
268
91
  if (queries.length === 0)
269
- return React.createElement(Text, { color: "gray" }, "No query parameters");
270
- const visible = queries.slice(reqScroll, reqScroll + contentHeight);
271
- return visible.map((q, i) => (React.createElement(Box, { key: reqScroll + i },
272
- React.createElement(Text, { color: "blue" },
273
- q.name,
274
- ": "),
275
- React.createElement(Text, null, parameterValues[q.name] || (React.createElement(Text, { color: "gray", dimColor: true }, "(empty)"))))));
92
+ return React.createElement(Text, { color: "gray" }, " No query parameters");
93
+ // 파라미터 값만 추출하여 JSON으로 표시
94
+ const json = JSON.stringify(parameterValues, null, 2);
95
+ return renderLines(json.split('\n'), contentHeight);
276
96
  }
277
97
  return null;
278
98
  };
279
- // Response Box Render
99
+ // Response Box Content Render
280
100
  const renderResponseContent = () => {
281
101
  if (activeResponseTab === 'curl') {
282
- return renderLines(curlCommand.split('\n'), resScroll, contentHeight);
102
+ return renderLines(curlCommand.split('\n'), contentHeight);
283
103
  }
284
104
  if (isLoading) {
285
105
  return (React.createElement(Box, null,
@@ -288,20 +108,22 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
288
108
  React.createElement(Text, { color: "green" }, " Sending request...")));
289
109
  }
290
110
  if (!response)
291
- return React.createElement(Text, { color: "gray" }, "Press Enter to execute request.");
111
+ return React.createElement(Text, { color: "gray" }, " Press Enter to execute request.");
292
112
  if (activeResponseTab === 'body') {
293
113
  const bodyStr = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2);
294
- return renderLines(bodyStr.split('\n'), resScroll, contentHeight);
114
+ return renderLines(bodyStr.split('\n'), contentHeight);
295
115
  }
296
116
  if (activeResponseTab === 'headers') {
297
117
  if (!response.headers)
298
- return React.createElement(Text, { color: "gray" }, "No headers");
299
- return renderKeyValue(response.headers, resScroll, contentHeight);
118
+ return React.createElement(Text, { color: "gray" }, " No headers");
119
+ const json = JSON.stringify(response.headers, null, 2);
120
+ return renderLines(json.split('\n'), contentHeight);
300
121
  }
301
122
  if (activeResponseTab === 'cookies') {
302
123
  if (!response.cookies || Object.keys(response.cookies).length === 0)
303
- return React.createElement(Text, { color: "gray" }, "No cookies");
304
- return renderKeyValue(response.cookies, resScroll, contentHeight);
124
+ return React.createElement(Text, { color: "gray" }, " No cookies");
125
+ const json = JSON.stringify(response.cookies, null, 2);
126
+ return renderLines(json.split('\n'), contentHeight);
305
127
  }
306
128
  return null;
307
129
  };
@@ -317,34 +139,28 @@ export default function RightPanel({ activeRequestTab, activeResponseTab, active
317
139
  { id: 'cookies', label: 'Cookies' },
318
140
  { id: 'curl', label: 'Curl' },
319
141
  ];
142
+ const renderEditorHint = (isVisible) => (React.createElement(Box, { height: 1 }, isVisible && React.createElement(Text, { color: "yellow" }, " Press 'e' to open external editor.")));
320
143
  return (React.createElement(Box, { flexDirection: "column", width: "50%", marginLeft: 1 },
321
- React.createElement(Box, { flexDirection: "column", height: requestBoxHeight, borderStyle: "single", borderColor: activePanel === 'request' ? 'green' : 'gray', paddingX: 1 },
144
+ React.createElement(Box, { flexDirection: "column", height: requestBoxHeight, borderStyle: "single", borderColor: activePanel === 'request' ? 'green' : 'gray', paddingX: 0 },
322
145
  React.createElement(Box, { justifyContent: "space-between", height: 1, marginBottom: 0 },
323
146
  React.createElement(Box, null,
324
147
  React.createElement(Text, { color: activePanel === 'request' && subFocus === 'TITLE' ? 'yellow' : 'white', bold: activePanel === 'request' && subFocus === 'TITLE' },
325
148
  activePanel === 'request' && subFocus === 'TITLE' ? '>' : ' ',
326
- "Request")),
327
- React.createElement(Box, { width: 15 },
328
- React.createElement(Text, { color: "gray" }, reqTotalLines > contentHeight
329
- ? `${reqScroll + 1}-${Math.min(reqScroll + contentHeight, reqTotalLines)}/${reqTotalLines}`
330
- : ' '))),
149
+ "Request"))),
331
150
  React.createElement(TabHeader, { tabs: reqTabs, active: activeRequestTab, isFocused: activePanel === 'request' && subFocus === 'TABS', shortcutPrefix: "rq", commandMode: commandMode }),
332
- React.createElement(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden", borderStyle: "single", borderColor: activePanel === 'request' && subFocus === 'CONTENT' ? 'green' : 'gray', paddingX: 1 }, renderRequestContent())),
333
- React.createElement(Box, { flexDirection: "column", height: responseBoxHeight, borderStyle: "single", borderColor: activePanel === 'response' ? 'green' : 'gray', paddingX: 1 },
151
+ React.createElement(Box, { flexDirection: "column", paddingX: 0, marginTop: 0, flexGrow: 1 }, renderRequestContent()),
152
+ renderEditorHint(activePanel === 'request' && subFocus === 'TABS')),
153
+ React.createElement(Box, { flexDirection: "column", height: responseBoxHeight, borderStyle: "single", borderColor: activePanel === 'response' ? 'green' : 'gray', paddingX: 0 },
334
154
  React.createElement(Box, { justifyContent: "space-between", height: 1, marginBottom: 0 },
335
155
  React.createElement(Box, null,
336
156
  React.createElement(Text, { color: activePanel === 'response' && subFocus === 'TITLE' ? 'yellow' : 'white', bold: activePanel === 'response' && subFocus === 'TITLE' },
337
157
  activePanel === 'response' && subFocus === 'TITLE' ? '>' : ' ',
338
158
  "Response")),
339
- React.createElement(Box, { width: 30, flexDirection: "row", justifyContent: "flex-end" },
340
- response ? (React.createElement(Text, { color: response.status < 300 ? 'green' : 'red' },
341
- response.status,
342
- " ",
343
- response.statusText)) : (React.createElement(Text, null, " ")),
344
- React.createElement(Box, { width: 15 },
345
- React.createElement(Text, { color: "gray" }, resTotalLines > contentHeight
346
- ? `${resScroll + 1}-${Math.min(resScroll + contentHeight, resTotalLines)}/${resTotalLines}`
347
- : ' ')))),
159
+ React.createElement(Box, { width: 30, flexDirection: "row", justifyContent: "flex-end" }, response ? (React.createElement(Text, { color: response.status < 300 ? 'green' : 'red' },
160
+ response.status,
161
+ " ",
162
+ response.statusText)) : (React.createElement(Text, null, " ")))),
348
163
  React.createElement(TabHeader, { tabs: resTabs, active: activeResponseTab, isFocused: activePanel === 'response' && subFocus === 'TABS', shortcutPrefix: "rs", commandMode: commandMode }),
349
- React.createElement(Box, { flexGrow: 1, flexDirection: "column", overflow: "hidden", borderStyle: "single", borderColor: activePanel === 'response' && subFocus === 'CONTENT' ? 'green' : 'gray', paddingX: 1 }, renderResponseContent()))));
164
+ React.createElement(Box, { flexDirection: "column", paddingX: 0, marginTop: 0, flexGrow: 1 }, renderResponseContent()),
165
+ renderEditorHint(activePanel === 'response' && subFocus === 'TABS'))));
350
166
  }
@@ -9,6 +9,6 @@ export declare const setDocument: import("@reduxjs/toolkit").ActionCreatorWithPa
9
9
  }, "app/detailSetParam">, detailSetBody: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/detailSetBody">, detailSetHeader: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
10
10
  key: string;
11
11
  value: string;
12
- }, "app/detailSetHeader">, detailSetPanel: import("@reduxjs/toolkit").ActionCreatorWithPayload<"left" | "request" | "response", "app/detailSetPanel">, detailSetRequestTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "query" | "body", "app/detailSetRequestTab">, detailSetResponseTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "body" | "cookies" | "curl", "app/detailSetResponseTab">, detailSetSubFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"TITLE" | "TABS" | "CONTENT", "app/detailSetSubFocus">, detailSetResponse: import("@reduxjs/toolkit").ActionCreatorWithPayload<ApiResponse | null, "app/detailSetResponse">, reset: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"app/reset">;
12
+ }, "app/detailSetHeader">, detailSetPanel: import("@reduxjs/toolkit").ActionCreatorWithPayload<"left" | "request" | "response", "app/detailSetPanel">, detailSetRequestTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "query" | "body", "app/detailSetRequestTab">, detailSetResponseTab: import("@reduxjs/toolkit").ActionCreatorWithPayload<"headers" | "body" | "cookies" | "curl", "app/detailSetResponseTab">, detailSetSubFocus: import("@reduxjs/toolkit").ActionCreatorWithPayload<"TITLE" | "TABS", "app/detailSetSubFocus">, detailSetResponse: import("@reduxjs/toolkit").ActionCreatorWithPayload<ApiResponse | null, "app/detailSetResponse">, reset: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"app/reset">;
13
13
  declare const _default: import("redux").Reducer<AppState>;
14
14
  export default _default;
@@ -2,6 +2,7 @@
2
2
  * 애플리케이션 Redux Slice
3
3
  */
4
4
  import { createSlice } from '@reduxjs/toolkit';
5
+ import { scaffoldSchema } from '../utils/schema-scaffolder.js';
5
6
  // 초기 상태
6
7
  const initialState = {
7
8
  document: null,
@@ -122,12 +123,28 @@ const appSlice = createSlice({
122
123
  },
123
124
  detailSet: (state, action) => {
124
125
  const endpoint = action.payload;
126
+ let initialRequestBody = '';
127
+ if (state.document) {
128
+ // 1. requestBody (OpenAPI 3.0)
129
+ if (endpoint.requestBody?.schema) {
130
+ const scaffolded = scaffoldSchema(endpoint.requestBody.schema, state.document);
131
+ initialRequestBody = JSON.stringify(scaffolded, null, 2);
132
+ }
133
+ // 2. in: body parameter (Swagger 2.0)
134
+ else {
135
+ const bodyParam = endpoint.parameters.find((p) => p.in === 'body');
136
+ if (bodyParam && bodyParam.schema) {
137
+ const scaffolded = scaffoldSchema(bodyParam.schema, state.document);
138
+ initialRequestBody = JSON.stringify(scaffolded, null, 2);
139
+ }
140
+ }
141
+ }
125
142
  state.screen = 'DETAIL';
126
143
  state.detail = {
127
144
  endpoint,
128
145
  focusedFieldIndex: 0,
129
146
  parameterValues: {},
130
- requestBody: '',
147
+ requestBody: initialRequestBody,
131
148
  headers: {},
132
149
  activePanel: 'left',
133
150
  activeRequestTab: 'headers',
@@ -37,17 +37,15 @@ export const navigateUp = () => (dispatch, getState) => {
37
37
  if (detail.subFocus === 'TABS') {
38
38
  dispatch(detailSetSubFocus('TITLE'));
39
39
  }
40
- // CONTENT: Handled by component (scroll)
41
40
  }
42
41
  else if (detail.activePanel === 'response') {
43
42
  if (detail.subFocus === 'TITLE') {
44
43
  dispatch(detailSetPanel('request'));
45
- dispatch(detailSetSubFocus('CONTENT'));
44
+ dispatch(detailSetSubFocus('TABS'));
46
45
  }
47
46
  else if (detail.subFocus === 'TABS') {
48
47
  dispatch(detailSetSubFocus('TITLE'));
49
48
  }
50
- // CONTENT: Handled by component (scroll)
51
49
  }
52
50
  }
53
51
  };
@@ -105,18 +103,14 @@ export const navigateDown = () => (dispatch, getState) => {
105
103
  dispatch(detailSetSubFocus('TABS'));
106
104
  }
107
105
  else if (detail.subFocus === 'TABS') {
108
- dispatch(detailSetSubFocus('CONTENT'));
106
+ dispatch(detailSetPanel('response'));
107
+ dispatch(detailSetSubFocus('TITLE'));
109
108
  }
110
- // CONTENT: Handled by component (scroll) -> Move to Response
111
109
  }
112
110
  else if (detail.activePanel === 'response') {
113
111
  if (detail.subFocus === 'TITLE') {
114
112
  dispatch(detailSetSubFocus('TABS'));
115
113
  }
116
- else if (detail.subFocus === 'TABS') {
117
- dispatch(detailSetSubFocus('CONTENT'));
118
- }
119
- // CONTENT: Handled by component (scroll)
120
114
  }
121
115
  }
122
116
  };
@@ -147,7 +141,7 @@ export const navigateLeft = () => (dispatch, getState) => {
147
141
  // Already at left, do nothing or go back to list (handled by u key usually)
148
142
  }
149
143
  else {
150
- if (detail.subFocus === 'TITLE' || detail.subFocus === 'CONTENT') {
144
+ if (detail.subFocus === 'TITLE') {
151
145
  dispatch(detailSetPanel('left'));
152
146
  }
153
147
  else if (detail.subFocus === 'TABS') {
@@ -41,7 +41,7 @@ export interface DetailViewState {
41
41
  activePanel: 'left' | 'request' | 'response';
42
42
  activeRequestTab: 'headers' | 'body' | 'query';
43
43
  activeResponseTab: 'body' | 'headers' | 'cookies' | 'curl';
44
- subFocus: 'TITLE' | 'TABS' | 'CONTENT';
44
+ subFocus: 'TITLE' | 'TABS';
45
45
  response: ApiResponse | null;
46
46
  }
47
47
  export interface ApiResponse {
@@ -130,7 +130,7 @@ export type AppAction = {
130
130
  payload: 'body' | 'headers' | 'cookies' | 'curl';
131
131
  } | {
132
132
  type: 'DETAIL_SET_SUB_FOCUS';
133
- payload: 'TITLE' | 'TABS' | 'CONTENT';
133
+ payload: 'TITLE' | 'TABS';
134
134
  } | {
135
135
  type: 'DETAIL_SET_RESPONSE';
136
136
  payload: ApiResponse | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gomjellie/lazyapi",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "A terminal-based Swagger/OpenAPI explorer with Vim-style keyboard navigation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",