@launchsecure/launch-kit 0.0.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/README.md +37 -0
- package/dist/client/assets/index-C8GAsRGO.css +32 -0
- package/dist/client/assets/index-CcHIoRl6.js +286 -0
- package/dist/client/index.html +22 -0
- package/dist/server/cli.js +8853 -0
- package/dist/server/fb-wizard.js +136 -0
- package/dist/server/graph-mcp-entry.js +1542 -0
- package/dist/server/public/app.js +1312 -0
- package/dist/server/public/icons.js +36 -0
- package/dist/server/public/index.html +159 -0
- package/dist/server/public/plan-detector.js +186 -0
- package/dist/server/public/session-manager.js +1129 -0
- package/dist/server/public/splits.js +569 -0
- package/dist/server/public/style.css +1620 -0
- package/package.json +73 -0
- package/prompts/analysis.md +992 -0
- package/prompts/architect-reconcile.md +931 -0
- package/prompts/architecture-sync.md +902 -0
- package/prompts/be-contract.md +709 -0
- package/prompts/be-impl.md +565 -0
- package/prompts/be-policy.md +551 -0
- package/prompts/be-test.md +591 -0
- package/prompts/bug-diagnosis.md +653 -0
- package/prompts/bug-intake.md +563 -0
- package/prompts/change-request-intake.md +593 -0
- package/prompts/db-contract.md +644 -0
- package/prompts/db-impl.md +522 -0
- package/prompts/db-interaction.md +569 -0
- package/prompts/db-test.md +630 -0
- package/prompts/decision-pack.md +654 -0
- package/prompts/fe-contract.md +992 -0
- package/prompts/fe-flow.md +537 -0
- package/prompts/fe-impl.md +597 -0
- package/prompts/fe-reconcile.md +506 -0
- package/prompts/fe-review.md +550 -0
- package/prompts/fe-test.md +705 -0
- package/prompts/fix-planner.md +1219 -0
- package/prompts/global-db-patterns.md +588 -0
- package/prompts/global-env-config.md +460 -0
- package/prompts/global-integrations.md +504 -0
- package/prompts/global-middleware.md +442 -0
- package/prompts/global-navigation.md +502 -0
- package/prompts/global-security.md +603 -0
- package/prompts/global-services.md +427 -0
- package/prompts/greenfield-classifier.md +590 -0
- package/prompts/llm-council.md +597 -0
- package/prompts/module-sequencer.md +529 -0
- package/prompts/normalize.md +611 -0
- package/prompts/optimization.md +633 -0
- package/prompts/prd-generation.md +544 -0
- package/prompts/prd-reconcile.md +584 -0
- package/prompts/prd-review.md +504 -0
- package/prompts/pre-code-analysis.md +565 -0
- package/prompts/pre-code-global-analysis.md +169 -0
- package/prompts/production-bootstrap.md +577 -0
- package/prompts/research.md +702 -0
- package/prompts/retrofit-analysis.md +845 -0
- package/prompts/spike.md +850 -0
- package/prompts/theming.md +835 -0
- package/prompts/triage.md +599 -0
- package/prompts/unified-reconcile.md +628 -0
- package/prompts/unified-review.md +592 -0
- package/prompts/user-stories.md +486 -0
- package/prompts/wireframe.md +576 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplitContainer - Simple VS Code-style split view
|
|
3
|
+
* Manages up to 2 terminal panes side-by-side with independent terminals
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Split {
|
|
7
|
+
constructor(container, index, app) {
|
|
8
|
+
this.container = container;
|
|
9
|
+
this.index = index;
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.sessionId = null;
|
|
12
|
+
this.isActive = false;
|
|
13
|
+
|
|
14
|
+
// Create independent terminal instance for this split
|
|
15
|
+
this.terminal = null;
|
|
16
|
+
this.fitAddon = null;
|
|
17
|
+
this.webLinksAddon = null;
|
|
18
|
+
this.socket = null;
|
|
19
|
+
|
|
20
|
+
this.createTerminal();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
createTerminal() {
|
|
24
|
+
// Create terminal wrapper
|
|
25
|
+
const wrapper = document.createElement('div');
|
|
26
|
+
wrapper.className = 'split-terminal-wrapper';
|
|
27
|
+
|
|
28
|
+
const terminalDiv = document.createElement('div');
|
|
29
|
+
terminalDiv.id = `split-terminal-${this.index}`;
|
|
30
|
+
wrapper.appendChild(terminalDiv);
|
|
31
|
+
|
|
32
|
+
this.container.appendChild(wrapper);
|
|
33
|
+
|
|
34
|
+
// Initialize xterm.js terminal
|
|
35
|
+
this.terminal = new Terminal({
|
|
36
|
+
fontFamily: this.app?.terminal?.options?.fontFamily || 'JetBrains Mono, monospace',
|
|
37
|
+
fontSize: this.app?.terminal?.options?.fontSize || 14,
|
|
38
|
+
cursorBlink: true,
|
|
39
|
+
convertEol: true,
|
|
40
|
+
allowProposedApi: true,
|
|
41
|
+
theme: this.app?.terminal?.options?.theme || {
|
|
42
|
+
background: '#0d1117',
|
|
43
|
+
foreground: '#c9d1d9',
|
|
44
|
+
cursor: '#58a6ff'
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.fitAddon = new FitAddon.FitAddon();
|
|
49
|
+
this.webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
|
50
|
+
|
|
51
|
+
this.terminal.loadAddon(this.fitAddon);
|
|
52
|
+
this.terminal.loadAddon(this.webLinksAddon);
|
|
53
|
+
this.terminal.open(terminalDiv);
|
|
54
|
+
|
|
55
|
+
// Setup terminal input handler
|
|
56
|
+
this.terminal.onData((data) => {
|
|
57
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
58
|
+
this.socket.send(JSON.stringify({ type: 'input', data }));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Setup resize handler
|
|
63
|
+
this.terminal.onResize(({ cols, rows }) => {
|
|
64
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
65
|
+
this.socket.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.fit();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async setSession(sessionId) {
|
|
73
|
+
if (this.sessionId === sessionId) return;
|
|
74
|
+
|
|
75
|
+
// Disconnect from old session
|
|
76
|
+
if (this.socket) {
|
|
77
|
+
this.disconnect();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.sessionId = sessionId;
|
|
81
|
+
|
|
82
|
+
// Connect to new session
|
|
83
|
+
if (sessionId) {
|
|
84
|
+
await this.connect(sessionId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Update active state
|
|
88
|
+
this.updateActiveState();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async connect(sessionId) {
|
|
92
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
93
|
+
const wsUrl = `${protocol}//${location.host}/terminal/ws?sessionId=${encodeURIComponent(sessionId)}`;
|
|
94
|
+
|
|
95
|
+
this.socket = new WebSocket(wsUrl);
|
|
96
|
+
|
|
97
|
+
this.socket.onopen = () => {
|
|
98
|
+
console.log(`[Split ${this.index}] Connected to session ${sessionId}`);
|
|
99
|
+
// Send initial resize
|
|
100
|
+
const { cols, rows } = this.terminal;
|
|
101
|
+
this.socket.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
this.socket.onmessage = (event) => {
|
|
105
|
+
try {
|
|
106
|
+
const msg = JSON.parse(event.data);
|
|
107
|
+
this.handleMessage(msg);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`[Split ${this.index}] Error handling message:`, error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.socket.onclose = () => {
|
|
114
|
+
console.log(`[Split ${this.index}] Disconnected from session ${sessionId}`);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
this.socket.onerror = (error) => {
|
|
118
|
+
console.error(`[Split ${this.index}] WebSocket error:`, error);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
handleMessage(msg) {
|
|
123
|
+
switch (msg.type) {
|
|
124
|
+
case 'output':
|
|
125
|
+
this.terminal.write(msg.data);
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'session_joined':
|
|
129
|
+
// Replay output buffer
|
|
130
|
+
if (msg.outputBuffer && msg.outputBuffer.length > 0) {
|
|
131
|
+
const joined = msg.outputBuffer.join('');
|
|
132
|
+
this.terminal.write(joined);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'claude_started':
|
|
137
|
+
case 'codex_started':
|
|
138
|
+
case 'agent_started':
|
|
139
|
+
console.log(`[Split ${this.index}] Agent started`);
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case 'exit':
|
|
143
|
+
this.terminal.write('\r\n[Process exited]\r\n');
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'error':
|
|
147
|
+
this.terminal.write(`\r\n\x1b[31mError: ${msg.message}\x1b[0m\r\n`);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
disconnect() {
|
|
153
|
+
if (this.socket) {
|
|
154
|
+
try {
|
|
155
|
+
this.socket.close();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
// Ignore errors
|
|
158
|
+
}
|
|
159
|
+
this.socket = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fit() {
|
|
164
|
+
try {
|
|
165
|
+
if (this.fitAddon) {
|
|
166
|
+
this.fitAddon.fit();
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
// Ignore fit errors
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
updateActiveState() {
|
|
174
|
+
if (this.container) {
|
|
175
|
+
if (this.isActive) {
|
|
176
|
+
this.container.classList.add('split-active');
|
|
177
|
+
} else {
|
|
178
|
+
this.container.classList.remove('split-active');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
clear() {
|
|
184
|
+
this.disconnect();
|
|
185
|
+
this.sessionId = null;
|
|
186
|
+
this.isActive = false;
|
|
187
|
+
if (this.terminal) {
|
|
188
|
+
this.terminal.clear();
|
|
189
|
+
}
|
|
190
|
+
this.updateActiveState();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
destroy() {
|
|
194
|
+
this.disconnect();
|
|
195
|
+
if (this.terminal) {
|
|
196
|
+
this.terminal.dispose();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class SplitContainer {
|
|
202
|
+
constructor(app) {
|
|
203
|
+
this.app = app;
|
|
204
|
+
this.enabled = false;
|
|
205
|
+
this.splits = [];
|
|
206
|
+
this.activeSplitIndex = 0;
|
|
207
|
+
this.dividerPosition = 50; // percentage
|
|
208
|
+
|
|
209
|
+
// Create split container elements
|
|
210
|
+
this.createSplitElements();
|
|
211
|
+
|
|
212
|
+
// Restore state from localStorage
|
|
213
|
+
this.restoreState();
|
|
214
|
+
|
|
215
|
+
// Setup keyboard shortcuts
|
|
216
|
+
this.setupKeyboardShortcuts();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
createSplitElements() {
|
|
220
|
+
const main = document.querySelector('.main');
|
|
221
|
+
if (!main) return;
|
|
222
|
+
|
|
223
|
+
// Create split container (initially hidden)
|
|
224
|
+
this.splitContainerEl = document.createElement('div');
|
|
225
|
+
this.splitContainerEl.className = 'split-container';
|
|
226
|
+
this.splitContainerEl.style.display = 'none';
|
|
227
|
+
|
|
228
|
+
// Create left split
|
|
229
|
+
const leftSplit = document.createElement('div');
|
|
230
|
+
leftSplit.className = 'split-pane split-left';
|
|
231
|
+
leftSplit.dataset.splitIndex = '0';
|
|
232
|
+
|
|
233
|
+
// Create divider
|
|
234
|
+
this.divider = document.createElement('div');
|
|
235
|
+
this.divider.className = 'split-divider';
|
|
236
|
+
this.setupDividerDrag();
|
|
237
|
+
|
|
238
|
+
// Create right split
|
|
239
|
+
const rightSplit = document.createElement('div');
|
|
240
|
+
rightSplit.className = 'split-pane split-right';
|
|
241
|
+
rightSplit.dataset.splitIndex = '1';
|
|
242
|
+
|
|
243
|
+
// Add close button to right split
|
|
244
|
+
const closeBtn = document.createElement('button');
|
|
245
|
+
closeBtn.className = 'split-close';
|
|
246
|
+
closeBtn.title = 'Close Split (Ctrl+\\)';
|
|
247
|
+
closeBtn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
248
|
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
249
|
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
250
|
+
</svg>`;
|
|
251
|
+
closeBtn.addEventListener('click', () => this.closeSplit());
|
|
252
|
+
rightSplit.appendChild(closeBtn);
|
|
253
|
+
|
|
254
|
+
this.splitContainerEl.appendChild(leftSplit);
|
|
255
|
+
this.splitContainerEl.appendChild(this.divider);
|
|
256
|
+
this.splitContainerEl.appendChild(rightSplit);
|
|
257
|
+
|
|
258
|
+
main.appendChild(this.splitContainerEl);
|
|
259
|
+
|
|
260
|
+
// Create Split instances with their own terminals
|
|
261
|
+
this.splits.push(new Split(leftSplit, 0, this.app));
|
|
262
|
+
this.splits.push(new Split(rightSplit, 1, this.app));
|
|
263
|
+
|
|
264
|
+
// Mark left as active by default
|
|
265
|
+
this.splits[0].isActive = true;
|
|
266
|
+
this.splits[0].updateActiveState();
|
|
267
|
+
|
|
268
|
+
// Click handlers to focus splits
|
|
269
|
+
leftSplit.addEventListener('click', () => this.focusSplit(0));
|
|
270
|
+
rightSplit.addEventListener('click', () => this.focusSplit(1));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
setupDividerDrag() {
|
|
274
|
+
let isDragging = false;
|
|
275
|
+
let startX = 0;
|
|
276
|
+
let startPosition = 50;
|
|
277
|
+
|
|
278
|
+
this.divider.addEventListener('mousedown', (e) => {
|
|
279
|
+
isDragging = true;
|
|
280
|
+
startX = e.clientX;
|
|
281
|
+
startPosition = this.dividerPosition;
|
|
282
|
+
document.body.style.cursor = 'col-resize';
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
document.addEventListener('mousemove', (e) => {
|
|
287
|
+
if (!isDragging) return;
|
|
288
|
+
|
|
289
|
+
const container = this.splitContainerEl.getBoundingClientRect();
|
|
290
|
+
const delta = e.clientX - startX;
|
|
291
|
+
const deltaPercent = (delta / container.width) * 100;
|
|
292
|
+
|
|
293
|
+
this.dividerPosition = Math.max(20, Math.min(80, startPosition + deltaPercent));
|
|
294
|
+
this.updateDividerPosition();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
document.addEventListener('mouseup', () => {
|
|
298
|
+
if (isDragging) {
|
|
299
|
+
isDragging = false;
|
|
300
|
+
document.body.style.cursor = '';
|
|
301
|
+
this.saveState();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
updateDividerPosition() {
|
|
307
|
+
const leftSplit = this.splitContainerEl.querySelector('.split-left');
|
|
308
|
+
const rightSplit = this.splitContainerEl.querySelector('.split-right');
|
|
309
|
+
|
|
310
|
+
if (leftSplit && rightSplit) {
|
|
311
|
+
leftSplit.style.width = `${this.dividerPosition}%`;
|
|
312
|
+
rightSplit.style.width = `${100 - this.dividerPosition}%`;
|
|
313
|
+
|
|
314
|
+
// Fit both terminals
|
|
315
|
+
this.splits.forEach(split => split.fit());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async createSplit(sessionId) {
|
|
320
|
+
if (this.enabled) return; // Already split
|
|
321
|
+
|
|
322
|
+
this.enabled = true;
|
|
323
|
+
|
|
324
|
+
// Hide single terminal container
|
|
325
|
+
const terminalContainer = document.getElementById('terminalContainer');
|
|
326
|
+
if (terminalContainer) {
|
|
327
|
+
terminalContainer.style.display = 'none';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Show split container
|
|
331
|
+
this.splitContainerEl.style.display = 'flex';
|
|
332
|
+
|
|
333
|
+
// Update divider position
|
|
334
|
+
this.updateDividerPosition();
|
|
335
|
+
|
|
336
|
+
// Set sessions - left gets current session, right gets the dragged session
|
|
337
|
+
const currentSessionId = this.app.currentClaudeSessionId;
|
|
338
|
+
await this.splits[0].setSession(currentSessionId);
|
|
339
|
+
await this.splits[1].setSession(sessionId);
|
|
340
|
+
|
|
341
|
+
// Focus right split (newly created)
|
|
342
|
+
this.focusSplit(1);
|
|
343
|
+
|
|
344
|
+
// Save state
|
|
345
|
+
this.saveState();
|
|
346
|
+
|
|
347
|
+
console.log(`[SplitContainer] Created split with sessions: ${currentSessionId} | ${sessionId}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
closeSplit() {
|
|
351
|
+
if (!this.enabled) return;
|
|
352
|
+
|
|
353
|
+
this.enabled = false;
|
|
354
|
+
|
|
355
|
+
// Disconnect both splits
|
|
356
|
+
this.splits.forEach(split => split.disconnect());
|
|
357
|
+
|
|
358
|
+
// Show single terminal container
|
|
359
|
+
const terminalContainer = document.getElementById('terminalContainer');
|
|
360
|
+
if (terminalContainer) {
|
|
361
|
+
terminalContainer.style.display = 'flex';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Hide split container
|
|
365
|
+
this.splitContainerEl.style.display = 'none';
|
|
366
|
+
|
|
367
|
+
// Clear splits but don't destroy terminals (we'll reuse them)
|
|
368
|
+
this.splits.forEach((split, i) => {
|
|
369
|
+
split.sessionId = null;
|
|
370
|
+
split.isActive = (i === 0);
|
|
371
|
+
split.updateActiveState();
|
|
372
|
+
if (split.terminal) {
|
|
373
|
+
split.terminal.clear();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
this.activeSplitIndex = 0;
|
|
378
|
+
|
|
379
|
+
// Reconnect main terminal to current session if we have one
|
|
380
|
+
if (this.app.currentClaudeSessionId) {
|
|
381
|
+
setTimeout(() => {
|
|
382
|
+
this.app.connect();
|
|
383
|
+
}, 100);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Save state
|
|
387
|
+
this.saveState();
|
|
388
|
+
|
|
389
|
+
console.log('[SplitContainer] Closed split, back to single pane');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
focusSplit(index) {
|
|
393
|
+
if (index < 0 || index >= this.splits.length) return;
|
|
394
|
+
if (this.activeSplitIndex === index) return;
|
|
395
|
+
|
|
396
|
+
// Update active state
|
|
397
|
+
this.splits.forEach((split, i) => {
|
|
398
|
+
split.isActive = (i === index);
|
|
399
|
+
split.updateActiveState();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
this.activeSplitIndex = index;
|
|
403
|
+
|
|
404
|
+
// Focus the terminal in this split
|
|
405
|
+
const split = this.splits[index];
|
|
406
|
+
if (split.terminal) {
|
|
407
|
+
split.terminal.focus();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Update app's current session to match this split
|
|
411
|
+
if (split.sessionId && this.app) {
|
|
412
|
+
this.app.currentClaudeSessionId = split.sessionId;
|
|
413
|
+
|
|
414
|
+
// Update tab selection
|
|
415
|
+
if (this.app.sessionTabManager) {
|
|
416
|
+
const tab = this.app.sessionTabManager.tabs.get(split.sessionId);
|
|
417
|
+
if (tab) {
|
|
418
|
+
// Update visual state of tabs
|
|
419
|
+
this.app.sessionTabManager.tabs.forEach((t, id) => {
|
|
420
|
+
if (id === split.sessionId) {
|
|
421
|
+
t.classList.add('active');
|
|
422
|
+
} else {
|
|
423
|
+
t.classList.remove('active');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
this.app.sessionTabManager.activeTabId = split.sessionId;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log(`[SplitContainer] Focused split ${index}, session: ${split.sessionId}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Called when a tab is switched - update the active split's session
|
|
435
|
+
async onTabSwitch(sessionId) {
|
|
436
|
+
if (!this.enabled) return;
|
|
437
|
+
|
|
438
|
+
const activeSplit = this.splits[this.activeSplitIndex];
|
|
439
|
+
if (activeSplit) {
|
|
440
|
+
await activeSplit.setSession(sessionId);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
setupKeyboardShortcuts() {
|
|
445
|
+
document.addEventListener('keydown', (e) => {
|
|
446
|
+
// Cmd/Ctrl + \ to toggle split
|
|
447
|
+
if ((e.metaKey || e.ctrlKey) && e.key === '\\') {
|
|
448
|
+
e.preventDefault();
|
|
449
|
+
if (this.enabled) {
|
|
450
|
+
this.closeSplit();
|
|
451
|
+
} else {
|
|
452
|
+
// Create split - need to pick a session to split with
|
|
453
|
+
// For now, just show a message
|
|
454
|
+
console.log('[SplitContainer] To create a split, drag a tab to the right edge of the terminal');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Cmd/Ctrl + 1/2 to focus splits
|
|
459
|
+
if ((e.metaKey || e.ctrlKey) && this.enabled) {
|
|
460
|
+
if (e.key === '1') {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
this.focusSplit(0);
|
|
463
|
+
} else if (e.key === '2') {
|
|
464
|
+
e.preventDefault();
|
|
465
|
+
this.focusSplit(1);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
saveState() {
|
|
472
|
+
try {
|
|
473
|
+
const state = {
|
|
474
|
+
enabled: this.enabled,
|
|
475
|
+
dividerPosition: this.dividerPosition,
|
|
476
|
+
activeSplitIndex: this.activeSplitIndex,
|
|
477
|
+
sessions: this.splits.map(s => s.sessionId)
|
|
478
|
+
};
|
|
479
|
+
localStorage.setItem('cc-web-splits', JSON.stringify(state));
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error('Failed to save split state:', error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
restoreState() {
|
|
486
|
+
try {
|
|
487
|
+
const saved = localStorage.getItem('cc-web-splits');
|
|
488
|
+
if (!saved) return;
|
|
489
|
+
|
|
490
|
+
const state = JSON.parse(saved);
|
|
491
|
+
|
|
492
|
+
// Restore divider position
|
|
493
|
+
if (state.dividerPosition) {
|
|
494
|
+
this.dividerPosition = state.dividerPosition;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Note: Don't auto-restore enabled state on page load
|
|
498
|
+
// User needs to manually create splits
|
|
499
|
+
// This prevents issues with stale session IDs
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error('Failed to restore split state:', error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Setup drop zones for drag-to-split
|
|
506
|
+
setupDropZones() {
|
|
507
|
+
const terminalContainer = document.getElementById('terminalContainer');
|
|
508
|
+
if (!terminalContainer) return;
|
|
509
|
+
|
|
510
|
+
// Create drop zone indicator
|
|
511
|
+
const dropZone = document.createElement('div');
|
|
512
|
+
dropZone.className = 'split-drop-zone';
|
|
513
|
+
dropZone.style.display = 'none';
|
|
514
|
+
terminalContainer.appendChild(dropZone);
|
|
515
|
+
|
|
516
|
+
// Listen for drag events on terminal container
|
|
517
|
+
terminalContainer.addEventListener('dragover', (e) => {
|
|
518
|
+
// Only show drop zone if we're not already in split mode
|
|
519
|
+
if (this.enabled) return;
|
|
520
|
+
|
|
521
|
+
const sessionId = e.dataTransfer?.getData('application/x-session-id');
|
|
522
|
+
if (!sessionId) return;
|
|
523
|
+
|
|
524
|
+
// Don't allow splitting with the current session
|
|
525
|
+
if (sessionId === this.app.currentClaudeSessionId) return;
|
|
526
|
+
|
|
527
|
+
e.preventDefault();
|
|
528
|
+
e.dataTransfer.dropEffect = 'move';
|
|
529
|
+
|
|
530
|
+
// Show drop zone if near right edge
|
|
531
|
+
const rect = terminalContainer.getBoundingClientRect();
|
|
532
|
+
const isNearRightEdge = (e.clientX > rect.right - 100);
|
|
533
|
+
|
|
534
|
+
if (isNearRightEdge) {
|
|
535
|
+
dropZone.style.display = 'block';
|
|
536
|
+
} else {
|
|
537
|
+
dropZone.style.display = 'none';
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
terminalContainer.addEventListener('dragleave', () => {
|
|
542
|
+
dropZone.style.display = 'none';
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
terminalContainer.addEventListener('drop', async (e) => {
|
|
546
|
+
const sessionId = e.dataTransfer?.getData('application/x-session-id');
|
|
547
|
+
if (!sessionId) return;
|
|
548
|
+
|
|
549
|
+
// Don't allow splitting with the current session
|
|
550
|
+
if (sessionId === this.app.currentClaudeSessionId) {
|
|
551
|
+
dropZone.style.display = 'none';
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const rect = terminalContainer.getBoundingClientRect();
|
|
556
|
+
const isNearRightEdge = (e.clientX > rect.right - 100);
|
|
557
|
+
|
|
558
|
+
if (isNearRightEdge && !this.enabled) {
|
|
559
|
+
e.preventDefault();
|
|
560
|
+
await this.createSplit(sessionId);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
dropZone.style.display = 'none';
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Export for use in app.js
|
|
569
|
+
window.SplitContainer = SplitContainer;
|