@a9i5k4/dsh-auto-memory 0.1.23 → 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()
@@ -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,19 @@ 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
+ ] },
327
362
  '0.1.23': { zh: [
328
363
  '修复:正式发布包首次欢迎文案仍显示“开发版”;发布转换与残留校验已加强,确保开发版和正式版身份完全隔离。',
329
364
  ], en: [
@@ -468,7 +503,10 @@ window.__ModuleLoader__.load({
468
503
  reflectAuto: '/api/dsh-auto-memory/reflect-auto',
469
504
  note: '/api/dsh-auto-memory/note',
470
505
  external: '/api/dsh-auto-memory/external',
506
+ externalView: '/api/dsh-auto-memory/external-view',
507
+ externalRemove: '/api/dsh-auto-memory/external-remove',
471
508
  externalImport: '/api/dsh-auto-memory/external-import',
509
+ calendar: '/api/dsh-auto-memory/calendar',
472
510
  summarize: '/api/dsh-auto-memory/summarize',
473
511
  greet: '/api/dsh-auto-memory/greet',
474
512
  notices: '/api/dsh-auto-memory/notices',
@@ -482,13 +520,13 @@ window.__ModuleLoader__.load({
482
520
  async function apiGet(path, params) {
483
521
  var res = await fetch(path + query(params))
484
522
  var body = await res.json().catch(function () { return {} })
485
- 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))
486
524
  return body
487
525
  }
488
526
  async function apiPost(path, payload) {
489
527
  var res = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
490
528
  var body = await res.json().catch(function () { return {} })
491
- 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))
492
530
  return body
493
531
  }
494
532
 
@@ -526,16 +564,61 @@ window.__ModuleLoader__.load({
526
564
  '[data-dam-resize]:hover { opacity: 1; }',
527
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; }',
528
566
  '[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; }',
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; }',
533
602
  '[data-dam-kv] { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: calc(12.5px * var(--dam-scale)); }',
534
603
  '[data-dam-kv] b { opacity: .55; font-weight: 500; }',
535
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));',
536
605
  ' background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent); }',
537
606
  '[data-dam-card] .dam-date { font-weight: 600; margin-bottom: 4px; }',
538
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; } }',
539
622
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
540
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)); }',
541
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; }',
@@ -547,6 +630,9 @@ window.__ModuleLoader__.load({
547
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); }',
548
631
  '[data-dam-error] { color: var(--dsw-alias-state-error-primary, #d64545); font-size: 12px; margin-top: 6px; white-space: pre-wrap; }',
549
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); } }',
550
636
  '@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {',
551
637
  ' [data-dam-panel] { background: var(--dsw-alias-bg-overlay, #ffffff); } }',
552
638
  ].join('\n')
@@ -573,7 +659,32 @@ window.__ModuleLoader__.load({
573
659
  h('div', { className: 'dam-content' }, props.children))
574
660
  }
575
661
 
576
- 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
+ }
577
688
 
578
689
  // ───────────────────────── 侧边栏入口 ─────────────────────────
579
690
  function SidebarButton() {
@@ -684,7 +795,7 @@ window.__ModuleLoader__.load({
684
795
  h('div', { style: { fontWeight: 600, fontSize: 'calc(12.5px * var(--dam-scale))', lineHeight: 1.5 } }, (isOpen ? '▼ ' : '▶ ') + bigTitle),
685
796
  h('div', { 'data-dam-muted': '', style: { fontSize: 'calc(11px * var(--dam-scale))', marginTop: '2px' } },
686
797
  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' } },
798
+ h(AnimatedDisclosure, { open: isOpen }, h('div', { style: { padding: '4px 2px 2px 8px' } },
688
799
  cached && cached.works && cached.works.length ? cached.works.map(function (w, wi) {
689
800
  var sk = dr.key + ':w' + wi
690
801
  var sOpen = subOpenMap[sk] !== undefined ? subOpenMap[sk] : false
@@ -702,7 +813,7 @@ window.__ModuleLoader__.load({
702
813
  })) : null)
703
814
  }) : cached && cached.failed ? h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summaryFailed'))
704
815
  : 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))
816
+ : h('div', { 'data-dam-hint': '', style: { padding: '4px 6px' } }, t('summarizing'))))))
706
817
  })(drawers[d])
707
818
  }
708
819
  // 待反思提醒
