Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/feature-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Flagship feature. C++ services + `dao://dao-agent` WebUI + vendor runtime.
| ☐ | Agent web tools search/fetch tiering | `src/dao/.../resources/agent/web_search/`, Settings Dao page patches, `dao_agent_ui.cc` | — | Provider built-in search is preferred; Auto mode uses configured Jina Search before DuckDuckGo HTML; DuckDuckGo anomaly/verification pages report that accurately instead of `HTML structure changed`; `fetch_url` still falls back from Jina Reader to browser fetch |
| ☐ | Dream scheduler and Agent settings controls | `src/dao/.../agent/dao_dream_service.*`, Settings Dao page patches, `dream_bridge.ts` | — | Dream remains off by default; enabling requires memory; nightly/catch-up/manual runs honor idle/time/date gates and show status/history on `dao://dream` |
| ☐ | Dream material privacy and excluded-domain filtering | `src/dao/.../agent/dao_dream_material_collector.*`, `dao_dream_domain_utils.*`, `dao_pref_names.*` | — | Excluded domains are normalized and removed before titles/search queries/debug material leave C++; stats do not leak excluded domain names |
| ☐ | Dream one-minute recap, history, rerun, sharing, and habit feedback | `dao_dream_app.ts`, `dao_dream_runner.ts`, `dao_dream_service.cc`, `dao_share_image.ts`, `dao_agent_ui.cc` | — | `dao://dream` loads up to 371 daily reports for the 53-week activity heatmap plus 53 weekly reports for the shared 14-item history rail; structured summaries, measured per-period foreground rhythm (with legacy model-estimate fallback), uncapped aggregate counts, themes, stats, and memory candidates render; candidates affect memory only after confirmation and rejection preserves existing memory; legacy markdown-only reports derive useful recap content; daily/weekly selection, rerun replacement/failure preservation, share image, full-report disclosure, domain exclusion, debug view, and feedback actions work |
| ☐ | Dream one-minute recap, history, rerun, sharing, and habit feedback | `dao_dream_app.ts`, `dao_dream_runner.ts`, `dao_dream_service.cc`, `dao_share_image.ts`, `dao_agent_ui.cc` | — | `dao://dream` loads up to 371 daily reports for the 53-week activity heatmap plus 53 weekly reports for the shared 14-item history rail; report cells show localized date and active-duration tooltips on pointer hover and keyboard focus, legacy reports without duration show the unavailable state, and pointer leave, blur, or heatmap scrolling dismisses the tooltip; structured summaries, measured per-period foreground rhythm (with legacy model-estimate fallback), uncapped aggregate counts, themes, stats, and memory candidates render; candidates affect memory only after confirmation and rejection preserves existing memory; legacy markdown-only reports derive useful recap content; daily/weekly selection, rerun replacement/failure preservation, share image, full-report disclosure, domain exclusion, debug view, and feedback actions work |

## 4. Picture-in-Picture Enhancements

