dsh-manage-session 0.5.7 → 2.0.4

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 (67) hide show
  1. package/CHANGELOG.ja.md +33 -229
  2. package/CHANGELOG.ko.md +33 -229
  3. package/CHANGELOG.md +33 -363
  4. package/INSTALL.ja.md +164 -164
  5. package/INSTALL.ko.md +164 -164
  6. package/INSTALL.md +164 -164
  7. package/INSTALL.zh.md +164 -164
  8. package/LICENSE +21 -21
  9. package/README.en.md +41 -46
  10. package/README.ja.md +33 -57
  11. package/README.ko.md +32 -55
  12. package/README.md +64 -64
  13. package/cordis.patch.yml +19 -16
  14. package/dsh.plugin.json +4 -4
  15. package/lib/client.js +661 -3082
  16. package/lib/index.d.ts +79 -970
  17. package/lib/index.js +249 -2669
  18. package/locale/de.json +2 -2
  19. package/locale/en.json +2 -2
  20. package/locale/es.json +2 -2
  21. package/locale/fr.json +2 -2
  22. package/locale/it.json +2 -2
  23. package/locale/ja.json +2 -2
  24. package/locale/ko.json +2 -2
  25. package/locale/ru.json +2 -2
  26. package/locale/zh.json +2 -2
  27. package/package.json +15 -25
  28. package/screenshots.json +3 -3
  29. package/src/client/bridge-scope.ts +202 -213
  30. package/src/client/card.tsx +154 -154
  31. package/src/client/health-panel.tsx +49 -49
  32. package/src/client/host-api.ts +72 -6
  33. package/src/client/index.ts +594 -503
  34. package/src/client/locales.ts +893 -893
  35. package/src/client/manage-locales.ts +178 -0
  36. package/src/client/{search/manage.tsx → manage.tsx} +430 -404
  37. package/src/client/panel.tsx +131 -129
  38. package/src/client/session-menu-legacy.ts +2 -3
  39. package/src/client/session-menu.tsx +5 -5
  40. package/src/config.ts +43 -99
  41. package/src/host/health/attribution.ts +118 -118
  42. package/src/host/health/lossless.ts +128 -128
  43. package/src/host/health/repair.ts +2 -2
  44. package/src/host/health/source-kind.ts +350 -350
  45. package/src/host/history/archive-source.ts +250 -250
  46. package/src/host/history/archive.ts +249 -249
  47. package/src/host/history/favorites.ts +71 -71
  48. package/src/host/history/purge.ts +370 -370
  49. package/src/host/history/tombstones.ts +3 -3
  50. package/src/host/registry/archive-registry.ts +317 -317
  51. package/src/host/settings-bridge.ts +222 -264
  52. package/src/index.ts +66 -694
  53. package/src/client/search/card.tsx +0 -563
  54. package/src/client/search/host-api.ts +0 -213
  55. package/src/client/search/locales.ts +0 -861
  56. package/src/client/search/locate.ts +0 -330
  57. package/src/client/search/panel.tsx +0 -940
  58. package/src/client/search/platform.ts +0 -136
  59. package/src/host/index/archive-source.ts +0 -90
  60. package/src/host/index/drs-source.ts +0 -211
  61. package/src/host/index/engine.ts +0 -699
  62. package/src/host/index/extract.ts +0 -272
  63. package/src/host/index/peers.ts +0 -62
  64. package/src/host/index/rebuild.ts +0 -432
  65. package/src/host/index/schema.ts +0 -136
  66. package/src/host/index/snapshot.ts +0 -135
  67. package/src/host/index/sync.ts +0 -413
