@a9i5k4/dsh-auto-memory 0.1.22 → 0.1.24

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/lib/client.js CHANGED
@@ -18,6 +18,7 @@ window.__ModuleLoader__.load({
18
18
  var useState = React.useState
19
19
  var useEffect = React.useEffect
20
20
  var useReducer = React.useReducer
21
+ var useRef = React.useRef
21
22
 
22
23
  // ───────────────────────── 控制器 ─────────────────────────
23
24
  var listeners = new Set()
@@ -115,7 +116,7 @@ window.__ModuleLoader__.load({
115
116
  kvAvail: '可用', kvUnavail: '不可用', kvDup: '项目笔记重复标题', kvDupCount: ' 个',
116
117
  kvMem: '记忆文件', kvMemUser: '用户', kvMemNotes: '笔记', kvMemLog: '日志', kvMemMissing: '缺失',
117
118
  kvWs: '当前工作区', kvWsUnknown: '(未取到)', kvApi: 'API 连通', unknown: '(未知)',
118
- guideTitle: '欢迎使用 dsh-auto-memory(开发版)', guideSub: '这个插件能干什么:',
119
+ guideTitle: '欢迎使用 dsh-auto-memory', guideSub: '这个插件能干什么:',
119
120
  gFeat1: '三层记忆自动注入与检索:用户级 / 项目笔记 / 每日日志,每轮对话自动读、自动写',
120
121
  gFeat2: '每轮对话结束自动沉淀:修了什么 bug、做了哪些决策,自动按主题写进今日日志',
121
122
  gFeat3: 'AI 时段问候与三级抽屉:打开「记忆」面板,AI 按时段向你汇报当天进展',
@@ -143,6 +144,7 @@ window.__ModuleLoader__.load({
143
144
  readFailed: '读取失败: ', ok: '正常', refreshTime: '刷新时间', notYetShort: '尚未',
144
145
  empty: '(空)', back: '← 返回', view: '查看',
145
146
  clickDateViewLog: '点击日期查看当日日志(append-only):',
147
+ userMemoryBlock: '用户记忆(用户画像 · 跨项目):', userMemoryEmpty: '(空)', userMemoryTruncated: '(内容过长,已截断显示)',
146
148
  appended: '已追加', notesPathLabel: '项目笔记: ',
147
149
  notesPlaceholder: '想追加到项目笔记的内容…(保存时自动带日期标题)',
148
150
  saving: '保存中…', append: '追加',
@@ -170,8 +172,13 @@ window.__ModuleLoader__.load({
170
172
  fProjectDir: '项目记忆目录', fProjectDirHint: '相对各工作区的目录名(默认 .dsh-memory)。',
171
173
  fInject: '注入记忆上下文', fInjectHint: '每次组装提示词时自动注入 <memory_system> 块。',
172
174
  fBudget: '注入预算(字符)', fBudgetHint: '记忆块总预算,超出部分截断。',
173
- fDays: '注入最近日志天数', fDaysHint: '会话开始时注入最近 N 天的工作日志尾部。',
175
+ fDays: '注入最近日志天数', fDaysHint: '会话开始时注入最近 N 天的工作日志尾部。默认 1。',
176
+ fExtBudget: '外部记忆注入预算(字符)', fExtBudgetHint: '外部记忆来源在上下文中的注入预算。默认 1400(路径模式下影响有限)。',
177
+ fConsolidateMin: '自动沉淀内容门槛(字符)', fConsolidateMinHint: '本轮 user+assistant 总字符低于此值视为寒暄跳过。默认 240。',
174
178
  fAway: '暂离阈值(分钟)', fAwayHint: '距上次活动超过该值视为暂离,回归时自动弹出记忆窗口。默认 60。',
179
+ fAutoConsolidate: '自动沉淀(每轮对话结束AI评估)', fAutoConsolidateHint: '关闭后每轮对话结束不再自动调用 AI 评估与写入今日日志。',
180
+ fConsolidate: '自动沉淀间隔(分钟)', fConsolidateHint: '两轮自动沉淀之间的最短间隔。默认 30;非工作时间(22:00-08:00)自动翻倍,避免短时间耗尽每日额度。',
181
+ fConsolidateMax: '自动沉淀每日额度(次)', fConsolidateMaxHint: '每天最多触发自动沉淀的次数,到点后当天不再调用。默认 8。',
175
182
  fAutoSum: '自动总结时间点(HH:MM,逗号分隔)', fAutoSumHint: '到点自动生成本时段总结并弹窗展示,如 12:00,18:00,22:00。空=关闭。',
176
183
  awayTitle: '欢迎回来', awayMsg: '你离开的这段时间,我已经帮你把日志整理好了。记忆窗口已打开,可以看看这段时间的状况。',
177
184
  sumTitle: '时段总结',
@@ -209,7 +216,7 @@ window.__ModuleLoader__.load({
209
216
  kvAvail: 'available', kvUnavail: 'unavailable', kvDup: 'Duplicate note headings', kvDupCount: '',
210
217
  kvMem: 'Memory files', kvMemUser: 'user', kvMemNotes: 'notes', kvMemLog: 'log', kvMemMissing: 'missing',
211
218
  kvWs: 'Current workspace', kvWsUnknown: '(unknown)', kvApi: 'API probes', unknown: '(unknown)',
212
- guideTitle: 'Welcome to dsh-auto-memory (dev)', guideSub: 'What this plugin does:',
219
+ guideTitle: 'Welcome to dsh-auto-memory', guideSub: 'What this plugin does:',
213
220
  gFeat1: 'Three-layer memory with automatic injection & retrieval: user / project notes / daily logs',
214
221
  gFeat2: 'Auto-consolidation after every turn: bugs fixed and decisions made are written into today\'s log automatically',
215
222
  gFeat3: 'AI period greetings with three-level drawers: open the Memory panel for a period-by-period digest',
@@ -237,6 +244,7 @@ window.__ModuleLoader__.load({
237
244
  readFailed: 'Read failed: ', ok: 'OK', refreshTime: 'Refreshed', notYetShort: 'never',
238
245
  empty: '(empty)', back: '← Back', view: 'View',
239
246
  clickDateViewLog: 'Click a date to view the daily log (append-only):',
247
+ userMemoryBlock: 'User memory (profile · cross-project):', userMemoryEmpty: '(empty)', userMemoryTruncated: '(content truncated for display)',
240
248
  appended: 'Appended', notesPathLabel: 'Project notes: ',
241
249
  notesPlaceholder: 'Content to append to project notes…(date heading added on save)',
242
250
  saving: 'Saving…', append: 'Append',
@@ -264,8 +272,13 @@ window.__ModuleLoader__.load({
264
272
  fProjectDir: 'Project memory dir', fProjectDirHint: 'Directory name relative to each workspace (default .dsh-memory).',
265
273
  fInject: 'Inject memory context', fInjectHint: 'Auto-inject <memory_system> block into every prompt.',
266
274
  fBudget: 'Injection budget (chars)', fBudgetHint: 'Total budget for the memory block; excess is truncated.',
267
- fDays: 'Recent days injected', fDaysHint: 'Inject tails of the last N days of work logs at session start.',
275
+ fDays: 'Recent days injected', fDaysHint: 'Inject tails of the last N days of work logs at session start. Default 1.',
276
+ fExtBudget: 'External memory injection budget (chars)', fExtBudgetHint: 'Budget for external memory sources in context. Default 1400 (limited effect with path mode).',
277
+ fConsolidateMin: 'Auto-consolidation content threshold (chars)', fConsolidateMinHint: 'Turns with fewer combined user+assistant chars are treated as chit-chat and skipped. Default 240.',
268
278
  fAway: 'Away threshold (minutes)', fAwayHint: 'Marked away when inactive longer than this; the memory panel auto-opens on return. Default 60.',
279
+ fAutoConsolidate: 'Auto-consolidation (AI review each turn end)', fAutoConsolidateHint: 'When off, no automatic AI review or daily-log writes at turn end.',
280
+ fConsolidate: 'Auto-consolidate interval (minutes)', fConsolidateHint: 'Minimum gap between auto-consolidations. Default 30; doubled automatically outside work hours (22:00-08:00).',
281
+ fConsolidateMax: 'Daily auto-consolidate quota', fConsolidateMaxHint: 'Max auto-consolidation runs per day; no more runs after the quota is reached. Default 8.',
269
282
  fAutoSum: 'Auto summary times (HH:MM, comma-separated)', fAutoSumHint: 'Auto-generate and show a period summary at these times, e.g. 12:00,18:00,22:00. Empty = off.',
270
283
  awayTitle: 'Welcome back', awayMsg: 'While you were away, I tidied up your logs. The memory panel is open — take a look at what happened.',
271
284
  sumTitle: 'Period summary',
@@ -308,6 +321,15 @@ window.__ModuleLoader__.load({
308
321
  var fontScale = 'lg'
309
322
  var FONT_SCALES = { sm: '小', md: '标准', lg: '大', xl: '特大' }
310
323
  var FONT_SCALE_VALUES = { sm: '0.9', md: '1', lg: '1.15', xl: '1.3' }
324
+ var accentTheme = 'deepseek'
325
+ var graphDensity = 'relaxed'
326
+ var ACCENT_VALUES = { deepseek: '#1d4ed8', graphite: '#8b949e', violet: '#9b8cff' }
327
+ try {
328
+ var savedAccent = localStorage.getItem('dsh-auto-memory.accentTheme.v1')
329
+ var savedDensity = localStorage.getItem('dsh-auto-memory.graphDensity.v1')
330
+ if (ACCENT_VALUES[savedAccent]) accentTheme = savedAccent
331
+ if (savedDensity === 'relaxed' || savedDensity === 'compact') graphDensity = savedDensity
332
+ } catch (e) {}
311
333
  var localeListeners = new Set()
312
334
  function t(key) { return (I18N[locale] && I18N[locale][key]) || I18N.zh[key] || key }
313
335
  function setLocale(l) { if (l !== 'zh' && l !== 'en') return; if (l === locale) return; locale = l; localeListeners.forEach(function (fn) { try { fn(l) } catch (e) {} }) }
@@ -324,6 +346,24 @@ window.__ModuleLoader__.load({
324
346
 
325
347
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
326
348
  var CHANGELOG = {
349
+ '0.1.24': { zh: [
350
+ '记忆面板:全新液态玻璃 UI——模块箭头滚动、抽屉丝滑展开/收起、工作区 AI 思维导图(拖动画布+缩放)、日历当天时间轴(07:00-22:00)+地点/提醒字段。',
351
+ '注入精简:只注入最近 1 天日志与反思精华,外部记忆改为绝对路径按需读取,新增 memory_read 工具;敏感段落(令牌/密钥)不再注入 prompt。',
352
+ '接续页:按来源查看内容/接入/移除(笔记与用户级独立),已接入来源显示 ✓;不再整段注入外部记忆。',
353
+ '设置页:全部参数可调(注入预算/天数/外部预算、自动沉淀开关/门槛/间隔/额度),浮动保存栏未保存时高亮。',
354
+ '稳定性:修复对话结束时 host 卡死/崩溃(惰性投影移除、超时兜底、延迟启动),自动沉淀恢复正常并每日限频。',
355
+ ], en: [
356
+ 'Memory panel: brand-new liquid-glass UI — arrow tab scrolling, smooth drawer transitions, AI workspace mind map (pan + zoom), day timeline (07:00-22:00) with location/reminder fields.',
357
+ 'Lean injection: only the last day of logs and a reflection digest; external memory is read by absolute path on demand via the new memory_read tool; credentials/tokens are filtered out of the prompt.',
358
+ 'Connect tab: per-source view/import/remove (notes vs user-level independently), imported sources show ✓; external memory is no longer injected in bulk.',
359
+ 'Settings: every parameter is now adjustable (injection budget/days/external budget, auto-consolidation toggle/threshold/interval/quota) with a floating save bar that highlights unsaved changes.',
360
+ 'Stability: fixed host freezes/crashes at turn end (lazy projection removed, timeout fallbacks, delayed start); auto-consolidation recovered with daily rate limiting.',
361
+ ] },
362
+ '0.1.23': { zh: [
363
+ '修复:正式发布包首次欢迎文案仍显示“开发版”;发布转换与残留校验已加强,确保开发版和正式版身份完全隔离。',
364
+ ], en: [
365
+ 'Fix: the published first-run guide still showed a dev label; release conversion and residual checks now enforce complete dev/release identity isolation.',
366
+ ] },
327
367
  '0.1.22': { zh: [
328
368
  '修复:更新说明可能被公告、欢迎或自动总结弹窗覆盖,导致升级后没有看到 changelog。',
329
369
  '优化:更新说明现在优先展示并排队其他弹窗,只有点击“知道了”后才标记为已读;未确认时重启仍会再次显示。',
@@ -463,7 +503,10 @@ window.__ModuleLoader__.load({
463
503
  reflectAuto: '/api/dsh-auto-memory/reflect-auto',
464
504
  note: '/api/dsh-auto-memory/note',
465
505
  external: '/api/dsh-auto-memory/external',
506
+ externalView: '/api/dsh-auto-memory/external-view',
507
+ externalRemove: '/api/dsh-auto-memory/external-remove',
466
508
  externalImport: '/api/dsh-auto-memory/external-import',
509
+ calendar: '/api/dsh-auto-memory/calendar',
467
510
  summarize: '/api/dsh-auto-memory/summarize',
468
511
  greet: '/api/dsh-auto-memory/greet',
469
512
  notices: '/api/dsh-auto-memory/notices',
@@ -477,13 +520,13 @@ window.__ModuleLoader__.load({
477
520
  async function apiGet(path, params) {
478
521
  var res = await fetch(path + query(params))
479
522
  var body = await res.json().catch(function () { return {} })
480
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status))
523
+ if (!res.ok) throw new Error(body.error || ('GET ' + path + ' → HTTP ' + res.status))
481
524
  return body
482
525
  }
483
526
  async function apiPost(path, payload) {
484
527
  var res = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
485
528
  var body = await res.json().catch(function () { return {} })
486
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status))
529
+ if (!res.ok) throw new Error(body.error || ('POST ' + path + ' → HTTP ' + res.status))
487
530
  return body
488
531
  }
489
532
 
@@ -521,16 +564,61 @@ window.__ModuleLoader__.load({
521
564
  '[data-dam-resize]:hover { opacity: 1; }',
522
565
  '[data-dam-btn] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .75; font-size: calc(13px * var(--dam-scale)); padding: 4px 8px; border-radius: 6px; }',
523
566
  '[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
524
- '[data-dam-tabs] { display: flex; gap: 2px; padding: 6px 10px; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent); }',
525
- '[data-dam-tab] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .6; padding: 5px 10px; border-radius: 7px; font-size: calc(12.5px * var(--dam-scale)); }',
526
- '[data-dam-tab][data-active="true"] { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 18%, transparent); font-weight: 600; }',
527
- '[data-dam-body] { flex: 1; overflow: auto; padding: 12px 14px; }',
567
+ '[data-dam-tabs-wrap] { display: flex; align-items: center; min-width: 0; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent); }',
568
+ '[data-dam-tabs] { flex: 1; min-width: 0; overflow: hidden; padding: 5px 4px; scrollbar-width: none; -ms-overflow-style: none; }',
569
+ '[data-dam-tabs]::-webkit-scrollbar { display: none; }',
570
+ '[data-dam-tab-strip] { display: flex; width: max-content; transition: transform .42s cubic-bezier(.22,.8,.2,1); will-change: transform; }',
571
+ '[data-dam-tab] { flex: 0 0 auto; min-width: max-content; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border: none; background: transparent; cursor: pointer; color: inherit; opacity: .68; padding: 6px 9px; border-radius: 7px; font-size: calc(12.5px * var(--dam-scale)); transition: color .25s ease, background .25s ease, opacity .25s ease, transform .25s ease; }',
572
+ '[data-dam-tabs-arrow] { flex: 0 0 26px; height: 30px; border: 0; background: transparent; color: inherit; cursor: pointer; opacity: .55; font-size: 18px; }',
573
+ '[data-dam-tabs-arrow]:hover:not(:disabled) { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #2456c4) 16%, transparent); transform: scale(1.12); }',
574
+ '[data-dam-tabs-arrow]:active:not(:disabled) { transform: scale(.92); }',
575
+ '[data-dam-tab][data-active="true"] { position: relative; opacity: 1; background: var(--dam-accent, #2456c4); color: #fff; font-weight: 700; box-shadow: 0 3px 10px color-mix(in srgb, var(--dam-accent, #2456c4) 30%, transparent); transform: translateY(-1px); }',
576
+ '[data-dam-tab][data-active="true"]::before { content: ""; position: absolute; left: 3px; top: 7px; bottom: 7px; width: 3px; border-radius: 3px; background: rgba(255,255,255,.9); }',
577
+ '[data-dam-body] { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: auto; padding: 16px; }',
578
+ '[data-dam-settings] { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 18px; min-height: 100%; }',
579
+ '[data-dam-settings-nav] { position: sticky; top: 0; align-self: start; display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }',
580
+ '[data-dam-settings-nav] button { border: 0; background: transparent; color: inherit; text-align: left; padding: 8px 9px; border-radius: 8px; cursor: pointer; opacity: .58; font: inherit; font-size: calc(11.5px * var(--dam-scale)); }',
581
+ '[data-dam-settings-nav] button[data-active="true"] { opacity: 1; color: var(--dam-accent, var(--dsw-alias-brand-primary, #4f7cff)); background: color-mix(in srgb, var(--dam-accent, #4f7cff) 11%, transparent); font-weight: 650; }',
582
+ '[data-dam-settings-content] { min-width: 0; padding-bottom: 24px; }',
583
+ '[data-dam-settings-group] { scroll-margin-top: 12px; padding-bottom: 22px; margin-bottom: 24px; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, #c5cbd3) 38%, transparent); }',
584
+ '[data-dam-settings-group] h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2328); font-size: calc(14px * var(--dam-scale)); }',
585
+ '[data-dam-settings-group] p { margin: 4px 0 14px; color: var(--dsw-alias-label-secondary, #68717d); font-size: calc(11.5px * var(--dam-scale)); line-height: 1.5; }',
586
+ '[data-dam-settings-row] { padding: 10px 0; }',
587
+ '[data-dam-savebar] { position: sticky; bottom: 0; z-index: 3; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 10px 14px; margin: 6px -16px -16px; background: color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.92)) 74%, transparent); backdrop-filter: blur(16px) saturate(1.4); -webkit-backdrop-filter: blur(16px) saturate(1.4); border-top: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, #c5cbd3) 38%, transparent); border-radius: 0 0 14px 14px; transition: background .25s ease; }',
588
+ '[data-dam-savebar] button { opacity: .5; transition: opacity .2s ease, background .2s ease, color .2s ease; }',
589
+ '[data-dam-savebar] button[data-dirty="true"] { opacity: 1; background: var(--dam-accent, #1d4ed8); color: #fff; font-weight: 650; box-shadow: 0 3px 12px color-mix(in srgb, var(--dam-accent, #1d4ed8) 35%, transparent); }',
590
+ '[data-dam-settings-row] > [data-dam-row] { margin-bottom: 3px; }',
591
+ '[data-dam-graph-toolbar] { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, #c5cbd3) 38%, transparent); }',
592
+ '[data-dam-graph-toolbar] input[type="range"] { flex: 1; min-width: 90px; accent-color: var(--dam-accent, #6b98ff); }',
593
+ '[data-dam-graph] { position: relative; min-height: 420px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, #c5cbd3) 42%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, #f5f6f7) 42%, transparent); }',
594
+ '[data-dam-graph] { cursor: grab; }',
595
+ '[data-dam-graph][data-panning="true"] { cursor: grabbing; user-select: none; }',
596
+ '[data-dam-graph] svg { display: block; width: auto; max-width: none; height: auto; min-height: 520px; transform-origin: 0 0; }',
597
+ '[data-dam-graph-node] { cursor: pointer; }',
598
+ '[data-dam-graph-legend] { display: flex; gap: 12px; flex-wrap: wrap; margin: 10px 0 14px; color: var(--dsw-alias-label-secondary, #68717d); font-size: calc(11px * var(--dam-scale)); }',
599
+ '[data-dam-legend-dot] { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 4px; background: var(--dam-accent, #4f7cff); }',
600
+ '[data-dam-legend-dot="branch"] { background: #7d8793; }',
601
+ '[data-dam-legend-dot="leaf"] { background: #b0b7c0; }',
528
602
  '[data-dam-kv] { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: calc(12.5px * var(--dam-scale)); }',
529
603
  '[data-dam-kv] b { opacity: .55; font-weight: 500; }',
530
604
  '[data-dam-card] { border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.22)) 60%, transparent); border-radius: 9px; padding: 8px 10px; margin-bottom: 8px; font-size: calc(12.5px * var(--dam-scale));',
531
605
  ' background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent); }',
532
606
  '[data-dam-card] .dam-date { font-weight: 600; margin-bottom: 4px; }',
533
607
  '[data-dam-card] .dam-content { white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow: auto; }',
608
+ '[data-dam-disclosure] { overflow: hidden; opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); transition: max-height .32s cubic-bezier(.22,.8,.2,1), opacity .2s ease, transform .28s cubic-bezier(.22,.8,.2,1); }',
609
+ '[data-dam-disclosure][data-phase="open"] { opacity: 1; max-height: 1800px; transform: translateY(0) scale(1); }',
610
+ '[data-dam-disclosure][data-phase="closing"] { opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); }',
611
+ '[data-dam-card], [data-dam-banner] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
612
+ '@keyframes dam-rise { from { opacity: 0; transform: translateY(7px) scale(.988); } to { opacity: 1; transform: translateY(0) scale(1); } }',
613
+ '@media (prefers-reduced-motion: reduce) { [data-dam-tab-strip], [data-dam-tab], [data-dam-disclosure], [data-dam-card], [data-dam-banner] { transition: none !important; animation: none !important; } }',
614
+ '[data-dam-calendar] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
615
+ '[data-dam-calendar] [data-dam-calendar-day] { transition: transform .18s ease, border-color .2s ease, background .2s ease, box-shadow .2s ease; }',
616
+ '[data-dam-calendar] [data-dam-calendar-day]:hover { transform: translateY(-1px); border-color: var(--dam-accent, #1d4ed8) !important; box-shadow: 0 4px 12px rgba(0,0,0,.08); }',
617
+ '[data-dam-calendar-event] { animation: dam-event-in .25s cubic-bezier(.22,.8,.2,1) both; transition: transform .18s ease, filter .18s ease; }',
618
+ '[data-dam-calendar-event]:hover { transform: translateX(2px); filter: brightness(1.06); }',
619
+ '[data-dam-calendar-modal] { animation: dam-modal-in .26s cubic-bezier(.22,.8,.2,1) both; }',
620
+ '@keyframes dam-event-in { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: none; } }',
621
+ '@keyframes dam-modal-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }',
534
622
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
535
623
  ' background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 13%, transparent); border-radius: 9px; padding: 8px 10px; margin-bottom: 10px; font-size: calc(12.5px * var(--dam-scale)); }',
536
624
  '[data-dam-input], [data-dam-select] { width: 100%; box-sizing: border-box; background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08)) 45%, transparent); color: inherit; border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 60%, transparent); border-radius: 7px; padding: 6px 8px; font: inherit; }',
@@ -542,6 +630,9 @@ window.__ModuleLoader__.load({
542
630
  '[data-dam-sidebar-btn][data-active="true"] { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f7cff) 16%, transparent); color: var(--dsw-alias-brand-primary, #4f7cff); }',
543
631
  '[data-dam-error] { color: var(--dsw-alias-state-error-primary, #d64545); font-size: 12px; margin-top: 6px; white-space: pre-wrap; }',
544
632
  '[data-dam-muted] { opacity: .55; }',
633
+ '[data-dam-loading] { display: flex; align-items: center; justify-content: center; gap: 9px; min-height: 96px; color: var(--dsw-alias-label-secondary, #7d8793); font-size: calc(12px * var(--dam-scale)); }',
634
+ '[data-dam-spinner] { width: 18px; height: 18px; border: 2px solid color-mix(in srgb, var(--dam-accent, #4f7cff) 22%, transparent); border-top-color: var(--dam-accent, #4f7cff); border-radius: 50%; animation: dam-spin .8s linear infinite; }',
635
+ '@keyframes dam-spin { to { transform: rotate(360deg); } }',
545
636
  '@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {',
546
637
  ' [data-dam-panel] { background: var(--dsw-alias-bg-overlay, #ffffff); } }',
547
638
  ].join('\n')
@@ -568,7 +659,32 @@ window.__ModuleLoader__.load({
568
659
  h('div', { className: 'dam-content' }, props.children))
569
660
  }
570
661
 
571
- function Loading() { return h('div', { 'data-dam-muted': '' }, t('loading')) }
662
+ function Loading(props) {
663
+ var label = props && props.label ? props.label : t('loading')
664
+ return h('div', { 'data-dam-loading': '' }, h('span', { 'data-dam-spinner': '', 'aria-hidden': 'true' }), h('span', null, label))
665
+ }
666
+ function AnimatedDisclosure(props) {
667
+ var open = !!props.open
668
+ var shownPair = useState(open)
669
+ var shown = shownPair[0]
670
+ var setShown = shownPair[1]
671
+ var phasePair = useState(open ? 'open' : 'closed')
672
+ var phase = phasePair[0]
673
+ var setPhase = phasePair[1]
674
+ useEffect(function () {
675
+ var timer
676
+ if (open) {
677
+ setShown(true)
678
+ timer = setTimeout(function () { setPhase('open') }, 16)
679
+ } else if (shown) {
680
+ setPhase('closing')
681
+ timer = setTimeout(function () { setShown(false); setPhase('closed') }, 260)
682
+ }
683
+ return function () { if (timer) clearTimeout(timer) }
684
+ }, [open])
685
+ if (!shown) return null
686
+ return h('div', { 'data-dam-disclosure': '', 'data-phase': phase }, props.children)
687
+ }
572
688
 
573
689
  // ───────────────────────── 侧边栏入口 ─────────────────────────
574
690
  function SidebarButton() {
@@ -679,7 +795,7 @@ window.__ModuleLoader__.load({
679
795
  h('div', { style: { fontWeight: 600, fontSize: 'calc(12.5px * var(--dam-scale))', lineHeight: 1.5 } }, (isOpen ? '▼ ' : '▶ ') + bigTitle),
680
796
  h('div', { 'data-dam-muted': '', style: { fontSize: 'calc(11px * var(--dam-scale))', marginTop: '2px' } },
681
797
  dr.label + ' · ' + dr.items.length + t('logEntries') + (cached && cached.at ? ' · ' + t('generatedAt') + new Date(cached.at).toLocaleTimeString() : ''))),
682
- isOpen ? h('div', { style: { padding: '4px 2px 2px 8px' } },
798
+ h(AnimatedDisclosure, { open: isOpen }, h('div', { style: { padding: '4px 2px 2px 8px' } },
683
799
  cached && cached.works && cached.works.length ? cached.works.map(function (w, wi) {
684
800
  var sk = dr.key + ':w' + wi
685
801
  var sOpen = subOpenMap[sk] !== undefined ? subOpenMap[sk] : false
@@ -697,7 +813,7 @@ window.__ModuleLoader__.load({
697
813
  })) : null)
698
814
  }) : cached && cached.failed ? h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summaryFailed'))
699
815
  : cached && cached.summary ? h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, cached.summary)
700
- : h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summarizing'))) : null))
816
+ : h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summarizing'))))))
701
817
  })(drawers[d])
702
818
  }
703
819
  // 待反思提醒
@@ -723,6 +839,9 @@ window.__ModuleLoader__.load({
723
839
  var wsPair = useState(null)
724
840
  var wsData = wsPair[0]
725
841
  var setWsData = wsPair[1]
842
+ var wsOpenPair = useState(false)
843
+ var wsOpen = wsOpenPair[0]
844
+ var setWsOpen = wsOpenPair[1]
726
845
  // 每次进入面板 / 点刷新(nonce 变化)/ 面板打开期间每 30s 自动重拉,保证自动沉淀等数据新鲜
727
846
  useEffect(function () {
728
847
  var alive = true
@@ -763,19 +882,12 @@ window.__ModuleLoader__.load({
763
882
  return h('div', null,
764
883
  // 今日问候卡:问候语 + 昨天时间轴 + 提醒(纯 GUI 渲染,不干扰对话流)
765
884
  h(GreetingCard, { greeting: state.greeting, periodSummary: state.periodSummary, t: t, nonce: props && props.nonce }),
766
- // 跨工作区总结(所有工作区的小总结 + 区内细分)
767
- h('div', { style: { marginTop: '10px' } },
768
- h('div', { style: { fontWeight: 700, fontSize: 'calc(12.5px * var(--dam-scale))', opacity: 0.75, marginBottom: '6px' } }, t('wsOverview')),
769
- wsData && wsData.workspaces && wsData.workspaces.length
770
- ? wsData.workspaces.map(function (ws) {
771
- return h('div', { key: ws.path, style: { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.22)) 60%, transparent)', borderRadius: '8px', padding: '7px 9px', marginBottom: '6px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.05)) 40%, transparent)' } },
772
- h('div', { style: { fontWeight: 600, fontSize: 'calc(12px * var(--dam-scale))' } }, ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '')),
773
- ws.summary ? h('div', { style: { fontSize: 'calc(12px * var(--dam-scale))', lineHeight: 1.5, marginTop: '3px' } }, ws.summary) : h('div', { 'data-dam-hint': '' }, t('wsNoSummary')),
774
- (ws.items && ws.items.length) ? ws.items.map(function (it, i) {
775
- return h('div', { key: i, style: { fontSize: 'calc(11.5px * var(--dam-scale))', marginTop: '2px', opacity: 0.75 } }, '· ' + it)
776
- }) : null)
777
- })
778
- : h('div', { 'data-dam-hint': '' }, t('wsGenerating'))),
885
+ // 跨工作区总结默认折叠,首页把空间留给问候与今日状态
886
+ h('div', { 'data-dam-collapsible': '', style: { marginTop: '16px' } },
887
+ h('button', { 'data-dam-btn': '', onClick: function () { setWsOpen(!wsOpen) }, style: { width: '100%', textAlign: 'left', padding: '11px 12px', fontWeight: 650, borderBottom: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, #c5cbd3) 38%, transparent)' } }, (wsOpen ? '▾ ' : '') + t('wsOverview')),
888
+ h(AnimatedDisclosure, { open: wsOpen }, wsData && wsData.workspaces && wsData.workspaces.length
889
+ ? h('div', { style: { paddingTop: '10px' } }, h(WorkspaceGraph, { workspaces: wsData.workspaces, graph: wsData.graph, onSelect: function () {} }))
890
+ : h(Loading, { label: locale === 'zh' ? '正在生成跨工作区总结…' : 'Generating workspace overview…' }))),
779
891
  state.pendingReflection
780
892
  ? h(Banner, null, t('pendingReflection') + state.pendingReflection + t('pendingReflectionHint'))
781
893
  : null,
@@ -817,9 +929,16 @@ window.__ModuleLoader__.load({
817
929
  var contentPair = useState('')
818
930
  var content = contentPair[0]
819
931
  var setContent = contentPair[1]
932
+ var userPair = useState('')
933
+ var userText = userPair[0]
934
+ var setUserText = userPair[1]
935
+ var userTruncPair = useState(false)
936
+ var userTruncated = userTruncPair[0]
937
+ var setUserTruncated = userTruncPair[1]
820
938
  useEffect(function () {
821
939
  var alive = true
822
940
  apiGet(API.list).then(function (d) { if (alive) setData(d) }).catch(function () {})
941
+ apiGet(API.state).then(function (s) { if (alive && s) { if (typeof s.userText === 'string') setUserText(s.userText); setUserTruncated(!!s.userTextTruncated) } }).catch(function () {})
823
942
  return function () { alive = false }
824
943
  }, [])
825
944
  useEffect(function () {
@@ -842,6 +961,11 @@ window.__ModuleLoader__.load({
842
961
  h('button', { 'data-dam-btn': '', onClick: function () { setOpen(log.date + '.md') } }, t('view'))))
843
962
  })(data.logs[i])
844
963
  }
964
+ // 三级记忆之用户级:当日日志下方展示用户画像(跨项目规则/偏好)
965
+ rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px' } }, t('userMemoryBlock')))
966
+ rows.push(h(Card, { title: 'MEMORY.md (' + (locale === 'zh' ? '用户级' : 'user-level') + ')' },
967
+ h('div', { 'data-dam-content': '' }, userText || t('userMemoryEmpty')),
968
+ userTruncated ? h('div', { 'data-dam-hint': '' }, t('userMemoryTruncated')) : null))
845
969
  }
846
970
  return h('div', null, rows)
847
971
  }
@@ -972,16 +1096,99 @@ window.__ModuleLoader__.load({
972
1096
  h('input', { 'data-dam-input': '', placeholder: t('searchPlaceholder'), value: q, onChange: function (e) { setQ(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') search() } }),
973
1097
  h('button', { 'data-dam-btn': '', onClick: search, disabled: busy }, t('searchBtn')),
974
1098
  h('button', { 'data-dam-btn': '', onClick: smartSearch, disabled: busy }, t('smartSearch'))),
1099
+ busy ? h(Loading, { label: locale === 'zh' ? 'AI 正在分析记忆…' : 'AI is analyzing memory…' }) : null,
975
1100
  smart ? h(Card, { title: t('smartAnswer') },
976
1101
  h('div', { 'data-dam-content': '' }, smart.answer || t('empty')),
977
1102
  smart.keywords && smart.keywords.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } }, t('keywordsLabel') + smart.keywords.join(' / ')) : null,
978
- smart.hits && smart.hits.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
979
- smart.hits.map(function (h, i) { return h('div', { key: i, style: { marginTop: '3px' } }, '· [' + h.where + '] ' + h.line) })) : null)
1103
+ Array.isArray(smart.hits) && smart.hits.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
1104
+ smart.hits.map(function (hit, i) { return hit && typeof hit === 'object' ? h('div', { key: i, style: { marginTop: '3px', overflowWrap: 'anywhere' } }, '· [' + String(hit.where || '') + '] ' + String(hit.line || '')) : null })) : null)
980
1105
  : null,
981
1106
  result ? h(Card, { title: t('resultTitle') }, h('div', { 'data-dam-content': '' }, result)) : null)
982
1107
  }
983
1108
 
984
1109
  // ───────────────────────── 工作区总览页签(跨工作区全局总结) ─────────────────────────
1110
+ function graphKeywords(text) {
1111
+ return String(text || '').toLowerCase().split(/[\s,,。::;;/|()[\]{}]+/).filter(function (x) { return x.length >= 3 }).slice(0, 18)
1112
+ }
1113
+ function WorkspaceGraph(props) {
1114
+ var workspaces = props.workspaces || []
1115
+ var panPair = useState({ x: 0, y: 0 })
1116
+ var pan = panPair[0]
1117
+ var setPan = panPair[1]
1118
+ var dragPair = useState(false)
1119
+ var panning = dragPair[0]
1120
+ var setPanning = dragPair[1]
1121
+ var startPair = useState(null)
1122
+ var dragStart = startPair[0]
1123
+ var setDragStart = startPair[1]
1124
+ function pointerDown(ev) {
1125
+ if (ev.button !== 0 || (ev.target && ev.target.closest && ev.target.closest('[data-dam-graph-node]'))) return
1126
+ ev.preventDefault()
1127
+ try { if (ev.currentTarget.setPointerCapture) ev.currentTarget.setPointerCapture(ev.pointerId) } catch (e) {}
1128
+ setPanning(true)
1129
+ setDragStart({ x: ev.clientX, y: ev.clientY, px: pan.x, py: pan.y, moved: false, pointerId: ev.pointerId })
1130
+ }
1131
+ function pointerMove(ev) {
1132
+ if (!panning || !dragStart || ev.pointerId !== dragStart.pointerId) return
1133
+ var dx = ev.clientX - dragStart.x, dy = ev.clientY - dragStart.y
1134
+ if (!dragStart.moved && Math.abs(dx) + Math.abs(dy) < 3) return
1135
+ if (!dragStart.moved) { dragStart = Object.assign({}, dragStart, { moved: true }); setDragStart(dragStart) }
1136
+ setPan({ x: dragStart.px + dx, y: dragStart.py + dy })
1137
+ }
1138
+ function pointerUp(ev) {
1139
+ if (!panning || !dragStart || (ev && ev.pointerId !== dragStart.pointerId)) return
1140
+ try { if (ev.currentTarget.releasePointerCapture) ev.currentTarget.releasePointerCapture(dragStart.pointerId) } catch (e) {}
1141
+ setPanning(false); setDragStart(null)
1142
+ }
1143
+ var onSelect = props.onSelect || function () {}
1144
+ var graph = props.graph || {}
1145
+ var scale = Number(props.scale) || 1.2
1146
+ var compact = graphDensity === 'compact'
1147
+ var columns = Math.min(2, Math.max(1, workspaces.length))
1148
+ var rows = Math.ceil(workspaces.length / columns)
1149
+ var width = 920
1150
+ var colGap = 420
1151
+ var rowGap = compact ? 220 : 270
1152
+ var height = Math.max(420, rows * rowGap + 80)
1153
+ var centers = workspaces.map(function (ws, i) {
1154
+ var col = i % columns, row = Math.floor(i / columns)
1155
+ return { ws: ws, x: 160 + col * colGap, y: 72 + row * rowGap, i: i }
1156
+ })
1157
+ var edges = []
1158
+ ;(graph.links || []).forEach(function (link) {
1159
+ var from = centers.find(function (c) { return c.ws.name === link.from })
1160
+ var to = centers.find(function (c) { return c.ws.name === link.to })
1161
+ if (from && to) edges.push([from, to, link.label || ''])
1162
+ })
1163
+ var children = []
1164
+ centers.forEach(function (c) {
1165
+ var topics = c.ws.graphTopics && c.ws.graphTopics.length ? c.ws.graphTopics : (c.ws.items || []).map(function (x) { return { label: x, detail: '' } })
1166
+ topics.slice(0, compact ? 3 : 4).forEach(function (topic, j) {
1167
+ var col = j % 2, row = Math.floor(j / 2)
1168
+ children.push({ center: c, item: topic, x: c.x - 105 + col * 210, y: c.y + 82 + row * 56, j: j })
1169
+ })
1170
+ })
1171
+ return h('div', { 'data-dam-graph': '', 'data-panning': panning ? 'true' : undefined, onPointerDown: pointerDown, onPointerMove: pointerMove, onPointerUp: pointerUp, onPointerCancel: pointerUp, onLostPointerCapture: pointerUp },
1172
+ h('svg', { viewBox: '0 0 ' + width + ' ' + height, width: Math.round(width * scale), height: Math.round(height * scale), role: 'img', 'aria-label': t('wsOverview'), style: { touchAction: 'none', userSelect: 'none' } },
1173
+ h('g', { transform: 'translate(' + (pan.x / scale) + ' ' + (pan.y / scale) + ')' },
1174
+ h('g', null, edges.map(function (e, i) {
1175
+ return h('g', { key: 'cross-' + i }, h('line', { x1: e[0].x, y1: e[0].y, x2: e[1].x, y2: e[1].y, stroke: 'var(--dam-accent, #4f7cff)', strokeOpacity: .35, strokeWidth: 1.5, strokeDasharray: '5 6' }), e[2] ? h('text', { x: (e[0].x + e[1].x) / 2, y: (e[0].y + e[1].y) / 2 - 4, textAnchor: 'middle', fontSize: 8, fill: 'var(--dam-accent, #4f7cff)' }, e[2].slice(0, 16)) : null)
1176
+ })),
1177
+ h('g', null, children.map(function (n, i) {
1178
+ return h('g', { key: 'branch-' + i },
1179
+ h('line', { x1: n.center.x, y1: n.center.y + 38, x2: n.x, y2: n.y - 22, stroke: '#8b949e', strokeOpacity: .48, strokeWidth: 1.2 }),
1180
+ h('rect', { x: n.x - (compact ? 82 : 92), y: n.y - 22, width: compact ? 164 : 184, height: 44, rx: 10, fill: 'color-mix(in srgb, var(--dsw-alias-bg-overlay, #fff) 92%, transparent)', stroke: 'color-mix(in srgb, var(--dam-accent, #1d4ed8) 42%, #8b949e)', strokeOpacity: .8 }),
1181
+ h('text', { x: n.x, y: n.y - 5, textAnchor: 'middle', fontSize: compact ? 9.5 : 10.5, fontWeight: 650, fill: 'currentColor' }, String(n.item && n.item.label || n.item || '').slice(0, 22)),
1182
+ h('text', { x: n.x, y: n.y + 10, textAnchor: 'middle', fontSize: 8, fill: '#7d8793' }, String(n.item && n.item.detail || '').slice(0, 26)))
1183
+ })),
1184
+ h('g', null, centers.map(function (c) {
1185
+ return h('g', { key: c.ws.path, 'data-dam-graph-node': '', onPointerDown: function (ev) { ev.stopPropagation() }, onClick: function () { onSelect(c.ws) } },
1186
+ h('rect', { x: c.x - 100, y: c.y - 38, width: 200, height: 76, rx: 16, fill: 'color-mix(in srgb, var(--dam-accent, #1d4ed8) 22%, var(--dsw-alias-bg-overlay, #fff))', stroke: 'var(--dam-accent, #1d4ed8)', strokeWidth: 2 }),
1187
+ h('text', { x: c.x, y: c.y - 5, textAnchor: 'middle', fontSize: compact ? 10.5 : 12, fontWeight: 650, fill: 'currentColor' }, h('tspan', { x: c.x, dy: 0 }, String(c.ws.name || '').slice(0, 12)), h('tspan', { x: c.x, dy: 13 }, String(c.ws.name || '').slice(12, 24))),
1188
+ h('text', { x: c.x, y: c.y + 15, textAnchor: 'middle', fontSize: 8, fill: '#7d8793' }, (c.ws.logCount || 0) + ' ' + t('logEntries')))
1189
+ }))))
1190
+ )
1191
+ }
985
1192
  function WorkspaceTab() {
986
1193
  var dataPair = useState(null)
987
1194
  var data = dataPair[0]
@@ -996,20 +1203,32 @@ window.__ModuleLoader__.load({
996
1203
  .catch(function () { setBusy(false) })
997
1204
  }
998
1205
  useEffect(function () { load(false) }, [])
999
- if (!data) return h('div', null, busy ? h('div', { 'data-dam-hint': '' }, t('wsGenerating')) : h(Loading))
1206
+ var selectedPair = useState(null)
1207
+ var selected = selectedPair[0]
1208
+ var setSelected = selectedPair[1]
1209
+ var collapsedPair = useState({})
1210
+ var collapsedByPath = collapsedPair[0]
1211
+ var setCollapsedByPath = collapsedPair[1]
1212
+ var graphScalePair = useState(1.0)
1213
+ var graphScale = graphScalePair[0]
1214
+ var setGraphScale = graphScalePair[1]
1215
+ if (!data) return h('div', null, busy ? h(Loading, { label: locale === 'zh' ? 'AI 正在整理工作区…' : 'AI is mapping workspaces…' }) : h(Loading))
1216
+ var workspaces = data.workspaces || []
1000
1217
  return h('div', null,
1001
1218
  h('div', { 'data-dam-row': '' },
1002
1219
  h('button', { 'data-dam-btn': '', onClick: function () { load(true) }, disabled: busy }, busy ? t('refreshing') : t('refresh')),
1003
1220
  h('span', { 'data-dam-hint': '' }, data.cached ? (t('generatedAt') + new Date(data.generatedAt).toLocaleTimeString()) : '')),
1004
- (data.workspaces && data.workspaces.length) ? data.workspaces.map(function (ws) {
1005
- return h(Card, { key: ws.path, title: ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '') },
1006
- ws.summary ? h('div', { 'data-dam-content': '' }, ws.summary) : h('div', { 'data-dam-hint': '' }, t('wsNoSummary')),
1007
- (ws.items && ws.items.length) ? h('div', { style: { marginTop: '6px' } },
1008
- ws.items.map(function (it, i) {
1009
- return h('div', { key: i, style: { fontSize: 'calc(12px * var(--dam-scale))', marginTop: '3px', lineHeight: 1.5 } }, ' + it)
1010
- })) : null,
1011
- h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, ws.path + ' · ' + (ws.logCount || 0) + t('logEntries')))
1012
- }) : h('div', { 'data-dam-hint': '' }, t('wsNone')))
1221
+ workspaces.length ? h('div', null,
1222
+ h('div', { 'data-dam-hint': '', style: { margin: '5px 0 8px' } }, locale === 'zh' ? '中心节点代表工作区;分支为记忆主题;虚线表示共享主题。' : 'Centers are workspaces, branches are topics, and dashed links show shared themes.'),
1223
+ h('div', { 'data-dam-graph-toolbar': '' }, h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '思维导图大小' : 'Map size'), h('input', { type: 'range', min: '0.55', max: '1.75', step: '0.05', value: graphScale, 'aria-label': locale === 'zh' ? '调整思维导图大小' : 'Adjust map size', onChange: function (e) { setGraphScale(Number(e.target.value) || 1.0) } }), h('span', { 'data-dam-hint': '' }, Math.round(graphScale * 100) + '%'), h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '重置视图' : 'Reset view', onClick: function () { setGraphScale(1.0) } }, '⌂')),
1224
+ h('div', { 'data-dam-hint': '' }, locale === 'zh' ? '点击工作区卡片查看摘要;拖动滑块调整图的大小。' : 'Select a workspace card for details; use the slider to resize the map.'),
1225
+ h(WorkspaceGraph, { workspaces: workspaces, graph: data.graph, scale: graphScale, onSelect: function (ws) { setSelected(ws); var n = Object.assign({}, collapsedByPath); n[ws.path] = false; setCollapsedByPath(n) } }),
1226
+ h('div', { style: { marginTop: '12px' } }, workspaces.map(function (ws) {
1227
+ var open = collapsedByPath[ws.path] === false
1228
+ return h('div', { key: ws.path, 'data-dam-card': '', style: { marginBottom: '8px', padding: '0' } },
1229
+ h('button', { 'data-dam-btn': '', 'aria-expanded': open ? 'true' : 'false', onClick: function () { var n = Object.assign({}, collapsedByPath); n[ws.path] = !open; setCollapsedByPath(n); setSelected(ws) }, style: { width: '100%', textAlign: 'left', padding: '10px 12px', fontWeight: 650 } }, (open ? '▾ ' : '▸ ') + ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '')),
1230
+ h(AnimatedDisclosure, { open: open }, h('div', { style: { padding: '4px 12px 12px' } }, ws.summary ? h('div', { 'data-dam-content': '' }, ws.summary) : null, (ws.items || []).map(function (it, i) { return h('div', { key: i, style: { fontSize: 'calc(12px * var(--dam-scale))', marginTop: '4px', lineHeight: 1.55 } }, '· ' + it) }), h('div', { 'data-dam-hint': '' }, ws.path + ' · ' + (ws.logCount || 0) + t('logEntries')))))
1231
+ })) ) : h('div', { 'data-dam-hint': '' }, t('wsNone')))
1013
1232
  }
1014
1233
 
1015
1234
  // ───────────────────────── 日历页签 ─────────────────────────
@@ -1031,11 +1250,20 @@ window.__ModuleLoader__.load({
1031
1250
  var draftPair = useState(null)
1032
1251
  var draft = draftPair[0] // {date, time, quadrant, title}
1033
1252
  var setDraft = draftPair[1]
1253
+ var dayViewPair = useState(null)
1254
+ var dayView = dayViewPair[0]
1255
+ var setDayView = dayViewPair[1]
1034
1256
  var msgPair = useState('')
1035
1257
  var msg = msgPair[0]
1036
1258
  var setMsg = msgPair[1]
1259
+ var savingPair = useState(false)
1260
+ var saving = savingPair[0]
1261
+ var setSaving = savingPair[1]
1262
+ var calErrorPair = useState('')
1263
+ var calError = calErrorPair[0]
1264
+ var setCalError = calErrorPair[1]
1037
1265
  function load() {
1038
- apiGet(API.calendar).then(function (d) { if (d) setData(d) }).catch(function () {})
1266
+ apiGet(API.calendar).then(function (d) { if (d) { setData(d); setCalError('') } }).catch(function (e) { setCalError(t('failed') + e.message) })
1039
1267
  }
1040
1268
  useEffect(function () {
1041
1269
  load()
@@ -1068,7 +1296,7 @@ window.__ModuleLoader__.load({
1068
1296
  h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(-1) } }, '◀'),
1069
1297
  h('span', { style: { fontWeight: 700, flex: 1, textAlign: 'center' } }, month.year + ' / ' + String(month.mon).padStart(2, '0')),
1070
1298
  h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(1) } }, '▶'),
1071
- h('button', { 'data-dam-btn': '', onClick: function () { setDraft({ date: today, time: '09:00', quadrant: '重要不紧急', title: '' }) } }, '+ ' + t('addItem'))))
1299
+ h('button', { 'data-dam-btn': '', onClick: function () { setDraft({ date: today, time: '09:00', quadrant: '重要不紧急', title: '', location: '', reminder: '', note: '' }) } }, '+ ' + t('addItem'))))
1072
1300
  // 图例
1073
1301
  rows.push(h('div', { 'data-dam-row': '', style: { flexWrap: 'wrap' } },
1074
1302
  Object.keys(QUADRANT_STYLE).map(function (q) {
@@ -1090,7 +1318,8 @@ window.__ModuleLoader__.load({
1090
1318
  var dayNum = Number(cell.slice(8))
1091
1319
  week.push(h('div', {
1092
1320
  key: cell,
1093
- onClick: function () { setDraft({ date: cell, time: '09:00', quadrant: '重要不紧急', title: '' }) },
1321
+ 'data-dam-calendar-day': '',
1322
+ onClick: function () { setDayView(cell) },
1094
1323
  style: {
1095
1324
  minHeight: '64px', padding: '4px', cursor: 'pointer', borderRadius: '8px',
1096
1325
  border: '1px solid ' + (isToday ? 'var(--dsw-alias-brand-primary, #4f7cff)' : 'color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 55%, transparent)'),
@@ -1103,6 +1332,7 @@ window.__ModuleLoader__.load({
1103
1332
  var qs = QUADRANT_STYLE[en.quadrant] || QUADRANT_STYLE['未分类']
1104
1333
  return h('div', {
1105
1334
  key: en.time + en.title,
1335
+ 'data-dam-calendar-event': '',
1106
1336
  title: en.time + ' ' + en.title,
1107
1337
  onClick: function (ev) { ev.stopPropagation(); toggleDone(en) },
1108
1338
  style: {
@@ -1120,19 +1350,38 @@ window.__ModuleLoader__.load({
1120
1350
  }
1121
1351
  rows.push(h('div', null, gridRows))
1122
1352
  function toggleDone(en) {
1353
+ if (saving) return
1354
+ setSaving(true); setCalError('')
1123
1355
  apiPost(API.calendar, { action: en.done ? 'remove' : 'done', date: en.date, time: en.time, title: en.title }).then(function (d) {
1124
- setMsg(d.result || ''); load()
1125
- }).catch(function (e) { setMsg(t('failed') + e.message) })
1356
+ setMsg(d.result || ''); setSaving(false); load()
1357
+ }).catch(function (e) { setSaving(false); setCalError(t('failed') + e.message) })
1126
1358
  }
1127
1359
  function saveDraft() {
1128
- if (!draft || !draft.title.trim()) { setMsg(t('needTitle')); return }
1129
- apiPost(API.calendar, { date: draft.date, time: draft.time, quadrant: draft.quadrant, title: draft.title.trim(), note: draft.note || '' }).then(function (d) {
1130
- setMsg(d.result || t('saved')); setDraft(null); load()
1131
- }).catch(function (e) { setMsg(t('failed') + e.message) })
1360
+ if (!draft || !draft.title.trim()) { setCalError(t('needTitle')); return }
1361
+ if (saving) return
1362
+ setSaving(true); setCalError('')
1363
+ apiPost(API.calendar, { date: draft.date, time: draft.time, quadrant: draft.quadrant, title: draft.title.trim(), note: [draft.location ? (locale === 'zh' ? '地点: ' : 'Location: ') + draft.location : '', draft.reminder ? (locale === 'zh' ? '提醒: ' : 'Reminder: ') + draft.reminder : '', draft.note || ''].filter(Boolean).join(' | ') }).then(function (d) {
1364
+ setMsg(d.result || t('saved')); setSaving(false); setDraft(null); load()
1365
+ }).catch(function (e) { setSaving(false); setCalError(t('failed') + e.message) })
1366
+ }
1367
+ // 日期格先打开当天时间轴;点击某个时间槽才创建事件。
1368
+ if (dayView) {
1369
+ var timeline = []
1370
+ for (var hourSlot = 7; hourSlot <= 22; hourSlot++) {
1371
+ (function (hourValue) {
1372
+ var hourText = String(hourValue).padStart(2, '0') + ':00'
1373
+ var hourEvents = dayEntries(dayView).filter(function (en) { return String(en.time || '').slice(0, 2) === String(hourValue).padStart(2, '0') })
1374
+ timeline.push(h('div', { key: hourText, 'data-dam-calendar-slot': '', onClick: function () { setDayView(null); setDraft({ date: dayView, time: hourText, quadrant: '重要不紧急', title: '', location: '', reminder: '', note: '' }) } },
1375
+ h('div', { style: { flex: '0 0 50px', fontSize: 'calc(11px * var(--dam-scale))', opacity: .65 } }, hourText),
1376
+ h('div', { style: { minHeight: '34px', flex: 1, borderLeft: '2px solid color-mix(in srgb, var(--dam-accent, #1d4ed8) 24%, transparent)', paddingLeft: '9px' } }, hourEvents.length ? hourEvents.map(function (en) { var qs = QUADRANT_STYLE[en.quadrant] || QUADRANT_STYLE['未分类']; return h('div', { key: en.time + en.title, 'data-dam-calendar-event': '', onClick: function (ev) { ev.stopPropagation(); toggleDone(en) }, style: { marginBottom: '4px', padding: '4px 7px', borderRadius: 6, background: 'color-mix(in srgb, ' + qs.color + ' 18%, transparent)', color: qs.color, textDecoration: en.done ? 'line-through' : 'none' } }, (en.time || '') + ' ' + en.title + (en.note ? ' · ' + en.note : '')) }) : h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '点击添加事件' : 'Click to add event'))))
1377
+ })(hourSlot)
1378
+ }
1379
+ rows.push(h('div', { 'data-dam-calendar-modal': '', style: { position: 'absolute', inset: 0, zIndex: 10, overflow: 'auto', borderRadius: '16px', background: 'color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.9)) 78%, transparent)', backdropFilter: 'blur(20px) saturate(1.4)', WebkitBackdropFilter: 'blur(20px) saturate(1.4)', padding: '16px' } }, h('div', { 'data-dam-row': '' }, h('div', { style: { fontWeight: 700, flex: 1 } }, dayView + ' · ' + (locale === 'zh' ? '当天安排' : 'Day schedule')), h('button', { 'data-dam-btn': '', onClick: function () { setDayView(null) } }, '✕')), timeline))
1132
1380
  }
1133
1381
  // 添加/编辑浮层(液态玻璃)
1134
1382
  if (draft) {
1135
1383
  rows.push(h('div', {
1384
+ 'data-dam-calendar-modal': '',
1136
1385
  style: {
1137
1386
  position: 'absolute', inset: 0, zIndex: 10, borderRadius: '16px',
1138
1387
  background: 'color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.9)) 70%, transparent)',
@@ -1147,12 +1396,17 @@ window.__ModuleLoader__.load({
1147
1396
  h('input', { 'data-dam-input': '', type: 'time', value: draft.time, onChange: function (e) { var n = Object.assign({}, draft); n.time = e.target.value || '09:00'; setDraft(n) } }),
1148
1397
  h('select', { 'data-dam-select': '', value: draft.quadrant, onChange: function (e) { var n = Object.assign({}, draft); n.quadrant = e.target.value; setDraft(n) } },
1149
1398
  ['重要紧急', '重要不紧急', '紧急不重要', '不重要不紧急'].map(function (q) { return h('option', { key: q, value: q }, t(QUADRANT_I18N[q])) }))),
1399
+ h('input', { 'data-dam-input': '', placeholder: locale === 'zh' ? '地点(可选)' : 'Location (optional)', value: draft.location || '', onChange: function (e) { var n = Object.assign({}, draft); n.location = e.target.value; setDraft(n) } }),
1400
+ h('input', { 'data-dam-input': '', placeholder: locale === 'zh' ? '提醒内容(可选)' : 'Reminder note (optional)', value: draft.reminder || '', onChange: function (e) { var n = Object.assign({}, draft); n.reminder = e.target.value; setDraft(n) } }),
1401
+ h('textarea', { 'data-dam-input': '', rows: 2, placeholder: locale === 'zh' ? '补充说明(可选)' : 'Details (optional)', value: draft.note || '', onChange: function (e) { var n = Object.assign({}, draft); n.note = e.target.value; setDraft(n) } }),
1150
1402
  h('div', { 'data-dam-row': '' },
1151
- h('button', { 'data-dam-btn': '', onClick: saveDraft }, t('save')),
1152
- h('button', { 'data-dam-btn': '', onClick: function () { setDraft(null) } }, t('cancel')))))
1403
+ h('button', { 'data-dam-btn': '', onClick: saveDraft, disabled: saving }, saving ? (locale === 'zh' ? '保存中…' : 'Saving…') : t('save')),
1404
+ h('button', { 'data-dam-btn': '', onClick: function () { if (!saving) setDraft(null) }, disabled: saving }, t('cancel')),
1405
+ calError ? h('div', { 'data-dam-error': '' }, calError) : null)))
1153
1406
  }
1154
- msg ? h('div', { 'data-dam-hint': '' }, msg) : null
1155
- return h('div', { style: { position: 'relative' } }, rows)
1407
+ if (msg) rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '8px' } }, msg))
1408
+ if (calError && !draft) rows.push(h('div', { 'data-dam-error': '' }, calError))
1409
+ return h('div', { 'data-dam-calendar': '', style: { position: 'relative' } }, rows)
1156
1410
  }
1157
1411
 
1158
1412
  var TOOL_LABEL = { workbuddy: t('aiAssistant'), codebuddy: 'CodeBuddy', claude: 'Claude Code', codex: 'Codex', 'project-files': t('projectFiles') }
@@ -1169,10 +1423,25 @@ window.__ModuleLoader__.load({
1169
1423
  var errPair = useState('')
1170
1424
  var err = errPair[0]
1171
1425
  var setErr = errPair[1]
1426
+ var expandedPair = useState(null)
1427
+ var expanded = expandedPair[0]
1428
+ var setExpanded = expandedPair[1]
1429
+ var viewCachePair = useState({})
1430
+ var viewCache = viewCachePair[0]
1431
+ var setViewCache = viewCachePair[1]
1432
+ var removeArmPair = useState('')
1433
+ var removeArm = removeArmPair[0]
1434
+ var setRemoveArm = removeArmPair[1]
1435
+ var hitsCachePair = useState({})
1436
+ var hitsCache = hitsCachePair[0]
1437
+ var setHitsCache = hitsCachePair[1]
1438
+ var hitsBusyForPair = useState(null)
1439
+ var hitsBusyFor = hitsBusyForPair[0]
1440
+ var setHitsBusyFor = hitsBusyForPair[1]
1172
1441
  function load() {
1173
1442
  apiGet(API.external).then(function (d) { setSources(d.sources || []) }).catch(function (e) { setErr(e.message) })
1174
1443
  }
1175
- useEffect(function () { load(); var t = setInterval(load, 60000); return function () { clearInterval(t) } }, [])
1444
+ useEffect(function () { load() }, [])
1176
1445
  if (!sources) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
1177
1446
  function doImport(source, target) {
1178
1447
  var next = Object.assign({}, busy); next[source] = true
@@ -1180,8 +1449,44 @@ window.__ModuleLoader__.load({
1180
1449
  apiPost(API.externalImport, { source: source, target: target }).then(function (d) {
1181
1450
  var done = Object.assign({}, busy); done[source] = false
1182
1451
  setBusy(done); setMsg(d.result || t('imported'))
1452
+ setSources(function (prev) { return (prev || []).map(function (x) { return x.id === source ? Object.assign({}, x, target === 'user' ? { importedUser: true } : { importedNotes: true }) : x }) })
1453
+ load()
1183
1454
  }).catch(function (e) { var done = Object.assign({}, busy); done[source] = false; setBusy(done); setErr(e.message) })
1184
1455
  }
1456
+ function toggleView(source) {
1457
+ if (expanded === source.id) { setExpanded(null); return }
1458
+ setExpanded(source.id); setRemoveArm('')
1459
+ if (viewCache[source.id] !== undefined) return
1460
+ var n = Object.assign({}, viewCache); n[source.id] = { source: source, loading: true }
1461
+ setViewCache(n)
1462
+ apiGet(API.externalView, { source: source.id }).then(function (d) {
1463
+ var m = Object.assign({}, viewCache); m[source.id] = Object.assign({ source: source }, d, { loading: false })
1464
+ setViewCache(m)
1465
+ }).catch(function (e) {
1466
+ var m = Object.assign({}, viewCache); m[source.id] = { source: source, loading: false, error: e.message }
1467
+ setViewCache(m)
1468
+ })
1469
+ }
1470
+ function removeImported(source, target) {
1471
+ var key = source.id + ':' + (target || '')
1472
+ if (removeArm !== key) { setRemoveArm(key); setMsg(''); return }
1473
+ setRemoveArm('')
1474
+ apiPost(API.externalRemove, { source: source.id, target: target }).then(function (d) {
1475
+ setMsg(d.result || ''); setExpanded(null); load()
1476
+ setSources(function (prev) { return (prev || []).map(function (x) { return x.id === source.id ? Object.assign({}, x, target === 'user' ? { importedUser: false } : target === 'project' ? { importedNotes: false } : { importedUser: false, importedNotes: false }) : x }) })
1477
+ }).catch(function (e) { setErr(t('failed') + e.message) })
1478
+ }
1479
+ function findImported(source) {
1480
+ if (!source) return
1481
+ setHitsBusyFor(source.id)
1482
+ apiPost(API.recall, { query: source.name || source.tool || '' }).then(function (d) {
1483
+ var n = Object.assign({}, hitsCache); n[source.id] = d && d.result ? d.result : (locale === 'zh' ? '未找到相关片段' : 'No matches')
1484
+ setHitsCache(n); setHitsBusyFor(null)
1485
+ }).catch(function (e) {
1486
+ var n = Object.assign({}, hitsCache); n[source.id] = t('failed') + e.message
1487
+ setHitsCache(n); setHitsBusyFor(null)
1488
+ })
1489
+ }
1185
1490
  function importAll() {
1186
1491
  var md = sources.filter(function (s) { return s.kind !== 'sessions' && s.enabled !== false })
1187
1492
  setMsg(t('importing') + md.length + t('importingSuffix')); setErr('')
@@ -1195,17 +1500,31 @@ window.__ModuleLoader__.load({
1195
1500
  }
1196
1501
  for (var i = 0; i < sources.length; i++) {
1197
1502
  (function (s) {
1503
+ var isOpen = expanded === s.id
1504
+ var v = viewCache[s.id]
1198
1505
  var actions = []
1506
+ actions.push(h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '只读查看该来源的内容' : 'Read-only view of this source', onClick: function () { toggleView(s) } }, isOpen ? (locale === 'zh' ? '收起' : 'Collapse') : (locale === 'zh' ? '查看内容' : 'View content')))
1199
1507
  if (s.kind === 'sessions') {
1200
1508
  actions.push(h('span', { 'data-dam-hint': '' }, t('sessionSource') + s.fileCount + t('sessionSourceSuffix')))
1201
1509
  } else {
1202
- actions.push(h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'project') } }, busy[s.id] ? t('importingOne') : t('importToNotes')))
1203
- actions.push(h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'user') } }, t('importToUser')))
1510
+ actions.push(s.importedUser ? h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '从用户级记忆删除已导入段落' : 'Delete the imported section from user-level memory', onClick: function () { removeImported(s, 'user') }, style: removeArm === s.id + ':user' ? { background: 'color-mix(in srgb, var(--dsw-alias-state-error-primary, #d64545) 22%, transparent)', color: 'var(--dsw-alias-state-error-primary, #d64545)' } : undefined }, removeArm === s.id + ':user' ? (locale === 'zh' ? '确认删除?' : 'Confirm?') : (locale === 'zh' ? '从 prompt 删除' : 'Delete')) : h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'user') } }, t('importToUser')))
1511
+ actions.push(s.importedNotes ? h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '从项目笔记删除已导入段落' : 'Delete the imported section from project notes', onClick: function () { removeImported(s, 'project') }, style: removeArm === s.id + ':project' ? { background: 'color-mix(in srgb, var(--dsw-alias-state-error-primary, #d64545) 22%, transparent)', color: 'var(--dsw-alias-state-error-primary, #d64545)' } : undefined }, removeArm === s.id + ':project' ? (locale === 'zh' ? '确认删除?' : 'Confirm?') : (locale === 'zh' ? '从 prompt 删除' : 'Delete')) : h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'project') } }, busy[s.id] ? t('importingOne') : t('importToNotes')))
1512
+
1204
1513
  }
1205
- cards.push(h(Card, { title: s.name + ' · ' + s.tool + (s.enabled === false ? t('disabled') : '') },
1206
- h('div', { 'data-dam-hint': '' }, s.kind + ' · ' + s.fileCount + t('filesCount') + fmtSize(s.size)),
1207
- s.preview ? h('div', { 'data-dam-content': '' }, s.preview) : null,
1208
- h('div', { 'data-dam-row': '' }, actions)))
1514
+ cards.push(h(Card, { key: s.id, title: s.name + ' · ' + s.tool + (s.enabled === false ? t('disabled') : '') },
1515
+ h('div', { 'data-dam-row': '', style: { marginBottom: '2px' } }, actions),
1516
+ h(AnimatedDisclosure, { open: isOpen },
1517
+ v && v.loading ? h(Loading, { label: locale === 'zh' ? '正在读取来源…' : 'Reading source…' })
1518
+ : v && v.error ? h('div', { 'data-dam-error': '' }, v.error)
1519
+ : v ? h('div', null,
1520
+ h('div', { 'data-dam-content': '' }, v.content || t('empty')),
1521
+ v.imported !== undefined ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
1522
+ (locale === 'zh' ? '已接入位置:' : 'Imported at: ') + (v.imported ? (v.locations || []).join(' ; ') : (locale === 'zh' ? '尚未接入' : 'Not imported'))) : null,
1523
+ h('div', { 'data-dam-row': '', style: { marginTop: '8px' } },
1524
+ h('button', { 'data-dam-btn': '', onClick: function () { findImported(s) } }, hitsBusyFor === s.id ? (locale === 'zh' ? '检索中…' : 'Searching…') : (locale === 'zh' ? '在记忆中查找' : 'Find in memory'))),
1525
+ hitsCache[s.id] ? h('div', { 'data-dam-content': '', style: { marginTop: '6px' } }, String(hitsCache[s.id])) : null,
1526
+ h('div', { 'data-dam-hint': '', style: { marginTop: '8px' } }, locale === 'zh' ? '提示:接入后按钮会变成「从 prompt 删除」,可单独删除用户级或项目笔记里的已导入段落;不会删除来源文件。' : 'Note: after import the button becomes "Delete" for that target; source files are never touched.'))
1527
+ : null)))
1209
1528
  })(sources[i])
1210
1529
  }
1211
1530
  return h('div', null,
@@ -1248,6 +1567,52 @@ window.__ModuleLoader__.load({
1248
1567
  }
1249
1568
  }
1250
1569
 
1570
+ function TabScroller(props) {
1571
+ var tabs = props.tabs || []
1572
+ var tab = props.tab
1573
+ var setTab = props.setTab
1574
+ var viewportRef = useRef(null)
1575
+ var canLeftPair = useState(false)
1576
+ var canLeft = canLeftPair[0]
1577
+ var setCanLeft = canLeftPair[1]
1578
+ var canRightPair = useState(false)
1579
+ var canRight = canRightPair[0]
1580
+ var setCanRight = canRightPair[1]
1581
+ function refreshArrows() {
1582
+ var vp = viewportRef.current
1583
+ if (!vp) return
1584
+ setCanLeft(vp.scrollLeft > 2)
1585
+ setCanRight(vp.scrollLeft + vp.clientWidth < vp.scrollWidth - 2)
1586
+ }
1587
+ useEffect(function () {
1588
+ var vp = viewportRef.current
1589
+ if (!vp) return
1590
+ refreshArrows()
1591
+ vp.addEventListener('scroll', refreshArrows)
1592
+ var ro = null
1593
+ if (typeof ResizeObserver !== 'undefined') { try { ro = new ResizeObserver(refreshArrows); ro.observe(vp) } catch (e) {} }
1594
+ return function () { vp.removeEventListener('scroll', refreshArrows); if (ro) ro.disconnect() }
1595
+ }, [tabs.length])
1596
+ useEffect(function () {
1597
+ var vp = viewportRef.current
1598
+ if (!vp) return
1599
+ var activeEl = vp.querySelector('[data-active="true"]')
1600
+ if (!activeEl) return
1601
+ var l = activeEl.offsetLeft, w = activeEl.offsetWidth
1602
+ if (l < vp.scrollLeft) vp.scrollTo({ left: Math.max(0, l - 8), behavior: 'smooth' })
1603
+ else if (l + w > vp.scrollLeft + vp.clientWidth) vp.scrollTo({ left: l + w - vp.clientWidth + 8, behavior: 'smooth' })
1604
+ }, [tab])
1605
+ function step(dir) {
1606
+ var vp = viewportRef.current
1607
+ if (!vp) return
1608
+ try { vp.scrollBy({ left: dir * Math.max(90, vp.clientWidth * 0.6), behavior: 'smooth' }) } catch (e) { vp.scrollLeft += dir * 90 }
1609
+ }
1610
+ return h('div', { 'data-dam-tabs-wrap': '' },
1611
+ h('button', { 'data-dam-tabs-arrow': '', title: locale === 'zh' ? '向左滚动模块' : 'Scroll modules left', 'aria-label': locale === 'zh' ? '向左滚动模块' : 'Scroll modules left', disabled: !canLeft, onClick: function () { step(-1) } }, '‹'),
1612
+ h('div', { 'data-dam-tabs': '', ref: viewportRef }, h('div', { 'data-dam-tab-strip': '' }, tabs.map(function (item) { return h('button', { key: item[0], 'data-dam-tab': '', 'data-active': tab === item[0] ? 'true' : undefined, onClick: function () { setTab(item[0]) } }, item[1]) }))),
1613
+ h('button', { 'data-dam-tabs-arrow': '', title: locale === 'zh' ? '向右滚动模块' : 'Scroll modules right', 'aria-label': locale === 'zh' ? '向右滚动模块' : 'Scroll modules right', disabled: !canRight, onClick: function () { step(1) } }, '›'))
1614
+ }
1615
+
1251
1616
  function MemoryPanel() {
1252
1617
  var tick = useTick()
1253
1618
  var tabPair = useState('overview')
@@ -1276,6 +1641,7 @@ window.__ModuleLoader__.load({
1276
1641
  width: g.width + 'px',
1277
1642
  height: g.height + 'px',
1278
1643
  '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
1644
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
1279
1645
  }
1280
1646
  var dragMove = startPointerDrag(function (dx, dy) {
1281
1647
  controller.setGeom({ left: g.left + dx, top: g.top + dy })
@@ -1295,13 +1661,12 @@ window.__ModuleLoader__.load({
1295
1661
  onPointerDown: dragMove,
1296
1662
  },
1297
1663
  h('strong', null, t('autoMemory')),
1664
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, 'v0.1.23-ui'),
1298
1665
  h('span', { className: 'dam-spacer' }),
1299
1666
  h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
1300
1667
  h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
1301
1668
  h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
1302
- h('div', { 'data-dam-tabs': '' }, tabs.map(function (t) {
1303
- return h('button', { key: t[0], 'data-dam-tab': '', 'data-active': tab === t[0] ? 'true' : undefined, onClick: function () { setTab(t[0]) } }, t[1])
1304
- })),
1669
+ h(TabScroller, { tabs: tabs, tab: tab, setTab: setTab }),
1305
1670
  h('div', { 'data-dam-body': '' }, body),
1306
1671
  h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove }))
1307
1672
  }
@@ -1478,9 +1843,15 @@ window.__ModuleLoader__.load({
1478
1843
  var errPair = useState('')
1479
1844
  var err = errPair[0]
1480
1845
  var setErr = errPair[1]
1846
+ var dirtyPair = useState(false)
1847
+ var dirty = dirtyPair[0]
1848
+ var setDirty = dirtyPair[1]
1481
1849
  var dbgOpenPair = useState(false)
1482
1850
  var dbgOpen = dbgOpenPair[0]
1483
1851
  var setDbgOpen = dbgOpenPair[1]
1852
+ var settingsSectionPair = useState('appearance')
1853
+ var settingsSection = settingsSectionPair[0]
1854
+ var setSettingsSection = settingsSectionPair[1]
1484
1855
  var browseOpenPair = useState(false)
1485
1856
  var browseOpen = browseOpenPair[0]
1486
1857
  var setBrowseOpen = browseOpenPair[1]
@@ -1539,7 +1910,7 @@ window.__ModuleLoader__.load({
1539
1910
  return function () { alive = false }
1540
1911
  }, [])
1541
1912
  if (!cfg) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
1542
- function set(key, value) { var next = Object.assign({}, cfg); next[key] = value; setCfg(next) }
1913
+ function set(key, value) { var next = Object.assign({}, cfg); next[key] = value; setCfg(next); setDirty(true) }
1543
1914
  function checkUpdate() {
1544
1915
  if (checkingUpdate) return
1545
1916
  setCheckingUpdate(true)
@@ -1558,21 +1929,51 @@ window.__ModuleLoader__.load({
1558
1929
  function save() {
1559
1930
  if (busy) return
1560
1931
  setBusy(true); setMsg(''); setErr('')
1561
- apiPost(API.config, cfg).then(function (d) { setCfg(d.config); setMsg(t('saved') + (d.migrated ? ' ' + d.migrated : '')); setBusy(false); if (d && d.config && d.config.locale) applyLocalePref(d.config.locale) })
1932
+ apiPost(API.config, cfg).then(function (d) { setCfg(d.config); setDirty(false); setMsg(t('saved') + (d.migrated ? ' ' + d.migrated : '')); setBusy(false); if (d && d.config && d.config.locale) applyLocalePref(d.config.locale) })
1562
1933
  .catch(function (e) { setErr(e.message); setBusy(false) })
1563
1934
  }
1564
1935
  function field(label, control, hint) {
1565
- return h('div', null,
1936
+ return h('div', { 'data-dam-settings-row': '' },
1566
1937
  h('div', { 'data-dam-row': '' }, h('label', null, label), control),
1567
1938
  hint ? h('div', { 'data-dam-hint': '' }, hint) : null)
1568
1939
  }
1569
- return h('div', null,
1570
- h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
1571
- field(t('fLocale'), h('select', { 'data-dam-select': '', value: cfg.locale || 'system', onChange: function (e) { set('locale', e.target.value) } },
1572
- LOCALE_IDS_LIST.map(function (id) { return h('option', { key: id, value: id }, id === 'system' ? t('followSystem') : t(id)) })), t('fLocaleHint')),
1573
- field(t('fFontSize'), h('select', { 'data-dam-select': '', value: fontScale, onChange: function (e) { fontScale = e.target.value; try { localStorage.setItem('dsh-auto-memory.fontScale.v2', fontScale) } catch (ee) {}; try { var pp = document.querySelector('[data-dam-panel]'); if (pp) pp.style.setProperty('--dam-scale', FONT_SCALE_VALUES[fontScale] || '1') } catch (ee2) {}; emit() } },
1574
- Object.keys(FONT_SCALES).map(function (k) { return h('option', { key: k, value: k }, t('fs' + k.charAt(0).toUpperCase() + k.slice(1))) })), t('fFontSizeHint')),
1575
- field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
1940
+ function setAccent(value) {
1941
+ accentTheme = ACCENT_VALUES[value] ? value : 'deepseek'
1942
+ try { localStorage.setItem('dsh-auto-memory.accentTheme.v1', accentTheme) } catch (e) {}
1943
+ emit()
1944
+ }
1945
+ function setDensity(value) {
1946
+ graphDensity = value === 'compact' ? 'compact' : 'relaxed'
1947
+ try { localStorage.setItem('dsh-auto-memory.graphDensity.v1', graphDensity) } catch (e) {}
1948
+ emit()
1949
+ }
1950
+ var sectionLabels = {
1951
+ appearance: locale === 'zh' ? '外观' : 'Appearance', storage: locale === 'zh' ? '存储' : 'Storage',
1952
+ injection: locale === 'zh' ? '记忆窗口' : 'Memory window', automation: locale === 'zh' ? '自动化' : 'Automation',
1953
+ maintenance: locale === 'zh' ? '维护' : 'Maintenance'
1954
+ }
1955
+ function section(key, title, content) { return h('section', { id: 'dam-settings-' + key, 'data-dam-settings-group': '' }, h('h3', null, title), content) }
1956
+ function jumpToSection(key) {
1957
+ setSettingsSection(key)
1958
+ try { var el = document.getElementById('dam-settings-' + key); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }) } catch (e) {}
1959
+ }
1960
+ return h('div', { 'data-dam-settings': '' },
1961
+ h('nav', { 'data-dam-settings-nav': '', 'aria-label': locale === 'zh' ? '设置分组' : 'Settings sections' }, Object.keys(sectionLabels).map(function (key) {
1962
+ return h('button', { key: key, 'data-dam-btn': '', 'data-active': settingsSection === key ? 'true' : undefined, onClick: function () { jumpToSection(key) } }, sectionLabels[key])
1963
+ })),
1964
+ h('div', { 'data-dam-settings-content': '' },
1965
+ section('appearance', sectionLabels.appearance, [
1966
+ h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
1967
+ field(t('fLocale'), h('select', { 'data-dam-select': '', value: cfg.locale || 'system', onChange: function (e) { set('locale', e.target.value) } },
1968
+ LOCALE_IDS_LIST.map(function (id) { return h('option', { key: id, value: id }, id === 'system' ? t('followSystem') : t(id)) })), t('fLocaleHint')),
1969
+ field(t('fFontSize'), h('select', { 'data-dam-select': '', value: fontScale, onChange: function (e) { fontScale = e.target.value; try { localStorage.setItem('dsh-auto-memory.fontScale.v2', fontScale) } catch (ee) {}; try { var pp = document.querySelector('[data-dam-panel]'); if (pp) pp.style.setProperty('--dam-scale', FONT_SCALE_VALUES[fontScale] || '1') } catch (ee2) {}; emit() } },
1970
+ Object.keys(FONT_SCALES).map(function (k) { return h('option', { key: k, value: k }, t('fs' + k.charAt(0).toUpperCase() + k.slice(1))) })), t('fFontSizeHint')),
1971
+ field(locale === 'zh' ? '强调色' : 'Accent color', h('select', { 'data-dam-select': '', value: accentTheme, onChange: function (e) { setAccent(e.target.value) } },
1972
+ h('option', { value: 'deepseek' }, locale === 'zh' ? 'DeepSeek 蓝' : 'DeepSeek blue'), h('option', { value: 'graphite' }, locale === 'zh' ? '石墨灰' : 'Graphite'), h('option', { value: 'violet' }, locale === 'zh' ? '雾紫' : 'Violet')), locale === 'zh' ? '默认使用 DeepSeek 蓝;日历与状态颜色保持语义色。' : 'DeepSeek blue by default; calendar and status colors stay semantic.'),
1973
+ field(locale === 'zh' ? '关系图密度' : 'Graph density', h('select', { 'data-dam-select': '', value: graphDensity, onChange: function (e) { setDensity(e.target.value) } },
1974
+ h('option', { value: 'relaxed' }, locale === 'zh' ? '舒展' : 'Relaxed'), h('option', { value: 'compact' }, locale === 'zh' ? '紧凑' : 'Compact')), locale === 'zh' ? '影响工作区关系图的节点间距和显示数量。' : 'Controls node spacing and detail in the workspace graph.')
1975
+ ]),
1976
+ section('storage', sectionLabels.storage, [field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
1576
1977
  field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
1577
1978
  field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
1578
1979
  h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
@@ -1588,30 +1989,36 @@ window.__ModuleLoader__.load({
1588
1989
  h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
1589
1990
  h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
1590
1991
  h('button', { 'data-dam-btn': '', onClick: function () { setBrowseOpen(false) } }, t('close'))))
1591
- : null,
1592
- field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
1992
+ : null]),
1993
+ section('injection', sectionLabels.injection, [field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
1593
1994
  field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
1594
- field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 3) } }), t('fDaysHint')),
1995
+ field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 1) } }), t('fDaysHint')),
1996
+ field(t('fExtBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.externalInjectionChars === undefined ? 1400 : cfg.externalInjectionChars, onChange: function (e) { set('externalInjectionChars', Number(e.target.value) || 1400) } }), t('fExtBudgetHint'))]),
1997
+ section('automation', sectionLabels.automation, [field(t('fConsolidateMin'), h('input', { 'data-dam-input': '', type: 'number', min: 80, value: cfg.autoConsolidateMinChars === undefined ? 240 : cfg.autoConsolidateMinChars, onChange: function (e) { set('autoConsolidateMinChars', Number(e.target.value) || 240) } }), t('fConsolidateMinHint')),
1998
+ field(t('fAutoConsolidate'), h('input', { type: 'checkbox', checked: cfg.autoConsolidate !== false, onChange: function (e) { set('autoConsolidate', e.target.checked) } }), t('fAutoConsolidateHint')),
1999
+ field(t('fConsolidate'), h('input', { 'data-dam-input': '', type: 'number', min: 5, value: cfg.autoConsolidateCooldownMinutes === undefined ? 30 : cfg.autoConsolidateCooldownMinutes, onChange: function (e) { set('autoConsolidateCooldownMinutes', Number(e.target.value) || 30) } }), t('fConsolidateHint')),
2000
+ field(t('fConsolidateMax'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 50, value: cfg.autoConsolidateDailyMax === undefined ? 8 : cfg.autoConsolidateDailyMax, onChange: function (e) { set('autoConsolidateDailyMax', Number(e.target.value) || 8) } }), t('fConsolidateMaxHint')),
1595
2001
  field(t('fAway'), h('input', { 'data-dam-input': '', type: 'number', min: 1, value: cfg.awayMinutes || 60, onChange: function (e) { set('awayMinutes', Number(e.target.value) || 60) } }), t('fAwayHint')),
1596
2002
  field(t('fAutoSum'), h('input', { 'data-dam-input': '', value: (cfg.autoSummaryTimes || []).join(','), onChange: function (e) { set('autoSummaryTimes', String(e.target.value || '').split(',').map(function (s) { return s.trim() }).filter(Boolean)) } }), t('fAutoSumHint')),
1597
2003
  field(t('fDayBoundary'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1439, value: (cfg.dayBoundaryMinutes === undefined ? 450 : cfg.dayBoundaryMinutes), onChange: function (e) { set('dayBoundaryMinutes', parseInt(e.target.value || '0', 10)) } }), t('fDayBoundaryHint')),
1598
2004
  field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
1599
2005
  field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
1600
- STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
1601
- field(t('fVersion'), h('div', { 'data-dam-row': '' },
2006
+ STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint'))]),
2007
+ section('maintenance', sectionLabels.maintenance, [field(t('fVersion'), h('div', { 'data-dam-row': '' },
1602
2008
  h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
1603
2009
  h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
1604
2010
  (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
1605
2011
  upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
1606
- t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
1607
- h('div', { 'data-dam-row': '' },
1608
- h('button', { 'data-dam-btn': '', onClick: save, disabled: busy }, busy ? t('saving') : t('saveSettings')),
1609
- msg ? h('span', null, msg) : null),
2012
+ t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : ''))]),
2013
+ h('div', { 'data-dam-savebar': '' },
2014
+ h('button', { 'data-dam-btn': '', 'data-dirty': dirty ? 'true' : undefined, onClick: save, disabled: busy }, busy ? t('saving') : (dirty ? (locale === 'zh' ? '保存更改' : 'Save changes') : t('saveSettings'))),
2015
+ dirty ? h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '有未保存的更改' : 'Unsaved changes') : null,
2016
+ msg ? h('span', { 'data-dam-hint': '' }, msg) : null),
1610
2017
  // 调试中心(折叠):模块状态一览,方便排查问题/提 issue
1611
2018
  h('div', { style: { marginTop: '14px', borderTop: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent)', paddingTop: '10px' } },
1612
2019
  h('button', { 'data-dam-btn': '', onClick: function () { setDbgOpen(!dbgOpen) } }, (dbgOpen ? '▴ ' : '▾ ') + t('debugCenter')),
1613
2020
  dbgOpen ? h('div', { style: { marginTop: '8px' } }, h(DebugCenter)) : null),
1614
- err ? h('div', { 'data-dam-error': '' }, err) : null)
2021
+ err ? h('div', { 'data-dam-error': '' }, err) : null))
1615
2022
  }
1616
2023
 
1617
2024
  // ───────────────────────── 插件挂载 ─────────────────────────
@@ -1746,7 +2153,7 @@ window.__ModuleLoader__.load({
1746
2153
  function registerSurfaces() {
1747
2154
  try {
1748
2155
  surfaceDisposers.push(slots.inject('sidebar.footer.action', function () {
1749
- return slots.register({ name: 'sidebar.footer.action', id: 'auto-memory', order: 5, label: t('memory') }, function () { return h(SidebarButton) })
2156
+ return slots.register({ name: 'sidebar.footer.action', id: 'auto-memory', order: 5, label: t('memory') + '' }, function () { return h(SidebarButton) })
1750
2157
  }))
1751
2158
  surfaceDisposers.push(slots.inject('shell.overlay', function () {
1752
2159
  return slots.register({ name: 'shell.overlay', id: 'auto-memory', order: 5 }, function () { return h(MemoryPanel) })
@@ -1755,7 +2162,7 @@ window.__ModuleLoader__.load({
1755
2162
  return slots.register({ name: 'shell.overlay', id: 'auto-memory-dialogs', order: 6 }, function () { return h(DialogHost) })
1756
2163
  }))
1757
2164
  surfaceDisposers.push(slots.inject('settings.section', function () {
1758
- return slots.register({ name: 'settings.section', id: 'auto-memory', order: 25, label: t('autoMemory') }, function (props) { return h(SettingsPage, { close: props && props.close }) })
2165
+ return slots.register({ name: 'settings.section', id: 'auto-memory', order: 25, label: t('autoMemory') + '' }, function (props) { return h(SettingsPage, { close: props && props.close }) })
1759
2166
  }))
1760
2167
  } catch (e) {
1761
2168
  console.warn('[dsh-auto-memory] slot registration failed', e)