dsh-session-flow 1.0.0 → 1.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/README.en.md CHANGED
@@ -33,7 +33,9 @@ Restructures DSH session message streams into a **foldable information flow with
33
33
  | Dual-mode summary | Rule summary (zero cost, assembled live) + LLM summary (cached, prompts regeneration) |
34
34
  | Live tracking | 3s polling for running sessions, breathing highlight + generating indicator |
35
35
  | ZIP export | Overview + chunked timeline as a Markdown report |
36
- | Session rename | Custom titles as a display-layer overlay, restore anytime |
36
+ | Session rename | Same data source as the official title (log-backed), synced both ways in real time |
37
+ | Health monitor | Four-level classification: active / tool-running / quiet / likely stalled; overview badges + detail live bar |
38
+ | Header health chip | Next to the mode label in the session header; a vertical bar that expands into a status card on hover, click to open detail |
37
39
  | Session Flow tab | Embedded tab on the native conversation page, cross-navigates with the workbench |
38
40
  | Turn rail | Turn jumps on the left of the Chat tab (Chat tab only) |
39
41
  | Cross-session full-text search | Content-level recall, hits jump into the Session Flow tab and locate |
@@ -56,7 +58,8 @@ Requirements: DSH Web GUI (latest stable); Node.js ≥ 22.19 (built-in zstd deco
56
58
 
57
59
  - **Overview**: sidebar "Session Flow" → all session cards. Search supports `tool:` / `file:` / `err:` prefixes; free text of ≥2 characters additionally triggers cross-session full-text search.
58
60
  - **Detail**: click a card. Turns are folded by default — click a turn header to expand; the four-tab navigation locates entries; the header offers LLM summary, ZIP export, dock right, and lineage.
59
- - **Native conversation page**: the "Session Flow" tab reviews the current session in place; the turn rail on the Chat tab jumps to any turn; hover the title for ✎ rename.
61
+ - **Native conversation page**: the "Session Flow" tab reviews the current session in place; the turn rail on the Chat tab jumps to any turn; hover the title for ✎ rename (synced with the official title in real time).
62
+ - **Health status**: the health chip right of the mode label in the header (vertical bar: green = active / amber = tool running / red = likely stalled) expands on hover; click to open the Session Flow detail. Overview cards and the live bar show the same badges.
60
63
  - **Live**: click "Live" on a running session, auto-refresh every 3 seconds.
61
64
 
62
65
  ## Acknowledgements
package/README.md CHANGED
@@ -33,7 +33,9 @@
33
33
  | 双模式摘要 | 规则摘要(零开销实时组装)+ LLM 摘要(落盘复用、新对话提示重生成) |
34
34
  | 实时跟踪 | 运行中会话 3 秒轮询,呼吸高亮 + 生成标志 |
35
35
  | ZIP 导出 | 概览 + 时间线分卷 Markdown 报告 |
36
- | 会话重命名 | 自定义标题显示层覆盖,随时恢复原名 |
36
+ | 会话重命名 | 与官方标题同一数据源(log-backed),双向实时同步,随时改名 |
37
+ | 健康监控 | 活跃 / 工具执行中 / 静默中 / 疑似卡死四级分类;总览徽标 + 详情实时条 |
38
+ | 头部健康芯片 | 会话页顶栏模式标识右侧,竖条收拢 ⇄ 悬浮展开状态卡,点击直达详情 |
37
39
  | 会话流标签页 | 原生会话页内嵌标签,与工作台互跳 |
38
40
  | 轮次悬浮条 | 对话页左侧轮次跳转(仅对话页显示) |
39
41
  | 跨会话全文检索 | 内容级召回,命中直达会话流标签页定位 |
@@ -56,7 +58,8 @@ dsh plugin add github:YeqingTang/dsh-session-flow
56
58
 
57
59
  - **总览**:侧边栏「会话流」→ 全部会话卡片。搜索支持 `tool:` / `file:` / `err:` 前缀;自由文本 ≥2 字符自动追加跨会话全文检索结果。
58
60
  - **详情**:点卡片进入。回合默认折叠,点回合头展开;右侧四标签导航定位;顶部可生成 LLM 摘要、导出 ZIP、并入右栏、查看血缘。
59
- - **原生会话页**:会话顶部「会话流」标签直接回顾当前会话;对话页左侧轮次悬浮条跳转任意轮次;标题 hover ✎ 重命名。
61
+ - **原生会话页**:会话顶部「会话流」标签直接回顾当前会话;对话页左侧轮次悬浮条跳转任意轮次;标题 hover ✎ 重命名(与官方标题实时同步)。
62
+ - **健康状态**:顶栏模式标识右侧的健康芯片(竖条:绿=活跃 / 黄=工具执行中 / 红=疑似卡死),悬浮展开看详情,点击直达会话流详情页;总览卡片与详情实时条同步显示徽标。
60
63
  - **实时**:运行中会话点「实时」,3 秒自动刷新。
61
64
 
62
65
  ## 致谢
package/lib/client.js CHANGED
@@ -72,6 +72,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
72
72
  summaryStale: '⚠ 摘要生成后有新对话,内容可能已不准确,建议重新生成',
73
73
  llmSummary: '生成 LLM 摘要', llmGenerating: '生成中…', llmFail: '生成失败', summaryRuleTag: '规则', summaryLlmTag: 'LLM',
74
74
  cacheHint: '仅清理本插件的缓存(可自动重建),不影响 DSH 会话存档与任何其他数据', cacheLimit: '时间线缓存上限',
75
+ healthActive: '活跃', healthToolWait: '工具执行中', healthQuiet: '静默中', healthStalled: '疑似卡死',
76
+ healthSilentMin: '静默 {N} 分钟', healthSilentSec: '静默 {N} 秒',
77
+ healthChipTitle: '会话健康状态(会话流)· 点击打开详情',
78
+ renameClearBlocked: '该会话标题由官方会话管理:请输入新标题(不支持清空)',
75
79
  } : {
76
80
  entry: 'Session Flow', title: 'Session Flow', rescan: 'Rescan', scanning: 'Scanning…',
77
81
  pianoTitle: 'Turns', pianoNew: 'New session', pianoFav: 'Favorite', pianoUnfav: 'Unfavorite', pianoFavs: 'Favorites', pianoOpenInFlow: 'Open in Session Flow',
@@ -111,6 +115,10 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
111
115
  summaryStale: '⚠ New conversation since summary was generated; content may be outdated — regenerate',
112
116
  llmSummary: 'Generate LLM summary', llmGenerating: 'Generating…', llmFail: 'Failed', summaryRuleTag: 'rule', summaryLlmTag: 'LLM',
113
117
  cacheHint: "Only this plugin's cache (auto-rebuilds); never touches DSH session archives or other data", cacheLimit: 'Timeline cache limit',
118
+ healthActive: 'Active', healthToolWait: 'Tool running', healthQuiet: 'Quiet', healthStalled: 'Likely stalled',
119
+ healthSilentMin: 'silent {N} min', healthSilentSec: 'silent {N} s',
120
+ healthChipTitle: 'Session health (Session Flow) · click to open detail',
121
+ renameClearBlocked: 'Title is managed by the official session: enter a new title (clearing unsupported)',
114
122
  }