@@ -1,136 +0,0 @@
1
- /**
2
- * Platform detection and the keyboard-label vocabulary, for the client half.
3
- *
4
- * Why this module exists
5
- * ----------------------
6
- * A shortcut hint is the one place where a single hard-coded string is wrong
7
- * for most users: `⌘K` is meaningless on Windows, `Ctrl K` is wrong on macOS.
8
- * The obvious implementation — one `navigator.platform` test — is also the
9
- * fragile one: that API is deprecated, and it returns an empty string in some
10
- * privacy configurations, which silently degrades every Mac user to the
11
- * Windows vocabulary without any error to notice.
12
- *
13
- * This module is the single owner of both the platform answer and every label
14
- * derived from it. The hint rendered in the sidebar, the hint rendered in the
15
- * panel footer, and the chord the keydown handler actually matches all read
16
- * from here — so a platform fix cannot land in one of them and miss another,
17
- * and the advertised key cannot drift away from the bound key.
18
- */
19
-
20
- /** The navigator fields the detection reads (all optional, all untrusted). */
21
- export interface NavigatorLike {
22
- userAgentData?: { platform?: unknown }
23
- platform?: unknown
24
- userAgent?: unknown
25
- }
26
-
27
- /**
28
- * Resolve the running platform string, three tiers deep.
29
- *
30
- * 1. UA-CH (`navigator.userAgentData.platform`) — modern and precise.
31
- * 2. `navigator.platform` — broadly available, deprecated, and empty in some
32
- * privacy modes.
33
- * 3. The UA string — last resort, still non-empty in practice.
34
- *
35
- * @param nav - the navigator to read; `undefined` is allowed (non-DOM host).
36
- * @returns a lower-cased platform string, or `''` when nothing could be read.
37
- */
38
- export function detectPlatform(nav: NavigatorLike | undefined): string {
39
- const chPlatform = nav?.userAgentData?.platform
40
- if (typeof chPlatform === 'string' && chPlatform !== '') return chPlatform.toLowerCase()
41
- const legacy = nav?.platform
42
- if (typeof legacy === 'string' && legacy !== '') return legacy.toLowerCase()
43
- const agent = nav?.userAgent
44
- if (typeof agent === 'string' && agent !== '') return agent.toLowerCase()
45
- return ''
46
- }
47
-
48
- /** The keyboard vocabulary for one platform. */
49
- export interface PlatformLabels {
50
- /** The lower-cased platform string these labels were derived from. */
51
- platform: string
52
- isMac: boolean
53
- isWindows: boolean
54
- /** Invoke-key modifier: `⌘` on macOS, `Ctrl` elsewhere. */
55
- modLabel: string
56
- /** Secondary modifier: `⌥` on macOS, `Alt` elsewhere. */
57
- altLabel: string
58
- /** Shift: `⇧` on macOS, `Shift` elsewhere. */
59
- shiftLabel: string
60
- /** Confirm: the arrow glyph is legible on every platform. */
61
- enterLabel: string
62
- /** Escape: `esc` is the macOS spelling, `Esc` the one everywhere else. */
63
- escLabel: string
64
- /**
65
- * The whole invoke chord as one face, ready for a key chip: `⌘K` on macOS,
66
- * `Ctrl K` on Windows and Linux. `Ctrl K` keeps the space because `CtrlK`
67
- * reads as a word rather than as two keys.
68
- */
69
- invokeLabel: string
70
- }
71
-
72
- /**
73
- * Derive the label vocabulary from a platform string.
74
- *
75
- * An unrecognised platform — an empty string, or a UA that names neither
76
- * system — takes the non-Mac vocabulary on purpose: `Ctrl` is legible to
77
- * everyone, while `⌘` is opaque to anyone who has never used a Mac.
78
- *
79
- * @param platform - the string produced by {@link detectPlatform}.
80
- * @returns the label vocabulary for that platform.
81
- */
82
- export function platformLabels(platform: string): PlatformLabels {
83
- const isMac = /mac|iphone|ipad|ipod/u.test(platform)
84
- const isWindows = /win/u.test(platform)
85
- const modLabel = isMac ? '⌘' : 'Ctrl'
86
- return {
87
- platform,
88
- isMac,
89
- isWindows,
90
- modLabel,
91
- altLabel: isMac ? '⌥' : 'Alt',
92
- shiftLabel: isMac ? '⇧' : 'Shift',
93
- enterLabel: '↵',
94
- escLabel: isMac ? 'esc' : 'Esc',
95
- invokeLabel: isMac ? `${modLabel}K` : `${modLabel} K`,
96
- }
97
- }
98
-
99
- /** Read `globalThis.navigator` without assuming a DOM (or a DOM lib). */
100
- function currentNavigator(): NavigatorLike | undefined {
101
- const candidate = (globalThis as { navigator?: unknown }).navigator
102
- if (candidate === null || typeof candidate !== 'object') return undefined
103
- return candidate as NavigatorLike
104
- }
105
-
106
- /** The labels for the platform this bundle is running on. */
107
- export const LABELS: PlatformLabels = platformLabels(detectPlatform(currentNavigator()))
108
-
109
- /** The minimal keydown face the chord test reads. */
110
- export interface KeyChordEvent {
111
- key: string
112
- ctrlKey: boolean
113
- metaKey: boolean
114
- altKey: boolean
115
- shiftKey: boolean
116
- }
117
-
118
- /**
119
- * Whether a keydown event is the invoke chord advertised by
120
- * {@link PlatformLabels.invokeLabel}.
121
- *
122
- * The hint and the binding have to agree: a footer promising `⌘K` while the
123
- * handler matches `Ctrl+K` is worse than showing no hint at all. Both sides
124
- * call this, so neither can be changed alone. Requiring the other modifier to
125
- * be *absent* keeps the two chords distinct instead of letting either one
126
- * satisfy both platforms.
127
- *
128
- * @param event - the keydown payload.
129
- * @param isMac - the running platform's macOS-ness, from {@link LABELS}.
130
- * @returns `true` when the event is the invoke chord.
131
- */
132
- export function isInvokeChord(event: KeyChordEvent, isMac: boolean): boolean {
133
- if (event.key !== 'k' && event.key !== 'K') return false
134
- if (event.altKey || event.shiftKey) return false
135
- return isMac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey
136
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * Official archive-set source resolution.
3
- *
4
- * Primary: the in-process `workspaceRegistry` service (the same fact the UI
5
- * filters by). Fallback: read the canonical storage hub file directly — the
6
- * workspace domain persists `archivedSessionIds` under the `global` segment
7
- * (storage-json: `~/.dsh/storages/workspace.json`; storage-sqlite variant
8
- * exists but the JSON fallback file is what stock web profiles ship).
9
- * Resolution order is decided per read; failures degrade to "no archive set"
10
- * and are reported through the diagnostics face.
11
- *
12
- * READ-ONLY by contract. The archive set is WRITTEN by dsh-manage-session
13
- * (会话管家 → 病案室); this package only consumes it to keep archived sessions
14
- * out of the index. Format contract: `dsh-归档文件格式契约-20260914.md`.
15
- */
16
- import { existsSync, readFileSync } from 'node:fs'
17
- import { homedir } from 'node:os'
18
- import { join } from 'node:path'
19
-
20
- /** One resolved archive read: ids plus which source served them. */
21
- export interface SwitchArchiveRead {
22
- ids: readonly string[]
23
- source: 'registry' | 'storage-file' | 'none'
24
- }
25
-
26
- /** The workspaceRegistry mirror face (getter only). */
27
- export interface SwitchRegistryFace {
28
- readonly archivedSessionIds: readonly string[]
29
- }
30
-
31
- /** Diagnostics describing how the archive set is being resolved. */
32
- export interface SwitchArchiveDiagnostics {
33
- source: 'registry' | 'storage-file' | 'none'
34
- ids: number
35
- error?: string
36
- }
37
-
38
- /** DSH storage hub candidates for the workspace domain (json backend). */
39
- function storageFileCandidates(): string[] {
40
- return [
41
- join(homedir(), '.dsh', 'storages', 'workspace.json'),
42
- ]
43
- }
44
-
45
- /** Parse the storage hub file's global.archivedSessionIds; throw on malformed content. */
46
- function readStorageFile(path: string): readonly string[] {
47
- const parsed = JSON.parse(readFileSync(path, 'utf8')) as {
48
- global?: { archivedSessionIds?: unknown }
49
- }
50
- const ids = parsed.global?.archivedSessionIds
51
- if (!Array.isArray(ids)) throw new Error(`storage hub "${path}" holds no global.archivedSessionIds array`)
52
- return ids.filter((id): id is string => typeof id === 'string')
53
- }
54
-
55
- /**
56
- * Resolve the official archive set once.
57
- * @param registry - lazy workspaceRegistry face (may be absent or throw).
58
- * @returns the archive ids plus which source served them.
59
- */
60
- export function readArchiveSet(registry?: SwitchRegistryFace): SwitchArchiveRead {
61
- if (registry !== undefined) {
62
- try {
63
- const ids = registry.archivedSessionIds
64
- if (Array.isArray(ids)) return { ids, source: 'registry' }
65
- } catch { /* registry not started yet — fall through to the file */ }
66
- }
67
- for (const path of storageFileCandidates()) {
68
- if (!existsSync(path)) continue
69
- try {
70
- return { ids: readStorageFile(path), source: 'storage-file' }
71
- } catch { /* malformed file — try the next candidate */ }
72
- }
73
- return { ids: [], source: 'none' }
74
- }
75
-
76
- /** Build the lazy source face the syncer expects, with diagnostics capture. */
77
- export function createArchiveSource(getRegistry: () => SwitchRegistryFace | undefined): {
78
- read: () => SwitchArchiveRead
79
- diagnostics: () => SwitchArchiveDiagnostics
80
- } {
81
- let last: SwitchArchiveDiagnostics = { source: 'none', ids: 0 }
82
- return {
83
- read: () => {
84
- const read = readArchiveSet(getRegistry())
85
- last = { source: read.source, ids: read.ids.length }
86
- return read
87
- },
88
- diagnostics: () => ({ ...last }),
89
- }
90
- }
@@ -1,211 +0,0 @@
1
- /**
2
- * DRS 语料源:dsh-reference-session-collector 投影库的 HTTP 只读客户端(T7.1)。
3
- *
4
- * 语料契约 = DRS 单条 fenced 路由 `GET /api/dsh-reference-session-collector`:
5
- * - `op=sessions&offset=&limit=` → `{ok, total, rows:[sessions 行]}`(分页);
6
- * - `op=session&session=<id>&fields=search` → `{ok, session, rows:[检索臂]}`。
7
- *
8
- * 字段对照(T7.0,DRS wire snake → steward 引擎列):
9
- * sessions.version ← artifact_mtime_ms(水位代理:日志重写必 bump mtime);
10
- * sessions.updated_at ← last_time_ms;title/cwd/archived ← 同名列;
11
- * docs.type ← kind;docs.surface ← on_surface(1='current'/0='shadowed');
12
- * docs.time ← time;docs.text ← search_text;index_text 由本引擎
13
- * segmentForIndex 重算(自有切分面不动)。
14
- *
15
- * 自调用信任面:DRS 的 Host 门是精确匹配(trustedHosts 全小写比对),本客户
16
- * 端可带 hostHeader 显式回显首个 trustedHosts 条目,规避「条目省略端口而
17
- * URL 带端口」的精确匹配失配;loopback + 无 trustedHosts 的老宿主天然放行。
18
- */
19
- /** DRS API 路径(与 dsh-reference-session-collector 的 DRS_API_PATH 字面一致;不跨包 import)。 */
20
- export const DRS_API_PATH = '/api/dsh-reference-session-collector'
21
-
22
- /** 语料列表分页大小(百会话级一轮 1-2 页;DRS 侧 offset/limit 透传)。 */
23
- export const DRS_CORPUS_PAGE = 500
24
-
25
- /** 一个 DRS 会话头(watermark 代理 + 检索排序字段)。 */
26
- export interface DrsCorpusHead {
27
- sessionId: string
28
- version: number
29
- title: string
30
- cwd: string
31
- updatedAt: number
32
- archived: boolean
33
- }
34
-
35
- /** 一个 DRS 检索臂文档(surface 已折算成引擎列的字符串域)。 */
36
- export interface DrsCorpusDoc {
37
- seq: number
38
- type: string
39
- surface: 'current' | 'shadowed'
40
- time: number
41
- text: string
42
- }
43
-
44
- /** 一个 DRS 会话的完整语料(头 + docs)。 */
45
- export interface DrsCorpusSession {
46
- head: DrsCorpusHead
47
- docs: DrsCorpusDoc[]
48
- }
49
-
50
- export interface DrsCorpusSourceOptions {
51
- /** DRS API 基址(http://host[:port];解析序见 resolveDrsBaseUrl)。 */
52
- baseUrl: string
53
- /** 注入面(单测);缺省全局 fetch。 */
54
- fetchImpl?: typeof fetch
55
- /** 显式 Host 头(trustedHosts 首条回显;缺省不覆盖)。 */
56
- hostHeader?: string
57
- log?: (message: string) => void
58
- }
59
-
60
- /**
61
- * 基址解析:显式配置 > 首个 trustedHosts 条目(补 http:// 前缀)>
62
- * `http://127.0.0.1:8080`(宿主默认口;trustedHosts 缺席的老线 DRS 门本就放行)。
63
- */
64
- export function resolveDrsBaseUrl(explicit: string | undefined, trustedHosts?: readonly string[]): string {
65
- const trimmed = explicit?.trim() ?? ''
66
- const pick = trimmed !== '' ? trimmed : trustedHosts?.find(entry => entry.trim() !== '')?.trim()
67
- if (pick === undefined || pick === '') return 'http://127.0.0.1:8080'
68
- return /^https?:\/\//u.test(pick) ? pick : `http://${pick}`
69
- }
70
-
71
- export class DrsCorpusSource {
72
- private readonly fetchImpl: typeof fetch
73
-
74
- constructor(private readonly options: DrsCorpusSourceOptions) {
75
- this.fetchImpl = options.fetchImpl ?? fetch
76
- }
77
-
78
- /** 一个 GET 的 JSON 包络(ok!==true 或非 2xx 一律抛,由调用方回退 native)。 */
79
- private async getJson(query: string, atHost?: string): Promise<Record<string, unknown>> {
80
- // atHost=同源转发臂(调用方请求的 Host 头):宿主端口动态分配时
81
- // 配置缺省口(8080)猜不中——同源直连以请求 Host 优先。
82
- const source = atHost !== undefined && atHost !== '' ? `http://${atHost}` : this.options.baseUrl
83
- const base = source.replace(/\/+$/u, '')
84
- const url = `${base}${DRS_API_PATH}${query}`
85
- let response: Response
86
- try {
87
- response = await this.fetchImpl(url, {
88
- headers: {
89
- accept: 'application/json',
90
- ...(this.options.hostHeader === undefined ? {} : { host: this.options.hostHeader }),
91
- },
92
- })
93
- } catch (err) {
94
- throw new Error(`DRS ${query}: ${String(err instanceof Error ? err.message : err)}`)
95
- }
96
- if (!response.ok) throw new Error(`DRS ${query} → HTTP ${String(response.status)}`)
97
- const body = await response.json() as unknown
98
- if (typeof body !== 'object' || body === null) throw new Error(`DRS ${query}: 响应非 JSON 对象`)
99
- const record = body as Record<string, unknown>
100
- if (record['ok'] !== true) {
101
- throw new Error(`DRS ${query}: ok=false(${String(record['error'] ?? 'unknown')})`)
102
- }
103
- return record
104
- }
105
-
106
- private static headOf(row: Record<string, unknown>, fallbackId: string): DrsCorpusHead {
107
- return {
108
- sessionId: String(row['session_id'] ?? fallbackId),
109
- version: Number(row['artifact_mtime_ms'] ?? 0),
110
- title: String(row['title'] ?? ''),
111
- cwd: String(row['cwd'] ?? ''),
112
- updatedAt: Number(row['last_time_ms'] ?? 0),
113
- archived: Number(row['archived'] ?? 0) === 1,
114
- }
115
- }
116
-
117
- /** 全量语料头(内部翻页;rows 空或凑满 total 即止)。 */
118
- async listSessions(): Promise<DrsCorpusHead[]> {
119
- const heads: DrsCorpusHead[] = []
120
- let offset = 0
121
- for (;;) {
122
- const page = await this.getJson(`?op=sessions&offset=${String(offset)}&limit=${String(DRS_CORPUS_PAGE)}`)
123
- const rows = Array.isArray(page['rows']) ? page['rows'] as Record<string, unknown>[] : []
124
- for (const row of rows) {
125
- const head = DrsCorpusSource.headOf(row, '')
126
- if (head.sessionId !== '') heads.push(head)
127
- }
128
- offset += rows.length
129
- const total = Number(page['total'] ?? heads.length)
130
- if (rows.length === 0 || offset >= total) break
131
- }
132
- return heads
133
- }
134
-
135
- /** 一个会话的检索臂 docs;DRS 无此会话(session:null)返回 undefined。 */
136
- async readSession(sessionId: string): Promise<DrsCorpusSession | undefined> {
137
- const body = await this.getJson(`?op=session&session=${encodeURIComponent(sessionId)}&fields=search`)
138
- if (body['session'] === null || body['session'] === undefined) return undefined
139
- const head = DrsCorpusSource.headOf(body['session'] as Record<string, unknown>, sessionId)
140
- const docs = (Array.isArray(body['rows']) ? body['rows'] as Record<string, unknown>[] : []).map(row => ({
141
- seq: Number(row['seq'] ?? 0),
142
- type: String(row['kind'] ?? ''),
143
- surface: Number(row['on_surface'] ?? 1) === 1 ? 'current' as const : 'shadowed' as const,
144
- time: Number(row['time'] ?? 0),
145
- text: String(row['search_text'] ?? ''),
146
- }))
147
- return { head, docs }
148
- }
149
-
150
- /**
151
- * 分组内容搜索直连臂(op=search&group=session,DRS surface.searchGrouped 同构):
152
- * 会话分组/加权/过滤/锚定节选全部在 DRS 侧完成,本方法只搬 wire →
153
- * 引擎命中形状(SwitchSearchHit 字段同名)。失败(网络/非 2xx/包络)直抛,
154
- * 调用侧按硬依赖语义显式报错——**不回退本地索引**。
155
- */
156
- async searchGrouped(request: {
157
- query: string
158
- /** 原事件 type 列表(调用方展开粗类);空=DRS 缺省 user+assistant。 */
159
- kinds?: readonly string[]
160
- archived?: 'active' | 'archived' | 'all'
161
- from?: number
162
- to?: number
163
- sortBy?: 'relevance' | 'time'
164
- limit?: number
165
- /** 命中展开(wire `expand=1`):同会话多命中各自成行;缺省聚合。 */
166
- expand?: boolean
167
- /** 同源 Host 覆盖(转发调用方请求头;优先于装配 baseUrl)。 */
168
- atHost?: string
169
- }): Promise<{
170
- sessionId: string
171
- title: string
172
- seq: number
173
- type: string
174
- time: number
175
- updatedAt: number
176
- snippet: string
177
- }[]> {
178
- const params = new URLSearchParams()
179
- params.set('op', 'search')
180
- params.set('group', 'session')
181
- params.set('q', request.query)
182
- if (request.kinds !== undefined && request.kinds.length > 0) params.set('kinds', request.kinds.join(','))
183
- if (request.archived !== undefined) params.set('archived', request.archived)
184
- if (request.from !== undefined) params.set('from', String(request.from))
185
- if (request.to !== undefined) params.set('to', String(request.to))
186
- if (request.sortBy === 'time') params.set('sort', 'time')
187
- if (request.limit !== undefined) params.set('limit', String(request.limit))
188
- if (request.expand === true) params.set('expand', '1')
189
- const body = await this.getJson(`?${params.toString()}`, request.atHost)
190
- const hits = Array.isArray(body['hits']) ? body['hits'] as Record<string, unknown>[] : []
191
- return hits.map(row => ({
192
- sessionId: String(row['session_id'] ?? ''),
193
- title: String(row['title'] ?? ''),
194
- seq: Number(row['seq'] ?? 0),
195
- type: String(row['type'] ?? ''),
196
- time: Number(row['time'] ?? 0),
197
- updatedAt: Number(row['updated_at'] ?? 0),
198
- snippet: String(row['snippet'] ?? ''),
199
- }))
200
- }
201
-
202
- /** 可性探针(op=status):硬依赖预检/降级提示用;失败返回 false。 */
203
- async probe(atHost?: string): Promise<boolean> {
204
- try {
205
- await this.getJson('?op=status', atHost)
206
- return true
207
- } catch {
208
- return false
209
- }
210
- }
211
- }