ccdock 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/sidebar.ts +9 -3
- package/src/tui/ansi.ts +2 -1
- package/src/tui/render.ts +15 -17
- package/src/workspace/window.ts +89 -29
package/package.json
CHANGED
package/src/sidebar.ts
CHANGED
|
@@ -357,6 +357,13 @@ async function handleDeleteConfirmInput(state: SidebarState, data: Buffer): Prom
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
function getManagedEditorTitles(sessions: SidebarState["sessions"]): string[] {
|
|
361
|
+
return sessions
|
|
362
|
+
.filter((s) => s.editorState !== "closed")
|
|
363
|
+
.map((s) => s.worktreePath.split("/").pop() ?? "")
|
|
364
|
+
.filter((t) => t.length > 0);
|
|
365
|
+
}
|
|
366
|
+
|
|
360
367
|
export async function runSidebar(): Promise<void> {
|
|
361
368
|
const config = loadConfig();
|
|
362
369
|
const state = createInitialState();
|
|
@@ -374,8 +381,7 @@ export async function runSidebar(): Promise<void> {
|
|
|
374
381
|
state.rows = process.stdout.rows ?? 24;
|
|
375
382
|
state.cols = process.stdout.columns ?? 80;
|
|
376
383
|
render(state);
|
|
377
|
-
|
|
378
|
-
await repositionAllEditors();
|
|
384
|
+
await repositionAllEditors(getManagedEditorTitles(state.sessions));
|
|
379
385
|
});
|
|
380
386
|
|
|
381
387
|
// Animation timer (200ms) — only repaint when there's something animating
|
|
@@ -516,7 +522,7 @@ export async function runSidebar(): Promise<void> {
|
|
|
516
522
|
break;
|
|
517
523
|
|
|
518
524
|
case "realign":
|
|
519
|
-
await repositionAllEditors();
|
|
525
|
+
await repositionAllEditors(getManagedEditorTitles(state.sessions));
|
|
520
526
|
break;
|
|
521
527
|
|
|
522
528
|
case "mouse_click": {
|
package/src/tui/ansi.ts
CHANGED
|
@@ -44,7 +44,8 @@ export const COLORS = {
|
|
|
44
44
|
editorFocused: fg256(255), // bright white — focused editor
|
|
45
45
|
editorOpen: fg256(117), // cyan — open but not focused
|
|
46
46
|
editorClosed: fg256(240), // dark gray — closed
|
|
47
|
-
borderFocused: fg256(255), // bright white border
|
|
47
|
+
borderFocused: fg256(255), // bright white border — VS Code window focused
|
|
48
|
+
borderSelected: fg256(75), // soft blue border — J/K cursor selection
|
|
48
49
|
borderOpen: fg256(248), // light gray border
|
|
49
50
|
borderClosed: fg256(235), // very dark border
|
|
50
51
|
} as const;
|
package/src/tui/render.ts
CHANGED
|
@@ -35,11 +35,9 @@ function padRight(str: string, len: number): string {
|
|
|
35
35
|
return str + " ".repeat(len - visible);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function renderDeleteConfirm(deleteConfirm: DeleteConfirm
|
|
38
|
+
function renderDeleteConfirm(deleteConfirm: DeleteConfirm): string[] {
|
|
39
39
|
const lines: string[] = [];
|
|
40
|
-
const width = Math.max(cols - 6, 16);
|
|
41
40
|
|
|
42
|
-
lines.push(` ${COLORS.border}${BOX.horizontal.repeat(width)}${RESET}`);
|
|
43
41
|
lines.push(` ${BOLD}${COLORS.error} Delete session?${RESET}`);
|
|
44
42
|
lines.push("");
|
|
45
43
|
|
|
@@ -58,7 +56,6 @@ function renderDeleteConfirm(deleteConfirm: DeleteConfirm, cols: number): string
|
|
|
58
56
|
|
|
59
57
|
lines.push("");
|
|
60
58
|
lines.push(` ${COLORS.muted}Enter: confirm | Esc: cancel${RESET}`);
|
|
61
|
-
lines.push(` ${COLORS.border}${BOX.horizontal.repeat(width)}${RESET}`);
|
|
62
59
|
|
|
63
60
|
return lines;
|
|
64
61
|
}
|
|
@@ -74,8 +71,6 @@ function renderCard(
|
|
|
74
71
|
): string[] {
|
|
75
72
|
const lines: string[] = [];
|
|
76
73
|
const width = Math.max(cols - 2, 20);
|
|
77
|
-
const bg = isSelected ? COLORS.bgSelected : "";
|
|
78
|
-
const resetBg = isSelected ? RESET : "";
|
|
79
74
|
|
|
80
75
|
// Colors based on editor state
|
|
81
76
|
// Focused: white border, normal title
|
|
@@ -85,11 +80,14 @@ function renderCard(
|
|
|
85
80
|
const isFocused = editorState === "focused";
|
|
86
81
|
const isClosed = editorState === "closed";
|
|
87
82
|
const isLaunching = editorState === "launching";
|
|
83
|
+
// Border color: VS Code focused > J/K selected > closed/open
|
|
88
84
|
const borderColor = isFocused
|
|
89
85
|
? COLORS.borderFocused
|
|
90
|
-
:
|
|
91
|
-
? COLORS.
|
|
92
|
-
:
|
|
86
|
+
: isSelected
|
|
87
|
+
? COLORS.borderSelected
|
|
88
|
+
: isClosed
|
|
89
|
+
? COLORS.borderClosed
|
|
90
|
+
: COLORS.border;
|
|
93
91
|
const titleColor = isClosed ? COLORS.editorClosed : COLORS.title;
|
|
94
92
|
const detailColor = isClosed ? COLORS.editorClosed : COLORS.subtitle;
|
|
95
93
|
const dimAll = isClosed ? DIM : "";
|
|
@@ -113,7 +111,7 @@ function renderCard(
|
|
|
113
111
|
const titleText = `${titleColor}${icon} ${session.repoName}:${session.branch}${RESET}`;
|
|
114
112
|
const title = `${sessionNum}${openDot}${titleText}`;
|
|
115
113
|
const titleTruncated = truncate(title, width - 4);
|
|
116
|
-
const titleLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${
|
|
114
|
+
const titleLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${padRight(titleTruncated, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
117
115
|
lines.push(titleLine);
|
|
118
116
|
|
|
119
117
|
if (!compact) {
|
|
@@ -121,13 +119,13 @@ function renderCard(
|
|
|
121
119
|
const shortPath = shortenHome(session.worktreePath);
|
|
122
120
|
const pathTruncated = truncate(shortPath, width - 4);
|
|
123
121
|
const pathColor = editorState === "closed" ? COLORS.editorClosed : COLORS.subtitle;
|
|
124
|
-
const pathLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${
|
|
122
|
+
const pathLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${pathColor}${padRight(pathTruncated, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
125
123
|
lines.push(pathLine);
|
|
126
124
|
|
|
127
125
|
// Agent status lines
|
|
128
126
|
if (session.agents.length === 0) {
|
|
129
127
|
const noAgent = `${DIM}no agents${RESET}`;
|
|
130
|
-
const agentLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${
|
|
128
|
+
const agentLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${padRight(noAgent, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
131
129
|
lines.push(agentLine);
|
|
132
130
|
} else {
|
|
133
131
|
for (const agent of session.agents) {
|
|
@@ -135,7 +133,7 @@ function renderCard(
|
|
|
135
133
|
const sColor = statusColor(agent.status);
|
|
136
134
|
const statusText = `${sColor}${sIcon} ${agent.status}${RESET}`;
|
|
137
135
|
const agentInfo = `${statusText} ${detailColor}${agent.agentType}${RESET}`;
|
|
138
|
-
const agentLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${
|
|
136
|
+
const agentLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${padRight(agentInfo, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
139
137
|
lines.push(agentLine);
|
|
140
138
|
|
|
141
139
|
// Show latest tool activity
|
|
@@ -144,7 +142,7 @@ function renderCard(
|
|
|
144
142
|
? `${detailColor}${agent.toolName}${RESET} ${detailColor}${agent.toolDetail}${RESET}`
|
|
145
143
|
: `${detailColor}${agent.toolName}${RESET}`;
|
|
146
144
|
const detailTruncated = truncate(` ${detail}`, width - 4);
|
|
147
|
-
const detailLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${
|
|
145
|
+
const detailLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${padRight(detailTruncated, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
148
146
|
lines.push(detailLine);
|
|
149
147
|
}
|
|
150
148
|
}
|
|
@@ -157,15 +155,15 @@ function renderCard(
|
|
|
157
155
|
.map((a) => `${statusColor(a.status)}${statusIcon(a.status, animFrame)}${RESET}`)
|
|
158
156
|
.join(" ")
|
|
159
157
|
: `${DIM}no agents${RESET}`;
|
|
160
|
-
const compactLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${
|
|
158
|
+
const compactLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${padRight(agentSummary, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
161
159
|
lines.push(compactLine);
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
// Delete confirmation inline
|
|
165
163
|
if (isSelected && deleteConfirm && deleteConfirm.sessionId === session.id) {
|
|
166
|
-
const confirmLines = renderDeleteConfirm(deleteConfirm
|
|
164
|
+
const confirmLines = renderDeleteConfirm(deleteConfirm);
|
|
167
165
|
for (const cl of confirmLines) {
|
|
168
|
-
const confirmLine = `${borderColor}${BOX.vertical}${RESET} ${padRight(cl, width - 4)} ${borderColor}${BOX.vertical}${RESET}`;
|
|
166
|
+
const confirmLine = `${dimAll}${borderColor}${BOX.vertical}${RESET} ${padRight(cl, width - 4)}${RESET} ${dimAll}${borderColor}${BOX.vertical}${RESET}`;
|
|
169
167
|
lines.push(confirmLine);
|
|
170
168
|
}
|
|
171
169
|
}
|
package/src/workspace/window.ts
CHANGED
|
@@ -20,6 +20,16 @@ async function runOsascript(script: string): Promise<string> {
|
|
|
20
20
|
return out.trim();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async function runJXA(script: string): Promise<string> {
|
|
24
|
+
const proc = Bun.spawn(["osascript", "-l", "JavaScript", "-e", script], {
|
|
25
|
+
stdout: "pipe",
|
|
26
|
+
stderr: "pipe",
|
|
27
|
+
});
|
|
28
|
+
const out = await new Response(proc.stdout).text();
|
|
29
|
+
await proc.exited;
|
|
30
|
+
return out.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* Get the bounds of the sidebar terminal window (the frontmost Ghostty window).
|
|
25
35
|
*/
|
|
@@ -44,27 +54,63 @@ end tell
|
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
/**
|
|
47
|
-
* Get the
|
|
48
|
-
*
|
|
57
|
+
* Get the right edge (x coordinate) of the screen containing the sidebar window.
|
|
58
|
+
* Uses JXA + NSScreen with proper coordinate conversion for multi-monitor setups.
|
|
59
|
+
*
|
|
60
|
+
* Coordinate systems:
|
|
61
|
+
* NSScreen: origin at bottom-left of primary display, y increases upward.
|
|
62
|
+
* Screens to the left have negative x.
|
|
63
|
+
* AppleScript: origin at top-left of primary display, y increases downward.
|
|
64
|
+
* x=0 is the left edge of the leftmost monitor.
|
|
65
|
+
*
|
|
66
|
+
* Conversions (using primaryH = height of primary NSScreen):
|
|
67
|
+
* nsX = asX + nsMinX (nsMinX = min x across all NSScreens)
|
|
68
|
+
* nsY = primaryH - asCenterY (Y axis is flipped)
|
|
49
69
|
*/
|
|
50
|
-
|
|
70
|
+
async function getScreenRightEdge(sidebar: WindowBounds): Promise<number> {
|
|
71
|
+
// Fallback: typical MacBook Pro screen width from the sidebar's right edge
|
|
72
|
+
const fallback = 1512;
|
|
51
73
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
// Use the window center for a more robust screen hit-test
|
|
75
|
+
const asCenterX = sidebar.x + sidebar.width / 2;
|
|
76
|
+
const asCenterY = sidebar.y + sidebar.height / 2;
|
|
77
|
+
const result = await runJXA(`
|
|
78
|
+
ObjC.import("AppKit");
|
|
79
|
+
var asCenterX = ${asCenterX};
|
|
80
|
+
var asCenterY = ${asCenterY};
|
|
81
|
+
var screens = $.NSScreen.screens;
|
|
82
|
+
var count = screens.count;
|
|
83
|
+
var primaryH = $.NSScreen.mainScreen.frame.size.height;
|
|
84
|
+
// NSScreen always places the primary display at x=0, same as AppleScript.
|
|
85
|
+
// So AS.x == NS.x (no x offset needed).
|
|
86
|
+
// Only Y is flipped: nsY = primaryH - asY.
|
|
87
|
+
var nsX = asCenterX;
|
|
88
|
+
var nsY = primaryH - asCenterY;
|
|
89
|
+
// Find screen whose bounds contain the converted center point
|
|
90
|
+
var found = false;
|
|
91
|
+
var result = 1512;
|
|
92
|
+
for (var i = 0; i < count; i++) {
|
|
93
|
+
var s = screens.objectAtIndex(i);
|
|
94
|
+
var left = s.frame.origin.x;
|
|
95
|
+
var bottom = s.frame.origin.y;
|
|
96
|
+
var width = s.frame.size.width;
|
|
97
|
+
var height = s.frame.size.height;
|
|
98
|
+
if (left <= nsX && nsX < left + width && bottom <= nsY && nsY < bottom + height) {
|
|
99
|
+
result = Math.round(left + width);
|
|
100
|
+
found = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!found) {
|
|
105
|
+
var main = $.NSScreen.mainScreen;
|
|
106
|
+
result = Math.round(main.frame.origin.x + main.frame.size.width);
|
|
107
|
+
}
|
|
108
|
+
result;
|
|
57
109
|
`);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
x: parts[0]!,
|
|
62
|
-
y: parts[1]!,
|
|
63
|
-
width: parts[2]! - parts[0]!,
|
|
64
|
-
height: parts[3]! - parts[1]!,
|
|
65
|
-
};
|
|
110
|
+
const value = Number.parseInt(result, 10);
|
|
111
|
+
return Number.isNaN(value) || value <= 0 ? fallback : value;
|
|
66
112
|
} catch {
|
|
67
|
-
return
|
|
113
|
+
return fallback;
|
|
68
114
|
}
|
|
69
115
|
}
|
|
70
116
|
|
|
@@ -80,10 +126,8 @@ export async function positionEditorWindow(
|
|
|
80
126
|
// Calculate editor position: right of sidebar, same height
|
|
81
127
|
const editorX = sidebarBounds.x + sidebarBounds.width + 4; // 4px gap
|
|
82
128
|
const editorY = sidebarBounds.y;
|
|
83
|
-
// Editor width: fill remaining
|
|
84
|
-
|
|
85
|
-
const screen = await getScreenBounds();
|
|
86
|
-
const screenRight = screen ? screen.x + screen.width : 5120; // fallback
|
|
129
|
+
// Editor width: fill remaining space on the screen containing the sidebar
|
|
130
|
+
const screenRight = await getScreenRightEdge(sidebarBounds);
|
|
87
131
|
const editorWidth = screenRight - editorX;
|
|
88
132
|
const editorHeight = sidebarBounds.height;
|
|
89
133
|
|
|
@@ -292,27 +336,43 @@ end tell
|
|
|
292
336
|
}
|
|
293
337
|
|
|
294
338
|
/**
|
|
295
|
-
* Reposition
|
|
339
|
+
* Reposition managed VS Code windows to fill the area right of the sidebar.
|
|
340
|
+
* Only windows whose title contains one of the managed titles are repositioned.
|
|
296
341
|
* Called when the sidebar terminal is resized.
|
|
297
342
|
*/
|
|
298
|
-
export async function repositionAllEditors(): Promise<void> {
|
|
299
|
-
if (
|
|
300
|
-
const [
|
|
301
|
-
if (!sidebar) return;
|
|
343
|
+
export async function repositionAllEditors(managedTitles: string[]): Promise<void> {
|
|
344
|
+
if (managedTitles.length === 0) return;
|
|
345
|
+
const [running, sidebar] = await Promise.all([isEditorRunning(), getSidebarBounds()]);
|
|
346
|
+
if (!running || !sidebar) return;
|
|
302
347
|
|
|
303
348
|
try {
|
|
304
349
|
const editorX = sidebar.x + sidebar.width + 4;
|
|
305
350
|
const editorY = sidebar.y;
|
|
306
|
-
const screenRight =
|
|
351
|
+
const screenRight = await getScreenRightEdge(sidebar);
|
|
307
352
|
const editorWidth = screenRight - editorX;
|
|
308
353
|
const editorHeight = sidebar.height;
|
|
309
354
|
|
|
355
|
+
const titlesAppleScript = managedTitles
|
|
356
|
+
.map((t) => `"${t.replace(/"/g, '\\"')}"`)
|
|
357
|
+
.join(", ");
|
|
358
|
+
|
|
310
359
|
await runOsascript(`
|
|
311
360
|
tell application "System Events"
|
|
312
361
|
tell process "Code"
|
|
362
|
+
set managedTitles to {${titlesAppleScript}}
|
|
313
363
|
repeat with w in every window
|
|
314
|
-
set
|
|
315
|
-
set
|
|
364
|
+
set wName to name of w
|
|
365
|
+
set isManaged to false
|
|
366
|
+
repeat with t in managedTitles
|
|
367
|
+
if wName contains t then
|
|
368
|
+
set isManaged to true
|
|
369
|
+
exit repeat
|
|
370
|
+
end if
|
|
371
|
+
end repeat
|
|
372
|
+
if isManaged then
|
|
373
|
+
set position of w to {${editorX}, ${editorY}}
|
|
374
|
+
set size of w to {${editorWidth}, ${editorHeight}}
|
|
375
|
+
end if
|
|
316
376
|
end repeat
|
|
317
377
|
end tell
|
|
318
378
|
end tell
|