clay-server 4.1.0-beta.14 → 4.1.0-beta.16

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 (35) hide show
  1. package/lib/project-connection.js +5 -1
  2. package/lib/project-sessions.js +2 -0
  3. package/lib/project-user-message.js +4 -0
  4. package/lib/project.js +2 -2
  5. package/lib/public/app.js +7 -1
  6. package/lib/public/css/mobile-nav.css +7 -0
  7. package/lib/public/css/sidebar.css +7 -0
  8. package/lib/public/modules/app-connection.js +9 -1
  9. package/lib/public/modules/app-messages.js +19 -9
  10. package/lib/public/modules/app-panels.js +68 -22
  11. package/lib/public/modules/app-projects.js +2 -0
  12. package/lib/public/modules/context-view-preference.js +143 -0
  13. package/lib/public/modules/default-vendor.js +116 -0
  14. package/lib/public/modules/pane-bridge.js +8 -8
  15. package/lib/public/modules/project-activation.js +49 -0
  16. package/lib/public/modules/sidebar-mobile.js +49 -2
  17. package/lib/public/modules/sidebar-sessions.js +65 -5
  18. package/lib/public/modules/split-session-boundary.js +28 -0
  19. package/lib/public/modules/split-view.js +40 -7
  20. package/lib/sdk-bridge.js +11 -44
  21. package/lib/sdk-message-processor.js +9 -2
  22. package/lib/server-context-view.js +53 -0
  23. package/lib/server-default-vendor.js +60 -0
  24. package/lib/server.js +8 -0
  25. package/lib/session-title-generator.js +89 -0
  26. package/lib/session-title-policy.js +8 -2
  27. package/lib/sessions.js +14 -0
  28. package/lib/user-presence.js +6 -0
  29. package/lib/users-context-view-preferences.js +65 -0
  30. package/lib/users-default-vendor-preferences.js +80 -0
  31. package/lib/users.js +12 -0
  32. package/lib/ws-schema.js +6 -0
  33. package/lib/yoke/acp-agent-profiles.js +15 -2
  34. package/lib/yoke/adapters/codex.js +56 -13
  35. package/package.json +1 -1
@@ -346,7 +346,11 @@ function attachConnection(ctx) {
346
346
  var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
347
347
  var dcExisting = userPresence.getPresence(slug, dcPresKey);
348
348
  var dcSession = sm.sessions.get(ws._clayActiveSession);
349
- userPresence.setPresence(slug, dcPresKey, userPresence.sessionIdForPersistence(dcSession), dcExisting ? dcExisting.mateDm : null);
349
+ var dcSessionId = userPresence.sessionIdForPersistence(dcSession);
350
+ var dcExistingSession = dcExisting && userPresence.findSession(sm.sessions, dcExisting.sessionId);
351
+ if (userPresence.shouldPersistDisconnectPresence(dcExisting, dcSessionId, dcExistingSession, dcSession)) {
352
+ userPresence.setPresence(slug, dcPresKey, dcSessionId, dcExisting ? dcExisting.mateDm : null);
353
+ }
350
354
  }
351
355
  tm.detachAll(ws);
352
356
  clients.delete(ws);
