@a9i5k4/dsh-auto-memory 0.1.23 → 0.1.25

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()
@@ -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: '时段总结',
@@ -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.25': { zh: [
350
+ '文档:README 界面截图更新为最新实机截图(记忆面板概览/接续/日历/工作区导图/设置),中英双语说明。',
351
+ ], en: [
352
+ 'Docs: README screenshots refreshed with the latest real captures (panel overview / connect / calendar / workspace mind map / settings) with bilingual captions.',
353
+ ] },
354
+ '0.1.24': { zh: [
355
+ '记忆面板:全新液态玻璃 UI——模块箭头滚动、抽屉丝滑展开/收起、工作区 AI 思维导图(拖动画布+缩放)、日历当天时间轴(07:00-22:00)+地点/提醒字段。',
356
+ '注入精简:只注入最近 1 天日志与反思精华,外部记忆改为绝对路径按需读取,新增 memory_read 工具;敏感段落(令牌/密钥)不再注入 prompt。',
357
+ '接续页:按来源查看内容/接入/移除(笔记与用户级独立),已接入来源显示 ✓;不再整段注入外部记忆。',
358
+ '设置页:全部参数可调(注入预算/天数/外部预算、自动沉淀开关/门槛/间隔/额度),浮动保存栏未保存时高亮。',
359
+ '稳定性:修复对话结束时 host 卡死/崩溃(惰性投影移除、超时兜底、延迟启动),自动沉淀恢复正常并每日限频。',
360
+ ], en: [
361
+ '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.',
362
+ '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.',
363
+ 'Connect tab: per-source view/import/remove (notes vs user-level independently), imported sources show ✓; external memory is no longer injected in bulk.',
364
+ '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.',
365
+ 'Stability: fixed host freezes/crashes at turn end (lazy projection removed, timeout fallbacks, delayed start); auto-consolidation recovered with daily rate limiting.',
366
+ ] },
327
367
  '0.1.23': { zh: [
328
368
  '修复:正式发布包首次欢迎文案仍显示“开发版”;发布转换与残留校验已加强,确保开发版和正式版身份完全隔离。',
329
369
  ], en: [
@@ -468,7 +508,10 @@ window.__ModuleLoader__.load({
468
508
  reflectAuto: '/api/dsh-auto-memory/reflect-auto',
469
509
  note: '/api/dsh-auto-memory/note',
470
510
  external: '/api/dsh-auto-memory/external',
511
+ externalView: '/api/dsh-auto-memory/external-view',
512
+ externalRemove: '/api/dsh-auto-memory/external-remove',
471
513
  externalImport: '/api/dsh-auto-memory/external-import',
514
+ calendar: '/api/dsh-auto-memory/calendar',
472
515
  summarize: '/api/dsh-auto-memory/summarize',
473
516
  greet: '/api/dsh-auto-memory/greet',
474
517
  notices: '/api/dsh-auto-memory/notices',
@@ -482,13 +525,13 @@ window.__ModuleLoader__.load({
482
525
  async function apiGet(path, params) {
483
526
  var res = await fetch(path + query(params))
484
527
  var body = await res.json().catch(function () { return {} })
485
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status))
528
+ if (!res.ok) throw new Error(body.error || ('GET ' + path + ' → HTTP ' + res.status))
486
529
  return body
487
530
  }
488
531
  async function apiPost(path, payload) {
489
532
  var res = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
490
533
  var body = await res.json().catch(function () { return {} })
491
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status))
534
+ if (!res.ok) throw new Error(body.error || ('POST ' + path + ' → HTTP ' + res.status))
492
535
  return body
493
536
  }
494
537
 
@@ -526,16 +569,61 @@ window.__ModuleLoader__.load({
526
569
  '[data-dam-resize]:hover { opacity: 1; }',
527
570
  '[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; }',
528
571
  '[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
529
- '[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); }',
530
- '[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)); }',
531
- '[data-dam-tab][data-active="true"] { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 18%, transparent); font-weight: 600; }',
532
- '[data-dam-body] { flex: 1; overflow: auto; padding: 12px 14px; }',
572
+ '[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); }',
573
+ '[data-dam-tabs] { flex: 1; min-width: 0; overflow: hidden; padding: 5px 4px; scrollbar-width: none; -ms-overflow-style: none; }',
574
+ '[data-dam-tabs]::-webkit-scrollbar { display: none; }',
575
+ '[data-dam-tab-strip] { display: flex; width: max-content; transition: transform .42s cubic-bezier(.22,.8,.2,1); will-change: transform; }',
576
+ '[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; }',
577
+ '[data-dam-tabs-arrow] { flex: 0 0 26px; height: 30px; border: 0; background: transparent; color: inherit; cursor: pointer; opacity: .55; font-size: 18px; }',
578
+ '[data-dam-tabs-arrow]:hover:not(:disabled) { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #2456c4) 16%, transparent); transform: scale(1.12); }',
579
+ '[data-dam-tabs-arrow]:active:not(:disabled) { transform: scale(.92); }',
580
+ '[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); }',
581
+ '[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); }',
582
+ '[data-dam-body] { flex: 1 1 auto; min-width: 0; min-height: 0; overflow: auto; padding: 16px; }',
583
+ '[data-dam-settings] { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 18px; min-height: 100%; }',
584
+ '[data-dam-settings-nav] { position: sticky; top: 0; align-self: start; display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }',
585
+ '[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)); }',
586
+ '[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; }',
587
+ '[data-dam-settings-content] { min-width: 0; padding-bottom: 24px; }',
588
+ '[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); }',
589
+ '[data-dam-settings-group] h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2328); font-size: calc(14px * var(--dam-scale)); }',
590
+ '[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; }',
591
+ '[data-dam-settings-row] { padding: 10px 0; }',
592
+ '[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; }',
593
+ '[data-dam-savebar] button { opacity: .5; transition: opacity .2s ease, background .2s ease, color .2s ease; }',
594
+ '[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); }',
595
+ '[data-dam-settings-row] > [data-dam-row] { margin-bottom: 3px; }',
596
+ '[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); }',
597
+ '[data-dam-graph-toolbar] input[type="range"] { flex: 1; min-width: 90px; accent-color: var(--dam-accent, #6b98ff); }',
598
+ '[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); }',
599
+ '[data-dam-graph] { cursor: grab; }',
600
+ '[data-dam-graph][data-panning="true"] { cursor: grabbing; user-select: none; }',
601
+ '[data-dam-graph] svg { display: block; width: auto; max-width: none; height: auto; min-height: 520px; transform-origin: 0 0; }',
602
+ '[data-dam-graph-node] { cursor: pointer; }',
603
+ '[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)); }',
604
+ '[data-dam-legend-dot] { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 4px; background: var(--dam-accent, #4f7cff); }',
605
+ '[data-dam-legend-dot="branch"] { background: #7d8793; }',
606
+ '[data-dam-legend-dot="leaf"] { background: #b0b7c0; }',
533
607
  '[data-dam-kv] { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: calc(12.5px * var(--dam-scale)); }',
534
608
  '[data-dam-kv] b { opacity: .55; font-weight: 500; }',
535
609
  '[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));',
536
610
  ' background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent); }',
537
611
  '[data-dam-card] .dam-date { font-weight: 600; margin-bottom: 4px; }',
538
612
  '[data-dam-card] .dam-content { white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow: auto; }',
613
+ '[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); }',
614
+ '[data-dam-disclosure][data-phase="open"] { opacity: 1; max-height: 1800px; transform: translateY(0) scale(1); }',
615
+ '[data-dam-disclosure][data-phase="closing"] { opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); }',
616
+ '[data-dam-card], [data-dam-banner] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
617
+ '@keyframes dam-rise { from { opacity: 0; transform: translateY(7px) scale(.988); } to { opacity: 1; transform: translateY(0) scale(1); } }',
618
+ '@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; } }',
619
+ '[data-dam-calendar] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
620
+ '[data-dam-calendar] [data-dam-calendar-day] { transition: transform .18s ease, border-color .2s ease, background .2s ease, box-shadow .2s ease; }',
621
+ '[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); }',
622
+ '[data-dam-calendar-event] { animation: dam-event-in .25s cubic-bezier(.22,.8,.2,1) both; transition: transform .18s ease, filter .18s ease; }',
623
+ '[data-dam-calendar-event]:hover { transform: translateX(2px); filter: brightness(1.06); }',
624
+ '[data-dam-calendar-modal] { animation: dam-modal-in .26s cubic-bezier(.22,.8,.2,1) both; }',
625
+ '@keyframes dam-event-in { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: none; } }',
626
+ '@keyframes dam-modal-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }',
539
627
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
540
628
  ' 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)); }',
541
629
  '[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; }',
@@ -547,6 +635,9 @@ window.__ModuleLoader__.load({
547
635
  '[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); }',
548
636
  '[data-dam-error] { color: var(--dsw-alias-state-error-primary, #d64545); font-size: 12px; margin-top: 6px; white-space: pre-wrap; }',
549
637
  '[data-dam-muted] { opacity: .55; }',
638
+ '[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)); }',
639
+ '[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; }',
640
+ '@keyframes dam-spin { to { transform: rotate(360deg); } }',
550
641
  '@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {',
551
642
  ' [data-dam-panel] { background: var(--dsw-alias-bg-overlay, #ffffff); } }',
552
643
  ].join('\n')
@@ -573,7 +664,32 @@ window.__ModuleLoader__.load({
573
664
  h('div', { className: 'dam-content' }, props.children))
574
665
  }
575
666
 
576
- function Loading() { return h('div', { 'data-dam-muted': '' }, t('loading')) }
667
+ function Loading(props) {
668
+ var label = props && props.label ? props.label : t('loading')
669
+ return h('div', { 'data-dam-loading': '' }, h('span', { 'data-dam-spinner': '', 'aria-hidden': 'true' }), h('span', null, label))
670
+ }
671
+ function AnimatedDisclosure(props) {
672
+ var open = !!props.open
673
+ var shownPair = useState(open)
674
+ var shown = shownPair[0]
675
+ var setShown = shownPair[1]
676
+ var phasePair = useState(open ? 'open' : 'closed')
677
+ var phase = phasePair[0]
678
+ var setPhase = phasePair[1]
679
+ useEffect(function () {
680
+ var timer
681
+ if (open) {
682
+ setShown(true)
683
+ timer = setTimeout(function () { setPhase('open') }, 16)
684
+ } else if (shown) {
685
+ setPhase('closing')
686
+ timer = setTimeout(function () { setShown(false); setPhase('closed') }, 260)
687
+ }
688
+ return function () { if (timer) clearTimeout(timer) }
689
+ }, [open])
690
+ if (!shown) return null
691
+ return h('div', { 'data-dam-disclosure': '', 'data-phase': phase }, props.children)
692
+ }
577
693
 
578
694
  // ───────────────────────── 侧边栏入口 ─────────────────────────
579
695
  function SidebarButton() {
@@ -684,7 +800,7 @@ window.__ModuleLoader__.load({
684
800
  h('div', { style: { fontWeight: 600, fontSize: 'calc(12.5px * var(--dam-scale))', lineHeight: 1.5 } }, (isOpen ? '▼ ' : '▶ ') + bigTitle),
685
801
  h('div', { 'data-dam-muted': '', style: { fontSize: 'calc(11px * var(--dam-scale))', marginTop: '2px' } },
686
802
  dr.label + ' · ' + dr.items.length + t('logEntries') + (cached && cached.at ? ' · ' + t('generatedAt') + new Date(cached.at).toLocaleTimeString() : ''))),
687
- isOpen ? h('div', { style: { padding: '4px 2px 2px 8px' } },
803
+ h(AnimatedDisclosure, { open: isOpen }, h('div', { style: { padding: '4px 2px 2px 8px' } },
688
804
  cached && cached.works && cached.works.length ? cached.works.map(function (w, wi) {
689
805
  var sk = dr.key + ':w' + wi
690
806
  var sOpen = subOpenMap[sk] !== undefined ? subOpenMap[sk] : false
@@ -702,7 +818,7 @@ window.__ModuleLoader__.load({
702
818
  })) : null)
703
819
  }) : cached && cached.failed ? h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summaryFailed'))
704
820
  : cached && cached.summary ? h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, cached.summary)
705
- : h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summarizing'))) : null))
821
+ : h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summarizing'))))))
706
822
  })(drawers[d])
707
823
  }
708
824
  // 待反思提醒
@@ -728,6 +844,9 @@ window.__ModuleLoader__.load({
728
844
  var wsPair = useState(null)
729
845
  var wsData = wsPair[0]
730
846
  var setWsData = wsPair[1]
847
+ var wsOpenPair = useState(false)
848
+ var wsOpen = wsOpenPair[0]
849
+ var setWsOpen = wsOpenPair[1]
731
850
  // 每次进入面板 / 点刷新(nonce 变化)/ 面板打开期间每 30s 自动重拉,保证自动沉淀等数据新鲜
732
851
  useEffect(function () {
733
852
  var alive = true
@@ -768,19 +887,12 @@ window.__ModuleLoader__.load({
768
887
  return h('div', null,
769
888
  // 今日问候卡:问候语 + 昨天时间轴 + 提醒(纯 GUI 渲染,不干扰对话流)
770
889
  h(GreetingCard, { greeting: state.greeting, periodSummary: state.periodSummary, t: t, nonce: props && props.nonce }),
771
- // 跨工作区总结(所有工作区的小总结 + 区内细分)
772
- h('div', { style: { marginTop: '10px' } },
773
- h('div', { style: { fontWeight: 700, fontSize: 'calc(12.5px * var(--dam-scale))', opacity: 0.75, marginBottom: '6px' } }, t('wsOverview')),
774
- wsData && wsData.workspaces && wsData.workspaces.length
775
- ? wsData.workspaces.map(function (ws) {
776
- 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)' } },
777
- h('div', { style: { fontWeight: 600, fontSize: 'calc(12px * var(--dam-scale))' } }, ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '')),
778
- 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')),
779
- (ws.items && ws.items.length) ? ws.items.map(function (it, i) {
780
- return h('div', { key: i, style: { fontSize: 'calc(11.5px * var(--dam-scale))', marginTop: '2px', opacity: 0.75 } }, '· ' + it)
781
- }) : null)
782
- })
783
- : h('div', { 'data-dam-hint': '' }, t('wsGenerating'))),
890
+ // 跨工作区总结默认折叠,首页把空间留给问候与今日状态
891
+ h('div', { 'data-dam-collapsible': '', style: { marginTop: '16px' } },
892
+ 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')),
893
+ h(AnimatedDisclosure, { open: wsOpen }, wsData && wsData.workspaces && wsData.workspaces.length
894
+ ? h('div', { style: { paddingTop: '10px' } }, h(WorkspaceGraph, { workspaces: wsData.workspaces, graph: wsData.graph, onSelect: function () {} }))
895
+ : h(Loading, { label: locale === 'zh' ? '正在生成跨工作区总结…' : 'Generating workspace overview…' }))),
784
896
  state.pendingReflection
785
897
  ? h(Banner, null, t('pendingReflection') + state.pendingReflection + t('pendingReflectionHint'))
786
898
  : null,
@@ -822,9 +934,16 @@ window.__ModuleLoader__.load({
822
934
  var contentPair = useState('')
823
935
  var content = contentPair[0]
824
936
  var setContent = contentPair[1]
937
+ var userPair = useState('')
938
+ var userText = userPair[0]
939
+ var setUserText = userPair[1]
940
+ var userTruncPair = useState(false)
941
+ var userTruncated = userTruncPair[0]
942
+ var setUserTruncated = userTruncPair[1]
825
943
  useEffect(function () {
826
944
  var alive = true
827
945
  apiGet(API.list).then(function (d) { if (alive) setData(d) }).catch(function () {})
946
+ apiGet(API.state).then(function (s) { if (alive && s) { if (typeof s.userText === 'string') setUserText(s.userText); setUserTruncated(!!s.userTextTruncated) } }).catch(function () {})
828
947
  return function () { alive = false }
829
948
  }, [])
830
949
  useEffect(function () {
@@ -847,6 +966,11 @@ window.__ModuleLoader__.load({
847
966
  h('button', { 'data-dam-btn': '', onClick: function () { setOpen(log.date + '.md') } }, t('view'))))
848
967
  })(data.logs[i])
849
968
  }
969
+ // 三级记忆之用户级:当日日志下方展示用户画像(跨项目规则/偏好)
970
+ rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px' } }, t('userMemoryBlock')))
971
+ rows.push(h(Card, { title: 'MEMORY.md (' + (locale === 'zh' ? '用户级' : 'user-level') + ')' },
972
+ h('div', { 'data-dam-content': '' }, userText || t('userMemoryEmpty')),
973
+ userTruncated ? h('div', { 'data-dam-hint': '' }, t('userMemoryTruncated')) : null))
850
974
  }
851
975
  return h('div', null, rows)
852
976
  }
@@ -977,16 +1101,99 @@ window.__ModuleLoader__.load({
977
1101
  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() } }),
978
1102
  h('button', { 'data-dam-btn': '', onClick: search, disabled: busy }, t('searchBtn')),
979
1103
  h('button', { 'data-dam-btn': '', onClick: smartSearch, disabled: busy }, t('smartSearch'))),
1104
+ busy ? h(Loading, { label: locale === 'zh' ? 'AI 正在分析记忆…' : 'AI is analyzing memory…' }) : null,
980
1105
  smart ? h(Card, { title: t('smartAnswer') },
981
1106
  h('div', { 'data-dam-content': '' }, smart.answer || t('empty')),
982
1107
  smart.keywords && smart.keywords.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } }, t('keywordsLabel') + smart.keywords.join(' / ')) : null,
983
- smart.hits && smart.hits.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
984
- smart.hits.map(function (h, i) { return h('div', { key: i, style: { marginTop: '3px' } }, '· [' + h.where + '] ' + h.line) })) : null)
1108
+ Array.isArray(smart.hits) && smart.hits.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
1109
+ 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)
985
1110
  : null,
