aicodeman 1.5.1 → 1.6.1
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 +48 -12
- package/README.zh-CN.md +68 -12
- package/dist/remote-hosts.d.ts +61 -1
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +142 -0
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-reconnect.d.ts +129 -0
- package/dist/remote-reconnect.d.ts.map +1 -0
- package/dist/remote-reconnect.js +119 -0
- package/dist/remote-reconnect.js.map +1 -0
- package/dist/services/unified-session-service.d.ts +13 -1
- package/dist/services/unified-session-service.d.ts.map +1 -1
- package/dist/services/unified-session-service.js +88 -3
- package/dist/services/unified-session-service.js.map +1 -1
- package/dist/session-order.d.ts +40 -0
- package/dist/session-order.d.ts.map +1 -0
- package/dist/session-order.js +67 -0
- package/dist/session-order.js.map +1 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +87 -19
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +12 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +33 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tmux-manager.d.ts +66 -0
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +226 -2
- package/dist/tmux-manager.js.map +1 -1
- package/dist/types/app-state.d.ts +2 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/session.d.ts +42 -0
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/web/public/admin-ui.js +303 -6
- package/dist/web/public/admin-ui.js.br +0 -0
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.9367fd20.js +38 -0
- package/dist/web/public/app.9367fd20.js.br +0 -0
- package/dist/web/public/app.9367fd20.js.gz +0 -0
- package/dist/web/public/{constants.ad3a8031.js → constants.21a0a83e.js} +9 -0
- package/dist/web/public/constants.21a0a83e.js.br +0 -0
- package/dist/web/public/{constants.ad3a8031.js.gz → constants.21a0a83e.js.gz} +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +34 -11
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
- package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
- package/dist/web/public/mobile.6a53092b.css +1 -0
- package/dist/web/public/mobile.6a53092b.css.br +0 -0
- package/dist/web/public/mobile.6a53092b.css.gz +0 -0
- package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.3c9b0465.js → panels-ui.58582d4c.js} +2 -2
- package/dist/web/public/panels-ui.58582d4c.js.br +0 -0
- package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/{session-ui.e5d4f9b5.js → session-ui.0cf86dd4.js} +30 -22
- package/dist/web/public/session-ui.0cf86dd4.js.br +0 -0
- package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
- package/dist/web/public/{settings-ui.29214cf1.js → settings-ui.bc5f4792.js} +2 -2
- package/dist/web/public/settings-ui.bc5f4792.js.br +0 -0
- package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
- package/dist/web/public/styles.e9dc275f.css +1 -0
- package/dist/web/public/styles.e9dc275f.css.br +0 -0
- package/dist/web/public/styles.e9dc275f.css.gz +0 -0
- package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.9ea677e9.js +3 -0
- package/dist/web/public/terminal-ui.9ea677e9.js.br +0 -0
- package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
- package/dist/web/routes/admin-routes.d.ts +2 -0
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +91 -2
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +19 -1
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +140 -3
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +13 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +31 -0
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts +9 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +57 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/sse-events.d.ts +15 -0
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +18 -0
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/app.1ccca158.js +0 -38
- package/dist/web/public/app.1ccca158.js.br +0 -0
- package/dist/web/public/app.1ccca158.js.gz +0 -0
- package/dist/web/public/constants.ad3a8031.js.br +0 -0
- package/dist/web/public/mobile.597136a6.css +0 -1
- package/dist/web/public/mobile.597136a6.css.br +0 -0
- package/dist/web/public/mobile.597136a6.css.gz +0 -0
- package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
- package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
- package/dist/web/public/session-ui.e5d4f9b5.js.br +0 -0
- package/dist/web/public/session-ui.e5d4f9b5.js.gz +0 -0
- package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
- package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
- package/dist/web/public/styles.9e6cdb9d.css +0 -1
- package/dist/web/public/styles.9e6cdb9d.css.br +0 -0
- package/dist/web/public/styles.9e6cdb9d.css.gz +0 -0
- package/dist/web/public/terminal-ui.efda6083.js +0 -3
- package/dist/web/public/terminal-ui.efda6083.js.br +0 -0
- package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure logic for the remote-session auto-reconnect watcher (COD-108).
|
|
3
|
+
*
|
|
4
|
+
* COD-104 made remote tmux sessions durable + idempotently reattachable, but a
|
|
5
|
+
* reconnect only fired at explicit trigger points. COD-108 adds a continuous
|
|
6
|
+
* watcher (in `TmuxManager`) that detects a dead remote pane and emits
|
|
7
|
+
* `remoteSessionDropped`; `SessionManager`/server then reassembles the respawn
|
|
8
|
+
* options and reattaches (re-running the idempotent remote command).
|
|
9
|
+
*
|
|
10
|
+
* This module holds the SIDE-EFFECT-FREE pieces so they can be unit-tested
|
|
11
|
+
* without real tmux:
|
|
12
|
+
* - the bounded exponential **backoff schedule** (attempt → delay, capped),
|
|
13
|
+
* - the per-session **reconnect state** shape,
|
|
14
|
+
* - the **eligibility decision** (`decideReconnect`) given a session + its
|
|
15
|
+
* reconnect state + the current time + the guard set.
|
|
16
|
+
*
|
|
17
|
+
* The watcher in `tmux-manager.ts` owns the live `isPaneDead` probe and the
|
|
18
|
+
* timers; everything here is pure and deterministic (time is injected).
|
|
19
|
+
*
|
|
20
|
+
* @module remote-reconnect
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Bounded exponential backoff delays (ms) between reconnect attempts.
|
|
24
|
+
* Attempt N (1-based) waits `BACKOFF_SCHEDULE_MS[N-1]` from the previous emit
|
|
25
|
+
* before the next emit is eligible. After the last entry the session is
|
|
26
|
+
* considered `reconnect-exhausted` and the watcher stops emitting for it.
|
|
27
|
+
*
|
|
28
|
+
* 5s, 15s, 45s, 2m, 5m, 5m → ~6 attempts spanning ~13 minutes.
|
|
29
|
+
*/
|
|
30
|
+
export declare const BACKOFF_SCHEDULE_MS: readonly number[];
|
|
31
|
+
/** Maximum number of reconnect attempts before exhaustion. */
|
|
32
|
+
export declare const MAX_RECONNECT_ATTEMPTS: number;
|
|
33
|
+
/**
|
|
34
|
+
* Delay (ms) to wait AFTER emitting attempt `attempt` (1-based) before the next
|
|
35
|
+
* attempt is eligible. `attempt <= 0` returns the first delay; an attempt at or
|
|
36
|
+
* beyond the cap returns the last delay (callers should check exhaustion via
|
|
37
|
+
* {@link isExhausted} rather than relying on this for the stop decision).
|
|
38
|
+
*
|
|
39
|
+
* Pure — no clock, no I/O.
|
|
40
|
+
*/
|
|
41
|
+
export declare function reconnectDelayForAttempt(attempt: number): number;
|
|
42
|
+
/** Whether `attempts` reconnect emits have reached/exceeded the cap. Pure. */
|
|
43
|
+
export declare function isExhausted(attempts: number): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Per-session reconnect bookkeeping held by the watcher. All time values are
|
|
46
|
+
* epoch ms. `inFlight` guards against stacking respawns when a tick fires while
|
|
47
|
+
* a previous reattach is still running. `exhaustedEmitted` ensures the
|
|
48
|
+
* `remoteReconnectExhausted` event fires at most once per session.
|
|
49
|
+
*/
|
|
50
|
+
export interface RemoteReconnectState {
|
|
51
|
+
/** Number of `remoteSessionDropped` emits so far (advances per emit). */
|
|
52
|
+
attempts: number;
|
|
53
|
+
/** Earliest time (epoch ms) the next emit is eligible. 0 = eligible now. */
|
|
54
|
+
nextEligibleAt: number;
|
|
55
|
+
/** A reattach triggered by a prior emit is currently running. */
|
|
56
|
+
inFlight: boolean;
|
|
57
|
+
/** Cap reached — stop auto-retrying for this session. */
|
|
58
|
+
exhausted: boolean;
|
|
59
|
+
/** The `remoteReconnectExhausted` SSE event has already been emitted. */
|
|
60
|
+
exhaustedEmitted: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** A fresh reconnect state (no attempts, immediately eligible). Pure. */
|
|
63
|
+
export declare function freshReconnectState(): RemoteReconnectState;
|
|
64
|
+
/**
|
|
65
|
+
* Advance the backoff after an emit at time `now`. Increments `attempts` and
|
|
66
|
+
* schedules `nextEligibleAt = now + delay`. Returns a NEW state object (does
|
|
67
|
+
* not mutate the input). Pure.
|
|
68
|
+
*
|
|
69
|
+
* NOTE: this does NOT set `exhausted`. Exhaustion is a decision the watcher
|
|
70
|
+
* makes on the FOLLOWING tick (via {@link decideReconnect} → `exhaust`), so the
|
|
71
|
+
* `remoteReconnectExhausted` event fires exactly once after the final attempt's
|
|
72
|
+
* backoff window elapses — not pre-emptively on the last emit.
|
|
73
|
+
*/
|
|
74
|
+
export declare function advanceBackoff(state: RemoteReconnectState, now: number): RemoteReconnectState;
|
|
75
|
+
/** Reset after a successful reattach — back to a fresh, eligible state. Pure. */
|
|
76
|
+
export declare function resetReconnectState(): RemoteReconnectState;
|
|
77
|
+
/** Minimal session view the decision needs (avoids importing MuxSession here). */
|
|
78
|
+
export interface ReconnectSessionView {
|
|
79
|
+
sessionId: string;
|
|
80
|
+
/** Truthy when this is a remote (SSH-wrapped) session. */
|
|
81
|
+
isRemote: boolean;
|
|
82
|
+
/** Result of `isPaneDead(muxName)` for this session. */
|
|
83
|
+
paneDead: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Decision outcomes for a single watcher tick on one session.
|
|
87
|
+
* - `emit` → emit `remoteSessionDropped { sessionId, attempt }`, then
|
|
88
|
+
* advance backoff (attempt = the returned `attempt`).
|
|
89
|
+
* - `exhaust` → cap reached this tick; emit `remoteReconnectExhausted` once.
|
|
90
|
+
* - `skip` → do nothing (not remote / pane alive / guarded / in-flight /
|
|
91
|
+
* not yet due / already exhausted).
|
|
92
|
+
*/
|
|
93
|
+
export type ReconnectAction = {
|
|
94
|
+
kind: 'emit';
|
|
95
|
+
attempt: number;
|
|
96
|
+
} | {
|
|
97
|
+
kind: 'exhaust';
|
|
98
|
+
} | {
|
|
99
|
+
kind: 'skip';
|
|
100
|
+
reason: ReconnectSkipReason;
|
|
101
|
+
};
|
|
102
|
+
export type ReconnectSkipReason = 'not-remote' | 'pane-alive' | 'guarded' | 'in-flight' | 'not-due' | 'exhausted' | 'disabled';
|
|
103
|
+
export interface DecideReconnectInput {
|
|
104
|
+
session: ReconnectSessionView;
|
|
105
|
+
state: RemoteReconnectState | undefined;
|
|
106
|
+
/** Session is in the intentional-teardown guard set (killed/detached/stopping). */
|
|
107
|
+
guarded: boolean;
|
|
108
|
+
/** Kill-switch: `remoteAutoReconnect` setting. When false, never reconnect. */
|
|
109
|
+
enabled: boolean;
|
|
110
|
+
now: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* PURE eligibility decision for one session on one tick. No clock, no I/O — all
|
|
114
|
+
* inputs are passed in. The watcher translates the result into emits + state
|
|
115
|
+
* transitions.
|
|
116
|
+
*
|
|
117
|
+
* Order of guards (most-decisive first):
|
|
118
|
+
* 1. kill-switch off → skip:disabled
|
|
119
|
+
* 2. not a remote session → skip:not-remote
|
|
120
|
+
* 3. pane is alive → skip:pane-alive
|
|
121
|
+
* 4. intentional teardown guard → skip:guarded (NEVER revive a killed tab)
|
|
122
|
+
* 5. a reattach already running → skip:in-flight (no stacked respawns)
|
|
123
|
+
* 6. already exhausted → skip:exhausted (one exhaust emit, then quiet)
|
|
124
|
+
* 7. cap reached this tick → exhaust
|
|
125
|
+
* 8. not yet due (backoff) → skip:not-due
|
|
126
|
+
* 9. otherwise → emit (attempt = attempts + 1)
|
|
127
|
+
*/
|
|
128
|
+
export declare function decideReconnect(input: DecideReconnectInput): ReconnectAction;
|
|
129
|
+
//# sourceMappingURL=remote-reconnect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-reconnect.d.ts","sourceRoot":"","sources":["../src/remote-reconnect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAuD,CAAC;AAEzG,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,QAA6B,CAAC;AAEjE;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIhE;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,SAAS,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,IAAI,oBAAoB,CAE1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAQ7F;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,IAAI,oBAAoB,CAE1D;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,QAAQ,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,YAAY,GACZ,SAAS,GACT,WAAW,GACX,SAAS,GACT,WAAW,GACX,UAAU,CAAC;AAEf,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,KAAK,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACxC,mFAAmF;IACnF,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,eAAe,CAuB5E"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure logic for the remote-session auto-reconnect watcher (COD-108).
|
|
3
|
+
*
|
|
4
|
+
* COD-104 made remote tmux sessions durable + idempotently reattachable, but a
|
|
5
|
+
* reconnect only fired at explicit trigger points. COD-108 adds a continuous
|
|
6
|
+
* watcher (in `TmuxManager`) that detects a dead remote pane and emits
|
|
7
|
+
* `remoteSessionDropped`; `SessionManager`/server then reassembles the respawn
|
|
8
|
+
* options and reattaches (re-running the idempotent remote command).
|
|
9
|
+
*
|
|
10
|
+
* This module holds the SIDE-EFFECT-FREE pieces so they can be unit-tested
|
|
11
|
+
* without real tmux:
|
|
12
|
+
* - the bounded exponential **backoff schedule** (attempt → delay, capped),
|
|
13
|
+
* - the per-session **reconnect state** shape,
|
|
14
|
+
* - the **eligibility decision** (`decideReconnect`) given a session + its
|
|
15
|
+
* reconnect state + the current time + the guard set.
|
|
16
|
+
*
|
|
17
|
+
* The watcher in `tmux-manager.ts` owns the live `isPaneDead` probe and the
|
|
18
|
+
* timers; everything here is pure and deterministic (time is injected).
|
|
19
|
+
*
|
|
20
|
+
* @module remote-reconnect
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Bounded exponential backoff delays (ms) between reconnect attempts.
|
|
24
|
+
* Attempt N (1-based) waits `BACKOFF_SCHEDULE_MS[N-1]` from the previous emit
|
|
25
|
+
* before the next emit is eligible. After the last entry the session is
|
|
26
|
+
* considered `reconnect-exhausted` and the watcher stops emitting for it.
|
|
27
|
+
*
|
|
28
|
+
* 5s, 15s, 45s, 2m, 5m, 5m → ~6 attempts spanning ~13 minutes.
|
|
29
|
+
*/
|
|
30
|
+
export const BACKOFF_SCHEDULE_MS = [5_000, 15_000, 45_000, 120_000, 300_000, 300_000];
|
|
31
|
+
/** Maximum number of reconnect attempts before exhaustion. */
|
|
32
|
+
export const MAX_RECONNECT_ATTEMPTS = BACKOFF_SCHEDULE_MS.length;
|
|
33
|
+
/**
|
|
34
|
+
* Delay (ms) to wait AFTER emitting attempt `attempt` (1-based) before the next
|
|
35
|
+
* attempt is eligible. `attempt <= 0` returns the first delay; an attempt at or
|
|
36
|
+
* beyond the cap returns the last delay (callers should check exhaustion via
|
|
37
|
+
* {@link isExhausted} rather than relying on this for the stop decision).
|
|
38
|
+
*
|
|
39
|
+
* Pure — no clock, no I/O.
|
|
40
|
+
*/
|
|
41
|
+
export function reconnectDelayForAttempt(attempt) {
|
|
42
|
+
if (!Number.isFinite(attempt) || attempt <= 1)
|
|
43
|
+
return BACKOFF_SCHEDULE_MS[0];
|
|
44
|
+
const idx = Math.min(Math.floor(attempt) - 1, BACKOFF_SCHEDULE_MS.length - 1);
|
|
45
|
+
return BACKOFF_SCHEDULE_MS[idx];
|
|
46
|
+
}
|
|
47
|
+
/** Whether `attempts` reconnect emits have reached/exceeded the cap. Pure. */
|
|
48
|
+
export function isExhausted(attempts) {
|
|
49
|
+
return attempts >= MAX_RECONNECT_ATTEMPTS;
|
|
50
|
+
}
|
|
51
|
+
/** A fresh reconnect state (no attempts, immediately eligible). Pure. */
|
|
52
|
+
export function freshReconnectState() {
|
|
53
|
+
return { attempts: 0, nextEligibleAt: 0, inFlight: false, exhausted: false, exhaustedEmitted: false };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Advance the backoff after an emit at time `now`. Increments `attempts` and
|
|
57
|
+
* schedules `nextEligibleAt = now + delay`. Returns a NEW state object (does
|
|
58
|
+
* not mutate the input). Pure.
|
|
59
|
+
*
|
|
60
|
+
* NOTE: this does NOT set `exhausted`. Exhaustion is a decision the watcher
|
|
61
|
+
* makes on the FOLLOWING tick (via {@link decideReconnect} → `exhaust`), so the
|
|
62
|
+
* `remoteReconnectExhausted` event fires exactly once after the final attempt's
|
|
63
|
+
* backoff window elapses — not pre-emptively on the last emit.
|
|
64
|
+
*/
|
|
65
|
+
export function advanceBackoff(state, now) {
|
|
66
|
+
const attempts = state.attempts + 1;
|
|
67
|
+
const delay = reconnectDelayForAttempt(attempts);
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
attempts,
|
|
71
|
+
nextEligibleAt: now + delay,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Reset after a successful reattach — back to a fresh, eligible state. Pure. */
|
|
75
|
+
export function resetReconnectState() {
|
|
76
|
+
return freshReconnectState();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* PURE eligibility decision for one session on one tick. No clock, no I/O — all
|
|
80
|
+
* inputs are passed in. The watcher translates the result into emits + state
|
|
81
|
+
* transitions.
|
|
82
|
+
*
|
|
83
|
+
* Order of guards (most-decisive first):
|
|
84
|
+
* 1. kill-switch off → skip:disabled
|
|
85
|
+
* 2. not a remote session → skip:not-remote
|
|
86
|
+
* 3. pane is alive → skip:pane-alive
|
|
87
|
+
* 4. intentional teardown guard → skip:guarded (NEVER revive a killed tab)
|
|
88
|
+
* 5. a reattach already running → skip:in-flight (no stacked respawns)
|
|
89
|
+
* 6. already exhausted → skip:exhausted (one exhaust emit, then quiet)
|
|
90
|
+
* 7. cap reached this tick → exhaust
|
|
91
|
+
* 8. not yet due (backoff) → skip:not-due
|
|
92
|
+
* 9. otherwise → emit (attempt = attempts + 1)
|
|
93
|
+
*/
|
|
94
|
+
export function decideReconnect(input) {
|
|
95
|
+
const { session, state, guarded, enabled, now } = input;
|
|
96
|
+
if (!enabled)
|
|
97
|
+
return { kind: 'skip', reason: 'disabled' };
|
|
98
|
+
if (!session.isRemote)
|
|
99
|
+
return { kind: 'skip', reason: 'not-remote' };
|
|
100
|
+
if (!session.paneDead)
|
|
101
|
+
return { kind: 'skip', reason: 'pane-alive' };
|
|
102
|
+
// Intentional kill / detach must NEVER be auto-revived.
|
|
103
|
+
if (guarded)
|
|
104
|
+
return { kind: 'skip', reason: 'guarded' };
|
|
105
|
+
const s = state ?? freshReconnectState();
|
|
106
|
+
// Only one reconnect in flight per session — don't stack respawns.
|
|
107
|
+
if (s.inFlight)
|
|
108
|
+
return { kind: 'skip', reason: 'in-flight' };
|
|
109
|
+
if (s.exhausted)
|
|
110
|
+
return { kind: 'skip', reason: 'exhausted' };
|
|
111
|
+
// Cap reached: surface exhaustion once, then go quiet.
|
|
112
|
+
if (isExhausted(s.attempts))
|
|
113
|
+
return { kind: 'exhaust' };
|
|
114
|
+
// Backoff gate — only emit when due.
|
|
115
|
+
if (now < s.nextEligibleAt)
|
|
116
|
+
return { kind: 'skip', reason: 'not-due' };
|
|
117
|
+
return { kind: 'emit', attempt: s.attempts + 1 };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=remote-reconnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-reconnect.js","sourceRoot":"","sources":["../src/remote-reconnect.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,9 +27,15 @@ export type UnifiedSessionItem = {
|
|
|
27
27
|
lastActivityAt?: number;
|
|
28
28
|
claudeSessionId?: string;
|
|
29
29
|
firstPrompt?: string;
|
|
30
|
+
/** Most recent user prompt from the transcript (COD-145), parallel to firstPrompt. */
|
|
31
|
+
lastPrompt?: string;
|
|
30
32
|
sizeBytes?: number;
|
|
31
33
|
projectKey?: string;
|
|
32
34
|
remote?: boolean;
|
|
35
|
+
/** Pinned to the top of the session manager list (COD-139). */
|
|
36
|
+
pinned?: boolean;
|
|
37
|
+
/** When the session was pinned (epoch ms) — orders the pinned group desc. */
|
|
38
|
+
pinnedAt?: number;
|
|
33
39
|
sources: string[];
|
|
34
40
|
stats?: {
|
|
35
41
|
memoryMB: number;
|
|
@@ -47,6 +53,8 @@ export type LiveSessionInput = {
|
|
|
47
53
|
createdAt?: number;
|
|
48
54
|
lastActivityAt?: number;
|
|
49
55
|
claudeSessionId?: string;
|
|
56
|
+
pinned?: boolean;
|
|
57
|
+
pinnedAt?: number;
|
|
50
58
|
};
|
|
51
59
|
/** Persisted session view (subset of `SessionState`). */
|
|
52
60
|
export type PersistedSessionInput = {
|
|
@@ -59,6 +67,8 @@ export type PersistedSessionInput = {
|
|
|
59
67
|
lastActivityAt?: number;
|
|
60
68
|
/** Claude conversation ID this session resumes (`SessionState.resumeSessionId`). */
|
|
61
69
|
claudeSessionId?: string;
|
|
70
|
+
pinned?: boolean;
|
|
71
|
+
pinnedAt?: number;
|
|
62
72
|
};
|
|
63
73
|
/** Lifecycle audit-log view. Entries are expected NEWEST-first (the order `SessionLifecycleLog.query()` returns). */
|
|
64
74
|
export type LifecycleInput = {
|
|
@@ -75,6 +85,8 @@ export type HistoryInput = {
|
|
|
75
85
|
sizeBytes: number;
|
|
76
86
|
lastModified: string;
|
|
77
87
|
firstPrompt?: string;
|
|
88
|
+
/** Most recent user prompt from the transcript (COD-145). */
|
|
89
|
+
lastPrompt?: string;
|
|
78
90
|
projectKey?: string;
|
|
79
91
|
};
|
|
80
92
|
/** Mux process-stat view. */
|
|
@@ -101,7 +113,7 @@ export type UnifiedSources = {
|
|
|
101
113
|
*/
|
|
102
114
|
export declare function mergeUnifiedSessions(sources: UnifiedSources): UnifiedSessionItem[];
|
|
103
115
|
/**
|
|
104
|
-
* Case-insensitive substring filter (name + firstPrompt + workingDir + sessionId)
|
|
116
|
+
* Case-insensitive substring filter (name + firstPrompt + lastPrompt + workingDir + sessionId)
|
|
105
117
|
* with offset/limit paging. `total` is the filtered count BEFORE paging.
|
|
106
118
|
*/
|
|
107
119
|
export declare function filterAndPaginate(items: UnifiedSessionItem[], opts: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unified-session-service.d.ts","sourceRoot":"","sources":["../../src/services/unified-session-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD,CAAC;AAEF,mEAAmE;AACnE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"unified-session-service.d.ts","sourceRoot":"","sources":["../../src/services/unified-session-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -63,6 +63,7 @@ export function mergeUnifiedSessions(sources) {
|
|
|
63
63
|
overwrite(item, 'workingDir', h.workingDir);
|
|
64
64
|
overwrite(item, 'sizeBytes', h.sizeBytes);
|
|
65
65
|
overwrite(item, 'firstPrompt', h.firstPrompt);
|
|
66
|
+
overwrite(item, 'lastPrompt', h.lastPrompt);
|
|
66
67
|
overwrite(item, 'projectKey', h.projectKey);
|
|
67
68
|
const ms = Date.parse(h.lastModified);
|
|
68
69
|
if (!Number.isNaN(ms) && item.lastActivityAt === undefined)
|
|
@@ -91,6 +92,8 @@ export function mergeUnifiedSessions(sources) {
|
|
|
91
92
|
overwrite(item, 'workingDir', p.workingDir);
|
|
92
93
|
overwrite(item, 'createdAt', p.createdAt);
|
|
93
94
|
overwrite(item, 'lastActivityAt', p.lastActivityAt);
|
|
95
|
+
overwrite(item, 'pinned', p.pinned);
|
|
96
|
+
overwrite(item, 'pinnedAt', p.pinnedAt);
|
|
94
97
|
}
|
|
95
98
|
// 4) live (highest precedence)
|
|
96
99
|
for (const v of sources.live ?? []) {
|
|
@@ -104,6 +107,8 @@ export function mergeUnifiedSessions(sources) {
|
|
|
104
107
|
overwrite(item, 'createdAt', v.createdAt);
|
|
105
108
|
overwrite(item, 'lastActivityAt', v.lastActivityAt);
|
|
106
109
|
overwrite(item, 'claudeSessionId', v.claudeSessionId);
|
|
110
|
+
overwrite(item, 'pinned', v.pinned);
|
|
111
|
+
overwrite(item, 'pinnedAt', v.pinnedAt);
|
|
107
112
|
}
|
|
108
113
|
// 5) mux stats + remote flag (create item if mux-only)
|
|
109
114
|
for (const m of sources.mux ?? []) {
|
|
@@ -115,6 +120,67 @@ export function mergeUnifiedSessions(sources) {
|
|
|
115
120
|
if (m.remote !== undefined)
|
|
116
121
|
item.remote = m.remote;
|
|
117
122
|
}
|
|
123
|
+
// firstPrompt backfill (COD-140): the only source that sets firstPrompt is the
|
|
124
|
+
// transcript-history view, keyed by the Claude transcript file's UUID. A live/persisted
|
|
125
|
+
// row keyed by its Codeman id only inherits firstPrompt when that id happens to equal an
|
|
126
|
+
// on-disk transcript UUID. When it doesn't (stale/wrong claudeSessionId, post-/clear new
|
|
127
|
+
// uuid, resumed/attached/worktree session, transcript not yet flushed), the row shows
|
|
128
|
+
// "(no prompt captured)" even though a real transcript for that working dir exists under a
|
|
129
|
+
// different UUID. Backfill from the already-passed history: first try the claudeSessionId
|
|
130
|
+
// join, then the newest transcript in the same workingDir. Never overwrite a non-empty
|
|
131
|
+
// firstPrompt (so rows keyed to their own transcript are untouched).
|
|
132
|
+
const firstPromptByUuid = new Map();
|
|
133
|
+
const firstPromptByWorkingDir = new Map();
|
|
134
|
+
// COD-145: lastPrompt rides the same backfill (build parallel indexes; never overwrite).
|
|
135
|
+
const lastPromptByUuid = new Map();
|
|
136
|
+
const lastPromptByWorkingDir = new Map();
|
|
137
|
+
for (const h of sources.history ?? []) {
|
|
138
|
+
const ms = Date.parse(h.lastModified);
|
|
139
|
+
const ts = Number.isNaN(ms) ? -Infinity : ms;
|
|
140
|
+
if (h.firstPrompt) {
|
|
141
|
+
firstPromptByUuid.set(h.sessionId, h.firstPrompt);
|
|
142
|
+
if (h.workingDir) {
|
|
143
|
+
const existing = firstPromptByWorkingDir.get(h.workingDir);
|
|
144
|
+
if (!existing || ts > existing.ms) {
|
|
145
|
+
firstPromptByWorkingDir.set(h.workingDir, { prompt: h.firstPrompt, ms: ts });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (h.lastPrompt) {
|
|
150
|
+
lastPromptByUuid.set(h.sessionId, h.lastPrompt);
|
|
151
|
+
if (h.workingDir) {
|
|
152
|
+
const existing = lastPromptByWorkingDir.get(h.workingDir);
|
|
153
|
+
if (!existing || ts > existing.ms) {
|
|
154
|
+
lastPromptByWorkingDir.set(h.workingDir, { prompt: h.lastPrompt, ms: ts });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const item of map.values()) {
|
|
160
|
+
if (!item.firstPrompt) {
|
|
161
|
+
// never overwrite an existing non-empty prompt
|
|
162
|
+
const byUuid = item.claudeSessionId ? firstPromptByUuid.get(item.claudeSessionId) : undefined;
|
|
163
|
+
if (byUuid) {
|
|
164
|
+
item.firstPrompt = byUuid;
|
|
165
|
+
}
|
|
166
|
+
else if (item.workingDir) {
|
|
167
|
+
const byDir = firstPromptByWorkingDir.get(item.workingDir);
|
|
168
|
+
if (byDir)
|
|
169
|
+
item.firstPrompt = byDir.prompt;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!item.lastPrompt) {
|
|
173
|
+
const byUuid = item.claudeSessionId ? lastPromptByUuid.get(item.claudeSessionId) : undefined;
|
|
174
|
+
if (byUuid) {
|
|
175
|
+
item.lastPrompt = byUuid;
|
|
176
|
+
}
|
|
177
|
+
else if (item.workingDir) {
|
|
178
|
+
const byDir = lastPromptByWorkingDir.get(item.workingDir);
|
|
179
|
+
if (byDir)
|
|
180
|
+
item.lastPrompt = byDir.prompt;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
118
184
|
// Meaningfulness floor: keep real rows, drop bare lifecycle/mux-only noise.
|
|
119
185
|
const kept = [];
|
|
120
186
|
for (const item of map.values()) {
|
|
@@ -125,8 +191,27 @@ export function mergeUnifiedSessions(sources) {
|
|
|
125
191
|
if (isReal)
|
|
126
192
|
kept.push(item);
|
|
127
193
|
}
|
|
128
|
-
// Stable sort:
|
|
194
|
+
// Stable sort (COD-139): pinned group first (pinnedAt desc, most-recently-pinned
|
|
195
|
+
// first), then unpinned by lastActivityAt desc (undefined last), createdAt desc,
|
|
196
|
+
// sessionId asc.
|
|
129
197
|
kept.sort((a, b) => {
|
|
198
|
+
const pa = a.pinned === true;
|
|
199
|
+
const pb = b.pinned === true;
|
|
200
|
+
if (pa !== pb)
|
|
201
|
+
return pa ? -1 : 1; // pinned floats above unpinned
|
|
202
|
+
if (pa && pb) {
|
|
203
|
+
// Both pinned: most-recently-pinned first (undefined pinnedAt sorts last).
|
|
204
|
+
const ta = a.pinnedAt;
|
|
205
|
+
const tb = b.pinnedAt;
|
|
206
|
+
if (ta !== tb) {
|
|
207
|
+
if (ta === undefined)
|
|
208
|
+
return 1;
|
|
209
|
+
if (tb === undefined)
|
|
210
|
+
return -1;
|
|
211
|
+
return tb - ta;
|
|
212
|
+
}
|
|
213
|
+
// tie-break falls through to the activity/createdAt/id rules below.
|
|
214
|
+
}
|
|
130
215
|
const la = a.lastActivityAt;
|
|
131
216
|
const lb = b.lastActivityAt;
|
|
132
217
|
if (la !== lb) {
|
|
@@ -150,14 +235,14 @@ export function mergeUnifiedSessions(sources) {
|
|
|
150
235
|
return kept;
|
|
151
236
|
}
|
|
152
237
|
/**
|
|
153
|
-
* Case-insensitive substring filter (name + firstPrompt + workingDir + sessionId)
|
|
238
|
+
* Case-insensitive substring filter (name + firstPrompt + lastPrompt + workingDir + sessionId)
|
|
154
239
|
* with offset/limit paging. `total` is the filtered count BEFORE paging.
|
|
155
240
|
*/
|
|
156
241
|
export function filterAndPaginate(items, opts) {
|
|
157
242
|
const q = (opts.q ?? '').trim().toLowerCase();
|
|
158
243
|
const filtered = q
|
|
159
244
|
? items.filter((it) => {
|
|
160
|
-
const hay = [it.name, it.firstPrompt, it.workingDir, it.sessionId]
|
|
245
|
+
const hay = [it.name, it.firstPrompt, it.lastPrompt, it.workingDir, it.sessionId]
|
|
161
246
|
.filter((v) => typeof v === 'string')
|
|
162
247
|
.join(' ')
|
|
163
248
|
.toLowerCase();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unified-session-service.js","sourceRoot":"","sources":["../../src/services/unified-session-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"unified-session-service.js","sourceRoot":"","sources":["../../src/services/unified-session-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helpers for the global session tab-order (COD-131).
|
|
3
|
+
*
|
|
4
|
+
* Tab order (drag-and-drop reorder + Ctrl+Shift+{/}) is persisted server-side
|
|
5
|
+
* so it follows the user across devices. The server is authoritative; the
|
|
6
|
+
* browser's localStorage (`codeman-session-order`) is the offline fallback.
|
|
7
|
+
*
|
|
8
|
+
* These helpers are pure (no IO) so they can be unit-tested in isolation and
|
|
9
|
+
* reused by both the PUT /api/session-order route and the StateStore accessor.
|
|
10
|
+
*
|
|
11
|
+
* - `normalizeSessionOrder` coerces arbitrary input into a clean string[]
|
|
12
|
+
* (non-empty strings only, deduped with first occurrence winning).
|
|
13
|
+
* - `mergeSessionOrder` lets the pushing device's order win, while preserving
|
|
14
|
+
* any server-only ids the pushing device didn't know about — they fall to the
|
|
15
|
+
* END in their existing relative order, never dropped.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Coerce arbitrary input into a clean ordered list of session ids:
|
|
19
|
+
* keep only non-empty strings and dedup (first occurrence wins).
|
|
20
|
+
*
|
|
21
|
+
* @param order - unknown input (expected to be a string[], but defensive)
|
|
22
|
+
* @returns a normalized string[] (empty array for non-array / all-junk input)
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeSessionOrder(order: unknown): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Merge an incoming order from a pushing device with the existing server order.
|
|
27
|
+
*
|
|
28
|
+
* The incoming order wins; any ids present in `existing` but NOT in `incoming`
|
|
29
|
+
* are appended at the END, preserving their relative order. This is the
|
|
30
|
+
* "server-only ids the pushing device didn't know about fall to the end, never
|
|
31
|
+
* dropped" rule.
|
|
32
|
+
*
|
|
33
|
+
* Both arguments are normalized first, so callers may pass raw input safely.
|
|
34
|
+
*
|
|
35
|
+
* @param incoming - the order the pushing device wants
|
|
36
|
+
* @param existing - the current server-side order
|
|
37
|
+
* @returns the merged, normalized order
|
|
38
|
+
*/
|
|
39
|
+
export declare function mergeSessionOrder(incoming: string[], existing: string[]): string[];
|
|
40
|
+
//# sourceMappingURL=session-order.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-order.d.ts","sourceRoot":"","sources":["../src/session-order.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAiB9D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAUlF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure helpers for the global session tab-order (COD-131).
|
|
3
|
+
*
|
|
4
|
+
* Tab order (drag-and-drop reorder + Ctrl+Shift+{/}) is persisted server-side
|
|
5
|
+
* so it follows the user across devices. The server is authoritative; the
|
|
6
|
+
* browser's localStorage (`codeman-session-order`) is the offline fallback.
|
|
7
|
+
*
|
|
8
|
+
* These helpers are pure (no IO) so they can be unit-tested in isolation and
|
|
9
|
+
* reused by both the PUT /api/session-order route and the StateStore accessor.
|
|
10
|
+
*
|
|
11
|
+
* - `normalizeSessionOrder` coerces arbitrary input into a clean string[]
|
|
12
|
+
* (non-empty strings only, deduped with first occurrence winning).
|
|
13
|
+
* - `mergeSessionOrder` lets the pushing device's order win, while preserving
|
|
14
|
+
* any server-only ids the pushing device didn't know about — they fall to the
|
|
15
|
+
* END in their existing relative order, never dropped.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Coerce arbitrary input into a clean ordered list of session ids:
|
|
19
|
+
* keep only non-empty strings and dedup (first occurrence wins).
|
|
20
|
+
*
|
|
21
|
+
* @param order - unknown input (expected to be a string[], but defensive)
|
|
22
|
+
* @returns a normalized string[] (empty array for non-array / all-junk input)
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeSessionOrder(order) {
|
|
25
|
+
if (!Array.isArray(order)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const result = [];
|
|
30
|
+
for (const entry of order) {
|
|
31
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(entry)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
seen.add(entry);
|
|
38
|
+
result.push(entry);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Merge an incoming order from a pushing device with the existing server order.
|
|
44
|
+
*
|
|
45
|
+
* The incoming order wins; any ids present in `existing` but NOT in `incoming`
|
|
46
|
+
* are appended at the END, preserving their relative order. This is the
|
|
47
|
+
* "server-only ids the pushing device didn't know about fall to the end, never
|
|
48
|
+
* dropped" rule.
|
|
49
|
+
*
|
|
50
|
+
* Both arguments are normalized first, so callers may pass raw input safely.
|
|
51
|
+
*
|
|
52
|
+
* @param incoming - the order the pushing device wants
|
|
53
|
+
* @param existing - the current server-side order
|
|
54
|
+
* @returns the merged, normalized order
|
|
55
|
+
*/
|
|
56
|
+
export function mergeSessionOrder(incoming, existing) {
|
|
57
|
+
const normalizedIncoming = normalizeSessionOrder(incoming);
|
|
58
|
+
const incomingSet = new Set(normalizedIncoming);
|
|
59
|
+
const merged = [...normalizedIncoming];
|
|
60
|
+
for (const id of normalizeSessionOrder(existing)) {
|
|
61
|
+
if (!incomingSet.has(id)) {
|
|
62
|
+
merged.push(id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=session-order.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-order.js","sourceRoot":"","sources":["../src/session-order.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,SAAS;QACX,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACpB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAkB,EAAE,QAAkB;IACtE,MAAM,kBAAkB,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAC3D,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,CAAC,GAAG,kBAAkB,CAAC,CAAC;IACvC,KAAK,MAAM,EAAE,IAAI,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|