115
123
 
116
124
  // ── 右侧导航图标(组件级共享,定义在顶层避免作用域问题)───────────
@@ -179,6 +187,29 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
179
187
  `.sf-badgeEnd{background:rgba(120,120,120,.12);color:#888}`,
180
188
  `.sf-badgeSub{background:rgba(240,150,60,.15);color:#d97706}`,
181
189
  `.sf-badgeErr{background:rgba(230,80,80,.13);color:#d43b3b}`,
190
+ // 卡死监控徽标:疑似卡死(红,呼吸动画引起注意)/ 工具执行中·静默中(黄)。
191
+ `.sf-badgeStall{background:rgba(230,80,80,.16);color:#d43b3b;animation:sfBadgePulse 2.8s ease-in-out infinite}`,
192
+ `.sf-badgeWait{background:rgba(220,170,40,.15);color:#a87b00}`,
193
+ // 会话页头部健康芯片(官方 header.actions 槽位,模式标识右侧)。
194
+ // 钢琴键同款语言:收拢态 = 彩色短胶囊条(语义色保留:绿/黄/红,卡死带呼吸脉冲);
195
+ // 悬浮/聚焦展开为毛玻璃文字标签(backdrop-filter + color-mix,与 pk-key 展开态同构)。
196
+ `.sf-healthChip{display:inline-flex;align-items:center;gap:6px;height:20px;padding:0 3px;margin-left:2px;border:none;border-radius:99px;background:transparent;cursor:pointer;font-family:inherit;overflow:hidden;outline:none;transition:padding .22s ease,background .18s ease,border-color .18s ease,box-shadow .18s ease}`,
197
+ `.sf-healthChip .sf-hcBar{flex:none;width:4px;height:16px;border-radius:99px;transition:width .22s ease,height .22s ease}`,
198
+ `.sf-healthChip .sf-hcLabel{font-size:11px;line-height:16px;white-space:nowrap;max-width:0;opacity:0;overflow:hidden;user-select:none;transition:max-width .24s ease,opacity .16s ease .05s}`,
199
+ // 展开态:毛玻璃底(blur 10px)+ 同色系淡 tint 背景(状态色 14% 透明混入 72% 基底色),
200
+ // 边框带 40% 状态色呼应;tint 淡雅、毛玻璃质感保留。
201
+ `.sf-healthChip:hover,.sf-healthChip:focus-visible{padding:3px 9px;border:1px solid var(--dsw-alias-border-l2,#e4e6eb);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:0 1px 3px rgba(0,0,0,.07)}`,
202
+ `.sf-healthChip.hc-active:hover,.sf-healthChip.hc-active:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(28,158,90,.14));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#1c9e5a)}`,
203
+ `.sf-healthChip.hc-wait:hover,.sf-healthChip.hc-wait:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(217,165,20,.14));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#d9a514)}`,
204
+ `.sf-healthChip.hc-stall:hover,.sf-healthChip.hc-stall:focus-visible{background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 72%,rgba(212,59,59,.14));border-color:color-mix(in srgb,var(--dsw-alias-border-l2,#e4e6eb) 60%,#d43b3b)}`,
205
+ `.sf-healthChip:hover .sf-hcBar,.sf-healthChip:focus-visible .sf-hcBar{width:6px;height:6px;border-radius:99px}`,
206
+ `.sf-healthChip:hover .sf-hcLabel,.sf-healthChip:focus-visible .sf-hcLabel{max-width:200px;opacity:1}`,
207
+ `.sf-healthChip.hc-active .sf-hcBar{background:#1c9e5a}`,
208
+ `.sf-healthChip.hc-active .sf-hcLabel{color:#1c9e5a}`,
209
+ `.sf-healthChip.hc-wait .sf-hcBar{background:#d9a514}`,
210
+ `.sf-healthChip.hc-wait .sf-hcLabel{color:#a87b00}`,
211
+ `.sf-healthChip.hc-stall .sf-hcBar{background:#d43b3b;animation:sfBadgePulse 2.8s ease-in-out infinite}`,
212
+ `.sf-healthChip.hc-stall .sf-hcLabel{color:#d43b3b}`,
182
213
  `.sf-statsRow{display:flex;gap:12px;flex-wrap:wrap;margin-top:3px}`,
183
214
  `.sf-hint{color:var(--dsw-alias-label-secondary,#888);font-size:12px;padding:2px 2px 0}`,
184
215
  // ── 工作区筛选 tabs(独立一行,醒目)──
@@ -562,6 +593,104 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
562
593
  )
563
594
  }
564
595
 