986
1111
  result ? h(Card, { title: t('resultTitle') }, h('div', { 'data-dam-content': '' }, result)) : null)
987
1112
  }
988
1113
 
989
1114
  // ───────────────────────── 工作区总览页签(跨工作区全局总结) ─────────────────────────
1115
+ function graphKeywords(text) {
1116
+ return String(text || '').toLowerCase().split(/[\s,,。::;;/|()[\]{}]+/).filter(function (x) { return x.length >= 3 }).slice(0, 18)
1117
+ }
1118
+ function WorkspaceGraph(props) {
1119
+ var workspaces = props.workspaces || []
1120
+ var panPair = useState({ x: 0, y: 0 })
1121
+ var pan = panPair[0]
1122
+ var setPan = panPair[1]
1123
+ var dragPair = useState(false)
1124
+ var panning = dragPair[0]
1125
+ var setPanning = dragPair[1]
1126
+ var startPair = useState(null)
1127
+ var dragStart = startPair[0]
1128
+ var setDragStart = startPair[1]
1129
+ function pointerDown(ev) {
1130
+ if (ev.button !== 0 || (ev.target && ev.target.closest && ev.target.closest('[data-dam-graph-node]'))) return
1131
+ ev.preventDefault()
1132
+ try { if (ev.currentTarget.setPointerCapture) ev.currentTarget.setPointerCapture(ev.pointerId) } catch (e) {}
1133
+ setPanning(true)
1134
+ setDragStart({ x: ev.clientX, y: ev.clientY, px: pan.x, py: pan.y, moved: false, pointerId: ev.pointerId })
1135
+ }
1136
+ function pointerMove(ev) {
1137
+ if (!panning || !dragStart || ev.pointerId !== dragStart.pointerId) return
1138
+ var dx = ev.clientX - dragStart.x, dy = ev.clientY - dragStart.y
1139
+ if (!dragStart.moved && Math.abs(dx) + Math.abs(dy) < 3) return
1140
+ if (!dragStart.moved) { dragStart = Object.assign({}, dragStart, { moved: true }); setDragStart(dragStart) }
1141
+ setPan({ x: dragStart.px + dx, y: dragStart.py + dy })
1142
+ }
1143
+ function pointerUp(ev) {
1144
+ if (!panning || !dragStart || (ev && ev.pointerId !== dragStart.pointerId)) return
1145
+ try { if (ev.currentTarget.releasePointerCapture) ev.currentTarget.releasePointerCapture(dragStart.pointerId) } catch (e) {}
1146
+ setPanning(false); setDragStart(null)
1147
+ }
1148
+ var onSelect = props.onSelect || function () {}
1149
+ var graph = props.graph || {}
1150
+ var scale = Number(props.scale) || 1.2
1151
+ var compact = graphDensity === 'compact'
1152
+ var columns = Math.min(2, Math.max(1, workspaces.length))
1153
+ var rows = Math.ceil(workspaces.length / columns)
1154
+ var width = 920
1155
+ var colGap = 420
1156
+ var rowGap = compact ? 220 : 270
1157
+ var height = Math.max(420, rows * rowGap + 80)
1158
+ var centers = workspaces.map(function (ws, i) {
1159
+ var col = i % columns, row = Math.floor(i / columns)
1160
+ return { ws: ws, x: 160 + col * colGap, y: 72 + row * rowGap, i: i }
1161
+ })
1162
+ var edges = []
1163
+ ;(graph.links || []).forEach(function (link) {
1164
+ var from = centers.find(function (c) { return c.ws.name === link.from })
1165
+ var to = centers.find(function (c) { return c.ws.name === link.to })
1166
+ if (from && to) edges.push([from, to, link.label || ''])
1167
+ })
1168
+ var children = []
1169
+ centers.forEach(function (c) {
1170
+ var topics = c.ws.graphTopics && c.ws.graphTopics.length ? c.ws.graphTopics : (c.ws.items || []).map(function (x) { return { label: x, detail: '' } })
1171
+ topics.slice(0, compact ? 3 : 4).forEach(function (topic, j) {
1172
+ var col = j % 2, row = Math.floor(j / 2)
1173
+ children.push({ center: c, item: topic, x: c.x - 105 + col * 210, y: c.y + 82 + row * 56, j: j })
1174
+ })
1175
+ })
1176
+ return h('div', { 'data-dam-graph': '', 'data-panning': panning ? 'true' : undefined, onPointerDown: pointerDown, onPointerMove: pointerMove, onPointerUp: pointerUp, onPointerCancel: pointerUp, onLostPointerCapture: pointerUp },
1177
+ 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' } },
1178
+ h('g', { transform: 'translate(' + (pan.x / scale) + ' ' + (pan.y / scale) + ')' },
1179
+ h('g', null, edges.map(function (e, i) {
1180
+ 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)
1181
+ })),
1182
+ h('g', null, children.map(function (n, i) {
1183
+ return h('g', { key: 'branch-' + i },
1184
+ 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 }),
1185
+ 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 }),
1186
+ 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)),
1187
+ h('text', { x: n.x, y: n.y + 10, textAnchor: 'middle', fontSize: 8, fill: '#7d8793' }, String(n.item && n.item.detail || '').slice(0, 26)))
1188
+ })),
1189
+ h('g', null, centers.map(function (c) {
1190
+ return h('g', { key: c.ws.path, 'data-dam-graph-node': '', onPointerDown: function (ev) { ev.stopPropagation() }, onClick: function () { onSelect(c.ws) } },
1191
+ 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 }),
1192
+ 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))),
1193
+ h('text', { x: c.x, y: c.y + 15, textAnchor: 'middle', fontSize: 8, fill: '#7d8793' }, (c.ws.logCount || 0) + ' ' + t('logEntries')))
1194
+ }))))
1195
+ )
1196
+ }
990
1197
  function WorkspaceTab() {
991
1198
  var dataPair = useState(null)
992
1199
  var data = dataPair[0]
@@ -1001,20 +1208,32 @@ window.__ModuleLoader__.load({
1001
1208
  .catch(function () { setBusy(false) })
1002
1209
  }
1003
1210
  useEffect(function () { load(false) }, [])
1004
- if (!data) return h('div', null, busy ? h('div', { 'data-dam-hint': '' }, t('wsGenerating')) : h(Loading))
1211
+ var selectedPair = useState(null)
1212
+ var selected = selectedPair[0]
1213
+ var setSelected = selectedPair[1]
1214
+ var collapsedPair = useState({})
1215
+ var collapsedByPath = collapsedPair[0]
1216
+ var setCollapsedByPath = collapsedPair[1]
1217
+ var graphScalePair = useState(1.0)
1218
+ var graphScale = graphScalePair[0]
1219
+ var setGraphScale = graphScalePair[1]
1220
+ if (!data) return h('div', null, busy ? h(Loading, { label: locale === 'zh' ? 'AI 正在整理工作区…' : 'AI is mapping workspaces…' }) : h(Loading))
1221
+ var workspaces = data.workspaces || []
1005
1222
  return h('div', null,
1006
1223
  h('div', { 'data-dam-row': '' },
1007
1224
  h('button', { 'data-dam-btn': '', onClick: function () { load(true) }, disabled: busy }, busy ? t('refreshing') : t('refresh')),
1008
1225
  h('span', { 'data-dam-hint': '' }, data.cached ? (t('generatedAt') + new Date(data.generatedAt).toLocaleTimeString()) : '')),
1009
- (data.workspaces && data.workspaces.length) ? data.workspaces.map(function (ws) {
1010
- return h(Card, { key: ws.path, title: ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '') },
1011
- ws.summary ? h('div', { 'data-dam-content': '' }, ws.summary) : h('div', { 'data-dam-hint': '' }, t('wsNoSummary')),
1012
- (ws.items && ws.items.length) ? h('div', { style: { marginTop: '6px' } },
1013
- ws.items.map(function (it, i) {
1014
- return h('div', { key: i, style: { fontSize: 'calc(12px * var(--dam-scale))', marginTop: '3px', lineHeight: 1.5 } }, ' + it)
1015
- })) : null,
1016
- h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, ws.path + ' · ' + (ws.logCount || 0) + t('logEntries')))
1017
- }) : h('div', { 'data-dam-hint': '' }, t('wsNone')))
1226
+ workspaces.length ? h('div', null,
1227
+ h('div', { 'data-dam-hint': '', style: { margin: '5px 0 8px' } }, locale === 'zh' ? '中心节点代表工作区;分支为记忆主题;虚线表示共享主题。' : 'Centers are workspaces, branches are topics, and dashed links show shared themes.'),
1228
+ 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) } }, '⌂')),
1229
+ h('div', { 'data-dam-hint': '' }, locale === 'zh' ? '点击工作区卡片查看摘要;拖动滑块调整图的大小。' : 'Select a workspace card for details; use the slider to resize the map.'),
1230
+ 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) } }),
1231
+ h('div', { style: { marginTop: '12px' } }, workspaces.map(function (ws) {
1232
+ var open = collapsedByPath[ws.path] === false
1233
+ return h('div', { key: ws.path, 'data-dam-card': '', style: { marginBottom: '8px', padding: '0' } },
1234
+ 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 : '')),
1235
+ 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')))))
1236
+ })) ) : h('div', { 'data-dam-hint': '' }, t('wsNone')))
1018
1237
  }