@@ -984,6 +984,8 @@ function attachSessions(ctx) {
984
984
  if (msg.id && sm.sessions.has(msg.id) && msg.title) {
985
985
  var s = sm.sessions.get(msg.id);
986
986
  s.title = String(msg.title).substring(0, 100);
987
+ s.titleProvisional = false;
988
+ s.titleAutoGenerated = false;
987
989
  s.titleManuallySet = true;
988
990
  sm.saveSessionFile(s);
989
991
  sm.notifySessionRenamed(s.localId);
@@ -479,6 +479,10 @@ function attachUserMessage(ctx) {
479
479
  function recordSessionTitle() {
480
480
  if (!session.title) {
481
481
  session.title = (msg.text || "Image").substring(0, 50);
482
+ // This title is an optimistic first-message label, not a semantic or
483
+ // explicit title. Persist its provenance so startQuery can preserve
484
+ // eligibility for model generation after this function returns.
485
+ session.titleProvisional = true;
482
486
  sm.saveSessionFile(session);
483
487
  sm.broadcastSessionList();
484
488
  // Sync auto-title to SDK
package/lib/project.js CHANGED
@@ -1598,8 +1598,8 @@ function createProjectContext(opts) {
1598
1598
  }
1599
1599
 
1600
1600
  // --- DM messages (delegated to server-level handler) ---
1601
- if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
1602
- if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg);
1601
+ if (msg.type === "home_debate_question_response" || msg.type === "home_debate_control" || msg.type === "home_mate_creation_question_response" || msg.type === "default_ai_get" || msg.type === "default_ai_catalog_get" || msg.type === "default_ai_set" || msg.type === "default_vendor_get" || msg.type === "default_vendor_set" || msg.type === "context_view_get" || msg.type === "context_view_set" || msg.type === "cursor_sharing_get" || msg.type === "cursor_sharing_set") {
1602
+ if (typeof opts.onDmMessage === "function") opts.onDmMessage(ws, msg, slug);
1603
1603
  return;
1604
1604
  }
1605
1605
  if (msg.type === "issue_reference_resolve" || msg.type === "home_clay_ask" || msg.type === "home_clay_session_resolve" || msg.type === "home_clay_log_resolve") {
package/lib/public/app.js CHANGED
@@ -71,6 +71,7 @@ import { rememberHomePrimarySurface } from './modules/home-surface.js';
71
71
  import { initRateLimit, handleRateLimitEvent as _rlHandleRateLimitEvent, updateRateLimitUsage as _rlUpdateRateLimitUsage, handleFastModeState as _rlHandleFastModeState, resetRateLimitState } from './modules/app-rate-limit.js';
72
72
  import { initCursors, handleRemoteCursorMove as _curHandleRemoteCursorMove, handleRemoteCursorLeave as _curHandleRemoteCursorLeave, handleRemoteSelection as _curHandleRemoteSelection, clearRemoteCursors as _curClearRemoteCursors, initCursorToggle } from './modules/app-cursors.js';
73
73
  import { initDefaultAi } from './modules/default-ai.js';
74
+ import { requestDefaultVendor } from './modules/default-vendor.js';
74
75
  import { initFavicon, updateFavicon as _favUpdateFavicon, setSendBtnMode as _favSetSendBtnMode, blinkIO as _favBlinkIO, blinkSessionDot as _favBlinkSessionDot, updateCrossProjectBlink as _favUpdateCrossProjectBlink, startUrgentBlink as _favStartUrgentBlink, stopUrgentBlink as _favStopUrgentBlink, setActivity as _favSetActivity } from './modules/app-favicon.js';
75
76
  import { initHeader, closeSessionInfoPopover as _hdrCloseSessionInfoPopover, updateHistorySentinel as _hdrUpdateHistorySentinel, requestMoreHistory as _hdrRequestMoreHistory, prependOlderHistory as _hdrPrependOlderHistory } from './modules/app-header.js';
76
77
  import { initSessionActions } from './modules/session-actions.js';
@@ -286,6 +287,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
286
287
  currentSlug: currentSlug,
287
288
  activeProjectSlug: null,
288
289
  sessionActivatedProjectSlug: null,
290
+ sessionListProjectSlug: null,
291
+ splitGroupsProjectSlug: null,
289
292
  socketPath: null,
290
293
  pendingHomeProjectSlug: null,
291
294
  currentProjectOwnerId: null,
@@ -427,6 +430,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
427
430
  defaultAiState: { loading: false, saving: false, refreshRequestId: null, saveRequestId: null, catalogRequestIds: {}, catalogs: {}, preference: null, selection: null, installedVendors: [], accountAvailable: true, accountId: null, serverEpoch: null, canonicalRevision: 0, error: "" },
428
431
  defaultAiDraft: { vendor: "", model: "", effort: "" },
429
432
  defaultAiDraftDirty: false,
433
+ defaultVendorState: { loading: false, saving: false, getRequestId: null, saveRequestId: null, preference: null, preferencePresent: false, installedVendors: [], accountId: null, projectSlug: null, serverEpoch: null, canonicalRevision: 0, error: "" },
430
434
 
431
435
  // dm
432
436
  dmTargetUser: null,
@@ -443,6 +447,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
443
447
  pendingShellCommandId: null,
444
448
  mateProjectSlug: null,
445
449
  myUserId: null,
450
+ contextViewPreferenceState: { mode: "off", canonicalMode: "off", preferencePresent: false, loading: false, saving: false, requestId: null, saveRequestId: null, pendingSaves: [], accountId: null, serverEpoch: null, rejectedEpoch: null, canonicalRevision: 0, error: "" },
451
+ contextViewOverride: null,
446
452
  isMultiUserMode: false,
447
453
  dmUnread: {},
448
454
  dmRemovedUsers: {},
@@ -1024,7 +1030,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1024
1030
  // the project Mate DM preference therefore hides only Mate shortcuts.
1025
1031
  if (document.body) document.body.classList.add('is-multi-user');
1026
1032
  }
1027
- if (d.user && d.user.id) { store.set({ myUserId: d.user.id }); }
1033
+ if (d.user && d.user.id) { store.set({ myUserId: d.user.id }); requestDefaultVendor(); }
1028
1034
  if (d.permissions) store.set({ permissions: d.permissions });
1029
1035
  if (d.mustChangePin) showForceChangePinOverlay();
1030
1036
  // Single-user mode: clear user strip skeletons immediately (no presence message will arrive)
@@ -423,6 +423,13 @@
423
423
  .mobile-vendor-list .mobile-session-new-vendor:active {
424
424
  background: rgba(var(--overlay-rgb), 0.1);
425
425
  }
426
+ .mobile-vendor-row { display: flex; align-items: stretch; gap: 4px; }
427
+ .mobile-vendor-row .mobile-session-new-vendor { flex: 1 1 auto; min-width: 0; }
428
+ .mobile-vendor-set-default { border: 1px solid var(--border-subtle); border-radius: 8px; background: transparent; color: var(--text-dimmer); padding: 0 8px; font-size: 11px; white-space: nowrap; }
429
+ .mobile-vendor-set-default:active { background: rgba(var(--overlay-rgb), 0.1); }
430
+ .mobile-vendor-set-default:disabled { opacity: 0.55; }
431
+ .mobile-session-new-preference-status { padding: 4px 8px 8px; color: var(--error); font-size: 11px; }
432
+ .mobile-session-new-fallback-note { color: var(--text-muted); }
426
433
  .mobile-vendor-note {
427
434
  margin-left: auto;
428
435
  font-size: 12px;
@@ -1464,6 +1464,13 @@
1464
1464
 
1465
1465
  /* --- New-session vendor picker --- */
1466
1466
  .session-new-menu { min-width: 200px; }
1467
+ .session-new-vendor-row { display: flex; align-items: stretch; }
1468
+ .session-new-vendor-row .session-new-vendor { flex: 1 1 auto; min-width: 0; }
1469
+ .session-new-set-default { border: 0; background: transparent; color: var(--text-dimmer); padding: 0 8px; font-size: 10px; cursor: pointer; }
1470
+ .session-new-set-default:hover { color: var(--text); background: rgba(var(--overlay-rgb), 0.05); }
1471
+ .session-new-set-default:disabled { opacity: 0.55; cursor: wait; }
1472
+ .session-new-preference-status { padding: 6px 10px; color: var(--error); font-size: 11px; }
1473
+ .session-new-fallback-note { color: var(--text-muted); border-bottom: 1px solid var(--border); }
1467
1474
 
1468
1475
  .session-ctx-sep {
1469
1476
  height: 1px;
@@ -17,6 +17,9 @@ import { resumeHomeChat } from './home-mate-chat.js';
17
17
  import { requestHomeSurfacePreference } from './home-surface.js';
18
18
  import { isHomeDebatesSurface } from './home-sub-surface.js';
19
19
  import { beginDefaultAiConnection, requestDefaultAi } from './default-ai.js';
20
+ import { beginContextViewConnection, requestContextView } from './context-view-preference.js';
21
+ import { beginDefaultVendorConnection, requestDefaultVendor } from './default-vendor.js';
22
+ import { clearProjectSplitState } from './split-session-boundary.js';
20
23
 
21
24
  var reconnectTimer = null;
22
25
  var reconnectDelay = 1000;
@@ -233,7 +236,8 @@ export function connect() {
233
236
 
234
237
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
235
238
  var socketPath = store.get('wsPath');
236
- store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null });
239
+ if (store.get('socketPath') && store.get('socketPath') !== socketPath) clearProjectSplitState();
240
+ store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
237
241
  var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
238
242
  setWs(newWs);
239
243
 
@@ -277,6 +281,10 @@ export function connect() {
277
281
  onConnected();
278
282
  beginDefaultAiConnection();
279
283
  requestDefaultAi();
284
+ beginDefaultVendorConnection();
285
+ requestDefaultVendor();
286
+ beginContextViewConnection();
287
+ requestContextView();
280
288
  };
281
289
 
282
290
  newWs.onclose = function (e) {
@@ -19,6 +19,8 @@ import { refreshMobileChatSheet } from './sidebar-mobile.js';
19
19
  import { renderMateSessionList, handleMateSearchResults, updateMateSidebarProfile } from './mate-sidebar.js';
20
20
  import { openHomeChat, handleHomeMateHistory, handleHomeMateDelta, handleHomeMateSegment, handleHomeMateDone, handleHomeMateError, handleHomeMateSessionsState } from './home-mate-chat.js';
21
21
  import { handleHomeSurfaceState } from './home-surface.js';
22
+ import { handleDefaultVendorMessage } from './default-vendor.js';
23
+ import { handleContextViewMessage } from './context-view-preference.js';
22
24
  import { handleHomeMateMemoryState, handleHomeMateKnowledgeState } from './home-mate-settings.js';
23
25
  import { renderKnowledgeList, handleKnowledgeContent } from './mate-knowledge.js';
24
26
  import { renderMemoryList } from './mate-memory.js';
@@ -63,9 +65,10 @@ import { setStatus } from './app-connection.js';
63
65
  import { handleWhatsNewState, handleWhatsNewSeenResult, setKnownEntries as setWhatsNewKnownEntries } from './whats-new.js';
64
66
  import { closeArticle as closeWhatsNewArticle } from './whats-new-article.js';
65
67
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
68
+ import { markProjectSessionListHydrated, applyProjectSplitGroups, clearProjectSplitState } from './split-session-boundary.js';
66
69
  import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
67
70
  import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
68
- import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
71
+ import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel, markContextUnavailable } from './app-panels.js';
69
72
  import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress, finishProjectSessionActivation } from './app-projects.js';
70
73
  import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
71
74
  import { hideHomeHub, showHomeHub } from './app-home-hub.js';
@@ -97,6 +100,8 @@ export function processMessage(msg) {
97
100
  if (handleAutonomousRunMessage(msg)) return;
98
101
  if (handleLoopInterviewMessage(msg)) return;
99
102
  if (handleScheduledTaskMessage(msg)) return;
103
+ if (handleDefaultVendorMessage(msg)) return;
104
+ if (handleContextViewMessage(msg)) return;
100
105
  if (msg && msg.type === "schedule_message_result") {
101
106
  handleScheduleMessageResult(msg);
102
107
  return;
@@ -226,9 +231,8 @@ export function processMessage(msg) {
226
231
  applyDeadSessionTodoCompaction();
227
232
  }
228
233
  // Restore cached rich context usage BEFORE updateContextPanel runs
229
- if (msg.contextUsage) {
230
- store.set({ richContextUsage: msg.contextUsage });
231
- }
234
+ if (msg.contextUsage && !msg.contextUsage.unavailable) store.set({ richContextUsage: msg.contextUsage });
235
+ else markContextUnavailable();
232
236
  // Restore accurate context data from the last result in full history
233
237
  if (msg.lastUsage || msg.lastModelUsage) {
234
238
  accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
@@ -330,10 +334,13 @@ export function processMessage(msg) {
330
334
  // start receiving the new project's session_switched. The TUI
331
335
  // host lives on document.body (it's position: fixed), so it
332
336
  // survives project navigation unless we detach explicitly here.
337
+ if (msg.slug && store.get('activeProjectSlug') && store.get('activeProjectSlug') !== msg.slug) {
338
+ clearProjectSplitState();
339
+ }
333
340
  detachTuiView();
334
341
  store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
335
342
  if (msg.cwd) store.set({ cwd: msg.cwd });
336
- if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug });
343
+ if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
337
344
  try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
338
345
  // In mate DM, keep title as mate name and re-apply mate color
339
346
  if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
@@ -690,6 +697,7 @@ export function processMessage(msg) {
690
697
  break;
691
698
 
692
699
  case "session_list":
700
+ markProjectSessionListHydrated();
693
701
  renderMateSessionList(msg.sessions || []);
694
702
  renderSessionList(msg.sessions || []);
695
703
  syncPaneTitles();
@@ -701,13 +709,14 @@ export function processMessage(msg) {
701
709
  }
702
710
  }
703
711
  handlePaletteSessionSwitch();
712
+ maybeRestoreSplitGroup();
704
713
  break;
705
714
 
706
715
  case "split_groups":
707
- store.set({ splitGroups: msg.groups || [] });
716
+ applyProjectSplitGroups(msg.groups || []);
708
717
  renderSessionList(null);
709
- maybeRestoreSplitGroup();
710
718
  syncPaneTitles();
719
+ maybeRestoreSplitGroup();
711
720
  break;
712
721
 
713
722
  case "split_group_result":
@@ -888,8 +897,8 @@ export function processMessage(msg) {
888
897
  }
889
898
  // Reload survival: a restored active session that is a split-group
890
899
  // member reopens its group (no-op when a split is already open).
891
- maybeRestoreSplitGroup();
892
900
  if (finishProjectSessionActivation()) hideHomeHub();
901
+ maybeRestoreSplitGroup();
893
902
  break;
894
903
 
895
904
  case "session_full_access_changed":
@@ -1273,7 +1282,8 @@ export function processMessage(msg) {
1273
1282
  case "context_usage":
1274
1283
  if (msg.sessionId != null && msg.sessionId !== store.get("activeSessionId")) break;
1275
1284
  if (msg.data && !store.get('replayingHistory')) {
1276
- store.set({ richContextUsage: msg.data });
1285
+ if (msg.data.unavailable) markContextUnavailable();
1286
+ else store.set({ richContextUsage: msg.data });
1277
1287
  // UI sync handled by store subscriber in app-panels.js
1278
1288
  }
1279
1289
  break;
@@ -5,15 +5,17 @@ import { refreshIcons } from "./icons.js";
5
5
  import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
+ import { getContextView as getStoredContextView, getEffectiveContextView, setContextView as setStoredContextView } from './context-view-preference.js';
8
9
  import { reportPaneContext } from './pane-bridge.js';
9
10
  import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
10
11
 
11
12
  // --- Module-owned state (not in store) ---
12
13
  var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
13
- var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
14
+ var contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
14
15
  var ctxPopoverEl = null;
15
16
  var ctxHoverTimer = null;
16
17
  var statusRefreshTimer = null;
18
+ var lastAppliedContextView = null;
17
19
 
18
20
  // --- DOM refs ---
19
21
  var configChipWrap = null;
@@ -374,6 +376,15 @@ export function initPanels() {
374
376
 
375
377
  // --- Reactive UI sync ---
376
378
  store.subscribe(function (state, prev) {
379
+ var effectiveContextView = getEffectiveContextView();
380
+ if (effectiveContextView !== lastAppliedContextView) {
381
+ lastAppliedContextView = effectiveContextView;
382
+ applyContextView(effectiveContextView);
383
+ }
384
+ if (state.contextViewPreferenceState && state.contextViewPreferenceState.error &&
385
+ state.contextViewPreferenceState.error !== (prev.contextViewPreferenceState && prev.contextViewPreferenceState.error)) {
386
+ showToast(state.contextViewPreferenceState.error, "error");
387
+ }
377
388
  // richContextUsage changed -> update popover + panel
378
389
  if (state.richContextUsage !== prev.richContextUsage) {
379
390
  if (state.richContextUsage) {
@@ -540,7 +551,6 @@ export function initPanels() {
540
551
  if (contextPanelClose) {
541
552
  contextPanelClose.addEventListener("click", function () {
542
553
  setContextView("off");
543
- applyContextView("off");
544
554
  });
545
555
  }
546
556
 
@@ -553,7 +563,8 @@ export function initPanels() {
553
563
  }
554
564
 
555
565
  // Restore context view on load
556
- applyContextView(getContextView());
566
+ lastAppliedContextView = getEffectiveContextView();
567
+ applyContextView(lastAppliedContextView);
557
568
  }
558
569
 
559
570
  // --- Config chip ---
@@ -718,9 +729,18 @@ export function contextPctClass(pct) {
718
729
 
719
730
  export function updateContextPanel() {
720
731
  if (!contextUsedEl) return;
721
- // Context window usage = input tokens only (includes cache read/write)
722
- var used = contextData.input;
723
- var win = contextData.contextWindow;
732
+ // Context occupancy is separate from generic per-turn accounting. Codex
733
+ // only supplies currentInput from a verified last-turn snapshot; rich
734
+ // context usage is more authoritative when available.
735
+ var rich = store.get('richContextUsage');
736
+ var hasRich = rich && (
737
+ (typeof rich.totalTokens === "number" && typeof rich.maxTokens === "number" && rich.maxTokens > 0) ||
738
+ (typeof rich.input_tokens === "number" && typeof rich.contextWindow === "number" && rich.contextWindow > 0)
739
+ );
740
+ var hasCurrent = typeof contextData.currentInput === "number";
741
+ var hasContext = !!(hasRich || hasCurrent);
742
+ var used = hasRich ? (typeof rich.totalTokens === "number" ? rich.totalTokens : rich.input_tokens) : (hasCurrent ? contextData.currentInput : 0);
743
+ var win = hasRich ? (typeof rich.maxTokens === "number" ? rich.maxTokens : rich.contextWindow) : (hasCurrent ? contextData.contextWindow : 0);
724
744
  var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
725
745
  var cls = contextPctClass(pct);
726
746
  reportPaneContext({ pct: pct, used: used, win: win, cls: cls, model: contextData.model, cost: contextData.cost });
@@ -734,7 +754,7 @@ export function updateContextPanel() {
734
754
  contextMiniFill.className = "context-mini-fill" + cls;
735
755
  }
736
756
  if (contextMiniLabel) {
737
- contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
757
+ contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "-");
738
758
  }
739
759
  // Header bar. A split group has no meaningful group-level context usage:
740
760
  // the two members have separate windows, and the parent shell's own numbers
@@ -742,9 +762,9 @@ export function updateContextPanel() {
742
762
  // chip instead, so hide the title-bar gauge while a split is open.
743
763
  var splitOpen = !!store.get('splitPanes');
744
764
  var headerCtxEl = store.get('headerContextEl');
745
- if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen);
746
- if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen);
747
- if (pct > 0 && !splitOpen) {
765
+ if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen || !hasContext);
766
+ if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen || !hasContext);
767
+ if (hasContext && !splitOpen) {
748
768
  var statusArea = document.querySelector(".title-bar-content .status");
749
769
  var hCtxEl = store.get('headerContextEl');
750
770
  if (statusArea && !hCtxEl) {
@@ -808,7 +828,7 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
808
828
  // when available -- result.usage.input_tokens sums all API calls in a turn,
809
829
  // inflating context usage when tools are involved.
810
830
  // Falls back to the summed value for setups that don't emit message_start.
811
- if (lastStreamInputTokens) {
831
+ if (typeof lastStreamInputTokens === "number") {
812
832
  contextData.input = lastStreamInputTokens;
813
833
  } else {
814
834
  contextData.input = (usage.input_tokens || usage.inputTokens || 0)
@@ -817,6 +837,11 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
817
837
  contextData.output = usage.output_tokens || usage.outputTokens || 0;
818
838
  contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
819
839
  contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
840
+ if (store.get('currentVendor') === "codex") {
841
+ contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : null;
842
+ } else {
843
+ contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : contextData.input;
844
+ }
820
845
  }
821
846
  contextData.turns++;
822
847
  if (modelUsage) {
@@ -830,6 +855,21 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
830
855
  contextData.model = displayModel;
831
856
  contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
832
857
  if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
858
+ if (store.get('currentVendor') === "codex") {
859
+ var snapshot = mu.contextSnapshot;
860
+ if (snapshot) store.set({ richContextUsage: null });
861
+ if (snapshot && snapshot.valid === true
862
+ && typeof snapshot.inputTokens === "number"
863
+ && typeof snapshot.contextWindow === "number"
864
+ && snapshot.contextWindow > 0) {
865
+ contextData.currentInput = snapshot.inputTokens;
866
+ contextData.contextWindow = snapshot.contextWindow;
867
+ } else {
868
+ // An explicit invalid marker is authoritative: do not retain an
869
+ // occupancy value from the previous live turn or history replay.
870
+ contextData.currentInput = null;
871
+ }
872
+ }
833
873
 
834
874
  // thinkingTokens and costBasis are per-model and optional: absent on
835
875
  // older CLI builds and, for a resumed session, on the turns that ran
@@ -860,11 +900,15 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
860
900
 
861
901
  // contextView: "off" | "mini" | "panel"
862
902
  export function getContextView() {
863
- try { return localStorage.getItem("clay-context-view") || "off"; } catch (e) { return "off"; }
903
+ return getStoredContextView();
864
904
  }
865
905
 
866
906
  export function setContextView(v) {
867
- try { localStorage.setItem("clay-context-view", v); } catch (e) {}
907
+ if (store.get("paneMode") === true) {
908
+ store.set({ contextViewOverride: v });
909
+ return true;
910
+ }
911
+ return setStoredContextView(v);
868
912
  }
869
913
 
870
914
  export function applyContextView(view) {
@@ -874,36 +918,38 @@ export function applyContextView(view) {
874
918
  }
875
919
 
876
920
  export function resetContextData() {
877
- contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
921
+ contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
878
922
  store.set({ richContextUsage: null });
879
923
  // hideCtxPopover + updateContextPanel handled by store subscriber
880
924
  }
881
925
 
926
+ export function markContextUnavailable() {
927
+ contextData.currentInput = null;
928
+ store.set({ richContextUsage: null });
929
+ updateContextPanel();
930
+ }
931
+
882
932
  export function resetContext() {
883
933
  resetContextData();
884
934
  // Keep view state, just reset data
885
- applyContextView(getContextView());
935
+ applyContextView(getEffectiveContextView());
886
936
  }
887
937
 
888
938
  export function minimizeContext() {
889
939
  setContextView("mini");
890
- applyContextView("mini");
891
940
  }
892
941
 
893
942
  export function expandContext() {
894
943
  setContextView("panel");
895
- applyContextView("panel");
896
944
  }
897
945
 
898
946
  export function toggleContextPanel() {
899
947
  if (!contextPanel) return;
900
- var view = getContextView();
948
+ var view = getEffectiveContextView();
901
949
  if (view === "panel") {
902
950
  setContextView("mini");
903
- applyContextView("mini");
904
951
  } else {
905
952
  setContextView("panel");
906
- applyContextView("panel");
907
953
  }
908
954
  }
909
955
 
@@ -952,8 +998,8 @@ export function renderCtxPopover() {
952
998
  if (!ctxPopoverEl || !richContextUsage) return;
953
999
  var d = richContextUsage;
954
1000
  var cats = d.categories || [];
955
- var total = d.totalTokens || 0;
956
- var max = d.maxTokens || 0;
1001
+ var total = d.totalTokens != null ? d.totalTokens : (d.input_tokens || 0);
1002
+ var max = d.maxTokens != null ? d.maxTokens : (d.contextWindow || 0);
957
1003
  var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
958
1004
 
959
1005
  var html = "";
@@ -39,6 +39,7 @@ import { closeProjectSettings } from './project-settings.js';
39
39
  import { chooseProjectAfterRemoval } from './project-removal-target.js';
40
40
  import { isProjectActivated, isProjectActivationPending, isProjectContextConnected, projectWsPath } from './project-activation.js';
41
41
  import { rememberHomePrimarySurface } from './home-surface.js';
42
+ import { clearProjectSplitState } from './split-session-boundary.js';
42
43
 
43
44
  // --- Module-owned state ---
44
45
  var cachedProjects = [];
@@ -447,6 +448,7 @@ export function switchProject(slug) {
447
448
  }
448
449
  if (homeVisible) store.set({ pendingHomeProjectSlug: slug });
449
450
  if (isProjectActivationPending(store.snap(), slug, ws)) return;
451
+ if (store.get('currentSlug') !== slug) clearProjectSplitState();
450
452
  resetFileBrowser();
451
453
  if (typeof closeScheduledTasks === "function") closeScheduledTasks();
452
454
  closeNotesBrowser();
@@ -0,0 +1,143 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+
4
+ var MODES = ["off", "mini", "panel"];
5
+ var sequence = 0;
6
+
7
+ function validMode(mode) { return MODES.indexOf(mode) !== -1; }
8
+ function nextRequestId(prefix) { sequence += 1; return prefix + Date.now() + "-" + sequence; }
9
+ function accountId() { return store.get("myUserId") || "default"; }
10
+ function initialState() {
11
+ return { mode: "off", canonicalMode: "off", preferencePresent: false, loading: false, saving: false, requestId: null, saveRequestId: null, pendingSaves: [], accountId: null, serverEpoch: null, rejectedEpoch: null, canonicalRevision: 0, error: "" };
12
+ }
13
+
14
+ function send(message) {
15
+ var ws = getWs();
16
+ if (!ws || ws.readyState !== 1) return false;
17
+ ws.send(JSON.stringify(message));
18
+ return true;
19
+ }
20
+
21
+ export function getContextView() {
22
+ var state = store.get("contextViewPreferenceState") || initialState();
23
+ return validMode(state.mode) ? state.mode : "off";
24
+ }
25
+
26
+ export function getEffectiveContextView() {
27
+ if (store.get("paneMode") !== true) return getContextView();
28
+ var override = store.get("contextViewOverride");
29
+ return override === null || override === undefined ? getContextView() : (validMode(override) ? override : getContextView());
30
+ }
31
+
32
+ export function beginContextViewConnection() {
33
+ var state = store.get("contextViewPreferenceState") || initialState();
34
+ store.set({ contextViewPreferenceState: Object.assign({}, state, {
35
+ mode: validMode(state.canonicalMode) ? state.canonicalMode : "off",
36
+ loading: false,
37
+ saving: false,
38
+ requestId: null,
39
+ saveRequestId: null,
40
+ pendingSaves: [],
41
+ rejectedEpoch: state.serverEpoch || null,
42
+ serverEpoch: null,
43
+ canonicalRevision: 0,
44
+ error: "",
45
+ }) });
46
+ }
47
+
48
+ export function requestContextView() {
49
+ var id = nextRequestId("context-view-");
50
+ var state = store.get("contextViewPreferenceState") || initialState();
51
+ store.set({ contextViewPreferenceState: Object.assign({}, state, { loading: true, requestId: id, error: "" }) });
52
+ if (!send({ type: "context_view_get", requestId: id })) {
53
+ store.set({ contextViewPreferenceState: Object.assign({}, store.get("contextViewPreferenceState") || initialState(), { loading: false, requestId: null, error: "Clay is offline. Reconnect and try again." }) });
54
+ return false;
55
+ }
56
+ return true;
57
+ }
58
+
59
+ export function setContextView(mode) {
60
+ if (!validMode(mode)) return false;
61
+ var id = nextRequestId("context-view-save-");
62
+ var state = store.get("contextViewPreferenceState") || initialState();
63
+ var pendingSaves = (state.pendingSaves || []).concat([{ requestId: id, mode: mode }]);
64
+ store.set({ contextViewPreferenceState: Object.assign({}, state, { mode: mode, saving: true, saveRequestId: id, pendingSaves: pendingSaves, error: "" }) });
65
+ if (!send({ type: "context_view_set", requestId: id, mode: mode })) {
66
+ var reverted = store.get("contextViewPreferenceState") || initialState();
67
+ pendingSaves = (reverted.pendingSaves || []).filter(function (item) { return item.requestId !== id; });
68
+ var pendingMode = pendingSaves.length ? pendingSaves[pendingSaves.length - 1].mode : (reverted.canonicalMode || "off");
69
+ store.set({ contextViewPreferenceState: Object.assign({}, reverted, { mode: pendingMode, saving: pendingSaves.length > 0, saveRequestId: pendingSaves.length ? pendingSaves[pendingSaves.length - 1].requestId : null, pendingSaves: pendingSaves, error: "Clay is offline. Reconnect and try again." }) });
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+
75
+ export function handleContextViewMessage(msg) {
76
+ if (!msg || msg.type !== "context_view_state") return false;
77
+ var state = store.get("contextViewPreferenceState") || initialState();
78
+ var currentAccount = accountId();
79
+ var isGetReply = !!msg.requestId && msg.requestId === state.requestId;
80
+ var pendingSaves = state.pendingSaves || [];
81
+ var saveIndex = -1;
82
+ for (var i = 0; i < pendingSaves.length; i++) if (pendingSaves[i].requestId === msg.requestId) saveIndex = i;
83
+ var isSaveReply = !!msg.requestId && saveIndex !== -1;
84
+ var isOlderSaveRevision = isSaveReply && typeof msg.canonicalRevision === "number" && msg.canonicalRevision < (state.canonicalRevision || 0);
85
+ var isCorrelatedAccountFailure = (isGetReply || isSaveReply) && msg.accountId === null && msg.accountAvailable === false;
86
+ if ((!isCorrelatedAccountFailure && msg.accountId !== currentAccount) || (msg.accountId === null && !isCorrelatedAccountFailure)) return true;
87
+ if (msg.serverEpoch && state.serverEpoch && msg.serverEpoch !== state.serverEpoch && (!msg.requestId || (!isGetReply && !isSaveReply))) return true;
88
+ if (msg.serverEpoch && state.rejectedEpoch && msg.serverEpoch === state.rejectedEpoch && (!isGetReply && !isSaveReply)) return true;
89
+ if (msg.canonicalRevision !== undefined && msg.canonicalRevision < (state.canonicalRevision || 0) && msg.serverEpoch === state.serverEpoch && !isSaveReply) return true;
90
+ if (msg.requestId && !isGetReply && !isSaveReply) return true;
91
+ if (msg.requestId && !isSaveReply && msg.ready === false && state.saving) return true;
92
+ var next = Object.assign({}, state, {
93
+ accountId: msg.accountId || currentAccount,
94
+ serverEpoch: msg.serverEpoch || state.serverEpoch || null,
95
+ rejectedEpoch: isGetReply || isSaveReply ? null : state.rejectedEpoch || null,
96
+ canonicalRevision: isOlderSaveRevision ? (state.canonicalRevision || 0) : (typeof msg.canonicalRevision === "number" ? msg.canonicalRevision : state.canonicalRevision || 0),
97
+ loading: isGetReply ? false : state.loading,
98
+ saving: state.saving,
99
+ requestId: isGetReply ? null : state.requestId,
100
+ saveRequestId: state.saveRequestId,
101
+ error: msg.error || "",
102
+ });
103
+ if (Object.prototype.hasOwnProperty.call(msg, "mode") && validMode(msg.mode) && msg.ready !== false && !isCorrelatedAccountFailure) {
104
+ if (isSaveReply) {
105
+ if (!isOlderSaveRevision && msg.ready !== false) {
106
+ pendingSaves = pendingSaves.slice(saveIndex + 1);
107
+ } else {
108
+ pendingSaves = pendingSaves.filter(function (item) { return item.requestId !== msg.requestId; });
109
+ }
110
+ }
111
+ if (!isOlderSaveRevision) {
112
+ next.canonicalMode = msg.mode;
113
+ next.preferencePresent = msg.preferencePresent === true;
114
+ }
115
+ if (pendingSaves.length) {
116
+ next.mode = pendingSaves[pendingSaves.length - 1].mode;
117
+ next.saving = true;
118
+ next.saveRequestId = pendingSaves[pendingSaves.length - 1].requestId;
119
+ } else {
120
+ next.mode = next.canonicalMode;
121
+ next.saving = false;
122
+ next.saveRequestId = null;
123
+ }
124
+ next.pendingSaves = pendingSaves;
125
+ } else if (isSaveReply && (msg.ready === false || isCorrelatedAccountFailure)) {
126
+ pendingSaves = pendingSaves.filter(function (item) { return item.requestId !== msg.requestId; });
127
+ next.pendingSaves = pendingSaves;
128
+ next.mode = pendingSaves.length ? pendingSaves[pendingSaves.length - 1].mode : (state.canonicalMode || "off");
129
+ next.saving = pendingSaves.length > 0;
130
+ next.saveRequestId = pendingSaves.length ? pendingSaves[pendingSaves.length - 1].requestId : null;
131
+ }
132
+ store.set({ contextViewPreferenceState: next });
133
+ return true;
134
+ }
135
+
136
+ store.subscribe(function (state, previous) {
137
+ if (state.myUserId !== previous.myUserId || state.currentSlug !== previous.currentSlug) {
138
+ store.set({ contextViewPreferenceState: Object.assign({}, initialState(), { accountId: accountId() }), contextViewOverride: null });
139
+ if (state.connected) requestContextView();
140
+ }
141
+ });
142
+
143
+ export { validMode };