@shendeguize/dsh-agent-sidecar 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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +8062 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +396 -0
- package/lib/index.js +4166 -0
- package/package.json +101 -0
- package/src/analysis.ts +782 -0
- package/src/bridge.ts +841 -0
- package/src/client/analysis/AnalysisPanel.tsx +191 -0
- package/src/client/analysis/analysis.module.css +183 -0
- package/src/client/analysis-glue.ts +331 -0
- package/src/client/api.ts +380 -0
- package/src/client/board/Board.tsx +214 -0
- package/src/client/board/board.module.css +302 -0
- package/src/client/board/logic.ts +556 -0
- package/src/client/board/project-view-logic.ts +361 -0
- package/src/client/board/project-view.module.css +307 -0
- package/src/client/board/project-view.tsx +189 -0
- package/src/client/board/strings.ts +112 -0
- package/src/client/commands.ts +484 -0
- package/src/client/controller.ts +360 -0
- package/src/client/css-modules.d.ts +11 -0
- package/src/client/detail/SessionDetail.tsx +270 -0
- package/src/client/detail/detail.module.css +433 -0
- package/src/client/detail/logic.ts +779 -0
- package/src/client/detail/strings.ts +98 -0
- package/src/client/detail/transport.ts +175 -0
- package/src/client/detail-glue.ts +397 -0
- package/src/client/detail-view.module.css +79 -0
- package/src/client/detail-view.tsx +233 -0
- package/src/client/dsh-tools/LineageTree.tsx +210 -0
- package/src/client/dsh-tools/SearchPanel.tsx +169 -0
- package/src/client/dsh-tools/dsh-tools.module.css +374 -0
- package/src/client/dsh-tools/logic.ts +596 -0
- package/src/client/dsh-tools/strings.ts +90 -0
- package/src/client/index.ts +315 -0
- package/src/client/inject/InjectPanel.tsx +482 -0
- package/src/client/inject/inject.module.css +446 -0
- package/src/client/inject/logic.ts +516 -0
- package/src/client/inject/overlay.module.css +22 -0
- package/src/client/inject-glue.ts +171 -0
- package/src/client/locales/command.ts +48 -0
- package/src/client/locales/en.ts +385 -0
- package/src/client/locales/index.ts +123 -0
- package/src/client/locales/zh.ts +402 -0
- package/src/client/m3-transport.ts +151 -0
- package/src/client/mount.tsx +307 -0
- package/src/client/project-glue.ts +134 -0
- package/src/client/search-glue.ts +143 -0
- package/src/client/settings-card.module.css +359 -0
- package/src/client/settings-card.tsx +565 -0
- package/src/client/settings-glue.ts +130 -0
- package/src/client/sidebar-tab.tsx +494 -0
- package/src/client/sse.ts +366 -0
- package/src/client/widget.tsx +80 -0
- package/src/config.ts +193 -0
- package/src/dsh-inject.ts +240 -0
- package/src/fusion.ts +988 -0
- package/src/guard.ts +274 -0
- package/src/index.ts +950 -0
- package/src/inject-gateway.ts +574 -0
- package/src/routes.ts +1133 -0
- package/src/send-cli.ts +340 -0
- package/src/session-store.ts +184 -0
- package/src/skills-provider.ts +293 -0
- package/src/supervisor.ts +463 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure view-model logic for the dsh deep-query tools (design §5.1 view 2
|
|
3
|
+
* dsh 会话专属区 / M3): lineage trace → tree view model, provenance-jump
|
|
4
|
+
* resolution, search-result normalization (snippet highlighting), and the
|
|
5
|
+
* honest-degradation models (§5.3: degradation is presented, never faked).
|
|
6
|
+
*
|
|
7
|
+
* No React, no I/O, no imports from the data layer — everything arrives
|
|
8
|
+
* and leaves as plain values, so this module is unit-testable in a bare
|
|
9
|
+
* node environment (same posture as board/logic.ts and inject/logic.ts).
|
|
10
|
+
*
|
|
11
|
+
* Decoupling contract: the wire-facing types below are this module's OWN
|
|
12
|
+
* hand-written mirrors of the host read contract (task constraint —
|
|
13
|
+
* components never import host code):
|
|
14
|
+
* - {@link LineageResponseVM} mirrors `GET <prefix>/lineage/<id>` — always
|
|
15
|
+
* 200 `{available, trace, reason, detail?}` (routes.ts handleLineage over
|
|
16
|
+
* fusion.ts LineageResult); `available:false` is DATA, not an error.
|
|
17
|
+
* - {@link LineageTraceVM} mirrors fusion.ts DshLineageTraceFace (the
|
|
18
|
+
* `SessionLineageTrace` subset from dsh-session-query traceSession).
|
|
19
|
+
* - {@link SearchResponseVM} mirrors `GET <prefix>/search?q=&project=&limit=`
|
|
20
|
+
* (routes.ts handleSearch over fusion.ts searchSessions): `mode:
|
|
21
|
+
* 'filter-only'` means the sessionQuery full-text engine is unavailable
|
|
22
|
+
* and the backend degraded to title/project filtering.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { DSH_TOOLS_STRINGS } from './strings.ts'
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Small shared helpers.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** Resolve `{name}` placeholders in a message template (board convention). */
|
|
34
|
+
export function formatTemplate(
|
|
35
|
+
template: string,
|
|
36
|
+
params: Record<string, string | number>,
|
|
37
|
+
): string {
|
|
38
|
+
return template.replace(/\{(\w+)\}/g, (match, key: string) => {
|
|
39
|
+
const value = params[key]
|
|
40
|
+
return value === undefined ? match : String(value)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Head…tail abbreviation for long session ids (same rule as the board). */
|
|
45
|
+
export function abbreviateId(id: string, max = 20): string {
|
|
46
|
+
if (id.length <= max) return id
|
|
47
|
+
return `${id.slice(0, 12)}…${id.slice(-6)}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Lineage wire mirrors (host source of truth noted per type).
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/** `SessionRecord` subset inside a trace (host: fusion.ts DshLineageRecordFace). */
|
|
55
|
+
export interface LineageRecordVM {
|
|
56
|
+
header: {
|
|
57
|
+
id: string
|
|
58
|
+
createdAt: number
|
|
59
|
+
cwd?: string
|
|
60
|
+
parentSession?: string
|
|
61
|
+
}
|
|
62
|
+
/** True when the session is live in the dsh process. */
|
|
63
|
+
live: boolean
|
|
64
|
+
/** True when dsh has persisted the session log. */
|
|
65
|
+
persisted: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One descendant branch (host: fusion.ts DshLineageNodeFace). */
|
|
69
|
+
export interface LineageBranchVM {
|
|
70
|
+
session: LineageRecordVM
|
|
71
|
+
descendants: readonly LineageBranchVM[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The trace body (host: fusion.ts DshLineageTraceFace ← traceSession). */
|
|
75
|
+
export interface LineageTraceVM {
|
|
76
|
+
target: LineageRecordVM
|
|
77
|
+
ancestors: readonly LineageRecordVM[]
|
|
78
|
+
descendants: readonly LineageBranchVM[]
|
|
79
|
+
complete: boolean
|
|
80
|
+
root?: LineageRecordVM
|
|
81
|
+
unresolvedParentId?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Backend degradation vocabulary (host: fusion.ts LineageResult.reason). */
|
|
85
|
+
export type LineageBackendReason = 'session_query_unavailable' | 'trace_failed'
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Client-side degradation vocabulary: the backend reasons plus
|
|
89
|
+
* `not_dsh_session`, minted locally by {@link externalLineageFallback} for
|
|
90
|
+
* sessions of non-dsh agents (lineage is a dsh-only capability, so the
|
|
91
|
+
* integration never even calls the backend for them).
|
|
92
|
+
*/
|
|
93
|
+
export type LineageDegradeReason = LineageBackendReason | 'not_dsh_session'
|
|
94
|
+
|
|
95
|
+
/** Body of `GET lineage/<id>` — always HTTP 200 (host: routes.ts). */
|
|
96
|
+
export interface LineageResponseVM {
|
|
97
|
+
available: boolean
|
|
98
|
+
trace: LineageTraceVM | null
|
|
99
|
+
reason: LineageBackendReason | null
|
|
100
|
+
detail?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Lineage trace → tree view model.
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/** Position of a node relative to the traced session. */
|
|
108
|
+
export type LineageRole = 'ancestor' | 'target' | 'descendant'
|
|
109
|
+
|
|
110
|
+
/** One flattened tree row, ready to render (indent by `depth`). */
|
|
111
|
+
export interface LineageNodeVM {
|
|
112
|
+
id: string
|
|
113
|
+
role: LineageRole
|
|
114
|
+
roleLabel: string
|
|
115
|
+
/** Indentation level; the outermost ancestor (or the target) is 0. */
|
|
116
|
+
depth: number
|
|
117
|
+
/** Structural tree parent (render edge), null for the outermost row. */
|
|
118
|
+
parentId: string | null
|
|
119
|
+
/** True when deeper rows directly under this one exist (collapse handle). */
|
|
120
|
+
hasChildren: boolean
|
|
121
|
+
/** True when this node IS the session the user is inspecting (highlight). */
|
|
122
|
+
isCurrent: boolean
|
|
123
|
+
isTarget: boolean
|
|
124
|
+
live: boolean
|
|
125
|
+
persisted: boolean
|
|
126
|
+
shortId: string
|
|
127
|
+
cwd: string | null
|
|
128
|
+
createdAt: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The whole lineage tree render model. */
|
|
132
|
+
export interface LineageTreeVM {
|
|
133
|
+
/** Rows in render order: ancestors root-first, target, descendants DFS. */
|
|
134
|
+
nodes: LineageNodeVM[]
|
|
135
|
+
targetId: string
|
|
136
|
+
complete: boolean
|
|
137
|
+
unresolvedParentId: string | null
|
|
138
|
+
/** Honest incompleteness banner; null when the trace is complete. */
|
|
139
|
+
incompleteNotice: string | null
|
|
140
|
+
nodeCount: number
|
|
141
|
+
maxDepth: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function toNode(
|
|
145
|
+
record: LineageRecordVM,
|
|
146
|
+
role: LineageRole,
|
|
147
|
+
depth: number,
|
|
148
|
+
parentId: string | null,
|
|
149
|
+
currentSessionId: string | null,
|
|
150
|
+
): LineageNodeVM {
|
|
151
|
+
return {
|
|
152
|
+
id: record.header.id,
|
|
153
|
+
role,
|
|
154
|
+
roleLabel: DSH_TOOLS_STRINGS.lineage.role[role],
|
|
155
|
+
depth,
|
|
156
|
+
parentId,
|
|
157
|
+
hasChildren: false,
|
|
158
|
+
isCurrent: currentSessionId !== null && record.header.id === currentSessionId,
|
|
159
|
+
isTarget: role === 'target',
|
|
160
|
+
live: record.live,
|
|
161
|
+
persisted: record.persisted,
|
|
162
|
+
shortId: abbreviateId(record.header.id),
|
|
163
|
+
cwd: record.header.cwd ?? null,
|
|
164
|
+
createdAt: record.header.createdAt,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Order the ancestors root-first by walking `header.parentSession` links
|
|
170
|
+
* up from the target. Ancestors the links cannot reach (broken chain —
|
|
171
|
+
* the trace then also carries `complete:false`) are still shown: they sit
|
|
172
|
+
* above the resolved chain in their reported order, since everything the
|
|
173
|
+
* backend lists in `ancestors` IS an ancestor even when the intermediate
|
|
174
|
+
* links are unresolved.
|
|
175
|
+
*/
|
|
176
|
+
function orderAncestors(trace: LineageTraceVM): LineageRecordVM[] {
|
|
177
|
+
const byId = new Map<string, LineageRecordVM>()
|
|
178
|
+
for (const record of trace.ancestors) byId.set(record.header.id, record)
|
|
179
|
+
const chain: LineageRecordVM[] = [] // nearest-first
|
|
180
|
+
const visited = new Set<string>()
|
|
181
|
+
let cursor = trace.target.header.parentSession
|
|
182
|
+
while (cursor !== undefined && !visited.has(cursor)) {
|
|
183
|
+
const record = byId.get(cursor)
|
|
184
|
+
if (record === undefined) break
|
|
185
|
+
visited.add(cursor)
|
|
186
|
+
chain.push(record)
|
|
187
|
+
cursor = record.header.parentSession
|
|
188
|
+
}
|
|
189
|
+
const leftovers = trace.ancestors.filter((r) => !visited.has(r.header.id))
|
|
190
|
+
return [...leftovers, ...chain.reverse()]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Lineage trace → flattened tree view model. Ancestors form the spine
|
|
195
|
+
* (root-most at depth 0), the target follows, and the descendant branches
|
|
196
|
+
* are appended depth-first in reported order. `currentSessionId` marks the
|
|
197
|
+
* highlight (usually the target, but a lineage panel opened from another
|
|
198
|
+
* node keeps whatever the owner is inspecting).
|
|
199
|
+
*/
|
|
200
|
+
export function buildLineageTree(
|
|
201
|
+
trace: LineageTraceVM,
|
|
202
|
+
currentSessionId: string | null,
|
|
203
|
+
): LineageTreeVM {
|
|
204
|
+
const nodes: LineageNodeVM[] = []
|
|
205
|
+
|
|
206
|
+
const ancestors = orderAncestors(trace)
|
|
207
|
+
let parentId: string | null = null
|
|
208
|
+
let depth = 0
|
|
209
|
+
for (const record of ancestors) {
|
|
210
|
+
nodes.push(toNode(record, 'ancestor', depth, parentId, currentSessionId))
|
|
211
|
+
parentId = record.header.id
|
|
212
|
+
depth += 1
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
nodes.push(toNode(trace.target, 'target', depth, parentId, currentSessionId))
|
|
216
|
+
const targetDepth = depth
|
|
217
|
+
|
|
218
|
+
const walk = (branch: LineageBranchVM, branchDepth: number, branchParent: string): void => {
|
|
219
|
+
nodes.push(
|
|
220
|
+
toNode(branch.session, 'descendant', branchDepth, branchParent, currentSessionId),
|
|
221
|
+
)
|
|
222
|
+
for (const child of branch.descendants) {
|
|
223
|
+
walk(child, branchDepth + 1, branch.session.header.id)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const branch of trace.descendants) {
|
|
227
|
+
walk(branch, targetDepth + 1, trace.target.header.id)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// hasChildren over the flattened DFS order: a row has children exactly
|
|
231
|
+
// when the next row is one level deeper.
|
|
232
|
+
let maxDepth = 0
|
|
233
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
234
|
+
const node = nodes[i]
|
|
235
|
+
if (node === undefined) continue
|
|
236
|
+
const next = nodes[i + 1]
|
|
237
|
+
node.hasChildren = next !== undefined && next.depth > node.depth
|
|
238
|
+
if (node.depth > maxDepth) maxDepth = node.depth
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const unresolvedParentId = trace.unresolvedParentId ?? null
|
|
242
|
+
const incompleteNotice = trace.complete
|
|
243
|
+
? null
|
|
244
|
+
: unresolvedParentId !== null
|
|
245
|
+
? formatTemplate(DSH_TOOLS_STRINGS.lineage.incompleteWithId, {
|
|
246
|
+
id: abbreviateId(unresolvedParentId),
|
|
247
|
+
})
|
|
248
|
+
: DSH_TOOLS_STRINGS.lineage.incomplete
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
nodes,
|
|
252
|
+
targetId: trace.target.header.id,
|
|
253
|
+
complete: trace.complete,
|
|
254
|
+
unresolvedParentId,
|
|
255
|
+
incompleteNotice,
|
|
256
|
+
nodeCount: nodes.length,
|
|
257
|
+
maxDepth,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Apply component-owned collapse state over the flattened rows: a
|
|
263
|
+
* collapsed row keeps itself but hides every deeper row until the DFS
|
|
264
|
+
* order returns to its level.
|
|
265
|
+
*/
|
|
266
|
+
export function visibleLineageNodes(
|
|
267
|
+
nodes: readonly LineageNodeVM[],
|
|
268
|
+
collapsedIds: ReadonlySet<string>,
|
|
269
|
+
): LineageNodeVM[] {
|
|
270
|
+
const out: LineageNodeVM[] = []
|
|
271
|
+
let hideDeeperThan: number | null = null
|
|
272
|
+
for (const node of nodes) {
|
|
273
|
+
if (hideDeeperThan !== null) {
|
|
274
|
+
if (node.depth > hideDeeperThan) continue
|
|
275
|
+
hideDeeperThan = null
|
|
276
|
+
}
|
|
277
|
+
out.push(node)
|
|
278
|
+
if (node.hasChildren && collapsedIds.has(node.id)) {
|
|
279
|
+
hideDeeperThan = node.depth
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return out
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Provenance jump (点击谱系节点 → onSelectSession(id)).
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/** What a click on a lineage node should do. */
|
|
290
|
+
export type LineageJump =
|
|
291
|
+
| { kind: 'select'; sessionId: string }
|
|
292
|
+
/** The node is the session already on screen — highlight, no navigation. */
|
|
293
|
+
| { kind: 'current' }
|
|
294
|
+
|
|
295
|
+
/** Resolve the jump target of a lineage-node click. */
|
|
296
|
+
export function resolveJumpTarget(
|
|
297
|
+
nodeId: string,
|
|
298
|
+
currentSessionId: string | null,
|
|
299
|
+
): LineageJump {
|
|
300
|
+
if (currentSessionId !== null && nodeId === currentSessionId) {
|
|
301
|
+
return { kind: 'current' }
|
|
302
|
+
}
|
|
303
|
+
return { kind: 'select', sessionId: nodeId }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// External-agent degradation (lineage is dsh-exclusive).
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
const DSH_AGENT = 'dsh'
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Client-side pre-check for the lineage panel: sessions of non-dsh agents
|
|
314
|
+
* get a synthetic degraded response (reason `not_dsh_session`) and the
|
|
315
|
+
* integration never calls the backend for them; dsh sessions return null
|
|
316
|
+
* (proceed to fetch). Honest presentation, not an error (§5.3).
|
|
317
|
+
*/
|
|
318
|
+
export function externalLineageFallback(
|
|
319
|
+
agent: string,
|
|
320
|
+
): { available: false; trace: null; reason: 'not_dsh_session' } | null {
|
|
321
|
+
if (agent.trim().toLowerCase() === DSH_AGENT) return null
|
|
322
|
+
return { available: false, trace: null, reason: 'not_dsh_session' }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Degradation cards and the lineage view resolver.
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/** Render model of one degradation card (available:false presentations). */
|
|
330
|
+
export interface LineageDegradeCardVM {
|
|
331
|
+
reason: LineageDegradeReason | 'unknown'
|
|
332
|
+
title: string
|
|
333
|
+
body: string
|
|
334
|
+
/** Backend `detail` passthrough (trace_failed carries the engine error). */
|
|
335
|
+
detail: string | null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Degradation reason → card copy. Reasons outside the known vocabulary
|
|
340
|
+
* (a newer backend) fall back to the generic card carrying the raw reason
|
|
341
|
+
* — the panel never invents a state.
|
|
342
|
+
*/
|
|
343
|
+
export function lineageDegradeCard(
|
|
344
|
+
reason: string | null | undefined,
|
|
345
|
+
detail?: string | null,
|
|
346
|
+
): LineageDegradeCardVM {
|
|
347
|
+
const strings = DSH_TOOLS_STRINGS.lineage.degrade
|
|
348
|
+
const detailOut = detail ?? null
|
|
349
|
+
switch (reason) {
|
|
350
|
+
case 'not_dsh_session':
|
|
351
|
+
return { reason, title: strings.notDshTitle, body: strings.notDshBody, detail: detailOut }
|
|
352
|
+
case 'session_query_unavailable':
|
|
353
|
+
return {
|
|
354
|
+
reason,
|
|
355
|
+
title: strings.queryUnavailableTitle,
|
|
356
|
+
body: strings.queryUnavailableBody,
|
|
357
|
+
detail: detailOut,
|
|
358
|
+
}
|
|
359
|
+
case 'trace_failed':
|
|
360
|
+
return {
|
|
361
|
+
reason,
|
|
362
|
+
title: strings.traceFailedTitle,
|
|
363
|
+
body: strings.traceFailedBody,
|
|
364
|
+
detail: detailOut,
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
return {
|
|
368
|
+
reason: 'unknown',
|
|
369
|
+
title: strings.unknownTitle,
|
|
370
|
+
body: formatTemplate(strings.unknownBody, { reason: reason ?? '?' }),
|
|
371
|
+
detail: detailOut,
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Discriminated render state of the lineage panel. */
|
|
377
|
+
export type LineageViewState =
|
|
378
|
+
| { kind: 'loading'; text: string }
|
|
379
|
+
/** Transport/HTTP failure (unlike available:false, this IS an error). */
|
|
380
|
+
| { kind: 'error'; text: string; detail: string | null }
|
|
381
|
+
| { kind: 'degraded'; card: LineageDegradeCardVM }
|
|
382
|
+
/** available:true but no trace body — honest empty state. */
|
|
383
|
+
| { kind: 'empty'; text: string }
|
|
384
|
+
| { kind: 'tree'; tree: LineageTreeVM }
|
|
385
|
+
|
|
386
|
+
/** Everything the lineage panel receives (mirrors LineageTree props). */
|
|
387
|
+
export interface LineageViewInput {
|
|
388
|
+
loading: boolean
|
|
389
|
+
/** Transport-level failure text from the integration, or null. */
|
|
390
|
+
error: string | null
|
|
391
|
+
available: boolean
|
|
392
|
+
/** Open vocabulary on purpose: newer backend reasons degrade gracefully. */
|
|
393
|
+
reason?: string | null
|
|
394
|
+
detail?: string | null
|
|
395
|
+
trace: LineageTraceVM | null
|
|
396
|
+
currentSessionId: string | null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Resolve what the lineage panel shows, in priority order:
|
|
401
|
+
* loading > transport error > degraded (available:false, per-reason card)
|
|
402
|
+
* > empty (no trace body) > the tree.
|
|
403
|
+
*/
|
|
404
|
+
export function deriveLineageView(input: LineageViewInput): LineageViewState {
|
|
405
|
+
if (input.loading) {
|
|
406
|
+
return { kind: 'loading', text: DSH_TOOLS_STRINGS.lineage.loading }
|
|
407
|
+
}
|
|
408
|
+
if (input.error !== null) {
|
|
409
|
+
return { kind: 'error', text: DSH_TOOLS_STRINGS.lineage.error, detail: input.error }
|
|
410
|
+
}
|
|
411
|
+
if (!input.available) {
|
|
412
|
+
return { kind: 'degraded', card: lineageDegradeCard(input.reason, input.detail) }
|
|
413
|
+
}
|
|
414
|
+
if (input.trace === null) {
|
|
415
|
+
return { kind: 'empty', text: DSH_TOOLS_STRINGS.lineage.empty }
|
|
416
|
+
}
|
|
417
|
+
return { kind: 'tree', tree: buildLineageTree(input.trace, input.currentSessionId) }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Search wire mirrors.
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
/** Search mode: 'filter-only' is the sessionQuery-absent degradation. */
|
|
425
|
+
export type SearchMode = 'full-text' | 'filter-only'
|
|
426
|
+
|
|
427
|
+
/** UnifiedSession subset the search rows render (host: fusion.ts). */
|
|
428
|
+
export interface SearchSessionVM {
|
|
429
|
+
agent: string
|
|
430
|
+
sessionId: string
|
|
431
|
+
title: string
|
|
432
|
+
project: string
|
|
433
|
+
status: string
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** One raw search hit (host: routes.ts handleSearch item). */
|
|
437
|
+
export interface SearchHitVM {
|
|
438
|
+
session: SearchSessionVM
|
|
439
|
+
/** 'full-text' | 'title' | 'project' today; open for newer backends. */
|
|
440
|
+
matchedBy: string
|
|
441
|
+
/** Best-match excerpt; full-text hits only, null otherwise. */
|
|
442
|
+
snippet: string | null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Body of `GET search?q=&project=&limit=` (host: routes.ts). */
|
|
446
|
+
export interface SearchResponseVM {
|
|
447
|
+
mode: SearchMode
|
|
448
|
+
query: string
|
|
449
|
+
project: string | null
|
|
450
|
+
items: readonly SearchHitVM[]
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Snippet highlighting (React-safe segments, no HTML injection).
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
/** One run of snippet text; `highlight` runs matched the query. */
|
|
458
|
+
export interface SnippetSegment {
|
|
459
|
+
text: string
|
|
460
|
+
highlight: boolean
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Split a snippet into plain/highlight segments around case-insensitive
|
|
465
|
+
* occurrences of the (trimmed) query. Best-effort: the engine does not
|
|
466
|
+
* mark its match, so a query the snippet spells differently simply yields
|
|
467
|
+
* one plain segment. Empty snippet → no segments.
|
|
468
|
+
*/
|
|
469
|
+
export function highlightSnippet(snippet: string, query: string): SnippetSegment[] {
|
|
470
|
+
if (snippet === '') return []
|
|
471
|
+
const needle = query.trim().toLowerCase()
|
|
472
|
+
if (needle === '') return [{ text: snippet, highlight: false }]
|
|
473
|
+
const haystack = snippet.toLowerCase()
|
|
474
|
+
const segments: SnippetSegment[] = []
|
|
475
|
+
let pos = 0
|
|
476
|
+
for (;;) {
|
|
477
|
+
const hit = haystack.indexOf(needle, pos)
|
|
478
|
+
if (hit === -1) break
|
|
479
|
+
if (hit > pos) segments.push({ text: snippet.slice(pos, hit), highlight: false })
|
|
480
|
+
segments.push({ text: snippet.slice(hit, hit + needle.length), highlight: true })
|
|
481
|
+
pos = hit + needle.length
|
|
482
|
+
}
|
|
483
|
+
if (pos < snippet.length) segments.push({ text: snippet.slice(pos), highlight: false })
|
|
484
|
+
return segments
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// Search-result normalization.
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
/** Known matchedBy vocabulary plus the honest catch-all. */
|
|
492
|
+
export type MatchedByToken = 'full-text' | 'title' | 'project' | 'other'
|
|
493
|
+
|
|
494
|
+
const MATCHED_BY_TOKENS: readonly MatchedByToken[] = ['full-text', 'title', 'project']
|
|
495
|
+
|
|
496
|
+
/** One render-ready search result row. */
|
|
497
|
+
export interface SearchItemVM {
|
|
498
|
+
sessionId: string
|
|
499
|
+
agent: string
|
|
500
|
+
/** Raw title (may be empty). */
|
|
501
|
+
title: string
|
|
502
|
+
/** Title with the untitled fallback applied. */
|
|
503
|
+
titleLabel: string
|
|
504
|
+
project: string
|
|
505
|
+
status: string
|
|
506
|
+
matchedBy: MatchedByToken
|
|
507
|
+
matchedByLabel: string
|
|
508
|
+
/** Pre-split highlight segments; null for non-full-text matches. */
|
|
509
|
+
snippet: SnippetSegment[] | null
|
|
510
|
+
shortId: string
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Wire hits → render rows. Snippets are split against the response's own
|
|
515
|
+
* echoed query; matchedBy values outside the known vocabulary map to the
|
|
516
|
+
* 'other' tag (label keeps a stable word, never invents a match kind).
|
|
517
|
+
*/
|
|
518
|
+
export function normalizeSearchItems(response: SearchResponseVM): SearchItemVM[] {
|
|
519
|
+
return response.items.map((hit) => {
|
|
520
|
+
const matchedBy = (MATCHED_BY_TOKENS as readonly string[]).includes(hit.matchedBy)
|
|
521
|
+
? (hit.matchedBy as MatchedByToken)
|
|
522
|
+
: 'other'
|
|
523
|
+
const title = hit.session.title
|
|
524
|
+
return {
|
|
525
|
+
sessionId: hit.session.sessionId,
|
|
526
|
+
agent: hit.session.agent,
|
|
527
|
+
title,
|
|
528
|
+
titleLabel: title.trim() === '' ? DSH_TOOLS_STRINGS.search.untitled : title,
|
|
529
|
+
project: hit.session.project,
|
|
530
|
+
status: hit.session.status,
|
|
531
|
+
matchedBy,
|
|
532
|
+
matchedByLabel: DSH_TOOLS_STRINGS.search.matchedBy[matchedBy],
|
|
533
|
+
snippet:
|
|
534
|
+
hit.snippet === null || hit.snippet === ''
|
|
535
|
+
? null
|
|
536
|
+
: highlightSnippet(hit.snippet, response.query),
|
|
537
|
+
shortId: abbreviateId(hit.session.sessionId),
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The filter-only degradation bar text; null when full-text search is up.
|
|
544
|
+
* Shown whenever the mode says so — the degradation is a capability fact,
|
|
545
|
+
* independent of whether the current query has results.
|
|
546
|
+
*/
|
|
547
|
+
export function searchDegradeNotice(mode: SearchMode): string | null {
|
|
548
|
+
return mode === 'filter-only' ? DSH_TOOLS_STRINGS.search.filterOnlyNotice : null
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Search panel view resolver.
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
/** Discriminated body state of the search panel. */
|
|
556
|
+
export type SearchBodyKind = 'loading' | 'error' | 'results' | 'empty' | 'idle'
|
|
557
|
+
|
|
558
|
+
export interface SearchViewState {
|
|
559
|
+
body: SearchBodyKind
|
|
560
|
+
/** Degradation bar text (independent of the body state), or null. */
|
|
561
|
+
notice: string | null
|
|
562
|
+
/** Localized text for loading/error/empty bodies, null otherwise. */
|
|
563
|
+
text: string | null
|
|
564
|
+
/** Transport error detail for the error body, null otherwise. */
|
|
565
|
+
detail: string | null
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Resolve the search panel body: loading > error > results > empty (a
|
|
570
|
+
* submitted non-blank query with zero hits) > idle (nothing asked yet).
|
|
571
|
+
*/
|
|
572
|
+
export function deriveSearchView(input: {
|
|
573
|
+
loading: boolean
|
|
574
|
+
error: string | null
|
|
575
|
+
mode: SearchMode
|
|
576
|
+
itemCount: number
|
|
577
|
+
query: string
|
|
578
|
+
}): SearchViewState {
|
|
579
|
+
const notice = searchDegradeNotice(input.mode)
|
|
580
|
+
if (input.loading) {
|
|
581
|
+
return { body: 'loading', notice, text: DSH_TOOLS_STRINGS.search.loading, detail: null }
|
|
582
|
+
}
|
|
583
|
+
if (input.error !== null) {
|
|
584
|
+
return {
|
|
585
|
+
body: 'error',
|
|
586
|
+
notice,
|
|
587
|
+
text: DSH_TOOLS_STRINGS.search.error,
|
|
588
|
+
detail: input.error,
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (input.itemCount > 0) return { body: 'results', notice, text: null, detail: null }
|
|
592
|
+
if (input.query.trim() !== '') {
|
|
593
|
+
return { body: 'empty', notice, text: DSH_TOOLS_STRINGS.search.empty, detail: null }
|
|
594
|
+
}
|
|
595
|
+
return { body: 'idle', notice, text: null, detail: null }
|
|
596
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized user-facing strings for the dsh deep-query tools (design
|
|
3
|
+
* §5.1 view 2 dsh 会话专属区: lineage tree / provenance jump / full-text
|
|
4
|
+
* search; §5.3 honest degradation wording).
|
|
5
|
+
*
|
|
6
|
+
* Deliberately a MODULE-LOCAL table (task T5.4 constraint): the main
|
|
7
|
+
* locale table (client/locales) and test/locales.test.ts are untouched —
|
|
8
|
+
* S7 merges this table into the locale registry in one move. Chinese is
|
|
9
|
+
* the primary UI language, same posture as board/strings.ts. Templates
|
|
10
|
+
* use `{name}` placeholders resolved by `formatTemplate` in logic.ts.
|
|
11
|
+
*
|
|
12
|
+
* Pure data module: no imports, no logic.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const DSH_TOOLS_STRINGS = {
|
|
18
|
+
/** Lineage tree panel (dsh 谱系/溯源). */
|
|
19
|
+
lineage: {
|
|
20
|
+
title: '会话谱系',
|
|
21
|
+
loading: '谱系加载中…',
|
|
22
|
+
/** Transport/HTTP failure headline (the detail carries the cause). */
|
|
23
|
+
error: '谱系加载失败',
|
|
24
|
+
/** available:true but the trace body is missing — honest empty state. */
|
|
25
|
+
empty: '暂无谱系数据',
|
|
26
|
+
/** Badge on the session the user is currently inspecting. */
|
|
27
|
+
currentBadge: '当前会话',
|
|
28
|
+
/** Badge on records live in this dsh process. */
|
|
29
|
+
liveBadge: '运行中',
|
|
30
|
+
/** Badge on records the trace knows but dsh has not persisted. */
|
|
31
|
+
notPersistedBadge: '未持久化',
|
|
32
|
+
/** Role labels for tree rows. */
|
|
33
|
+
role: {
|
|
34
|
+
ancestor: '祖先',
|
|
35
|
+
target: '目标',
|
|
36
|
+
descendant: '子会话',
|
|
37
|
+
},
|
|
38
|
+
/** Hover title for a clickable node (provenance jump). */
|
|
39
|
+
jumpTitle: '跳转到该会话',
|
|
40
|
+
/** Hover title for the highlighted current node (no jump). */
|
|
41
|
+
currentTitle: '正在查看该会话',
|
|
42
|
+
expand: '展开',
|
|
43
|
+
collapse: '折叠',
|
|
44
|
+
nodeCount: '{n} 个会话',
|
|
45
|
+
/** trace.complete === false with a known unresolved parent id. */
|
|
46
|
+
incompleteWithId: '谱系不完整:父会话 {id} 无法解析',
|
|
47
|
+
/** trace.complete === false without an unresolved parent id. */
|
|
48
|
+
incomplete: '谱系不完整:部分父链无法解析',
|
|
49
|
+
/** Degradation cards — available:false is data, not an error (§4.e.4). */
|
|
50
|
+
degrade: {
|
|
51
|
+
/** Client-side reason for sessions of non-dsh agents (task spec copy). */
|
|
52
|
+
notDshTitle: '谱系/溯源为 dsh 会话专属',
|
|
53
|
+
notDshBody: '当前会话来自外部 agent,dsh 的谱系与溯源能力不适用。',
|
|
54
|
+
/** Backend reason `session_query_unavailable`. */
|
|
55
|
+
queryUnavailableTitle: 'dsh 谱系服务不可用',
|
|
56
|
+
queryUnavailableBody:
|
|
57
|
+
'当前 dsh 组合未挂载 sessionQuery,谱系与溯源暂不可用。',
|
|
58
|
+
/** Backend reason `trace_failed`. */
|
|
59
|
+
traceFailedTitle: '谱系追溯失败',
|
|
60
|
+
traceFailedBody: 'dsh 无法解析该会话的谱系。',
|
|
61
|
+
/** Any reason outside the known vocabulary (never invent a state). */
|
|
62
|
+
unknownTitle: '谱系不可用',
|
|
63
|
+
unknownBody: '后端报告谱系不可用(原因: {reason})。',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
/** Cross-agent search panel (dsh 全文检索 + 降级过滤). */
|
|
67
|
+
search: {
|
|
68
|
+
title: '会话检索',
|
|
69
|
+
placeholder: '检索会话(标题 / 项目 / 全文)',
|
|
70
|
+
submit: '检索',
|
|
71
|
+
loading: '检索中…',
|
|
72
|
+
error: '检索失败',
|
|
73
|
+
/** Query submitted, zero hits. */
|
|
74
|
+
empty: '没有匹配的会话',
|
|
75
|
+
/** mode: 'filter-only' degradation bar (task spec copy, verbatim). */
|
|
76
|
+
filterOnlyNotice: 'dsh 全文检索不可用,已降级为标题/项目过滤',
|
|
77
|
+
/** Active project-filter chip. */
|
|
78
|
+
projectFilter: '项目过滤: {project}',
|
|
79
|
+
/** matchedBy tags on result rows. */
|
|
80
|
+
matchedBy: {
|
|
81
|
+
'full-text': '全文',
|
|
82
|
+
title: '标题',
|
|
83
|
+
project: '项目',
|
|
84
|
+
other: '其他',
|
|
85
|
+
},
|
|
86
|
+
untitled: '(无标题)',
|
|
87
|
+
},
|
|
88
|
+
} as const
|
|
89
|
+
|
|
90
|
+
export type DshToolsStrings = typeof DSH_TOOLS_STRINGS
|