1019
1238
 
1020
1239
  // ───────────────────────── 日历页签 ─────────────────────────
@@ -1036,11 +1255,20 @@ window.__ModuleLoader__.load({
1036
1255
  var draftPair = useState(null)
1037
1256
  var draft = draftPair[0] // {date, time, quadrant, title}
1038
1257
  var setDraft = draftPair[1]
1258
+ var dayViewPair = useState(null)
1259
+ var dayView = dayViewPair[0]
1260
+ var setDayView = dayViewPair[1]
1039
1261
  var msgPair = useState('')
1040
1262
  var msg = msgPair[0]
1041
1263
  var setMsg = msgPair[1]
1264
+ var savingPair = useState(false)
1265
+ var saving = savingPair[0]
1266
+ var setSaving = savingPair[1]
1267
+ var calErrorPair = useState('')
1268
+ var calError = calErrorPair[0]
1269
+ var setCalError = calErrorPair[1]
1042
1270
  function load() {
1043
- apiGet(API.calendar).then(function (d) { if (d) setData(d) }).catch(function () {})
1271
+ apiGet(API.calendar).then(function (d) { if (d) { setData(d); setCalError('') } }).catch(function (e) { setCalError(t('failed') + e.message) })
1044
1272
  }
1045
1273
  useEffect(function () {
1046
1274
  load()
@@ -1073,7 +1301,7 @@ window.__ModuleLoader__.load({
1073
1301
  h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(-1) } }, '◀'),
1074
1302
  h('span', { style: { fontWeight: 700, flex: 1, textAlign: 'center' } }, month.year + ' / ' + String(month.mon).padStart(2, '0')),
1075
1303
  h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(1) } }, '▶'),
