shraga 0.1.97 → 0.1.98
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/dist/client/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
14
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
15
15
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-DPy-vyyF.js"></script>
|
|
17
17
|
<link rel="stylesheet" crossorigin href="/assets/index-DlgCLI89.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shraga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.98",
|
|
4
4
|
"description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -127,10 +127,41 @@ export function ChatView({ messages, busy, connectionStatus, onPermissionRespond
|
|
|
127
127
|
const screenMap = screenMapRef.current;
|
|
128
128
|
|
|
129
129
|
// Only the last slice is mounted — an agent thread runs to thousands of blocks and the DOM
|
|
130
|
-
// (not React) is what goes sluggish.
|
|
131
|
-
|
|
132
|
-
const
|
|
130
|
+
// (not React) is what goes sluggish. Scrolling to the top reveals the next slice, so a long
|
|
131
|
+
// thread walks backwards a page at a time instead of mounting 12k rows in one jump.
|
|
132
|
+
const [limit, setLimit] = useState(VISIBLE_MESSAGES);
|
|
133
|
+
const hiddenCount = Math.max(0, messages.length - limit);
|
|
133
134
|
const visibleMessages = hiddenCount > 0 ? messages.slice(hiddenCount) : messages;
|
|
135
|
+
const showEarlier = useCallback(() => setLimit((n) => n + VISIBLE_MESSAGES), []);
|
|
136
|
+
|
|
137
|
+
const pendingAnchor = useRef<{ box: HTMLDivElement | null; before: number } | null>(null);
|
|
138
|
+
// Reveal-on-scroll, with the button below as the visible fallback. The scroll position is pinned
|
|
139
|
+
// to the same message afterwards: prepending rows above the viewport otherwise yanks the thread.
|
|
140
|
+
const topRef = useRef<HTMLDivElement>(null);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const el = topRef.current;
|
|
143
|
+
if (!el || hiddenCount === 0) return;
|
|
144
|
+
const io = new IntersectionObserver((entries) => {
|
|
145
|
+
if (!entries.some((e) => e.isIntersecting)) return;
|
|
146
|
+
// On mount the thread is pinned to the bottom and the top sentinel can be "visible" simply
|
|
147
|
+
// because nothing has scrolled yet — expanding there would cascade the whole 12k thread into
|
|
148
|
+
// the DOM, which is exactly what the window exists to prevent. Only reveal on a real scroll up.
|
|
149
|
+
if (isNearBottom.current) return;
|
|
150
|
+
const box = scrollRef.current;
|
|
151
|
+
const before = box?.scrollHeight ?? 0;
|
|
152
|
+
pendingAnchor.current = { box, before };
|
|
153
|
+
showEarlier();
|
|
154
|
+
}, { rootMargin: '400px' });
|
|
155
|
+
io.observe(el);
|
|
156
|
+
return () => io.disconnect();
|
|
157
|
+
}, [hiddenCount, limit, showEarlier]);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const a = pendingAnchor.current;
|
|
161
|
+
if (!a?.box) return;
|
|
162
|
+
pendingAnchor.current = null;
|
|
163
|
+
a.box.scrollTop += a.box.scrollHeight - a.before; // keep the reader on the same message
|
|
164
|
+
}, [limit]);
|
|
134
165
|
|
|
135
166
|
useEffect(() => {
|
|
136
167
|
if (isNearBottom.current) {
|
|
@@ -172,10 +203,10 @@ export function ChatView({ messages, busy, connectionStatus, onPermissionRespond
|
|
|
172
203
|
</button>
|
|
173
204
|
</div>
|
|
174
205
|
{hiddenCount > 0 && (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
// Scroll-based, like the sidebar: scrolling to the top reveals the previous slice. A
|
|
207
|
+
// status row, not a control — the earlier button read as a dead box at the top of a thread.
|
|
208
|
+
<div ref={topRef} className="py-3 text-center text-[11px] text-muted-foreground/60">
|
|
209
|
+
{hiddenCount} earlier message{hiddenCount === 1 ? '' : 's'} · scroll up to load
|
|
179
210
|
</div>
|
|
180
211
|
)}
|
|
181
212
|
{visibleMessages.map((msg, i) => {
|
|
@@ -120,6 +120,26 @@ export function Sidebar({ getToken, activeSessionId, onSelect, onNew, refreshKey
|
|
|
120
120
|
}
|
|
121
121
|
}, [activeSessionId, filtered, activeRow]);
|
|
122
122
|
|
|
123
|
+
// Load the next page when the end of the list comes into view. A list that simply stops when you
|
|
124
|
+
// scroll to the bottom reads as broken — the button below stays as the visible fallback, but it
|
|
125
|
+
// is not the mechanism. `root: null` still respects the Radix viewport's clipping, so the sentinel
|
|
126
|
+
// only intersects once the user has actually scrolled near the end.
|
|
127
|
+
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
|
128
|
+
const loadMoreRef = useRef(loadMore);
|
|
129
|
+
loadMoreRef.current = loadMore;
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const el = sentinelRef.current;
|
|
132
|
+
if (!el || !cursor || unreadOnly) return;
|
|
133
|
+
const io = new IntersectionObserver(
|
|
134
|
+
(entries) => { if (entries.some((e) => e.isIntersecting)) loadMoreRef.current(); },
|
|
135
|
+
{ rootMargin: '300px' },
|
|
136
|
+
);
|
|
137
|
+
io.observe(el);
|
|
138
|
+
return () => io.disconnect();
|
|
139
|
+
// `loading` is a dep so the observer re-arms after a page lands: if the end is STILL in view
|
|
140
|
+
// (a short page, a tall sidebar), re-observing fires again and walks on to the next page.
|
|
141
|
+
}, [cursor, loading, unreadOnly]);
|
|
142
|
+
|
|
123
143
|
useEffect(() => {
|
|
124
144
|
getToken().then(t => t ? fetch('/api/version', { headers: { Authorization: `Bearer ${t}` } }) : null).then(r => r?.json()).then(d => d && setVersion(d.version)).catch(() => {});
|
|
125
145
|
}, []);
|
|
@@ -212,13 +232,11 @@ export function Sidebar({ getToken, activeSessionId, onSelect, onNew, refreshKey
|
|
|
212
232
|
{activeRow && renderRow(activeRow)}
|
|
213
233
|
{filtered.map(renderRow)}
|
|
214
234
|
{!unreadOnly && cursor && (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
>
|
|
220
|
-
{loading ? 'Loading…' : 'Show older'}
|
|
221
|
-
</button>
|
|
235
|
+
// Scroll-based: reaching the end of the list loads the next page. This is a status row,
|
|
236
|
+
// not a control — a "Show older" button read as a dead grey box at the end of the list.
|
|
237
|
+
<div ref={sentinelRef} className="py-3 text-center text-[11px] text-muted-foreground/60">
|
|
238
|
+
{loading ? 'Loading older…' : '···'}
|
|
239
|
+
</div>
|
|
222
240
|
)}
|
|
223
241
|
</div>
|
|
224
242
|
</ScrollArea>
|