@crazx/dsh-api-session-controller 0.1.5-alpha.1.zw.1 → 0.1.5-alpha.1.zw.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +166 -0
- package/lib/types/client/contract/sessions.d.ts +12 -0
- package/lib/types/client/sessions/manager.d.ts +23 -0
- package/lib/types/client/sessions/manager.js +82 -0
- package/lib/types/client/sessions/selection-history.d.ts +55 -0
- package/lib/types/client/sessions/selection-history.js +91 -0
- package/lib/types/client/sessions/service.d.ts +8 -0
- package/lib/types/client/sessions/service.js +16 -0
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -2179,6 +2179,87 @@ window.__ModuleLoader__.load({
|
|
|
2179
2179
|
return refs;
|
|
2180
2180
|
}
|
|
2181
2181
|
//#endregion
|
|
2182
|
+
//#region lib/types/client/sessions/selection-history.js
|
|
2183
|
+
function sameSelection(left, right) {
|
|
2184
|
+
return left.sessionId === right.sessionId && left.address?.parentSessionId === right.address?.parentSessionId && left.address?.childSessionId === right.address?.childSessionId;
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Bounded LIFO-with-cursor history. Recording a selection made outside
|
|
2188
|
+
* navigation collapses consecutive repeats, truncates the forward tail after
|
|
2189
|
+
* a backtrack, and drops the oldest entry past the bound. Pruning removes
|
|
2190
|
+
* deleted sessions wherever they sit and re-points the cursor onto the same
|
|
2191
|
+
* logical position.
|
|
2192
|
+
*/
|
|
2193
|
+
var SelectionHistory = class {
|
|
2194
|
+
limit;
|
|
2195
|
+
/** Oldest-first; the cursor sits ON the current entry (-1 while empty). */
|
|
2196
|
+
#entries = [];
|
|
2197
|
+
#cursor = -1;
|
|
2198
|
+
constructor(limit = 50) {
|
|
2199
|
+
this.limit = limit;
|
|
2200
|
+
}
|
|
2201
|
+
get entries() {
|
|
2202
|
+
return this.#entries;
|
|
2203
|
+
}
|
|
2204
|
+
get cursor() {
|
|
2205
|
+
return this.#cursor;
|
|
2206
|
+
}
|
|
2207
|
+
/** Whether an older entry exists to go back to. */
|
|
2208
|
+
canBack() {
|
|
2209
|
+
return this.#cursor > 0;
|
|
2210
|
+
}
|
|
2211
|
+
/** Whether a newer entry exists to go forward to. */
|
|
2212
|
+
canForward() {
|
|
2213
|
+
return this.#cursor < this.#entries.length - 1;
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Record one selection made outside history navigation.
|
|
2217
|
+
* @param entry - the selection (session, addressed subagent, or clear).
|
|
2218
|
+
*/
|
|
2219
|
+
record(entry) {
|
|
2220
|
+
const current = this.#entries[this.#cursor];
|
|
2221
|
+
if (current !== void 0 && sameSelection(current, entry)) return;
|
|
2222
|
+
this.#entries.length = this.#cursor + 1;
|
|
2223
|
+
this.#entries.push(entry);
|
|
2224
|
+
if (this.#entries.length > this.limit) this.#entries.shift();
|
|
2225
|
+
this.#cursor = this.#entries.length - 1;
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Step one entry back.
|
|
2229
|
+
* @returns the entry to replay, or undefined at the oldest bound.
|
|
2230
|
+
*/
|
|
2231
|
+
back() {
|
|
2232
|
+
if (!this.canBack()) return void 0;
|
|
2233
|
+
this.#cursor -= 1;
|
|
2234
|
+
return this.#entries[this.#cursor];
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Step one entry forward.
|
|
2238
|
+
* @returns the entry to replay, or undefined at the newest bound.
|
|
2239
|
+
*/
|
|
2240
|
+
forward() {
|
|
2241
|
+
if (!this.canForward()) return void 0;
|
|
2242
|
+
this.#cursor += 1;
|
|
2243
|
+
return this.#entries[this.#cursor];
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Drop every entry referencing the removed session wherever it sits,
|
|
2247
|
+
* keeping the cursor on the same logical position.
|
|
2248
|
+
* @param sessionId - the removed session.
|
|
2249
|
+
*/
|
|
2250
|
+
prune(sessionId) {
|
|
2251
|
+
let removedBeforeCursor = 0;
|
|
2252
|
+
for (let index = this.#entries.length - 1; index >= 0; index -= 1) {
|
|
2253
|
+
const entry = this.#entries[index];
|
|
2254
|
+
if (entry === void 0 || entry.sessionId !== sessionId) continue;
|
|
2255
|
+
this.#entries.splice(index, 1);
|
|
2256
|
+
if (index <= this.#cursor) removedBeforeCursor += 1;
|
|
2257
|
+
}
|
|
2258
|
+
this.#cursor -= removedBeforeCursor;
|
|
2259
|
+
if (this.#cursor > this.#entries.length - 1) this.#cursor = this.#entries.length - 1;
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
//#endregion
|
|
2182
2263
|
//#region lib/types/client/sessions/manager.js
|
|
2183
2264
|
function sessionSeqCursor(value) {
|
|
2184
2265
|
return value === -1 ? -1 : SessionSeq(value);
|
|
@@ -2229,6 +2310,10 @@ window.__ModuleLoader__.load({
|
|
|
2229
2310
|
*/
|
|
2230
2311
|
jobsBySession = /* @__PURE__ */ new Map();
|
|
2231
2312
|
selected;
|
|
2313
|
+
/** Toolbar back/forward history over selections (session, addressed child, or clear). */
|
|
2314
|
+
selectionHistory = new SelectionHistory();
|
|
2315
|
+
/** While replaying a history entry, the selections it makes must not re-record. */
|
|
2316
|
+
navigatingHistory = false;
|
|
2232
2317
|
listSnapshotCache;
|
|
2233
2318
|
/** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
|
|
2234
2319
|
* object when every field matches — wire refreshes mint all-new summary objects, so identity
|
|
@@ -2246,6 +2331,10 @@ window.__ModuleLoader__.load({
|
|
|
2246
2331
|
this.remote = remote;
|
|
2247
2332
|
this.selected = restoredSelection;
|
|
2248
2333
|
if (restoredAddress !== void 0) this.addresses.set(restoredAddress.childSessionId, restoredAddress);
|
|
2334
|
+
this.selectionHistory.record(restoredSelection === void 0 ? {} : {
|
|
2335
|
+
sessionId: restoredSelection,
|
|
2336
|
+
...restoredAddress !== void 0 ? { address: restoredAddress } : {}
|
|
2337
|
+
});
|
|
2249
2338
|
this.listSnapshotCache = this.buildListSnapshot();
|
|
2250
2339
|
}
|
|
2251
2340
|
/**
|
|
@@ -2259,6 +2348,10 @@ window.__ModuleLoader__.load({
|
|
|
2259
2348
|
this.sessions.get(sessionId)?.configureSubagent(address, address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable);
|
|
2260
2349
|
this.selected = sessionId;
|
|
2261
2350
|
this.completedNotifications.delete(sessionId);
|
|
2351
|
+
if (!this.navigatingHistory) this.selectionHistory.record(address === void 0 ? { sessionId } : {
|
|
2352
|
+
sessionId,
|
|
2353
|
+
address
|
|
2354
|
+
});
|
|
2262
2355
|
this.refreshSubagents(sessionId);
|
|
2263
2356
|
this.notifier.notifyNow();
|
|
2264
2357
|
}
|
|
@@ -2274,14 +2367,69 @@ window.__ModuleLoader__.load({
|
|
|
2274
2367
|
this.sessions.get(address.childSessionId)?.configureSubagent(address, catalog?.parentAvailable);
|
|
2275
2368
|
this.selected = address.childSessionId;
|
|
2276
2369
|
this.completedNotifications.delete(address.childSessionId);
|
|
2370
|
+
if (!this.navigatingHistory) this.selectionHistory.record({
|
|
2371
|
+
sessionId: address.childSessionId,
|
|
2372
|
+
address
|
|
2373
|
+
});
|
|
2277
2374
|
this.refreshSubagents(address.childSessionId);
|
|
2278
2375
|
this.notifier.notifyNow();
|
|
2279
2376
|
}
|
|
2280
2377
|
/** Clear the selection (the layout falls to the no-session view state). */
|
|
2281
2378
|
clearSelection() {
|
|
2282
2379
|
this.selected = void 0;
|
|
2380
|
+
if (!this.navigatingHistory) this.selectionHistory.record({});
|
|
2283
2381
|
this.notifier.notifyNow();
|
|
2284
2382
|
}
|
|
2383
|
+
/** Whether an older selection exists to go back to. */
|
|
2384
|
+
canBack() {
|
|
2385
|
+
return this.selectionHistory.canBack();
|
|
2386
|
+
}
|
|
2387
|
+
/** Whether a newer selection exists to go forward to. */
|
|
2388
|
+
canForward() {
|
|
2389
|
+
return this.selectionHistory.canForward();
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Select the previous history entry. Unreachable entries (sessions removed
|
|
2393
|
+
* before their prune landed) are pruned and the walk continues to older
|
|
2394
|
+
* entries; the walk stops silently at the oldest bound.
|
|
2395
|
+
*/
|
|
2396
|
+
back() {
|
|
2397
|
+
while (this.selectionHistory.canBack()) {
|
|
2398
|
+
const entry = this.selectionHistory.back();
|
|
2399
|
+
if (entry === void 0) return;
|
|
2400
|
+
if (!this.canReplay(entry)) {
|
|
2401
|
+
if (entry.sessionId !== void 0) this.selectionHistory.prune(entry.sessionId);
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
this.replaySelection(entry);
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Select the next history entry after a back navigation; silently a
|
|
2410
|
+
* no-op at the newest bound.
|
|
2411
|
+
*/
|
|
2412
|
+
forward() {
|
|
2413
|
+
const entry = this.selectionHistory.forward();
|
|
2414
|
+
if (entry === void 0) return;
|
|
2415
|
+
if (this.canReplay(entry)) this.replaySelection(entry);
|
|
2416
|
+
else if (entry.sessionId !== void 0) this.selectionHistory.prune(entry.sessionId);
|
|
2417
|
+
}
|
|
2418
|
+
/** Whether a history entry names a session that is still listed or addressed. */
|
|
2419
|
+
canReplay(entry) {
|
|
2420
|
+
if (entry.sessionId === void 0) return true;
|
|
2421
|
+
return this.summaries.some((summary) => summary.sessionId === entry.sessionId) || this.addresses.has(entry.sessionId);
|
|
2422
|
+
}
|
|
2423
|
+
/** Replay one entry without recording it back into the history. */
|
|
2424
|
+
replaySelection(entry) {
|
|
2425
|
+
this.navigatingHistory = true;
|
|
2426
|
+
try {
|
|
2427
|
+
if (entry.sessionId === void 0) this.clearSelection();
|
|
2428
|
+
else this.select(entry.sessionId);
|
|
2429
|
+
} finally {
|
|
2430
|
+
this.navigatingHistory = false;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2285
2433
|
/**
|
|
2286
2434
|
* Return the durable catalog address retained for one child.
|
|
2287
2435
|
* @param sessionId - possible addressed child id.
|
|
@@ -2498,6 +2646,7 @@ window.__ModuleLoader__.load({
|
|
|
2498
2646
|
let summaries = baseline;
|
|
2499
2647
|
for (const mutation of mutations) {
|
|
2500
2648
|
summaries = applyMutation(summaries, mutation);
|
|
2649
|
+
if (mutation.kind === "remove") this.selectionHistory.prune(mutation.sessionId);
|
|
2501
2650
|
this.summaries = summaries;
|
|
2502
2651
|
this.syncCompletedNotifications();
|
|
2503
2652
|
}
|
|
@@ -2638,6 +2787,7 @@ window.__ModuleLoader__.load({
|
|
|
2638
2787
|
recordMutation(mutation) {
|
|
2639
2788
|
this.listMutations?.push(mutation);
|
|
2640
2789
|
this.summaries = applyMutation(this.summaries, mutation);
|
|
2790
|
+
if (mutation.kind === "remove") this.selectionHistory.prune(mutation.sessionId);
|
|
2641
2791
|
this.syncCompletedNotifications();
|
|
2642
2792
|
this.notifier.markDirty();
|
|
2643
2793
|
}
|
|
@@ -3132,6 +3282,22 @@ window.__ModuleLoader__.load({
|
|
|
3132
3282
|
clear() {
|
|
3133
3283
|
this.manager.clearSelection();
|
|
3134
3284
|
}
|
|
3285
|
+
/** Whether the transient selection history has an older entry to go back to. */
|
|
3286
|
+
canBack() {
|
|
3287
|
+
return this.manager.canBack();
|
|
3288
|
+
}
|
|
3289
|
+
/** Whether the transient selection history has a newer entry to go forward to. */
|
|
3290
|
+
canForward() {
|
|
3291
|
+
return this.manager.canForward();
|
|
3292
|
+
}
|
|
3293
|
+
/** Select the previous history entry (prunes unreachable entries on the way). */
|
|
3294
|
+
back() {
|
|
3295
|
+
this.manager.back();
|
|
3296
|
+
}
|
|
3297
|
+
/** Select the next history entry after a back navigation. */
|
|
3298
|
+
forward() {
|
|
3299
|
+
this.manager.forward();
|
|
3300
|
+
}
|
|
3135
3301
|
/**
|
|
3136
3302
|
* Refresh the real Session baseline, reusing an in-flight pull.
|
|
3137
3303
|
* @returns completion of the current or newly started baseline pull.
|
|
@@ -65,6 +65,18 @@ export interface ISessions {
|
|
|
65
65
|
refreshSubagents(parentSessionId: SessionId): Promise<void>;
|
|
66
66
|
/** Clear the current selection into the no-session view state. */
|
|
67
67
|
clear(): void;
|
|
68
|
+
/** Whether the transient selection history has an older entry to go back to. */
|
|
69
|
+
canBack(): boolean;
|
|
70
|
+
/** Whether the transient selection history has a newer entry to go forward to. */
|
|
71
|
+
canForward(): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Select the previous history entry (bounded, consecutive repeats
|
|
74
|
+
* collapsed, forward truncated by a fresh selection, removed sessions
|
|
75
|
+
* pruned). The cleared no-session state participates as an entry.
|
|
76
|
+
*/
|
|
77
|
+
back(): void;
|
|
78
|
+
/** Select the next history entry after a back navigation. */
|
|
79
|
+
forward(): void;
|
|
68
80
|
/**
|
|
69
81
|
* Refresh the Host-authoritative Session list.
|
|
70
82
|
* @returns completion of the current or newly started Session-list refresh.
|
|
@@ -84,6 +84,10 @@ export declare class SessionManager {
|
|
|
84
84
|
*/
|
|
85
85
|
private readonly jobsBySession;
|
|
86
86
|
private selected;
|
|
87
|
+
/** Toolbar back/forward history over selections (session, addressed child, or clear). */
|
|
88
|
+
private readonly selectionHistory;
|
|
89
|
+
/** While replaying a history entry, the selections it makes must not re-record. */
|
|
90
|
+
private navigatingHistory;
|
|
87
91
|
private listSnapshotCache;
|
|
88
92
|
/** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
|
|
89
93
|
* object when every field matches — wire refreshes mint all-new summary objects, so identity
|
|
@@ -108,6 +112,25 @@ export declare class SessionManager {
|
|
|
108
112
|
selectSubagent(address: SubagentAddress): void;
|
|
109
113
|
/** Clear the selection (the layout falls to the no-session view state). */
|
|
110
114
|
clearSelection(): void;
|
|
115
|
+
/** Whether an older selection exists to go back to. */
|
|
116
|
+
canBack(): boolean;
|
|
117
|
+
/** Whether a newer selection exists to go forward to. */
|
|
118
|
+
canForward(): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Select the previous history entry. Unreachable entries (sessions removed
|
|
121
|
+
* before their prune landed) are pruned and the walk continues to older
|
|
122
|
+
* entries; the walk stops silently at the oldest bound.
|
|
123
|
+
*/
|
|
124
|
+
back(): void;
|
|
125
|
+
/**
|
|
126
|
+
* Select the next history entry after a back navigation; silently a
|
|
127
|
+
* no-op at the newest bound.
|
|
128
|
+
*/
|
|
129
|
+
forward(): void;
|
|
130
|
+
/** Whether a history entry names a session that is still listed or addressed. */
|
|
131
|
+
private canReplay;
|
|
132
|
+
/** Replay one entry without recording it back into the history. */
|
|
133
|
+
private replaySelection;
|
|
111
134
|
/**
|
|
112
135
|
* Return the durable catalog address retained for one child.
|
|
113
136
|
* @param sessionId - possible addressed child id.
|
|
@@ -8,6 +8,7 @@ import { flattenLineage } from "./lineage.js";
|
|
|
8
8
|
import { Notifier } from "./notifier.js";
|
|
9
9
|
import { ProjectionValueStore } from "./projection-store.js";
|
|
10
10
|
import { Session } from "./session.js";
|
|
11
|
+
import { SelectionHistory } from "./selection-history.js";
|
|
11
12
|
function sessionSeqCursor(value) {
|
|
12
13
|
return value === -1 ? -1 : SessionSeq(value);
|
|
13
14
|
}
|
|
@@ -57,6 +58,10 @@ export class SessionManager {
|
|
|
57
58
|
*/
|
|
58
59
|
jobsBySession = new Map();
|
|
59
60
|
selected;
|
|
61
|
+
/** Toolbar back/forward history over selections (session, addressed child, or clear). */
|
|
62
|
+
selectionHistory = new SelectionHistory();
|
|
63
|
+
/** While replaying a history entry, the selections it makes must not re-record. */
|
|
64
|
+
navigatingHistory = false;
|
|
60
65
|
listSnapshotCache;
|
|
61
66
|
/** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
|
|
62
67
|
* object when every field matches — wire refreshes mint all-new summary objects, so identity
|
|
@@ -75,6 +80,11 @@ export class SessionManager {
|
|
|
75
80
|
this.selected = restoredSelection;
|
|
76
81
|
if (restoredAddress !== undefined)
|
|
77
82
|
this.addresses.set(restoredAddress.childSessionId, restoredAddress);
|
|
83
|
+
// The restored selection seeds the history so back/forward always has a
|
|
84
|
+
// home position; a cold start without one records the cleared state.
|
|
85
|
+
this.selectionHistory.record(restoredSelection === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { sessionId: restoredSelection, ...(restoredAddress !== undefined ? { address: restoredAddress } : {}) });
|
|
78
88
|
this.listSnapshotCache = this.buildListSnapshot();
|
|
79
89
|
}
|
|
80
90
|
// ---- Selection ----
|
|
@@ -95,6 +105,8 @@ export class SessionManager {
|
|
|
95
105
|
this.selected = sessionId;
|
|
96
106
|
// Looking at the session consumes its completion reminder (dot clears).
|
|
97
107
|
this.completedNotifications.delete(sessionId);
|
|
108
|
+
if (!this.navigatingHistory)
|
|
109
|
+
this.selectionHistory.record(address === undefined ? { sessionId } : { sessionId, address });
|
|
98
110
|
void this.refreshSubagents(sessionId);
|
|
99
111
|
this.notifier.notifyNow();
|
|
100
112
|
}
|
|
@@ -112,14 +124,79 @@ export class SessionManager {
|
|
|
112
124
|
this.sessions.get(address.childSessionId)?.configureSubagent(address, catalog?.parentAvailable);
|
|
113
125
|
this.selected = address.childSessionId;
|
|
114
126
|
this.completedNotifications.delete(address.childSessionId);
|
|
127
|
+
if (!this.navigatingHistory)
|
|
128
|
+
this.selectionHistory.record({ sessionId: address.childSessionId, address });
|
|
115
129
|
void this.refreshSubagents(address.childSessionId);
|
|
116
130
|
this.notifier.notifyNow();
|
|
117
131
|
}
|
|
118
132
|
/** Clear the selection (the layout falls to the no-session view state). */
|
|
119
133
|
clearSelection() {
|
|
120
134
|
this.selected = undefined;
|
|
135
|
+
if (!this.navigatingHistory)
|
|
136
|
+
this.selectionHistory.record({});
|
|
121
137
|
this.notifier.notifyNow();
|
|
122
138
|
}
|
|
139
|
+
// ---- Selection history (toolbar back/forward) ----
|
|
140
|
+
/** Whether an older selection exists to go back to. */
|
|
141
|
+
canBack() {
|
|
142
|
+
return this.selectionHistory.canBack();
|
|
143
|
+
}
|
|
144
|
+
/** Whether a newer selection exists to go forward to. */
|
|
145
|
+
canForward() {
|
|
146
|
+
return this.selectionHistory.canForward();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Select the previous history entry. Unreachable entries (sessions removed
|
|
150
|
+
* before their prune landed) are pruned and the walk continues to older
|
|
151
|
+
* entries; the walk stops silently at the oldest bound.
|
|
152
|
+
*/
|
|
153
|
+
back() {
|
|
154
|
+
while (this.selectionHistory.canBack()) {
|
|
155
|
+
const entry = this.selectionHistory.back();
|
|
156
|
+
if (entry === undefined)
|
|
157
|
+
return;
|
|
158
|
+
if (!this.canReplay(entry)) {
|
|
159
|
+
if (entry.sessionId !== undefined)
|
|
160
|
+
this.selectionHistory.prune(entry.sessionId);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
this.replaySelection(entry);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Select the next history entry after a back navigation; silently a
|
|
169
|
+
* no-op at the newest bound.
|
|
170
|
+
*/
|
|
171
|
+
forward() {
|
|
172
|
+
const entry = this.selectionHistory.forward();
|
|
173
|
+
if (entry === undefined)
|
|
174
|
+
return;
|
|
175
|
+
if (this.canReplay(entry))
|
|
176
|
+
this.replaySelection(entry);
|
|
177
|
+
else if (entry.sessionId !== undefined)
|
|
178
|
+
this.selectionHistory.prune(entry.sessionId);
|
|
179
|
+
}
|
|
180
|
+
/** Whether a history entry names a session that is still listed or addressed. */
|
|
181
|
+
canReplay(entry) {
|
|
182
|
+
if (entry.sessionId === undefined)
|
|
183
|
+
return true;
|
|
184
|
+
return this.summaries.some(summary => summary.sessionId === entry.sessionId)
|
|
185
|
+
|| this.addresses.has(entry.sessionId);
|
|
186
|
+
}
|
|
187
|
+
/** Replay one entry without recording it back into the history. */
|
|
188
|
+
replaySelection(entry) {
|
|
189
|
+
this.navigatingHistory = true;
|
|
190
|
+
try {
|
|
191
|
+
if (entry.sessionId === undefined)
|
|
192
|
+
this.clearSelection();
|
|
193
|
+
else
|
|
194
|
+
this.select(entry.sessionId);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
this.navigatingHistory = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
123
200
|
/**
|
|
124
201
|
* Return the durable catalog address retained for one child.
|
|
125
202
|
* @param sessionId - possible addressed child id.
|
|
@@ -379,6 +456,8 @@ export class SessionManager {
|
|
|
379
456
|
let summaries = baseline;
|
|
380
457
|
for (const mutation of mutations) {
|
|
381
458
|
summaries = applyMutation(summaries, mutation);
|
|
459
|
+
if (mutation.kind === 'remove')
|
|
460
|
+
this.selectionHistory.prune(mutation.sessionId);
|
|
382
461
|
this.summaries = summaries;
|
|
383
462
|
this.syncCompletedNotifications();
|
|
384
463
|
}
|
|
@@ -524,6 +603,9 @@ export class SessionManager {
|
|
|
524
603
|
recordMutation(mutation) {
|
|
525
604
|
this.listMutations?.push(mutation);
|
|
526
605
|
this.summaries = applyMutation(this.summaries, mutation);
|
|
606
|
+
// A removal forgets the session's toolbar back/forward history entries.
|
|
607
|
+
if (mutation.kind === 'remove')
|
|
608
|
+
this.selectionHistory.prune(mutation.sessionId);
|
|
527
609
|
// Eager edge reconciliation — a snapshot-build-time pass would miss consecutive status frames.
|
|
528
610
|
this.syncCompletedNotifications();
|
|
529
611
|
this.notifier.markDirty();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded transient selection history behind the desktop toolbar's
|
|
3
|
+
* back/forward navigation. Entries record WHICH selection was made — a
|
|
4
|
+
* listed session, an addressed subagent, or the cleared no-session state —
|
|
5
|
+
* never when, and never any session content. Browser-local and transient by
|
|
6
|
+
* design: nothing here is persisted, so a reload starts empty.
|
|
7
|
+
*/
|
|
8
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
9
|
+
import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client';
|
|
10
|
+
/** One selection-history entry; `sessionId: undefined` is the cleared state. */
|
|
11
|
+
export interface SelectionEntry {
|
|
12
|
+
readonly sessionId?: SessionId;
|
|
13
|
+
/** Retained direct-parent address for an addressed subagent selection. */
|
|
14
|
+
readonly address?: SubagentAddress;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Bounded LIFO-with-cursor history. Recording a selection made outside
|
|
18
|
+
* navigation collapses consecutive repeats, truncates the forward tail after
|
|
19
|
+
* a backtrack, and drops the oldest entry past the bound. Pruning removes
|
|
20
|
+
* deleted sessions wherever they sit and re-points the cursor onto the same
|
|
21
|
+
* logical position.
|
|
22
|
+
*/
|
|
23
|
+
export declare class SelectionHistory {
|
|
24
|
+
#private;
|
|
25
|
+
private readonly limit;
|
|
26
|
+
constructor(limit?: number);
|
|
27
|
+
get entries(): readonly SelectionEntry[];
|
|
28
|
+
get cursor(): number;
|
|
29
|
+
/** Whether an older entry exists to go back to. */
|
|
30
|
+
canBack(): boolean;
|
|
31
|
+
/** Whether a newer entry exists to go forward to. */
|
|
32
|
+
canForward(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Record one selection made outside history navigation.
|
|
35
|
+
* @param entry - the selection (session, addressed subagent, or clear).
|
|
36
|
+
*/
|
|
37
|
+
record(entry: SelectionEntry): void;
|
|
38
|
+
/**
|
|
39
|
+
* Step one entry back.
|
|
40
|
+
* @returns the entry to replay, or undefined at the oldest bound.
|
|
41
|
+
*/
|
|
42
|
+
back(): SelectionEntry | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Step one entry forward.
|
|
45
|
+
* @returns the entry to replay, or undefined at the newest bound.
|
|
46
|
+
*/
|
|
47
|
+
forward(): SelectionEntry | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Drop every entry referencing the removed session wherever it sits,
|
|
50
|
+
* keeping the cursor on the same logical position.
|
|
51
|
+
* @param sessionId - the removed session.
|
|
52
|
+
*/
|
|
53
|
+
prune(sessionId: SessionId): void;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=selection-history.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function sameSelection(left, right) {
|
|
2
|
+
return left.sessionId === right.sessionId
|
|
3
|
+
&& left.address?.parentSessionId === right.address?.parentSessionId
|
|
4
|
+
&& left.address?.childSessionId === right.address?.childSessionId;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Bounded LIFO-with-cursor history. Recording a selection made outside
|
|
8
|
+
* navigation collapses consecutive repeats, truncates the forward tail after
|
|
9
|
+
* a backtrack, and drops the oldest entry past the bound. Pruning removes
|
|
10
|
+
* deleted sessions wherever they sit and re-points the cursor onto the same
|
|
11
|
+
* logical position.
|
|
12
|
+
*/
|
|
13
|
+
export class SelectionHistory {
|
|
14
|
+
limit;
|
|
15
|
+
/** Oldest-first; the cursor sits ON the current entry (-1 while empty). */
|
|
16
|
+
#entries = [];
|
|
17
|
+
#cursor = -1;
|
|
18
|
+
constructor(limit = 50) {
|
|
19
|
+
this.limit = limit;
|
|
20
|
+
}
|
|
21
|
+
get entries() {
|
|
22
|
+
return this.#entries;
|
|
23
|
+
}
|
|
24
|
+
get cursor() {
|
|
25
|
+
return this.#cursor;
|
|
26
|
+
}
|
|
27
|
+
/** Whether an older entry exists to go back to. */
|
|
28
|
+
canBack() {
|
|
29
|
+
return this.#cursor > 0;
|
|
30
|
+
}
|
|
31
|
+
/** Whether a newer entry exists to go forward to. */
|
|
32
|
+
canForward() {
|
|
33
|
+
return this.#cursor < this.#entries.length - 1;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Record one selection made outside history navigation.
|
|
37
|
+
* @param entry - the selection (session, addressed subagent, or clear).
|
|
38
|
+
*/
|
|
39
|
+
record(entry) {
|
|
40
|
+
const current = this.#entries[this.#cursor];
|
|
41
|
+
if (current !== undefined && sameSelection(current, entry))
|
|
42
|
+
return;
|
|
43
|
+
this.#entries.length = this.#cursor + 1;
|
|
44
|
+
this.#entries.push(entry);
|
|
45
|
+
if (this.#entries.length > this.limit)
|
|
46
|
+
this.#entries.shift();
|
|
47
|
+
this.#cursor = this.#entries.length - 1;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Step one entry back.
|
|
51
|
+
* @returns the entry to replay, or undefined at the oldest bound.
|
|
52
|
+
*/
|
|
53
|
+
back() {
|
|
54
|
+
if (!this.canBack())
|
|
55
|
+
return undefined;
|
|
56
|
+
this.#cursor -= 1;
|
|
57
|
+
const entry = this.#entries[this.#cursor];
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Step one entry forward.
|
|
62
|
+
* @returns the entry to replay, or undefined at the newest bound.
|
|
63
|
+
*/
|
|
64
|
+
forward() {
|
|
65
|
+
if (!this.canForward())
|
|
66
|
+
return undefined;
|
|
67
|
+
this.#cursor += 1;
|
|
68
|
+
const entry = this.#entries[this.#cursor];
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Drop every entry referencing the removed session wherever it sits,
|
|
73
|
+
* keeping the cursor on the same logical position.
|
|
74
|
+
* @param sessionId - the removed session.
|
|
75
|
+
*/
|
|
76
|
+
prune(sessionId) {
|
|
77
|
+
let removedBeforeCursor = 0;
|
|
78
|
+
for (let index = this.#entries.length - 1; index >= 0; index -= 1) {
|
|
79
|
+
const entry = this.#entries[index];
|
|
80
|
+
if (entry === undefined || entry.sessionId !== sessionId)
|
|
81
|
+
continue;
|
|
82
|
+
this.#entries.splice(index, 1);
|
|
83
|
+
if (index <= this.#cursor)
|
|
84
|
+
removedBeforeCursor += 1;
|
|
85
|
+
}
|
|
86
|
+
this.#cursor -= removedBeforeCursor;
|
|
87
|
+
if (this.#cursor > this.#entries.length - 1)
|
|
88
|
+
this.#cursor = this.#entries.length - 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=selection-history.js.map
|
|
@@ -185,6 +185,14 @@ export declare class ClientSessions implements ISessions {
|
|
|
185
185
|
* per the masked-gap contract until the next open() moves the stage.
|
|
186
186
|
*/
|
|
187
187
|
clear(): void;
|
|
188
|
+
/** Whether the transient selection history has an older entry to go back to. */
|
|
189
|
+
canBack(): boolean;
|
|
190
|
+
/** Whether the transient selection history has a newer entry to go forward to. */
|
|
191
|
+
canForward(): boolean;
|
|
192
|
+
/** Select the previous history entry (prunes unreachable entries on the way). */
|
|
193
|
+
back(): void;
|
|
194
|
+
/** Select the next history entry after a back navigation. */
|
|
195
|
+
forward(): void;
|
|
188
196
|
/**
|
|
189
197
|
* Refresh the real Session baseline, reusing an in-flight pull.
|
|
190
198
|
* @returns completion of the current or newly started baseline pull.
|
|
@@ -193,6 +193,22 @@ export class ClientSessions {
|
|
|
193
193
|
clear() {
|
|
194
194
|
this.manager.clearSelection();
|
|
195
195
|
}
|
|
196
|
+
/** Whether the transient selection history has an older entry to go back to. */
|
|
197
|
+
canBack() {
|
|
198
|
+
return this.manager.canBack();
|
|
199
|
+
}
|
|
200
|
+
/** Whether the transient selection history has a newer entry to go forward to. */
|
|
201
|
+
canForward() {
|
|
202
|
+
return this.manager.canForward();
|
|
203
|
+
}
|
|
204
|
+
/** Select the previous history entry (prunes unreachable entries on the way). */
|
|
205
|
+
back() {
|
|
206
|
+
this.manager.back();
|
|
207
|
+
}
|
|
208
|
+
/** Select the next history entry after a back navigation. */
|
|
209
|
+
forward() {
|
|
210
|
+
this.manager.forward();
|
|
211
|
+
}
|
|
196
212
|
/**
|
|
197
213
|
* Refresh the real Session baseline, reusing an in-flight pull.
|
|
198
214
|
* @returns completion of the current or newly started baseline pull.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crazx/dsh-api-session-controller",
|
|
3
3
|
"description": "Session Remote commands, cold reads, and live control transport",
|
|
4
|
-
"version": "0.1.5-alpha.1.zw.
|
|
4
|
+
"version": "0.1.5-alpha.1.zw.3",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
75
75
|
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
|
|
76
|
-
"@deepseek-ai/dsh-agent-default-model": "0.1.5-alpha.1.zw.
|
|
76
|
+
"@deepseek-ai/dsh-agent-default-model": "npm:@crazx/dsh-agent-default-model@0.1.5-alpha.1.zw.3",
|
|
77
77
|
"@deepseek-ai/dsh-agent-presets": "^0.1.5-alpha.1",
|
|
78
78
|
"@deepseek-ai/dsh-api-gateway": "^0.1.5-alpha.1",
|
|
79
79
|
"@deepseek-ai/dsh-attachment": "^0.1.5-alpha.1",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"devDependencies": {
|
|
116
116
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
117
117
|
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.1",
|
|
118
|
-
"@deepseek-ai/dsh-agent-default-model": "npm:@crazx/dsh-agent-default-model@0.1.5-alpha.1.zw.
|
|
118
|
+
"@deepseek-ai/dsh-agent-default-model": "npm:@crazx/dsh-agent-default-model@0.1.5-alpha.1.zw.3",
|
|
119
119
|
"@deepseek-ai/dsh-agent-loop": "^0.1.5-alpha.1",
|
|
120
120
|
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-alpha.1",
|
|
121
121
|
"@deepseek-ai/dsh-agent-presets": "^0.1.5-alpha.1",
|