1076
- h('button', { 'data-dam-btn': '', onClick: function () { setDraft({ date: today, time: '09:00', quadrant: '重要不紧急', title: '' }) } }, '+ ' + t('addItem'))))
1304
+ h('button', { 'data-dam-btn': '', onClick: function () { setDraft({ date: today, time: '09:00', quadrant: '重要不紧急', title: '', location: '', reminder: '', note: '' }) } }, '+ ' + t('addItem'))))
1077
1305
  // 图例
1078
1306
  rows.push(h('div', { 'data-dam-row': '', style: { flexWrap: 'wrap' } },
1079
1307
  Object.keys(QUADRANT_STYLE).map(function (q) {
@@ -1095,7 +1323,8 @@ window.__ModuleLoader__.load({
1095
1323
  var dayNum = Number(cell.slice(8))
1096
1324
  week.push(h('div', {
1097
1325
  key: cell,
1098
- onClick: function () { setDraft({ date: cell, time: '09:00', quadrant: '重要不紧急', title: '' }) },
1326
+ 'data-dam-calendar-day': '',
1327
+ onClick: function () { setDayView(cell) },
1099
1328
  style: {
1100
1329
  minHeight: '64px', padding: '4px', cursor: 'pointer', borderRadius: '8px',
1101
1330
  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)'),
@@ -1108,6 +1337,7 @@ window.__ModuleLoader__.load({
1108
1337
  var qs = QUADRANT_STYLE[en.quadrant] || QUADRANT_STYLE['未分类']
1109
1338
  return h('div', {
1110
1339
  key: en.time + en.title,
1340
+ 'data-dam-calendar-event': '',
1111
1341
  title: en.time + ' ' + en.title,
1112
1342
  onClick: function (ev) { ev.stopPropagation(); toggleDone(en) },
1113
1343
  style: {
@@ -1125,19 +1355,38 @@ window.__ModuleLoader__.load({
1125
1355
  }
1126
1356
  rows.push(h('div', null, gridRows))
1127
1357
  function toggleDone(en) {
1358
+ if (saving) return
1359
+ setSaving(true); setCalError('')
1128
1360
  apiPost(API.calendar, { action: en.done ? 'remove' : 'done', date: en.date, time: en.time, title: en.title }).then(function (d) {
1129
- setMsg(d.result || ''); load()
1130
- }).catch(function (e) { setMsg(t('failed') + e.message) })
1361
+ setMsg(d.result || ''); setSaving(false); load()
1362
+ }).catch(function (e) { setSaving(false); setCalError(t('failed') + e.message) })
1131
1363
  }
1132
1364
  function saveDraft() {
1133
- if (!draft || !draft.title.trim()) { setMsg(t('needTitle')); return }
1134
- apiPost(API.calendar, { date: draft.date, time: draft.time, quadrant: draft.quadrant, title: draft.title.trim(), note: draft.note || '' }).then(function (d) {
1135
- setMsg(d.result || t('saved')); setDraft(null); load()
1136
- }).catch(function (e) { setMsg(t('failed') + e.message) })
1365
+ if (!draft || !draft.title.trim()) { setCalError(t('needTitle')); return }
1366
+ if (saving) return
1367
+ setSaving(true); setCalError('')
1368
+ 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) {
1369
+ setMsg(d.result || t('saved')); setSaving(false); setDraft(null); load()
1370
+ }).catch(function (e) { setSaving(false); setCalError(t('failed') + e.message) })
1371
+ }
1372
+ // 日期格先打开当天时间轴;点击某个时间槽才创建事件。
1373
+ if (dayView) {
1374
+ var timeline = []
1375
+ for (var hourSlot = 7; hourSlot <= 22; hourSlot++) {
1376
+ (function (hourValue) {
1377
+ var hourText = String(hourValue).padStart(2, '0') + ':00'
1378
+ var hourEvents = dayEntries(dayView).filter(function (en) { return String(en.time || '').slice(0, 2) === String(hourValue).padStart(2, '0') })
1379
+ timeline.push(h('div', { key: hourText, 'data-dam-calendar-slot': '', onClick: function () { setDayView(null); setDraft({ date: dayView, time: hourText, quadrant: '重要不紧急', title: '', location: '', reminder: '', note: '' }) } },
1380
+ h('div', { style: { flex: '0 0 50px', fontSize: 'calc(11px * var(--dam-scale))', opacity: .65 } }, hourText),
1381
+ 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'))))
1382
+ })(hourSlot)
1383
+ }
1384
+ 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))
1137
1385
  }
1138
1386
  // 添加/编辑浮层(液态玻璃)
1139
1387
  if (draft) {
1140
1388
  rows.push(h('div', {
1389
+ 'data-dam-calendar-modal': '',
1141
1390
  style: {
1142
1391
  position: 'absolute', inset: 0, zIndex: 10, borderRadius: '16px',
1143
1392
  background: 'color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.9)) 70%, transparent)',
@@ -1152,12 +1401,17 @@ window.__ModuleLoader__.load({
1152
1401
  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) } }),
1153
1402
  h('select', { 'data-dam-select': '', value: draft.quadrant, onChange: function (e) { var n = Object.assign({}, draft); n.quadrant = e.target.value; setDraft(n) } },
1154
1403
  ['重要紧急', '重要不紧急', '紧急不重要', '不重要不紧急'].map(function (q) { return h('option', { key: q, value: q }, t(QUADRANT_I18N[q])) }))),
1404
+ 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) } }),
1405
+ 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) } }),
1406
+ 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) } }),
1155
1407
  h('div', { 'data-dam-row': '' },
1156
- h('button', { 'data-dam-btn': '', onClick: saveDraft }, t('save')),
1157
- h('button', { 'data-dam-btn': '', onClick: function () { setDraft(null) } }, t('cancel')))))
1408
+ h('button', { 'data-dam-btn': '', onClick: saveDraft, disabled: saving }, saving ? (locale === 'zh' ? '保存中…' : 'Saving…') : t('save')),
1409
+ h('button', { 'data-dam-btn': '', onClick: function () { if (!saving) setDraft(null) }, disabled: saving }, t('cancel')),
1410
+ calError ? h('div', { 'data-dam-error': '' }, calError) : null)))
1158
1411
  }
