@longshot/cli 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/LICENSE +8 -0
- package/dist/agent.js +214 -0
- package/dist/cli.js +172 -0
- package/dist/git.js +291 -0
- package/dist/index.js +1250 -0
- package/dist/profile.js +79 -0
- package/dist/projects.js +337 -0
- package/dist/queue.js +868 -0
- package/dist/services.js +194 -0
- package/dist/store.js +612 -0
- package/dist/views/agent-progress.js +242 -0
- package/dist/views/branches.js +191 -0
- package/dist/views/chat.js +386 -0
- package/dist/views/diff.js +321 -0
- package/dist/views/history.js +124 -0
- package/dist/views/layout.js +121 -0
- package/dist/views/run.js +92 -0
- package/dist/views/services.js +230 -0
- package/dist/views/spec.js +18 -0
- package/dist/views/tasks.js +898 -0
- package/dist/views/verify.js +209 -0
- package/package.json +36 -0
- package/public/style.css +2088 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { layout } from "./layout.js";
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str
|
|
4
|
+
.replace(/&/g, "&")
|
|
5
|
+
.replace(/</g, "<")
|
|
6
|
+
.replace(/>/g, ">")
|
|
7
|
+
.replace(/"/g, """);
|
|
8
|
+
}
|
|
9
|
+
function truncate(str, max) {
|
|
10
|
+
if (str.length <= max)
|
|
11
|
+
return str;
|
|
12
|
+
return str.slice(0, max) + "\u2026";
|
|
13
|
+
}
|
|
14
|
+
function renderEvent(event) {
|
|
15
|
+
const time = new Date(event.ts).toLocaleTimeString();
|
|
16
|
+
switch (event.type) {
|
|
17
|
+
case "user_message":
|
|
18
|
+
return `<div class="run-event run-event-user">
|
|
19
|
+
<div class="run-event-marker">U</div>
|
|
20
|
+
<div class="run-event-body">
|
|
21
|
+
<div class="run-event-label">Prompt <span class="run-event-time">${time}</span></div>
|
|
22
|
+
<div class="run-event-content">${escapeHtml(truncate(event.content || "", 300))}</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>`;
|
|
25
|
+
case "text":
|
|
26
|
+
return `<div class="run-event run-event-text">
|
|
27
|
+
<div class="run-event-marker">T</div>
|
|
28
|
+
<div class="run-event-body">
|
|
29
|
+
<div class="run-event-label">Text <span class="run-event-time">${time}</span></div>
|
|
30
|
+
<div class="run-event-content">${escapeHtml(truncate(event.content || "", 300))}</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>`;
|
|
33
|
+
case "tool_use": {
|
|
34
|
+
const name = event.name || "unknown";
|
|
35
|
+
let summary = name;
|
|
36
|
+
if (event.input) {
|
|
37
|
+
const inp = event.input;
|
|
38
|
+
if (name === "Read" && inp.file_path)
|
|
39
|
+
summary = `Read ${inp.file_path.split("/").pop()}`;
|
|
40
|
+
else if (name === "Write" && inp.file_path)
|
|
41
|
+
summary = `Write ${inp.file_path.split("/").pop()}`;
|
|
42
|
+
else if (name === "Edit" && inp.file_path)
|
|
43
|
+
summary = `Edit ${inp.file_path.split("/").pop()}`;
|
|
44
|
+
else if (name === "Bash" && inp.command)
|
|
45
|
+
summary = `Bash: ${truncate(inp.command, 60)}`;
|
|
46
|
+
else if (name === "Glob" && inp.pattern)
|
|
47
|
+
summary = `Glob ${inp.pattern}`;
|
|
48
|
+
else if (name === "Grep" && inp.pattern)
|
|
49
|
+
summary = `Grep ${inp.pattern}`;
|
|
50
|
+
}
|
|
51
|
+
return `<div class="run-event run-event-tool-use">
|
|
52
|
+
<div class="run-event-marker">⚙</div>
|
|
53
|
+
<div class="run-event-body">
|
|
54
|
+
<div class="run-event-label">${escapeHtml(summary)} <span class="run-event-time">${time}</span></div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>`;
|
|
57
|
+
}
|
|
58
|
+
case "tool_result":
|
|
59
|
+
return `<div class="run-event run-event-tool-result">
|
|
60
|
+
<div class="run-event-marker">✓</div>
|
|
61
|
+
<div class="run-event-body">
|
|
62
|
+
<div class="run-event-label">Result <span class="run-event-time">${time}</span></div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>`;
|
|
65
|
+
case "error":
|
|
66
|
+
return `<div class="run-event run-event-error">
|
|
67
|
+
<div class="run-event-marker">!</div>
|
|
68
|
+
<div class="run-event-body">
|
|
69
|
+
<div class="run-event-label">Error <span class="run-event-time">${time}</span></div>
|
|
70
|
+
<div class="run-event-content">${escapeHtml(truncate(event.message || "", 300))}</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>`;
|
|
73
|
+
case "done":
|
|
74
|
+
return `<div class="run-event run-event-done">
|
|
75
|
+
<div class="run-event-marker">●</div>
|
|
76
|
+
<div class="run-event-body">
|
|
77
|
+
<div class="run-event-label">Agent finished <span class="run-event-time">${time}</span></div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>`;
|
|
80
|
+
case "diff_ready":
|
|
81
|
+
return `<div class="run-event run-event-done">
|
|
82
|
+
<div class="run-event-marker">●</div>
|
|
83
|
+
<div class="run-event-body">
|
|
84
|
+
<div class="run-event-label">Diff ready <span class="run-event-time">${time}</span></div>
|
|
85
|
+
<div class="run-event-content">${escapeHtml(event.summary || "")}</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>`;
|
|
88
|
+
case "agent_done_no_changes":
|
|
89
|
+
return `<div class="run-event run-event-done">
|
|
90
|
+
<div class="run-event-marker">●</div>
|
|
91
|
+
<div class="run-event-body">
|
|
92
|
+
<div class="run-event-label">Done — no file changes <span class="run-event-time">${time}</span></div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>`;
|
|
95
|
+
case "auto_committed":
|
|
96
|
+
return `<div class="run-event run-event-done">
|
|
97
|
+
<div class="run-event-marker">✓</div>
|
|
98
|
+
<div class="run-event-body">
|
|
99
|
+
<div class="run-event-label">Auto-committed <span class="run-event-time">${time}</span></div>
|
|
100
|
+
<div class="run-event-content">${escapeHtml(event.summary || "")}</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>`;
|
|
103
|
+
default:
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function agentProgressPage(runId, events, isRunning) {
|
|
108
|
+
const eventsHtml = events.map(renderEvent).filter(Boolean).join("\n");
|
|
109
|
+
// Check for terminal states from server-rendered events
|
|
110
|
+
let diffId = null;
|
|
111
|
+
let noChanges = false;
|
|
112
|
+
let autoCommitted = false;
|
|
113
|
+
for (const ev of events) {
|
|
114
|
+
if (ev.type === "diff_ready")
|
|
115
|
+
diffId = ev.diffId;
|
|
116
|
+
if (ev.type === "agent_done_no_changes")
|
|
117
|
+
noChanges = true;
|
|
118
|
+
if (ev.type === "auto_committed")
|
|
119
|
+
autoCommitted = true;
|
|
120
|
+
}
|
|
121
|
+
const doneSection = diffId
|
|
122
|
+
? `<div class="agent-progress-done">
|
|
123
|
+
<a href="/diff/${diffId}" class="btn btn-review">Review Diff</a>
|
|
124
|
+
</div>`
|
|
125
|
+
: autoCommitted
|
|
126
|
+
? `<div class="agent-progress-done">
|
|
127
|
+
<div class="agent-done">Done — changes committed</div>
|
|
128
|
+
<a href="/tasks" class="btn btn-small" style="margin-top:12px">Back to Tasks</a>
|
|
129
|
+
</div>`
|
|
130
|
+
: noChanges
|
|
131
|
+
? `<div class="agent-progress-done">
|
|
132
|
+
<div class="agent-done">Done — no file changes</div>
|
|
133
|
+
<a href="/chat" class="btn btn-small" style="margin-top:12px">Back to Chat</a>
|
|
134
|
+
</div>`
|
|
135
|
+
: "";
|
|
136
|
+
const body = `
|
|
137
|
+
<div class="run-container">
|
|
138
|
+
<div class="run-header">
|
|
139
|
+
<a href="/chat" class="back-link">Chat</a>
|
|
140
|
+
<h1>Agent Progress</h1>
|
|
141
|
+
${isRunning ? '<div class="agent-status-indicator">Agent working...</div>' : ""}
|
|
142
|
+
</div>
|
|
143
|
+
<div class="run-timeline" id="events-container">
|
|
144
|
+
${eventsHtml}
|
|
145
|
+
</div>
|
|
146
|
+
<div id="done-section">${doneSection}</div>
|
|
147
|
+
</div>
|
|
148
|
+
<script>
|
|
149
|
+
const runId = ${JSON.stringify(runId)};
|
|
150
|
+
let eventCount = ${events.length};
|
|
151
|
+
let done = ${!isRunning && (!!diffId || noChanges || autoCommitted)};
|
|
152
|
+
const container = document.getElementById('events-container');
|
|
153
|
+
const doneSection = document.getElementById('done-section');
|
|
154
|
+
|
|
155
|
+
function escapeHtml(str) {
|
|
156
|
+
const div = document.createElement('div');
|
|
157
|
+
div.textContent = str;
|
|
158
|
+
return div.innerHTML;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function truncate(str, max) {
|
|
162
|
+
if (str.length <= max) return str;
|
|
163
|
+
return str.slice(0, max) + '\\u2026';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function toolSummary(name, input) {
|
|
167
|
+
if (!input) return name;
|
|
168
|
+
if (name === 'Read' && input.file_path) return 'Read ' + input.file_path.split('/').pop();
|
|
169
|
+
if (name === 'Write' && input.file_path) return 'Write ' + input.file_path.split('/').pop();
|
|
170
|
+
if (name === 'Edit' && input.file_path) return 'Edit ' + input.file_path.split('/').pop();
|
|
171
|
+
if (name === 'Bash' && input.command) return 'Bash: ' + truncate(input.command, 60);
|
|
172
|
+
if (name === 'Glob' && input.pattern) return 'Glob ' + input.pattern;
|
|
173
|
+
if (name === 'Grep' && input.pattern) return 'Grep ' + input.pattern;
|
|
174
|
+
return name;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderEventClient(ev) {
|
|
178
|
+
const time = new Date(ev.ts).toLocaleTimeString();
|
|
179
|
+
switch (ev.type) {
|
|
180
|
+
case 'user_message':
|
|
181
|
+
return '<div class="run-event run-event-user"><div class="run-event-marker">U</div><div class="run-event-body"><div class="run-event-label">Prompt <span class="run-event-time">' + time + '</span></div><div class="run-event-content">' + escapeHtml(truncate(ev.content || '', 300)) + '</div></div></div>';
|
|
182
|
+
case 'text':
|
|
183
|
+
return '<div class="run-event run-event-text"><div class="run-event-marker">T</div><div class="run-event-body"><div class="run-event-label">Text <span class="run-event-time">' + time + '</span></div><div class="run-event-content">' + escapeHtml(truncate(ev.content || '', 300)) + '</div></div></div>';
|
|
184
|
+
case 'tool_use':
|
|
185
|
+
return '<div class="run-event run-event-tool-use"><div class="run-event-marker">\\u2699</div><div class="run-event-body"><div class="run-event-label">' + escapeHtml(toolSummary(ev.name || 'unknown', ev.input)) + ' <span class="run-event-time">' + time + '</span></div></div></div>';
|
|
186
|
+
case 'tool_result':
|
|
187
|
+
return '<div class="run-event run-event-tool-result"><div class="run-event-marker">\\u2713</div><div class="run-event-body"><div class="run-event-label">Result <span class="run-event-time">' + time + '</span></div></div></div>';
|
|
188
|
+
case 'error':
|
|
189
|
+
return '<div class="run-event run-event-error"><div class="run-event-marker">!</div><div class="run-event-body"><div class="run-event-label">Error <span class="run-event-time">' + time + '</span></div><div class="run-event-content">' + escapeHtml(truncate(ev.message || '', 300)) + '</div></div></div>';
|
|
190
|
+
case 'done':
|
|
191
|
+
return '<div class="run-event run-event-done"><div class="run-event-marker">\\u25CF</div><div class="run-event-body"><div class="run-event-label">Agent finished <span class="run-event-time">' + time + '</span></div></div></div>';
|
|
192
|
+
case 'diff_ready':
|
|
193
|
+
return '<div class="run-event run-event-done"><div class="run-event-marker">\\u25CF</div><div class="run-event-body"><div class="run-event-label">Diff ready <span class="run-event-time">' + time + '</span></div><div class="run-event-content">' + escapeHtml(ev.summary || '') + '</div></div></div>';
|
|
194
|
+
case 'agent_done_no_changes':
|
|
195
|
+
return '<div class="run-event run-event-done"><div class="run-event-marker">\\u25CF</div><div class="run-event-body"><div class="run-event-label">Done \\u2014 no file changes <span class="run-event-time">' + time + '</span></div></div></div>';
|
|
196
|
+
case 'auto_committed':
|
|
197
|
+
return '<div class="run-event run-event-done"><div class="run-event-marker">\\u2713</div><div class="run-event-body"><div class="run-event-label">Auto-committed <span class="run-event-time">' + time + '</span></div><div class="run-event-content">' + escapeHtml(ev.summary || '') + '</div></div></div>';
|
|
198
|
+
default:
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function poll() {
|
|
204
|
+
if (done) return;
|
|
205
|
+
try {
|
|
206
|
+
const resp = await fetch('/api/agent/' + runId + '/events?after=' + eventCount);
|
|
207
|
+
const data = await resp.json();
|
|
208
|
+
|
|
209
|
+
for (const ev of data.events) {
|
|
210
|
+
const html = renderEventClient(ev);
|
|
211
|
+
if (html) container.insertAdjacentHTML('beforeend', html);
|
|
212
|
+
}
|
|
213
|
+
eventCount = data.total;
|
|
214
|
+
|
|
215
|
+
if (data.done) {
|
|
216
|
+
done = true;
|
|
217
|
+
// Remove the working indicator
|
|
218
|
+
const indicator = document.querySelector('.agent-status-indicator');
|
|
219
|
+
if (indicator) indicator.remove();
|
|
220
|
+
|
|
221
|
+
if (data.diffId) {
|
|
222
|
+
doneSection.innerHTML = '<div class="agent-progress-done"><a href="/diff/' + data.diffId + '" class="btn btn-review">Review Diff</a></div>';
|
|
223
|
+
} else if (data.autoCommitted) {
|
|
224
|
+
doneSection.innerHTML = '<div class="agent-progress-done"><div class="agent-done">Done \\u2014 changes committed</div><a href="/tasks" class="btn btn-small" style="margin-top:12px">Back to Tasks</a></div>';
|
|
225
|
+
} else {
|
|
226
|
+
doneSection.innerHTML = '<div class="agent-progress-done"><div class="agent-done">Done \\u2014 no file changes</div><a href="/chat" class="btn btn-small" style="margin-top:12px">Back to Chat</a></div>';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Auto-scroll to bottom
|
|
231
|
+
container.scrollTop = container.scrollHeight;
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!done) {
|
|
236
|
+
setInterval(poll, 2000);
|
|
237
|
+
// Also scroll to bottom on load
|
|
238
|
+
container.scrollTop = container.scrollHeight;
|
|
239
|
+
}
|
|
240
|
+
</script>`;
|
|
241
|
+
return layout("Agent", "chat", body);
|
|
242
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { layout } from "./layout.js";
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str
|
|
4
|
+
.replace(/&/g, "&")
|
|
5
|
+
.replace(/</g, "<")
|
|
6
|
+
.replace(/>/g, ">")
|
|
7
|
+
.replace(/"/g, """);
|
|
8
|
+
}
|
|
9
|
+
function escapeAttr(str) {
|
|
10
|
+
return str.replace(/'/g, "\\'").replace(/"/g, """);
|
|
11
|
+
}
|
|
12
|
+
export function branchesPage(branches, currentBranch, hasPendingDiffs) {
|
|
13
|
+
const branchCards = branches.map((b) => {
|
|
14
|
+
const currentClass = b.isCurrent ? " branch-current" : "";
|
|
15
|
+
const currentLabel = b.isCurrent
|
|
16
|
+
? `<span class="branch-current-label">current</span>`
|
|
17
|
+
: "";
|
|
18
|
+
const createdFrom = b.info?.createdFrom
|
|
19
|
+
? `<div class="branch-meta">from ${escapeHtml(b.info.createdFrom)}</div>`
|
|
20
|
+
: "";
|
|
21
|
+
const taskCount = b.info?.taskIds && b.info.taskIds.length > 0
|
|
22
|
+
? `<div class="branch-meta">${b.info.taskIds.length} task${b.info.taskIds.length === 1 ? "" : "s"} completed</div>`
|
|
23
|
+
: "";
|
|
24
|
+
const isMasterOrMain = b.name === "master" || b.name === "main";
|
|
25
|
+
let actions = "";
|
|
26
|
+
if (!b.isCurrent) {
|
|
27
|
+
actions += `<button class="btn btn-small btn-branch-switch" onclick="switchBranch('${escapeAttr(b.name)}')"${hasPendingDiffs ? " disabled title=\"Resolve pending diffs first\"" : ""}>Switch</button>`;
|
|
28
|
+
}
|
|
29
|
+
if (!isMasterOrMain) {
|
|
30
|
+
actions += `<button class="btn btn-small btn-branch-push" onclick="pushBranch('${escapeAttr(b.name)}')">Push</button>`;
|
|
31
|
+
if (!b.isCurrent) {
|
|
32
|
+
actions += `<button class="btn btn-small btn-branch-merge" onclick="mergeBranch('${escapeAttr(b.name)}')">Merge to ${escapeHtml(b.info?.createdFrom || "master")}</button>`;
|
|
33
|
+
actions += `<button class="btn btn-small btn-branch-delete" onclick="deleteBranch('${escapeAttr(b.name)}')">Delete</button>`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Can merge current branch into its parent (will switch to parent first)
|
|
37
|
+
actions += `<button class="btn btn-small btn-branch-merge" onclick="mergeBranch('${escapeAttr(b.name)}')">Merge to ${escapeHtml(b.info?.createdFrom || "master")}</button>`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return `
|
|
41
|
+
<div class="branch-card${currentClass}">
|
|
42
|
+
<div class="branch-card-top">
|
|
43
|
+
<span class="branch-name">${escapeHtml(b.name)}</span>
|
|
44
|
+
${currentLabel}
|
|
45
|
+
</div>
|
|
46
|
+
${createdFrom}
|
|
47
|
+
${taskCount}
|
|
48
|
+
${actions ? `<div class="branch-actions">${actions}</div>` : ""}
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
}).join("");
|
|
52
|
+
const body = `
|
|
53
|
+
<div class="branches-container">
|
|
54
|
+
<div class="branches-header">
|
|
55
|
+
<h1>Branches</h1>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="branch-create-form" id="branch-create-form">
|
|
59
|
+
<input type="text" id="new-branch-name" class="branch-input" placeholder="feature/my-branch" />
|
|
60
|
+
<button class="btn btn-primary" onclick="createBranch()" id="create-branch-btn">New Branch</button>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
${hasPendingDiffs ? `<div class="branch-warning">Pending diffs exist — switch is blocked until you approve or reject them.</div>` : ""}
|
|
64
|
+
|
|
65
|
+
<div id="branch-message" class="branch-message" style="display:none"></div>
|
|
66
|
+
|
|
67
|
+
<div class="branch-list" id="branch-list">
|
|
68
|
+
${branchCards || `<div class="empty-state">No branches found</div>`}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<script>
|
|
73
|
+
async function createBranch() {
|
|
74
|
+
const input = document.getElementById('new-branch-name');
|
|
75
|
+
const name = input.value.trim();
|
|
76
|
+
if (!name) return;
|
|
77
|
+
const btn = document.getElementById('create-branch-btn');
|
|
78
|
+
btn.disabled = true;
|
|
79
|
+
btn.textContent = 'Creating...';
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch('/api/branches', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {'Content-Type': 'application/json'},
|
|
84
|
+
body: JSON.stringify({ name })
|
|
85
|
+
});
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
if (res.ok) {
|
|
88
|
+
location.reload();
|
|
89
|
+
} else {
|
|
90
|
+
showMessage(data.error || 'Failed to create branch', true);
|
|
91
|
+
btn.disabled = false;
|
|
92
|
+
btn.textContent = 'New Branch';
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
showMessage('Network error', true);
|
|
96
|
+
btn.disabled = false;
|
|
97
|
+
btn.textContent = 'New Branch';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function switchBranch(name) {
|
|
102
|
+
if (!confirm('Switch to branch "' + name + '"?')) return;
|
|
103
|
+
showMessage('Switching...', false);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch('/api/branches/switch', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {'Content-Type': 'application/json'},
|
|
108
|
+
body: JSON.stringify({ name })
|
|
109
|
+
});
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (res.ok) {
|
|
112
|
+
location.reload();
|
|
113
|
+
} else {
|
|
114
|
+
showMessage(data.error || 'Failed to switch branch', true);
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
showMessage('Network error', true);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function pushBranch(name) {
|
|
122
|
+
showMessage('Pushing...', false);
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch('/api/branches/' + encodeURIComponent(name) + '/push', {
|
|
125
|
+
method: 'POST'
|
|
126
|
+
});
|
|
127
|
+
const data = await res.json();
|
|
128
|
+
if (res.ok && data.success) {
|
|
129
|
+
showMessage('Pushed ' + name + ' to remote', false);
|
|
130
|
+
} else {
|
|
131
|
+
showMessage(data.error || 'Failed to push', true);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
showMessage('Network error', true);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function mergeBranch(name) {
|
|
139
|
+
if (!confirm('Merge branch "' + name + '" into its parent?')) return;
|
|
140
|
+
showMessage('Merging...', false);
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch('/api/branches/' + encodeURIComponent(name) + '/merge', {
|
|
143
|
+
method: 'POST'
|
|
144
|
+
});
|
|
145
|
+
const data = await res.json();
|
|
146
|
+
if (res.ok && data.success) {
|
|
147
|
+
location.reload();
|
|
148
|
+
} else {
|
|
149
|
+
const msg = data.conflicts
|
|
150
|
+
? 'Merge conflicts in: ' + data.conflicts.join(', ')
|
|
151
|
+
: (data.error || 'Failed to merge');
|
|
152
|
+
showMessage(msg, true);
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
showMessage('Network error', true);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function deleteBranch(name) {
|
|
160
|
+
if (!confirm('Delete branch "' + name + '"? (Must be fully merged)')) return;
|
|
161
|
+
showMessage('Deleting...', false);
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch('/api/branches/' + encodeURIComponent(name), {
|
|
164
|
+
method: 'DELETE'
|
|
165
|
+
});
|
|
166
|
+
const data = await res.json();
|
|
167
|
+
if (res.ok) {
|
|
168
|
+
location.reload();
|
|
169
|
+
} else {
|
|
170
|
+
showMessage(data.error || 'Failed to delete branch', true);
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
showMessage('Network error', true);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function showMessage(text, isError) {
|
|
178
|
+
const el = document.getElementById('branch-message');
|
|
179
|
+
el.textContent = text;
|
|
180
|
+
el.style.display = 'block';
|
|
181
|
+
el.className = 'branch-message' + (isError ? ' branch-message-error' : ' branch-message-success');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Enter key creates branch
|
|
185
|
+
document.getElementById('new-branch-name').addEventListener('keydown', function(e) {
|
|
186
|
+
if (e.key === 'Enter') createBranch();
|
|
187
|
+
});
|
|
188
|
+
</script>
|
|
189
|
+
`;
|
|
190
|
+
return layout("Branches", "tasks", body);
|
|
191
|
+
}
|