@anionex/dsh-pinned-sessions 0.1.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.
@@ -0,0 +1,116 @@
1
+ export const PIN_STORAGE_KEY = 'dsh.pinned-sessions.v1'
2
+ const MAX_PINS = 500
3
+
4
+ export interface PinRecord {
5
+ readonly id: string
6
+ readonly pinnedAt: number
7
+ }
8
+
9
+ interface PersistedPins {
10
+ readonly version: 1
11
+ readonly pins: readonly PinRecord[]
12
+ }
13
+
14
+ export interface StorageLike {
15
+ getItem(key: string): string | null
16
+ setItem(key: string, value: string): void
17
+ }
18
+
19
+ function isPinRecord(value: unknown): value is PinRecord {
20
+ if (typeof value !== 'object' || value === null) return false
21
+ const row = value as Record<string, unknown>
22
+ return typeof row.id === 'string'
23
+ && row.id.length > 0
24
+ && Number.isFinite(row.pinnedAt)
25
+ && (row.pinnedAt as number) >= 0
26
+ }
27
+
28
+ /** Decode, de-duplicate, and normalize one persisted pin snapshot. */
29
+ export function decodePins(raw: string | null): readonly PinRecord[] {
30
+ if (raw === null) return []
31
+ try {
32
+ const parsed = JSON.parse(raw) as Partial<PersistedPins>
33
+ if (parsed.version !== 1 || !Array.isArray(parsed.pins)) return []
34
+ const byId = new Map<string, PinRecord>()
35
+ for (const candidate of parsed.pins) {
36
+ if (!isPinRecord(candidate)) continue
37
+ const previous = byId.get(candidate.id)
38
+ if (previous === undefined || candidate.pinnedAt > previous.pinnedAt) {
39
+ byId.set(candidate.id, { id: candidate.id, pinnedAt: candidate.pinnedAt })
40
+ }
41
+ }
42
+ return [...byId.values()]
43
+ .sort((left, right) => right.pinnedAt - left.pinnedAt || left.id.localeCompare(right.id))
44
+ .slice(0, MAX_PINS)
45
+ } catch {
46
+ return []
47
+ }
48
+ }
49
+
50
+ /** Browser-local, versioned pin state with React-compatible subscriptions. */
51
+ export class PinStore {
52
+ private snapshot: readonly PinRecord[]
53
+ private readonly listeners = new Set<() => void>()
54
+
55
+ constructor(private readonly storage: StorageLike | undefined) {
56
+ this.snapshot = this.read()
57
+ }
58
+
59
+ readonly getSnapshot = (): readonly PinRecord[] => this.snapshot
60
+
61
+ readonly subscribe = (listener: () => void): (() => void) => {
62
+ this.listeners.add(listener)
63
+ return () => { this.listeners.delete(listener) }
64
+ }
65
+
66
+ isPinned(id: string): boolean {
67
+ return this.snapshot.some(row => row.id === id)
68
+ }
69
+
70
+ toggle(id: string, now = Date.now()): void {
71
+ if (id.length === 0) return
72
+ const without = this.snapshot.filter(row => row.id !== id)
73
+ this.publish(this.isPinned(id) ? without : [{ id, pinnedAt: now }, ...without])
74
+ }
75
+
76
+ prune(keep: (id: string) => boolean): void {
77
+ const next = this.snapshot.filter(row => keep(row.id))
78
+ if (next.length !== this.snapshot.length) this.publish(next)
79
+ }
80
+
81
+ reload(): void {
82
+ const next = this.read()
83
+ if (samePins(this.snapshot, next)) return
84
+ this.snapshot = next
85
+ this.emit()
86
+ }
87
+
88
+ private read(): readonly PinRecord[] {
89
+ try {
90
+ return decodePins(this.storage?.getItem(PIN_STORAGE_KEY) ?? null)
91
+ } catch {
92
+ return []
93
+ }
94
+ }
95
+
96
+ private publish(next: readonly PinRecord[]): void {
97
+ this.snapshot = [...next]
98
+ .sort((left, right) => right.pinnedAt - left.pinnedAt || left.id.localeCompare(right.id))
99
+ .slice(0, MAX_PINS)
100
+ try {
101
+ this.storage?.setItem(PIN_STORAGE_KEY, JSON.stringify({ version: 1, pins: this.snapshot }))
102
+ } catch {
103
+ // Storage can be unavailable in hardened or quota-constrained browser profiles.
104
+ }
105
+ this.emit()
106
+ }
107
+
108
+ private emit(): void {
109
+ for (const listener of [...this.listeners]) listener()
110
+ }
111
+ }
112
+
113
+ function samePins(left: readonly PinRecord[], right: readonly PinRecord[]): boolean {
114
+ return left.length === right.length
115
+ && left.every((row, index) => row.id === right[index]?.id && row.pinnedAt === right[index]?.pinnedAt)
116
+ }
@@ -0,0 +1,27 @@
1
+ interface SessionPresentationInput {
2
+ readonly id: string
3
+ readonly title?: string | undefined
4
+ readonly cwd?: string | undefined
5
+ }
6
+
7
+ function nonBlank(value: string | undefined): string | undefined {
8
+ const trimmed = value?.trim()
9
+ return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed
10
+ }
11
+
12
+ /** Match the native sidebar's useful fallback for sessions without a title. */
13
+ export function sessionDisplayTitle(
14
+ session: SessionPresentationInput,
15
+ workspaceTitle: string | undefined,
16
+ ): string {
17
+ const explicit = nonBlank(session.title)
18
+ if (explicit !== undefined) return explicit
19
+ const workspace = nonBlank(workspaceTitle)
20
+ if (workspace !== undefined) return workspace
21
+ const cwd = nonBlank(session.cwd)
22
+ if (cwd !== undefined) {
23
+ const leaf = cwd.replace(/[\\/]+$/u, '').split(/[\\/]/u).at(-1)
24
+ if (leaf !== undefined && leaf.length > 0) return leaf
25
+ }
26
+ return session.id
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Pure-UI host marker for the DSH Pinned Sessions profile bundle. */
2
+ export function apply(): void {}