Expand Down
3 changes: 2 additions & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ The stack includes: **LLM tool calling**, **long-term memory** (SQLite + FTS5),
and an optional debug view of the exact LLM input (`dao.dream_debug`).
- **`dao://dream` one-minute recap** — responsive two-column report with a
53-week real-report activity heatmap, compact daily-and-weekly history rail,
summary card,
localized date-and-active-duration tooltips for report-bearing heatmap cells
on pointer hover and keyboard focus, summary card,
measured foreground-focus rhythm, topic cards, aggregate counts,
memory-candidate actions, and a folded full report. Existing rerun, image
sharing, source-domain exclusion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ vi.mock('../dao_share_image.js', () => ({

vi.mock('../i18n/i18n.js', () => ({
initI18n: vi.fn(async () => undefined),
currentLocale: () => 'zh-CN',
t: (key: string, vars?: Record<string, string | number>) => {
const templates: Record<string, string> = {
'chat.dream.card_date': 'About {date}',
Expand Down Expand Up @@ -76,6 +77,8 @@ vi.mock('../i18n/i18n.js', () => ({
'dream.page.activity_less': 'Less',
'dream.page.activity_more': 'More',
'dream.page.activity_label': 'Daily Dream report activity',
'dream.page.activity_tooltip': '{date} · {duration}',
'dream.page.activity_duration_unavailable': 'Duration unavailable',
'dream.page.weekly_badge': 'Weekly',
'dream.page.weekly_eyebrow': 'Weekly Dream Recap',
'dream.page.weekly_period': '{start} – {end}',
Expand Down Expand Up @@ -118,15 +121,29 @@ vi.mock('../vendor/pi_runtime_bundle.js', () => ({

import '../dao_dream_app.js';

const dreamAppCtor = customElements.get('dao-dream-app') as
CustomElementConstructor & {
invokeLifecycleCallbacksForTesting?: boolean;
};
dreamAppCtor.invokeLifecycleCallbacksForTesting = true;

type TestDreamApp = HTMLElement & {updateComplete: Promise<boolean>};
type TestDreamReport = {dreamDate: string};
type TestDreamAppPrototype = {
reports_: TestDreamReport[];
renderActivityHeatmap_: () => ReturnType<typeof html>;
renderActivityHeatmapCell_: (
dateKey: string, label: string, report: TestDreamReport|null,
level: number, column: number, row: number) => ReturnType<typeof html>;
renderActivityTooltip_: () => ReturnType<typeof html>;
hideActivityTooltip_: () => void;
};

const dreamAppPrototype =
(customElements.get('dao-dream-app') as CustomElementConstructor)
.prototype as unknown as TestDreamAppPrototype;
let restoreActivityHeatmap: () => void;
let useSingleCellActivityHeatmap: (dateKey: string, label: string) => void;

function report(
dreamDate: string,
Expand Down Expand Up @@ -248,6 +265,24 @@ function expectIconOnlyCopyButton(
expect(button!.querySelector('svg[aria-hidden="true"]')).toBeTruthy();
}

function countTemplateMarkers(value: unknown, marker: string): number {
if (Array.isArray(value)) {
return value.reduce(
(count, item) => count + countTemplateMarkers(item, marker), 0);
}
if (typeof value !== 'object' || value === null ||
!('strings' in value) || !('values' in value)) {
return 0;
}
const template = value as {
strings: readonly string[];
values: readonly unknown[];
};
const ownMarkers = template.strings.join('').split(marker).length - 1;
return ownMarkers + template.values.reduce(
(count, item) => count + countTemplateMarkers(item, marker), 0);
}

describe('dao-dream-app routing', () => {
beforeEach(() => {
document.body.innerHTML = '';
Expand All @@ -269,6 +304,22 @@ describe('dao-dream-app routing', () => {
<div class="heatmap-scroll"></div>
</section>`);
restoreActivityHeatmap = () => activityHeatmapSpy.mockRestore();
useSingleCellActivityHeatmap = (dateKey: string, label: string) => {
activityHeatmapSpy.mockImplementation(function(
this: TestDreamAppPrototype) {
const report =
this.reports_.find(item => item.dreamDate === dateKey) || null;
return html`
<section class="activity-heatmap">
<div class="heatmap-scroll"
@scroll=${() => this.hideActivityTooltip_()}>
${this.renderActivityHeatmapCell_(
dateKey, label, report, report ? 1 : 0, 1, 1)}
</div>
</section>
${this.renderActivityTooltip_()}`;
});
};
});

afterEach(() => {
Expand All @@ -288,9 +339,86 @@ describe('dao-dream-app routing', () => {
expect(bridgeMocks.callNative).toHaveBeenCalledWith(
'getDreamReports', {limit: 371});
expect(el.shadowRoot!.textContent).toContain('2026-06-12');
expect(el.shadowRoot!.textContent).toContain('Thu, Jun 11');
expect(el.shadowRoot!.textContent).toContain('6月11日周四');
});

it('opens the activity heatmap at the newest dates', async () => {
vi.spyOn(Element.prototype, 'scrollWidth', 'get').mockReturnValue(760);
vi.spyOn(Element.prototype, 'clientWidth', 'get').mockReturnValue(220);
bridgeMocks.callNative.mockResolvedValueOnce([report('2026-06-12')]);

const el = await mountDreamApp('/');
const heatmap =
el.shadowRoot!.querySelector<HTMLElement>('.heatmap-scroll');

expect(heatmap).toBeTruthy();
expect(heatmap!.scrollLeft).toBe(540);
});

it('shows and hides localized active duration for a heatmap cell',
async () => {
useSingleCellActivityHeatmap('2026-06-19', '6月19日周五');
bridgeMocks.callNative.mockResolvedValueOnce([
report('2026-06-19', '[]', recapMaterialStats()),
]);

const el = await mountDreamApp('/');
const cell = el.shadowRoot!.querySelector<HTMLButtonElement>(
'.heat-cell[aria-label="6月19日周五"]');
expect(cell).toBeTruthy();

cell!.dispatchEvent(new Event('pointerenter'));
await el.updateComplete;

const tooltip = el.shadowRoot!.querySelector<HTMLElement>(
'[role="tooltip"]');
expect(tooltip?.textContent).toContain('6月19日');
expect(tooltip?.textContent).toContain('3小时 54分钟');
expect(tooltip?.closest('.activity-heatmap')).toBeNull();
expect(el.shadowRoot!
.querySelector<HTMLButtonElement>(
'.heat-cell[aria-label="6月19日周五"]')
?.getAttribute('aria-describedby'))
.toBe('dream-activity-tooltip');

cell!.dispatchEvent(new Event('pointerleave'));
await el.updateComplete;
expect(el.shadowRoot!.querySelector('[role="tooltip"]')).toBeNull();

el.shadowRoot!
.querySelector<HTMLButtonElement>(
'.heat-cell[aria-label="6月19日周五"]')!
.dispatchEvent(new Event('pointerenter'));
await el.updateComplete;
expect(el.shadowRoot!.querySelector('[role="tooltip"]')).toBeTruthy();

el.shadowRoot!.querySelector<HTMLElement>('.heatmap-scroll')!
.dispatchEvent(new Event('scroll'));
await el.updateComplete;
expect(el.shadowRoot!.querySelector('[role="tooltip"]')).toBeNull();
});

it('shows unavailable duration for a legacy heatmap report on focus',
async () => {
useSingleCellActivityHeatmap('2026-06-19', '6月19日周五');
bridgeMocks.callNative.mockResolvedValueOnce([report('2026-06-19')]);

const el = await mountDreamApp('/');
const cell = el.shadowRoot!.querySelector<HTMLButtonElement>(
'.heat-cell[aria-label="6月19日周五"]');
expect(cell).toBeTruthy();

cell!.dispatchEvent(new Event('focus'));
await el.updateComplete;

expect(el.shadowRoot!.querySelector('[role="tooltip"]')?.textContent)
.toContain('Duration unavailable');

cell!.dispatchEvent(new Event('blur'));
await el.updateComplete;
expect(el.shadowRoot!.querySelector('[role="tooltip"]')).toBeNull();
});

it('loads dream history for dao://dream/history', async () => {
bridgeMocks.callNative.mockResolvedValueOnce([report('2026-06-10')]);

Expand Down Expand Up @@ -378,7 +506,6 @@ describe('dao-dream-app routing', () => {

it('renders the selected one-minute recap design from structured data',
async () => {
restoreActivityHeatmap();
bridgeMocks.callNative.mockResolvedValueOnce([
report('2026-06-19', habitCandidates(), recapMaterialStats()),
report('2026-06-18'),
Expand All @@ -388,14 +515,13 @@ describe('dao-dream-app routing', () => {
const root = el.shadowRoot!;

expect(root.querySelector('.activity-heatmap')).toBeTruthy();
expect(root.querySelectorAll('.heat-cell').length).toBeGreaterThan(350);
expect(root.querySelectorAll('.history-item')).toHaveLength(2);
expect(root.querySelector('.recap-summary')?.textContent).toContain(
'Afternoon focus shifted');
expect(root.querySelectorAll('.rhythm-slot')).toHaveLength(4);
expect(root.querySelector(
'.rhythm-slot[data-peak="true"]')?.textContent)
.toContain('125 min');
.toContain('2小时 5分钟');
expect(root.querySelectorAll('.theme-card')).toHaveLength(1);
expect(root.querySelector('.theme-card')?.textContent)
.toContain('Rust async programming');
Expand All @@ -409,6 +535,18 @@ describe('dao-dream-app routing', () => {
expect(root.querySelector('.memory-candidates')).toBeTruthy();
});

it('builds a full year of activity heatmap cells without mounting them',
() => {
restoreActivityHeatmap();
const el = document.createElement('dao-dream-app') as
TestDreamApp & TestDreamAppPrototype;

const template = el.renderActivityHeatmap_();

expect(countTemplateMarkers(template, 'class="heat-cell"'))
.toBeGreaterThan(350);
Comment on lines +546 to +547

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

收紧完整一年的热图断言。

toBeGreaterThan(350) 仍允许缺少多个日期单元格,因此不能验证完整一年。生产实现至少生成 365 个单元格。请将阈值提高到 364 以上,或按固定结束日期断言精确数量。

建议修改
        expect(countTemplateMarkers(template, 'class="heat-cell"'))
-           .toBeGreaterThan(350);
+           .toBeGreaterThan(364);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(countTemplateMarkers(template, 'class="heat-cell"'))
.toBeGreaterThan(350);
expect(countTemplateMarkers(template, 'class="heat-cell"'))
.toBeGreaterThan(364);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dao/browser/ui/webui/resources/agent/__tests__/dao_dream_app.test.ts`
around lines 546 - 547, 收紧 dao_dream_app 热图测试中的 class="heat-cell" 数量断言,将
toBeGreaterThan(350) 提高为至少大于 364,以验证完整年度的日期单元格数量。

});

it('derives a concise recap fallback from legacy markdown', async () => {
bridgeMocks.callNative.mockResolvedValueOnce([{
...report('2026-06-19'),
Expand All @@ -424,13 +562,46 @@ describe('dao-dream-app routing', () => {
.toContain('Main thread');
});

it('uses legacy report content instead of its generic heading in history',
async () => {
bridgeMocks.callNative.mockResolvedValueOnce([{
...report('2026-06-19'),
reportMarkdown:
'## 昨天的主线\n完成了发布流程整理,并验证了关键配置。',
}]);

const el = await mountDreamApp('/');
const historySummary =
el.shadowRoot!.querySelector('.history-kind')?.textContent || '';

expect(historySummary).toContain(
'完成了发布流程整理,并验证了关键配置。');
expect(historySummary).not.toContain('昨天的主线');
});

it('keeps a valid structured theme when recap summary is empty', async () => {
const stats = JSON.parse(recapMaterialStats());
stats.recap.summary = '';
bridgeMocks.callNative.mockResolvedValueOnce([{
...report('2026-06-19', '[]', JSON.stringify(stats)),
reportMarkdown: '## 昨天的主线\n旧版正文不应覆盖结构化主题。',
}]);

const el = await mountDreamApp('/');
const historySummary =
el.shadowRoot!.querySelector('.history-kind')?.textContent || '';

expect(historySummary).toContain('Rust async programming');
expect(historySummary).not.toContain('旧版正文');
});

it('uses measured foreground buckets instead of model-estimated rhythm',
async () => {
const stats = JSON.parse(recapMaterialStats());
stats.foreground_seconds_by_bucket = {
morning: 600,
afternoon: 7200,
evening: 1800,
evening: 7500,
night: 0,
};
stats.recap.time_buckets = {
Expand All @@ -449,8 +620,8 @@ describe('dao-dream-app routing', () => {

expect(slots.map(slot => slot.textContent)).toEqual([
expect.stringContaining('10 min'),
expect.stringContaining('120 min'),
expect.stringContaining('30 min'),
expect.stringContaining('2小时'),
expect.stringContaining('2小时 5分钟'),
expect.stringContaining('0 min'),
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {beforeEach, describe, expect, it, vi} from 'vitest';
const callLLMStreaming = vi.fn();
const recordApiCall = vi.fn();
const addWebUIListener = vi.fn();
const i18nMocks = vi.hoisted(() => ({
initialized: false,
locale: 'zh-CN',
initI18n: vi.fn(),
currentLocale: vi.fn(),
}));

vi.mock('../agent_bridge.js', () => ({
addWebUIListener: (...args: unknown[]) => addWebUIListener(...args),
Expand All @@ -33,7 +39,8 @@ vi.mock('../llm_config.js', () => ({
}),
}));
vi.mock('../i18n/i18n.js', () => ({
currentLocale: () => 'zh-CN',
initI18n: () => i18nMocks.initI18n(),
currentLocale: () => i18nMocks.currentLocale(),
}));

import {extractJson, runDream} from '../dao_dream_runner.js';
Expand Down Expand Up @@ -98,6 +105,15 @@ describe('runDream', () => {
beforeEach(() => {
callLLMStreaming.mockReset();
recordApiCall.mockReset();
i18nMocks.initialized = false;
i18nMocks.locale = 'zh-CN';
i18nMocks.initI18n.mockReset();
i18nMocks.initI18n.mockImplementation(async () => {
i18nMocks.initialized = true;
});
i18nMocks.currentLocale.mockReset();
i18nMocks.currentLocale.mockImplementation(
() => i18nMocks.initialized ? i18nMocks.locale : 'en');
});

it('parses a valid response and caps confidence at 0.8', async () => {
Expand Down Expand Up @@ -139,7 +155,8 @@ describe('runDream', () => {
expect(recordApiCall).toHaveBeenCalledWith(11, 7, 2, 6);
});

it('asks the model to keep user-facing text in the current locale', async () => {
it('injects the resolved locale into the report prompts', async () => {
i18nMocks.locale = 'fr';
respondWith(VALID);
await runDream('2026-06-11', {});

Expand All @@ -149,9 +166,13 @@ describe('runDream', () => {
}>;
const systemPrompt = messages[0]!.content;

expect(messages[0]!.content).toContain('Required output locale: fr');
expect(messages[0]!.content).not.toContain('zh-CN');
expect(messages[1]!.content).toContain('Locale: fr');
expect(systemPrompt).toContain(
'All user-facing report text, habit values, evidence, and questions');
expect(systemPrompt).toContain('For zh-CN, use Simplified Chinese');
expect(systemPrompt).not.toContain(
'recent questions clearly use another language');
expect(systemPrompt).not.toContain('Habit keys and values in English');
});

Expand Down
Loading
Loading