1159
- msg ? h('div', { 'data-dam-hint': '' }, msg) : null
1160
- return h('div', { style: { position: 'relative' } }, rows)
1412
+ if (msg) rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '8px' } }, msg))
1413
+ if (calError && !draft) rows.push(h('div', { 'data-dam-error': '' }, calError))
1414
+ return h('div', { 'data-dam-calendar': '', style: { position: 'relative' } }, rows)
1161
1415
  }
1162
1416
 
1163
1417
  var TOOL_LABEL = { workbuddy: t('aiAssistant'), codebuddy: 'CodeBuddy', claude: 'Claude Code', codex: 'Codex', 'project-files': t('projectFiles') }
@@ -1174,10 +1428,25 @@ window.__ModuleLoader__.load({
1174
1428
  var errPair = useState('')
1175
1429
  var err = errPair[0]
1176
1430
  var setErr = errPair[1]
1431
+ var expandedPair = useState(null)
1432
+ var expanded = expandedPair[0]
1433
+ var setExpanded = expandedPair[1]
1434
+ var viewCachePair = useState({})
1435
+ var viewCache = viewCachePair[0]
1436
+ var setViewCache = viewCachePair[1]
1437
+ var removeArmPair = useState('')
1438
+ var removeArm = removeArmPair[0]
1439
+ var setRemoveArm = removeArmPair[1]
1440
+ var hitsCachePair = useState({})
1441
+ var hitsCache = hitsCachePair[0]
1442
+ var setHitsCache = hitsCachePair[1]
1443
+ var hitsBusyForPair = useState(null)
1444
+ var hitsBusyFor = hitsBusyForPair[0]
1445
+ var setHitsBusyFor = hitsBusyForPair[1]
1177
1446
  function load() {
1178
1447
  apiGet(API.external).then(function (d) { setSources(d.sources || []) }).catch(function (e) { setErr(e.message) })
1179
1448
  }
1180
- useEffect(function () { load(); var t = setInterval(load, 60000); return function () { clearInterval(t) } }, [])
1449
+ useEffect(function () { load() }, [])
1181
1450
  if (!sources) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
1182
1451
  function doImport(source, target) {
1183
1452
  var next = Object.assign({}, busy); next[source] = true
@@ -1185,8 +1454,44 @@ window.__ModuleLoader__.load({
1185
1454
  apiPost(API.externalImport, { source: source, target: target }).then(function (d) {
1186
1455
  var done = Object.assign({}, busy); done[source] = false
1187
1456
  setBusy(done); setMsg(d.result || t('imported'))
1457
+ setSources(function (prev) { return (prev || []).map(function (x) { return x.id === source ? Object.assign({}, x, target === 'user' ? { importedUser: true } : { importedNotes: true }) : x }) })
1458
+ load()
1188
1459
  }).catch(function (e) { var done = Object.assign({}, busy); done[source] = false; setBusy(done); setErr(e.message) })
1189
1460
  }
1461
+ function toggleView(source) {
1462
+ if (expanded === source.id) { setExpanded(null); return }
1463
+ setExpanded(source.id); setRemoveArm('')
1464
+ if (viewCache[source.id] !== undefined) return
1465
+ var n = Object.assign({}, viewCache); n[source.id] = { source: source, loading: true }
1466
+ setViewCache(n)
1467
+ apiGet(API.externalView, { source: source.id }).then(function (d) {
1468
+ var m = Object.assign({}, viewCache); m[source.id] = Object.assign({ source: source }, d, { loading: false })
1469
+ setViewCache(m)
1470
+ }).catch(function (e) {
1471
+ var m = Object.assign({}, viewCache); m[source.id] = { source: source, loading: false, error: e.message }
1472
+ setViewCache(m)
1473
+ })
1474
+ }
1475
+ function removeImported(source, target) {
1476
+ var key = source.id + ':' + (target || '')
1477
+ if (removeArm !== key) { setRemoveArm(key); setMsg(''); return }
1478
+ setRemoveArm('')
1479
+ apiPost(API.externalRemove, { source: source.id, target: target }).then(function (d) {
1480
+ setMsg(d.result || ''); setExpanded(null); load()
1481
+ 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 }) })
1482
+ }).catch(function (e) { setErr(t('failed') + e.message) })
1483
+ }
1484
+ function findImported(source) {
1485
+ if (!source) return
1486
+ setHitsBusyFor(source.id)
1487
+ apiPost(API.recall, { query: source.name || source.tool || '' }).then(function (d) {
1488
+ var n = Object.assign({}, hitsCache); n[source.id] = d && d.result ? d.result : (locale === 'zh' ? '未找到相关片段' : 'No matches')
1489
+ setHitsCache(n); setHitsBusyFor(null)
1490
+ }).catch(function (e) {
1491
+ var n = Object.assign({}, hitsCache); n[source.id] = t('failed') + e.message
1492
+ setHitsCache(n); setHitsBusyFor(null)
1493
+ })
1494
+ }
1190
1495
  function importAll() {
1191
1496
  var md = sources.filter(function (s) { return s.kind !== 'sessions' && s.enabled !== false })
1192
1497
  setMsg(t('importing') + md.length + t('importingSuffix')); setErr('')
@@ -1200,17 +1505,31 @@ window.__ModuleLoader__.load({
1200
1505
  }
1201
1506
  for (var i = 0; i < sources.length; i++) {
1202
1507
  (function (s) {
1508
+ var isOpen = expanded === s.id
1509
+ var v = viewCache[s.id]
1203
1510
  var actions = []
1511
+ 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')))
1204
1512
  if (s.kind === 'sessions') {
1205
1513
  actions.push(h('span', { 'data-dam-hint': '' }, t('sessionSource') + s.fileCount + t('sessionSourceSuffix')))
1206
1514
  } else {
1207
- actions.push(h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'project') } }, busy[s.id] ? t('importingOne') : t('importToNotes')))
1208
- actions.push(h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'user') } }, t('importToUser')))
1515
+ 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')))
1516
+ 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')))
1517
+
1209
1518
  }
