clay-server 4.0.0-beta.2 → 4.0.0-beta.21

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 (177) hide show
  1. package/lib/background-task-timing.js +105 -0
  2. package/lib/capsule-display-floor.js +51 -0
  3. package/lib/capsule-frame-server.js +255 -0
  4. package/lib/capsule-mate-turn.js +105 -0
  5. package/lib/capsule-pig-logic.js +300 -0
  6. package/lib/capsule-server-runtimes.js +39 -0
  7. package/lib/capsules/pig/display.js +198 -0
  8. package/lib/capsules/pig/manifest.json +9 -0
  9. package/lib/capsules/pig/ui.json +84 -0
  10. package/lib/config.js +25 -1
  11. package/lib/daemon-projects.js +13 -8
  12. package/lib/daemon.js +127 -14
  13. package/lib/git-cli.js +95 -1
  14. package/lib/knowledge-import.js +436 -0
  15. package/lib/knowledge-record-store.js +188 -0
  16. package/lib/knowledge-search.js +240 -0
  17. package/lib/mate-knowledge-mcp-server.js +151 -0
  18. package/lib/mate-knowledge-migration.js +283 -0
  19. package/lib/mate-knowledge-service.js +488 -0
  20. package/lib/mate-knowledge-sync.js +482 -0
  21. package/lib/migrate-single-user.js +2 -0
  22. package/lib/notes-lifecycle.js +149 -0
  23. package/lib/notes.js +60 -3
  24. package/lib/os-users.js +3 -1
  25. package/lib/project-capsule-catalog.js +9 -1
  26. package/lib/project-capsule-turn.js +96 -0
  27. package/lib/project-connection.js +14 -1
  28. package/lib/project-http.js +15 -1
  29. package/lib/project-knowledge.js +22 -0
  30. package/lib/project-log-context.js +172 -0
  31. package/lib/project-log-feedback-delivery.js +118 -0
  32. package/lib/project-logs-comments.js +137 -0
  33. package/lib/project-logs-context-state.js +42 -0
  34. package/lib/project-logs-mcp-server.js +328 -0
  35. package/lib/project-logs-query.js +185 -0
  36. package/lib/project-logs-root.js +75 -0
  37. package/lib/project-logs-schema.js +259 -0
  38. package/lib/project-logs-service.js +463 -0
  39. package/lib/project-logs-snapshot.js +205 -0
  40. package/lib/project-logs-store.js +495 -0
  41. package/lib/project-logs-versioning.js +132 -0
  42. package/lib/project-logs.js +400 -0
  43. package/lib/project-mate-interaction.js +14 -0
  44. package/lib/project-mate-knowledge.js +110 -0
  45. package/lib/project-memory.js +22 -0
  46. package/lib/project-message-delivery.js +70 -0
  47. package/lib/project-pair-lifecycle.js +460 -0
  48. package/lib/project-session-handoff.js +1 -1
  49. package/lib/project-session-notes.js +64 -13
  50. package/lib/project-session-pair.js +159 -146
  51. package/lib/project-session-spawn.js +1 -1
  52. package/lib/project-sessions.js +58 -13
  53. package/lib/project-user-message.js +53 -4
  54. package/lib/project-vendor-login.js +20 -3
  55. package/lib/project-worker-permission.js +430 -0
  56. package/lib/project-worker-proposal.js +208 -62
  57. package/lib/project.js +178 -6
  58. package/lib/public/app.js +58 -35
  59. package/lib/public/css/admin.css +2 -2
  60. package/lib/public/css/capsule-ui.css +38 -0
  61. package/lib/public/css/command-palette.css +19 -9
  62. package/lib/public/css/git-placard.css +154 -0
  63. package/lib/public/css/home-hub.css +4 -0
  64. package/lib/public/css/home-session-actions.css +66 -0
  65. package/lib/public/css/home-sidebar.css +66 -0
  66. package/lib/public/css/input.css +163 -28
  67. package/lib/public/css/mobile-nav.css +80 -0
  68. package/lib/public/css/notifications-center.css +67 -0
  69. package/lib/public/css/pane.css +52 -0
  70. package/lib/public/css/project-logs.css +198 -0
  71. package/lib/public/css/scheduler.css +1 -1
  72. package/lib/public/css/session-actions.css +16 -101
  73. package/lib/public/css/sidebar.css +103 -0
  74. package/lib/public/css/sticky-notes.css +237 -229
  75. package/lib/public/css/worker-proposal.css +34 -1
  76. package/lib/public/index.html +45 -11
  77. package/lib/public/modules/admin.js +8 -8
  78. package/lib/public/modules/app-connection.js +109 -4
  79. package/lib/public/modules/app-home-hub.js +19 -7
  80. package/lib/public/modules/app-message-cards.js +2 -1
  81. package/lib/public/modules/app-messages.js +84 -7
  82. package/lib/public/modules/app-misc.js +2 -14
  83. package/lib/public/modules/app-notifications.js +11 -7
  84. package/lib/public/modules/app-panels.js +46 -4
  85. package/lib/public/modules/app-projects.js +13 -8
  86. package/lib/public/modules/app-rendering.js +1 -0
  87. package/lib/public/modules/background-tasks-ui.js +139 -16
  88. package/lib/public/modules/capsule-preference.js +44 -0
  89. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  90. package/lib/public/modules/clay-log-links.js +70 -0
  91. package/lib/public/modules/command-palette.js +52 -15
  92. package/lib/public/modules/context-sources.js +0 -3
  93. package/lib/public/modules/git-agent-sessions.js +85 -0
  94. package/lib/public/modules/git-panel.js +111 -84
  95. package/lib/public/modules/git-placard.js +161 -0
  96. package/lib/public/modules/home-chat-empty-state.js +39 -0
  97. package/lib/public/modules/home-chat-identity.js +9 -0
  98. package/lib/public/modules/home-conversations-sheet.js +102 -48
  99. package/lib/public/modules/home-mate-chat.js +15 -35
  100. package/lib/public/modules/home-session-actions.js +6 -0
  101. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  102. package/lib/public/modules/home-sidebar.js +7 -0
  103. package/lib/public/modules/home-tool-frame.js +182 -0
  104. package/lib/public/modules/home-tools.js +138 -1
  105. package/lib/public/modules/input.js +38 -7
  106. package/lib/public/modules/markdown.js +2 -0
  107. package/lib/public/modules/mate-sidebar.js +0 -8
  108. package/lib/public/modules/message-delivery.js +97 -0
  109. package/lib/public/modules/paste-modal.js +84 -0
  110. package/lib/public/modules/project-logs-render.js +405 -0
  111. package/lib/public/modules/project-logs.js +461 -0
  112. package/lib/public/modules/project-removal-target.js +26 -0
  113. package/lib/public/modules/scheduler.js +17 -3
  114. package/lib/public/modules/session-actions.js +29 -105
  115. package/lib/public/modules/session-hierarchy.js +54 -0
  116. package/lib/public/modules/sidebar-mobile.js +40 -15
  117. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  118. package/lib/public/modules/sidebar-sessions.js +77 -11
  119. package/lib/public/modules/sidebar.js +17 -5
  120. package/lib/public/modules/split-group-helpers.js +8 -0
  121. package/lib/public/modules/split-view.js +5 -2
  122. package/lib/public/modules/sticky-note-markdown.js +5 -0
  123. package/lib/public/modules/sticky-notes-browser.js +326 -0
  124. package/lib/public/modules/sticky-notes-card.js +360 -0
  125. package/lib/public/modules/sticky-notes-editor.js +291 -0
  126. package/lib/public/modules/sticky-notes-shared.js +90 -0
  127. package/lib/public/modules/sticky-notes.js +170 -946
  128. package/lib/public/modules/tool-palette-order.js +143 -0
  129. package/lib/public/modules/tool-palette-overlays.js +159 -0
  130. package/lib/public/modules/tool-palette.js +21 -188
  131. package/lib/public/modules/tools.js +21 -1
  132. package/lib/public/modules/update-snooze.js +169 -0
  133. package/lib/public/modules/user-settings.js +4 -0
  134. package/lib/public/modules/worker-pane-lock.js +176 -0
  135. package/lib/public/modules/worker-proposal-state.js +16 -0
  136. package/lib/public/modules/worker-proposal.js +49 -12
  137. package/lib/public/style.css +3 -1
  138. package/lib/sdk-bridge.js +132 -46
  139. package/lib/sdk-message-processor.js +60 -14
  140. package/lib/server-experimental-settings.js +49 -0
  141. package/lib/server-global-ws.js +11 -0
  142. package/lib/server-home-chat-events.js +31 -1
  143. package/lib/server-home-chat.js +27 -4
  144. package/lib/server-home-clay-entry.js +1 -1
  145. package/lib/server-home-clay-session-links.js +34 -5
  146. package/lib/server-settings.js +5 -0
  147. package/lib/server-tools.js +127 -9
  148. package/lib/server.js +124 -18
  149. package/lib/session-driver-eligibility.js +38 -0
  150. package/lib/session-driver-orchestration.js +31 -0
  151. package/lib/session-hygiene.js +3 -0
  152. package/lib/session-notes-mcp-server.js +25 -5
  153. package/lib/session-pair-factory.js +247 -0
  154. package/lib/session-pair-mcp-server.js +46 -8
  155. package/lib/session-pair-prompts.js +79 -0
  156. package/lib/session-pair-turn-control.js +153 -0
  157. package/lib/session-provenance.js +119 -0
  158. package/lib/session-spawn-mcp-server.js +1 -1
  159. package/lib/session-split-groups.js +4 -1
  160. package/lib/session-title-policy.js +60 -0
  161. package/lib/session-visibility.js +39 -0
  162. package/lib/sessions.js +61 -11
  163. package/lib/tools-registry.js +89 -10
  164. package/lib/update-snooze.js +437 -0
  165. package/lib/users-experimental-preferences.js +30 -0
  166. package/lib/users-permissions.js +2 -2
  167. package/lib/users.js +22 -16
  168. package/lib/workspace-query-access.js +102 -0
  169. package/lib/workspace-query-service.js +24 -17
  170. package/lib/worktree.js +25 -38
  171. package/lib/ws-schema.js +38 -7
  172. package/lib/yoke/adapters/claude-worker.js +16 -6
  173. package/lib/yoke/adapters/claude.js +81 -4
  174. package/lib/yoke/adapters/codex.js +63 -15
  175. package/lib/yoke/codex-background-tasks.js +8 -2
  176. package/lib/yoke/vendor-registry.js +1 -1
  177. package/package.json +3 -3
