ccdock 0.1.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.
@@ -0,0 +1,116 @@
1
+ export type KeyAction =
2
+ | { type: "up" }
3
+ | { type: "down" }
4
+ | { type: "enter" }
5
+ | { type: "tab" }
6
+ | { type: "quit" }
7
+ | { type: "new" }
8
+ | { type: "delete" }
9
+ | { type: "compact" }
10
+ | { type: "log" }
11
+ | { type: "realign" }
12
+ | { type: "mouse_click"; row: number; col: number }
13
+ | { type: "unknown" };
14
+
15
+ export type WizardKeyAction =
16
+ | { type: "up" }
17
+ | { type: "down" }
18
+ | { type: "enter" }
19
+ | { type: "escape" }
20
+ | { type: "backspace" }
21
+ | { type: "char"; char: string }
22
+ | { type: "unknown" };
23
+
24
+ export function parseKey(data: Buffer): KeyAction {
25
+ // SGR mouse: \x1b[<button;col;rowM (press) or \x1b[<button;col;rowm (release)
26
+ const s = data.toString();
27
+ const sgrMatch = s.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
28
+ if (sgrMatch) {
29
+ const button = Number.parseInt(sgrMatch[1]!, 10);
30
+ const col = Number.parseInt(sgrMatch[2]!, 10);
31
+ const row = Number.parseInt(sgrMatch[3]!, 10);
32
+ const isPress = sgrMatch[4] === "M";
33
+ // button 0 = left click press
34
+ if (isPress && button === 0) {
35
+ return { type: "mouse_click", row, col };
36
+ }
37
+ // Scroll up/down
38
+ if (isPress && button === 64) return { type: "up" };
39
+ if (isPress && button === 65) return { type: "down" };
40
+ return { type: "unknown" };
41
+ }
42
+
43
+ // Escape sequences for arrow keys
44
+ if (s === "\x1b[A" || s === "k") return { type: "up" };
45
+ if (s === "\x1b[B" || s === "j") return { type: "down" };
46
+
47
+ // Enter
48
+ if (s === "\r" || s === "\n") return { type: "enter" };
49
+
50
+ // Tab
51
+ if (s === "\t") return { type: "tab" };
52
+
53
+ // Quit: q or Ctrl+C
54
+ if (s === "q" || s === "\x03") return { type: "quit" };
55
+
56
+ // New session: n
57
+ if (s === "n") return { type: "new" };
58
+
59
+ // Delete: d or x
60
+ if (s === "d" || s === "x") return { type: "delete" };
61
+
62
+ // Compact mode: c
63
+ if (s === "c") return { type: "compact" };
64
+
65
+ // Activity log: l
66
+ if (s === "l") return { type: "log" };
67
+
68
+ // Realign VS Code windows: r
69
+ if (s === "r") return { type: "realign" };
70
+
71
+ return { type: "unknown" };
72
+ }
73
+
74
+ export function parseKeyWizard(data: Buffer): WizardKeyAction {
75
+ const s = data.toString();
76
+
77
+ // Escape sequences for arrow keys
78
+ if (s === "\x1b[A") return { type: "up" };
79
+ if (s === "\x1b[B") return { type: "down" };
80
+
81
+ // Enter
82
+ if (s === "\r" || s === "\n") return { type: "enter" };
83
+
84
+ // Escape or Ctrl+C
85
+ if (s === "\x1b" || s === "\x03") return { type: "escape" };
86
+
87
+ // Backspace
88
+ if (s === "\x7f" || s === "\x08") return { type: "backspace" };
89
+
90
+ // Printable characters
91
+ if (s.length === 1 && s.charCodeAt(0) >= 32 && s.charCodeAt(0) <= 126) {
92
+ return { type: "char", char: s };
93
+ }
94
+
95
+ return { type: "unknown" };
96
+ }
97
+
98
+ // SGR extended mouse mode: supports coordinates > 223
99
+ const MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h";
100
+ const MOUSE_DISABLE = "\x1b[?1000l\x1b[?1006l";
101
+
102
+ export function enableRawMode(): void {
103
+ if (process.stdin.isTTY) {
104
+ process.stdin.setRawMode(true);
105
+ process.stdin.resume();
106
+ process.stdout.write(MOUSE_ENABLE);
107
+ }
108
+ }
109
+
110
+ export function disableRawMode(): void {
111
+ if (process.stdin.isTTY) {
112
+ process.stdout.write(MOUSE_DISABLE);
113
+ process.stdin.setRawMode(false);
114
+ process.stdin.pause();
115
+ }
116
+ }
@@ -0,0 +1,321 @@
1
+ import type { DeleteConfirm, SidebarState, WorkspaceSession } from "../types.ts";
2
+
3
+ const SPINNER_FRAMES = [
4
+ "\u280b",
5
+ "\u2819",
6
+ "\u2839",
7
+ "\u2838",
8
+ "\u283c",
9
+ "\u2834",
10
+ "\u2826",
11
+ "\u2827",
12
+ "\u2807",
13
+ "\u280f",
14
+ ];
15
+ import {
16
+ BOLD,
17
+ BOX,
18
+ CLEAR_SCREEN,
19
+ COLORS,
20
+ CURSOR_HOME,
21
+ DIM,
22
+ RESET,
23
+ clearLine,
24
+ moveCursor,
25
+ shortenHome,
26
+ statusColor,
27
+ statusIcon,
28
+ truncate,
29
+ visibleLength,
30
+ } from "./ansi.ts";
31
+
32
+ function padRight(str: string, len: number): string {
33
+ const visible = visibleLength(str);
34
+ if (visible >= len) return str;
35
+ return str + " ".repeat(len - visible);
36
+ }
37
+
38
+ function renderDeleteConfirm(deleteConfirm: DeleteConfirm, cols: number): string[] {
39
+ const lines: string[] = [];
40
+ const width = Math.max(cols - 6, 16);
41
+
42
+ lines.push(` ${COLORS.border}${BOX.horizontal.repeat(width)}${RESET}`);
43
+ lines.push(` ${BOLD}${COLORS.error} Delete session?${RESET}`);
44
+ lines.push("");
45
+
46
+ const options = ["Remove session only", "Remove session + worktree"];
47
+
48
+ for (let i = 0; i < options.length; i++) {
49
+ const opt = options[i];
50
+ if (!opt) continue;
51
+ const isSelected = i === deleteConfirm.selectedIndex;
52
+ const marker = isSelected ? `${COLORS.highlight}\u25b6${RESET}` : " ";
53
+ const label = isSelected
54
+ ? `${BOLD}${COLORS.title}${opt}${RESET}`
55
+ : `${COLORS.subtitle}${opt}${RESET}`;
56
+ lines.push(` ${marker} ${label}`);
57
+ }
58
+
59
+ lines.push("");
60
+ lines.push(` ${COLORS.muted}Enter: confirm | Esc: cancel${RESET}`);
61
+ lines.push(` ${COLORS.border}${BOX.horizontal.repeat(width)}${RESET}`);
62
+
63
+ return lines;
64
+ }
65
+
66
+ function renderCard(
67
+ session: WorkspaceSession,
68
+ isSelected: boolean,
69
+ cols: number,
70
+ animFrame: number,
71
+ compact: boolean,
72
+ deleteConfirm: DeleteConfirm | null,
73
+ sessionIndex: number,
74
+ ): string[] {
75
+ const lines: string[] = [];
76
+ const width = Math.max(cols - 2, 20);
77
+ const bg = isSelected ? COLORS.bgSelected : "";
78
+ const resetBg = isSelected ? RESET : "";
79
+
80
+ // Colors based on editor state
81
+ // Focused: white border, normal title
82
+ // Open: normal border + green ● dot
83
+ // Closed: dim everything
84
+ const editorState = session.editorState;
85
+ const isFocused = editorState === "focused";
86
+ const isClosed = editorState === "closed";
87
+ const isLaunching = editorState === "launching";
88
+ const borderColor = isFocused
89
+ ? COLORS.borderFocused
90
+ : isClosed
91
+ ? COLORS.borderClosed
92
+ : COLORS.border;
93
+ const titleColor = isClosed ? COLORS.editorClosed : COLORS.title;
94
+ const detailColor = isClosed ? COLORS.editorClosed : COLORS.subtitle;
95
+ const dimAll = isClosed ? DIM : "";
96
+
97
+ // Status dot: spinning for launching, green solid for open/focused, none for closed
98
+ let openDot = "";
99
+ if (isLaunching) {
100
+ const frame = SPINNER_FRAMES[animFrame % SPINNER_FRAMES.length]!;
101
+ openDot = `${COLORS.waiting}${frame}${RESET} `;
102
+ } else if (!isClosed) {
103
+ openDot = `${COLORS.running}\u25cf${RESET} `;
104
+ }
105
+
106
+ // Card border top
107
+ const topBorder = `${dimAll}${borderColor}${BOX.topLeft}${BOX.horizontal.repeat(width - 2)}${BOX.topRight}${RESET}`;
108
+ lines.push(topBorder);
109
+
110
+ // Title line: #N + dot (if open) + icon + repo:branch
111
+ const icon = "\uf418";
112
+ const sessionNum = `${COLORS.muted}#${sessionIndex + 1}${RESET} `;
113
+ const titleText = `${titleColor}${icon} ${session.repoName}:${session.branch}${RESET}`;
114
+ const title = `${sessionNum}${openDot}${titleText}`;
115
+ const titleTruncated = truncate(title, width - 4);
116
+ const titleLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${bg}${padRight(titleTruncated, width - 4)}${resetBg}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
117
+ lines.push(titleLine);
118
+
119
+ if (!compact) {
120
+ // Path line
121
+ const shortPath = shortenHome(session.worktreePath);
122
+ const pathTruncated = truncate(shortPath, width - 4);
123
+ const pathColor = editorState === "closed" ? COLORS.editorClosed : COLORS.subtitle;
124
+ const pathLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${bg}${pathColor}${padRight(pathTruncated, width - 4)}${resetBg}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
125
+ lines.push(pathLine);
126
+
127
+ // Agent status lines
128
+ if (session.agents.length === 0) {
129
+ const noAgent = `${DIM}no agents${RESET}`;
130
+ const agentLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${bg}${padRight(noAgent, width - 4)}${resetBg}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
131
+ lines.push(agentLine);
132
+ } else {
133
+ for (const agent of session.agents) {
134
+ const sIcon = statusIcon(agent.status, animFrame);
135
+ const sColor = statusColor(agent.status);
136
+ const statusText = `${sColor}${sIcon} ${agent.status}${RESET}`;
137
+ const agentInfo = `${statusText} ${detailColor}${agent.agentType}${RESET}`;
138
+ const agentLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${bg}${padRight(agentInfo, width - 4)}${resetBg}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
139
+ lines.push(agentLine);
140
+
141
+ // Show latest tool activity
142
+ if (agent.toolName) {
143
+ const detail = agent.toolDetail
144
+ ? `${detailColor}${agent.toolName}${RESET} ${detailColor}${agent.toolDetail}${RESET}`
145
+ : `${detailColor}${agent.toolName}${RESET}`;
146
+ const detailTruncated = truncate(` ${detail}`, width - 4);
147
+ const detailLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${bg}${padRight(detailTruncated, width - 4)}${resetBg}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
148
+ lines.push(detailLine);
149
+ }
150
+ }
151
+ }
152
+ } else {
153
+ // Compact: just show agent status inline
154
+ const agentSummary =
155
+ session.agents.length > 0
156
+ ? session.agents
157
+ .map((a) => `${statusColor(a.status)}${statusIcon(a.status, animFrame)}${RESET}`)
158
+ .join(" ")
159
+ : `${DIM}no agents${RESET}`;
160
+ const compactLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${bg}${padRight(agentSummary, width - 4)}${resetBg}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
161
+ lines.push(compactLine);
162
+ }
163
+
164
+ // Delete confirmation inline
165
+ if (isSelected && deleteConfirm && deleteConfirm.sessionId === session.id) {
166
+ const confirmLines = renderDeleteConfirm(deleteConfirm, cols);
167
+ for (const cl of confirmLines) {
168
+ const confirmLine = `${borderColor}${BOX.vertical}${RESET} ${padRight(cl, width - 4)} ${borderColor}${BOX.vertical}${RESET}`;
169
+ lines.push(confirmLine);
170
+ }
171
+ }
172
+
173
+ // Card border bottom
174
+ const bottomBorder = `${dimAll}${borderColor}${BOX.bottomLeft}${BOX.horizontal.repeat(width - 2)}${BOX.bottomRight}${RESET}`;
175
+ lines.push(bottomBorder);
176
+
177
+ return lines;
178
+ }
179
+
180
+ function renderActivityLog(state: SidebarState, maxLines: number): string[] {
181
+ const lines: string[] = [];
182
+ const width = Math.max(state.cols - 2, 20);
183
+
184
+ lines.push(`${COLORS.border}${BOX.horizontal.repeat(width)}${RESET}`);
185
+ lines.push(`${BOLD}${COLORS.highlight} Activity Log${RESET}`);
186
+
187
+ const entries = state.activityLog.slice(-maxLines);
188
+ for (const entry of entries) {
189
+ const sessionTag =
190
+ entry.sessionIndex >= 0 ? `${COLORS.accent}#${entry.sessionIndex + 1}${RESET} ` : "";
191
+ const detail = entry.toolDetail ? ` ${COLORS.muted}${entry.toolDetail}${RESET}` : "";
192
+ const line = `${COLORS.muted}${entry.time}${RESET} ${sessionTag}${COLORS.highlight}${entry.tool}${RESET}${detail}`;
193
+ lines.push(` ${truncate(line, width - 2)}`);
194
+ }
195
+
196
+ if (entries.length === 0) {
197
+ lines.push(`${DIM} (no activity yet)${RESET}`);
198
+ }
199
+
200
+ return lines;
201
+ }
202
+
203
+ function renderFooter(cols: number): string[] {
204
+ const line1 = [
205
+ `${BOLD}j/k${RESET} nav`,
206
+ `${BOLD}Enter${RESET} focus`,
207
+ `${BOLD}n${RESET} new`,
208
+ `${BOLD}d${RESET} del`,
209
+ `${BOLD}r${RESET} realign`,
210
+ ].join(`${COLORS.muted} | ${RESET}`);
211
+ const line2 = [`${BOLD}c${RESET} compact`, `${BOLD}l${RESET} log`, `${BOLD}q${RESET} quit`].join(
212
+ `${COLORS.muted} | ${RESET}`,
213
+ );
214
+ return [
215
+ `${COLORS.muted}${BOX.horizontal.repeat(Math.max(cols - 2, 1))}${RESET}`,
216
+ ` ${truncate(line1, cols - 2)}`,
217
+ ` ${truncate(line2, cols - 2)}`,
218
+ ];
219
+ }
220
+
221
+ function renderQuitConfirm(selectedIndex: number, cols: number): string[] {
222
+ const width = Math.max(cols - 4, 20);
223
+ const lines: string[] = [];
224
+ lines.push("");
225
+ lines.push(`${BOLD}${COLORS.highlight} Quit ccdock?${RESET}`);
226
+ lines.push("");
227
+
228
+ const options = [
229
+ "Quit sidebar only (keep VS Code open)",
230
+ "Quit sidebar and close all VS Code windows",
231
+ ];
232
+
233
+ for (let i = 0; i < options.length; i++) {
234
+ const isSelected = i === selectedIndex;
235
+ const marker = isSelected ? `${COLORS.highlight}\u25b6${RESET}` : " ";
236
+ const label = isSelected
237
+ ? `${BOLD}${COLORS.title}${options[i]}${RESET}`
238
+ : `${COLORS.subtitle}${options[i]}${RESET}`;
239
+ lines.push(truncate(` ${marker} ${label}`, width));
240
+ }
241
+
242
+ lines.push("");
243
+ lines.push(`${COLORS.muted} Enter: confirm | Esc: cancel${RESET}`);
244
+ return lines;
245
+ }
246
+
247
+ export function renderSidebar(state: SidebarState): string {
248
+ const output: string[] = [];
249
+ output.push(CLEAR_SCREEN + CURSOR_HOME);
250
+
251
+ // Quit confirmation takes over the screen
252
+ if (state.quitConfirm) {
253
+ output.push(...renderQuitConfirm(state.quitConfirm.selectedIndex, state.cols));
254
+ return output.join("\n");
255
+ }
256
+
257
+ // Header
258
+ const header = `${BOLD}${COLORS.highlight} ccdock${RESET} ${COLORS.muted}(${state.sessions.length} sessions)${RESET}`;
259
+ output.push(header);
260
+ output.push("");
261
+
262
+ // Calculate available space
263
+ const footerHeight = 3;
264
+ const headerHeight = 2;
265
+ const logHeight = state.showActivityLog ? Math.min(8, state.activityLog.length + 2) : 0;
266
+ const availableForCards = state.rows - headerHeight - footerHeight - logHeight;
267
+
268
+ // Render session cards
269
+ let linesUsed = 0;
270
+ state.cardRowRanges = [];
271
+ const cardStartOffset = output.length; // rows before cards (header)
272
+ for (let i = 0; i < state.sessions.length; i++) {
273
+ const session = state.sessions[i];
274
+ if (!session) continue;
275
+ const isSelected = i === state.selectedIndex;
276
+ const cardLines = renderCard(
277
+ session,
278
+ isSelected,
279
+ state.cols,
280
+ state.animationFrame,
281
+ state.compactMode,
282
+ state.deleteConfirm,
283
+ i,
284
+ );
285
+
286
+ if (linesUsed + cardLines.length > availableForCards) break;
287
+ const startRow = cardStartOffset + linesUsed + 1; // 1-based row
288
+ state.cardRowRanges.push({
289
+ sessionIndex: i,
290
+ startRow,
291
+ endRow: startRow + cardLines.length - 1,
292
+ });
293
+ output.push(...cardLines);
294
+ linesUsed += cardLines.length;
295
+ }
296
+
297
+ if (state.sessions.length === 0) {
298
+ output.push("");
299
+ output.push(`${DIM} No active sessions.${RESET}`);
300
+ output.push(`${DIM} Press 'n' to create a new session.${RESET}`);
301
+ }
302
+
303
+ // Fill remaining space
304
+ const currentLines = output.length;
305
+ const targetLine = state.rows - footerHeight - logHeight;
306
+ for (let i = currentLines; i < targetLine; i++) {
307
+ output.push("");
308
+ }
309
+
310
+ // Activity log
311
+ if (state.showActivityLog) {
312
+ const logLines = renderActivityLog(state, 5);
313
+ output.push(...logLines);
314
+ }
315
+
316
+ // Footer
317
+ const footerLines = renderFooter(state.cols);
318
+ output.push(...footerLines);
319
+
320
+ return output.join("\n");
321
+ }
@@ -0,0 +1,166 @@
1
+ import type { RepoInfo, WizardState, WorktreeEntry } from "../types.ts";
2
+ import { BOLD, BOX, CLEAR_SCREEN, COLORS, CURSOR_HOME, DIM, RESET, truncate } from "./ansi.ts";
3
+
4
+ function renderRepoList(
5
+ repos: RepoInfo[],
6
+ selectedIndex: number,
7
+ filter: string,
8
+ cols: number,
9
+ ): string[] {
10
+ const lines: string[] = [];
11
+ const width = Math.max(cols - 4, 20);
12
+
13
+ lines.push(`${BOLD}${COLORS.highlight} Select Repository${RESET}`);
14
+ lines.push("");
15
+
16
+ if (filter) {
17
+ lines.push(`${COLORS.muted} Filter: ${RESET}${filter}`);
18
+ lines.push("");
19
+ }
20
+
21
+ const filtered = repos.filter((r) => r.name.toLowerCase().includes(filter.toLowerCase()));
22
+
23
+ if (filtered.length === 0) {
24
+ lines.push(`${DIM} No repos matching "${filter}"${RESET}`);
25
+ } else {
26
+ for (let i = 0; i < filtered.length; i++) {
27
+ const repo = filtered[i];
28
+ if (!repo) continue;
29
+ const isSelected = i === selectedIndex;
30
+ const marker = isSelected ? `${COLORS.highlight}\u25b6${RESET}` : " ";
31
+ const name = isSelected
32
+ ? `${BOLD}${COLORS.title}${repo.name}${RESET}`
33
+ : `${COLORS.subtitle}${repo.name}${RESET}`;
34
+ const branch = `${COLORS.muted}(${repo.defaultBranch})${RESET}`;
35
+ const line = ` ${marker} ${name} ${branch}`;
36
+ lines.push(truncate(line, width));
37
+ }
38
+ }
39
+
40
+ lines.push("");
41
+ lines.push(`${COLORS.muted} j/k: navigate | Enter: select | Esc: cancel${RESET}`);
42
+ lines.push(`${COLORS.muted} Type to filter repos${RESET}`);
43
+
44
+ return lines;
45
+ }
46
+
47
+ function renderModeSelect(repo: RepoInfo, selectedIndex: number, cols: number): string[] {
48
+ const lines: string[] = [];
49
+ const width = Math.max(cols - 4, 20);
50
+
51
+ lines.push(`${BOLD}${COLORS.highlight} Create Session: ${repo.name}${RESET}`);
52
+ lines.push("");
53
+ lines.push(`${COLORS.muted} Select mode:${RESET}`);
54
+ lines.push("");
55
+
56
+ const modes = [
57
+ { label: "Create new worktree (git wt)", desc: "Create a new feature branch worktree" },
58
+ { label: "Use existing worktree", desc: "Select from existing worktrees" },
59
+ { label: "Open repository root", desc: "Open the main repository directory" },
60
+ ];
61
+
62
+ for (let i = 0; i < modes.length; i++) {
63
+ const mode = modes[i];
64
+ if (!mode) continue;
65
+ const isSelected = i === selectedIndex;
66
+ const marker = isSelected ? `${COLORS.highlight}\u25b6${RESET}` : " ";
67
+ const label = isSelected
68
+ ? `${BOLD}${COLORS.title}${mode.label}${RESET}`
69
+ : `${COLORS.subtitle}${mode.label}${RESET}`;
70
+ const desc = `${COLORS.muted}${mode.desc}${RESET}`;
71
+ lines.push(truncate(` ${marker} ${label}`, width));
72
+ lines.push(truncate(` ${desc}`, width));
73
+ }
74
+
75
+ lines.push("");
76
+ lines.push(`${COLORS.muted} j/k: navigate | Enter: select | Esc: back${RESET}`);
77
+
78
+ return lines;
79
+ }
80
+
81
+ function renderWorktreeList(
82
+ repo: RepoInfo,
83
+ worktrees: WorktreeEntry[],
84
+ selectedIndex: number,
85
+ cols: number,
86
+ ): string[] {
87
+ const lines: string[] = [];
88
+ const width = Math.max(cols - 4, 20);
89
+
90
+ lines.push(`${BOLD}${COLORS.highlight} Select Worktree: ${repo.name}${RESET}`);
91
+ lines.push("");
92
+
93
+ if (worktrees.length === 0) {
94
+ lines.push(`${DIM} No existing worktrees found.${RESET}`);
95
+ } else {
96
+ for (let i = 0; i < worktrees.length; i++) {
97
+ const wt = worktrees[i];
98
+ if (!wt) continue;
99
+ const isSelected = i === selectedIndex;
100
+ const marker = isSelected ? `${COLORS.highlight}\u25b6${RESET}` : " ";
101
+ const branchLabel = isSelected
102
+ ? `${BOLD}${COLORS.title}${wt.branch || "(detached)"}${RESET}`
103
+ : `${COLORS.subtitle}${wt.branch || "(detached)"}${RESET}`;
104
+ const pathLabel = `${COLORS.muted}${wt.path}${RESET}`;
105
+ lines.push(truncate(` ${marker} ${branchLabel}`, width));
106
+ lines.push(truncate(` ${pathLabel}`, width));
107
+ }
108
+ }
109
+
110
+ lines.push("");
111
+ lines.push(`${COLORS.muted} j/k: navigate | Enter: select | Esc: back${RESET}`);
112
+
113
+ return lines;
114
+ }
115
+
116
+ function renderBranchInput(repo: RepoInfo, branchName: string, cols: number): string[] {
117
+ const lines: string[] = [];
118
+ const width = Math.max(cols - 4, 20);
119
+
120
+ lines.push(`${BOLD}${COLORS.highlight} Create Session: ${repo.name}${RESET}`);
121
+ lines.push("");
122
+ lines.push(`${COLORS.muted} Enter branch name:${RESET}`);
123
+ lines.push("");
124
+ lines.push(` ${COLORS.border}${BOX.horizontal.repeat(width - 6)}${RESET}`);
125
+ lines.push(` ${BOLD}${branchName}${RESET}\u2588`);
126
+ lines.push(` ${COLORS.border}${BOX.horizontal.repeat(width - 6)}${RESET}`);
127
+ lines.push("");
128
+
129
+ if (branchName) {
130
+ const preview = `${repo.name}--${branchName.replace(/\//g, "-")}`;
131
+ lines.push(`${COLORS.muted} Worktree dir: ${preview}${RESET}`);
132
+ }
133
+
134
+ lines.push("");
135
+ lines.push(`${COLORS.muted} Enter: create | Esc: back${RESET}`);
136
+
137
+ return lines;
138
+ }
139
+
140
+ export function renderWizard(wizard: WizardState, cols: number): string {
141
+ if (!wizard) return "";
142
+
143
+ const output: string[] = [];
144
+ output.push(CLEAR_SCREEN + CURSOR_HOME);
145
+
146
+ let content: string[];
147
+
148
+ switch (wizard.step) {
149
+ case "select-repo":
150
+ content = renderRepoList(wizard.repos, wizard.selectedIndex, wizard.filter, cols);
151
+ break;
152
+ case "select-mode":
153
+ content = renderModeSelect(wizard.repo, wizard.selectedIndex, cols);
154
+ break;
155
+ case "select-worktree":
156
+ content = renderWorktreeList(wizard.repo, wizard.worktrees, wizard.selectedIndex, cols);
157
+ break;
158
+ case "enter-branch":
159
+ content = renderBranchInput(wizard.repo, wizard.branchName, cols);
160
+ break;
161
+ }
162
+
163
+ output.push(...content);
164
+
165
+ return output.join("\n");
166
+ }
package/src/types.ts ADDED
@@ -0,0 +1,86 @@
1
+ export type AgentType = "claude-code" | "codex";
2
+ export type AgentStatus = "running" | "waiting" | "idle" | "error" | "unknown";
3
+
4
+ export interface AgentState {
5
+ sessionId: string;
6
+ agentType: AgentType;
7
+ status: AgentStatus;
8
+ prompt: string;
9
+ toolName: string;
10
+ toolDetail: string; // human-readable detail from tool_input
11
+ cwd: string;
12
+ updatedAt: number; // Date.now()
13
+ }
14
+
15
+ export type EditorState = "focused" | "open" | "closed" | "launching";
16
+
17
+ export interface WorkspaceSession {
18
+ id: string; // unique session ID (uuid or short hash)
19
+ sessionName: string; // display name
20
+ worktreePath: string;
21
+ branch: string;
22
+ repoName: string; // extracted from path
23
+ agents: AgentState[]; // populated from state files
24
+ editorState: EditorState; // VS Code window state
25
+ createdAt: number;
26
+ lastActiveAt: number;
27
+ }
28
+
29
+ export interface RepoInfo {
30
+ name: string;
31
+ path: string;
32
+ defaultBranch: string;
33
+ }
34
+
35
+ export interface WorktreeEntry {
36
+ path: string;
37
+ branch: string;
38
+ }
39
+
40
+ export interface DeleteConfirm {
41
+ sessionId: string;
42
+ worktreePath: string;
43
+ selectedIndex: number; // 0 = session only, 1 = session + worktree
44
+ }
45
+
46
+ export interface SidebarState {
47
+ sessions: WorkspaceSession[];
48
+ selectedIndex: number;
49
+ rows: number;
50
+ cols: number;
51
+ animationFrame: number;
52
+ compactMode: boolean;
53
+ showActivityLog: boolean;
54
+ cardRowRanges: Array<{ sessionIndex: number; startRow: number; endRow: number }>;
55
+ activityLog: Array<{
56
+ time: string;
57
+ sessionId: string;
58
+ sessionIndex: number;
59
+ agent: string;
60
+ tool: string;
61
+ toolDetail: string;
62
+ }>;
63
+ wizard: WizardState;
64
+ deleteConfirm: DeleteConfirm | null;
65
+ quitConfirm: { selectedIndex: number } | null; // 0=quit only, 1=quit+close editors
66
+ }
67
+
68
+ // Wizard steps for creating new sessions
69
+ export type WizardStep =
70
+ | { step: "select-repo"; repos: RepoInfo[]; selectedIndex: number; filter: string }
71
+ | { step: "select-mode"; repo: RepoInfo; selectedIndex: number; repos: RepoInfo[] }
72
+ | {
73
+ step: "select-worktree";
74
+ repo: RepoInfo;
75
+ worktrees: WorktreeEntry[];
76
+ selectedIndex: number;
77
+ repos: RepoInfo[];
78
+ }
79
+ | { step: "enter-branch"; repo: RepoInfo; branchName: string; repos: RepoInfo[] };
80
+
81
+ export type WizardState = WizardStep | null;
82
+
83
+ export interface HubConfig {
84
+ workspace_dirs: string[];
85
+ editor: "code" | "cursor";
86
+ }