596
+ // ── 重命名对齐官方 + 卡死监控 共享辅助 ─────────────────────────────
597
+ /** 官方 rename RPC(log-backed session/title user 事件):本插件与官方侧栏同一数据源。 */
598
+ const renameViaOfficial = (conn, sessionId, title) => {
599
+ return conn.api.sessions.rename({ sessionId, title }).then((resp) => {
600
+ const res = resp && resp.result
601
+ if (res && res.ok === true && res.value && typeof res.value.title === 'string') return res.value.title
602
+ throw new Error((res && res.error && res.error.message) || STR.renameFail)
603
+ })
604
+ }
605
+ const canOfficialRename = (conn) =>
606
+ conn !== undefined && conn.api !== undefined && conn.api.sessions !== undefined && typeof conn.api.sessions.rename === 'function'
607
+ /** 静默时长文本({N} 占位)。 */
608
+ const fmtSilent = (idleMs) => {
609
+ if (idleMs === null || idleMs === undefined) return ''
610
+ const min = Math.floor(idleMs / 60000)
611
+ return min >= 1
612
+ ? STR.healthSilentMin.replace('{N}', String(min))
613
+ : STR.healthSilentSec.replace('{N}', String(Math.max(1, Math.round(idleMs / 1000))))
614
+ }
615
+ /** 健康徽标(详情实时条 / 总览卡片共用);active/ended/unknown 不渲染(已有进行中/已结束徽标)。 */
616
+ const healthBadge = (health) => {
617
+ if (!health) return null
618
+ const silent = fmtSilent(health.idleMs)
619
+ if (health.kind === 'stalled') return h('span', { className: 'sf-badge sf-badgeStall' }, '🔴 ' + STR.healthStalled + (silent ? ' · ' + silent : ''))
620
+ if (health.kind === 'tool-wait') return h('span', { className: 'sf-badge sf-badgeWait' }, '🟡 ' + STR.healthToolWait + (silent ? ' · ' + silent : ''))
621
+ if (health.kind === 'quiet') return h('span', { className: 'sf-badge sf-badgeWait' }, '🟡 ' + STR.healthQuiet + (silent ? ' · ' + silent : ''))
622
+ return null
623
+ }
624
+
625
+ // ── 会话页头部健康芯片(官方 conversation.session.header.actions 槽位)──
626
+ // 位置:官方模式标识(agent-preset label, order -10)右侧(order -9)。
627
+ // 数据:sessions.list 快照跟踪 running(官方真实状态,随 host 流即时翻转)→
628
+ // running 时 10s 轮询 tail history → host derive 健康分类(与总览探测同管线)。
629
+ // 点击:唤起会话流工作台并直达该会话详情(workbenchBridge 意图桥)。
630
+ function SessionHealthChip(props) {
631
+ const { sessionId, connection, sessions } = props
632
+ const [running, setRunning] = useState(false)
633
+ const [health, setHealth] = useState(null)
634
+ useEffect(() => {
635
+ if (!sessionId || !sessions || !sessions.list || typeof sessions.list.getSnapshot !== 'function') return undefined
636
+ const compute = () => {
637
+ try {
638
+ // sessions.list 快照 = { ids, current, byId }(byId[id] 含 running)——
639
+ // 不是 { items }(那是 workspace 浏览器用的另一个视图快照,曾误用导致芯片恒 null)。
640
+ const snap = sessions.list.getSnapshot()
641
+ const it = snap && snap.byId ? snap.byId[sessionId] : undefined
642
+ setRunning(it ? it.running === true : false)
643
+ } catch (e) { setRunning(false) }
644
+ }
645
+ compute()
646
+ if (typeof sessions.list.subscribe === 'function') return sessions.list.subscribe(compute)
647
+ return undefined
648
+ }, [sessions, sessionId])
649
+ useEffect(() => {
650
+ if (!running || !sessionId || !canOfficialRename(connection)) { setHealth(null); return undefined }
651
+ let alive = true
652
+ const probe = async () => {
653
+ try {
654
+ const hr = await connection.api.sessions.history({ sessionId, maxMessages: 3 })
655
+ const val = hr && hr.result && hr.result.value
656
+ const events = val && Array.isArray(val.events) ? val.events.map((e) => (e && e.event) || e) : []
657
+ if (events.length === 0) { if (alive) setHealth(null); return }
658
+ const d = await api('derive', { events, now: Date.now(), assumeRunning: true })
659
+ if (alive && d && d.ok) setHealth(d.health || null)
660
+ } catch (e) {}
661
+ }
662
+ probe()
663
+ const iv = setInterval(probe, 10000)
664
+ return () => { alive = false; clearInterval(iv) }
665
+ }, [running, sessionId, connection])
666
+ if (!running || !health) return null
667
+ const kind = health.kind
668
+ const silent = fmtSilent(health.idleMs)
669
+ // 收拢态胶囊条颜色语义:绿=活跃 / 黄=工具执行中·静默中 / 红=疑似卡死(脉冲)。
670
+ let text = STR.healthActive, cls = 'hc-active'
671
+ if (kind === 'tool-wait') { text = STR.healthToolWait; cls = 'hc-wait' }
672
+ else if (kind === 'quiet') { text = STR.healthQuiet; cls = 'hc-wait' }
673
+ else if (kind === 'stalled') { text = STR.healthStalled; cls = 'hc-stall' }
674
+ else if (kind !== 'active') return null // ended/unknown 不显示
675
+ return h('button', {
676
+ className: 'sf-healthChip ' + cls,
677
+ title: STR.healthChipTitle,
678
+ onClick: (e) => { e.stopPropagation(); if (props.onOpen) props.onOpen() },
679
+ },
680
+ h('span', { className: 'sf-hcBar' }),
681
+ h('span', { className: 'sf-hcLabel' }, text + (silent && kind !== 'active' ? ' · ' + silent : '')),
682
+ )
683
+ }
684
+
685
+ // 工作台打开意图桥:头部芯片点击 → 唤起工作台并直达该会话详情。
686
+ // (工厂作用域:芯片渲染于官方槽位,与本插件视图分属不同 React 子树。)
687
+ const workbenchBridge = {
688
+ intent: null,
689
+ listeners: new Set(),
690
+ open(sid) { this.intent = sid; for (const fn of [...this.listeners]) fn(sid) },
691
+ subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn) },
692
+ }
693
+
565
694
  // ── 总览页 ──────────────────────────────────────────────────────────
566
695
  // props: { onOpen: (session) => void, onClose: () => void }