@@ -728,6 +839,9 @@ window.__ModuleLoader__.load({
728
839
  var wsPair = useState(null)
729
840
  var wsData = wsPair[0]
730
841
  var setWsData = wsPair[1]
842
+ var wsOpenPair = useState(false)
843
+ var wsOpen = wsOpenPair[0]
844
+ var setWsOpen = wsOpenPair[1]
731
845
  // 每次进入面板 / 点刷新(nonce 变化)/ 面板打开期间每 30s 自动重拉,保证自动沉淀等数据新鲜
732
846
  useEffect(function () {
733
847
  var alive = true
@@ -768,19 +882,12 @@ window.__ModuleLoader__.load({
768
882
  return h('div', null,
769
883
  // 今日问候卡:问候语 + 昨天时间轴 + 提醒(纯 GUI 渲染,不干扰对话流)
770
884
  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'))),
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…' }))),
784
891
  state.pendingReflection
785
892
  ? h(Banner, null, t('pendingReflection') + state.pendingReflection + t('pendingReflectionHint'))
786
893
  : null,
@@ -822,9 +929,16 @@ window.__ModuleLoader__.load({
822
929
  var contentPair = useState('')
823
930
  var content = contentPair[0]
824
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]
825
938
  useEffect(function () {
826
939
  var alive = true
827
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 () {})
828
942
  return function () { alive = false }
829
943
  }, [])
830
944
  useEffect(function () {
@@ -847,6 +961,11 @@ window.__ModuleLoader__.load({
847
961
  h('button', { 'data-dam-btn': '', onClick: function () { setOpen(log.date + '.md') } }, t('view'))))
848
962
  })(data.logs[i])
849
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))
850
969
  }
851
970
  return h('div', null, rows)
852
971
  }
@@ -977,16 +1096,99 @@ window.__ModuleLoader__.load({
977
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() } }),
978
1097
  h('button', { 'data-dam-btn': '', onClick: search, disabled: busy }, t('searchBtn')),
979
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,
980
1100
  smart ? h(Card, { title: t('smartAnswer') },
981
1101
  h('div', { 'data-dam-content': '' }, smart.answer || t('empty')),
982
1102
  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)
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)
985
1105
  : null,
986
1106
  result ? h(Card, { title: t('resultTitle') }, h('div', { 'data-dam-content': '' }, result)) : null)
987
1107
  }
988
1108
 
989
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
+ }
990
1192
  function WorkspaceTab() {
991
1193
  var dataPair = useState(null)
992
1194
  var data = dataPair[0]
@@ -1001,20 +1203,32 @@ window.__ModuleLoader__.load({
1001
1203
  .catch(function () { setBusy(false) })
1002
1204
  }
1003
1205
  useEffect(function () { load(false) }, [])
1004
- 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 || []
1005
1217
  return h('div', null,
1006
1218
  h('div', { 'data-dam-row': '' },
1007
1219
  h('button', { 'data-dam-btn': '', onClick: function () { load(true) }, disabled: busy }, busy ? t('refreshing') : t('refresh')),
1008
1220
  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')))
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')))
1018
1232
  }
1019
1233
 
1020
1234
  // ───────────────────────── 日历页签 ─────────────────────────
@@ -1036,11 +1250,20 @@ window.__ModuleLoader__.load({
1036
1250
  var draftPair = useState(null)
1037
1251
  var draft = draftPair[0] // {date, time, quadrant, title}
1038
1252
  var setDraft = draftPair[1]
1253
+ var dayViewPair = useState(null)
1254
+ var dayView = dayViewPair[0]
1255
+ var setDayView = dayViewPair[1]
1039
1256
  var msgPair = useState('')
1040
1257
  var msg = msgPair[0]
1041
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]
1042
1265
  function load() {
1043
- 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) })
1044
1267
  }
1045
1268
  useEffect(function () {
1046
1269
  load()
@@ -1073,7 +1296,7 @@ window.__ModuleLoader__.load({
1073
1296
  h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(-1) } }, '◀'),
1074
1297
  h('span', { style: { fontWeight: 700, flex: 1, textAlign: 'center' } }, month.year + ' / ' + String(month.mon).padStart(2, '0')),
1075
1298
  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'))))
1299
+ h('button', { 'data-dam-btn': '', onClick: function () { setDraft({ date: today, time: '09:00', quadrant: '重要不紧急', title: '', location: '', reminder: '', note: '' }) } }, '+ ' + t('addItem'))))
1077
1300
  // 图例
