@enruana/claude-orka 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.
Files changed (162) hide show
  1. package/README.md +240 -0
  2. package/bin/orka.js +19 -0
  3. package/dist/core/ClaudeOrka.d.ts +111 -0
  4. package/dist/core/ClaudeOrka.d.ts.map +1 -0
  5. package/dist/core/ClaudeOrka.js +160 -0
  6. package/dist/core/ClaudeOrka.js.map +1 -0
  7. package/dist/core/SessionManager.d.ts +82 -0
  8. package/dist/core/SessionManager.d.ts.map +1 -0
  9. package/dist/core/SessionManager.js +519 -0
  10. package/dist/core/SessionManager.js.map +1 -0
  11. package/dist/core/StateManager.d.ts +92 -0
  12. package/dist/core/StateManager.d.ts.map +1 -0
  13. package/dist/core/StateManager.js +307 -0
  14. package/dist/core/StateManager.js.map +1 -0
  15. package/dist/core/index.d.ts +4 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +20 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/electron/main/ipc-handlers.d.ts +5 -0
  20. package/dist/electron/main/ipc-handlers.d.ts.map +1 -0
  21. package/dist/electron/main/ipc-handlers.js +169 -0
  22. package/dist/electron/main/ipc-handlers.js.map +1 -0
  23. package/dist/electron/main/main.d.ts +5 -0
  24. package/dist/electron/main/main.d.ts.map +1 -0
  25. package/dist/electron/main/main.js +66 -0
  26. package/dist/electron/main/main.js.map +1 -0
  27. package/dist/electron/preload/preload.d.ts +25 -0
  28. package/dist/electron/preload/preload.d.ts.map +1 -0
  29. package/dist/electron/preload/preload.js +31 -0
  30. package/dist/electron/preload/preload.js.map +1 -0
  31. package/dist/electron/renderer/app.js +808 -0
  32. package/dist/electron/renderer/index.html +189 -0
  33. package/dist/electron/renderer/styles.css +736 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +18 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/models/Fork.d.ts +26 -0
  39. package/dist/models/Fork.d.ts.map +1 -0
  40. package/dist/models/Fork.js +3 -0
  41. package/dist/models/Fork.js.map +1 -0
  42. package/dist/models/Session.d.ts +38 -0
  43. package/dist/models/Session.d.ts.map +1 -0
  44. package/dist/models/Session.js +3 -0
  45. package/dist/models/Session.js.map +1 -0
  46. package/dist/models/State.d.ts +24 -0
  47. package/dist/models/State.d.ts.map +1 -0
  48. package/dist/models/State.js +3 -0
  49. package/dist/models/State.js.map +1 -0
  50. package/dist/models/index.d.ts +4 -0
  51. package/dist/models/index.d.ts.map +1 -0
  52. package/dist/models/index.js +20 -0
  53. package/dist/models/index.js.map +1 -0
  54. package/dist/src/cli/commands/doctor.d.ts +3 -0
  55. package/dist/src/cli/commands/doctor.d.ts.map +1 -0
  56. package/dist/src/cli/commands/doctor.js +266 -0
  57. package/dist/src/cli/commands/doctor.js.map +1 -0
  58. package/dist/src/cli/commands/fork.d.ts +3 -0
  59. package/dist/src/cli/commands/fork.d.ts.map +1 -0
  60. package/dist/src/cli/commands/fork.js +136 -0
  61. package/dist/src/cli/commands/fork.js.map +1 -0
  62. package/dist/src/cli/commands/init.d.ts +3 -0
  63. package/dist/src/cli/commands/init.d.ts.map +1 -0
  64. package/dist/src/cli/commands/init.js +22 -0
  65. package/dist/src/cli/commands/init.js.map +1 -0
  66. package/dist/src/cli/commands/merge.d.ts +3 -0
  67. package/dist/src/cli/commands/merge.d.ts.map +1 -0
  68. package/dist/src/cli/commands/merge.js +84 -0
  69. package/dist/src/cli/commands/merge.js.map +1 -0
  70. package/dist/src/cli/commands/prepare.d.ts +3 -0
  71. package/dist/src/cli/commands/prepare.d.ts.map +1 -0
  72. package/dist/src/cli/commands/prepare.js +154 -0
  73. package/dist/src/cli/commands/prepare.js.map +1 -0
  74. package/dist/src/cli/commands/session.d.ts +3 -0
  75. package/dist/src/cli/commands/session.d.ts.map +1 -0
  76. package/dist/src/cli/commands/session.js +166 -0
  77. package/dist/src/cli/commands/session.js.map +1 -0
  78. package/dist/src/cli/commands/status.d.ts +3 -0
  79. package/dist/src/cli/commands/status.d.ts.map +1 -0
  80. package/dist/src/cli/commands/status.js +28 -0
  81. package/dist/src/cli/commands/status.js.map +1 -0
  82. package/dist/src/cli/index.d.ts +3 -0
  83. package/dist/src/cli/index.d.ts.map +1 -0
  84. package/dist/src/cli/index.js +25 -0
  85. package/dist/src/cli/index.js.map +1 -0
  86. package/dist/src/cli/utils/errors.d.ts +24 -0
  87. package/dist/src/cli/utils/errors.d.ts.map +1 -0
  88. package/dist/src/cli/utils/errors.js +57 -0
  89. package/dist/src/cli/utils/errors.js.map +1 -0
  90. package/dist/src/cli/utils/output.d.ts +59 -0
  91. package/dist/src/cli/utils/output.d.ts.map +1 -0
  92. package/dist/src/cli/utils/output.js +222 -0
  93. package/dist/src/cli/utils/output.js.map +1 -0
  94. package/dist/src/core/ClaudeOrka.d.ts +158 -0
  95. package/dist/src/core/ClaudeOrka.d.ts.map +1 -0
  96. package/dist/src/core/ClaudeOrka.js +264 -0
  97. package/dist/src/core/ClaudeOrka.js.map +1 -0
  98. package/dist/src/core/SessionManager.d.ts +84 -0
  99. package/dist/src/core/SessionManager.d.ts.map +1 -0
  100. package/dist/src/core/SessionManager.js +501 -0
  101. package/dist/src/core/SessionManager.js.map +1 -0
  102. package/dist/src/core/StateManager.d.ts +108 -0
  103. package/dist/src/core/StateManager.d.ts.map +1 -0
  104. package/dist/src/core/StateManager.js +317 -0
  105. package/dist/src/core/StateManager.js.map +1 -0
  106. package/dist/src/core/index.d.ts +4 -0
  107. package/dist/src/core/index.d.ts.map +1 -0
  108. package/dist/src/core/index.js +4 -0
  109. package/dist/src/core/index.js.map +1 -0
  110. package/dist/src/index.d.ts +9 -0
  111. package/dist/src/index.d.ts.map +1 -0
  112. package/dist/src/index.js +10 -0
  113. package/dist/src/index.js.map +1 -0
  114. package/dist/src/models/Fork.d.ts +24 -0
  115. package/dist/src/models/Fork.d.ts.map +1 -0
  116. package/dist/src/models/Fork.js +2 -0
  117. package/dist/src/models/Fork.js.map +1 -0
  118. package/dist/src/models/Session.d.ts +36 -0
  119. package/dist/src/models/Session.d.ts.map +1 -0
  120. package/dist/src/models/Session.js +2 -0
  121. package/dist/src/models/Session.js.map +1 -0
  122. package/dist/src/models/State.d.ts +24 -0
  123. package/dist/src/models/State.d.ts.map +1 -0
  124. package/dist/src/models/State.js +2 -0
  125. package/dist/src/models/State.js.map +1 -0
  126. package/dist/src/models/Summary.d.ts +68 -0
  127. package/dist/src/models/Summary.d.ts.map +1 -0
  128. package/dist/src/models/Summary.js +2 -0
  129. package/dist/src/models/Summary.js.map +1 -0
  130. package/dist/src/models/index.d.ts +5 -0
  131. package/dist/src/models/index.d.ts.map +1 -0
  132. package/dist/src/models/index.js +5 -0
  133. package/dist/src/models/index.js.map +1 -0
  134. package/dist/src/utils/claude-history.d.ts +34 -0
  135. package/dist/src/utils/claude-history.d.ts.map +1 -0
  136. package/dist/src/utils/claude-history.js +82 -0
  137. package/dist/src/utils/claude-history.js.map +1 -0
  138. package/dist/src/utils/index.d.ts +4 -0
  139. package/dist/src/utils/index.d.ts.map +1 -0
  140. package/dist/src/utils/index.js +4 -0
  141. package/dist/src/utils/index.js.map +1 -0
  142. package/dist/src/utils/logger.d.ts +20 -0
  143. package/dist/src/utils/logger.d.ts.map +1 -0
  144. package/dist/src/utils/logger.js +38 -0
  145. package/dist/src/utils/logger.js.map +1 -0
  146. package/dist/src/utils/tmux.d.ts +89 -0
  147. package/dist/src/utils/tmux.d.ts.map +1 -0
  148. package/dist/src/utils/tmux.js +299 -0
  149. package/dist/src/utils/tmux.js.map +1 -0
  150. package/dist/utils/index.d.ts +3 -0
  151. package/dist/utils/index.d.ts.map +1 -0
  152. package/dist/utils/index.js +19 -0
  153. package/dist/utils/index.js.map +1 -0
  154. package/dist/utils/logger.d.ts +20 -0
  155. package/dist/utils/logger.d.ts.map +1 -0
  156. package/dist/utils/logger.js +41 -0
  157. package/dist/utils/logger.js.map +1 -0
  158. package/dist/utils/tmux.d.ts +77 -0
  159. package/dist/utils/tmux.d.ts.map +1 -0
  160. package/dist/utils/tmux.js +270 -0
  161. package/dist/utils/tmux.js.map +1 -0
  162. package/package.json +110 -0
@@ -0,0 +1,808 @@
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
+ })