567
696
  function SessionFlowOverview(props) {
@@ -583,28 +712,93 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
583
712
  // M8a:当前编辑重命名的会话 id(null = 无编辑态)。
584
713
  const [renamingId, setRenamingId] = useState(null)
585
714
  // M8a:保存自定义标题 → 更新本地列表数据(userTitle 覆盖显示,不整页重刷)。
715
+ // 对齐官方(2026-08 起):非空标题且连接可用时优先走官方 session.rename RPC
716
+ // (log-backed,官方侧栏同步);空标题(清除)/官方拒绝(子代理 agent-busy 等)回退 legacy renames.json。
586
717
  const saveRename = (sid, title) => {
587
- return api('rename', { sessionId: sid, title }).then((json) => {
588
- if (json && json.ok) {
589
- setState((prev) => {
590
- if (!prev || !prev.data) return prev
591
- return {
592
- ...prev,
593
- data: {
594
- ...prev.data,
595
- workspaces: (prev.data.workspaces || []).map((g) => ({
596
- ...g,
597
- sessions: (g.sessions || []).map((s) => (s.id === sid ? { ...s, userTitle: json.userTitle } : s)),
598
- })),
599
- },
600
- }
601
- })
602
- setRenamingId(null)
603
- return true
604
- }
718
+ const conn = props.connection
719
+ const applyLocal = (userTitle) => {
720
+ setState((prev) => {
721
+ if (!prev || !prev.data) return prev
722
+ return {
723
+ ...prev,
724
+ data: {
725
+ ...prev.data,
726
+ workspaces: (prev.data.workspaces || []).map((g) => ({
727
+ ...g,
728
+ sessions: (g.sessions || []).map((s) => (s.id === sid ? { ...s, userTitle } : s)),
729
+ })),
730
+ },
731
+ }
732
+ })
733
+ setRenamingId(null)
734
+ return true
735
+ }
736
+ const legacy = () => api('rename', { sessionId: sid, title }).then((json) => {
737
+ if (json && json.ok) return applyLocal(json.userTitle)
605
738
  throw new Error((json && json.error) || STR.renameFail)
606
739
  })
740
+ const trimmed = String(title || '').trim()
741
+ // 官方 user 标题不可经本插件清空(官方服务无删除语义):阻止并提示。
742
+ if (trimmed === '') {
743
+ const all = ((state.data && state.data.workspaces) || []).flatMap((g) => g.sessions || [])
744
+ const target = all.find((x) => x.id === sid)
745
+ if (target && target.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
746
+ }
747
+ if (trimmed !== '' && canOfficialRename(conn)) {
748
+ return renameViaOfficial(conn, sid, trimmed)
749
+ .then((accepted) => applyLocal(accepted))
750
+ .catch(() => legacy())
751
+ }
752
+ return legacy()
607
753
  }
754
+ // ── 卡死监控:总览探测 ─────────────────────────────────────────────
755
+ // 官方 sessions.list 的 running(attached agent 真实状态,比 15 分钟启发式准)+
756
+ // tail history → host derive 健康分类。30s 轮询,上限 8 个、并发 3。
757
+ const [healthMap, setHealthMap] = useState({})
758
+ const [liveRunning, setLiveRunning] = useState(null) // null=未探测;Set<sessionId>
759
+ useEffect(() => {
760
+ const conn = props.connection
761
+ if (!canOfficialRename(conn) || state.phase !== 'ready') return undefined
762
+ let alive = true
763
+ let probing = false
764
+ const probe = async () => {
765
+ if (probing) return
766
+ probing = true
767
+ try {
768
+ const resp = await conn.api.sessions.list({})
769
+ const items = (resp && resp.result && resp.result.value && resp.result.value.items) || []
770
+ const runningIds = items.filter((i) => i.running === true).map((i) => i.sessionId)
771
+ if (!alive) return
772
+ setLiveRunning(new Set(runningIds))
773
+ const next = {}
774
+ for (let i = 0; i < Math.min(runningIds.length, 8); i += 3) {
775
+ await Promise.all(runningIds.slice(i, i + 3).map(async (sid) => {
776
+ try {
777
+ const hr = await conn.api.sessions.history({ sessionId: sid, maxMessages: 3 })
778
+ const val = hr && hr.result && hr.result.value
779
+ const events = val && Array.isArray(val.events) ? val.events.map((e) => (e && e.event) || e) : []
780
+ if (events.length === 0) return
781
+ const d = await api('derive', { events, now: Date.now(), assumeRunning: true })
782
+ if (d && d.ok && d.health) next[sid] = d.health
783
+ } catch {}
784
+ }))
785
+ if (!alive) return
786
+ }
787
+ if (alive) setHealthMap(next)
788
+ } catch {} finally { probing = false }
789
+ }
790
+ probe()
791
+ const iv = setInterval(probe, 30000)
792
+ return () => { alive = false; clearInterval(iv) }
793
+ }, [props.connection, state.phase])
794
+ // 头部芯片直达:工作台打开意图 → 列表就绪后自动进入该会话详情(每个意图只消费一次)。
795
+ const intentConsumed = useRef(null)
796
+ useEffect(() => {
797
+ if (!props.intentId || intentConsumed.current === props.intentId || state.phase !== 'ready') return
798
+ const all = ((state.data && state.data.workspaces) || []).flatMap((g) => g.sessions || [])
799
+ const target = all.find((x) => x.id === props.intentId)
800
+ if (target) { intentConsumed.current = props.intentId; props.onOpen(target) }
801
+ }, [props.intentId, state.phase])
608
802
  useEffect(() => () => {
609
803
  if (fullTextTimer.current) clearTimeout(fullTextTimer.current)
610
804
  if (fullTextAbort.current) fullTextAbort.current.abort()
@@ -738,7 +932,11 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
738
932
  }
739
933
  }
740
934
  const now = Date.now()
741
- const running = s.lastEventTime !== null && s.lastEventTime !== undefined && (now - s.lastEventTime) < ACTIVE_WINDOW_MS
935
+ // 卡死监控:官方探测过(liveRunning null)则以 attached agent 真实状态为准,
936
+ // 否则回退 15 分钟活动窗口启发式。
937
+ const running = liveRunning === null
938
+ ? s.lastEventTime !== null && s.lastEventTime !== undefined && (now - s.lastEventTime) < ACTIVE_WINDOW_MS
939
+ : liveRunning.has(s.id)
742
940
  out.push({ ...s, workspace: group.name, workspaceLabel: wsLabelOf(group), workspaceCwd: wsCwdOf(group), running, isEmpty: isSessionEmpty(s) })
743
941
  }
744
942
  }
@@ -754,7 +952,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
754
952
  const ascending = sort === 'oldest'
755
953
  out.sort((a, b) => (ascending ? key(a) - key(b) : key(b) - key(a)))
756
954
  return out
757
- }, [state.data, query, wsFilter, sort, showEmpty, showIssues])
955
+ }, [state.data, query, wsFilter, sort, showEmpty, showIssues, liveRunning])
758
956
 
