clay-server 4.0.0-beta.12 → 4.0.0-beta.14
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/lib/project-log-feedback-delivery.js +76 -0
- package/lib/project-logs-mcp-server.js +14 -12
- package/lib/project-logs.js +4 -0
- package/lib/project-pair-lifecycle.js +16 -13
- package/lib/project-session-pair.js +34 -34
- package/lib/project-sessions.js +2 -1
- package/lib/project-user-message.js +1 -0
- package/lib/project-worker-permission.js +3 -2
- package/lib/project.js +10 -0
- package/lib/public/app.js +0 -2
- package/lib/public/css/project-logs.css +25 -79
- package/lib/public/modules/project-logs-render.js +2 -2
- package/lib/public/modules/project-logs.js +62 -59
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-view.js +5 -2
- package/lib/sdk-bridge.js +21 -19
- package/lib/sdk-message-processor.js +36 -7
- package/lib/server-home-chat.js +5 -0
- package/lib/session-driver-eligibility.js +14 -167
- package/lib/session-pair-factory.js +4 -13
- package/lib/session-pair-mcp-server.js +2 -0
- package/lib/session-pair-prompts.js +3 -3
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/yoke/adapters/claude.js +12 -0
- package/package.json +1 -1
- package/lib/public/modules/project-logs-ambient.js +0 -238
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Project Logs — restrained,
|
|
1
|
+
/* Project Logs — a restrained editorial ledger, designed for close reading. */
|
|
2
2
|
|
|
3
3
|
/* Wide screens: bounded workbench window beside #app, matching the geometry of
|
|
4
4
|
the document viewer and the terminal. */
|
|
@@ -94,90 +94,32 @@
|
|
|
94
94
|
.project-logs-icon-btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
|
|
95
95
|
.project-logs-icon-btn:disabled { opacity: 0.4; cursor: default; }
|
|
96
96
|
.project-logs-window-actions { display: inline-flex; align-items: center; gap: 6px; }
|
|
97
|
-
/*
|
|
98
|
-
A slim notch on the right workbench boundary, offered only where a real
|
|
99
|
-
pointer can hover. Touch keeps the explicit toolbar route. */
|
|
100
|
-
@media (min-width: 1024px) {
|
|
101
|
-
.project-logs-handle {
|
|
102
|
-
position: absolute;
|
|
103
|
-
top: 50%;
|
|
104
|
-
right: 0;
|
|
105
|
-
transform: translateY(-50%);
|
|
106
|
-
width: 14px;
|
|
107
|
-
height: 88px;
|
|
108
|
-
padding: 0;
|
|
109
|
-
display: flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
justify-content: center;
|
|
112
|
-
border: 1px solid var(--border);
|
|
113
|
-
border-right: 0;
|
|
114
|
-
border-radius: 8px 0 0 8px;
|
|
115
|
-
background: var(--bg-alt);
|
|
116
|
-
cursor: pointer;
|
|
117
|
-
z-index: 40;
|
|
118
|
-
transition: width 0.16s ease, background 0.16s ease;
|
|
119
|
-
}
|
|
120
|
-
.project-logs-handle:hover,
|
|
121
|
-
.project-logs-handle:focus-visible { width: 18px; background: var(--bg); }
|
|
122
|
-
.project-logs-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
123
|
-
.project-logs-handle.hidden { display: none; }
|
|
124
|
-
.project-logs-handle-notch {
|
|
125
|
-
width: 3px;
|
|
126
|
-
height: 34px;
|
|
127
|
-
border-radius: 999px;
|
|
128
|
-
background: var(--border);
|
|
129
|
-
transition: background 0.16s ease, height 0.16s ease;
|
|
130
|
-
}
|
|
131
|
-
.project-logs-handle:hover .project-logs-handle-notch { background: var(--text-dimmer); }
|
|
132
|
-
/* Unread: a brighter notch, no motion of its own. */
|
|
133
|
-
.project-logs-handle.has-unread .project-logs-handle-notch { background: var(--accent); height: 46px; }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* Touch and narrow viewports never depend on hover. */
|
|
137
|
-
@media (max-width: 1023px) {
|
|
138
|
-
.project-logs-handle { display: none; }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* A preview is the same pane, revealed but uncommitted. */
|
|
142
|
-
#project-logs-panel.project-logs-previewing { box-shadow: 0 0 0 1px var(--accent), 4px 8px 24px rgba(var(--shadow-rgb), 0.2); }
|
|
143
|
-
|
|
144
|
-
/* --- Unread cues --- */
|
|
97
|
+
/* The explicit tool button is the only discovery affordance. */
|
|
145
98
|
#project-logs-btn.project-logs-unread { color: var(--accent); }
|
|
146
99
|
#project-logs-count { background: var(--accent); color: #fff; }
|
|
147
100
|
|
|
148
|
-
@keyframes project-logs-pulse {
|
|
149
|
-
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
|
|
150
|
-
70% { box-shadow: 0 0 0 7px transparent; }
|
|
151
|
-
100% { box-shadow: 0 0 0 0 transparent; }
|
|
152
|
-
}
|
|
153
|
-
.project-logs-pulse { animation: project-logs-pulse 1.1s ease-out 2; }
|
|
154
|
-
|
|
155
|
-
/* Reduced motion keeps the static unread marker and drops every flourish. */
|
|
156
|
-
@media (prefers-reduced-motion: reduce) {
|
|
157
|
-
.project-logs-pulse { animation: none; }
|
|
158
|
-
.project-logs-handle { transition: none; }
|
|
159
|
-
.project-logs-handle-notch { transition: none; }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
101
|
/* --- Ledger toolbar --- */
|
|
163
|
-
.project-logs-toolbar { display: flex; align-items: center; gap: 8px; padding:
|
|
102
|
+
.project-logs-toolbar { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border-subtle); flex: 0 0 auto; background: color-mix(in srgb, var(--bg-alt) 72%, transparent); }
|
|
164
103
|
.project-logs-toolbar.hidden { display: none; }
|
|
165
|
-
.project-logs-search { flex: 1; min-width: 0; height:
|
|
104
|
+
.project-logs-search { flex: 1; min-width: 0; height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-dimmer); }
|
|
166
105
|
.project-logs-search .lucide { width: 14px; flex: 0 0 auto; }
|
|
167
106
|
.project-logs-search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font: inherit; font-size: 12px; }
|
|
168
|
-
.project-logs-
|
|
107
|
+
.project-logs-search:focus-within, .project-logs-filter:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent); border-color: var(--accent); }
|
|
108
|
+
.project-logs-filter { height: 36px; max-width: 42%; padding: 0 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-secondary); font: inherit; font-size: 12px; cursor: pointer; }
|
|
169
109
|
|
|
170
110
|
/* --- Ledger list: full pane, chronological, commit-history feel --- */
|
|
171
|
-
.project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding:
|
|
111
|
+
.project-logs-list { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 0 18px; }
|
|
172
112
|
.project-logs-list.hidden { display: none; }
|
|
173
|
-
.project-log-row { width: 100%; display: block; padding:
|
|
174
|
-
.project-log-row:hover { background:
|
|
113
|
+
.project-log-row { width: 100%; display: block; padding: 15px 18px 14px; border: 0; border-bottom: 1px solid var(--border-subtle); border-left: 2px solid transparent; background: transparent; color: inherit; text-align: left; cursor: pointer; transition: background 0.14s ease, border-color 0.14s ease; }
|
|
114
|
+
.project-log-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); border-left-color: var(--accent); }
|
|
115
|
+
.project-log-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
|
|
175
116
|
.project-log-row:last-child { border-bottom: 0; }
|
|
176
117
|
.project-log-row-head { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
|
|
177
|
-
.project-log-row-title { flex: 1; min-width: 0; font-size:
|
|
178
|
-
.project-log-row-summary { margin:
|
|
179
|
-
.project-log-row-meta { margin-top:
|
|
180
|
-
.project-logs-list-empty { padding:
|
|
118
|
+
.project-log-row-title { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
|
|
119
|
+
.project-log-row-summary { margin: 7px 0 0; color: var(--text-secondary); font-size: 12.5px; line-height: 1.55; }
|
|
120
|
+
.project-log-row-meta { margin-top: 8px; color: var(--text-dimmer); font-size: 10.5px; font-family: var(--font-mono); }
|
|
121
|
+
.project-logs-list-empty { max-width: 320px; margin: 54px auto; padding: 20px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); color: var(--text-dimmer); font-size: 12px; text-align: center; line-height: 1.7; }
|
|
122
|
+
.project-logs-list[aria-busy="true"] .project-logs-list-empty { color: var(--text-secondary); font-family: var(--font-mono); }
|
|
181
123
|
|
|
182
124
|
/* Category chip and priority signal. Categories are project-defined, so the
|
|
183
125
|
chip is deliberately neutral: no per-category selectors and no exhaustive
|
|
@@ -191,13 +133,13 @@
|
|
|
191
133
|
/* --- Entry detail: full pane, replaces the list --- */
|
|
192
134
|
.project-logs-detail { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; }
|
|
193
135
|
.project-logs-detail.hidden { display: none; }
|
|
194
|
-
.project-log-document { max-width:
|
|
136
|
+
.project-log-document { max-width: 760px; margin: 0 auto; padding: 32px 28px 52px; }
|
|
195
137
|
#project-logs-panel.panel-fullscreen .project-log-document { padding: clamp(28px, 5vw, 64px); }
|
|
196
138
|
.project-log-doc-chips { display: flex; align-items: center; gap: 7px; margin-bottom: 10px; }
|
|
197
|
-
.project-log-document h1 { margin: 0 0
|
|
198
|
-
.project-log-doc-summary { margin: 0 0
|
|
199
|
-
.project-log-byline { margin: 0 0
|
|
200
|
-
.project-log-markdown { line-height: 1.
|
|
139
|
+
.project-log-document h1 { margin: 0 0 10px; color: var(--text); font-family: var(--font-display); font-size: clamp(24px, 2.8vw, 34px); letter-spacing: -0.025em; line-height: 1.12; }
|
|
140
|
+
.project-log-doc-summary { margin: 0 0 13px; color: var(--text-secondary); font-size: 14px; line-height: 1.6; }
|
|
141
|
+
.project-log-byline { margin: 0 0 25px; padding-bottom: 18px; border-bottom: 2px solid var(--border-subtle); color: var(--text-dimmer); font-size: 11px; font-family: var(--font-mono); }
|
|
142
|
+
.project-log-markdown { font-size: 14px; line-height: 1.75; }
|
|
201
143
|
|
|
202
144
|
/* --- Discussion --- */
|
|
203
145
|
.project-log-discussion { margin-top: 30px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
|
|
@@ -230,5 +172,9 @@
|
|
|
230
172
|
|
|
231
173
|
@media (max-width: 768px) {
|
|
232
174
|
.project-logs-subtitle { display: none; }
|
|
233
|
-
.project-
|
|
175
|
+
.project-logs-topbar { gap: 8px; padding: 0 12px; }
|
|
176
|
+
.project-logs-toolbar { padding: 10px 12px; }
|
|
177
|
+
.project-logs-filter { max-width: 44%; }
|
|
178
|
+
.project-log-row { padding: 14px 14px 13px; }
|
|
179
|
+
.project-log-document { padding: 24px 18px 38px; }
|
|
234
180
|
}
|
|
@@ -159,12 +159,12 @@ function renderRow(entry, onOpen) {
|
|
|
159
159
|
return row;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export function renderList(listEl, entries, onOpen) {
|
|
162
|
+
export function renderList(listEl, entries, onOpen, emptyState) {
|
|
163
163
|
listEl.innerHTML = "";
|
|
164
164
|
if (!entries.length) {
|
|
165
165
|
var empty = document.createElement("div");
|
|
166
166
|
empty.className = "project-logs-list-empty";
|
|
167
|
-
empty.textContent = "No logs yet. This project's agent sessions record decisions and work here.";
|
|
167
|
+
empty.textContent = emptyState || "No logs yet. This project's agent sessions record decisions and work here.";
|
|
168
168
|
listEl.appendChild(empty);
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
@@ -15,15 +15,10 @@ import { getWs } from './ws-ref.js';
|
|
|
15
15
|
import { refreshIcons } from './icons.js';
|
|
16
16
|
import { showToast } from './utils.js';
|
|
17
17
|
import { closeScheduler } from './scheduler.js';
|
|
18
|
-
import { hideNotes } from './sticky-notes.js';
|
|
19
18
|
import { closeNotesBrowser } from './sticky-notes-browser.js';
|
|
20
19
|
import { closeFileViewer } from './filebrowser.js';
|
|
21
20
|
import { closeTerminal } from './terminal.js';
|
|
22
21
|
import { renderFilter, renderList, renderDetail } from './project-logs-render.js';
|
|
23
|
-
import {
|
|
24
|
-
initProjectLogsAmbient, bindPanelHover, noteCanonicalUpdate,
|
|
25
|
-
acknowledgeUpdates, resetAmbient, syncAmbient, hidePreview, pinFromPreview
|
|
26
|
-
} from './project-logs-ambient.js';
|
|
27
22
|
|
|
28
23
|
var panel = null;
|
|
29
24
|
var listEl = null;
|
|
@@ -34,6 +29,37 @@ var backBtn = null;
|
|
|
34
29
|
var searchTimer = null;
|
|
35
30
|
var requestCounter = 0;
|
|
36
31
|
|
|
32
|
+
function syncUnreadBadge() {
|
|
33
|
+
var button = document.getElementById("project-logs-btn");
|
|
34
|
+
if (!button) return;
|
|
35
|
+
var unread = store.get('projectLogsUnread') || 0;
|
|
36
|
+
button.classList.toggle("project-logs-unread", unread > 0);
|
|
37
|
+
var badge = document.getElementById("project-logs-count");
|
|
38
|
+
if (!badge) return;
|
|
39
|
+
badge.textContent = unread > 0 ? String(unread) : "";
|
|
40
|
+
badge.classList.toggle("hidden", unread < 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function acknowledgeUpdates() {
|
|
44
|
+
if (!store.get('projectLogsUnread')) return;
|
|
45
|
+
store.set({ projectLogsUnread: 0 });
|
|
46
|
+
syncUnreadBadge();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function noteCanonicalUpdate(msg) {
|
|
50
|
+
if (!msg || !msg.ref || !msg.revision) return false;
|
|
51
|
+
var seen = store.get('projectLogsSeenRevisions') || {};
|
|
52
|
+
if (seen[msg.ref] >= msg.revision) return false;
|
|
53
|
+
var next = Object.assign({}, seen);
|
|
54
|
+
next[msg.ref] = msg.revision;
|
|
55
|
+
store.set({
|
|
56
|
+
projectLogsSeenRevisions: next,
|
|
57
|
+
projectLogsUnread: store.get('projectLogsOpen') ? 0 : (store.get('projectLogsUnread') || 0) + 1,
|
|
58
|
+
});
|
|
59
|
+
syncUnreadBadge();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
function nextRequestId(prefix) {
|
|
38
64
|
requestCounter += 1;
|
|
39
65
|
return prefix + "-" + Date.now() + "-" + requestCounter;
|
|
@@ -80,11 +106,7 @@ function ensurePanel() {
|
|
|
80
106
|
backBtn = panel.querySelector("#project-logs-back");
|
|
81
107
|
|
|
82
108
|
panel.querySelector("#project-logs-close").addEventListener("click", closeProjectLogs);
|
|
83
|
-
backBtn.addEventListener("click",
|
|
84
|
-
// Any meaningful interaction inside a preview commits to it.
|
|
85
|
-
searchEl.addEventListener("focus", pinOnInteraction);
|
|
86
|
-
panel.addEventListener("pointerdown", pinOnInteraction);
|
|
87
|
-
bindPanelHover(panel);
|
|
109
|
+
backBtn.addEventListener("click", showList);
|
|
88
110
|
panel.querySelector("#project-logs-wide").addEventListener("click", function () {
|
|
89
111
|
applyWindowState(!store.get('projectLogsWide'), store.get('projectLogsFullscreen'));
|
|
90
112
|
});
|
|
@@ -98,24 +120,6 @@ function ensurePanel() {
|
|
|
98
120
|
refreshIcons();
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
// A preview reveals the pane without committing to it: no acknowledgement, no
|
|
102
|
-
// open state, and no list refresh beyond what is already on screen.
|
|
103
|
-
function revealPreview() {
|
|
104
|
-
ensurePanel();
|
|
105
|
-
if (!panel) return;
|
|
106
|
-
panel.classList.remove("hidden");
|
|
107
|
-
panel.classList.add("project-logs-previewing");
|
|
108
|
-
applyWindowState(store.get('projectLogsWide'), false);
|
|
109
|
-
if (!store.get('projectLogsEntries') || !store.get('projectLogsEntries').length) requestList();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function hideRevealedPreview() {
|
|
113
|
-
if (!panel) return;
|
|
114
|
-
panel.classList.remove("project-logs-previewing");
|
|
115
|
-
if (store.get('projectLogsOpen')) return;
|
|
116
|
-
panel.classList.add("hidden");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
123
|
// Width and fullscreen are transient view state, so they live in the store like
|
|
120
124
|
// every other mutable UI value. `panel-fullscreen` is the shared class the
|
|
121
125
|
// document viewer and terminal already use.
|
|
@@ -147,13 +151,6 @@ function showList() {
|
|
|
147
151
|
listEl.scrollTop = store.get('projectLogsListScroll') || 0;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
// A preview becomes a real open the moment the user does something with it.
|
|
151
|
-
function pinOnInteraction() {
|
|
152
|
-
if (store.get('projectLogsOpen')) return;
|
|
153
|
-
if (!store.get('projectLogsPreview')) return;
|
|
154
|
-
pinFromPreview();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
154
|
function showDetail(entry) {
|
|
158
155
|
if (!panel || !entry) return;
|
|
159
156
|
store.set({ projectLogsListScroll: listEl.scrollTop, projectLogsView: "detail", projectLogsSelectedRef: entry.ref });
|
|
@@ -170,12 +167,21 @@ function showDetail(entry) {
|
|
|
170
167
|
function requestList() {
|
|
171
168
|
var requestId = nextRequestId("list-logs");
|
|
172
169
|
store.set({ projectLogsListRequestId: requestId });
|
|
173
|
-
|
|
170
|
+
if (listEl && !store.get('projectLogsEntries').length) {
|
|
171
|
+
listEl.setAttribute("aria-busy", "true");
|
|
172
|
+
renderList(listEl, [], requestEntry, "Loading the project ledger...");
|
|
173
|
+
}
|
|
174
|
+
if (!send({
|
|
174
175
|
type: "project_logs_list",
|
|
175
176
|
requestId: requestId,
|
|
176
177
|
query: searchEl ? searchEl.value.trim() : "",
|
|
177
178
|
category: store.get('projectLogsCategory') || "",
|
|
178
|
-
})
|
|
179
|
+
})) {
|
|
180
|
+
if (listEl) {
|
|
181
|
+
listEl.removeAttribute("aria-busy");
|
|
182
|
+
renderList(listEl, [], requestEntry, "Logs are unavailable while disconnected.");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
function requestEntry(ref) {
|
|
@@ -185,7 +191,6 @@ function requestEntry(ref) {
|
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
function submitComment(ref, body, statusEl, inputEl) {
|
|
188
|
-
pinOnInteraction();
|
|
189
194
|
var requestId = nextRequestId("comment-log");
|
|
190
195
|
store.set({ projectLogsCommentRequestId: requestId, projectLogsCommentInput: inputEl || null });
|
|
191
196
|
if (!send({ type: "project_log_comment", requestId: requestId, ref: ref, body: body })) {
|
|
@@ -197,7 +202,6 @@ function submitComment(ref, body, statusEl, inputEl) {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
function applyFilter(category) {
|
|
200
|
-
pinOnInteraction();
|
|
201
205
|
store.set({ projectLogsCategory: category || "" });
|
|
202
206
|
requestList();
|
|
203
207
|
}
|
|
@@ -211,18 +215,13 @@ export function initProjectLogs() {
|
|
|
211
215
|
if (store.get('projectLogsOpen')) closeProjectLogs();
|
|
212
216
|
else openProjectLogs();
|
|
213
217
|
});
|
|
214
|
-
|
|
215
|
-
reveal: revealPreview,
|
|
216
|
-
hide: hideRevealedPreview,
|
|
217
|
-
pin: openProjectLogs,
|
|
218
|
-
});
|
|
218
|
+
syncUnreadBadge();
|
|
219
219
|
store.subscribe(function (state, previous) {
|
|
220
220
|
if (state.currentSlug === previous.currentSlug) return;
|
|
221
221
|
closeProjectLogs();
|
|
222
222
|
applyWindowState(false, false);
|
|
223
223
|
// A different project must never inherit the previous project's unread
|
|
224
|
-
// state
|
|
225
|
-
resetAmbient();
|
|
224
|
+
// state.
|
|
226
225
|
store.set({
|
|
227
226
|
projectLogsEntries: [],
|
|
228
227
|
projectLogsSelectedRef: null,
|
|
@@ -231,8 +230,11 @@ export function initProjectLogs() {
|
|
|
231
230
|
projectLogsListScroll: 0,
|
|
232
231
|
projectLogsListRequestId: null,
|
|
233
232
|
projectLogsReadRequestId: null,
|
|
234
|
-
projectLogsCommentRequestId: null
|
|
233
|
+
projectLogsCommentRequestId: null,
|
|
234
|
+
projectLogsUnread: 0,
|
|
235
|
+
projectLogsSeenRevisions: {}
|
|
235
236
|
});
|
|
237
|
+
syncUnreadBadge();
|
|
236
238
|
});
|
|
237
239
|
}
|
|
238
240
|
|
|
@@ -243,37 +245,31 @@ export function openProjectLogs() {
|
|
|
243
245
|
if (!panel) return;
|
|
244
246
|
closeScheduler();
|
|
245
247
|
closeNotesBrowser();
|
|
246
|
-
hideNotes();
|
|
247
248
|
// Claim the single right workbench slot.
|
|
248
249
|
try { closeFileViewer(); } catch (e) {}
|
|
249
250
|
try { closeTerminal(); } catch (e) {}
|
|
250
251
|
panel.classList.remove("hidden");
|
|
251
|
-
panel.classList.remove("project-logs-previewing");
|
|
252
252
|
applyWindowState(store.get('projectLogsWide'), false);
|
|
253
253
|
var button = document.getElementById("project-logs-btn");
|
|
254
254
|
if (button) button.classList.add("active");
|
|
255
|
-
store.set({ projectLogsOpen: true
|
|
256
|
-
// Opening the
|
|
257
|
-
// nowhere else.
|
|
255
|
+
store.set({ projectLogsOpen: true });
|
|
256
|
+
// Opening through the explicit tool button acknowledges the live badge.
|
|
258
257
|
acknowledgeUpdates();
|
|
259
258
|
showList();
|
|
260
259
|
requestList();
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
export function closeProjectLogs() {
|
|
264
|
-
hidePreview();
|
|
265
263
|
if (!store.get('projectLogsOpen')) return;
|
|
266
264
|
if (panel) {
|
|
267
265
|
panel.classList.add("hidden");
|
|
268
|
-
panel.classList.remove("project-logs-previewing");
|
|
269
266
|
}
|
|
270
267
|
// Fullscreen is always dropped on close so the next open is a bounded pane
|
|
271
268
|
// and never silently hides the conversation.
|
|
272
269
|
applyWindowState(store.get('projectLogsWide'), false);
|
|
273
270
|
var button = document.getElementById("project-logs-btn");
|
|
274
271
|
if (button) button.classList.remove("active");
|
|
275
|
-
store.set({ projectLogsOpen: false
|
|
276
|
-
syncAmbient();
|
|
272
|
+
store.set({ projectLogsOpen: false });
|
|
277
273
|
}
|
|
278
274
|
|
|
279
275
|
// --- Server messages -----------------------------------------------------
|
|
@@ -282,10 +278,13 @@ export function handleProjectLogsState(msg) {
|
|
|
282
278
|
if (msg.requestId && msg.requestId !== store.get('projectLogsListRequestId')) return;
|
|
283
279
|
store.set({ projectLogsEntries: msg.entries || [] });
|
|
284
280
|
if (!listEl) return;
|
|
281
|
+
listEl.removeAttribute("aria-busy");
|
|
285
282
|
if (filterEl && Array.isArray(msg.categories)) {
|
|
286
283
|
renderFilter(filterEl, msg.categories, store.get('projectLogsCategory'), applyFilter);
|
|
287
284
|
}
|
|
288
|
-
|
|
285
|
+
var isFiltered = !!((searchEl && searchEl.value.trim()) || store.get('projectLogsCategory'));
|
|
286
|
+
renderList(listEl, msg.entries || [], requestEntry,
|
|
287
|
+
isFiltered ? "No entries match this search or category." : "No logs yet. This project's agent sessions record decisions and work here.");
|
|
289
288
|
if (store.get('projectLogsView') !== "detail") listEl.scrollTop = store.get('projectLogsListScroll') || 0;
|
|
290
289
|
}
|
|
291
290
|
|
|
@@ -309,8 +308,8 @@ export function handleProjectLogCommented(msg) {
|
|
|
309
308
|
requestList();
|
|
310
309
|
}
|
|
311
310
|
|
|
312
|
-
// A canonical revision landed. This marks the
|
|
313
|
-
// focuses, or scrolls
|
|
311
|
+
// A canonical revision landed. This marks the explicit button only; it never
|
|
312
|
+
// opens, reveals, focuses, or scrolls the pane.
|
|
314
313
|
export function handleProjectLogUpdated(msg) {
|
|
315
314
|
noteCanonicalUpdate(msg);
|
|
316
315
|
// Refresh the ledger only when it is already on screen, so the row moves to
|
|
@@ -325,5 +324,9 @@ export function handleProjectLogsError(msg) {
|
|
|
325
324
|
store.get('projectLogsCommentRequestId')
|
|
326
325
|
];
|
|
327
326
|
if (msg.requestId && pending.indexOf(msg.requestId) === -1) return;
|
|
327
|
+
if (msg.requestId && msg.requestId === store.get('projectLogsListRequestId') && listEl) {
|
|
328
|
+
listEl.removeAttribute("aria-busy");
|
|
329
|
+
renderList(listEl, [], requestEntry, "The ledger could not be loaded. Try opening Logs again.");
|
|
330
|
+
}
|
|
328
331
|
showToast(msg.message || "Project Logs could not complete the request.", "error");
|
|
329
332
|
}
|
|
@@ -16,3 +16,11 @@ export function findSplitGroup(groups, memberIds) {
|
|
|
16
16
|
}
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
export function isConfiguredWorker(groups, sessionId) {
|
|
21
|
+
var list = groups || [];
|
|
22
|
+
for (var i = 0; i < list.length; i++) {
|
|
23
|
+
if (list[i].pair && list[i].pair.workerId === sessionId) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
@@ -7,7 +7,7 @@ import { iconHtml, refreshIcons } from './icons.js';
|
|
|
7
7
|
import { detachTuiView } from './session-tui-view.js';
|
|
8
8
|
import { formatTokens } from './app-panels.js';
|
|
9
9
|
import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
10
|
-
import { groupedSessionIds, findSplitGroup } from './split-group-helpers.js';
|
|
10
|
+
import { groupedSessionIds, findSplitGroup, isConfiguredWorker } from './split-group-helpers.js';
|
|
11
11
|
import { showConfirm } from './app-misc.js';
|
|
12
12
|
import { syncPairChrome } from './split-pair-ui.js';
|
|
13
13
|
import { presentMarkdownEdit } from './filebrowser.js';
|
|
@@ -159,7 +159,10 @@ export function syncPaneTitles() {
|
|
|
159
159
|
function updatePaneFullAccessButton(button, session) {
|
|
160
160
|
if (!button) return;
|
|
161
161
|
var mode = session && (session.runtimeMode || session.mode || "gui");
|
|
162
|
-
var
|
|
162
|
+
var worker = !!session && isConfiguredWorker(store.get('splitGroups'), session.id);
|
|
163
|
+
// Permission policy belongs to the Driver. A configured Worker inherits it
|
|
164
|
+
// server-side and must not present a second, misleading control.
|
|
165
|
+
var visible = !!session && !worker && !store.get('skipPermsEnabled') && mode === "gui";
|
|
163
166
|
var enabled = visible && session.permissionMode === "bypassPermissions";
|
|
164
167
|
button.classList.toggle("hidden", !visible);
|
|
165
168
|
button.classList.toggle("active", enabled);
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -12,6 +12,7 @@ var { createMessageQueue } = require("./sdk-message-queue");
|
|
|
12
12
|
var { attachMessageProcessor } = require("./sdk-message-processor");
|
|
13
13
|
var homeDebateToolPolicy = require("./home-debate-tool-policy");
|
|
14
14
|
var yoke = require("./yoke");
|
|
15
|
+
var sessionTitlePolicy = require("./session-title-policy");
|
|
15
16
|
|
|
16
17
|
// Extract serializable tool descriptors from MCP server instances.
|
|
17
18
|
// Used for IPC to worker processes (McpSdkServerConfigWithInstance is not serializable).
|
|
@@ -663,6 +664,14 @@ function createSDKBridge(opts) {
|
|
|
663
664
|
return { behavior: "allow", updatedInput: input };
|
|
664
665
|
}
|
|
665
666
|
|
|
667
|
+
// Project Logs are exact-session, project-bound tools whose authority is
|
|
668
|
+
// enforced again by the server binding. Canonical changes are append-only
|
|
669
|
+
// revisions, and the surface exposes no destructive delete operation, so
|
|
670
|
+
// a second provider permission prompt adds no additional safety.
|
|
671
|
+
if (toolName.indexOf("mcp__clay-logs__") === 0) {
|
|
672
|
+
return { behavior: "allow", updatedInput: input };
|
|
673
|
+
}
|
|
674
|
+
|
|
666
675
|
// This tool only prepares a read-only UI projection for the document the
|
|
667
676
|
// agent is about to edit. The subsequent Edit/Write keeps its own normal
|
|
668
677
|
// permission behavior.
|
|
@@ -1608,24 +1617,17 @@ function createSDKBridge(opts) {
|
|
|
1608
1617
|
sessionToolDefs = sessionToolDefs.concat(yoke.userInput.fallbackToolDefs(sessionUserInputHandler));
|
|
1609
1618
|
}
|
|
1610
1619
|
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1620
|
+
// Give fresh queries an immediate provisional label. The visible user
|
|
1621
|
+
// message in history wins over `text`, which may contain an internal
|
|
1622
|
+
// provider wrapper rather than what the person typed.
|
|
1614
1623
|
// Only applied to NEW sessions (no cliSessionId yet) — when resuming,
|
|
1615
1624
|
// the SDK ignores Options.title in favor of the persisted title.
|
|
1616
1625
|
var initialTitle = null;
|
|
1626
|
+
var initialTitleProvisional = false;
|
|
1617
1627
|
if (!session.cliSessionId && !session.titleManuallySet && !session.titleAutoGenerated) {
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
} else if (typeof text === "string") {
|
|
1622
|
-
// Derive a quick first-line snippet from the user's first message.
|
|
1623
|
-
// Skip if too short to be meaningful — fall back to autoGenerateTitle.
|
|
1624
|
-
var firstLine = text.replace(/\s+/g, " ").trim();
|
|
1625
|
-
if (firstLine.length >= 10) {
|
|
1626
|
-
initialTitle = firstLine.length > 60 ? firstLine.substring(0, 60) : firstLine;
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1628
|
+
var initialTitleChoice = sessionTitlePolicy.selectInitialTitle(session, text);
|
|
1629
|
+
initialTitle = initialTitleChoice.title;
|
|
1630
|
+
initialTitleProvisional = initialTitleChoice.provisional;
|
|
1629
1631
|
}
|
|
1630
1632
|
|
|
1631
1633
|
var queryOpts = {
|
|
@@ -1720,16 +1722,16 @@ function createSDKBridge(opts) {
|
|
|
1720
1722
|
try {
|
|
1721
1723
|
handle = await sessionAdapter.createQuery(queryOpts);
|
|
1722
1724
|
console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
|
|
1723
|
-
// SDK accepted the explicit title
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1725
|
+
// SDK accepted the explicit title. Provisional labels remain eligible
|
|
1726
|
+
// for semantic generation after the first Home exchange (or the normal
|
|
1727
|
+
// project threshold); seeded titles remain fixed.
|
|
1726
1728
|
if (initialTitle && !session.title) {
|
|
1727
1729
|
session.title = initialTitle;
|
|
1728
|
-
session.titleAutoGenerated =
|
|
1730
|
+
session.titleAutoGenerated = !initialTitleProvisional;
|
|
1729
1731
|
sm.saveSessionFile(session);
|
|
1730
1732
|
sm.broadcastSessionList();
|
|
1731
1733
|
} else if (initialTitle && session.title === initialTitle) {
|
|
1732
|
-
session.titleAutoGenerated =
|
|
1734
|
+
session.titleAutoGenerated = !initialTitleProvisional;
|
|
1733
1735
|
sm.saveSessionFile(session);
|
|
1734
1736
|
}
|
|
1735
1737
|
} catch (e) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
var usersModule = require("./users");
|
|
2
2
|
var yoke = require("./yoke");
|
|
3
3
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
4
|
+
var sessionTitlePolicy = require("./session-title-policy");
|
|
5
|
+
|
|
6
|
+
var OVERLOAD_RETRY_LIMIT = 3;
|
|
4
7
|
|
|
5
8
|
function attachMessageProcessor(ctx) {
|
|
6
9
|
var sm = ctx.sm;
|
|
@@ -23,8 +26,6 @@ function attachMessageProcessor(ctx) {
|
|
|
23
26
|
var saveImageFile = ctx.saveImageFile;
|
|
24
27
|
var getSessionLinuxUser = ctx.getSessionLinuxUser || function () { return null; };
|
|
25
28
|
|
|
26
|
-
var AUTO_TITLE_TURN_THRESHOLD = 2;
|
|
27
|
-
|
|
28
29
|
function getMateIdForNotification() {
|
|
29
30
|
if (!isMate) return null;
|
|
30
31
|
if (typeof slug === "string" && slug.indexOf("mate-") === 0) {
|
|
@@ -488,6 +489,8 @@ function attachMessageProcessor(ctx) {
|
|
|
488
489
|
}
|
|
489
490
|
|
|
490
491
|
} else if (parsed.yokeType === "result") {
|
|
492
|
+
var overloadAbortRequested = !!session._overloadAbortRequested;
|
|
493
|
+
session._overloadAbortRequested = false;
|
|
491
494
|
session.blocks = {};
|
|
492
495
|
session.sentToolResults = {};
|
|
493
496
|
session.pendingPermissions = {};
|
|
@@ -533,7 +536,9 @@ function attachMessageProcessor(ctx) {
|
|
|
533
536
|
session._awaitingTurnResult = false;
|
|
534
537
|
session._queuedTurnCount = 0;
|
|
535
538
|
onProcessingChanged();
|
|
536
|
-
|
|
539
|
+
if (!overloadAbortRequested) {
|
|
540
|
+
sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
|
|
541
|
+
}
|
|
537
542
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
538
543
|
sm.broadcastSessionList();
|
|
539
544
|
return;
|
|
@@ -625,7 +630,7 @@ function attachMessageProcessor(ctx) {
|
|
|
625
630
|
sm.broadcastSessionList();
|
|
626
631
|
|
|
627
632
|
// Auto-generate title after N turns (skip if loop or already auto-generated)
|
|
628
|
-
if (session.turnCount ===
|
|
633
|
+
if (session.turnCount === sessionTitlePolicy.generationTurn(isMate)
|
|
629
634
|
&& !session.titleAutoGenerated
|
|
630
635
|
&& !session.titleManuallySet
|
|
631
636
|
&& !session.loop
|
|
@@ -820,9 +825,33 @@ function attachMessageProcessor(ctx) {
|
|
|
820
825
|
}
|
|
821
826
|
|
|
822
827
|
} else if (parsed.yokeType === "api_retry") {
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
828
|
+
// The Claude SDK may otherwise paint one persistent red error for every
|
|
829
|
+
// retry and remain busy for many minutes. Show one transient status, and
|
|
830
|
+
// stop a repeatedly overloaded turn after a small bounded number of
|
|
831
|
+
// attempts so the user regains control and can retry or change models.
|
|
832
|
+
var apiRetryAttempt = parsed.attempt || 1;
|
|
833
|
+
if (parsed.error === "overloaded") {
|
|
834
|
+
if (apiRetryAttempt === 1) {
|
|
835
|
+
session._overloadAbortRequested = false;
|
|
836
|
+
sendToSession(session, {
|
|
837
|
+
type: "system_info",
|
|
838
|
+
text: "The model is overloaded. Retrying briefly...",
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (apiRetryAttempt >= OVERLOAD_RETRY_LIMIT && !session._overloadAbortRequested) {
|
|
842
|
+
session._overloadAbortRequested = true;
|
|
843
|
+
sendAndRecord(session, {
|
|
844
|
+
type: "error",
|
|
845
|
+
text: "The model remained overloaded after " + apiRetryAttempt +
|
|
846
|
+
" attempts, so this turn was stopped. Try again or choose another model.",
|
|
847
|
+
});
|
|
848
|
+
if (session.abortController && typeof session.abortController.abort === "function") {
|
|
849
|
+
try { session.abortController.abort(); } catch (e) {}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} else if (apiRetryAttempt === 1) {
|
|
853
|
+
sendToSession(session, { type: "system_info", text: "The model request failed temporarily. Retrying..." });
|
|
854
|
+
}
|
|
826
855
|
|
|
827
856
|
} else if (parsed.yokeType === "commands_changed") {
|
|
828
857
|
// Mid-session command list change: rebuild the union with skills (same as
|
package/lib/server-home-chat.js
CHANGED
|
@@ -9,6 +9,7 @@ var attachHomeMateCreation = require("./server-home-mate-creation").attachHomeMa
|
|
|
9
9
|
var attachHomeDebates = require("./server-home-debates").attachHomeDebates, attachHomeClayEntry = require("./server-home-clay-entry").attachHomeClayEntry;
|
|
10
10
|
var homeChatEvents = require("./server-home-chat-events");
|
|
11
11
|
var homeCapsuleCreation = require("./server-home-capsule-creation");
|
|
12
|
+
var sessionTitlePolicy = require("./session-title-policy");
|
|
12
13
|
var historyToHomeChat = homeChatEvents.historyToHomeChat;
|
|
13
14
|
var transformEvent = homeChatEvents.transformEvent;
|
|
14
15
|
function attachHomeChat(deps) {
|
|
@@ -84,6 +85,10 @@ function attachHomeChat(deps) {
|
|
|
84
85
|
var result = [];
|
|
85
86
|
sessionManager.sessions.forEach(function (session) {
|
|
86
87
|
if (!ownsSession(session, userId)) return;
|
|
88
|
+
if (sessionTitlePolicy.repairLegacySearchTitle(session)
|
|
89
|
+
&& typeof sessionManager.saveSessionFile === "function") {
|
|
90
|
+
sessionManager.saveSessionFile(session);
|
|
91
|
+
}
|
|
87
92
|
var cliSessionId = sessionString(session.cliSessionId, 512);
|
|
88
93
|
var createdAt = sessionTimestamp(session.createdAt);
|
|
89
94
|
var lastActivity = sessionTimestamp(session.lastActivity) || createdAt;
|