1078
1301
  rows.push(h('div', { 'data-dam-row': '', style: { flexWrap: 'wrap' } },
1079
1302
  Object.keys(QUADRANT_STYLE).map(function (q) {
@@ -1095,7 +1318,8 @@ window.__ModuleLoader__.load({
1095
1318
  var dayNum = Number(cell.slice(8))
1096
1319
  week.push(h('div', {
1097
1320
  key: cell,
1098
- onClick: function () { setDraft({ date: cell, time: '09:00', quadrant: '重要不紧急', title: '' }) },
1321
+ 'data-dam-calendar-day': '',
1322
+ onClick: function () { setDayView(cell) },
1099
1323
  style: {
1100
1324
  minHeight: '64px', padding: '4px', cursor: 'pointer', borderRadius: '8px',
1101
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)'),
@@ -1108,6 +1332,7 @@ window.__ModuleLoader__.load({
1108
1332
  var qs = QUADRANT_STYLE[en.quadrant] || QUADRANT_STYLE['未分类']
1109
1333
  return h('div', {
1110
1334
  key: en.time + en.title,
1335
+ 'data-dam-calendar-event': '',
1111
1336
  title: en.time + ' ' + en.title,
1112
1337
  onClick: function (ev) { ev.stopPropagation(); toggleDone(en) },
1113
1338
  style: {
@@ -1125,19 +1350,38 @@ window.__ModuleLoader__.load({
1125
1350
  }
1126
1351
  rows.push(h('div', null, gridRows))
1127
1352
  function toggleDone(en) {
1353
+ if (saving) return
1354
+ setSaving(true); setCalError('')
1128
1355
  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) })
1356
+ setMsg(d.result || ''); setSaving(false); load()
1357
+ }).catch(function (e) { setSaving(false); setCalError(t('failed') + e.message) })
1131
1358
  }
1132
1359
  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) })
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))
1137
1380
  }
1138
1381
  // 添加/编辑浮层(液态玻璃)
1139
1382
  if (draft) {
1140
1383
  rows.push(h('div', {
1384
+ 'data-dam-calendar-modal': '',
1141
1385
  style: {
1142
1386
  position: 'absolute', inset: 0, zIndex: 10, borderRadius: '16px',
1143
1387
  background: 'color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.9)) 70%, transparent)',
@@ -1152,12 +1396,17 @@ window.__ModuleLoader__.load({
1152
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) } }),
1153
1397
  h('select', { 'data-dam-select': '', value: draft.quadrant, onChange: function (e) { var n = Object.assign({}, draft); n.quadrant = e.target.value; setDraft(n) } },
1154
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) } }),
1155
1402
  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')))))
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)))
1158
1406
  }
1159
- msg ? h('div', { 'data-dam-hint': '' }, msg) : null
1160
- 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)
1161
1410
  }
1162
1411
 
1163
1412
  var TOOL_LABEL = { workbuddy: t('aiAssistant'), codebuddy: 'CodeBuddy', claude: 'Claude Code', codex: 'Codex', 'project-files': t('projectFiles') }
@@ -1174,10 +1423,25 @@ window.__ModuleLoader__.load({
1174
1423
  var errPair = useState('')
1175
1424
  var err = errPair[0]
1176
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]
1177
1441
  function load() {
1178
1442
  apiGet(API.external).then(function (d) { setSources(d.sources || []) }).catch(function (e) { setErr(e.message) })
1179
1443
  }
1180
- useEffect(function () { load(); var t = setInterval(load, 60000); return function () { clearInterval(t) } }, [])
1444
+ useEffect(function () { load() }, [])
1181
1445
  if (!sources) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
1182
1446
  function doImport(source, target) {
1183
1447
  var next = Object.assign({}, busy); next[source] = true
@@ -1185,8 +1449,44 @@ window.__ModuleLoader__.load({
1185
1449
  apiPost(API.externalImport, { source: source, target: target }).then(function (d) {
1186
1450
  var done = Object.assign({}, busy); done[source] = false
1187
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()
1188
1454
  }).catch(function (e) { var done = Object.assign({}, busy); done[source] = false; setBusy(done); setErr(e.message) })
1189
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
+ }
1190
1490
  function importAll() {
1191
1491
  var md = sources.filter(function (s) { return s.kind !== 'sessions' && s.enabled !== false })
1192
1492
  setMsg(t('importing') + md.length + t('importingSuffix')); setErr('')
@@ -1200,17 +1500,31 @@ window.__ModuleLoader__.load({
1200
1500
  }
1201
1501
  for (var i = 0; i < sources.length; i++) {
1202
1502
  (function (s) {
1503
+ var isOpen = expanded === s.id
1504
+ var v = viewCache[s.id]
1203
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')))
1204
1507
  if (s.kind === 'sessions') {
1205
1508
  actions.push(h('span', { 'data-dam-hint': '' }, t('sessionSource') + s.fileCount + t('sessionSourceSuffix')))
1206
1509
  } 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')))
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
+
1209
1513
  }
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)))
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)))
1214
1528
  })(sources[i])
1215
1529
  }
