dsh-nextsteps 1.1.0 → 1.2.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.
Files changed (2) hide show
  1. package/client.js +91 -4
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -144,6 +144,57 @@ window.__ModuleLoader__.load({
144
144
 
145
145
  const captures = new Map();
146
146
  const captureListeners = new Set();
147
+ const CAPTURES_KEY = 'dsh.nextsteps.captures.v1';
148
+ const CAPTURE_LIMIT = 400;
149
+
150
+ function readStoredCaptures() {
151
+ try {
152
+ const raw = window.localStorage.getItem(CAPTURES_KEY);
153
+ const parsed = raw === null ? {} : JSON.parse(raw);
154
+ return parsed !== null && typeof parsed === 'object' ? parsed : {};
155
+ } catch (error) {
156
+ console.warn('dsh-nextsteps: could not read stored captures:', error);
157
+ return {};
158
+ }
159
+ }
160
+
161
+ function writeStoredCaptures(store) {
162
+ try {
163
+ window.localStorage.setItem(CAPTURES_KEY, JSON.stringify(store));
164
+ } catch (error) {
165
+ console.warn('dsh-nextsteps: could not persist captures:', error);
166
+ }
167
+ }
168
+
169
+ function rehydrateCaptures() {
170
+ const store = readStoredCaptures();
171
+ for (const [sessionId, sessionEntries] of Object.entries(store)) {
172
+ if (sessionEntries === null || typeof sessionEntries !== 'object') continue;
173
+ const entries = sessionCaptures(sessionId);
174
+ for (const [callId, entry] of Object.entries(sessionEntries)) {
175
+ if (entry === null || typeof entry !== 'object' || !Array.isArray(entry.followups) || entry.followups.length === 0) continue;
176
+ if (entries.has(callId)) continue;
177
+ entries.set(callId, { turn: entry.turn, followups: entry.followups, clickedId: entry.clickedId });
178
+ }
179
+ }
180
+ }
181
+
182
+ function pruneStoredCaptures(store) {
183
+ let total = 0;
184
+ for (const sessionEntries of Object.values(store)) total += Object.keys(sessionEntries ?? {}).length;
185
+ if (total <= CAPTURE_LIMIT) return;
186
+ const sessionIds = Object.keys(store);
187
+ for (const sessionId of sessionIds) {
188
+ if (total <= CAPTURE_LIMIT) break;
189
+ const keys = Object.keys(store[sessionId]);
190
+ for (const callId of keys) {
191
+ if (total <= CAPTURE_LIMIT) break;
192
+ delete store[sessionId][callId];
193
+ total -= 1;
194
+ }
195
+ if (Object.keys(store[sessionId]).length === 0) delete store[sessionId];
196
+ }
197
+ }
147
198
 
148
199
  function sessionCaptures(sessionId) {
149
200
  let entries = captures.get(sessionId);
@@ -162,6 +213,12 @@ window.__ModuleLoader__.load({
162
213
  const clickedId = previous?.clickedId
163
214
  ?? `sf-${contentHash(`${sessionId}|${mergedTurn ?? callId}|${followups.map((entry) => entry.prompt).join('|')}`)}`;
164
215
  entries.set(callId, { turn: mergedTurn, followups, clickedId });
216
+ const store = readStoredCaptures();
217
+ const sessionEntries = store[sessionId] !== null && typeof store[sessionId] === 'object' ? store[sessionId] : {};
218
+ sessionEntries[callId] = { turn: mergedTurn, followups, clickedId };
219
+ store[sessionId] = sessionEntries;
220
+ pruneStoredCaptures(store);
221
+ writeStoredCaptures(store);
165
222
  for (const listener of captureListeners) listener(sessionId);
166
223
  }
167
224
 
@@ -180,9 +237,34 @@ window.__ModuleLoader__.load({
180
237
 
181
238
  function newestCapture(sessionId) {
182
239
  const entries = captures.get(sessionId);
183
- if (entries === undefined || entries.size !== 1) return null;
184
- for (const entry of entries.values()) return entry;
185
- return null;
240
+ if (entries === undefined || entries.size === 0) return null;
241
+ let best = null;
242
+ for (const entry of entries.values()) {
243
+ if (best === null || (entry.turn ?? -1) > (best.turn ?? -1)) best = entry;
244
+ }
245
+ return best;
246
+ }
247
+
248
+ const adoptedCallIds = new Set();
249
+
250
+ function unclaimedCaptures(sessionId, turn) {
251
+ const entries = captures.get(sessionId);
252
+ if (entries === undefined) return null;
253
+ const candidates = [];
254
+ for (const [callId, entry] of entries.entries()) {
255
+ if (entry.turn === undefined && !adoptedCallIds.has(callId)) candidates.push([callId, entry]);
256
+ }
257
+ if (candidates.length === 0) return null;
258
+ candidates.sort((a, b) => String(a[0]).localeCompare(String(b[0])));
259
+ const [callId, entry] = candidates[0];
260
+ adoptedCallIds.add(callId);
261
+ entries.set(callId, { ...entry, turn });
262
+ const store = readStoredCaptures();
263
+ const sessionEntries = store[sessionId] !== null && typeof store[sessionId] === 'object' ? store[sessionId] : {};
264
+ sessionEntries[callId] = { turn, followups: entry.followups, clickedId: entry.clickedId };
265
+ store[sessionId] = sessionEntries;
266
+ writeStoredCaptures(store);
267
+ return entry;
186
268
  }
187
269
 
188
270
  function displayLabel(followup) {
@@ -384,7 +466,11 @@ window.__ModuleLoader__.load({
384
466
 
385
467
 
386
468
 
387
- const entry = turn !== undefined ? captureForTurn(sessionId, turn) : newestCapture(sessionId);
469
+ let entry = turn !== undefined ? captureForTurn(sessionId, turn) : newestCapture(sessionId);
470
+ if (entry === null) {
471
+ const unclaimed = unclaimedCaptures(sessionId, turn);
472
+ if (unclaimed !== null) entry = unclaimed;
473
+ }
388
474
  return h(FollowupsCard, { entry, sessionId, ctx, t: props.t });
389
475
  };
390
476
  }
@@ -501,6 +587,7 @@ window.__ModuleLoader__.load({
501
587
  inject: ['slots', 'locale', 'remote'],
502
588
  apply(ctx) {
503
589
  installStyles(ctx);
590
+ rehydrateCaptures();
504
591
  refreshPluginSettings();
505
592
  for (const [code, dictionary] of Object.entries(DICTIONARIES)) {
506
593
  ctx.effect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-nextsteps",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",