@enruana/claude-orka 0.2.2 → 0.3.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/dist/cli.js +41 -2
- package/dist/electron/main/main.d.ts +1 -4
- package/dist/electron/main/main.d.ts.map +1 -1
- package/dist/electron/main/main.js +148 -43
- package/dist/electron/main/main.js.map +1 -1
- package/dist/electron/preload/preload.d.ts +0 -23
- package/dist/electron/preload/preload.d.ts.map +1 -1
- package/dist/electron/preload/preload.js +13 -29
- package/dist/electron/preload/preload.js.map +1 -1
- package/dist/src/cli/index.js +0 -0
- package/dist/src/core/SessionManager.d.ts +4 -0
- package/dist/src/core/SessionManager.d.ts.map +1 -1
- package/dist/src/core/SessionManager.js +44 -0
- package/dist/src/core/SessionManager.js.map +1 -1
- package/package.json +16 -5
- package/dist/core/ClaudeOrka.d.ts +0 -111
- package/dist/core/ClaudeOrka.d.ts.map +0 -1
- package/dist/core/ClaudeOrka.js +0 -160
- package/dist/core/ClaudeOrka.js.map +0 -1
- package/dist/core/SessionManager.d.ts +0 -82
- package/dist/core/SessionManager.d.ts.map +0 -1
- package/dist/core/SessionManager.js +0 -519
- package/dist/core/SessionManager.js.map +0 -1
- package/dist/core/StateManager.d.ts +0 -92
- package/dist/core/StateManager.d.ts.map +0 -1
- package/dist/core/StateManager.js +0 -307
- package/dist/core/StateManager.js.map +0 -1
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -20
- package/dist/core/index.js.map +0 -1
- package/dist/electron/main/ipc-handlers.d.ts +0 -5
- package/dist/electron/main/ipc-handlers.d.ts.map +0 -1
- package/dist/electron/main/ipc-handlers.js +0 -169
- package/dist/electron/main/ipc-handlers.js.map +0 -1
- package/dist/electron/renderer/app.js +0 -808
- package/dist/electron/renderer/index.html +0 -189
- package/dist/electron/renderer/styles.css +0 -736
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -18
- package/dist/index.js.map +0 -1
- package/dist/models/Fork.d.ts +0 -26
- package/dist/models/Fork.d.ts.map +0 -1
- package/dist/models/Fork.js +0 -3
- package/dist/models/Fork.js.map +0 -1
- package/dist/models/Session.d.ts +0 -38
- package/dist/models/Session.d.ts.map +0 -1
- package/dist/models/Session.js +0 -3
- package/dist/models/Session.js.map +0 -1
- package/dist/models/State.d.ts +0 -24
- package/dist/models/State.d.ts.map +0 -1
- package/dist/models/State.js +0 -3
- package/dist/models/State.js.map +0 -1
- package/dist/models/index.d.ts +0 -4
- package/dist/models/index.d.ts.map +0 -1
- package/dist/models/index.js +0 -20
- package/dist/models/index.js.map +0 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -19
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -20
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -41
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/tmux.d.ts +0 -77
- package/dist/utils/tmux.d.ts.map +0 -1
- package/dist/utils/tmux.js +0 -270
- package/dist/utils/tmux.js.map +0 -1
|
@@ -1,808 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Orka - Electron Renderer
|
|
3
|
-
* GitKraken-inspired UI for managing Claude sessions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// ===== STATE =====
|
|
7
|
-
const state = {
|
|
8
|
-
projectPath: '',
|
|
9
|
-
sessions: [],
|
|
10
|
-
currentSession: null,
|
|
11
|
-
currentFilter: 'all',
|
|
12
|
-
selectedNode: null, // {type: 'main'|'fork', data: {...}}
|
|
13
|
-
zoom: 1.0,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ===== DOM ELEMENTS =====
|
|
17
|
-
const $el = {
|
|
18
|
-
// Header
|
|
19
|
-
projectPath: document.getElementById('project-path'),
|
|
20
|
-
refreshBtn: document.getElementById('refresh-btn'),
|
|
21
|
-
newSessionBtn: document.getElementById('new-session-btn'),
|
|
22
|
-
|
|
23
|
-
// Left sidebar
|
|
24
|
-
sessionsList: document.getElementById('sessions-list'),
|
|
25
|
-
filterTabs: document.querySelectorAll('.filter-tab'),
|
|
26
|
-
|
|
27
|
-
// Center graph
|
|
28
|
-
graphCanvas: document.getElementById('graph-canvas'),
|
|
29
|
-
graphEmpty: document.getElementById('graph-empty'),
|
|
30
|
-
currentSessionTitle: document.getElementById('current-session-title'),
|
|
31
|
-
zoomInBtn: document.getElementById('zoom-in-btn'),
|
|
32
|
-
zoomOutBtn: document.getElementById('zoom-out-btn'),
|
|
33
|
-
zoomResetBtn: document.getElementById('zoom-reset-btn'),
|
|
34
|
-
|
|
35
|
-
// Right details panel
|
|
36
|
-
detailsEmpty: document.getElementById('details-empty'),
|
|
37
|
-
detailsMain: document.getElementById('details-main'),
|
|
38
|
-
detailsFork: document.getElementById('details-fork'),
|
|
39
|
-
detailsSession: document.getElementById('details-session'),
|
|
40
|
-
|
|
41
|
-
// Command modal
|
|
42
|
-
commandModal: document.getElementById('command-modal'),
|
|
43
|
-
commandInput: document.getElementById('command-input'),
|
|
44
|
-
commandTargetLabel: document.getElementById('command-target-label'),
|
|
45
|
-
commandSendBtn: document.getElementById('command-send-btn'),
|
|
46
|
-
commandCancelBtn: document.getElementById('command-cancel-btn'),
|
|
47
|
-
|
|
48
|
-
// Toast
|
|
49
|
-
toastContainer: document.getElementById('toast-container'),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ===== CONSTANTS =====
|
|
53
|
-
const COLORS = {
|
|
54
|
-
main: '#00d9ff', // Cyan vibrante como GitKraken
|
|
55
|
-
forks: ['#ff1b8d', '#af52de', '#00d26a', '#ffa500', '#5ac8fa'], // Colores vibrantes
|
|
56
|
-
active: '#00d26a',
|
|
57
|
-
saved: '#606060',
|
|
58
|
-
merged: '#af52de',
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const NODE_RADIUS = 6
|
|
62
|
-
const NODE_SPACING = 50
|
|
63
|
-
const FORK_OFFSET = 150
|
|
64
|
-
const COMMITS_PER_BRANCH = 4 // Número de commits a mostrar por branch
|
|
65
|
-
|
|
66
|
-
// ===== UTILITIES =====
|
|
67
|
-
function formatDate(dateStr) {
|
|
68
|
-
if (!dateStr) return 'N/A'
|
|
69
|
-
const date = new Date(dateStr)
|
|
70
|
-
return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function showToast(message, type = 'info') {
|
|
74
|
-
const toast = document.createElement('div')
|
|
75
|
-
toast.className = `toast ${type}`
|
|
76
|
-
toast.innerHTML = `
|
|
77
|
-
<div class="toast-icon">${type === 'success' ? '✓' : type === 'error' ? '✕' : 'ℹ'}</div>
|
|
78
|
-
<div class="toast-content">
|
|
79
|
-
<div class="toast-message">${message}</div>
|
|
80
|
-
</div>
|
|
81
|
-
`
|
|
82
|
-
$el.toastContainer.appendChild(toast)
|
|
83
|
-
|
|
84
|
-
setTimeout(() => toast.remove(), 4000)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function askUser(message, defaultValue = '') {
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
const dialog = document.createElement('div')
|
|
90
|
-
dialog.style.cssText = `
|
|
91
|
-
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
92
|
-
background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);
|
|
93
|
-
display: flex; align-items: center; justify-content: center; z-index: 10000;
|
|
94
|
-
`
|
|
95
|
-
dialog.innerHTML = `
|
|
96
|
-
<div style="background: #1e1e1e; padding: 24px; border-radius: 8px; min-width: 400px; border: 1px solid #363636;">
|
|
97
|
-
<h3 style="margin: 0 0 16px 0; color: #e8e8e8; font-size: 16px;">${message}</h3>
|
|
98
|
-
<input type="text" id="dialog-input" value="${defaultValue}"
|
|
99
|
-
style="width: 100%; padding: 8px 12px; background: #141414; border: 1px solid #363636; border-radius: 4px; color: #e8e8e8; font-size: 13px; margin-bottom: 16px;" />
|
|
100
|
-
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
|
101
|
-
<button id="dialog-cancel" style="padding: 6px 12px; background: #252525; color: #e8e8e8; border: 1px solid #363636; border-radius: 4px; cursor: pointer; font-size: 12px;">Cancel</button>
|
|
102
|
-
<button id="dialog-ok" style="padding: 6px 12px; background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">OK</button>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
`
|
|
106
|
-
document.body.appendChild(dialog)
|
|
107
|
-
|
|
108
|
-
const input = dialog.querySelector('#dialog-input')
|
|
109
|
-
const okBtn = dialog.querySelector('#dialog-ok')
|
|
110
|
-
const cancelBtn = dialog.querySelector('#dialog-cancel')
|
|
111
|
-
|
|
112
|
-
input.focus()
|
|
113
|
-
input.select()
|
|
114
|
-
|
|
115
|
-
okBtn.onclick = () => { resolve(input.value); dialog.remove() }
|
|
116
|
-
cancelBtn.onclick = () => { resolve(''); dialog.remove() }
|
|
117
|
-
input.onkeydown = (e) => {
|
|
118
|
-
if (e.key === 'Enter') { resolve(input.value); dialog.remove() }
|
|
119
|
-
if (e.key === 'Escape') { resolve(''); dialog.remove() }
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function confirmAction(message) {
|
|
125
|
-
return new Promise((resolve) => {
|
|
126
|
-
const dialog = document.createElement('div')
|
|
127
|
-
dialog.style.cssText = `
|
|
128
|
-
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
129
|
-
background: rgba(0,0,0,0.7); backdrop-filter: blur(4px);
|
|
130
|
-
display: flex; align-items: center; justify-content: center; z-index: 10000;
|
|
131
|
-
`
|
|
132
|
-
dialog.innerHTML = `
|
|
133
|
-
<div style="background: #1e1e1e; padding: 24px; border-radius: 8px; min-width: 400px; border: 1px solid #363636;">
|
|
134
|
-
<h3 style="margin: 0 0 16px 0; color: #e8e8e8; font-size: 16px;">${message}</h3>
|
|
135
|
-
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
|
136
|
-
<button id="dialog-no" style="padding: 6px 12px; background: #252525; color: #e8e8e8; border: 1px solid #363636; border-radius: 4px; cursor: pointer; font-size: 12px;">No</button>
|
|
137
|
-
<button id="dialog-yes" style="padding: 6px 12px; background: #ff5757; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">Yes</button>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
`
|
|
141
|
-
document.body.appendChild(dialog)
|
|
142
|
-
|
|
143
|
-
const yesBtn = dialog.querySelector('#dialog-yes')
|
|
144
|
-
const noBtn = dialog.querySelector('#dialog-no')
|
|
145
|
-
|
|
146
|
-
yesBtn.onclick = () => { resolve(true); dialog.remove() }
|
|
147
|
-
noBtn.onclick = () => { resolve(false); dialog.remove() }
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ===== SESSIONS LIST (Left Sidebar) =====
|
|
152
|
-
function renderSessionsList() {
|
|
153
|
-
const filtered = state.sessions.filter(s => {
|
|
154
|
-
if (state.currentFilter === 'all') return true
|
|
155
|
-
return s.status === state.currentFilter
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
if (filtered.length === 0) {
|
|
159
|
-
$el.sessionsList.innerHTML = '<div class="loading">No sessions found</div>'
|
|
160
|
-
return
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
$el.sessionsList.innerHTML = filtered.map((session, idx) => `
|
|
164
|
-
<div class="session-item ${state.currentSession?.id === session.id ? 'selected' : ''}"
|
|
165
|
-
data-session-id="${session.id}">
|
|
166
|
-
<div class="session-item-header">
|
|
167
|
-
<div class="session-item-icon">🌿</div>
|
|
168
|
-
<div class="session-item-name">${session.name}</div>
|
|
169
|
-
<div class="session-item-status status-${session.status}"></div>
|
|
170
|
-
</div>
|
|
171
|
-
<div class="session-item-meta">
|
|
172
|
-
<span>${session.forks.length} fork${session.forks.length !== 1 ? 's' : ''}</span>
|
|
173
|
-
<span>${formatDate(session.main.createdAt)}</span>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
`).join('')
|
|
177
|
-
|
|
178
|
-
// Attach click listeners
|
|
179
|
-
$el.sessionsList.querySelectorAll('.session-item').forEach(item => {
|
|
180
|
-
item.addEventListener('click', () => {
|
|
181
|
-
const sessionId = item.dataset.sessionId
|
|
182
|
-
selectSession(sessionId)
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function selectSession(sessionId) {
|
|
188
|
-
state.currentSession = state.sessions.find(s => s.id === sessionId)
|
|
189
|
-
state.selectedNode = null
|
|
190
|
-
|
|
191
|
-
renderSessionsList()
|
|
192
|
-
renderGraph()
|
|
193
|
-
showDetailsEmpty()
|
|
194
|
-
|
|
195
|
-
if (state.currentSession) {
|
|
196
|
-
$el.currentSessionTitle.textContent = state.currentSession.name
|
|
197
|
-
$el.graphEmpty.classList.add('hidden')
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ===== GRAPH VISUALIZATION (Center Canvas) =====
|
|
202
|
-
function renderGraph() {
|
|
203
|
-
if (!state.currentSession) {
|
|
204
|
-
$el.graphEmpty.classList.remove('hidden')
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
$el.graphEmpty.classList.add('hidden')
|
|
209
|
-
const canvas = $el.graphCanvas
|
|
210
|
-
const ctx = canvas.getContext('2d')
|
|
211
|
-
|
|
212
|
-
// Set canvas size
|
|
213
|
-
canvas.width = canvas.offsetWidth
|
|
214
|
-
canvas.height = canvas.offsetHeight
|
|
215
|
-
|
|
216
|
-
// Clear canvas
|
|
217
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
218
|
-
|
|
219
|
-
const session = state.currentSession
|
|
220
|
-
const centerX = canvas.width / 2
|
|
221
|
-
const startY = 80
|
|
222
|
-
|
|
223
|
-
// Calcular cuántos commits totales mostrar
|
|
224
|
-
const totalHeight = session.forks.length > 0
|
|
225
|
-
? (session.forks.length + 1) * COMMITS_PER_BRANCH * NODE_SPACING
|
|
226
|
-
: COMMITS_PER_BRANCH * NODE_SPACING
|
|
227
|
-
|
|
228
|
-
// Array para almacenar todos los nodos clickeables
|
|
229
|
-
const allNodes = []
|
|
230
|
-
|
|
231
|
-
// ===== DIBUJAR MAIN BRANCH =====
|
|
232
|
-
const mainCommits = []
|
|
233
|
-
for (let i = 0; i < COMMITS_PER_BRANCH + session.forks.length; i++) {
|
|
234
|
-
const y = startY + i * NODE_SPACING
|
|
235
|
-
mainCommits.push({ x: centerX, y, type: 'main', data: session.main })
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Dibujar línea principal
|
|
239
|
-
ctx.strokeStyle = COLORS.main
|
|
240
|
-
ctx.lineWidth = 2 * state.zoom
|
|
241
|
-
ctx.beginPath()
|
|
242
|
-
ctx.moveTo(centerX, mainCommits[0].y)
|
|
243
|
-
ctx.lineTo(centerX, mainCommits[mainCommits.length - 1].y + NODE_SPACING)
|
|
244
|
-
ctx.stroke()
|
|
245
|
-
|
|
246
|
-
// Dibujar commits de main
|
|
247
|
-
mainCommits.forEach((commit, idx) => {
|
|
248
|
-
const isHead = idx === 0
|
|
249
|
-
drawNode(ctx, commit.x, commit.y, session.status, COLORS.main, isHead)
|
|
250
|
-
if (isHead) {
|
|
251
|
-
allNodes.push(commit)
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
// Label de Main
|
|
256
|
-
ctx.fillStyle = '#e8e8e8'
|
|
257
|
-
ctx.font = `bold ${11 * state.zoom}px -apple-system, sans-serif`
|
|
258
|
-
ctx.textAlign = 'left'
|
|
259
|
-
ctx.fillText('main', centerX + 15, mainCommits[0].y + 4)
|
|
260
|
-
|
|
261
|
-
// ===== DIBUJAR FORKS =====
|
|
262
|
-
session.forks.forEach((fork, idx) => {
|
|
263
|
-
const isLeft = idx % 2 === 0
|
|
264
|
-
const forkX = isLeft ? centerX - FORK_OFFSET : centerX + FORK_OFFSET
|
|
265
|
-
const color = COLORS.forks[idx % COLORS.forks.length]
|
|
266
|
-
|
|
267
|
-
// Punto de ramificación en main (después de algunos commits)
|
|
268
|
-
const branchPointIdx = Math.min(idx + 1, mainCommits.length - 1)
|
|
269
|
-
const branchPoint = mainCommits[branchPointIdx]
|
|
270
|
-
|
|
271
|
-
// Crear commits para el fork
|
|
272
|
-
const forkCommits = []
|
|
273
|
-
for (let i = 0; i < COMMITS_PER_BRANCH; i++) {
|
|
274
|
-
const y = branchPoint.y + i * NODE_SPACING
|
|
275
|
-
forkCommits.push({
|
|
276
|
-
x: forkX,
|
|
277
|
-
y,
|
|
278
|
-
type: 'fork',
|
|
279
|
-
data: fork,
|
|
280
|
-
color
|
|
281
|
-
})
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Dibujar curva de ramificación desde main hasta el primer commit del fork
|
|
285
|
-
ctx.strokeStyle = color
|
|
286
|
-
ctx.lineWidth = 2 * state.zoom
|
|
287
|
-
ctx.beginPath()
|
|
288
|
-
ctx.moveTo(branchPoint.x, branchPoint.y)
|
|
289
|
-
|
|
290
|
-
// Bezier curve para transición suave
|
|
291
|
-
const cp1x = branchPoint.x
|
|
292
|
-
const cp1y = branchPoint.y + NODE_SPACING / 3
|
|
293
|
-
const cp2x = forkCommits[0].x
|
|
294
|
-
const cp2y = branchPoint.y + NODE_SPACING * 0.7
|
|
295
|
-
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, forkCommits[0].x, forkCommits[0].y)
|
|
296
|
-
ctx.stroke()
|
|
297
|
-
|
|
298
|
-
// Dibujar línea del fork
|
|
299
|
-
if (forkCommits.length > 1) {
|
|
300
|
-
ctx.beginPath()
|
|
301
|
-
ctx.moveTo(forkCommits[0].x, forkCommits[0].y)
|
|
302
|
-
ctx.lineTo(forkCommits[forkCommits.length - 1].x, forkCommits[forkCommits.length - 1].y)
|
|
303
|
-
ctx.stroke()
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Si el fork está merged, dibujar línea de vuelta a main
|
|
307
|
-
if (fork.mergedToMain) {
|
|
308
|
-
const lastForkCommit = forkCommits[forkCommits.length - 1]
|
|
309
|
-
const mergeTargetIdx = branchPointIdx + COMMITS_PER_BRANCH
|
|
310
|
-
const mergeTarget = mainCommits[Math.min(mergeTargetIdx, mainCommits.length - 1)]
|
|
311
|
-
|
|
312
|
-
ctx.strokeStyle = color
|
|
313
|
-
ctx.globalAlpha = 0.5
|
|
314
|
-
ctx.setLineDash([5, 5])
|
|
315
|
-
ctx.beginPath()
|
|
316
|
-
ctx.moveTo(lastForkCommit.x, lastForkCommit.y)
|
|
317
|
-
|
|
318
|
-
const mcp1x = lastForkCommit.x
|
|
319
|
-
const mcp1y = lastForkCommit.y + NODE_SPACING / 3
|
|
320
|
-
const mcp2x = mergeTarget.x
|
|
321
|
-
const mcp2y = mergeTarget.y - NODE_SPACING / 3
|
|
322
|
-
ctx.bezierCurveTo(mcp1x, mcp1y, mcp2x, mcp2y, mergeTarget.x, mergeTarget.y)
|
|
323
|
-
ctx.stroke()
|
|
324
|
-
ctx.setLineDash([])
|
|
325
|
-
ctx.globalAlpha = 1
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Dibujar commits del fork
|
|
329
|
-
forkCommits.forEach((commit, commitIdx) => {
|
|
330
|
-
const isHead = commitIdx === 0
|
|
331
|
-
drawNode(ctx, commit.x, commit.y, fork.status, color, isHead)
|
|
332
|
-
if (isHead) {
|
|
333
|
-
allNodes.push(commit)
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
// Label del fork
|
|
338
|
-
ctx.fillStyle = '#e8e8e8'
|
|
339
|
-
ctx.font = `bold ${11 * state.zoom}px -apple-system, sans-serif`
|
|
340
|
-
ctx.textAlign = isLeft ? 'right' : 'left'
|
|
341
|
-
const labelX = isLeft ? forkCommits[0].x - 15 : forkCommits[0].x + 15
|
|
342
|
-
ctx.fillText(fork.name, labelX, forkCommits[0].y + 4)
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
// Store nodes for click detection
|
|
346
|
-
canvas.nodes = allNodes
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function drawNode(ctx, x, y, status, color, isHead = false) {
|
|
350
|
-
const radius = (isHead ? NODE_RADIUS * 1.3 : NODE_RADIUS) * state.zoom
|
|
351
|
-
|
|
352
|
-
// Outer circle (branch color)
|
|
353
|
-
ctx.beginPath()
|
|
354
|
-
ctx.arc(x, y, radius, 0, Math.PI * 2)
|
|
355
|
-
ctx.fillStyle = color
|
|
356
|
-
ctx.fill()
|
|
357
|
-
|
|
358
|
-
// Outline más grueso para HEAD
|
|
359
|
-
ctx.strokeStyle = '#141414'
|
|
360
|
-
ctx.lineWidth = isHead ? 3 : 2
|
|
361
|
-
ctx.stroke()
|
|
362
|
-
|
|
363
|
-
// Inner circle for status indicator (solo en HEAD)
|
|
364
|
-
if (isHead && (status === 'saved' || status === 'merged')) {
|
|
365
|
-
ctx.beginPath()
|
|
366
|
-
ctx.arc(x, y, radius / 2.5, 0, Math.PI * 2)
|
|
367
|
-
ctx.fillStyle = status === 'saved' ? COLORS.saved : COLORS.merged
|
|
368
|
-
ctx.fill()
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Highlight ring para HEAD activo
|
|
372
|
-
if (isHead && status === 'active') {
|
|
373
|
-
ctx.beginPath()
|
|
374
|
-
ctx.arc(x, y, radius + 3, 0, Math.PI * 2)
|
|
375
|
-
ctx.strokeStyle = color
|
|
376
|
-
ctx.lineWidth = 2
|
|
377
|
-
ctx.globalAlpha = 0.5
|
|
378
|
-
ctx.stroke()
|
|
379
|
-
ctx.globalAlpha = 1
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// ===== DETAILS PANEL (Right Sidebar) =====
|
|
384
|
-
function showDetailsEmpty() {
|
|
385
|
-
$el.detailsEmpty.classList.remove('hidden')
|
|
386
|
-
$el.detailsMain.classList.remove('active')
|
|
387
|
-
$el.detailsFork.classList.remove('active')
|
|
388
|
-
$el.detailsSession.classList.remove('active')
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function showMainDetails() {
|
|
392
|
-
if (!state.currentSession) return
|
|
393
|
-
|
|
394
|
-
$el.detailsEmpty.classList.add('hidden')
|
|
395
|
-
$el.detailsMain.classList.add('active')
|
|
396
|
-
$el.detailsFork.classList.remove('active')
|
|
397
|
-
$el.detailsSession.classList.remove('active')
|
|
398
|
-
|
|
399
|
-
const session = state.currentSession
|
|
400
|
-
const main = session.main
|
|
401
|
-
|
|
402
|
-
// Update main details
|
|
403
|
-
document.getElementById('main-session-name').textContent = session.name
|
|
404
|
-
document.getElementById('main-created').textContent = formatDate(main.createdAt)
|
|
405
|
-
document.getElementById('main-tmux').textContent = session.tmuxSessionName
|
|
406
|
-
document.getElementById('main-status').className = `status-indicator status-${session.status}`
|
|
407
|
-
|
|
408
|
-
// Context (only if saved)
|
|
409
|
-
const contextRow = document.getElementById('main-context-row')
|
|
410
|
-
if (main.contextPath) {
|
|
411
|
-
contextRow.style.display = 'block'
|
|
412
|
-
document.getElementById('main-context').textContent = main.contextPath
|
|
413
|
-
} else {
|
|
414
|
-
contextRow.style.display = 'none'
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Button states
|
|
418
|
-
document.getElementById('main-resume-btn').disabled = session.status === 'active'
|
|
419
|
-
document.getElementById('main-close-btn').disabled = session.status === 'saved'
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function showForkDetails(fork) {
|
|
423
|
-
if (!fork) return
|
|
424
|
-
|
|
425
|
-
$el.detailsEmpty.classList.add('hidden')
|
|
426
|
-
$el.detailsMain.classList.remove('active')
|
|
427
|
-
$el.detailsFork.classList.add('active')
|
|
428
|
-
$el.detailsSession.classList.remove('active')
|
|
429
|
-
|
|
430
|
-
// Update fork details
|
|
431
|
-
document.getElementById('fork-name').textContent = fork.name
|
|
432
|
-
document.getElementById('fork-id').textContent = fork.id
|
|
433
|
-
document.getElementById('fork-created').textContent = formatDate(fork.createdAt)
|
|
434
|
-
document.getElementById('fork-pane').textContent = fork.tmuxPaneId || 'N/A'
|
|
435
|
-
document.getElementById('fork-status').className = `status-indicator status-${fork.status}`
|
|
436
|
-
|
|
437
|
-
// Context (only if saved)
|
|
438
|
-
const contextRow = document.getElementById('fork-context-row')
|
|
439
|
-
if (fork.contextPath) {
|
|
440
|
-
contextRow.style.display = 'block'
|
|
441
|
-
document.getElementById('fork-context').textContent = fork.contextPath
|
|
442
|
-
} else {
|
|
443
|
-
contextRow.style.display = 'none'
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Merged info
|
|
447
|
-
const mergedRow = document.getElementById('fork-merged-row')
|
|
448
|
-
if (fork.mergedToMain) {
|
|
449
|
-
mergedRow.style.display = 'block'
|
|
450
|
-
document.getElementById('fork-merged').textContent = `Yes (${formatDate(fork.mergedAt)})`
|
|
451
|
-
} else {
|
|
452
|
-
mergedRow.style.display = 'none'
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Button states
|
|
456
|
-
document.getElementById('fork-export-btn').disabled = fork.status !== 'active'
|
|
457
|
-
document.getElementById('fork-merge-btn').disabled = fork.status !== 'active' || !fork.contextPath
|
|
458
|
-
document.getElementById('fork-resume-btn').disabled = fork.status === 'active'
|
|
459
|
-
document.getElementById('fork-close-btn').disabled = fork.status !== 'active'
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// ===== EVENT HANDLERS =====
|
|
463
|
-
|
|
464
|
-
// Canvas click - detect node clicks
|
|
465
|
-
$el.graphCanvas.addEventListener('click', (e) => {
|
|
466
|
-
if (!$el.graphCanvas.nodes) return
|
|
467
|
-
|
|
468
|
-
const rect = $el.graphCanvas.getBoundingClientRect()
|
|
469
|
-
const x = e.clientX - rect.left
|
|
470
|
-
const y = e.clientY - rect.top
|
|
471
|
-
|
|
472
|
-
const clickedNode = $el.graphCanvas.nodes.find(node => {
|
|
473
|
-
const dx = x - node.x
|
|
474
|
-
const dy = y - node.y
|
|
475
|
-
const clickRadius = NODE_RADIUS * 1.5 * state.zoom + 5
|
|
476
|
-
return Math.sqrt(dx * dx + dy * dy) <= clickRadius
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
if (clickedNode) {
|
|
480
|
-
state.selectedNode = clickedNode
|
|
481
|
-
if (clickedNode.type === 'main') {
|
|
482
|
-
showMainDetails()
|
|
483
|
-
} else {
|
|
484
|
-
showForkDetails(clickedNode.data)
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
// Filter tabs
|
|
490
|
-
$el.filterTabs.forEach(tab => {
|
|
491
|
-
tab.addEventListener('click', () => {
|
|
492
|
-
$el.filterTabs.forEach(t => t.classList.remove('active'))
|
|
493
|
-
tab.classList.add('active')
|
|
494
|
-
state.currentFilter = tab.dataset.filter
|
|
495
|
-
renderSessionsList()
|
|
496
|
-
})
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
// Zoom controls
|
|
500
|
-
$el.zoomInBtn.addEventListener('click', () => {
|
|
501
|
-
state.zoom = Math.min(state.zoom + 0.1, 2.0)
|
|
502
|
-
renderGraph()
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
$el.zoomOutBtn.addEventListener('click', () => {
|
|
506
|
-
state.zoom = Math.max(state.zoom - 0.1, 0.5)
|
|
507
|
-
renderGraph()
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
$el.zoomResetBtn.addEventListener('click', () => {
|
|
511
|
-
state.zoom = 1.0
|
|
512
|
-
renderGraph()
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
// Header buttons
|
|
516
|
-
$el.newSessionBtn.addEventListener('click', createSession)
|
|
517
|
-
$el.refreshBtn.addEventListener('click', loadSessions)
|
|
518
|
-
|
|
519
|
-
// Main branch actions
|
|
520
|
-
document.getElementById('main-resume-btn').addEventListener('click', () => resumeSession(state.currentSession.id))
|
|
521
|
-
document.getElementById('main-close-btn').addEventListener('click', () => closeSession(state.currentSession.id))
|
|
522
|
-
document.getElementById('new-fork-btn').addEventListener('click', () => createFork(state.currentSession.id))
|
|
523
|
-
document.getElementById('send-command-main-btn').addEventListener('click', () => openCommandModal('main'))
|
|
524
|
-
|
|
525
|
-
// Fork actions
|
|
526
|
-
document.getElementById('fork-export-btn').addEventListener('click', () => exportFork())
|
|
527
|
-
document.getElementById('fork-merge-btn').addEventListener('click', () => mergeFork())
|
|
528
|
-
document.getElementById('fork-resume-btn').addEventListener('click', () => resumeFork())
|
|
529
|
-
document.getElementById('fork-close-btn').addEventListener('click', () => closeFork())
|
|
530
|
-
document.getElementById('send-command-fork-btn').addEventListener('click', () => openCommandModal('fork'))
|
|
531
|
-
document.getElementById('fork-delete-btn').addEventListener('click', () => deleteFork())
|
|
532
|
-
|
|
533
|
-
// Session details actions
|
|
534
|
-
document.getElementById('session-detail-resume-btn').addEventListener('click', () => resumeSession(state.currentSession.id))
|
|
535
|
-
document.getElementById('session-detail-close-btn').addEventListener('click', () => closeSession(state.currentSession.id))
|
|
536
|
-
document.getElementById('session-detail-delete-btn').addEventListener('click', () => deleteSession(state.currentSession.id))
|
|
537
|
-
|
|
538
|
-
// Command modal
|
|
539
|
-
document.querySelector('.modal-close').addEventListener('click', closeCommandModal)
|
|
540
|
-
$el.commandCancelBtn.addEventListener('click', closeCommandModal)
|
|
541
|
-
$el.commandSendBtn.addEventListener('click', sendCommand)
|
|
542
|
-
|
|
543
|
-
// ===== API CALLS =====
|
|
544
|
-
|
|
545
|
-
async function loadSessions() {
|
|
546
|
-
try {
|
|
547
|
-
const result = await window.orka.getSessions()
|
|
548
|
-
if (result.success) {
|
|
549
|
-
state.sessions = result.data
|
|
550
|
-
renderSessionsList()
|
|
551
|
-
if (state.currentSession) {
|
|
552
|
-
state.currentSession = state.sessions.find(s => s.id === state.currentSession.id)
|
|
553
|
-
renderGraph()
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
} catch (error) {
|
|
557
|
-
showToast(`Error loading sessions: ${error.message}`, 'error')
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function createSession() {
|
|
562
|
-
const name = await askUser('Enter session name (optional):', '')
|
|
563
|
-
try {
|
|
564
|
-
const result = await window.orka.createSession(name || undefined, true)
|
|
565
|
-
if (result.success) {
|
|
566
|
-
showToast('Session created!', 'success')
|
|
567
|
-
await loadSessions()
|
|
568
|
-
selectSession(result.data.id)
|
|
569
|
-
} else {
|
|
570
|
-
showToast(result.error, 'error')
|
|
571
|
-
}
|
|
572
|
-
} catch (error) {
|
|
573
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async function resumeSession(sessionId) {
|
|
578
|
-
try {
|
|
579
|
-
const result = await window.orka.resumeSession(sessionId, true)
|
|
580
|
-
if (result.success) {
|
|
581
|
-
showToast('Session resumed!', 'success')
|
|
582
|
-
await loadSessions()
|
|
583
|
-
} else {
|
|
584
|
-
showToast(result.error, 'error')
|
|
585
|
-
}
|
|
586
|
-
} catch (error) {
|
|
587
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async function closeSession(sessionId) {
|
|
592
|
-
const confirmed = await confirmAction('Close this session? Context will be saved.')
|
|
593
|
-
if (!confirmed) return
|
|
594
|
-
|
|
595
|
-
try {
|
|
596
|
-
const result = await window.orka.closeSession(sessionId)
|
|
597
|
-
if (result.success) {
|
|
598
|
-
showToast('Session closed and saved!', 'success')
|
|
599
|
-
await loadSessions()
|
|
600
|
-
} else {
|
|
601
|
-
showToast(result.error, 'error')
|
|
602
|
-
}
|
|
603
|
-
} catch (error) {
|
|
604
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
async function deleteSession(sessionId) {
|
|
609
|
-
const confirmed = await confirmAction('Delete this session? This cannot be undone.')
|
|
610
|
-
if (!confirmed) return
|
|
611
|
-
|
|
612
|
-
try {
|
|
613
|
-
const result = await window.orka.deleteSession(sessionId)
|
|
614
|
-
if (result.success) {
|
|
615
|
-
showToast('Session deleted!', 'success')
|
|
616
|
-
state.currentSession = null
|
|
617
|
-
state.selectedNode = null
|
|
618
|
-
await loadSessions()
|
|
619
|
-
showDetailsEmpty()
|
|
620
|
-
$el.graphEmpty.classList.remove('hidden')
|
|
621
|
-
} else {
|
|
622
|
-
showToast(result.error, 'error')
|
|
623
|
-
}
|
|
624
|
-
} catch (error) {
|
|
625
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async function createFork(sessionId) {
|
|
630
|
-
const name = await askUser('Enter fork name (optional):', '')
|
|
631
|
-
try {
|
|
632
|
-
const result = await window.orka.createFork(sessionId, name || undefined)
|
|
633
|
-
if (result.success) {
|
|
634
|
-
showToast('Fork created!', 'success')
|
|
635
|
-
await loadSessions()
|
|
636
|
-
renderGraph()
|
|
637
|
-
} else {
|
|
638
|
-
showToast(result.error, 'error')
|
|
639
|
-
}
|
|
640
|
-
} catch (error) {
|
|
641
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async function exportFork() {
|
|
646
|
-
if (!state.selectedNode || state.selectedNode.type !== 'fork') return
|
|
647
|
-
|
|
648
|
-
try {
|
|
649
|
-
const result = await window.orka.export(state.currentSession.id, state.selectedNode.data.id)
|
|
650
|
-
if (result.success) {
|
|
651
|
-
showToast(`Context exported: ${result.data}`, 'success')
|
|
652
|
-
await loadSessions()
|
|
653
|
-
renderGraph()
|
|
654
|
-
} else {
|
|
655
|
-
showToast(result.error, 'error')
|
|
656
|
-
}
|
|
657
|
-
} catch (error) {
|
|
658
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async function mergeFork() {
|
|
663
|
-
if (!state.selectedNode || state.selectedNode.type !== 'fork') return
|
|
664
|
-
|
|
665
|
-
const confirmed = await confirmAction('Merge this fork to main?')
|
|
666
|
-
if (!confirmed) return
|
|
667
|
-
|
|
668
|
-
try {
|
|
669
|
-
const result = await window.orka.merge(state.currentSession.id, state.selectedNode.data.id)
|
|
670
|
-
if (result.success) {
|
|
671
|
-
showToast('Fork merged to main!', 'success')
|
|
672
|
-
await loadSessions()
|
|
673
|
-
renderGraph()
|
|
674
|
-
} else {
|
|
675
|
-
showToast(result.error, 'error')
|
|
676
|
-
}
|
|
677
|
-
} catch (error) {
|
|
678
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async function resumeFork() {
|
|
683
|
-
if (!state.selectedNode || state.selectedNode.type !== 'fork') return
|
|
684
|
-
|
|
685
|
-
try {
|
|
686
|
-
const result = await window.orka.resumeFork(state.currentSession.id, state.selectedNode.data.id)
|
|
687
|
-
if (result.success) {
|
|
688
|
-
showToast('Fork resumed!', 'success')
|
|
689
|
-
await loadSessions()
|
|
690
|
-
renderGraph()
|
|
691
|
-
} else {
|
|
692
|
-
showToast(result.error, 'error')
|
|
693
|
-
}
|
|
694
|
-
} catch (error) {
|
|
695
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
async function closeFork() {
|
|
700
|
-
if (!state.selectedNode || state.selectedNode.type !== 'fork') return
|
|
701
|
-
|
|
702
|
-
const confirmed = await confirmAction('Close this fork? Context will be saved.')
|
|
703
|
-
if (!confirmed) return
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
const result = await window.orka.closeFork(state.currentSession.id, state.selectedNode.data.id)
|
|
707
|
-
if (result.success) {
|
|
708
|
-
showToast('Fork closed and saved!', 'success')
|
|
709
|
-
await loadSessions()
|
|
710
|
-
renderGraph()
|
|
711
|
-
} else {
|
|
712
|
-
showToast(result.error, 'error')
|
|
713
|
-
}
|
|
714
|
-
} catch (error) {
|
|
715
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function deleteFork() {
|
|
720
|
-
if (!state.selectedNode || state.selectedNode.type !== 'fork') return
|
|
721
|
-
|
|
722
|
-
const confirmed = await confirmAction('Delete this fork? This cannot be undone.')
|
|
723
|
-
if (!confirmed) return
|
|
724
|
-
|
|
725
|
-
try {
|
|
726
|
-
const result = await window.orka.deleteFork(state.currentSession.id, state.selectedNode.data.id)
|
|
727
|
-
if (result.success) {
|
|
728
|
-
showToast('Fork deleted!', 'success')
|
|
729
|
-
state.selectedNode = null
|
|
730
|
-
await loadSessions()
|
|
731
|
-
renderGraph()
|
|
732
|
-
showDetailsEmpty()
|
|
733
|
-
} else {
|
|
734
|
-
showToast(result.error, 'error')
|
|
735
|
-
}
|
|
736
|
-
} catch (error) {
|
|
737
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// Command modal
|
|
742
|
-
let commandTarget = null
|
|
743
|
-
|
|
744
|
-
function openCommandModal(target) {
|
|
745
|
-
commandTarget = target
|
|
746
|
-
if (target === 'main') {
|
|
747
|
-
$el.commandTargetLabel.textContent = 'Main Branch'
|
|
748
|
-
} else if (state.selectedNode) {
|
|
749
|
-
$el.commandTargetLabel.textContent = `Fork: ${state.selectedNode.data.name}`
|
|
750
|
-
}
|
|
751
|
-
$el.commandModal.classList.add('active')
|
|
752
|
-
$el.commandInput.value = ''
|
|
753
|
-
$el.commandInput.focus()
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function closeCommandModal() {
|
|
757
|
-
$el.commandModal.classList.remove('active')
|
|
758
|
-
commandTarget = null
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
async function sendCommand() {
|
|
762
|
-
const command = $el.commandInput.value.trim()
|
|
763
|
-
if (!command) return
|
|
764
|
-
|
|
765
|
-
try {
|
|
766
|
-
let result
|
|
767
|
-
if (commandTarget === 'main') {
|
|
768
|
-
result = await window.orka.sendCommand(state.currentSession.id, '', command)
|
|
769
|
-
} else if (state.selectedNode) {
|
|
770
|
-
result = await window.orka.sendCommand(state.currentSession.id, state.selectedNode.data.id, command)
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (result.success) {
|
|
774
|
-
showToast('Command sent!', 'success')
|
|
775
|
-
closeCommandModal()
|
|
776
|
-
} else {
|
|
777
|
-
showToast(result.error, 'error')
|
|
778
|
-
}
|
|
779
|
-
} catch (error) {
|
|
780
|
-
showToast(`Error: ${error.message}`, 'error')
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// ===== INITIALIZATION =====
|
|
785
|
-
async function init() {
|
|
786
|
-
try {
|
|
787
|
-
const result = await window.orka.initialize()
|
|
788
|
-
if (result.success) {
|
|
789
|
-
state.projectPath = result.data
|
|
790
|
-
$el.projectPath.textContent = result.data
|
|
791
|
-
await loadSessions()
|
|
792
|
-
} else {
|
|
793
|
-
showToast('Failed to initialize: ' + result.error, 'error')
|
|
794
|
-
}
|
|
795
|
-
} catch (error) {
|
|
796
|
-
showToast('Error initializing: ' + error.message, 'error')
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Start the app
|
|
801
|
-
init()
|
|
802
|
-
|
|
803
|
-
// Handle window resize
|
|
804
|
-
window.addEventListener('resize', () => {
|
|
805
|
-
if (state.currentSession) {
|
|
806
|
-
renderGraph()
|
|
807
|
-
}
|
|
808
|
-
})
|