@@ -6,6 +6,7 @@ import { showHomeHub, minimizeHomeHub } from './app-home-hub.js';
6
6
  import { openHomeConversation } from './home-mate-chat.js';
7
7
  import { startSearchClayChat, attachSearchClayChat, detachSearchClayChat, getSearchClayChatSummary } from './search-clay-chat.js';
8
8
  import { getWs } from './ws-ref.js';
9
+ import { openProjectLog } from './project-logs.js';
9
10
 
10
11
  function formatRelativeDate(ts) {
11
12
  if (!ts) return "";
@@ -27,6 +28,7 @@ var ctx;
27
28
  var paletteEl = null, inputEl = null, resultsEl = null, footerEl = null;
28
29
  var activeIndex = -1, items = [], debounceTimer = null, abortCtrl = null;
29
30
  var pendingNav = null, cachedHomeData = [], cachedVersion = null;
31
+ var pendingLogNav = null;
30
32
  var searchResults = [], searchQuery = "", searchPending = false, chatMode = false;
31
33
 
32
34
  export function initCommandPalette(value) {
@@ -52,43 +54,62 @@ export function initCommandPalette(value) {
52
54
  }
53
55
 
54
56
  function handleClaySessionLinkClick(event) {
55
- var link = event.target.closest(".clayos-session-link");
57
+ var link = event.target.closest(".clayos-session-link, .clayos-log-link");
56
58
  if (!link || link.classList.contains("is-loading")) return;
59
+ event.preventDefault();
60
+ event.stopPropagation();
61
+ if (link.classList.contains("clayos-log-link") && !link.closest(".search-clay-chat, #home-mate-chat")) {
62
+ openProjectLog(link.dataset.logRef);
63
+ return;
64
+ }
57
65
  var ws = getWs();
58
66
  if (!ws || ws.readyState !== 1) {
59
67
  link.classList.add("is-error");
60
- link.title = "Reconnect to open this session";
68
+ link.title = "Reconnect to open this item";
61
69
  return;
62
70
  }
63
- var requestId = "clayos-session-" + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
64
- link.dataset.sessionRequest = requestId;
71
+ var isLog = link.classList.contains("clayos-log-link");
72
+ var requestId = "clayos-" + (isLog ? "log-" : "session-") + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
73
+ if (isLog) link.dataset.logRequest = requestId;
74
+ else link.dataset.sessionRequest = requestId;
65
75
  link.classList.remove("is-error");
66
76
  link.classList.add("is-loading");
67
- ws.send(JSON.stringify({
68
- type: "home_clay_session_resolve",
69
- requestId: requestId,
70
- sessionRef: link.dataset.sessionRef,
71
- surface: link.closest(".search-clay-chat") ? "search" : "home",
72
- }));
77
+ var request = { requestId: requestId, surface: link.closest(".search-clay-chat") ? "search" : "home" };
78
+ if (isLog) {
79
+ request.type = "home_clay_log_resolve";
80
+ request.ref = link.dataset.logRef;
81
+ } else {
82
+ request.type = "home_clay_session_resolve";
83
+ request.sessionRef = link.dataset.sessionRef;
84
+ }
85
+ ws.send(JSON.stringify(request));
73
86
  }
74
87
 
75
88
  export function handleClaySessionTarget(msg) {
76
- if (!msg || msg.type !== "home_clay_session_target") return false;
77
- var links = document.querySelectorAll(".clayos-session-link[data-session-request]");
89
+ if (!msg || (msg.type !== "home_clay_session_target" && msg.type !== "home_clay_log_target")) return false;
90
+ var isLog = msg.type === "home_clay_log_target";
91
+ var links = document.querySelectorAll(isLog ? ".clayos-log-link[data-log-request]" : ".clayos-session-link[data-session-request]");
78
92
  var link = null;
79
- for (var i = 0; i < links.length; i++) if (links[i].dataset.sessionRequest === msg.requestId) link = links[i];
93
+ for (var i = 0; i < links.length; i++) {
94
+ if ((isLog ? links[i].dataset.logRequest : links[i].dataset.sessionRequest) === msg.requestId) link = links[i];
95
+ }
80
96
  if (link) {
81
97
  link.classList.remove("is-loading");
82
- delete link.dataset.sessionRequest;
98
+ if (isLog) delete link.dataset.logRequest;
99
+ else delete link.dataset.sessionRequest;
83
100
  }
84
101
  if (msg.status !== "ready" || !msg.target) {
85
102
  if (link) {
86
103
  link.classList.add("is-error");
87
- link.title = msg.error || "Session not available";
104
+ link.title = msg.error || "Item not available";
88
105
  }
89
106
  return true;
90
107
  }
91
108
  closeCommandPalette();
109
+ if (isLog) {
110
+ navigateToLog(msg.target);
111
+ return true;
112
+ }
92
113
  if (msg.target.isMate && msg.target.mateId && msg.target.homeSessionId) {
93
114
  showHomeHub();
94
115
  openHomeConversation(msg.target.mateId, msg.target.homeSessionId);
@@ -129,6 +150,11 @@ export function closeCommandPalette() {
129
150
  export function setPaletteVersion(version) { cachedVersion = version; }
130
151
 
131
152
  export function handlePaletteSessionSwitch() {
153
+ if (pendingLogNav && ctx.currentSlug && ctx.currentSlug() === pendingLogNav.projectSlug) {
154
+ var logNav = pendingLogNav;
155
+ pendingLogNav = null;
156
+ openProjectLog(logNav.ref);
157
+ }
132
158
  if (!pendingNav) return;
133
159
  var nav = pendingNav;
134
160
  pendingNav = null;
@@ -138,6 +164,17 @@ export function handlePaletteSessionSwitch() {
138
164
  }
139
165
  }
140
166
 
167
+ function navigateToLog(target) {
168
+ closeCommandPalette();
169
+ minimizeHomeHub();
170
+ if (ctx.currentSlug && ctx.currentSlug() === target.projectSlug) {
171
+ openProjectLog(target.ref);
172
+ } else {
173
+ pendingLogNav = { projectSlug: target.projectSlug, ref: target.ref };
174
+ ctx.switchProject(target.projectSlug);
175
+ }
176
+ }
177
+
141
178
  function buildDOM() {
142
179
  paletteEl = document.createElement("div");
143
180
  paletteEl.className = "cmd-palette hidden";
@@ -230,10 +230,8 @@ function getActiveSourceRowsHTML() {
230
230
  function renderChips() {
231
231
  // Update add button — show badge count when sources are active
232
232
  var addBtn = document.getElementById("context-sources-add");
233
- var labelSpan = addBtn.querySelector(".ctx-label");
234
233
  var existingBadge = addBtn.querySelector(".ctx-badge");
235
234
  if (activeSourceIds.size > 0) {
236
- if (labelSpan) labelSpan.style.display = "none";
237
235
  if (!existingBadge) {
238
236
  existingBadge = document.createElement("span");
239
237
  existingBadge.className = "ctx-badge";
@@ -251,7 +249,6 @@ function renderChips() {
251
249
  }
252
250
  addBtn.removeAttribute("title");
253
251
  } else {
254
- if (labelSpan) { labelSpan.style.display = ""; }
255
252
  if (existingBadge) existingBadge.remove();
256
253
  addBtn.setAttribute("data-tip", "Add context sources");
257
254
  addBtn.removeAttribute("data-tip-html");
@@ -0,0 +1,85 @@
1
+ // git-agent-sessions.js — agent handoffs launched from the Git surface.
2
+ //
3
+ // Extracted from git-panel.js so that module stays under the size limit while
4
+ // the Git data path (one status reading, one summary reading, one placard)
5
+ // remains in a single place. This module owns only the "hand this over to a
6
+ // focused agent session" concern, plus the small path/title helpers those
7
+ // prompts and the panel's own file rows share, so neither is defined twice.
8
+
9
+ import { store } from './store.js';
10
+ import { showToast } from './utils.js';
11
+ import { sendTextMessage } from './input.js';
12
+ import { resolveDefaultVendor, startNewSession } from './sidebar-sessions.js';
13
+ import { getWs } from './ws-ref.js';
14
+
15
+ export function splitFilePath(filePath) {
16
+ var slash = filePath.lastIndexOf("/");
17
+ if (slash === -1) return { name: filePath, dir: "" };
18
+ return { name: filePath.slice(slash + 1), dir: filePath.slice(0, slash + 1) };
19
+ }
20
+
21
+ export function compactSessionTitle(title) {
22
+ var value = String(title || "Agent session").replace(/\s+/g, " ").trim();
23
+ if (/^Review the current uncommitted changes/i.test(value)) return "File review";
24
+ if (/^Create a Git commit for the currently staged changes/i.test(value)) return "Commit session";
25
+ return value;
26
+ }
27
+
28
+ // Close the Git surface through its own close control, which owns the panel's
29
+ // exit animation and the return to the sessions panel. Going through the
30
+ // existing control keeps one lifecycle rather than a second teardown path.
31
+ export function closeGitSurface() {
32
+ var gitPanel = document.getElementById("sidebar-panel-git");
33
+ var closeBtn = document.getElementById("git-panel-close");
34
+ if (closeBtn && gitPanel && !gitPanel.classList.contains("hidden")) closeBtn.click();
35
+ }
36
+
37
+ function startFocusedAgentSession(prompt, successMessage, sessionTitle) {
38
+ if (!store.get('connected')) {
39
+ showToast("Not connected — agent session was not started.", "warn");
40
+ return;
41
+ }
42
+ var previousSessionId = store.get('activeSessionId');
43
+ var unsubscribe = null;
44
+ var sessionReady = false;
45
+ var timer = setTimeout(function () {
46
+ if (unsubscribe) unsubscribe();
47
+ showToast("Agent session could not be started", "warn");
48
+ }, 10000);
49
+ unsubscribe = store.subscribe(function (state) {
50
+ if (sessionReady || !state.activeSessionId || state.activeSessionId === previousSessionId) return;
51
+ sessionReady = true;
52
+ clearTimeout(timer);
53
+ setTimeout(function () {
54
+ if (unsubscribe) unsubscribe();
55
+ unsubscribe = null;
56
+ var newSessionId = store.get('activeSessionId');
57
+ var ws = getWs();
58
+ if (sessionTitle && ws && ws.readyState === WebSocket.OPEN && newSessionId) {
59
+ ws.send(JSON.stringify({ type: "rename_session", id: newSessionId, title: sessionTitle }));
60
+ }
61
+ closeGitSurface();
62
+ if (sendTextMessage(prompt)) showToast(successMessage || "Agent session started");
63
+ }, 0);
64
+ });
65
+ startNewSession(resolveDefaultVendor(), { mode: "gui", forceNew: true });
66
+ }
67
+
68
+ export function startAgentCommitSession(stagedCount) {
69
+ if (!stagedCount) return;
70
+ var prompt = "Create a Git commit for the currently staged changes.\n\n" +
71
+ "Inspect `git diff --cached` and the repository instructions before committing. " +
72
+ "Commit exactly the staged changes; do not stage additional files. " +
73
+ "Write a concise Angular Commit Convention message, use the angular-commit skill, " +
74
+ "and do not add Co-Authored-By lines. If nothing is staged, explain that and stop.";
75
+ startFocusedAgentSession(prompt, "Commit session started", "Commit staged changes");
76
+ }
77
+
78
+ export function startAgentFileReview(filePath) {
79
+ var prompt = "Review the current uncommitted changes in `" + filePath + "`.\n\n" +
80
+ "Inspect the Git diff and relevant repository context. Explain the intent of the change, " +
81
+ "identify correctness or regression risks, and suggest specific improvements. " +
82
+ "Do not edit files or run Git actions unless I ask in this session.";
83
+ var parts = splitFilePath(filePath);
84
+ startFocusedAgentSession(prompt, "File review session started", "Review · " + parts.name);
85
+ }
@@ -2,16 +2,41 @@ import { store } from './store.js';
2
2
  import { refreshIcons, iconHtml } from './icons.js';
3
3
  import { escapeHtml, showToast } from './utils.js';
4
4
  import { openWorkingTreeDiff } from './filebrowser.js';
5
- import { sendTextMessage } from './input.js';
6
- import { resolveDefaultVendor, startNewSession } from './sidebar-sessions.js';
7
5
  import { getWs } from './ws-ref.js';
6
+ import { hideGitPlacard, mergeStatusIntoSummary, renderGitPlacard } from './git-placard.js';
7
+ import {
8
+ closeGitSurface,
9
+ compactSessionTitle,
10
+ splitFilePath,
11
+ startAgentCommitSession,
12
+ startAgentFileReview
13
+ } from './git-agent-sessions.js';
8
14
 
9
15
  var currentStatus = null;
16
+ var currentSummary = null;
10
17
  var busy = false;
11
18
  var lastError = "";
12
19
  var statusBasePath = null;
13
20
  var expandedFileActionsPath = null;
14
21
 
22
+ // One timer drives both readings so there is never a second poll. The full
23
+ // status runs at the panel's existing cadence only while the panel is open;
24
+ // with the panel closed the placard settles for the cheap summary at a much
25
+ // slower cadence, and neither path ever does diff work.
26
+ var POLL_MS = 2500;
27
+ var SUMMARY_TICKS = 6;
28
+ var summaryTicks = 0;
29
+
30
+ // A project surface always has a /p/<slug>/ base path. Home and any surface
31
+ // without a project must not issue a repository request at all.
32
+ function hasProjectBasePath() {
33
+ return String(store.get('basePath') || "").indexOf("/p/") === 0;
34
+ }
35
+
36
+ function placardMounted() {
37
+ return !!document.getElementById("git-placard");
38
+ }
39
+
15
40
  function apiPath(path) {
16
41
  return store.get('basePath') + "api/git/" + path;
17
42
  }
@@ -33,12 +58,6 @@ function shortOrigin(origin) {
33
58
  .replace(/\.git$/, "");
34
59
  }
35
60
 
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
61
  function fileStatusLabel(file, staged) {
43
62
  if (file.conflicted) return "!";
44
63
  if (file.untracked) return "U";
@@ -46,13 +65,6 @@ function fileStatusLabel(file, staged) {
46
65
  return code === "." ? "M" : code;
47
66
  }
48
67
 
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
68
  function renderFileMeta(file, directory) {
57
69
  var sessions = Array.isArray(file.sessions) ? file.sessions : [];
58
70
  var first = sessions.length > 0 ? sessions[0] : null;
@@ -200,19 +212,54 @@ function render() {
200
212
  var stagedList = body.querySelector(".git-section-staged .git-file-list");
201
213
  if (changesList) changesList.scrollTop = changesScrollTop;
202
214
  if (stagedList) stagedList.scrollTop = stagedScrollTop;
203
- updateBadge();
215
+ syncPlacard();
204
216
  }
205
217
 
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");
218
+ // The placard is a projection of whichever reading is current. A full status
219
+ // supersedes the summary when it exists, so the two never disagree.
220
+ function syncPlacard() {
221
+ if (!placardMounted()) return;
222
+ if (currentStatus && currentStatus.isRepository) {
223
+ currentSummary = mergeStatusIntoSummary(currentStatus, currentSummary);
224
+ }
225
+ if (!currentSummary || !currentSummary.isRepository) {
226
+ hideGitPlacard();
211
227
  return;
212
228
  }
213
- var count = currentStatus.files.length;
214
- badge.textContent = count > 99 ? "99+" : String(count);
215
- badge.classList.toggle("hidden", count === 0);
229
+ renderGitPlacard(currentSummary);
230
+ }
231
+
232
+ // Cheap placard-only reading. Used on sidebar lifecycle (connect, reconnect,
233
+ // project switch) and on the slow tick while the full panel is closed.
234
+ export function refreshGitSummary() {
235
+ if (busy || !hasProjectBasePath() || !placardMounted()) return Promise.resolve(currentSummary);
236
+ var basePath = store.get('basePath');
237
+ return requestJson(basePath + "api/git/summary", { cache: "no-store" }).then(function (summary) {
238
+ if (store.get('basePath') !== basePath) return currentSummary;
239
+ currentSummary = summary && summary.isRepository ? summary : null;
240
+ syncPlacard();
241
+ return currentSummary;
242
+ }).catch(function () {
243
+ // An unavailable or forbidden repository shows nothing rather than an
244
+ // error placeholder; the full panel still reports errors when opened.
245
+ if (store.get('basePath') !== basePath) return currentSummary;
246
+ currentSummary = null;
247
+ syncPlacard();
248
+ return null;
249
+ });
250
+ }
251
+
252
+ // Project switch and reconnect both land here, so no stale repository ever
253
+ // survives into another project and nothing flashes before detection lands.
254
+ export function resetGitSurface() {
255
+ currentStatus = null;
256
+ currentSummary = null;
257
+ lastError = "";
258
+ expandedFileActionsPath = null;
259
+ statusBasePath = store.get('basePath');
260
+ summaryTicks = 0;
261
+ hideGitPlacard();
262
+ render();
216
263
  }
217
264
 
218
265
  export function refreshGitStatus() {
@@ -221,8 +268,10 @@ export function refreshGitStatus() {
221
268
  if (statusBasePath !== basePath) {
222
269
  statusBasePath = basePath;
223
270
  currentStatus = null;
271
+ currentSummary = null;
224
272
  lastError = "";
225
273
  expandedFileActionsPath = null;
274
+ hideGitPlacard();
226
275
  render();
227
276
  }
228
277
  var refreshBtn = document.getElementById("git-panel-refresh");
@@ -309,63 +358,7 @@ function returnToSession(sessionId) {
309
358
  var ws = getWs();
310
359
  if (!ws || ws.readyState !== WebSocket.OPEN || !sessionId) return;
311
360
  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);
361
+ closeGitSurface();
369
362
  }
370
363
 
371
364
  function toggleFileActions(fileMain) {
@@ -394,10 +387,32 @@ function toggleFileActions(fileMain) {
394
387
  }
395
388
  }
396
389
 
390
+ // Sidebar lifecycle. A project switch resets everything and asks once; a
391
+ // reconnect re-asks without resetting the surface the user is looking at.
392
+ // Entering a Mate DM hides the placard, since that surface has no repository.
393
+ function initGitLifecycle() {
394
+ statusBasePath = store.get('basePath');
395
+ store.subscribe(function (state, prev) {
396
+ if (state.basePath !== prev.basePath || state.currentSlug !== prev.currentSlug) {
397
+ resetGitSurface();
398
+ if (!state.dmMode) refreshGitSummary();
399
+ return;
400
+ }
401
+ if (state.dmMode !== prev.dmMode) {
402
+ if (state.dmMode) hideGitPlacard();
403
+ else refreshGitSummary();
404
+ return;
405
+ }
406
+ if (state.connected && !prev.connected && !state.dmMode) refreshGitSummary();
407
+ });
408
+ if (!store.get('dmMode')) refreshGitSummary();
409
+ }
410
+
397
411
  export function initGitPanel() {
398
412
  var body = document.getElementById("git-panel-body");
399
413
  var refreshBtn = document.getElementById("git-panel-refresh");
400
414
  if (!body || !refreshBtn) return;
415
+ initGitLifecycle();
401
416
  refreshBtn.addEventListener("click", function () { refreshGitStatus(); });
402
417
  body.addEventListener("click", function (event) {
403
418
  var openDiff = event.target.closest("[data-git-open-diff]");
@@ -427,7 +442,9 @@ export function initGitPanel() {
427
442
  }
428
443
  var agentCommitButton = event.target.closest("[data-git-agent-commit]");
429
444
  if (agentCommitButton) {
430
- if (!agentCommitButton.disabled) startAgentCommitSession();
445
+ if (!agentCommitButton.disabled && !busy && currentStatus) {
446
+ startAgentCommitSession(currentStatus.files.filter(function (file) { return file.staged; }).length);
447
+ }
431
448
  return;
432
449
  }
433
450
  var actionButton = event.target.closest("[data-git-action]");
@@ -451,6 +468,16 @@ export function initGitPanel() {
451
468
  });
452
469
  setInterval(function () {
453
470
  var panel = document.getElementById("sidebar-panel-git");
454
- if (panel && !panel.classList.contains("hidden") && store.get('connected') && !busy) refreshGitStatus();
455
- }, 2500);
471
+ var panelOpen = !!(panel && !panel.classList.contains("hidden"));
472
+ if (panelOpen) {
473
+ summaryTicks = 0;
474
+ if (store.get('connected') && !busy) refreshGitStatus();
475
+ return;
476
+ }
477
+ summaryTicks++;
478
+ if (summaryTicks < SUMMARY_TICKS) return;
479
+ summaryTicks = 0;
480
+ if (document.hidden || store.get('dmMode') || !store.get('connected') || busy) return;
481
+ refreshGitSummary();
482
+ }, POLL_MS);
456
483
  }
@@ -0,0 +1,161 @@
1
+ // git-placard.js — compact always-visible repository placard.
2
+ //
3
+ // Git is no longer a tool-palette tile. Instead, a real Git repository gets a
4
+ // quiet placard directly under the tool strip in the project sidebar: repo
5
+ // identity, current branch, and a bounded status glance. The placard is a
6
+ // projection, never a second source of truth — git-panel.js owns the fetch and
7
+ // hands data here, so there is exactly one Git request path in the client.
8
+ //
9
+ // The placard renders only server-derived, bounded fields. It never receives or
10
+ // renders a repository path, and it stays hidden entirely until a real
11
+ // repository is confirmed, so a non-Git project, an unavailable worktree, a
12
+ // Home/Mate surface, or a still-pending detection shows nothing at all rather
13
+ // than an empty placeholder.
14
+
15
+ import { escapeHtml } from './utils.js';
16
+ import { iconHtml, refreshIcons } from './icons.js';
17
+
18
+ var lastRenderedHtml = "";
19
+ var lastRenderedLabel = "";
20
+
21
+ function countChangedFiles(files) {
22
+ var counts = { changed: 0, staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
23
+ var list = Array.isArray(files) ? files : [];
24
+ counts.changed = list.length;
25
+ for (var i = 0; i < list.length; i++) {
26
+ var file = list[i];
27
+ if (file.conflicted) counts.conflicted++;
28
+ if (file.untracked) counts.untracked++;
29
+ if (file.staged) counts.staged++;
30
+ if (file.unstaged && !file.untracked) counts.unstaged++;
31
+ }
32
+ return counts;
33
+ }
34
+
35
+ // Fold a full panel status into the placard shape. The panel status is the
36
+ // fresher, more authoritative reading whenever it is available, so it wins for
37
+ // branch and counts. `remote` is kept from the last server summary because the
38
+ // label is derived server-side; the client never parses a remote URL itself.
39
+ export function mergeStatusIntoSummary(status, previous) {
40
+ if (!status || !status.isRepository) return null;
41
+ var base = previous || {};
42
+ var counts = countChangedFiles(status.files);
43
+ return {
44
+ isRepository: true,
45
+ name: status.name || base.name || null,
46
+ branch: status.detached ? null : (status.branch || null),
47
+ detached: !!status.detached,
48
+ shortOid: status.oid ? String(status.oid).slice(0, 8) : (base.shortOid || null),
49
+ isWorktree: !!status.isWorktree,
50
+ hasUpstream: !!status.upstream,
51
+ ahead: status.ahead || 0,
52
+ behind: status.behind || 0,
53
+ changed: counts.changed,
54
+ staged: counts.staged,
55
+ unstaged: counts.unstaged,
56
+ untracked: counts.untracked,
57
+ conflicted: counts.conflicted,
58
+ remote: base.remote || null,
59
+ };
60
+ }
61
+
62
+ function branchLabel(summary) {
63
+ if (summary.detached) return summary.shortOid ? "Detached at " + summary.shortOid : "Detached HEAD";
64
+ return summary.branch || "No commits yet";
65
+ }
66
+
67
+ function changeLabel(summary) {
68
+ if (summary.conflicted > 0) {
69
+ return summary.conflicted + " conflicted";
70
+ }
71
+ if (summary.changed > 0) {
72
+ return summary.changed + " changed";
73
+ }
74
+ return "Clean";
75
+ }
76
+
77
+ // One plain-text sentence for assistive technology. The rendered rows are
78
+ // marked aria-hidden so the group is announced once, in full, rather than as a
79
+ // stream of bare numbers and glyphs.
80
+ export function accessibleLabel(summary) {
81
+ if (!summary || !summary.isRepository) return "";
82
+ var parts = ["Repository " + (summary.name || "unknown")];
83
+ parts.push(summary.detached ? branchLabel(summary) : "branch " + branchLabel(summary));
84
+ if (summary.isWorktree) parts.push("linked worktree");
85
+ parts.push(summary.changed > 0 ? changeLabel(summary) + " files" : "working tree clean");
86
+ if (summary.hasUpstream && summary.ahead > 0) parts.push(summary.ahead + " ahead");
87
+ if (summary.hasUpstream && summary.behind > 0) parts.push(summary.behind + " behind");
88
+ if (summary.remote) parts.push("remote " + summary.remote);
89
+ return parts.join(", ");
90
+ }
91
+
92
+ function metaHtml(summary) {
93
+ var html = "";
94
+ if (summary.isWorktree) html += '<span class="git-placard-badge">Linked worktree</span>';
95
+ if (summary.detached) html += '<span class="git-placard-badge git-placard-badge-warn">Detached</span>';
96
+
97
+ var stateClass = "git-placard-stat";
98
+ if (summary.conflicted > 0) stateClass += " git-placard-stat-warn";
99
+ else if (summary.changed > 0) stateClass += " git-placard-stat-active";
100
+ html += '<span class="' + stateClass + '">' + escapeHtml(changeLabel(summary)) + '</span>';
101
+
102
+ if (summary.hasUpstream && summary.ahead > 0) {
103
+ html += '<span class="git-placard-stat" title="' + summary.ahead + ' to push">' +
104
+ iconHtml("arrow-up") + summary.ahead + '</span>';
105
+ }
106
+ if (summary.hasUpstream && summary.behind > 0) {
107
+ html += '<span class="git-placard-stat" title="' + summary.behind + ' to pull">' +
108
+ iconHtml("arrow-down") + summary.behind + '</span>';
109
+ }
110
+ return '<div class="git-placard-meta">' + html + '</div>';
111
+ }
112
+
113
+ function placardHtml(summary) {
114
+ var html = '<div class="git-placard-identity">' +
115
+ '<span class="git-placard-glyph">' + iconHtml("git-branch") + '</span>' +
116
+ '<span class="git-placard-names">' +
117
+ '<span class="git-placard-repo">' + escapeHtml(summary.name || "Repository") + '</span>' +
118
+ '<span class="git-placard-branch">' + escapeHtml(branchLabel(summary)) + '</span>' +
119
+ '</span>' +
120
+ '</div>';
121
+ html += metaHtml(summary);
122
+ if (summary.remote) {
123
+ html += '<div class="git-placard-remote">' + iconHtml("cloud") +
124
+ '<span>' + escapeHtml(summary.remote) + '</span></div>';
125
+ }
126
+ return html;
127
+ }
128
+
129
+ export function hideGitPlacard() {
130
+ var el = document.getElementById("git-placard");
131
+ var body = document.getElementById("git-placard-body");
132
+ lastRenderedHtml = "";
133
+ lastRenderedLabel = "";
134
+ if (body) body.innerHTML = "";
135
+ if (el) {
136
+ el.classList.add("hidden");
137
+ el.removeAttribute("aria-label");
138
+ }
139
+ }
140
+
141
+ export function renderGitPlacard(summary) {
142
+ var el = document.getElementById("git-placard");
143
+ var body = document.getElementById("git-placard-body");
144
+ if (!el || !body) return;
145
+ if (!summary || !summary.isRepository) {
146
+ hideGitPlacard();
147
+ return;
148
+ }
149
+ var html = placardHtml(summary);
150
+ if (html !== lastRenderedHtml) {
151
+ body.innerHTML = html;
152
+ lastRenderedHtml = html;
153
+ refreshIcons();
154
+ }
155
+ var label = accessibleLabel(summary);
156
+ if (label !== lastRenderedLabel) {
157
+ el.setAttribute("aria-label", label);
158
+ lastRenderedLabel = label;
159
+ }
160
+ el.classList.remove("hidden");
161
+ }