code-squad-cli 1.2.22 → 1.3.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.
@@ -14,8 +14,20 @@ export class GitAdapter {
14
14
  }
15
15
  }
16
16
  async getCurrentBranch(workspaceRoot) {
17
- const { stdout } = await exec(`cd "${workspaceRoot}" && git rev-parse --abbrev-ref HEAD`, execOptions);
18
- return stdout.trim();
17
+ try {
18
+ const { stdout } = await exec(`cd "${workspaceRoot}" && git rev-parse --abbrev-ref HEAD`, execOptions);
19
+ return stdout.trim();
20
+ }
21
+ catch {
22
+ // No commits yet — fall back to symbolic-ref (e.g. "main" on fresh repos)
23
+ try {
24
+ const { stdout } = await exec(`cd "${workspaceRoot}" && git symbolic-ref --short HEAD`, execOptions);
25
+ return stdout.trim();
26
+ }
27
+ catch {
28
+ return '';
29
+ }
30
+ }
19
31
  }
20
32
  async listWorktrees(workspaceRoot) {
21
33
  try {
@@ -57,10 +69,12 @@ export class GitAdapter {
57
69
  }
58
70
  }
59
71
  async createWorktree(worktreePath, branch, workspaceRoot) {
60
- // Extract parent directory and create it if needed
72
+ // Prune stale worktree entries (e.g. directory was deleted but still registered)
73
+ await exec(`cd "${workspaceRoot}" && git worktree prune`, execOptions).catch(() => { });
74
+ // Recursively create parent directory
61
75
  const parentDir = worktreePath.substring(0, worktreePath.lastIndexOf('/'));
62
76
  const mkdirCmd = parentDir ? `mkdir -p "${parentDir}" && ` : '';
63
- await exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add "${worktreePath}" -b "${branch}"`, execOptions);
77
+ await exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add -f "${worktreePath}" -b "${branch}"`, execOptions);
64
78
  }