1210
- cards.push(h(Card, { title: s.name + ' · ' + s.tool + (s.enabled === false ? t('disabled') : '') },
1211
- h('div', { 'data-dam-hint': '' }, s.kind + ' · ' + s.fileCount + t('filesCount') + fmtSize(s.size)),
1212
- s.preview ? h('div', { 'data-dam-content': '' }, s.preview) : null,
1213
- h('div', { 'data-dam-row': '' }, actions)))
1519
+ cards.push(h(Card, { key: s.id, title: s.name + ' · ' + s.tool + (s.enabled === false ? t('disabled') : '') },
1520
+ h('div', { 'data-dam-row': '', style: { marginBottom: '2px' } }, actions),
1521
+ h(AnimatedDisclosure, { open: isOpen },
1522
+ v && v.loading ? h(Loading, { label: locale === 'zh' ? '正在读取来源…' : 'Reading source…' })
1523
+ : v && v.error ? h('div', { 'data-dam-error': '' }, v.error)
1524
+ : v ? h('div', null,
1525
+ h('div', { 'data-dam-content': '' }, v.content || t('empty')),
1526
+ v.imported !== undefined ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
1527
+ (locale === 'zh' ? '已接入位置:' : 'Imported at: ') + (v.imported ? (v.locations || []).join(' ; ') : (locale === 'zh' ? '尚未接入' : 'Not imported'))) : null,
1528
+ h('div', { 'data-dam-row': '', style: { marginTop: '8px' } },
1529
+ h('button', { 'data-dam-btn': '', onClick: function () { findImported(s) } }, hitsBusyFor === s.id ? (locale === 'zh' ? '检索中…' : 'Searching…') : (locale === 'zh' ? '在记忆中查找' : 'Find in memory'))),
1530
+ hitsCache[s.id] ? h('div', { 'data-dam-content': '', style: { marginTop: '6px' } }, String(hitsCache[s.id])) : null,
1531
+ 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.'))
1532
+ : null)))
1214
1533
  })(sources[i])