1216
1530
  return h('div', null,
@@ -1253,6 +1567,52 @@ window.__ModuleLoader__.load({
1253
1567
  }
1254
1568
  }
1255
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
+
1256
1616
  function MemoryPanel() {
1257
1617
  var tick = useTick()
1258
1618
  var tabPair = useState('overview')
@@ -1281,6 +1641,7 @@ window.__ModuleLoader__.load({
1281
1641
  width: g.width + 'px',
1282
1642
  height: g.height + 'px',
1283
1643
  '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
1644
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
1284
1645
  }
1285
1646
  var dragMove = startPointerDrag(function (dx, dy) {
1286
1647
  controller.setGeom({ left: g.left + dx, top: g.top + dy })
@@ -1300,13 +1661,12 @@ window.__ModuleLoader__.load({
1300
1661
  onPointerDown: dragMove,
1301
1662
  },
1302
1663
  h('strong', null, t('autoMemory')),
1664
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, 'v0.1.23-ui'),
1303
1665
  h('span', { className: 'dam-spacer' }),
1304
1666
  h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
1305
1667
  h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
1306
1668
  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
- })),
1669
+ h(TabScroller, { tabs: tabs, tab: tab, setTab: setTab }),
1310
1670
  h('div', { 'data-dam-body': '' }, body),
1311
1671
  h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove }))
1312
1672
  }
@@ -1483,9 +1843,15 @@ window.__ModuleLoader__.load({
1483
1843
  var errPair = useState('')
1484
1844
  var err = errPair[0]
1485
1845
  var setErr = errPair[1]
1846
+ var dirtyPair = useState(false)
1847
+ var dirty = dirtyPair[0]
1848
+ var setDirty = dirtyPair[1]
1486
1849
  var dbgOpenPair = useState(false)
1487
1850
  var dbgOpen = dbgOpenPair[0]
1488
1851
  var setDbgOpen = dbgOpenPair[1]
1852
+ var settingsSectionPair = useState('appearance')
1853
+ var settingsSection = settingsSectionPair[0]
1854
+ var setSettingsSection = settingsSectionPair[1]
1489
1855
  var browseOpenPair = useState(false)
1490
1856
  var browseOpen = browseOpenPair[0]
1491
1857
  var setBrowseOpen = browseOpenPair[1]
@@ -1544,7 +1910,7 @@ window.__ModuleLoader__.load({
1544
1910
  return function () { alive = false }
1545
1911
  }, [])
1546
1912
  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) }
1913
+ function set(key, value) { var next = Object.assign({}, cfg); next[key] = value; setCfg(next); setDirty(true) }
1548
1914
  function checkUpdate() {
1549
1915
  if (checkingUpdate) return
1550
1916
  setCheckingUpdate(true)
@@ -1563,21 +1929,51 @@ window.__ModuleLoader__.load({
1563
1929
  function save() {
1564
1930
  if (busy) return
1565
1931
  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) })
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) })
1567
1933
  .catch(function (e) { setErr(e.message); setBusy(false) })
1568
1934
  }
1569
1935
  function field(label, control, hint) {
1570
- return h('div', null,
1936
+ return h('div', { 'data-dam-settings-row': '' },
1571
1937
  h('div', { 'data-dam-row': '' }, h('label', null, label), control),
1572
1938
  hint ? h('div', { 'data-dam-hint': '' }, hint) : null)
1573
1939
  }
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')),
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')),
1581
1977
  field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
1582
1978
  field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
1583
1979
  h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
@@ -1593,30 +1989,36 @@ window.__ModuleLoader__.load({
1593
1989
  h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
1594
1990
  h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
1595
1991
  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')),
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')),
1598
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')),
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')),
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')),
1600
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')),
1601
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')),
1602
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')),
1603
2004
  field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
1604
2005
  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': '' },
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': '' },
1607
2008
  h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
1608
2009
  h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
1609
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,
1610
2011
  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),
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),
1615
2017
  // 调试中心(折叠):模块状态一览,方便排查问题/提 issue
1616
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' } },
1617
2019
  h('button', { 'data-dam-btn': '', onClick: function () { setDbgOpen(!dbgOpen) } }, (dbgOpen ? '▴ ' : '▾ ') + t('debugCenter')),
1618
2020
  dbgOpen ? h('div', { style: { marginTop: '8px' } }, h(DebugCenter)) : null),
1619
- err ? h('div', { 'data-dam-error': '' }, err) : null)
2021
+ err ? h('div', { 'data-dam-error': '' }, err) : null))
1620
2022
  }
1621
2023
 
1622
2024
  // ───────────────────────── 插件挂载 ─────────────────────────