65
79
  async removeWorktree(worktreePath, workspaceRoot, force = false) {
66
80
  const forceFlag = force ? ' --force' : '';
@@ -0,0 +1,13 @@
1
+ import type { TmuxWindowInfo } from './types.js';
2
+ import { TmuxAdapter } from './TmuxAdapter.js';
3
+ interface DashboardProps {
4
+ workspaceRoot: string;
5
+ repoName: string;
6
+ currentBranch: string;
7
+ initialWindows: TmuxWindowInfo[];
8
+ tmuxAdapter: TmuxAdapter;
9
+ dashWindowIndex: number;
10
+ paneHeight: number;
11
+ }
12
+ export declare function runInkDashboard(config: DashboardProps): Promise<void>;
13
+ export {};
@@ -0,0 +1,442 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { render, Box, Text, useInput, useStdin } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { loadAllWindows, deleteWindowById } from './windowHelpers.js';
6
+ import { createThread, deleteThread } from './threadHelpers.js';
7
+ import { expandTilde } from './pathUtils.js';
8
+ import { useDirectorySuggestions } from './useDirectorySuggestions.js';
9
+ import { usePathValidation } from './usePathValidation.js';
10
+ function parseMouseEvent(data) {
11
+ const match = data.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
12
+ if (!match)
13
+ return null;
14
+ return {
15
+ button: parseInt(match[1], 10),
16
+ col: parseInt(match[2], 10),
17
+ row: parseInt(match[3], 10),
18
+ release: match[4] === 'm',
19
+ };
20
+ }
21
+ // 마우스 훅
22
+ function useMouse(onMouseClick, enabled = true) {
23
+ const { stdin } = useStdin();
24
+ useEffect(() => {
25
+ if (!enabled) {
26
+ process.stdout.write('\x1b[?1006l');
27
+ process.stdout.write('\x1b[?1000l');
28
+ return;
29
+ }
30
+ process.stdout.write('\x1b[?1000h');
31
+ process.stdout.write('\x1b[?1006h');
32
+ const handleData = (data) => {
33
+ const str = data.toString();
34
+ if (str.includes('\x1b[<')) {
35
+ const mouseEvent = parseMouseEvent(str);
36
+ if (mouseEvent && mouseEvent.button === 0 && !mouseEvent.release) {
37
+ onMouseClick(mouseEvent.row, mouseEvent.col);
38
+ }
39
+ }
40
+ };
41
+ stdin?.on('data', handleData);
42
+ return () => {
43
+ process.stdout.write('\x1b[?1006l');
44
+ process.stdout.write('\x1b[?1000l');
45
+ stdin?.off('data', handleData);
46
+ };
47
+ }, [stdin, onMouseClick, enabled]);
48
+ }
49
+ // Window 카드 컴포넌트
50
+ function WindowCard({ window, isSelected, }) {
51
+ const borderColor = isSelected ? 'cyan' : 'gray';
52
+ const nameColor = isSelected ? 'cyan' : 'white';
53
+ const statusIcon = window.isActive ? '●' : '○';
54
+ const statusColor = window.isActive ? 'green' : 'gray';
55
+ const projectName = window.projectRoot
56
+ ? window.projectRoot.split('/').slice(-1)[0]
57
+ : window.cwd.split('/').slice(-1)[0];
58
+ const threadName = window.worktreeBranch ?? window.name;
59
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: borderColor, paddingX: 1, marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " "] }), _jsx(Text, { color: nameColor, bold: isSelected, children: window.name }), isSelected && _jsx(Text, { color: "red", children: " \u2715" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: projectName }), _jsx(Text, { color: "gray", children: " \u2192 " }), _jsx(Text, { color: "cyan", children: threadName })] })] }));
60
+ }
61
+ // 새 Window 폼 컴포넌트
62
+ function NewWindowForm({ windowName, onWindowNameChange, rootPath, onRootPathChange, validation, onSubmit, onCancel, }) {
63
+ const [focusedField, setFocusedField] = useState('name');
64
+ const dirSuggestions = useDirectorySuggestions();
65
+ useInput(async (input, key) => {
66
+ if (key.escape) {
67
+ if (dirSuggestions.isOpen) {
68
+ dirSuggestions.clearSuggestions();
69
+ }
70
+ else {
71
+ onCancel();
72
+ }
73
+ return;
74
+ }
75
+ if (key.return) {
76
+ if (dirSuggestions.isOpen) {
77
+ const accepted = dirSuggestions.acceptSelected(rootPath);
78
+ if (accepted)
79
+ onRootPathChange(accepted);
80
+ }
81
+ else {
82
+ onSubmit();
83
+ }
84
+ return;
85
+ }
86
+ if (key.tab) {
87
+ if (dirSuggestions.isOpen) {
88
+ dirSuggestions.clearSuggestions();
89
+ }
90
+ setFocusedField(prev => prev === 'name' ? 'root' : 'name');
91
+ return;
92
+ }
93
+ if (key.upArrow) {
94
+ if (dirSuggestions.isOpen) {
95
+ dirSuggestions.selectPrev();
96
+ }
97
+ else {
98
+ setFocusedField('name');
99
+ }
100
+ return;
101
+ }
102
+ if (key.downArrow) {
103
+ if (dirSuggestions.isOpen) {
104
+ dirSuggestions.selectNext();
105
+ }
106
+ else {
107
+ setFocusedField('root');
108
+ }
109
+ return;
110
+ }
111
+ });
112
+ // Auto-trigger suggestions as user types
113
+ const handleRootPathChange = useCallback(async (value) => {
114
+ onRootPathChange(value);
115
+ if (value.endsWith('/')) {
116
+ const completed = await dirSuggestions.triggerComplete(value);
117
+ if (completed)
118
+ onRootPathChange(completed);
119
+ }
120
+ else if (value.length > 1 && value.includes('/')) {
121
+ await dirSuggestions.triggerComplete(value);
122
+ }
123
+ else {
124
+ dirSuggestions.clearSuggestions();
125
+ }
126
+ }, [dirSuggestions, onRootPathChange]);
127
+ // Validation indicator
128
+ let validationIcon = '';
129
+ let validationColor;
130
+ let validationText = '';
131
+ if (validation) {
132
+ if (validation.status === 'valid') {
133
+ if (validation.isGitRepo) {
134
+ validationIcon = '✓';
135
+ validationColor = 'green';
136
+ validationText = 'git repo';
137
+ }
138
+ else {
139
+ validationIcon = '✗';
140
+ validationColor = 'red';
141
+ validationText = 'not a git repo';
142
+ }
143
+ }
144
+ else if (validation.status === 'creatable') {
145
+ validationIcon = '✗';
146
+ validationColor = 'red';
147
+ validationText = 'not a git repo';
148
+ }
149
+ else {
150
+ validationIcon = '✗';
151
+ validationColor = 'red';
152
+ validationText = 'invalid path';
153
+ }
154
+ }
155
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "+ New Window" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: focusedField === 'name' ? 'cyan' : undefined, children: "Name: " }), _jsx(TextInput, { value: windowName, onChange: focusedField === 'name' ? onWindowNameChange : () => { }, placeholder: "window-name", focus: focusedField === 'name' })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: focusedField === 'root' ? 'cyan' : 'gray', children: "Root: " }), focusedField === 'root' ? (_jsx(TextInput, { value: rootPath, onChange: handleRootPathChange, placeholder: "/path/to/dir", focus: true })) : (_jsx(Text, { color: "gray", children: rootPath }))] }), validation && (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsxs(Text, { color: validationColor, children: [validationIcon, " ", validationText] })] })), dirSuggestions.isOpen && dirSuggestions.visibleSuggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [dirSuggestions.visibleSuggestions.map((s) => (_jsx(Box, { children: _jsxs(Text, { color: s.isSelected ? 'cyan' : 'gray', children: [s.isSelected ? ' > ' : ' ', s.name, "/"] }) }, s.name))), dirSuggestions.hasMore && (_jsx(Text, { color: "gray", children: " \u2191\u2193 for more" }))] })), dirSuggestions.isOpen ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "\u2191\u2193:select Enter:pick Esc:close" }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Tab:field Enter:ok Esc:cancel" }) }))] }));
156
+ }
157
+ // 삭제 확인 컴포넌트
158
+ function DeleteConfirm({ windowName, onConfirm, onCancel, }) {
159
+ useInput((input, key) => {
160
+ if (input === 'y' || input === 'Y') {
161
+ onConfirm();
162
+ }
163
+ else if (input === 'n' || input === 'N' || key.escape) {
164
+ onCancel();
165
+ }
166
+ });
167
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, children: [_jsxs(Text, { color: "red", children: ["Delete \"", windowName, "\"? "] }), _jsx(Text, { color: "gray", children: "(y/n)" })] }));
168
+ }
169
+ // 힌트 바 컴포넌트
170
+ function HintBar({ mode }) {
171
+ if (mode === 'new-window') {
172
+ return null;
173
+ }
174
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "\u2191\u2193/jk: nav Enter: switch r: refresh q: detach" }) }));
175
+ }
176
+ // 메인 대시보드 컴포넌트
177
+ function Dashboard({ workspaceRoot, repoName, currentBranch, initialWindows, tmuxAdapter, dashWindowIndex, paneHeight, }) {
178
+ const [windows, setWindows] = useState(initialWindows);
179
+ const [selectedIndex, setSelectedIndex] = useState(0);
180
+ const [inputMode, setInputMode] = useState('normal');
181
+ const [newWindowName, setNewWindowName] = useState('');
182
+ const [rootPath, setRootPath] = useState(workspaceRoot);
183
+ // Dynamic git repo detection from path validation
184
+ const { validation, isGitRepo: formIsGitRepo } = usePathValidation(rootPath);
185
+ // name 변경 핸들러 (공백 제거)
186
+ const handleWindowNameChange = useCallback((value) => {
187
+ const sanitized = value.replace(/\s/g, '');
188
+ setNewWindowName(sanitized);
189
+ }, []);
190
+ const [status, setStatus] = useState('');
191
+ const [isProcessing, setIsProcessing] = useState(false);
192
+ // Track which window's content is currently shown in the right pane
193
+ const activeWindowIdRef = useRef(null);
194
+ const statusTimerRef = useRef(null);
195
+ // Auto-clear status message after 2 seconds
196
+ const showStatus = useCallback((msg) => {
197
+ if (statusTimerRef.current)
198
+ clearTimeout(statusTimerRef.current);
199
+ setStatus(msg);
200
+ statusTimerRef.current = setTimeout(() => setStatus(''), 2000);
201
+ }, []);
202
+ // Window 목록 새로고침
203
+ const refreshWindows = useCallback(async () => {
204
+ const updatedWindows = await loadAllWindows(tmuxAdapter, dashWindowIndex);
205
+ setWindows(updatedWindows);
206
+ return { windows: updatedWindows };
207
+ }, [tmuxAdapter, dashWindowIndex]);
208
+ // Auto-select first thread on restore (mount)
209
+ useEffect(() => {
210
+ if (initialWindows.length === 0)
211
+ return;
212
+ const first = initialWindows[0];
213
+ if (first) {
214
+ void handleSelectWindow(first);
215
+ }
216
+ }, []);
217
+ // 레이아웃 행 계산 (마우스 클릭 매핑용)
218
+ const HEADER_ROWS = 3;
219
+ const NEW_WINDOW_ROWS = 3;
220
+ const WINDOW_START_ROW = HEADER_ROWS + NEW_WINDOW_ROWS + 2;
221
+ const WINDOW_HEIGHT = 4;
222
+ // 마우스 클릭 핸들러
223
+ const handleMouseClick = useCallback((row, col) => {
224
+ if (isProcessing || inputMode !== 'normal')
225
+ return;
226
+ // 새 Window 버튼 클릭
227
+ if (row >= HEADER_ROWS + 1 && row <= HEADER_ROWS + NEW_WINDOW_ROWS + 1) {
228
+ setInputMode('new-window');
229
+ setNewWindowName('');
230
+ setRootPath(workspaceRoot);
231
+ return;
232
+ }
233
+ // Window 리스트 클릭
234
+ if (row >= WINDOW_START_ROW && windows.length > 0) {
235
+ const windowIndex = Math.floor((row - WINDOW_START_ROW) / WINDOW_HEIGHT);
236
+ if (windowIndex >= 0 && windowIndex < windows.length) {
237
+ setSelectedIndex(windowIndex);
238
+ // 클릭하면 바로 전환
239
+ void handleSelectWindow(windows[windowIndex]);
240
+ }
241
+ }
242
+ }, [isProcessing, inputMode, windows, workspaceRoot]);
243
+ // 마우스 이벤트 (normal 모드에서만 활성화)
244
+ useMouse(handleMouseClick, inputMode === 'normal');
245
+ // 오른쪽 pane 찾기 (대시보드 pane index 0 제외)
246
+ const findRightPane = async () => {
247
+ const panes = await tmuxAdapter.listPanes();
248
+ return panes.find(p => p.index !== 0);
249
+ };
250
+ // Window 선택 (오른쪽 pane에 swap)
251
+ const handleSelectWindow = async (win) => {
252
+ try {
253
+ // Already showing this window — just focus the right pane
254
+ if (win.windowId === activeWindowIdRef.current) {
255
+ const rightPane = await findRightPane();
256
+ if (rightPane)
257
+ await tmuxAdapter.selectPane(rightPane.id);
258
+ return;
259
+ }
260
+ // Swap current right pane back to its original window
261
+ if (activeWindowIdRef.current) {
262
+ const rightPane = await findRightPane();
263
+ if (rightPane) {
264
+ try {
265
+ await tmuxAdapter.swapPaneWithWindow(activeWindowIdRef.current, rightPane.id);
266
+ }
267
+ catch {
268
+ // Original window may have been killed externally
269
+ }
270
+ }
271
+ }
272
+ // Swap selected window's pane into the right position
273
+ const rightPane = await findRightPane();
274
+ if (rightPane) {
275
+ await tmuxAdapter.swapPaneWithWindow(win.windowId, rightPane.id);
276
+ activeWindowIdRef.current = win.windowId;
277
+ await tmuxAdapter.selectPane((await findRightPane()).id);
278
+ }
279
+ showStatus(`Switched: ${win.name}`);
280
+ }
281
+ catch (error) {
282
+ showStatus(`Error: ${error.message}`);
283
+ }
284
+ };
285
+ // 키보드 입력 처리 (normal 모드)
286
+ useInput(async (input, key) => {
287
+ // Allow Escape to cancel during processing
288
+ if (isProcessing) {
289
+ if (key.escape) {
290
+ setIsProcessing(false);
291
+ setInputMode('normal');
292
+ showStatus('Cancelled');
293
+ }
294
+ return;
295
+ }
296
+ if (inputMode !== 'normal')
297
+ return;
298
+ if (input === 'j' || key.downArrow) {
299
+ setSelectedIndex(prev => (prev + 1) % Math.max(windows.length, 1));
300
+ }
301
+ else if (input === 'k' || key.upArrow) {
302
+ setSelectedIndex(prev => (prev - 1 + Math.max(windows.length, 1)) % Math.max(windows.length, 1));
303
+ }
304
+ else if (input === 'q') {
305
+ try {
306
+ // Swap active pane back to its original window before quitting
307
+ if (activeWindowIdRef.current) {
308
+ const rightPane = await findRightPane();
309
+ if (rightPane) {
310
+ try {
311
+ await tmuxAdapter.swapPaneWithWindow(activeWindowIdRef.current, rightPane.id);
312
+ }
313
+ catch {
314
+ // Original window may have been killed externally
315
+ }
316
+ }
317
+ }
318
+ // Detach + kill dashboard window in one atomic tmux command.
319
+ // Thread windows stay alive in the detached session.
320
+ const sessionName = `csq-${repoName}`;
321
+ await tmuxAdapter.detachAndKillWindow(sessionName, dashWindowIndex);
322
+ }
323
+ catch {
324
+ // Detach/kill may race, ignore
325
+ }
326
+ }
327
+ else if (key.return) {
328
+ const selected = windows[selectedIndex];
329
+ if (selected) {
330
+ await handleSelectWindow(selected);
331
+ }
332
+ }
333
+ else if (input === 'n' || input === '+') {
334
+ setInputMode('new-window');
335
+ setNewWindowName('');
336
+ setRootPath(workspaceRoot);
337
+ }
338
+ else if (input === 'd') {
339
+ if (windows[selectedIndex]) {
340
+ setInputMode('confirm-delete');
341
+ }
342
+ }
343
+ else if (input === 'r') {
344
+ process.stdout.write('\x1b[2J\x1b[H');
345
+ process.stdout.emit('resize');
346
+ await refreshWindows();
347
+ showStatus('Refreshed');
348
+ }
349
+ });
350
+ // 새 Window 생성
351
+ const handleCreateWindow = async () => {
352
+ if (!newWindowName.trim()) {
353
+ showStatus('Window name required');
354
+ return;
355
+ }
356
+ if (!validation || !formIsGitRepo) {
357
+ showStatus('Root must be a git repo');
358
+ return;
359
+ }
360
+ setIsProcessing(true);
361
+ setStatus(`Creating ${newWindowName}...`);
362
+ try {
363
+ const expandedRoot = expandTilde(rootPath);
364
+ const newThread = await createThread(workspaceRoot, newWindowName.trim(), expandedRoot);
365
+ const newWindowId = await tmuxAdapter.createNewWindow(newThread.path, newWindowName.trim());
366
+ const { windows: updatedWindows } = await refreshWindows();
367
+ setInputMode('normal');
368
+ setNewWindowName('');
369
+ showStatus(`Created: ${newWindowName}`);
370
+ // Auto-select the new window into the right pane
371
+ const newWin = updatedWindows.find(w => w.windowId === newWindowId);
372
+ if (newWin) {
373
+ await handleSelectWindow(newWin);
374
+ }
375
+ }
376
+ catch (error) {
377
+ showStatus(`Error: ${error.message}`);
378
+ }
379
+ setIsProcessing(false);
380
+ };
381
+ // Window 삭제
382
+ const handleDeleteWindow = async () => {
383
+ const selected = windows[selectedIndex];
384
+ if (!selected)
385
+ return;
386
+ setIsProcessing(true);
387
+ setStatus(`Deleting ${selected.name}...`);
388
+ try {
389
+ // If this window's pane is currently in the right pane, swap it back first
390
+ if (selected.windowId === activeWindowIdRef.current) {
391
+ const rightPane = await findRightPane();
392
+ if (rightPane) {
393
+ try {
394
+ await tmuxAdapter.swapPaneWithWindow(selected.windowId, rightPane.id);
395
+ }
396
+ catch { /* ignore */ }
397
+ }
398
+ activeWindowIdRef.current = null;
399
+ }
400
+ // Kill the tmux window
401
+ await deleteWindowById(tmuxAdapter, selected.windowId);
402
+ // If it's a worktree-based window, also remove the worktree + branch
403
+ if (selected.worktreeBranch) {
404
+ try {
405
+ await deleteThread(workspaceRoot, {
406
+ id: selected.cwd,
407
+ name: selected.worktreeBranch,
408
+ path: selected.cwd,
409
+ branch: selected.worktreeBranch,
410
+ });
411
+ }
412
+ catch {
413
+ // Worktree cleanup failed — window is already deleted
414
+ }
415
+ }
416
+ const { windows: updatedWindows } = await refreshWindows();
417
+ const newIndex = Math.min(selectedIndex, Math.max(0, updatedWindows.length - 1));
418
+ setSelectedIndex(newIndex);
419
+ // Auto-select the previous/next window into the right pane
420
+ const fallbackWin = updatedWindows[newIndex];
421
+ if (fallbackWin) {
422
+ await handleSelectWindow(fallbackWin);
423
+ }
424
+ setInputMode('normal');
425
+ showStatus(`Deleted: ${selected.name}`);
426
+ }
427
+ catch (error) {
428
+ showStatus(`Error: ${error.message}`);
429
+ setInputMode('normal');
430
+ }
431
+ setIsProcessing(false);
432
+ };
433
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: paneHeight, children: [_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "white", children: repoName }), _jsxs(Text, { color: "cyan", children: [" [", currentBranch, "]"] }), _jsxs(Text, { color: "gray", children: [" (", windows.length, ")"] })] }), inputMode === 'new-window' ? (_jsx(NewWindowForm, { windowName: newWindowName, onWindowNameChange: handleWindowNameChange, rootPath: rootPath, onRootPathChange: setRootPath, validation: validation, onSubmit: handleCreateWindow, onCancel: () => {
434
+ setInputMode('normal');
435
+ setNewWindowName('');
436
+ } })) : (_jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginBottom: 1, children: _jsx(Text, { color: "gray", children: "+ New Window (press + or n)" }) })), inputMode === 'confirm-delete' && windows[selectedIndex] && (_jsx(DeleteConfirm, { windowName: windows[selectedIndex].name, onConfirm: handleDeleteWindow, onCancel: () => setInputMode('normal') })), _jsx(Box, { flexDirection: "column", marginTop: 1, children: windows.length === 0 ? (_jsx(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: _jsx(Text, { color: "gray", children: "No windows yet" }) })) : (windows.map((win, i) => (_jsx(WindowCard, { window: win, isSelected: i === selectedIndex }, win.windowId)))) }), _jsx(HintBar, { mode: inputMode }), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: status }) }))] }));
437
+ }
438
+ // 대시보드 실행
439
+ export async function runInkDashboard(config) {
440
+ render(_jsx(Dashboard, { ...config }));
441
+ await new Promise(() => { });
442
+ }