1215
1534
  }
1216
1535
  return h('div', null,
@@ -1253,6 +1572,52 @@ window.__ModuleLoader__.load({
1253
1572
  }
1254
1573
  }
1255
1574
 
1575
+ function TabScroller(props) {
1576
+ var tabs = props.tabs || []
1577
+ var tab = props.tab
1578
+ var setTab = props.setTab
1579
+ var viewportRef = useRef(null)
1580
+ var canLeftPair = useState(false)
1581
+ var canLeft = canLeftPair[0]
1582
+ var setCanLeft = canLeftPair[1]
1583
+ var canRightPair = useState(false)
1584
+ var canRight = canRightPair[0]
1585
+ var setCanRight = canRightPair[1]
1586
+ function refreshArrows() {
1587
+ var vp = viewportRef.current
1588
+ if (!vp) return
1589
+ setCanLeft(vp.scrollLeft > 2)
1590
+ setCanRight(vp.scrollLeft + vp.clientWidth < vp.scrollWidth - 2)
1591
+ }
1592
+ useEffect(function () {
1593
+ var vp = viewportRef.current
1594
+ if (!vp) return
1595
+ refreshArrows()
1596
+ vp.addEventListener('scroll', refreshArrows)
1597
+ var ro = null
1598
+ if (typeof ResizeObserver !== 'undefined') { try { ro = new ResizeObserver(refreshArrows); ro.observe(vp) } catch (e) {} }
1599
+ return function () { vp.removeEventListener('scroll', refreshArrows); if (ro) ro.disconnect() }
1600
+ }, [tabs.length])
1601
+ useEffect(function () {
1602
+ var vp = viewportRef.current
1603
+ if (!vp) return
1604
+ var activeEl = vp.querySelector('[data-active="true"]')
1605
+ if (!activeEl) return
1606
+ var l = activeEl.offsetLeft, w = activeEl.offsetWidth
1607
+ if (l < vp.scrollLeft) vp.scrollTo({ left: Math.max(0, l - 8), behavior: 'smooth' })
1608
+ else if (l + w > vp.scrollLeft + vp.clientWidth) vp.scrollTo({ left: l + w - vp.clientWidth + 8, behavior: 'smooth' })
1609
+ }, [tab])
1610
+ function step(dir) {
1611
+ var vp = viewportRef.current
1612
+ if (!vp) return
1613
+ try { vp.scrollBy({ left: dir * Math.max(90, vp.clientWidth * 0.6), behavior: 'smooth' }) } catch (e) { vp.scrollLeft += dir * 90 }
1614
+ }
1615
+ return h('div', { 'data-dam-tabs-wrap': '' },
1616
+ 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) } }, '‹'),
1617
+ 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]) }))),
1618
+ 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) } }, '›'))
1619
+ }
1620
+
1256
1621
  function MemoryPanel() {
1257
1622
  var tick = useTick()
1258
1623
  var tabPair = useState('overview')
@@ -1281,6 +1646,7 @@ window.__ModuleLoader__.load({
1281
1646
  width: g.width + 'px',
1282
1647
  height: g.height + 'px',
1283
1648
  '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
1649
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
1284
1650
  }
1285
1651
  var dragMove = startPointerDrag(function (dx, dy) {
1286
1652
  controller.setGeom({ left: g.left + dx, top: g.top + dy })
@@ -1300,13 +1666,12 @@ window.__ModuleLoader__.load({
1300
1666
  onPointerDown: dragMove,
1301
1667
  },
1302
1668
  h('strong', null, t('autoMemory')),
1669
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, 'v0.1.23-ui'),
1303
1670
  h('span', { className: 'dam-spacer' }),
1304
1671
  h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
1305
1672
  h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
1306
1673
  h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
1307
- h('div', { 'data-dam-tabs': '' }, tabs.map(function (t) {
1308
- return h('button', { key: t[0], 'data-dam-tab': '', 'data-active': tab === t[0] ? 'true' : undefined, onClick: function () { setTab(t[0]) } }, t[1])
1309
- })),
1674
+ h(TabScroller, { tabs: tabs, tab: tab, setTab: setTab }),
1310
1675
  h('div', { 'data-dam-body': '' }, body),
1311
1676
  h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove }))
1312
1677
  }
@@ -1483,9 +1848,15 @@ window.__ModuleLoader__.load({
1483
1848
  var errPair = useState('')
1484
1849
  var err = errPair[0]
1485
1850
  var setErr = errPair[1]
1851
+ var dirtyPair = useState(false)
1852
+ var dirty = dirtyPair[0]
1853
+ var setDirty = dirtyPair[1]
1486
1854
  var dbgOpenPair = useState(false)
1487
1855
  var dbgOpen = dbgOpenPair[0]
1488
1856
  var setDbgOpen = dbgOpenPair[1]
1857
+ var settingsSectionPair = useState('appearance')
1858
+ var settingsSection = settingsSectionPair[0]
1859
+ var setSettingsSection = settingsSectionPair[1]
1489
1860
  var browseOpenPair = useState(false)
1490
1861
  var browseOpen = browseOpenPair[0]
1491
1862
  var setBrowseOpen = browseOpenPair[1]
@@ -1544,7 +1915,7 @@ window.__ModuleLoader__.load({
1544
1915
  return function () { alive = false }
1545
1916
  }, [])
1546
1917
  if (!cfg) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
1547
- function set(key, value) { var next = Object.assign({}, cfg); next[key] = value; setCfg(next) }
1918
+ function set(key, value) { var next = Object.assign({}, cfg); next[key] = value; setCfg(next); setDirty(true) }
1548
1919
  function checkUpdate() {
1549
1920
  if (checkingUpdate) return
1550
1921
  setCheckingUpdate(true)
@@ -1563,21 +1934,51 @@ window.__ModuleLoader__.load({
1563
1934
  function save() {
1564
1935
  if (busy) return
1565
1936
  setBusy(true); setMsg(''); setErr('')
1566
- 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) })
1937
+ 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) })
1567
1938
  .catch(function (e) { setErr(e.message); setBusy(false) })
