@kabmo-labs/kabmo 0.8.0 → 0.9.0
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/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +314 -314
- package/dist/client/static/js/{index-qwpvd9fv.js → index-bpptfkz2.js} +922 -892
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +9 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +47 -67
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/mobile/LayoutWrapper.tsx +1 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +8 -2
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +16 -8
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +8 -11
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +19 -24
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +2 -0
- package/src/client/hooks/ui/updates/shell-download.ts +55 -14
- package/src/client/hooks/ui/updates/shell-offer.test.ts +26 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +20 -5
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +37 -8
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/fs/worktree/create.ts +2 -3
- package/src/server/lib/fs/worktree/list.ts +13 -0
- package/src/server/lib/fs/worktree/remove.ts +3 -3
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.ts +94 -3
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +33 -2
- package/src/server/lib/updates/shell-pages.ts +17 -5
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +10 -27
- package/src/server/lib/updates/shell-release.ts +107 -111
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +29 -7
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/list.ts +11 -1
- package/src/server/routes/worktrees/routes.test.ts +17 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +4 -4
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-gtbpewr7.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/chat-timeline/target-picker/index.test.tsx +0 -227
- package/src/client/components/workspace/chat-timeline/target-picker/index.tsx +0 -260
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -66,91 +66,52 @@
|
|
|
66
66
|
* from another conversation is never restored
|
|
67
67
|
* - chat.draft string
|
|
68
68
|
* - chat.draftAttachments attachment metadata array
|
|
69
|
+
*
|
|
70
|
+
* The module is three files, because one had passed the per-file ceiling: this
|
|
71
|
+
* one owns the PUBLIC API (read, write, load, migrate) and what a write means;
|
|
72
|
+
* `cache.ts` owns the storage (the maps, the LRU order, the idle TTL, the dirty
|
|
73
|
+
* flag); `merge.ts` owns the write counter and the "which copy wins when a blob
|
|
74
|
+
* lands" decision. The maps live only in `cache.ts`, so "is this session dirty"
|
|
75
|
+
* and "drop this session" cannot be answered from two places that disagree.
|
|
69
76
|
*/
|
|
70
77
|
|
|
71
78
|
import { notifySessionKey, notifySessionReplaced } from '@/shared/lib/workspace/session-state/listeners';
|
|
79
|
+
import {
|
|
80
|
+
clearDirty,
|
|
81
|
+
dropEntry,
|
|
82
|
+
dropPersistTimer,
|
|
83
|
+
entryForWrite,
|
|
84
|
+
evictIfNeeded,
|
|
85
|
+
isDirty,
|
|
86
|
+
lastAccess,
|
|
87
|
+
markDirty,
|
|
88
|
+
readEntry,
|
|
89
|
+
setPersistTimer,
|
|
90
|
+
stampAccess,
|
|
91
|
+
touchEntry,
|
|
92
|
+
writeEntry,
|
|
93
|
+
} from '@/shared/lib/workspace/session-state/cache';
|
|
94
|
+
import {
|
|
95
|
+
currentWriteSeq,
|
|
96
|
+
forgetWrites,
|
|
97
|
+
keysWrittenAfter,
|
|
98
|
+
mergeStoredBlob,
|
|
99
|
+
noteWrite,
|
|
100
|
+
} from '@/shared/lib/workspace/session-state/merge';
|
|
72
101
|
|
|
73
102
|
type SessionState = Record<string, unknown>;
|
|
74
103
|
|
|
75
|
-
const cache = new Map<string, SessionState>();
|
|
76
|
-
/** Epoch ms of the last user-driven access per session (get/set/hydrate). */
|
|
77
|
-
const lastTouched = new Map<string, number>();
|
|
78
|
-
const dirtySessions = new Map<string, boolean>();
|
|
79
|
-
const persistTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
80
|
-
|
|
81
104
|
const PERSIST_DEBOUNCE_MS = 600;
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/** A session whose last user access is older than this is evicted regardless
|
|
87
|
-
* of the size cap, so stale per-session UI state (drafts, panel layouts)
|
|
88
|
-
* stops occupying memory 10 minutes after the user last opened it. */
|
|
89
|
-
const SESSION_IDLE_TTL_MS = 10 * 60 * 1000;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Reorder a cache entry to the end of the Map's iteration order (the
|
|
93
|
-
* most-recently-used position) via delete-then-set. Map.set on an existing
|
|
94
|
-
* key updates the value but keeps its original position, so the explicit
|
|
95
|
-
* delete is required for true LRU semantics.
|
|
96
|
-
*/
|
|
97
|
-
function touchCacheEntry(sessionId: string): void {
|
|
98
|
-
const value = cache.get(sessionId);
|
|
99
|
-
if (value !== undefined) {
|
|
100
|
-
cache.delete(sessionId);
|
|
101
|
-
cache.set(sessionId, value);
|
|
102
|
-
}
|
|
103
|
-
lastTouched.set(sessionId, Date.now());
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Evict the oldest clean sessions until the cache is within bounds.
|
|
108
|
-
*
|
|
109
|
-
* Dirty-safety rule: a session with unsaved writes (`dirtySessions.get(id) ===
|
|
110
|
-
* true`) must never be evicted, or the user's drafts and editor state would be
|
|
111
|
-
* silently lost. If every candidate is dirty (or is the protected id currently
|
|
112
|
-
* being written), stop evicting rather than lose data.
|
|
113
|
-
*
|
|
114
|
-
* Entries idle past SESSION_IDLE_TTL_MS are evicted first — even when the
|
|
115
|
-
* cache is under the size cap — so long-unused sessions drop out on schedule.
|
|
116
|
-
*/
|
|
117
|
-
function evictIfNeeded(protectedId: string | null): void {
|
|
118
|
-
const now = Date.now();
|
|
119
|
-
const evict = (id: string): void => {
|
|
120
|
-
cache.delete(id);
|
|
121
|
-
dirtySessions.delete(id);
|
|
122
|
-
lastTouched.delete(id);
|
|
123
|
-
const timer = persistTimers.get(id);
|
|
124
|
-
if (timer) {
|
|
125
|
-
clearTimeout(timer);
|
|
126
|
-
persistTimers.delete(id);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
// TTL pass: safe to skip only while the timer still holds the unsaved state
|
|
130
|
-
// or the session is the one currently being written.
|
|
131
|
-
for (const [id, touched] of lastTouched) {
|
|
132
|
-
if (now - touched <= SESSION_IDLE_TTL_MS) continue;
|
|
133
|
-
if (id === protectedId) continue;
|
|
134
|
-
if (dirtySessions.get(id)) continue;
|
|
135
|
-
evict(id);
|
|
136
|
-
}
|
|
137
|
-
while (cache.size > MAX_CACHED_SESSIONS) {
|
|
138
|
-
let evicted = false;
|
|
139
|
-
for (const id of cache.keys()) {
|
|
140
|
-
if (id === protectedId) continue;
|
|
141
|
-
if (dirtySessions.get(id)) continue;
|
|
142
|
-
evict(id);
|
|
143
|
-
evicted = true;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
if (!evicted) break;
|
|
147
|
-
}
|
|
106
|
+
/** Evict past the cap, dropping this session's write history with it. */
|
|
107
|
+
function evictNow(protectedId: string | null): void {
|
|
108
|
+
evictIfNeeded(protectedId, forgetWrites);
|
|
148
109
|
}
|
|
149
110
|
|
|
150
111
|
export function getSessionValue<T>(sessionId: string | null, key: string): T | undefined {
|
|
151
112
|
if (!sessionId) return undefined;
|
|
152
|
-
|
|
153
|
-
return
|
|
113
|
+
touchEntry(sessionId);
|
|
114
|
+
return readEntry(sessionId)?.[key] as T | undefined;
|
|
154
115
|
}
|
|
155
116
|
|
|
156
117
|
/**
|
|
@@ -166,18 +127,20 @@ export function getSessionValue<T>(sessionId: string | null, key: string): T | u
|
|
|
166
127
|
*/
|
|
167
128
|
export function getSessionState(sessionId: string | null): Readonly<Record<string, unknown>> | undefined {
|
|
168
129
|
if (!sessionId) return undefined;
|
|
169
|
-
return
|
|
130
|
+
return readEntry(sessionId);
|
|
170
131
|
}
|
|
171
132
|
|
|
172
133
|
export function setSessionKey(sessionId: string | null, key: string, value: unknown): void {
|
|
173
134
|
if (!sessionId) return;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
135
|
+
// Mutated IN PLACE, never copied: `useSessionState` compares the store's entry
|
|
136
|
+
// against its own copy by IDENTITY to decide whether a fresher write exists,
|
|
137
|
+
// and a copy per write made every reader see a different object for one slot.
|
|
138
|
+
entryForWrite(sessionId)[key] = value;
|
|
139
|
+
touchEntry(sessionId);
|
|
140
|
+
markDirty(sessionId);
|
|
141
|
+
noteWrite(sessionId, key);
|
|
179
142
|
schedulePersist(sessionId);
|
|
180
|
-
|
|
143
|
+
evictNow(sessionId);
|
|
181
144
|
notifySessionKey(sessionId, key);
|
|
182
145
|
}
|
|
183
146
|
|
|
@@ -188,22 +151,25 @@ export function setSessionKey(sessionId: string | null, key: string, value: unkn
|
|
|
188
151
|
*/
|
|
189
152
|
export function clearSessionKey(sessionId: string | null, key: string): void {
|
|
190
153
|
if (!sessionId) return;
|
|
191
|
-
const state =
|
|
154
|
+
const state = readEntry(sessionId);
|
|
192
155
|
if (!state || !(key in state)) return;
|
|
193
|
-
delete state[key];
|
|
194
|
-
|
|
195
|
-
|
|
156
|
+
delete (state as SessionState)[key];
|
|
157
|
+
touchEntry(sessionId);
|
|
158
|
+
markDirty(sessionId);
|
|
159
|
+
// A DELETE is a write: a blob that still carries the key must not bring it
|
|
160
|
+
// back (a retired key cleared on this side would return from the other tab's
|
|
161
|
+
// older snapshot).
|
|
162
|
+
noteWrite(sessionId, key);
|
|
196
163
|
schedulePersist(sessionId);
|
|
197
164
|
notifySessionKey(sessionId, key);
|
|
198
165
|
}
|
|
199
166
|
|
|
200
167
|
function schedulePersist(sessionId: string): void {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
persistTimers.set(
|
|
168
|
+
dropPersistTimer(sessionId);
|
|
169
|
+
setPersistTimer(
|
|
204
170
|
sessionId,
|
|
205
171
|
setTimeout(() => {
|
|
206
|
-
|
|
172
|
+
dropPersistTimer(sessionId);
|
|
207
173
|
void persistSession(sessionId);
|
|
208
174
|
}, PERSIST_DEBOUNCE_MS),
|
|
209
175
|
);
|
|
@@ -211,9 +177,9 @@ function schedulePersist(sessionId: string): void {
|
|
|
211
177
|
|
|
212
178
|
async function persistSession(sessionId: string): Promise<void> {
|
|
213
179
|
if (typeof window === 'undefined' || !sessionId) return;
|
|
214
|
-
if (!
|
|
215
|
-
|
|
216
|
-
const state =
|
|
180
|
+
if (!isDirty(sessionId)) return;
|
|
181
|
+
clearDirty(sessionId);
|
|
182
|
+
const state = readEntry(sessionId) ?? {};
|
|
217
183
|
try {
|
|
218
184
|
await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/state`, {
|
|
219
185
|
method: 'POST',
|
|
@@ -222,18 +188,14 @@ async function persistSession(sessionId: string): Promise<void> {
|
|
|
222
188
|
});
|
|
223
189
|
} catch (err) {
|
|
224
190
|
console.warn('session-state persist failed:', err);
|
|
225
|
-
|
|
191
|
+
markDirty(sessionId);
|
|
226
192
|
}
|
|
227
193
|
}
|
|
228
194
|
|
|
229
195
|
/** Immediately persist any pending writes for a session (used on switch). */
|
|
230
196
|
export function flushSession(sessionId: string | null): Promise<void> {
|
|
231
197
|
if (!sessionId || typeof window === 'undefined') return Promise.resolve();
|
|
232
|
-
|
|
233
|
-
if (timer) {
|
|
234
|
-
clearTimeout(timer);
|
|
235
|
-
persistTimers.delete(sessionId);
|
|
236
|
-
}
|
|
198
|
+
dropPersistTimer(sessionId);
|
|
237
199
|
return persistSession(sessionId);
|
|
238
200
|
}
|
|
239
201
|
|
|
@@ -241,27 +203,34 @@ export function flushSession(sessionId: string | null): Promise<void> {
|
|
|
241
203
|
export async function loadSession(sessionId: string): Promise<void> {
|
|
242
204
|
if (!sessionId) return;
|
|
243
205
|
if (typeof window === 'undefined') return;
|
|
206
|
+
/**
|
|
207
|
+
* The write counter as of the moment the read STARTED.
|
|
208
|
+
*
|
|
209
|
+
* Everything written after this is newer than the response, because the
|
|
210
|
+
* response describes the row as it was when the request arrived. Two
|
|
211
|
+
* mechanisms are needed and neither is sufficient alone: `dirtySessions`
|
|
212
|
+
* covers a write whose persist has not landed yet, while this covers one that
|
|
213
|
+
* already persisted — at which point the dirty flag is clear and a stale
|
|
214
|
+
* response would otherwise be adopted wholesale. Measured: a file opened
|
|
215
|
+
* during the first `/state` read came back with `layout.openedFiles` gone.
|
|
216
|
+
*/
|
|
217
|
+
const startedAt = currentWriteSeq();
|
|
244
218
|
try {
|
|
245
219
|
const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/state`);
|
|
246
220
|
if (res.ok) {
|
|
247
221
|
const data = (await res.json()) as { state?: SessionState };
|
|
248
222
|
const incoming = data.state && typeof data.state === 'object' ? data.state : {};
|
|
249
|
-
const existing =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// one tab never appeared in the other.
|
|
256
|
-
const dirty = dirtySessions.get(sessionId) === true;
|
|
257
|
-
const merged = dirty ? { ...incoming, ...existing } : incoming;
|
|
258
|
-
cache.set(sessionId, merged);
|
|
259
|
-
touchCacheEntry(sessionId);
|
|
223
|
+
const existing = readEntry(sessionId) ?? {};
|
|
224
|
+
writeEntry(
|
|
225
|
+
sessionId,
|
|
226
|
+
mergeStoredBlob(incoming, existing, keysWrittenAfter(sessionId, startedAt), isDirty(sessionId)),
|
|
227
|
+
);
|
|
228
|
+
touchEntry(sessionId);
|
|
260
229
|
}
|
|
261
230
|
} catch (err) {
|
|
262
231
|
console.warn('session-state load failed:', err);
|
|
263
232
|
}
|
|
264
|
-
|
|
233
|
+
evictNow(sessionId);
|
|
265
234
|
// The blob replaces every slot at once, so a follower that read the store
|
|
266
235
|
// before it arrived (no pick yet) has to re-read — including a private copy
|
|
267
236
|
// held by `useSessionState`, which does not follow per-slot writes.
|
|
@@ -272,9 +241,9 @@ export async function loadSession(sessionId: string): Promise<void> {
|
|
|
272
241
|
/** Seed the cache synchronously (e.g. from SSR-provided data). */
|
|
273
242
|
export function hydrateSession(sessionId: string | null, state: SessionState): void {
|
|
274
243
|
if (!sessionId) return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
244
|
+
writeEntry(sessionId, { ...(readEntry(sessionId) ?? {}), ...state });
|
|
245
|
+
touchEntry(sessionId);
|
|
246
|
+
evictNow(sessionId);
|
|
278
247
|
notifySessionKey(sessionId);
|
|
279
248
|
notifySessionReplaced(sessionId);
|
|
280
249
|
}
|
|
@@ -285,12 +254,12 @@ export function hydrateSession(sessionId: string | null, state: SessionState): v
|
|
|
285
254
|
* its last open is within SESSION_IDLE_TTL_MS.
|
|
286
255
|
*/
|
|
287
256
|
export function recordSessionOpen(sessionId: string | null): void {
|
|
288
|
-
if (sessionId)
|
|
257
|
+
if (sessionId) stampAccess(sessionId);
|
|
289
258
|
}
|
|
290
259
|
|
|
291
260
|
/** Epoch ms of the user's last open of this session in this tab, if any. */
|
|
292
261
|
export function getLastOpenedAt(sessionId: string): number | undefined {
|
|
293
|
-
return
|
|
262
|
+
return lastAccess(sessionId);
|
|
294
263
|
}
|
|
295
264
|
|
|
296
265
|
/**
|
|
@@ -302,14 +271,8 @@ export function getLastOpenedAt(sessionId: string): number | undefined {
|
|
|
302
271
|
* the delete and resurrect the state.
|
|
303
272
|
*/
|
|
304
273
|
export function forgetSession(sessionId: string): void {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
clearTimeout(timer);
|
|
308
|
-
persistTimers.delete(sessionId);
|
|
309
|
-
}
|
|
310
|
-
cache.delete(sessionId);
|
|
311
|
-
dirtySessions.delete(sessionId);
|
|
312
|
-
lastTouched.delete(sessionId);
|
|
274
|
+
dropEntry(sessionId);
|
|
275
|
+
forgetWrites(sessionId);
|
|
313
276
|
notifySessionKey(sessionId);
|
|
314
277
|
notifySessionReplaced(sessionId);
|
|
315
278
|
}
|
|
@@ -319,15 +282,14 @@ export function forgetSession(sessionId: string): void {
|
|
|
319
282
|
* by a spawn, then discard the transient slot.
|
|
320
283
|
*/
|
|
321
284
|
export function migrateSessionState(fromId: string, toId: string): void {
|
|
322
|
-
const from =
|
|
323
|
-
if (from)
|
|
324
|
-
|
|
325
|
-
|
|
285
|
+
const from = readEntry(fromId);
|
|
286
|
+
if (from) writeEntry(toId, { ...from, ...(readEntry(toId) ?? {}) });
|
|
287
|
+
touchEntry(toId);
|
|
288
|
+
markDirty(toId);
|
|
326
289
|
schedulePersist(toId);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
evictIfNeeded(toId);
|
|
290
|
+
dropEntry(fromId);
|
|
291
|
+
forgetWrites(fromId);
|
|
292
|
+
evictNow(toId);
|
|
331
293
|
// The adopted slot gained the whole transient state; the transient one lost it.
|
|
332
294
|
notifySessionKey(toId);
|
|
333
295
|
notifySessionKey(fromId);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The desktop workspace group's three panel widths, resolved from one place.
|
|
8
|
+
*
|
|
9
|
+
* `WorkspacePanels` used to compute these inline, which was correct but made the
|
|
10
|
+
* component's own contract hard to read and impossible to unit-test: every rule
|
|
11
|
+
* here — the two-call order, the pair caps, the leftover width a collapsed chat
|
|
12
|
+
* hands the editor — is arithmetic over values the caller already has.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no hooks, no DOM. The caller measures the group and reads the stored
|
|
15
|
+
* widths; this module decides what those mean.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_EDITOR_FRACTIONS,
|
|
20
|
+
DEFAULT_EDITOR_WIDTHS,
|
|
21
|
+
HANDLE_WIDTH,
|
|
22
|
+
MIN_EDITOR_PANEL_WIDTH,
|
|
23
|
+
panelFraction,
|
|
24
|
+
pairMaxSize,
|
|
25
|
+
resolvePanelWidth,
|
|
26
|
+
type EditorWidthMode,
|
|
27
|
+
type PanelWidths,
|
|
28
|
+
} from '@/shared/lib/workspace/panel-widths';
|
|
29
|
+
import { DEFAULT_RIGHT_PANEL_FRACTION } from '@/shared/lib/workspace/right-panels';
|
|
30
|
+
|
|
31
|
+
export interface WorkspacePanelWidthsArgs {
|
|
32
|
+
/** Measured area of the group, or null before the observer reports. */
|
|
33
|
+
available: number | null;
|
|
34
|
+
/** This session's remembered widths. */
|
|
35
|
+
panelWidths: PanelWidths;
|
|
36
|
+
/** Which slot the editor column is using (a diff wants more room). */
|
|
37
|
+
editorWidthMode: EditorWidthMode;
|
|
38
|
+
/** The right panel's active view, whose slot and defaults apply. */
|
|
39
|
+
activeRightPanel: string;
|
|
40
|
+
/** The active view's own defaults and floor, when it has them. */
|
|
41
|
+
activePanel?: { defaultFraction?: number; defaultWidth?: number; minWidth?: number };
|
|
42
|
+
showEditor: boolean;
|
|
43
|
+
showRightPanel: boolean;
|
|
44
|
+
/** The chat column is collapsed, so it holds no floor and no pair share. */
|
|
45
|
+
chatHidden: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WorkspacePanelWidths {
|
|
49
|
+
right: number;
|
|
50
|
+
editor: number;
|
|
51
|
+
/** Drag ceilings; `undefined` before the group is measured. */
|
|
52
|
+
editorMax: number | undefined;
|
|
53
|
+
rightMax: number | undefined;
|
|
54
|
+
/** Separators actually rendered, which also consume width. */
|
|
55
|
+
handleCount: number;
|
|
56
|
+
/** The right panel's floor, which the drag ceiling also reserves. */
|
|
57
|
+
rightMin: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveWorkspacePanelWidths(args: WorkspacePanelWidthsArgs): WorkspacePanelWidths {
|
|
61
|
+
const { available, panelWidths, editorWidthMode, activeRightPanel, activePanel, showEditor, showRightPanel, chatHidden } = args;
|
|
62
|
+
|
|
63
|
+
// The separator before the editor is gone while the chat is collapsed: there
|
|
64
|
+
// is nothing on its left to transfer width to, and the drag's neighbour walk
|
|
65
|
+
// skips a collapsed panel, so it would render a handle that does nothing.
|
|
66
|
+
const handleCount = (showEditor && !chatHidden ? 1 : 0) + (showRightPanel ? 1 : 0);
|
|
67
|
+
const rightMin = activePanel?.minWidth ?? MIN_EDITOR_PANEL_WIDTH;
|
|
68
|
+
|
|
69
|
+
// The editor and the right panel are a COUPLED pair: the right panel is
|
|
70
|
+
// resolved first, reserving the editor's WANTED share (the editor has no
|
|
71
|
+
// result yet), and the editor then reserves the right panel's ACTUAL width.
|
|
72
|
+
// That asymmetry is deliberate — an editor asking for more than its default
|
|
73
|
+
// makes the right panel give way first, and the pair still sums to the total.
|
|
74
|
+
const editorFraction = panelFraction(panelWidths[editorWidthMode], DEFAULT_EDITOR_FRACTIONS[editorWidthMode], available);
|
|
75
|
+
const editorReservePx = available != null && available > 0 ? Math.round(editorFraction * available) : 0;
|
|
76
|
+
|
|
77
|
+
const right = resolvePanelWidth({
|
|
78
|
+
stored: panelWidths.right?.[activeRightPanel],
|
|
79
|
+
defaultFraction: activePanel?.defaultFraction ?? DEFAULT_RIGHT_PANEL_FRACTION,
|
|
80
|
+
defaultPx: activePanel?.defaultWidth ?? MIN_EDITOR_PANEL_WIDTH,
|
|
81
|
+
available,
|
|
82
|
+
min: rightMin,
|
|
83
|
+
pairReservePx: showEditor ? editorReservePx : 0,
|
|
84
|
+
// The editor has not been resolved yet, so only its floor can be reserved.
|
|
85
|
+
siblingWidth: showEditor ? MIN_EDITOR_PANEL_WIDTH : 0,
|
|
86
|
+
handleCount,
|
|
87
|
+
chatHidden,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// With the chat collapsed the editor takes everything the right panel and the
|
|
91
|
+
// separators do not. Resolving from its remembered FRACTION would leave the
|
|
92
|
+
// freed width as an empty gap, which is the opposite of what the mode
|
|
93
|
+
// promises; the remembered width is untouched, so leaving restores it.
|
|
94
|
+
const editor = chatHidden
|
|
95
|
+
? Math.max(
|
|
96
|
+
MIN_EDITOR_PANEL_WIDTH,
|
|
97
|
+
available != null && available > 0
|
|
98
|
+
? Math.floor(available - (showRightPanel ? right : 0) - handleCount * HANDLE_WIDTH)
|
|
99
|
+
: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
|
|
100
|
+
)
|
|
101
|
+
: resolvePanelWidth({
|
|
102
|
+
stored: panelWidths[editorWidthMode],
|
|
103
|
+
defaultFraction: DEFAULT_EDITOR_FRACTIONS[editorWidthMode],
|
|
104
|
+
defaultPx: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
|
|
105
|
+
available,
|
|
106
|
+
min: MIN_EDITOR_PANEL_WIDTH,
|
|
107
|
+
pairReservePx: showRightPanel ? right : 0,
|
|
108
|
+
siblingWidth: showRightPanel ? right : 0,
|
|
109
|
+
handleCount,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
right,
|
|
114
|
+
editor,
|
|
115
|
+
// `resolvePanelWidth` caps what it HANDS to `defaultSize`, but a separator
|
|
116
|
+
// drag bypasses it — the resizer transfers pixels between two panels clamped
|
|
117
|
+
// only by their own `maxSize`. Each max is its own cap and the pair total
|
|
118
|
+
// minus the sibling's FLOOR (it gives way down to its minimum mid-drag).
|
|
119
|
+
editorMax: pairMaxSize({ available, siblingMin: rightMin, siblingOpen: showRightPanel, chatHidden }),
|
|
120
|
+
rightMax: pairMaxSize({ available, siblingMin: MIN_EDITOR_PANEL_WIDTH, siblingOpen: showEditor, chatHidden }),
|
|
121
|
+
handleCount,
|
|
122
|
+
rightMin,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -32,3 +32,32 @@ export interface SidebarStatusEntry {
|
|
|
32
32
|
|
|
33
33
|
/** `sidebar:status` — keyed by session id. Absent id means "no volatile fields". */
|
|
34
34
|
export type SidebarStatusPayload = Record<string, SidebarStatusEntry>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One live session's row fields that can still move between turn boundaries.
|
|
38
|
+
*
|
|
39
|
+
* Exactly two, and the list is closed on purpose: everything else about a row
|
|
40
|
+
* (title, folder, cwd, worktree, archived) only changes on a STRUCTURAL event
|
|
41
|
+
* that publishes the structure for its own reason. These two are the ones a
|
|
42
|
+
* run advances mid-turn — the transcript appends, and a subagent's transcript
|
|
43
|
+
* appears beside it (which is what lights the row's roster chevron).
|
|
44
|
+
*/
|
|
45
|
+
export interface SidebarActivityEntry {
|
|
46
|
+
/** Newest transcript entry's ISO timestamp — the row's age and its
|
|
47
|
+
* `LATEST_SESSION` position. */
|
|
48
|
+
updatedAt?: string;
|
|
49
|
+
/** Whether the session has any subagent transcript on disk. */
|
|
50
|
+
hasSubagents?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `sidebar:activity` — session id → the live fields above, for the sessions
|
|
55
|
+
* with a run in flight.
|
|
56
|
+
*
|
|
57
|
+
* Only a running session can move between turn boundaries, so an id that is
|
|
58
|
+
* absent here simply keeps what the structure gave it. Both sides read the same
|
|
59
|
+
* sources (the newest JSONL entry's own `timestamp`, and the subagent
|
|
60
|
+
* transcript directory), so the client merges field by field and lets the
|
|
61
|
+
* newer stamp win.
|
|
62
|
+
*/
|
|
63
|
+
export type SidebarActivityPayload = Record<string, SidebarActivityEntry>;
|
|
@@ -48,6 +48,16 @@ export interface SettingsState {
|
|
|
48
48
|
* cannot render a broken editor, only a different one.
|
|
49
49
|
*/
|
|
50
50
|
editorFont: string;
|
|
51
|
+
/**
|
|
52
|
+
* Colour theme for the code editor, INDEPENDENT of `theme` above.
|
|
53
|
+
*
|
|
54
|
+
* `auto` (the default) keeps the editor on the app palette. Any other value
|
|
55
|
+
* is a Shiki theme id from `EDITOR_THEMES` — the editor then paints its own
|
|
56
|
+
* surface, gutter, selection and tokens while the console around it keeps the
|
|
57
|
+
* app palette. `resolveEditorTheme` treats an id this build no longer ships
|
|
58
|
+
* as `auto`, so a downgrade renders the app palette rather than half a theme.
|
|
59
|
+
*/
|
|
60
|
+
editorTheme: string;
|
|
51
61
|
/** Generate a session title from the first run. omp suppresses its
|
|
52
62
|
* own auto-titling under `--mode rpc-ui` (PI_NO_TITLE), so without this a
|
|
53
63
|
* Kabmo session keeps its `New Session - <timestamp>` placeholder. Asked
|