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,317 @@
1
+ var crypto = require("crypto");
2
+ var yoke = require("./yoke");
3
+ var buildShape = require("./session-spawn-mcp-server").buildShape;
4
+
5
+ var MAX_SUMMARY_CHARS = 600;
6
+ var MAX_PLAN_CHARS = 6000;
7
+ var MAX_TASK_CHARS = 30000;
8
+
9
+ function modelValue(entry) {
10
+ if (typeof entry === "string") return entry;
11
+ return entry && (entry.value || entry.id) || "";
12
+ }
13
+
14
+ function modelLabel(entry) {
15
+ if (typeof entry === "string") return entry;
16
+ return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
17
+ }
18
+
19
+ function isFableSession(session, modelsByVendor, fallbackModel) {
20
+ if (!session || (session.vendor || "claude") !== "claude") return false;
21
+ var effectiveModel = session.model || fallbackModel || "";
22
+ var selected = String(effectiveModel).toLowerCase();
23
+ if (selected.indexOf("fable") !== -1) return true;
24
+ var models = (modelsByVendor && modelsByVendor.claude) || [];
25
+ for (var i = 0; i < models.length; i++) {
26
+ if (modelValue(models[i]) !== effectiveModel) continue;
27
+ if (modelLabel(models[i]).toLowerCase().indexOf("fable") !== -1) return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ function toolResult(value) {
33
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
34
+ }
35
+
36
+ function attachWorkerProposal(ctx) {
37
+ var sm = ctx.sm;
38
+ var store = ctx.splitStore;
39
+
40
+ function isEligible(session) {
41
+ if (ctx.isMate || !session || session.mode === "tui") return false;
42
+ if (store.groupForMember(session.localId)) return false;
43
+ return isFableSession(session, sm.modelsByVendor, sm.currentModel);
44
+ }
45
+
46
+ function safeModelsByVendor(installed) {
47
+ var result = {};
48
+ for (var i = 0; i < installed.length; i++) {
49
+ var vendor = installed[i];
50
+ var source = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
51
+ result[vendor] = source.map(function (entry) {
52
+ if (typeof entry === "string") return entry;
53
+ return {
54
+ value: modelValue(entry),
55
+ displayName: modelLabel(entry),
56
+ supportedEffortLevels: entry.supportedEffortLevels || null,
57
+ };
58
+ });
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function proposalOptions() {
64
+ var installed = (sm.installedVendors || []).slice();
65
+ var capabilities = {};
66
+ for (var i = 0; i < installed.length; i++) {
67
+ var source = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[installed[i]]) || {};
68
+ var vendorInfo = yoke.getVendorInfo(installed[i]) || {};
69
+ var supportsEffort = Array.isArray(vendorInfo.effortLevels) && vendorInfo.effortLevels.length > 0;
70
+ capabilities[installed[i]] = { effort: source.effort === undefined ? supportsEffort : source.effort !== false };
71
+ }
72
+ return {
73
+ installedVendors: installed,
74
+ modelsByVendor: safeModelsByVendor(installed),
75
+ capabilitiesByVendor: capabilities,
76
+ };
77
+ }
78
+
79
+ async function ensureModelCatalogs() {
80
+ var installed = sm.installedVendors || [];
81
+ sm.modelsByVendor = sm.modelsByVendor || {};
82
+ for (var i = 0; i < installed.length; i++) {
83
+ var vendor = installed[i];
84
+ if (sm.modelsByVendor[vendor] && sm.modelsByVendor[vendor].length > 0) continue;
85
+ var adapter = ctx.adapters && ctx.adapters[vendor];
86
+ if (!adapter || typeof adapter.supportedModels !== "function") continue;
87
+ try { sm.modelsByVendor[vendor] = await adapter.supportedModels(); }
88
+ catch (err) { console.warn("[worker-proposal] Could not load " + vendor + " models:", err.message || err); }
89
+ }
90
+ }
91
+
92
+ function modelIsAvailable(options, vendor, model) {
93
+ if (!model) return true;
94
+ var models = options.modelsByVendor[vendor] || [];
95
+ for (var i = 0; i < models.length; i++) {
96
+ if (modelValue(models[i]) === model) return true;
97
+ }
98
+ return models.length === 0;
99
+ }
100
+
101
+ function chooseRecommendation(args, session, options) {
102
+ var installed = options.installedVendors;
103
+ var vendor = args.recommendedVendor;
104
+ if (installed.indexOf(vendor) === -1) {
105
+ if (installed.indexOf("codex") !== -1) vendor = "codex";
106
+ else {
107
+ vendor = installed[0] || session.vendor || "claude";
108
+ for (var i = 0; i < installed.length; i++) {
109
+ if (installed[i] !== session.vendor) { vendor = installed[i]; break; }
110
+ }
111
+ }
112
+ }
113
+ var models = options.modelsByVendor[vendor] || [];
114
+ var model = modelIsAvailable(options, vendor, args.recommendedModel) ? (args.recommendedModel || "") : "";
115
+ var currentModel = session.model || sm.currentModel || "";
116
+ if (vendor === session.vendor && model === currentModel) model = "";
117
+ if (!model && models.length > 0) {
118
+ for (var j = 0; j < models.length; j++) {
119
+ if (vendor === session.vendor && modelValue(models[j]) === currentModel) continue;
120
+ if (modelLabel(models[j]).toLowerCase().indexOf("fable") !== -1) continue;
121
+ model = modelValue(models[j]);
122
+ break;
123
+ }
124
+ }
125
+ if (!model && models.length > 0 && vendor !== session.vendor) model = modelValue(models[0]);
126
+ var effort = yoke.clampEffort(vendor, args.recommendedEffort || "medium") || "";
127
+ return { vendor: vendor, model: model, effort: effort };
128
+ }
129
+
130
+ function findProposal(session, proposalId) {
131
+ var history = (session && session.history) || [];
132
+ for (var i = history.length - 1; i >= 0; i--) {
133
+ if (history[i] && history[i].type === "worker_proposal" && history[i].proposalId === proposalId) return history[i];
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function hasPendingProposal(session) {
139
+ var history = (session && session.history) || [];
140
+ for (var i = history.length - 1; i >= 0; i--) {
141
+ var item = history[i];
142
+ if (!item || item.type !== "worker_proposal") continue;
143
+ return item.status === "pending" || item.status === "starting" || item.status === "running";
144
+ }
145
+ return false;
146
+ }
147
+
148
+ function updateProposal(session, proposal, patch) {
149
+ Object.assign(proposal, patch, { updatedAt: Date.now() });
150
+ sm.saveSessionFile(session);
151
+ sm.sendToSession(session, Object.assign({
152
+ type: "worker_proposal_update",
153
+ proposalId: proposal.proposalId,
154
+ }, patch));
155
+ }
156
+
157
+ async function propose(args, session) {
158
+ if (!isEligible(session)) return toolResult({ error: "Worker suggestions are only available in an unpaired Fable session." });
159
+ if (hasPendingProposal(session)) return toolResult({ error: "A Worker suggestion is already awaiting a decision." });
160
+ var summary = typeof args.summary === "string" ? args.summary.trim() : "";
161
+ var plan = typeof args.plan === "string" ? args.plan.trim() : "";
162
+ var task = typeof args.message === "string" ? args.message.trim() : "";
163
+ if (!summary || !plan || !task) return toolResult({ error: "summary, plan, and message are required." });
164
+ if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Worker task is too long." });
165
+ await ensureModelCatalogs();
166
+ var options = proposalOptions();
167
+ if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Worker session." });
168
+ var recommendation = chooseRecommendation(args, session, options);
169
+ var proposal = {
170
+ type: "worker_proposal",
171
+ proposalId: "worker_" + crypto.randomUUID(),
172
+ summary: summary.slice(0, MAX_SUMMARY_CHARS),
173
+ plan: plan.slice(0, MAX_PLAN_CHARS),
174
+ message: task,
175
+ status: "pending",
176
+ recommendedVendor: recommendation.vendor,
177
+ recommendedModel: recommendation.model,
178
+ recommendedEffort: recommendation.effort,
179
+ options: options,
180
+ };
181
+ sm.sendAndRecord(session, proposal);
182
+ return toolResult({
183
+ status: "posted",
184
+ proposalId: proposal.proposalId,
185
+ instruction: "The Worker suggestion is visible in the chat. End this turn now and wait for the user's decision.",
186
+ });
187
+ }
188
+
189
+ function resumeDriver(session, text) {
190
+ var sdk = ctx.getSdk();
191
+ if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
192
+ var message = { type: "user_message", text: text, _internal: true };
193
+ sm.sendAndRecord(session, message);
194
+ session.sentToolResults = {};
195
+ if (!session.isProcessing) {
196
+ session.isProcessing = true;
197
+ ctx.onProcessingChanged();
198
+ sm.sendToSession(session, { type: "status", status: "processing" });
199
+ }
200
+ if (sdk.pushMessage(session, text)) return Promise.resolve();
201
+ return Promise.resolve(sdk.startQuery(session, text, undefined, ctx.getLinuxUserForSession(session)));
202
+ }
203
+
204
+ function parsePartnerResult(result) {
205
+ if (!result || result.isError || !result.content || !result.content[0]) {
206
+ return { status: "error", error: "Worker execution failed." };
207
+ }
208
+ try { return JSON.parse(result.content[0].text); }
209
+ catch (e) { return { status: "error", error: result.content[0].text || "Worker execution failed." }; }
210
+ }
211
+
212
+ async function runWorker(session, proposal) {
213
+ var result = parsePartnerResult(await ctx.sendToPartner({ message: proposal.message, wait: true, timeoutSeconds: 900 }, session));
214
+ var status = result.status === "complete" ? "completed" : result.status;
215
+ updateProposal(session, proposal, {
216
+ status: status || "error",
217
+ resultPreview: result.response ? result.response.slice(0, 1200) : "",
218
+ error: result.error || null,
219
+ });
220
+ var followup;
221
+ if (result.status === "complete") {
222
+ followup = "[Worker execution completed]\nReview the Worker's result, verify the implementation, and integrate any remaining changes.\n\n" + (result.response || "The Worker completed without a text summary.");
223
+ } else if (result.status === "running") {
224
+ followup = "[Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
225
+ } else {
226
+ followup = "[Worker execution failed]\nInspect the failure and decide whether to fix it here or delegate a narrower follow-up.\n\n" + (result.error || result.response || "Unknown Worker error.");
227
+ }
228
+ await resumeDriver(session, followup);
229
+ }
230
+
231
+ function sessionForResponse(ws) {
232
+ var session = sm.sessions.get(ws._clayActiveSession);
233
+ if (!session) throw new Error("active Driver session not found");
234
+ if (ctx.usersModule.isMultiUser()) {
235
+ var userId = ws._clayUser && ws._clayUser.id;
236
+ if (!userId || session.ownerId !== userId) throw new Error("Driver session access denied");
237
+ }
238
+ return session;
239
+ }
240
+
241
+ async function respondToProposal(ws, msg) {
242
+ var session = sessionForResponse(ws);
243
+ var proposal = findProposal(session, msg.proposalId);
244
+ if (!proposal) throw new Error("Worker suggestion not found");
245
+ if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
246
+ if (!msg.accepted) {
247
+ updateProposal(session, proposal, { status: "declined" });
248
+ await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
249
+ return { ok: true, status: "declined" };
250
+ }
251
+ var options = proposal.options || proposalOptions();
252
+ var vendor = msg.vendor || proposal.recommendedVendor;
253
+ var model = msg.model || "";
254
+ if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Worker vendor is not installed");
255
+ if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Worker model is unavailable");
256
+ var effort = yoke.clampEffort(vendor, msg.effort || proposal.recommendedEffort || "medium") || "";
257
+ updateProposal(session, proposal, { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort });
258
+ try {
259
+ var created = ctx.createPairRecord(ws, {
260
+ driver: { sessionId: session.localId },
261
+ worker: { vendor: vendor, model: model, effort: effort },
262
+ });
263
+ updateProposal(session, proposal, { status: "running", groupId: created.group.id, workerId: created.worker.localId });
264
+ ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
265
+ runWorker(session, proposal).catch(function (err) {
266
+ updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
267
+ resumeDriver(session, "[Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
268
+ });
269
+ return { ok: true, status: "running", group: created.group };
270
+ } catch (err) {
271
+ updateProposal(session, proposal, { status: "pending", error: err.message || String(err) });
272
+ return { ok: false, status: "pending", error: err.message || String(err) };
273
+ }
274
+ }
275
+
276
+ function handleMessage(ws, msg) {
277
+ if (msg.type !== "worker_proposal_response") return false;
278
+ respondToProposal(ws, msg).catch(function (err) {
279
+ ctx.sendTo(ws, { type: "worker_proposal_update", proposalId: msg.proposalId, status: "pending", error: err.message || String(err) });
280
+ });
281
+ return true;
282
+ }
283
+
284
+ function getToolDefs(session) {
285
+ if (!isEligible(session)) return [];
286
+ return [{
287
+ name: "propose_worker",
288
+ description: "After making a concise plan for an implementation-heavy task, suggest that the user run the execution with another model in a visible Worker session. Use this only before substantial execution begins, never for small edits, explanations, or tasks the user asked you to keep in this session.",
289
+ inputSchema: buildShape({
290
+ summary: { type: "string", description: "One short sentence explaining why a Worker is useful for this task." },
291
+ plan: { type: "string", description: "A concise numbered implementation plan to show in the approval card." },
292
+ message: { type: "string", description: "Complete execution instructions for the Worker, including scope, constraints, validation, and expected result." },
293
+ recommendedVendor: { type: "string", description: "Optional installed vendor id for the Worker." },
294
+ recommendedModel: { type: "string", description: "Optional exact Worker model id. Omit when uncertain." },
295
+ recommendedEffort: { type: "string", description: "Optional reasoning effort: minimal, low, medium, high, xhigh, or max." },
296
+ }, ["summary", "plan", "message"]),
297
+ handler: function (args) { return propose(args || {}, session); },
298
+ }];
299
+ }
300
+
301
+ function getSystemPrompt(session) {
302
+ if (!isEligible(session)) return "";
303
+ return "You can introduce Clay's Driver/Worker workflow with propose_worker. For an implementation-heavy task that is likely to consume substantial execution time, first reason about the work and form a concise plan. Before editing files or running substantial execution, call propose_worker once with that plan and a complete Worker instruction, recommending another suitable execution model. Use it conservatively: do not suggest a Worker for questions, investigations, tiny fixes, or when the user explicitly wants you to execute here. After posting a proposal, end the turn and wait. If accepted, the Worker runs visibly and its result returns to you for review.";
304
+ }
305
+
306
+ return {
307
+ getToolDefs: getToolDefs,
308
+ getSystemPrompt: getSystemPrompt,
309
+ handleMessage: handleMessage,
310
+ respondToProposal: respondToProposal,
311
+ };
312
+ }
313
+
314
+ module.exports = {
315
+ attachWorkerProposal: attachWorkerProposal,
316
+ isFableSession: isFableSession,
317
+ };
package/lib/project.js CHANGED
@@ -3,6 +3,7 @@ var path = require("path");
3
3
  var os = require("os");
4
4
  var crypto = require("crypto");
5
5
  var { createSessionManager } = require("./sessions");
6
+ var { attachGitSessionAttribution } = require("./git-session-attribution");
6
7
  var { createSDKBridge, createMessageQueue } = require("./sdk-bridge");
7
8
  var { createTerminalManager } = require("./terminal-manager");
8
9
  var { createNotesManager } = require("./notes");
@@ -172,7 +173,9 @@ function createProjectContext(opts) {
172
173
  // --- YOKE adapters (multi-vendor, lazy init) ---
173
174
  var _yokeState = yoke.createAdapters({ cwd: cwd, slug: slug, osUsers: osUsers });
174
175
  var adapters = _yokeState.adapters;
175
- var defaultVendor = adapters.claude ? "claude" : Object.keys(adapters)[0] || "claude";
176
+ // A new project has no remembered vendor. Select the first installed
177
+ // provider by the shared Claude -> Codex -> Kiro preference order.
178
+ var defaultVendor = yoke.resolveDefaultVendor(adapters);
176
179
  var adapter = adapters[defaultVendor] || null;
177
180
 
178
181
  // Browser MCP server runs in-process via createSdkMcpServer (no child process spawn).
@@ -399,6 +402,7 @@ function createProjectContext(opts) {
399
402
  var stopAllDirWatches = _fileWatch.stopAllDirWatches;
400
403
 
401
404
  // --- Session manager ---
405
+ var _gitAttribution = null;
402
406
  var sm = createSessionManager({
403
407
  cwd: cwd,
404
408
  send: send,
@@ -417,7 +421,17 @@ function createProjectContext(opts) {
417
421
  fn(ws, filterFn);
418
422
  }
419
423
  },
420
- onSessionDone: onSessionDone,
424
+ onSessionDone: function (session) {
425
+ if (_gitAttribution) _gitAttribution.finishTurn(session);
426
+ onSessionDone();
427
+ },
428
+ });
429
+ _gitAttribution = attachGitSessionAttribution({
430
+ cwd: cwd,
431
+ getOsUserInfoForSession: function (session) {
432
+ var linuxUser = getLinuxUserForSession(session);
433
+ return linuxUser ? resolveOsUserInfo(linuxUser) : null;
434
+ },
421
435
  });
422
436
  sm.availableVendors = Object.keys(adapters);
423
437
  sm.defaultVendor = defaultVendor;
@@ -541,6 +555,7 @@ function createProjectContext(opts) {
541
555
  usersModule: usersModule,
542
556
  getLinuxUserForSession: getLinuxUserForSession,
543
557
  onProcessingChanged: onProcessingChanged,
558
+ adapters: adapters,
544
559
  });
545
560
  var _sessionSpawn = attachSessionSpawn({
546
561
  cwd: cwd,
@@ -817,9 +832,10 @@ function createProjectContext(opts) {
817
832
  clayTls: serverTls,
818
833
  clayAuthToken: serverAuthToken,
819
834
  onProcessingChanged: onProcessingChanged,
820
- onTurnDone: isMate ? function (session, preview) {
821
- digestDmTurn(session, preview);
822
- } : null,
835
+ onTurnDone: function (session, preview) {
836
+ if (isMate) digestDmTurn(session, preview);
837
+ else _sessionPair.handleTurnDone(session);
838
+ },
823
839
  scheduleMessage: function (session, text, resetsAt) {
824
840
  scheduleMessage(session, text, resetsAt);
825
841
  },
@@ -1150,9 +1166,6 @@ function createProjectContext(opts) {
1150
1166
  return;
1151
1167
  }
1152
1168
 
1153
- // --- Email defaults (project-level) ---
1154
- if (_email.handleEmailMessage(ws, msg)) return;
1155
-
1156
1169
  // --- MCP bridge (remote MCP servers via extension) ---
1157
1170
  if (_mcp.handleMcpMessage(ws, msg)) return;
1158
1171
 
@@ -1406,6 +1419,7 @@ function createProjectContext(opts) {
1406
1419
  saveImageFile: saveImageFile,
1407
1420
  imagesDir: imagesDir,
1408
1421
  onProcessingChanged: onProcessingChanged,
1422
+ gitAttribution: _gitAttribution,
1409
1423
  _loop: _loop,
1410
1424
  browserState: browserState,
1411
1425
  sendExtensionCommandAny: sendExtensionCommandAny,
@@ -1617,6 +1631,7 @@ function createProjectContext(opts) {
1617
1631
  slug: slug,
1618
1632
  project: title || project,
1619
1633
  sm: sm,
1634
+ gitAttribution: _gitAttribution,
1620
1635
  send: send,
1621
1636
  imagesDir: imagesDir,
1622
1637
  osUsers: osUsers,
package/lib/public/app.js CHANGED
@@ -25,8 +25,9 @@ import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEna
25
25
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage, hasSendableContent, setScheduleBtnDisabled, setScheduleDelayMs, clearScheduleDelay } from './modules/input.js';
26
26
  import { initQrCode, triggerShare } from './modules/qrcode.js';
27
27
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
28
+ import { initGitPanel } from './modules/git-panel.js';
28
29
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
29
- import { initContextSources, initEmailDefaultsModal, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
30
+ import { initContextSources, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
30
31
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible, createNote } from './modules/sticky-notes.js';
31
32
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme, getChatLayout } from './modules/theme.js';
32
33
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
@@ -324,6 +325,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
324
325
  currentFullText: "",
325
326
  matePreThinkingEl: null,
326
327
  // panels
328
+ fileViewerFullscreen: false,
329
+ markdownSlidesActive: false,
330
+ markdownSlideIndex: 0,
331
+ markdownSlideCount: 0,
332
+ markdownSlideLevel: 1,
333
+ markdownSlidePreferredLevel: 1,
334
+ markdownSlideLevelExplicit: false,
327
335
  currentModel: "",
328
336
  currentModels: [],
329
337
  // Project's last-used vendor; seeds the sidebar's "New session" button.
@@ -875,6 +883,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
875
883
  if (!_perms.fileBrowser) {
876
884
  var fbBtn = document.getElementById("file-browser-btn");
877
885
  if (fbBtn) fbBtn.style.display = "none";
886
+ var gitBtn = document.getElementById("git-sidebar-btn");
887
+ if (gitBtn) gitBtn.style.display = "none";
878
888
  }
879
889
  if (!_perms.skills) {
880
890
  var sBtn = document.getElementById("skills-btn");
@@ -963,6 +973,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
963
973
  fileTreeEl: $("file-tree"),
964
974
  fileViewerEl: $("file-viewer"),
965
975
  });
976
+ initGitPanel();
966
977
 
967
978
  // --- Terminal ---
968
979
  initTerminal({
@@ -1047,8 +1058,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1047
1058
 
1048
1059
  // Close archive / scheduler panel when switching to other sidebar panels
1049
1060
  var fileBrowserBtn = $("file-browser-btn");
1061
+ var gitSidebarBtn = $("git-sidebar-btn");
1050
1062
  var terminalSidebarBtn = $("terminal-sidebar-btn");
1051
1063
  if (fileBrowserBtn) fileBrowserBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
1064
+ if (gitSidebarBtn) gitSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
1052
1065
  if (terminalSidebarBtn) terminalSidebarBtn.addEventListener("click", function () { if (isArchiveOpen()) closeArchive(); if (isSchedulerOpen()) closeScheduler(); });
1053
1066
 
1054
1067
  // --- Ralph Loop UI (delegated to app-loop-ui.js + app-loop-wizard.js) ---
@@ -1114,7 +1127,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1114
1127
 
1115
1128
  // --- MCP Servers ---
1116
1129
  initMcp();
1117
- initEmailDefaultsModal();
1118
1130
 
1119
1131
  // --- Skills ---
1120
1132
  initSkills();