1568
1939
  }
1569
1940
  function field(label, control, hint) {
1570
- return h('div', null,
1941
+ return h('div', { 'data-dam-settings-row': '' },
1571
1942
  h('div', { 'data-dam-row': '' }, h('label', null, label), control),
1572
1943
  hint ? h('div', { 'data-dam-hint': '' }, hint) : null)
1573
1944
  }
1574
- return h('div', null,
1575
- h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
1576
- field(t('fLocale'), h('select', { 'data-dam-select': '', value: cfg.locale || 'system', onChange: function (e) { set('locale', e.target.value) } },
1577
- LOCALE_IDS_LIST.map(function (id) { return h('option', { key: id, value: id }, id === 'system' ? t('followSystem') : t(id)) })), t('fLocaleHint')),
1578
- 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() } },
1579
- 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')),
1580
- field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
1945
+ function setAccent(value) {
1946
+ accentTheme = ACCENT_VALUES[value] ? value : 'deepseek'
1947
+ try { localStorage.setItem('dsh-auto-memory.accentTheme.v1', accentTheme) } catch (e) {}
1948
+ emit()
1949
+ }
1950
+ function setDensity(value) {
1951
+ graphDensity = value === 'compact' ? 'compact' : 'relaxed'
1952
+ try { localStorage.setItem('dsh-auto-memory.graphDensity.v1', graphDensity) } catch (e) {}
1953
+ emit()
1954
+ }
1955
+ var sectionLabels = {
1956
+ appearance: locale === 'zh' ? '外观' : 'Appearance', storage: locale === 'zh' ? '存储' : 'Storage',
1957
+ injection: locale === 'zh' ? '记忆窗口' : 'Memory window', automation: locale === 'zh' ? '自动化' : 'Automation',
1958
+ maintenance: locale === 'zh' ? '维护' : 'Maintenance'
1959
+ }
1960
+ function section(key, title, content) { return h('section', { id: 'dam-settings-' + key, 'data-dam-settings-group': '' }, h('h3', null, title), content) }
1961
+ function jumpToSection(key) {
1962
+ setSettingsSection(key)
1963
+ try { var el = document.getElementById('dam-settings-' + key); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }) } catch (e) {}
1964
+ }
1965
+ return h('div', { 'data-dam-settings': '' },
1966
+ h('nav', { 'data-dam-settings-nav': '', 'aria-label': locale === 'zh' ? '设置分组' : 'Settings sections' }, Object.keys(sectionLabels).map(function (key) {
1967
+ return h('button', { key: key, 'data-dam-btn': '', 'data-active': settingsSection === key ? 'true' : undefined, onClick: function () { jumpToSection(key) } }, sectionLabels[key])
1968
+ })),
1969
+ h('div', { 'data-dam-settings-content': '' },
1970
+ section('appearance', sectionLabels.appearance, [
1971
+ h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
1972
+ field(t('fLocale'), h('select', { 'data-dam-select': '', value: cfg.locale || 'system', onChange: function (e) { set('locale', e.target.value) } },
1973
+ LOCALE_IDS_LIST.map(function (id) { return h('option', { key: id, value: id }, id === 'system' ? t('followSystem') : t(id)) })), t('fLocaleHint')),
1974
+ 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() } },
1975
+ 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')),
1976
+ field(locale === 'zh' ? '强调色' : 'Accent color', h('select', { 'data-dam-select': '', value: accentTheme, onChange: function (e) { setAccent(e.target.value) } },
1977
+ 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.'),
1978
+ field(locale === 'zh' ? '关系图密度' : 'Graph density', h('select', { 'data-dam-select': '', value: graphDensity, onChange: function (e) { setDensity(e.target.value) } },
1979
+ 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.')
1980
+ ]),
1981
+ 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')),
1581
1982
  field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
1582
1983
  field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
1583
1984
  h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
@@ -1593,30 +1994,36 @@ window.__ModuleLoader__.load({
1593
1994
  h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
1594
1995
  h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
1595
1996
  h('button', { 'data-dam-btn': '', onClick: function () { setBrowseOpen(false) } }, t('close'))))
1596
- : null,
1597
- field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
1997
+ : null]),
1998
+ section('injection', sectionLabels.injection, [field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
1598
1999
  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')),
1599
- 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')),
2000
+ 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')),
2001
+ 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'))]),
2002
+ 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')),
2003
+ field(t('fAutoConsolidate'), h('input', { type: 'checkbox', checked: cfg.autoConsolidate !== false, onChange: function (e) { set('autoConsolidate', e.target.checked) } }), t('fAutoConsolidateHint')),
2004
+ 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')),
2005
+ 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')),
1600
2006
  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')),
1601
2007
  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')),
1602
2008
  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')),
1603
2009
  field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
1604
2010
  field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
1605
- STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
1606
- field(t('fVersion'), h('div', { 'data-dam-row': '' },
2011
+ STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint'))]),
2012
+ section('maintenance', sectionLabels.maintenance, [field(t('fVersion'), h('div', { 'data-dam-row': '' },
1607
2013
  h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
1608
2014
  h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
1609
2015
  (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,
1610
2016
  upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
1611
- t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
1612
- h('div', { 'data-dam-row': '' },
1613
- h('button', { 'data-dam-btn': '', onClick: save, disabled: busy }, busy ? t('saving') : t('saveSettings')),
1614
- msg ? h('span', null, msg) : null),
2017
+ t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : ''))]),
2018
+ h('div', { 'data-dam-savebar': '' },
2019
+ h('button', { 'data-dam-btn': '', 'data-dirty': dirty ? 'true' : undefined, onClick: save, disabled: busy }, busy ? t('saving') : (dirty ? (locale === 'zh' ? '保存更改' : 'Save changes') : t('saveSettings'))),
2020
+ dirty ? h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '有未保存的更改' : 'Unsaved changes') : null,
2021
+ msg ? h('span', { 'data-dam-hint': '' }, msg) : null),
1615
2022
  // 调试中心(折叠):模块状态一览,方便排查问题/提 issue
1616
2023
  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' } },
1617
2024
  h('button', { 'data-dam-btn': '', onClick: function () { setDbgOpen(!dbgOpen) } }, (dbgOpen ? '▴ ' : '▾ ') + t('debugCenter')),
1618
2025
  dbgOpen ? h('div', { style: { marginTop: '8px' } }, h(DebugCenter)) : null),
1619
- err ? h('div', { 'data-dam-error': '' }, err) : null)
2026
+ err ? h('div', { 'data-dam-error': '' }, err) : null))
1620
2027
  }
1621
2028
 
1622
2029
  // ───────────────────────── 插件挂载 ─────────────────────────