clay-server 3.2.2-beta.3 → 3.3.0-beta.2

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.
@@ -0,0 +1,456 @@
1
+ import { store } from './store.js';
2
+ import { refreshIcons, iconHtml } from './icons.js';
3
+ import { escapeHtml, showToast } from './utils.js';
4
+ import { openWorkingTreeDiff } from './filebrowser.js';
5
+ import { sendTextMessage } from './input.js';
6
+ import { resolveDefaultVendor, startNewSession } from './sidebar-sessions.js';
7
+ import { getWs } from './ws-ref.js';
8
+
9
+ var currentStatus = null;
10
+ var busy = false;
11
+ var lastError = "";
12
+ var statusBasePath = null;
13
+ var expandedFileActionsPath = null;
14
+
15
+ function apiPath(path) {
16
+ return store.get('basePath') + "api/git/" + path;
17
+ }
18
+
19
+ function requestJson(url, options) {
20
+ return fetch(url, options).then(function (response) {
21
+ return response.json().catch(function () { return {}; }).then(function (body) {
22
+ if (!response.ok || body.error) throw new Error(body.error || "Git request failed");
23
+ return body;
24
+ });
25
+ });
26
+ }
27
+
28
+ function shortOrigin(origin) {
29
+ if (!origin) return "No origin configured";
30
+ return origin
31
+ .replace(/^git@([^:]+):/, "$1/")
32
+ .replace(/^https?:\/\//, "")
33
+ .replace(/\.git$/, "");
34
+ }
35
+
36
+ function splitFilePath(filePath) {
37
+ var slash = filePath.lastIndexOf("/");
38
+ if (slash === -1) return { name: filePath, dir: "" };
39
+ return { name: filePath.slice(slash + 1), dir: filePath.slice(0, slash + 1) };
40
+ }
41
+
42
+ function fileStatusLabel(file, staged) {
43
+ if (file.conflicted) return "!";
44
+ if (file.untracked) return "U";
45
+ var code = staged ? file.code.charAt(0) : file.code.charAt(1);
46
+ return code === "." ? "M" : code;
47
+ }
48
+
49
+ function compactSessionTitle(title) {
50
+ var value = String(title || "Agent session").replace(/\s+/g, " ").trim();
51
+ if (/^Review the current uncommitted changes/i.test(value)) return "File review";
52
+ if (/^Create a Git commit for the currently staged changes/i.test(value)) return "Commit session";
53
+ return value;
54
+ }
55
+
56
+ function renderFileMeta(file, directory) {
57
+ var sessions = Array.isArray(file.sessions) ? file.sessions : [];
58
+ var first = sessions.length > 0 ? sessions[0] : null;
59
+ var html = '<div class="git-file-subline">' +
60
+ (directory ? '<span class="git-file-dir">' + escapeHtml(directory) + '</span>' : '<span class="git-file-dir">Project root</span>');
61
+ if (first) {
62
+ var title = first.title || "Agent session";
63
+ var compactTitle = compactSessionTitle(title);
64
+ var timing = first.preExisting
65
+ ? "This file already had changes when the session started"
66
+ : "This file became changed during the session";
67
+ html += '<span class="git-file-meta-separator">\u00b7</span>' +
68
+ '<button type="button" class="git-session-link" data-git-session-id="' + (first.sessionId == null ? '' : first.sessionId) + '"' +
69
+ (first.sessionId == null ? ' disabled' : '') + ' data-tip="Return to ' + escapeHtml(title) + '\n' + timing + '">' +
70
+ '<span>' + escapeHtml(compactTitle) + (sessions.length > 1 ? ' +' + (sessions.length - 1) : '') + '</span></button>';
71
+ }
72
+ var actionsOpen = expandedFileActionsPath === file.path;
73
+ html += '</div><div class="git-file-actions' + (actionsOpen ? ' open' : '') + '" aria-hidden="' + (actionsOpen ? 'false' : 'true') + '">' +
74
+ '<button type="button" data-git-open-diff="' + encodeURIComponent(file.path) + '" class="git-file-text-action">' +
75
+ iconHtml("file-diff") + '<span>Open diff</span></button>';
76
+ if (first) {
77
+ html += '<button type="button" data-git-session-key="' + encodeURIComponent(first.key) + '" data-git-path="' + encodeURIComponent(file.path) + '" class="git-file-text-action git-session-compare">' +
78
+ iconHtml("history") + '<span>Compare to start</span></button>';
79
+ }
80
+ html += '<button type="button" data-git-agent-review="' + encodeURIComponent(file.path) + '" class="git-file-text-action' + (first ? ' git-file-text-action-wide' : '') + '">' +
81
+ iconHtml("sparkles") + '<span>Review with Agent</span></button></div>';
82
+ return html;
83
+ }
84
+
85
+ function renderFileRow(file, staged) {
86
+ var parts = splitFilePath(file.path);
87
+ var pathKey = encodeURIComponent(file.path);
88
+ var statusClass = file.conflicted ? " conflicted" : (file.untracked ? " untracked" : "");
89
+ var action = staged ? "unstage" : "stage";
90
+ var actionTitle = staged ? "Unstage file — keep its changes" : "Stage file for commit";
91
+ var actionIcon = staged ? "minus" : "plus";
92
+ var actionsOpen = expandedFileActionsPath === file.path;
93
+ return '<div class="git-file-row' + (actionsOpen ? ' active' : '') + '">' +
94
+ '<span class="git-file-status' + statusClass + '">' + escapeHtml(fileStatusLabel(file, staged)) + '</span>' +
95
+ '<div class="git-file-main" data-git-file-item="' + pathKey + '" tabindex="0" aria-label="File actions for ' + escapeHtml(parts.name) + '" aria-expanded="' + (actionsOpen ? 'true' : 'false') + '">' +
96
+ '<div class="git-file-name">' + escapeHtml(parts.name) + '</div>' +
97
+ renderFileMeta(file, parts.dir) +
98
+ '</div>' +
99
+ '<button type="button" class="git-icon-btn git-file-action" data-git-action="' + action + '" data-git-path="' + pathKey + '" data-tip="' + actionTitle + '" aria-label="' + actionTitle + '"' + (busy ? ' disabled' : '') + '>' +
100
+ iconHtml(actionIcon) +
101
+ '</button>' +
102
+ '</div>';
103
+ }
104
+
105
+ function renderFileSection(title, files, staged, onlySection) {
106
+ if (files.length === 0) return "";
107
+ var bulkAction = staged ? "unstage_all" : "stage_all";
108
+ var bulkLabel = staged ? "Unstage all" : "Stage all";
109
+ var sectionClass = staged ? " git-section-staged" : " git-section-changes";
110
+ if (onlySection) sectionClass += " git-section-only";
111
+ var html = '<section class="git-section' + sectionClass + '">' +
112
+ '<div class="git-section-header"><span>' + title + '</span><span class="git-section-count">' + files.length + '</span>' +
113
+ '<button type="button" class="git-action-btn" data-git-action="' + bulkAction + '"' + (busy ? ' disabled' : '') + '>' + bulkLabel + '</button>' +
114
+ '</div><div class="git-file-list">';
115
+ for (var i = 0; i < files.length; i++) html += renderFileRow(files[i], staged);
116
+ return html + '</div></section>';
117
+ }
118
+
119
+ function renderRepository(status) {
120
+ var staged = [];
121
+ var unstaged = [];
122
+ for (var i = 0; i < status.files.length; i++) {
123
+ if (status.files[i].staged) staged.push(status.files[i]);
124
+ if (status.files[i].unstaged || status.files[i].untracked) unstaged.push(status.files[i]);
125
+ }
126
+ var branchLabel = status.detached ? "Detached at " + String(status.oid || "").slice(0, 8) : (status.branch || "No commits yet");
127
+ var badges = "";
128
+ if (status.isWorktree) badges += '<span class="git-badge">Linked worktree</span>';
129
+ else if (status.worktrees && status.worktrees.length > 1) badges += '<span class="git-badge">Main checkout</span>';
130
+ if (status.detached) badges += '<span class="git-badge detached">Detached HEAD</span>';
131
+
132
+ var pullDisabled = busy || !status.upstream;
133
+ var pushDisabled = busy || !status.origin || status.detached;
134
+ var html = lastError ? '<div class="git-panel-error">' + escapeHtml(lastError) + '</div>' : '';
135
+ html += '<div class="git-repo-card">' +
136
+ '<div class="git-repo-primary">' + iconHtml("git-branch") +
137
+ '<div style="min-width:0"><div class="git-repo-name">' + escapeHtml(status.name || "Repository") + '</div>' +
138
+ '<div class="git-branch-name">' + escapeHtml(branchLabel) + '</div></div>' +
139
+ '</div>' +
140
+ (badges ? '<div class="git-badges">' + badges + '</div>' : '') +
141
+ '<div class="git-repo-path">' + iconHtml("folder") + '<span>' + escapeHtml(status.root || "") + '</span></div>' +
142
+ (status.isWorktree ? '<div class="git-repo-path">' + iconHtml("trees") + '<span>Main: ' + escapeHtml(status.mainWorktree || "") + '</span></div>' : '') +
143
+ '<div class="git-origin">' + iconHtml("cloud") + '<span>' + escapeHtml(shortOrigin(status.origin)) + '</span></div>' +
144
+ '</div>' +
145
+ '<div class="git-sync-row">' +
146
+ '<button type="button" class="git-action-btn" data-git-action="pull"' + (pullDisabled ? ' disabled' : '') + '>' + iconHtml("download") + ' Pull' +
147
+ (status.behind ? '<span class="git-sync-count">' + status.behind + '</span>' : '') + '</button>' +
148
+ '<button type="button" class="git-action-btn" data-git-action="push"' + (pushDisabled ? ' disabled' : '') + '>' + iconHtml("upload") + ' Push' +
149
+ (status.ahead ? '<span class="git-sync-count">' + status.ahead + '</span>' : '') + '</button>' +
150
+ '</div>';
151
+
152
+ if (status.files.length === 0) {
153
+ html += '<div class="git-panel-empty" style="min-height:80px">' + iconHtml("check-circle-2") + '<span>Working tree clean</span></div>';
154
+ } else {
155
+ html += '<button type="button" class="git-review-all" data-git-review-all>' +
156
+ '<span>' + iconHtml("scan-search") + '<strong>Review all changes</strong></span>' +
157
+ '<span class="git-review-count">' + status.files.length + ' files ' + iconHtml("chevron-right") + '</span>' +
158
+ '</button>';
159
+ html += renderFileSection("Staged changes", staged, true, unstaged.length === 0);
160
+ html += renderFileSection("Changes", unstaged, false, staged.length === 0);
161
+ }
162
+
163
+ if (staged.length > 0) {
164
+ html += '<div class="git-agent-commit">' +
165
+ '<div class="git-agent-commit-copy"><span class="git-agent-commit-icon">' + iconHtml("sparkles") + '</span>' +
166
+ '<div><strong>Commit with Agent</strong><span>Review and commit exactly ' + staged.length + ' staged file' + (staged.length === 1 ? '' : 's') + '.</span></div></div>' +
167
+ '<button type="button" class="git-action-btn git-agent-commit-btn" data-git-agent-commit' + (busy ? ' disabled' : '') + '>' +
168
+ iconHtml("message-square-code") + ' Commit ' + staged.length + ' file' + (staged.length === 1 ? '' : 's') + '</button>' +
169
+ '</div>';
170
+ } else if (status.files.length > 0) {
171
+ html += '<div class="git-agent-commit git-next-action">' +
172
+ '<button type="button" class="git-action-btn git-agent-commit-btn" data-git-review-all>' +
173
+ iconHtml("scan-search") + ' Review ' + status.files.length + ' changed file' + (status.files.length === 1 ? '' : 's') + '</button>' +
174
+ '</div>';
175
+ } else if (status.ahead > 0) {
176
+ html += '<div class="git-agent-commit git-next-action"><button type="button" class="git-action-btn git-agent-commit-btn" data-git-action="push">' +
177
+ iconHtml("upload") + ' Push ' + status.ahead + ' commit' + (status.ahead === 1 ? '' : 's') + '</button></div>';
178
+ }
179
+ return html;
180
+ }
181
+
182
+ function render() {
183
+ var body = document.getElementById("git-panel-body");
184
+ if (!body) return;
185
+ var previousChangesList = body.querySelector(".git-section-changes .git-file-list");
186
+ var previousStagedList = body.querySelector(".git-section-staged .git-file-list");
187
+ var changesScrollTop = previousChangesList ? previousChangesList.scrollTop : 0;
188
+ var stagedScrollTop = previousStagedList ? previousStagedList.scrollTop : 0;
189
+ if (!currentStatus) {
190
+ body.innerHTML = '<div class="git-panel-loading"><span class="git-panel-spinner"></span> Reading repository</div>';
191
+ } else if (currentStatus.loadError) {
192
+ body.innerHTML = '<div class="git-panel-empty">' + iconHtml("circle-alert") + '<span>' + escapeHtml(currentStatus.loadError) + '</span></div>';
193
+ } else if (!currentStatus.isRepository) {
194
+ body.innerHTML = '<div class="git-panel-empty">' + iconHtml("folder-git-2") + '<span>This project is not a Git repository.</span></div>';
195
+ } else {
196
+ body.innerHTML = renderRepository(currentStatus);
197
+ }
198
+ refreshIcons(body);
199
+ var changesList = body.querySelector(".git-section-changes .git-file-list");
200
+ var stagedList = body.querySelector(".git-section-staged .git-file-list");
201
+ if (changesList) changesList.scrollTop = changesScrollTop;
202
+ if (stagedList) stagedList.scrollTop = stagedScrollTop;
203
+ updateBadge();
204
+ }
205
+
206
+ function updateBadge() {
207
+ var badge = document.getElementById("git-sidebar-count");
208
+ if (!badge) return;
209
+ if (!currentStatus || !currentStatus.isRepository) {
210
+ badge.classList.add("hidden");
211
+ return;
212
+ }
213
+ var count = currentStatus.files.length;
214
+ badge.textContent = count > 99 ? "99+" : String(count);
215
+ badge.classList.toggle("hidden", count === 0);
216
+ }
217
+
218
+ export function refreshGitStatus() {
219
+ if (busy) return Promise.resolve(currentStatus);
220
+ var basePath = store.get('basePath');
221
+ if (statusBasePath !== basePath) {
222
+ statusBasePath = basePath;
223
+ currentStatus = null;
224
+ lastError = "";
225
+ expandedFileActionsPath = null;
226
+ render();
227
+ }
228
+ var refreshBtn = document.getElementById("git-panel-refresh");
229
+ if (refreshBtn) refreshBtn.classList.add("spinning");
230
+ return requestJson(basePath + "api/git/status", { cache: "no-store" }).then(function (status) {
231
+ if (statusBasePath !== basePath) return status;
232
+ currentStatus = status;
233
+ if (expandedFileActionsPath && !status.files.some(function (file) { return file.path === expandedFileActionsPath; })) {
234
+ expandedFileActionsPath = null;
235
+ }
236
+ lastError = "";
237
+ render();
238
+ return status;
239
+ }).catch(function (err) {
240
+ if (statusBasePath !== basePath) return;
241
+ lastError = err.message;
242
+ if (!currentStatus) currentStatus = { isRepository: false, loadError: err.message };
243
+ render();
244
+ }).finally(function () {
245
+ if (refreshBtn) refreshBtn.classList.remove("spinning");
246
+ });
247
+ }
248
+
249
+ function actionLabel(action) {
250
+ if (action === "stage" || action === "stage_all") return "Staged changes";
251
+ if (action === "unstage" || action === "unstage_all") return "Unstaged changes";
252
+ if (action === "pull") return "Pulled changes";
253
+ if (action === "push") return "Pushed changes";
254
+ return "Git action completed";
255
+ }
256
+
257
+ function runAction(action, paths) {
258
+ if (busy) return;
259
+ var payload = { action: action };
260
+ if (paths) payload.paths = paths;
261
+ busy = true;
262
+ lastError = "";
263
+ render();
264
+ requestJson(apiPath("action"), {
265
+ method: "POST",
266
+ headers: { "Content-Type": "application/json" },
267
+ body: JSON.stringify(payload),
268
+ }).then(function (result) {
269
+ currentStatus = result.status;
270
+ showToast(actionLabel(action), null, result.output || "");
271
+ }).catch(function (err) {
272
+ lastError = err.message;
273
+ showToast("Git action failed", "warn", err.message);
274
+ }).finally(function () {
275
+ busy = false;
276
+ render();
277
+ });
278
+ }
279
+
280
+ function openFileDiff(filePath, reviewIndex) {
281
+ requestJson(apiPath("file-diff?path=" + encodeURIComponent(filePath)), { cache: "no-store" }).then(function (result) {
282
+ var paths = currentStatus ? currentStatus.files.map(function (file) { return file.path; }) : [filePath];
283
+ var index = typeof reviewIndex === "number" ? reviewIndex : paths.indexOf(filePath);
284
+ if (index < 0) index = 0;
285
+ result.review = {
286
+ index: index,
287
+ total: paths.length,
288
+ previous: index > 0 ? function () { openFileDiff(paths[index - 1], index - 1); } : null,
289
+ next: index + 1 < paths.length ? function () { openFileDiff(paths[index + 1], index + 1); } : null,
290
+ askAgent: function () { startAgentFileReview(filePath); },
291
+ };
292
+ openWorkingTreeDiff(result);
293
+ }).catch(function (err) {
294
+ showToast("Unable to open Git diff", "warn", err.message);
295
+ });
296
+ }
297
+
298
+ function openSessionDiff(sessionKey, filePath) {
299
+ var url = apiPath("session-diff?session=" + encodeURIComponent(sessionKey) + "&path=" + encodeURIComponent(filePath));
300
+ requestJson(url, { cache: "no-store" }).then(function (result) {
301
+ openWorkingTreeDiff(result);
302
+ showToast("Compared with start of " + (result.sessionTitle || "session"));
303
+ }).catch(function (err) {
304
+ showToast("Unable to compare session changes", "warn", err.message);
305
+ });
306
+ }
307
+
308
+ function returnToSession(sessionId) {
309
+ var ws = getWs();
310
+ if (!ws || ws.readyState !== WebSocket.OPEN || !sessionId) return;
311
+ ws.send(JSON.stringify({ type: "switch_session", id: sessionId }));
312
+ var gitBtn = document.getElementById("git-sidebar-btn");
313
+ var gitPanel = document.getElementById("sidebar-panel-git");
314
+ if (gitBtn && gitPanel && !gitPanel.classList.contains("hidden")) gitBtn.click();
315
+ }
316
+
317
+ function startFocusedAgentSession(prompt, successMessage, sessionTitle) {
318
+ if (!store.get('connected')) {
319
+ showToast("Not connected — agent session was not started.", "warn");
320
+ return;
321
+ }
322
+ var previousSessionId = store.get('activeSessionId');
323
+ var unsubscribe = null;
324
+ var sessionReady = false;
325
+ var timer = setTimeout(function () {
326
+ if (unsubscribe) unsubscribe();
327
+ showToast("Agent session could not be started", "warn");
328
+ }, 10000);
329
+ unsubscribe = store.subscribe(function (state) {
330
+ if (sessionReady || !state.activeSessionId || state.activeSessionId === previousSessionId) return;
331
+ sessionReady = true;
332
+ clearTimeout(timer);
333
+ setTimeout(function () {
334
+ if (unsubscribe) unsubscribe();
335
+ unsubscribe = null;
336
+ var newSessionId = store.get('activeSessionId');
337
+ var ws = getWs();
338
+ if (sessionTitle && ws && ws.readyState === WebSocket.OPEN && newSessionId) {
339
+ ws.send(JSON.stringify({ type: "rename_session", id: newSessionId, title: sessionTitle }));
340
+ }
341
+ var gitBtn = document.getElementById("git-sidebar-btn");
342
+ var gitPanel = document.getElementById("sidebar-panel-git");
343
+ if (gitBtn && gitPanel && !gitPanel.classList.contains("hidden")) gitBtn.click();
344
+ if (sendTextMessage(prompt)) showToast(successMessage || "Agent session started");
345
+ }, 0);
346
+ });
347
+ startNewSession(resolveDefaultVendor(), { mode: "gui", forceNew: true });
348
+ }
349
+
350
+ function startAgentCommitSession() {
351
+ if (busy || !currentStatus) return;
352
+ var hasStaged = currentStatus.files.some(function (file) { return file.staged; });
353
+ if (!hasStaged) return;
354
+ var prompt = "Create a Git commit for the currently staged changes.\n\n" +
355
+ "Inspect `git diff --cached` and the repository instructions before committing. " +
356
+ "Commit exactly the staged changes; do not stage additional files. " +
357
+ "Write a concise Angular Commit Convention message, use the angular-commit skill, " +
358
+ "and do not add Co-Authored-By lines. If nothing is staged, explain that and stop.";
359
+ startFocusedAgentSession(prompt, "Commit session started", "Commit staged changes");
360
+ }
361
+
362
+ function startAgentFileReview(filePath) {
363
+ var prompt = "Review the current uncommitted changes in `" + filePath + "`.\n\n" +
364
+ "Inspect the Git diff and relevant repository context. Explain the intent of the change, " +
365
+ "identify correctness or regression risks, and suggest specific improvements. " +
366
+ "Do not edit files or run Git actions unless I ask in this session.";
367
+ var parts = splitFilePath(filePath);
368
+ startFocusedAgentSession(prompt, "File review session started", "Review \u00b7 " + parts.name);
369
+ }
370
+
371
+ function toggleFileActions(fileMain) {
372
+ if (!fileMain) return;
373
+ var filePath = decodeURIComponent(fileMain.dataset.gitFileItem);
374
+ var fileActions = fileMain.querySelector(".git-file-actions");
375
+ var wasOpen = fileActions && fileActions.classList.contains("open");
376
+ var openActions = document.querySelectorAll("#git-panel-body .git-file-actions.open");
377
+ for (var openIndex = 0; openIndex < openActions.length; openIndex++) {
378
+ openActions[openIndex].classList.remove("open");
379
+ openActions[openIndex].setAttribute("aria-hidden", "true");
380
+ var openMain = openActions[openIndex].closest(".git-file-main");
381
+ var openRow = openActions[openIndex].closest(".git-file-row");
382
+ if (openMain) openMain.setAttribute("aria-expanded", "false");
383
+ if (openRow) openRow.classList.remove("active");
384
+ }
385
+ if (fileActions && !wasOpen) {
386
+ expandedFileActionsPath = filePath;
387
+ fileActions.classList.add("open");
388
+ fileActions.setAttribute("aria-hidden", "false");
389
+ fileMain.setAttribute("aria-expanded", "true");
390
+ var fileRow = fileMain.closest(".git-file-row");
391
+ if (fileRow) fileRow.classList.add("active");
392
+ } else {
393
+ expandedFileActionsPath = null;
394
+ }
395
+ }
396
+
397
+ export function initGitPanel() {
398
+ var body = document.getElementById("git-panel-body");
399
+ var refreshBtn = document.getElementById("git-panel-refresh");
400
+ if (!body || !refreshBtn) return;
401
+ refreshBtn.addEventListener("click", function () { refreshGitStatus(); });
402
+ body.addEventListener("click", function (event) {
403
+ var openDiff = event.target.closest("[data-git-open-diff]");
404
+ if (openDiff) {
405
+ openFileDiff(decodeURIComponent(openDiff.dataset.gitOpenDiff));
406
+ return;
407
+ }
408
+ var agentReview = event.target.closest("[data-git-agent-review]");
409
+ if (agentReview) {
410
+ startAgentFileReview(decodeURIComponent(agentReview.dataset.gitAgentReview));
411
+ return;
412
+ }
413
+ var reviewAll = event.target.closest("[data-git-review-all]");
414
+ if (reviewAll) {
415
+ if (currentStatus && currentStatus.files.length > 0) openFileDiff(currentStatus.files[0].path, 0);
416
+ return;
417
+ }
418
+ var sessionCompare = event.target.closest(".git-session-compare");
419
+ if (sessionCompare) {
420
+ openSessionDiff(decodeURIComponent(sessionCompare.dataset.gitSessionKey), decodeURIComponent(sessionCompare.dataset.gitPath));
421
+ return;
422
+ }
423
+ var sessionLink = event.target.closest(".git-session-link");
424
+ if (sessionLink) {
425
+ if (!sessionLink.disabled) returnToSession(parseInt(sessionLink.dataset.gitSessionId, 10));
426
+ return;
427
+ }
428
+ var agentCommitButton = event.target.closest("[data-git-agent-commit]");
429
+ if (agentCommitButton) {
430
+ if (!agentCommitButton.disabled) startAgentCommitSession();
431
+ return;
432
+ }
433
+ var actionButton = event.target.closest("[data-git-action]");
434
+ if (actionButton) {
435
+ if (!actionButton.disabled) {
436
+ var action = actionButton.dataset.gitAction;
437
+ var filePath = actionButton.dataset.gitPath ? decodeURIComponent(actionButton.dataset.gitPath) : null;
438
+ runAction(action, filePath ? [filePath] : null);
439
+ }
440
+ return;
441
+ }
442
+ var fileRow = event.target.closest(".git-file-row");
443
+ var fileItem = fileRow ? fileRow.querySelector("[data-git-file-item]") : null;
444
+ if (fileItem) toggleFileActions(fileItem);
445
+ });
446
+ body.addEventListener("keydown", function (event) {
447
+ var fileItem = event.target.closest("[data-git-file-item]");
448
+ if (!fileItem || event.target !== fileItem || (event.key !== "Enter" && event.key !== " ")) return;
449
+ event.preventDefault();
450
+ toggleFileActions(fileItem);
451
+ });
452
+ setInterval(function () {
453
+ var panel = document.getElementById("sidebar-panel-git");
454
+ if (panel && !panel.classList.contains("hidden") && store.get('connected') && !busy) refreshGitStatus();
455
+ }, 2500);
456
+ }
@@ -346,6 +346,14 @@ export function sendMessage() {
346
346
  }
347
347
  }
348
348
 
349
+ export function sendTextMessage(text) {
350
+ if (!ctx || !ctx.inputEl || typeof text !== "string" || !text.trim()) return false;
351
+ clearPendingImages();
352
+ ctx.inputEl.value = text;
353
+ sendMessage();
354
+ return true;
355
+ }
356
+
349
357
  export function autoResize() {
350
358
  ctx.inputEl.style.height = "auto";
351
359
  ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";