759
957
  // M5b 档案统计:聚合工具出现热度与问题会话数,跟随选中的工作区(wsFilter)变化。
760
958
  const stats = useMemo(() => {
@@ -904,13 +1102,14 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
904
1102
  h('div', { className: 'sf-cardTitle' },
905
1103
  renamingId === s.id
906
1104
  ? h(RenameInline, {
907
- initial: s.userTitle || s.title || '',
1105
+ initial: (props.liveTitles && props.liveTitles[s.id]) || s.userTitle || s.title || '',
908
1106
  onSave: (t) => saveRename(s.id, t),
909
1107
  onCancel: () => setRenamingId(null),
910
1108
  })
911
1109
  : h('span', { className: 'sf-cardText', style: { overflow: 'hidden', textOverflow: 'ellipsis' } },
912
- s.userTitle || s.title || STR.unknownTitle,
913
- s.userTitle && h('span', { className: 'sf-origTitle', title: s.title || STR.unknownTitle },
1110
+ // 显示优先级:mux 近实时官方标题 > userTitle(档案 user/遗留 overlay)> 自动标题。
1111
+ (props.liveTitles && props.liveTitles[s.id]) || s.userTitle || s.title || STR.unknownTitle,
1112
+ (s.userTitle || (props.liveTitles && props.liveTitles[s.id])) && h('span', { className: 'sf-origTitle', title: s.title || STR.unknownTitle },
914
1113
  STR.origTitle + ': ' + (s.title || STR.unknownTitle)),
915
1114
  h('button', {
916
1115
  className: 'sf-renameBtn', title: STR.rename,
@@ -919,6 +1118,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
919
1118
  ),
920
1119
  s.isEmpty && h('span', { className: 'sf-badge sf-badgeEnd' }, STR.emptySession),
921
1120
  s.running ? h('span', { className: 'sf-badge sf-badgeRun' }, STR.running) : h('span', { className: 'sf-badge sf-badgeEnd' }, STR.ended),
1121
+ healthBadge(healthMap[s.id]),
922
1122
  s.delegationDepth > 0 && h('span', { className: 'sf-badge sf-badgeSub' }, STR.subagent + ' · d' + s.delegationDepth),
923
1123
  s.toolErrors > 0 && h('span', { className: 'sf-badge sf-badgeErr' }, STR.errors + ': ' + s.toolErrors),
924
1124
  ),
@@ -1357,8 +1557,12 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1357
1557
  // M8a:标题重命名(编辑态 + 保存后的显示覆盖,优先于 session.userTitle)。
1358
1558
  const [renaming, setRenaming] = useState(false)
1359
1559
  const [titleOverride, setTitleOverride] = useState(null)
1560
+ // mux 近实时官方标题(SessionFlowView 订阅 session/projection(title) 帧传入)。
1561
+ const liveTitleOf = (id) => (props.liveTitles && props.liveTitles[id]) || null
1562
+ // 对齐官方:优先官方 session.rename RPC(log-backed,双向一致);空标题/官方拒绝回退 legacy。
1360
1563
  const saveRename = (title) => {
1361
- return api('rename', { sessionId: session.id, title }).then((json) => {
1564
+ const conn = props.connection
1565
+ const legacy = () => api('rename', { sessionId: session.id, title }).then((json) => {
1362
1566
  if (json && json.ok) {
1363
1567
  setTitleOverride(json.userTitle) // null = 清除恢复原名
1364
1568
  setRenaming(false)
@@ -1366,6 +1570,14 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1366
1570
  }
1367
1571
  throw new Error((json && json.error) || STR.renameFail)
1368
1572
  })
1573
+ const trimmed = String(title || '').trim()
1574
+ if (trimmed === '' && session.titleSource === 'user') return Promise.reject(new Error(STR.renameClearBlocked))
1575
+ if (trimmed !== '' && canOfficialRename(conn)) {
1576
+ return renameViaOfficial(conn, session.id, trimmed)
1577
+ .then((accepted) => { setTitleOverride(accepted); setRenaming(false); return true })
1578
+ .catch(() => legacy())
1579
+ }
1580
+ return legacy()
1369
1581
  }
1370
1582
 
1371
1583
  useEffect(() => {
@@ -1514,7 +1726,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1514
1726
  return
1515
1727
  }
1516
1728
  const events = val.events.map((entry) => (entry && entry.event) || entry)
1517
- const derived = await api('derive', { events })
1729
+ const derived = await api('derive', { events, now: Date.now() })
1518
1730
  if (!derived || !derived.ok) {
1519
1731
  setLiveState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
1520
1732
  return
@@ -1537,6 +1749,8 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1537
1749
  // 运行中 = 事件结构信号(未闭合回合/步骤/工具调用或流式中间态),
1538
1750
  // 不依赖时间戳:输出间隔长(模型思考/长工具执行)时不会误判停止。
1539
1751
  running: liveRunning,
1752
+ // 卡死监控:host 分类(active/tool-wait/quiet/stalled/ended)。
1753
+ health: derived.health || null,
1540
1754
  })
1541
1755
  // 只把「新出现的回合」并入折叠集合(默认折叠);已展开的回合不被收拢。
1542
1756
  const newTurns = displayTl.filter((t) => !liveSeenTurns.current.has(t.turn))
@@ -1563,7 +1777,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1563
1777
  const exitLive = () => {
1564
1778
  setLiveOn(false)
1565
1779
  liveSeenTurns.current.clear()
1566
- setLiveState({ phase: 'idle', error: null, timeline: null, eventCount: 0, lastActive: null, running: false })
1780
+ setLiveState({ phase: 'idle', error: null, timeline: null, eventCount: 0, lastActive: null, running: false, health: null })
1567
1781
  if (liveTimer.current !== null) { clearInterval(liveTimer.current); liveTimer.current = null }
1568
1782
  }
1569
1783
  useEffect(() => () => { if (liveTimer.current !== null) clearInterval(liveTimer.current) }, [])
@@ -1779,13 +1993,13 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1779
1993
  h('h2', { className: 'sf-viewTitle', style: { flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis' } },
1780
1994
  renaming
1781
1995
  ? h(RenameInline, {
1782
- initial: titleOverride || session.userTitle || session.title || '',
1996
+ initial: titleOverride || liveTitleOf(session.id) || session.userTitle || session.title || '',
1783
1997
  onSave: saveRename,
1784
1998
  onCancel: () => setRenaming(false),
1785
1999
  })
1786
2000
  : h('span', { style: { display: 'inline-flex', alignItems: 'center', minWidth: 0 } },
1787
- h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: (titleOverride || session.userTitle) ? session.title || STR.unknownTitle : undefined },
1788
- titleOverride || session.userTitle || session.title || STR.unknownTitle),
2001
+ h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: (titleOverride || liveTitleOf(session.id) || session.userTitle) ? session.title || STR.unknownTitle : undefined },
2002
+ titleOverride || liveTitleOf(session.id) || session.userTitle || session.title || STR.unknownTitle),
1789
2003
  h('button', {
1790
2004
  className: 'sf-renameBtn', title: STR.rename,
1791
2005
  onClick: () => setRenaming(true),
@@ -1889,6 +2103,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
1889
2103
  // M6 实时模式:sessions.history → derive 的完整折叠时间线(轮询刷新)。
1890
2104
  liveOn && h('div', { className: 'sf-liveBar', style: { flex: 'none' } },
1891
2105
  h('span', { className: 'sf-badge sf-badgeRun', style: { flex: 'none' } }, STR.liveActive),
2106
+ healthBadge(liveState.health),
1892
2107
  h('span', { className: 'sf-liveText' },
1893
2108
  h('span', { className: 'sf-muted', style: { flex: 'none' } }, (liveState.eventCount || 0) + ' ' + STR.liveEvents),
1894
2109
  liveState.lastActive && h('span', { className: 'sf-muted', style: { flex: 'none' } }, STR.liveLastActive + ' ' + fmtTime(liveState.lastActive)),
@@ -2119,7 +2334,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2119
2334
  // 内实时通道同构(结构信号运行判定 / 只折叠新回合 / 不传 hideHead),自包含实现。
2120
2335
  function SessionFlowLiveFallback(props) {
2121
2336
  const { sessionId, connection } = props
2122
- const [state, setState] = useState({ phase: 'loading', error: null, timeline: null, eventCount: 0, running: false })
2337
+ const [state, setState] = useState({ phase: 'loading', error: null, timeline: null, eventCount: 0, running: false, health: null })
2123
2338
  const [collapsed, setCollapsed] = useState(() => new Set())
2124
2339
  const [activeTurn, setActiveTurn] = useState(null)
2125
2340
  const liveSeenTurns = useRef(new Set())
@@ -2140,7 +2355,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2140
2355
  return
2141
2356
  }
2142
2357
  const events = val.events.map((entry) => (entry && entry.event) || entry)
2143
- const derived = await api('derive', { events })
2358
+ const derived = await api('derive', { events, now: Date.now() })
2144
2359
  if (!derived || !derived.ok) {
2145
2360
  setState((s) => ({ ...s, phase: 'error', error: (derived && derived.error) || STR.liveFail }))
2146
2361
  return
@@ -2149,6 +2364,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2149
2364
  setState({
2150
2365
  phase: 'ready', error: null, timeline: tl, eventCount: events.length,
2151
2366
  running: derived.running === true,
2367
+ health: derived.health || null,
2152
2368
  })
2153
2369
  // 只折叠新出现的回合(默认折叠),已展开的保持用户状态(M6 经验)。
2154
2370
  const newTurns = tl.filter((t) => !liveSeenTurns.current.has(t.turn))
@@ -2184,6 +2400,7 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2184
2400
  return h('div', { className: 'sf-view' },
2185
2401
  h('div', { className: 'sf-viewHeader' },
2186
2402
  h('span', { className: 'sf-badge sf-badgeRun', style: { flex: 'none' } }, STR.liveActive),
2403
+ healthBadge(state.health),
2187
2404
  h('span', { className: 'sf-liveText' }, STR.liveFallbackHint),
2188
2405
  h('button', { className: 'sf-btn sf-liveExitBtn', onClick: props.onRetry }, STR.retryArchive),
2189
2406
  ),
@@ -2202,6 +2419,32 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2202
2419
  // ── 主视图(总览 ⇄ 详情)───────────────────────────────────────────
2203
2420
  function SessionFlowView(props) {
2204
2421
  const [detail, setDetail] = useState(null) // { session, searchQuery }
2422
+ // 头部芯片直达意图(workbenchBridge):收到会话 id → 总览数据就绪后自动进详情。
2423
+ const [intentId, setIntentId] = useState(workbenchBridge.intent)
2424
+ useEffect(() => workbenchBridge.subscribe((sid) => { setDetail(null); setIntentId(sid) }), [])
2425
+ // ── 官方标题近实时同步(rename 对齐增强)──────────────────────────
2426
+ // 自建 mux 下行流监听 session/projection(title) 帧:官方 rename / 自动标题生成后
2427
+ // host 向每个 mux 消费者即时广播(value 为标题字符串),秒级同步、免等重扫。
2428
+ const [liveTitles, setLiveTitles] = useState({}) // sessionId -> 最新官方标题
2429
+ useEffect(() => {
2430
+ const conn = props.connection
2431
+ if (conn === undefined || conn.api === undefined || conn.api.events === undefined || typeof conn.api.events.mux !== 'function') return undefined
2432
+ const ac = new AbortController()
2433
+ ;(async () => {
2434
+ try {
2435
+ for await (const envelope of conn.api.events.mux({}, ac.signal)) {
2436
+ const f = (envelope && envelope.payload) || envelope
2437
+ if (!f || f.type !== 'session/projection' || f.key !== 'title') continue
2438
+ const title = typeof f.value === 'string' ? f.value : (f.value && typeof f.value.title === 'string' ? f.value.title : '')
2439
+ if (f.sessionId && title !== '') {
2440
+ const sid = f.sessionId
2441
+ setLiveTitles((prev) => (prev[sid] === title ? prev : { ...prev, [sid]: title }))
2442
+ }
2443
+ }
2444
+ } catch (e) { /* 流断开/中止:静默(组件卸载或下次挂载时重建) */ }
2445
+ })()
2446
+ return () => ac.abort()
2447
+ }, [props.connection])
2205
2448
 
2206
2449
  return detail === null
2207
2450
  ? h(SessionFlowOverview, {
@@ -2209,12 +2452,16 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2209
2452
  onOpen: (session, searchQuery) => setDetail({ session, searchQuery }),
2210
2453
  onClose: props.onClose,
2211
2454
  sessions: props.sessions,
2455
+ connection: props.connection,
2456
+ liveTitles,
2457
+ intentId,
2212
2458
  })
2213
2459
  : h(SessionFlowDetail, {
2214
2460
  session: detail.session,
2215
2461
  initialSearchQuery: detail.searchQuery || '',
2216
2462
  sessions: props.sessions,
2217
2463
  connection: props.connection,
2464
+ liveTitles,
2218
2465
  onBack: () => setDetail(null),
2219
2466
  onClose: props.onClose,
2220
2467
  })
@@ -2947,6 +3194,27 @@ window.__ModuleLoader__.load({ id: 'dsh-session-flow', factory: (require) => {
2947
3194
  } catch (error) {
2948
3195
  console.error('[dsh-session-flow] conversation.view registration failed:', error)
2949
3196
  }
3197
+ // 会话健康芯片:官方 conversation.session.header.actions 槽位(ui-jobs/ui-subagent 同款),
3198
+ // order -9 紧随官方模式标识(agent-preset label, order -10)右侧。
3199
+ // 点击芯片 → workbenchBridge 意图桥直达该会话详情。
3200
+ try {
3201
+ slots.inject('conversation.session.header.actions', () => slots.register({
3202
+ name: 'conversation.session.header.actions',
3203
+ id: 'session-flow-health',
3204
+ order: -9,
3205
+ }, (slotProps) => h(SessionHealthChip, {
3206
+ sessionId: slotProps ? slotProps.sessionId : undefined,
3207
+ sessions,
3208
+ connection,
3209
+ onOpen: () => {
3210
+ const sid = slotProps && slotProps.sessionId
3211
+ if (sid) workbenchBridge.open(sid)
3212
+ controller.open()
3213
+ },
3214
+ })))
3215
+ } catch (error) {
3216
+ console.error('[dsh-session-flow] header health chip registration failed:', error)
3217
+ }
2950
3218
  }
2951
3219
 
2952
3220
  // M12:详情并入右侧栏 —— details 槽位动态占用(priority -1 低于官方 tool-details)。
package/lib/host.js CHANGED
@@ -66,6 +66,43 @@ function sendJson(res, status, obj) {
66
66
  res.end(JSON.stringify(obj))
67
67
  }
68
68
 
69
+ // ── 卡死监控(stall-monitor):健康分类 ─────────────────────────────
70
+ // 「进行中但长时间无输出」≠ 卡死(模型长思考/长工具执行都静默)。
71
+ // 分类只看结构事实:运行中 + 无流式 chunk + 无未闭合工具 + 静默超阈值 → 疑似卡死。
72
+ // TODO(设置页待办):STALL_THRESHOLD_MS 待插件设置页落地后可配置化(见 docs HANDOVER §7)。
73
+ export const STALL_THRESHOLD_MS = 3 * 60 * 1000
74
+ export const HEALTH_ACTIVE_WINDOW_MS = 60 * 1000
75
+
76
+ /**
77
+ * 健康分类(纯函数,verify 可测)。
78
+ * @param {{running:boolean,lastEventTime:number|null,openTool:boolean,inflight:boolean}} facts
79
+ * @param {number} now - 调用方时间戳(client 传 Date.now(),避免 host/client 时钟偏差语义混乱)。
80
+ * @returns {{kind:'active'|'tool-wait'|'quiet'|'stalled'|'ended'|'unknown', idleMs:number|null}}
81
+ */
82
+ export function classifyHealth(facts, now) {
83
+ if (!facts || typeof facts !== 'object') return { kind: 'unknown', idleMs: null }
84
+ const idleMs = typeof facts.lastEventTime === 'number' ? Math.max(0, now - facts.lastEventTime) : null
85
+ if (facts.running !== true) return { kind: 'ended', idleMs }
86
+ // 有流式中间态或 60s 内有事件 → 确实在跑。
87
+ if (facts.inflight === true || (idleMs !== null && idleMs < HEALTH_ACTIVE_WINDOW_MS)) return { kind: 'active', idleMs }
88
+ // 有未闭合工具调用 → 静默是工具在执行(长跑命令正常),不算卡死。
89
+ if (facts.openTool === true) return { kind: 'tool-wait', idleMs }
90
+ if (idleMs === null) return { kind: 'unknown', idleMs }
91
+ if (idleMs >= STALL_THRESHOLD_MS) return { kind: 'stalled', idleMs }
92
+ return { kind: 'quiet', idleMs }
93
+ }
94
+
95
+ /**
96
+ * 对齐官方重命名(官方 rename 为 log-backed session/title user 事件,同一数据源)。
97
+ * 显示优先级:档案 user 源标题(官方唯一真源)> renames.json 遗留 overlay(自然淘汰)。
98
+ */
99
+ function effectiveUserTitle(home, entry, sessionId) {
100
+ if (entry && entry.titleSource === 'user' && typeof entry.title === 'string' && entry.title !== '') {
101
+ return entry.title
102
+ }
103
+ return userTitleOf(home, sessionId)
104
+ }
105
+
69
106
  /** 汇总输出视图(去掉 counts 细节,浏览器更轻)。 */
70
107
  function sessionView(summary) {
71
108
  if (!summary) return summary
@@ -629,7 +666,7 @@ export function apply(ctx) {
629
666
  if (requested !== null && ws.name !== requested) continue
630
667
  const result = scanWorkspaceIndex(home, ws.name, { force })
631
668
  const sessions = Object.values(result.index.sessions)
632
- .map((s) => ({ ...sessionView(s), userTitle: userTitleOf(home, s.id) }))
669
+ .map((s) => ({ ...sessionView(s), userTitle: effectiveUserTitle(home, s, s.id) }))
633
670
  .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
634
671
  const meta = workspaceLabelOf(ws.name, sessions)
635
672
  workspaces.push({
@@ -717,7 +754,7 @@ export function apply(ctx) {
717
754
  workspaceLabel: wsMeta.label,
718
755
  workspaceCwd: wsMeta.cwd,
719
756
  session: sessionView(sum),
720
- userTitle: userTitleOf(home, sessionId),
757
+ userTitle: effectiveUserTitle(home, sum, sessionId),
721
758
  counts,
722
759
  lightTurns: entry.lightTurns,
723
760
  toolStats: entry.toolStats,
@@ -874,8 +911,11 @@ export function apply(ctx) {
874
911
  const chunks = chunkTurns(entry.turns, EXPORT_CHUNK_TARGET)
875
912
  // 概览(含分卷指引)。
876
913
  const overview = renderOverviewMd({ entry, sum, found, title, llmText, chunkCount: chunks.length })
877
- // 文件名用自定义标题优先(M8a:导出在 host 端合并,前端无法覆盖)。
878
- const rawTitle = userTitleOf(home, sessionId) || title || sessionId
914
+ // 文件名用自定义标题优先(档案 user 源标题 = 官方真源优先;renames.json 遗留回退)。
915
+ const parsedTitle = entry.parsed.title
916
+ const archiveUserTitle = parsedTitle && parsedTitle.source && parsedTitle.source.kind === 'user'
917
+ ? parsedTitle.title : null
918
+ const rawTitle = archiveUserTitle || userTitleOf(home, sessionId) || title || sessionId
879
919
  const safeTitle = String(rawTitle).replace(/[\\/:*?"<>|\r\n]/g, '_').slice(0, 60)
880
920
  // 组装 ZIP 文件清单:概览 + 时间线分卷(00-概览.md / 01-时间线-回合X-Y.md …)。
881
921
  const zipFiles = [{ name: '00-概览.md', data: Buffer.from(overview, 'utf8') }]
@@ -934,7 +974,7 @@ export function apply(ctx) {
934
974
  const buildNode = (s) => ({
935
975
  id: s.id,
936
976
  title: s.title || null,
937
- userTitle: userTitleOf(home, s.id),
977
+ userTitle: effectiveUserTitle(home, s, s.id),
938
978
  delegationDepth: s.delegationDepth || 0,
939
979
  createdAt: s.createdAt || null,
940
980
  lastEventTime: s.lastEventTime || null,
@@ -982,12 +1022,25 @@ export function apply(ctx) {
982
1022
  }
983
1023
  const STREAM_MID_TYPES = new Set(['assistant/chunk', 'assistant/message', 'tool/call', 'step/start', 'turn/start', 'user/message', 'request/header'])
984
1024
  const running = openTurns > 0 || openSteps > 0 || openTools > 0 || STREAM_MID_TYPES.has(lastType)
1025
+ // 卡死监控:健康事实 + 分类。assumeRunning:总览探测以官方 sessions.list 的
1026
+ // running 为准(tail 窗口可能不含 turn/start,结构信号会漏判),此时结构信号只供
1027
+ // openTool/inflight 事实;now 由 client 传入(缺省 host 本地时间)。
1028
+ const now = typeof body.now === 'number' ? body.now : Date.now()
1029
+ const effectiveRunning = running || body.assumeRunning === true
1030
+ const healthFacts = {
1031
+ running: effectiveRunning,
1032
+ lastEventTime: summary.lastEventTime !== undefined ? summary.lastEventTime : null,
1033
+ lastEventType: lastType,
1034
+ openTool: openTools > 0,
1035
+ inflight: lastType === 'assistant/chunk',
1036
+ }
985
1037
  return sendJson(res, 200, {
986
1038
  ok: true,
987
1039
  session: sessionView(summary),
988
1040
  counts,
989
1041
  timeline,
990
1042
  running,
1043
+ health: { ...healthFacts, ...classifyHealth(healthFacts, now) },
991
1044
  })
992
1045
  }
993
1046
 
@@ -1149,18 +1202,19 @@ export function apply(ctx) {
1149
1202
  if (matches.length === 0) continue
1150
1203
  // 标题:索引优先({title, source} 对象取 .title),无索引时用会话 id。
1151
1204
  let title = c.id
1205
+ let entry = null
1152
1206
  try {
1153
1207
  const index = readIndex(home, c.workspace)
1154
- const ent = index.sessions && index.sessions[c.id]
1155
- if (ent && ent.title) {
1156
- title = typeof ent.title === 'object' ? String(ent.title.title || c.id) : String(ent.title)
1208
+ entry = (index.sessions && index.sessions[c.id]) || null
1209
+ if (entry && entry.title) {
1210
+ title = typeof entry.title === 'object' ? String(entry.title.title || c.id) : String(entry.title)
1157
1211
  }
1158
1212
  } catch {}
1159
1213
  results.push({
1160
1214
  sessionId: c.id,
1161
1215
  workspace: c.workspace,
1162
1216
  title,
1163
- userTitle: userTitleOf(home, c.id),
1217
+ userTitle: effectiveUserTitle(home, entry, c.id),
1164
1218
  matchCount: matches.length,
1165
1219
  matches: matches.slice(0, SEARCH_MATCHES_PER_SESSION),
1166
1220
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-session-flow",
3
- "description": "DSH 会话回顾与归档插件:可折叠信息流 + 会话级汇总——总览工作台、折叠时间线、血缘树、双模式摘要、全文检索、ZIP 导出、实时跟踪、会话重命名",
4
- "version": "1.0.0",
3
+ "description": "DSH 会话回顾与归档插件:可折叠信息流 + 会话级汇总——总览工作台、折叠时间线、血缘树、双模式摘要、全文检索、ZIP 导出、实时跟踪、会话重命名(官方数据源对齐)、健康监控",
4
+ "version": "1.1.0",
5
5
  "type": "module",
6
6
  "main": "lib/host.js",
7
7
  "exports": {