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 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
- contextData.input = (usage.input_tokens || usage.inputTokens || 0)
1061
- + (usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0);
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) ---
@@ -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: flex-start;
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 (raised) --- */
92
+ /* --- Center "+" button --- */
92
93
  .mobile-tab-new {
93
- flex: none;
94
- width: 48px;
95
- height: 48px;
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: 24px;
112
- height: 24px;
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 sun */
135
- .theme-toggle-sun { color: var(--bg); } /* dark icon on lighter thumb */
136
- .theme-toggle-moon { color: var(--text-muted); } /* light icon on dark track */
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 moon */
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-sun {
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-moon {
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
 
@@ -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 encodedCwd = utils.encodeCwd(cwd);
20
- var sessionsDir = path.join(config.CONFIG_DIR, "sessions", encodedCwd);
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 forward slashes and dots with hyphens so that usernames
8
- * like "jon.doe" don't break session/note lookups.
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.doe/my-project" -> "-Users-jon-doe-my-project"
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",