clay-server 2.7.0 → 2.7.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.
- package/lib/notes.js +1 -1
- package/lib/project.js +1 -1
- package/lib/public/app.js +15 -6
- package/lib/public/css/menus.css +5 -0
- package/lib/public/css/mobile-nav.css +15 -15
- package/lib/public/css/title-bar.css +6 -6
- package/lib/public/index.html +2 -1
- package/lib/public/modules/input.js +13 -3
- package/lib/sdk-bridge.js +8 -0
- package/lib/sessions.js +6 -3
- package/lib/utils.js +49 -3
- package/package.json +1 -1
package/lib/notes.js
CHANGED
|
@@ -8,8 +8,8 @@ function createNotesManager(opts) {
|
|
|
8
8
|
var cwd = opts.cwd;
|
|
9
9
|
|
|
10
10
|
// Storage path: ~/.clay/notes/{encodedCwd}.json
|
|
11
|
-
var encodedCwd = utils.encodeCwd(cwd);
|
|
12
11
|
var notesDir = path.join(config.CONFIG_DIR, "notes");
|
|
12
|
+
var encodedCwd = utils.resolveEncodedFile(notesDir, cwd, ".json");
|
|
13
13
|
var notesFile = path.join(notesDir, encodedCwd + ".json");
|
|
14
14
|
|
|
15
15
|
// In-memory cache
|
package/lib/project.js
CHANGED
|
@@ -253,8 +253,8 @@ function createProjectContext(opts) {
|
|
|
253
253
|
// Loop state persistence
|
|
254
254
|
var _loopConfig = require("./config");
|
|
255
255
|
var _loopUtils = require("./utils");
|
|
256
|
-
var _loopEncodedCwd = _loopUtils.encodeCwd(cwd);
|
|
257
256
|
var _loopDir = path.join(_loopConfig.CONFIG_DIR, "loops");
|
|
257
|
+
var _loopEncodedCwd = _loopUtils.resolveEncodedFile(_loopDir, cwd, ".json");
|
|
258
258
|
var _loopStatePath = path.join(_loopDir, _loopEncodedCwd + ".json");
|
|
259
259
|
|
|
260
260
|
function saveLoopState() {
|
package/lib/public/app.js
CHANGED
|
@@ -560,7 +560,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
|
|
|
560
560
|
} else if (status === "processing") {
|
|
561
561
|
if (dot) { dot.classList.add("connected"); dot.classList.add("processing"); }
|
|
562
562
|
processing = true;
|
|
563
|
-
setSendBtnMode("stop");
|
|
563
|
+
setSendBtnMode(inputEl.value.trim() ? "send" : "stop");
|
|
564
564
|
} else {
|
|
565
565
|
connected = false;
|
|
566
566
|
sendBtn.disabled = true;
|
|
@@ -1052,13 +1052,21 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
|
|
|
1052
1052
|
contextTurnsEl.textContent = String(contextData.turns);
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
|
-
function accumulateContext(cost, usage, modelUsage) {
|
|
1055
|
+
function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
|
|
1056
1056
|
if (cost != null) contextData.cost += cost;
|
|
1057
1057
|
// Use latest turn values (not cumulative) since each turn's input_tokens
|
|
1058
1058
|
// already includes the full conversation context up to that point
|
|
1059
1059
|
if (usage) {
|
|
1060
|
-
|
|
1061
|
-
|
|
1060
|
+
// Prefer per-call input_tokens from the last stream message_start event
|
|
1061
|
+
// when available — result.usage.input_tokens sums all API calls in a turn,
|
|
1062
|
+
// inflating context usage when tools are involved.
|
|
1063
|
+
// Falls back to the summed value for setups that don't emit message_start.
|
|
1064
|
+
if (lastStreamInputTokens) {
|
|
1065
|
+
contextData.input = lastStreamInputTokens;
|
|
1066
|
+
} else {
|
|
1067
|
+
contextData.input = (usage.input_tokens || usage.inputTokens || 0)
|
|
1068
|
+
+ (usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
|
|
1069
|
+
}
|
|
1062
1070
|
contextData.output = usage.output_tokens || usage.outputTokens || 0;
|
|
1063
1071
|
contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
1064
1072
|
contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
@@ -1880,7 +1888,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
|
|
|
1880
1888
|
replayingHistory = false;
|
|
1881
1889
|
// Restore accurate context data from the last result in full history
|
|
1882
1890
|
if (msg.lastUsage || msg.lastModelUsage) {
|
|
1883
|
-
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage);
|
|
1891
|
+
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
|
|
1884
1892
|
}
|
|
1885
1893
|
updateContextPanel();
|
|
1886
1894
|
updateUsagePanel();
|
|
@@ -2302,7 +2310,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
|
|
|
2302
2310
|
finalizeAssistantBlock();
|
|
2303
2311
|
addTurnMeta(msg.cost, msg.duration);
|
|
2304
2312
|
accumulateUsage(msg.cost, msg.usage);
|
|
2305
|
-
accumulateContext(msg.cost, msg.usage, msg.modelUsage);
|
|
2313
|
+
accumulateContext(msg.cost, msg.usage, msg.modelUsage, msg.lastStreamInputTokens);
|
|
2306
2314
|
break;
|
|
2307
2315
|
|
|
2308
2316
|
case "done":
|
|
@@ -2779,6 +2787,7 @@ import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modu
|
|
|
2779
2787
|
resetContextData: resetContextData,
|
|
2780
2788
|
showImageModal: showImageModal,
|
|
2781
2789
|
hideSuggestionChips: hideSuggestionChips,
|
|
2790
|
+
setSendBtnMode: setSendBtnMode,
|
|
2782
2791
|
});
|
|
2783
2792
|
|
|
2784
2793
|
// --- Notifications module (viewport, banners, notifications, debug, service worker) ---
|
package/lib/public/css/menus.css
CHANGED
|
@@ -507,6 +507,7 @@
|
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
#config-chip .lucide { width: 10px; height: 10px; }
|
|
510
|
+
#config-chip .config-chip-icon { display: none; }
|
|
510
511
|
#config-chip:hover { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.06); }
|
|
511
512
|
#config-chip.active { color: var(--text-secondary); background: rgba(var(--overlay-rgb),0.06); }
|
|
512
513
|
|
|
@@ -851,6 +852,10 @@
|
|
|
851
852
|
width: auto;
|
|
852
853
|
max-height: 60vh;
|
|
853
854
|
}
|
|
855
|
+
|
|
856
|
+
/* Config chip: icon-only on mobile */
|
|
857
|
+
#config-chip .config-chip-icon { display: inline; width: 14px; height: 14px; }
|
|
858
|
+
#config-chip-label { display: none; }
|
|
854
859
|
}
|
|
855
860
|
|
|
856
861
|
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
left: 0;
|
|
18
18
|
right: 0;
|
|
19
19
|
height: calc(56px + var(--safe-bottom));
|
|
20
|
+
padding-top: 1px;
|
|
20
21
|
padding-bottom: var(--safe-bottom);
|
|
21
22
|
background: var(--bg);
|
|
22
23
|
border-top: 1px solid var(--border);
|
|
23
24
|
display: flex;
|
|
24
|
-
align-items:
|
|
25
|
+
align-items: center;
|
|
25
26
|
justify-content: space-around;
|
|
26
27
|
z-index: 200;
|
|
27
28
|
}
|
|
@@ -88,33 +89,32 @@
|
|
|
88
89
|
|
|
89
90
|
.mobile-tab { position: relative; }
|
|
90
91
|
|
|
91
|
-
/* --- Center "+" button
|
|
92
|
+
/* --- Center "+" button --- */
|
|
92
93
|
.mobile-tab-new {
|
|
93
|
-
flex:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
border-radius: 50%;
|
|
97
|
-
background: var(--accent);
|
|
98
|
-
color: #fff;
|
|
94
|
+
flex: 1;
|
|
95
|
+
height: 100%;
|
|
96
|
+
background: none;
|
|
99
97
|
border: none;
|
|
100
98
|
display: flex;
|
|
101
99
|
align-items: center;
|
|
102
100
|
justify-content: center;
|
|
103
101
|
cursor: pointer;
|
|
104
|
-
margin-top: -12px;
|
|
105
|
-
box-shadow: 0 2px 12px rgba(var(--shadow-rgb), 0.25);
|
|
106
102
|
-webkit-tap-highlight-color: transparent;
|
|
107
|
-
transition: transform 0.1s, box-shadow 0.15s;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
105
|
.mobile-tab-new .lucide {
|
|
111
|
-
width:
|
|
112
|
-
height:
|
|
106
|
+
width: 20px;
|
|
107
|
+
height: 20px;
|
|
108
|
+
padding: 8px;
|
|
109
|
+
box-sizing: content-box;
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
background: var(--border);
|
|
112
|
+
color: #fff;
|
|
113
|
+
transition: transform 0.1s;
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
.mobile-tab-new:active {
|
|
116
|
+
.mobile-tab-new:active .lucide {
|
|
116
117
|
transform: scale(0.92);
|
|
117
|
-
box-shadow: 0 1px 6px rgba(var(--shadow-rgb), 0.2);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/* --- Mobile project list items (inside sidebar) --- */
|
|
@@ -131,21 +131,21 @@
|
|
|
131
131
|
z-index: 1;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/* Dark mode (unchecked) — thumb LEFT on
|
|
135
|
-
.theme-toggle-
|
|
136
|
-
.theme-toggle-
|
|
134
|
+
/* Dark mode (unchecked) — thumb LEFT on moon */
|
|
135
|
+
.theme-toggle-moon { color: var(--bg); } /* dark icon on lighter thumb */
|
|
136
|
+
.theme-toggle-sun { color: var(--text-muted); } /* light icon on dark track */
|
|
137
137
|
|
|
138
|
-
/* Light mode (checked) — thumb RIGHT on
|
|
138
|
+
/* Light mode (checked) — thumb RIGHT on sun */
|
|
139
139
|
.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-thumb {
|
|
140
140
|
left: 24px;
|
|
141
141
|
background: var(--bg-alt);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-
|
|
144
|
+
.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-moon {
|
|
145
145
|
color: var(--text-muted); /* darker icon on light track */
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-
|
|
148
|
+
.theme-toggle input:checked ~ .theme-toggle-track .theme-toggle-sun {
|
|
149
149
|
color: var(--text-dimmer); /* darker icon on light thumb */
|
|
150
150
|
}
|
|
151
151
|
|
package/lib/public/index.html
CHANGED
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
<label id="theme-toggle-btn" class="theme-toggle" title="Toggle dark/light mode">
|
|
61
61
|
<input type="checkbox" id="theme-toggle-check">
|
|
62
62
|
<span class="theme-toggle-track">
|
|
63
|
-
<span class="theme-toggle-icon theme-toggle-sun"><i data-lucide="sun"></i></span>
|
|
64
63
|
<span class="theme-toggle-icon theme-toggle-moon"><i data-lucide="moon"></i></span>
|
|
64
|
+
<span class="theme-toggle-icon theme-toggle-sun"><i data-lucide="sun"></i></span>
|
|
65
65
|
<span class="theme-toggle-thumb"></span>
|
|
66
66
|
</span>
|
|
67
67
|
</label>
|
|
@@ -262,6 +262,7 @@
|
|
|
262
262
|
<div id="input-bottom-right">
|
|
263
263
|
<div id="config-chip-wrap" class="hidden">
|
|
264
264
|
<button id="config-chip" title="Model, mode, and effort settings">
|
|
265
|
+
<i class="config-chip-icon" data-lucide="sliders-horizontal"></i>
|
|
265
266
|
<span id="config-chip-label"></span>
|
|
266
267
|
<i data-lucide="chevron-down"></i>
|
|
267
268
|
</button>
|
|
@@ -105,6 +105,10 @@ export function sendMessage() {
|
|
|
105
105
|
clearPendingImages();
|
|
106
106
|
autoResize();
|
|
107
107
|
ctx.inputEl.focus();
|
|
108
|
+
// Input cleared — switch back to stop mode if still processing
|
|
109
|
+
if (ctx.processing && ctx.setSendBtnMode) {
|
|
110
|
+
ctx.setSendBtnMode("stop");
|
|
111
|
+
}
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
export function autoResize() {
|
|
@@ -580,6 +584,10 @@ export function initInput(_ctx) {
|
|
|
580
584
|
} else {
|
|
581
585
|
hideSlashMenu();
|
|
582
586
|
}
|
|
587
|
+
// Toggle send/stop button based on input content during processing
|
|
588
|
+
if (ctx.processing && ctx.setSendBtnMode) {
|
|
589
|
+
ctx.setSendBtnMode(val.trim() ? "send" : "stop");
|
|
590
|
+
}
|
|
583
591
|
});
|
|
584
592
|
|
|
585
593
|
ctx.inputEl.addEventListener("compositionstart", function () { isComposing = true; });
|
|
@@ -639,13 +647,15 @@ export function initInput(_ctx) {
|
|
|
639
647
|
ctx.inputEl.setAttribute("enterkeyhint", "enter");
|
|
640
648
|
}
|
|
641
649
|
|
|
642
|
-
// Send/Stop button
|
|
650
|
+
// Send/Stop button — if input has text, always send; otherwise stop
|
|
643
651
|
ctx.sendBtn.addEventListener("click", function () {
|
|
652
|
+
if (ctx.inputEl.value.trim()) {
|
|
653
|
+
sendMessage();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
644
656
|
if (ctx.processing && ctx.connected) {
|
|
645
657
|
ctx.ws.send(JSON.stringify({ type: "stop" }));
|
|
646
|
-
return;
|
|
647
658
|
}
|
|
648
|
-
sendMessage();
|
|
649
659
|
});
|
|
650
660
|
ctx.sendBtn.addEventListener("dblclick", function (e) { e.preventDefault(); });
|
|
651
661
|
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -159,6 +159,11 @@ function createSDKBridge(opts) {
|
|
|
159
159
|
if (parsed.type === "stream_event" && parsed.event) {
|
|
160
160
|
var evt = parsed.event;
|
|
161
161
|
|
|
162
|
+
if (evt.type === "message_start" && evt.message && evt.message.usage) {
|
|
163
|
+
var u = evt.message.usage;
|
|
164
|
+
session.lastStreamInputTokens = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
162
167
|
if (evt.type === "content_block_start") {
|
|
163
168
|
var block = evt.content_block;
|
|
164
169
|
var idx = evt.index;
|
|
@@ -309,6 +314,8 @@ function createSDKBridge(opts) {
|
|
|
309
314
|
session.taskIdMap = {};
|
|
310
315
|
session.isProcessing = false;
|
|
311
316
|
onProcessingChanged();
|
|
317
|
+
var lastStreamInput = session.lastStreamInputTokens || null;
|
|
318
|
+
session.lastStreamInputTokens = null;
|
|
312
319
|
sendAndRecord(session, {
|
|
313
320
|
type: "result",
|
|
314
321
|
cost: parsed.total_cost_usd,
|
|
@@ -316,6 +323,7 @@ function createSDKBridge(opts) {
|
|
|
316
323
|
usage: parsed.usage || null,
|
|
317
324
|
modelUsage: parsed.modelUsage || null,
|
|
318
325
|
sessionId: parsed.session_id,
|
|
326
|
+
lastStreamInputTokens: lastStreamInput,
|
|
319
327
|
});
|
|
320
328
|
if (parsed.fast_mode_state) {
|
|
321
329
|
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fast_mode_state });
|
package/lib/sessions.js
CHANGED
|
@@ -16,8 +16,9 @@ function createSessionManager(opts) {
|
|
|
16
16
|
var skillNames = null; // Claude-only skills to filter from slash menu
|
|
17
17
|
|
|
18
18
|
// --- Session persistence (centralized in ~/.clay/sessions/{encoded-cwd}/) ---
|
|
19
|
-
var
|
|
20
|
-
var
|
|
19
|
+
var sessionsBase = path.join(config.CONFIG_DIR, "sessions");
|
|
20
|
+
var encodedCwd = utils.resolveEncodedDir(sessionsBase, cwd);
|
|
21
|
+
var sessionsDir = path.join(sessionsBase, encodedCwd);
|
|
21
22
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
22
23
|
|
|
23
24
|
// Auto-migrate sessions from legacy locations:
|
|
@@ -237,17 +238,19 @@ function createSessionManager(opts) {
|
|
|
237
238
|
var lastUsage = null;
|
|
238
239
|
var lastModelUsage = null;
|
|
239
240
|
var lastCost = null;
|
|
241
|
+
var lastStreamInputTokens = null;
|
|
240
242
|
for (var j = total - 1; j >= 0; j--) {
|
|
241
243
|
if (session.history[j].type === "result") {
|
|
242
244
|
var r = session.history[j];
|
|
243
245
|
lastUsage = r.usage || null;
|
|
244
246
|
lastModelUsage = r.modelUsage || null;
|
|
245
247
|
lastCost = r.cost != null ? r.cost : null;
|
|
248
|
+
lastStreamInputTokens = r.lastStreamInputTokens || null;
|
|
246
249
|
break;
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost });
|
|
253
|
+
send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens });
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
function switchSession(localId) {
|
package/lib/utils.js
CHANGED
|
@@ -2,17 +2,63 @@
|
|
|
2
2
|
* Shared utility functions.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
var fs = require("fs");
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Encode a cwd path into a filesystem-safe directory/file name.
|
|
7
|
-
* Replaces
|
|
8
|
-
*
|
|
9
|
+
* Replaces all non-alphanumeric characters with hyphens, matching
|
|
10
|
+
* Claude Code CLI's encoding logic exactly (/[^a-zA-Z0-9]/g -> "-").
|
|
9
11
|
*
|
|
10
|
-
* Example: "/Users/jon.
|
|
12
|
+
* Example: "/Users/jon.doe_42/my project" -> "-Users-jon-doe-42-my-project"
|
|
11
13
|
*/
|
|
12
14
|
function encodeCwd(cwd) {
|
|
15
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Legacy encoding (pre-#182 fix). Only slashes and dots were replaced.
|
|
20
|
+
* Used for fallback resolution of on-disk data written before the fix.
|
|
21
|
+
*/
|
|
22
|
+
function legacyEncodeCwd(cwd) {
|
|
13
23
|
return cwd.replace(/[\/\.]/g, "-");
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Try candidate encoded names against a base directory.
|
|
28
|
+
* Returns the first match that exists on disk, or the first candidate
|
|
29
|
+
* (newest encoding) if none exist yet.
|
|
30
|
+
*/
|
|
31
|
+
function resolveEncoded(baseDir, cwd, ext, checkFn) {
|
|
32
|
+
var newEncoded = encodeCwd(cwd);
|
|
33
|
+
var legacyEncoded = legacyEncodeCwd(cwd);
|
|
34
|
+
if (newEncoded === legacyEncoded) return newEncoded;
|
|
35
|
+
var full = baseDir + "/" + newEncoded + (ext || "");
|
|
36
|
+
try { if (checkFn(full)) return newEncoded; } catch (e) {}
|
|
37
|
+
var legacyFull = baseDir + "/" + legacyEncoded + (ext || "");
|
|
38
|
+
try { if (checkFn(legacyFull)) return legacyEncoded; } catch (e) {}
|
|
39
|
+
return newEncoded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve an encoded directory path with legacy fallback.
|
|
44
|
+
*/
|
|
45
|
+
function resolveEncodedDir(baseDir, cwd) {
|
|
46
|
+
return resolveEncoded(baseDir, cwd, "", function(p) {
|
|
47
|
+
return fs.statSync(p).isDirectory();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve an encoded file path with legacy fallback.
|
|
53
|
+
*/
|
|
54
|
+
function resolveEncodedFile(baseDir, cwd, ext) {
|
|
55
|
+
return resolveEncoded(baseDir, cwd, ext, function(p) {
|
|
56
|
+
return fs.statSync(p).isFile();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
16
60
|
module.exports = {
|
|
17
61
|
encodeCwd: encodeCwd,
|
|
62
|
+
resolveEncodedDir: resolveEncodedDir,
|
|
63
|
+
resolveEncodedFile: resolveEncodedFile,
|
|
18
64
|
};
|