diff --git a/frontend/e2e/command-palette.spec.js b/frontend/e2e/command-palette.spec.js
index 9d6a3d7..29b63ce 100644
--- a/frontend/e2e/command-palette.spec.js
+++ b/frontend/e2e/command-palette.spec.js
@@ -52,7 +52,7 @@ test.describe('Command Palette', () => {
const palette = page.locator('.command-palette');
await expect(palette).toBeVisible();
const items = palette.locator('.command-palette-item');
- await expect(items.nth(0)).toHaveAttribute('data-command-id', 'verstak.shell.open-today');
+ await expect(items.nth(0)).toHaveAttribute('data-command-id', 'verstak.shell.open-overview');
await expect(items.nth(1)).toHaveAttribute('data-command-id', 'verstak.shell.open-files');
await expect(items.nth(2)).toHaveAttribute('data-command-id', 'verstak.shell.open-activity');
await expect(items.nth(3)).toHaveAttribute('data-command-id', 'verstak.shell.open-browser-inbox');
diff --git a/frontend/e2e/plugin-manager-layout.spec.js b/frontend/e2e/plugin-manager-layout.spec.js
index 99b238d..6e72548 100644
--- a/frontend/e2e/plugin-manager-layout.spec.js
+++ b/frontend/e2e/plugin-manager-layout.spec.js
@@ -99,19 +99,19 @@ test.describe('E: Plugin Manager layout', () => {
await expect(selected).toHaveText('Test');
});
- test('workspace tools render Today first with Files as one tab', async ({ page }) => {
+ test('workspace tools render Overview first with Files as one tab', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
const tabs = page.locator('.workspace-tabs');
await expect(tabs).toBeVisible({ timeout: 10000 });
- const todayTab = tabs.locator('[role="tab"]').filter({ hasText: 'Today' });
+ const overviewTab = tabs.locator('[role="tab"]').filter({ hasText: 'Overview' });
const filesTab = tabs.locator('[role="tab"]').filter({ hasText: 'Files' });
- await expect(todayTab).toBeVisible();
- await expect(todayTab).toHaveAttribute('aria-selected', 'true');
+ await expect(overviewTab).toBeVisible();
+ await expect(overviewTab).toHaveAttribute('aria-selected', 'true');
await expect(filesTab).toBeVisible();
await expect(filesTab).toHaveAttribute('aria-selected', 'false');
await expect(page.locator('.workspace-tool')).toHaveCount(0);
- await expect(page.locator('.today-root')).toBeVisible();
+ await expect(page.locator('[data-overview-root]')).toBeVisible();
await filesTab.click();
await expect(page.locator('.files-root')).toBeVisible();
diff --git a/frontend/e2e/ux-followup.spec.js b/frontend/e2e/ux-followup.spec.js
index 9297916..a4a6b32 100644
--- a/frontend/e2e/ux-followup.spec.js
+++ b/frontend/e2e/ux-followup.spec.js
@@ -99,7 +99,7 @@ test.describe('UX follow-up fixes', () => {
expect(workspaceBox.width).toBeGreaterThan(340);
expect(workspaceBox.y).toBeGreaterThan(sidebarBox.y + sidebarBox.height - 1);
await expect(page.locator('.workspace-header [data-global-search-input]')).toBeVisible();
- await expect(page.getByRole('tab', { name: 'Today' })).toBeVisible();
+ await expect(page.getByRole('tab', { name: 'Overview' })).toBeVisible();
const hasHorizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth);
expect(hasHorizontalOverflow).toBe(false);
diff --git a/frontend/e2e/ux-today.spec.js b/frontend/e2e/ux-today.spec.js
index f585b1b..fb63ef5 100644
--- a/frontend/e2e/ux-today.spec.js
+++ b/frontend/e2e/ux-today.spec.js
@@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
-test.describe('UX Today workspace flow', () => {
+test.describe('UX Overview workspace flow', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
@@ -15,37 +15,40 @@ test.describe('UX Today workspace flow', () => {
consoleCollector.assertNoErrors();
});
- test('workspace opens with Today before plugin tools', async ({ page }) => {
+ test('workspace opens with Overview before plugin tools', async ({ page }) => {
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
const tabs = page.getByRole('tab');
- await expect(tabs.nth(0)).toHaveText('Today');
+ await expect(tabs.nth(0)).toHaveText('Overview');
await expect(tabs.nth(1)).toHaveText('Files');
- await expect(page.getByRole('tab', { name: 'Today' })).toHaveAttribute('aria-selected', 'true');
+ await expect(page.getByRole('tab', { name: 'Overview' })).toHaveAttribute('aria-selected', 'true');
+ await expect(page.getByRole('tab', { name: 'Today' })).toHaveCount(0);
- const today = page.locator('.today-root');
- await expect(today).toBeVisible();
- await expect(today.locator('[data-today-section="captured"]')).toContainText('Captured');
- await expect(today.locator('[data-today-section="activity"]')).toContainText('Recent Activity');
- await expect(today.locator('[data-today-section="worklog"]')).toContainText('Worklog Suggestions');
- await expect(today.locator('[data-today-section="quick-actions"]')).toContainText('Quick Actions');
- await expect(today).toContainText('No browser captures yet');
- await expect(today).toContainText('No activity events yet');
+ const overview = page.locator('[data-overview-root]');
+ await expect(overview).toBeVisible();
+ await expect(overview.locator('[data-overview-section="continue"]')).toContainText('Continue working');
+ await expect(overview.locator('[data-overview-section="recent"]')).toContainText('Recent changes');
+ await expect(overview.locator('[data-overview-section="attention"]')).toContainText('Needs attention');
+ await expect(overview.locator('[data-overview-section="quick-actions"]')).toContainText('Quick actions');
+ await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('recent changes');
+ await expect(overview.locator('[data-overview-summary="captures"]')).toContainText('unprocessed captures');
+ await expect(overview).toContainText('No clear resume point yet');
+ await expect(overview).toContainText('No meaningful changes for this filter yet');
});
- test('Today quick action opens Browser Inbox workspace tool', async ({ page }) => {
- await page.locator('[data-today-action="browser-inbox"]').click();
+ test('Overview quick action opens Browser Inbox workspace tool', async ({ page }) => {
+ await page.locator('[data-overview-action="browser-inbox"]').click();
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
});
- test('Today summarizes available work and highlights the next resume item', async ({ page }) => {
+ test('Overview prioritizes resume work and filters meaningful recent changes', async ({ page }) => {
await page.evaluate(async () => {
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
'captures:workspace:Project': [
{
- captureId: 'today-capture-1',
+ captureId: 'overview-capture-1',
capturedAt: '2026-06-30T08:00:00.000Z',
kind: 'page',
url: 'https://example.com/research',
@@ -54,7 +57,7 @@ test.describe('UX Today workspace flow', () => {
workspaceRootPath: 'Project',
},
{
- captureId: 'today-capture-2',
+ captureId: 'overview-capture-2',
capturedAt: '2026-06-30T08:15:00.000Z',
kind: 'selection',
title: 'Quote to process',
@@ -66,41 +69,108 @@ test.describe('UX Today workspace flow', () => {
await window.go.api.App.WritePluginSettings('verstak.activity', {
'events:workspace:Project': [
{
- activityId: 'today-activity-1',
+ activityId: 'overview-selected-file',
+ occurredAt: '2026-06-30T08:50:00.000Z',
+ type: 'file.selected',
+ title: 'Selected file',
+ summary: 'Project/draft.md',
+ workspaceRootPath: 'Project',
+ },
+ {
+ activityId: 'overview-opened-file',
+ occurredAt: '2026-06-30T08:40:00.000Z',
+ type: 'file.opened',
+ title: 'draft.md',
+ summary: 'Project/draft.md',
+ workspaceRootPath: 'Project',
+ },
+ {
+ activityId: 'overview-note',
occurredAt: '2026-06-30T08:25:00.000Z',
type: 'note.saved',
- title: 'Saved research note',
- summary: 'Project/Notes/Research.md',
+ title: 'Overview',
+ summary: 'Project/Notes/Overview.md',
+ workspaceRootPath: 'Project',
+ },
+ {
+ activityId: 'overview-file',
+ occurredAt: '2026-06-30T08:20:00.000Z',
+ type: 'file.changed',
+ title: 'draft.md',
+ summary: 'Project/draft.md',
+ workspaceRootPath: 'Project',
+ },
+ {
+ activityId: 'overview-workspace-selected',
+ occurredAt: '2026-06-30T08:10:00.000Z',
+ type: 'case.selected',
+ title: 'Workspace selected',
workspaceRootPath: 'Project',
},
],
});
await window.go.api.App.WritePluginSettings('verstak.journal', {
- 'suggestions:workspace:Project': [
+ 'worklog:workspace:Project': [
{
- entryId: 'today-worklog-1',
+ entryId: 'overview-journal-1',
+ date: '2026-06-30',
title: 'Write project summary',
summary: 'Turn recent captures into a worklog entry',
minutes: 35,
workspaceRootPath: 'Project',
},
],
+ 'suggestions:workspace:Project': [
+ {
+ suggestionId: 'overview-suggestion-1',
+ date: '2026-06-30',
+ title: 'Project work on 2026-06-30',
+ minutes: 50,
+ workspaceRootPath: 'Project',
+ },
+ ],
});
});
- await page.locator('.today-header').getByRole('button', { name: 'Refresh' }).click();
+ await page.locator('[data-overview-action="refresh"]').click();
- const today = page.locator('.today-root');
- await expect(today.locator('[data-today-summary="captures"]')).toContainText('2');
- await expect(today.locator('[data-today-summary="activity"]')).toContainText('1');
- await expect(today.locator('[data-today-summary="worklog"]')).toContainText('1');
+ const overview = page.locator('[data-overview-root]');
+ await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('1');
+ await expect(overview.locator('[data-overview-summary="files"]')).toContainText('1');
+ await expect(overview.locator('[data-overview-summary="captures"]')).toContainText('2');
+ await expect(overview.locator('[data-overview-summary="journal"]')).toContainText('1');
+ await expect(overview.locator('[data-overview-summary="attention"]')).toContainText('3');
- const resume = today.locator('[data-today-section="resume"]');
- await expect(resume).toContainText('Resume next');
- await expect(resume).toContainText('Research Report');
- await resume.locator('[data-today-action="resume-primary"]').click();
+ const resume = overview.locator('[data-overview-section="continue"]');
+ await expect(resume).toContainText('Opened file "draft.md"');
+ await expect(resume).not.toContainText('Research Report');
+ await resume.locator('[data-overview-action="continue-primary"]').click();
- await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
- await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
+ await expect(page.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true');
+ await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
+
+ await page.getByRole('tab', { name: 'Overview' }).click();
+ const recent = overview.locator('[data-overview-section="recent"]');
+ await expect(recent).toContainText('Edited note "Overview"');
+ await expect(recent).toContainText('Changed file "draft.md"');
+ await expect(recent).toContainText('Captured page "Research Report"');
+ await expect(recent).toContainText('Added journal entry "Write project summary"');
+ await expect(recent).not.toContainText('Selected file');
+ await expect(recent).not.toContainText('Workspace selected');
+ await expect(recent).not.toContainText('file.opened');
+
+ await overview.locator('[data-overview-filter="notes"]').click();
+ await expect(recent).toContainText('Edited note "Overview"');
+ await expect(recent).not.toContainText('Changed file "draft.md"');
+ await expect(recent).not.toContainText('Research Report');
+
+ await overview.locator('[data-overview-filter="captures"]').click();
+ await expect(recent).toContainText('Captured page "Research Report"');
+ await expect(recent).toContainText('Captured selection "Quote to process"');
+ await expect(recent).not.toContainText('Edited note "Overview"');
+
+ await overview.locator('[data-overview-filter="journal"]').click();
+ await expect(recent).toContainText('Added journal entry "Write project summary"');
+ await expect(recent).not.toContainText('Changed file "draft.md"');
});
});
diff --git a/frontend/src/lib/shell/CommandPalette.svelte b/frontend/src/lib/shell/CommandPalette.svelte
index 4262ee4..c997495 100644
--- a/frontend/src/lib/shell/CommandPalette.svelte
+++ b/frontend/src/lib/shell/CommandPalette.svelte
@@ -13,7 +13,7 @@
const inactiveStatuses = new Set(['disabled', 'failed', 'incompatible', 'missing-required-capability']);
const shellCommands = [
- { id: 'verstak.shell.open-today', title: 'Open Today', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 10, shellAction: 'today' },
+ { id: 'verstak.shell.open-overview', title: 'Open Overview', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 10, shellAction: 'overview' },
{ id: 'verstak.shell.open-files', title: 'Open Files', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 20, shellAction: 'files' },
{ id: 'verstak.shell.open-activity', title: 'Open Activity', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 30, shellAction: 'activity' },
{ id: 'verstak.shell.open-browser-inbox', title: 'Open Browser Inbox', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 40, shellAction: 'browser-inbox' },
@@ -156,7 +156,7 @@
return;
}
const actionToTab = {
- today: 'today',
+ overview: 'overview',
files: 'files',
activity: 'activity',
'browser-inbox': 'browser inbox',
diff --git a/frontend/src/lib/shell/TodaySurface.svelte b/frontend/src/lib/shell/TodaySurface.svelte
index ee40b74..616aa92 100644
--- a/frontend/src/lib/shell/TodaySurface.svelte
+++ b/frontend/src/lib/shell/TodaySurface.svelte
@@ -3,36 +3,79 @@
import * as App from '../../../wailsjs/go/api/App';
export let workspaceRootPath = '';
- export let workspaceTitle = '';
export let availableTools = [];
const dispatch = createEventDispatcher();
+ const FILTERS = [
+ { key: 'all', label: 'All' },
+ { key: 'notes', label: 'Notes' },
+ { key: 'files', label: 'Files' },
+ { key: 'captures', label: 'Captures' },
+ { key: 'journal', label: 'Journal' },
+ ];
+ const LOW_VALUE_RECENT_TYPES = new Set([
+ 'workspace.selected',
+ 'case.selected',
+ 'file.selected',
+ 'file.opened',
+ 'note.opened',
+ ]);
+ const LOW_VALUE_RESUME_TYPES = new Set([
+ 'workspace.selected',
+ 'case.selected',
+ 'file.selected',
+ ]);
let loading = true;
+ let activeFilter = 'all';
let captures = [];
- let activity = [];
+ let activityEvents = [];
+ let journalEntries = [];
let worklogSuggestions = [];
+ let keyResources = [];
+ let loadedWorkspaceRoot = '';
+ let toolProbe = 0;
+ $: hasNotes = hasTool('notes');
$: hasBrowserInbox = hasTool('browser inbox') || hasTool('inbox');
$: hasActivity = hasTool('activity');
$: hasFiles = hasTool('files');
+ $: recentChanges = buildRecentChanges(activityEvents, captures, journalEntries);
+ $: filteredRecentChanges = activeFilter === 'all'
+ ? recentChanges
+ : recentChanges.filter(item => item.category === activeFilter);
+ $: needsAttention = buildNeedsAttention(captures, worklogSuggestions);
+ $: continueItems = buildContinueItems(activityEvents, captures);
+ $: primaryContinue = continueItems[0] || null;
+ $: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries);
$: summaryItems = [
- { key: 'captures', label: 'Captured', count: captures.length },
- { key: 'activity', label: 'Activity', count: activity.length },
- { key: 'worklog', label: 'Worklog', count: worklogSuggestions.length },
+ { key: 'notes', label: 'Notes', count: countCategory(recentChanges, 'notes'), detail: countLabel(countCategory(recentChanges, 'notes'), 'recent change') },
+ { key: 'files', label: 'Files', count: countCategory(recentChanges, 'files'), detail: countLabel(countCategory(recentChanges, 'files'), 'recent change') },
+ { key: 'captures', label: 'Captures', count: captures.length, detail: countLabel(captures.length, 'unprocessed capture') },
+ { key: 'journal', label: 'Journal / Activity', count: countCategory(recentChanges, 'journal'), detail: countLabel(countCategory(recentChanges, 'journal'), 'entry or event') },
+ { key: 'attention', label: 'Needs attention', count: needsAttention.length, detail: countLabel(needsAttention.length, 'pending item') },
];
- $: resumeItem = nextResumeItem(captures, worklogSuggestions, activity, hasActivity);
onMount(() => {
- loadToday();
+ toolProbe += 1;
});
+ $: if (workspaceRootPath && workspaceRootPath !== loadedWorkspaceRoot) {
+ loadOverview();
+ }
+
function hasTool(name) {
+ toolProbe;
name = String(name || '').toLowerCase();
- return (availableTools || []).some(tool => {
- const label = String(tool?.title || tool?.id || tool?.pluginId || '').toLowerCase();
+ const fromProps = (availableTools || []).some(tool => {
+ const label = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
return label.includes(name);
});
+ if (fromProps) return true;
+ if (typeof document === 'undefined') return false;
+ return Array.from(document.querySelectorAll('.workspace-tabs [role="tab"]')).some(tab => {
+ return String(tab.textContent || '').trim().toLowerCase().includes(name);
+ });
}
function decodeTuple(response, fallback) {
@@ -57,97 +100,307 @@
}
function rowsFor(settings, keys) {
- return keys.flatMap(key => normalizeRows(settings?.[key]));
+ const workspace = String(workspaceRootPath || '').trim();
+ return keys.flatMap(key => normalizeRows(settings?.[key])).filter(item => {
+ const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
+ return !tagged || !workspace || tagged === workspace;
+ });
+ }
+
+ function timeValue(item) {
+ return item?.capturedAt || item?.occurredAt || item?.receivedAt || item?.updatedAt || item?.modifiedAt || item?.date || item?.time || '';
+ }
+
+ function timeMs(item) {
+ const value = timeValue(item);
+ if (!value) return 0;
+ const normalized = /^\d{4}-\d{2}-\d{2}$/.test(String(value)) ? `${value}T00:00:00` : value;
+ const date = new Date(normalized);
+ return Number.isNaN(date.getTime()) ? 0 : date.getTime();
+ }
+
+ function sortByTime(rows) {
+ return [...rows].sort((a, b) => timeMs(b) - timeMs(a));
+ }
+
+ function fileName(path) {
+ const value = String(path || '').split('/').filter(Boolean).pop() || '';
+ return value || String(path || '').trim();
+ }
+
+ function titleFromPath(path) {
+ return fileName(path).replace(/\.(md|markdown|txt)$/i, '').replace(/_/g, ' ') || 'Untitled';
+ }
+
+ function quoted(value) {
+ const clean = String(value || '').trim();
+ return clean ? `"${clean}"` : '';
+ }
+
+ function entityName(item) {
+ const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {};
+ const path = payload.path || payload.notePath || item?.path || item?.relativePath || looksLikePath(item?.summary);
+ const title = String(item?.title || payload.title || '').trim();
+ if (path && (!title || /^(saved note|file opened|file changed|activity event)$/i.test(title))) return titleFromPath(path);
+ return title || titleFromPath(path) || String(item?.summary || item?.activityId || 'item');
+ }
+
+ function looksLikePath(value) {
+ const text = String(value || '').trim();
+ if (!text) return '';
+ return text.includes('/') || /\.[a-z0-9]{1,8}$/i.test(text) ? text : '';
}
function captureTitle(capture) {
- return capture.title || capture.fileName || capture.url || capture.captureId || 'Untitled capture';
+ return capture?.title || capture?.fileName || capture?.url || capture?.captureId || 'Untitled capture';
+ }
+
+ function journalTitle(entry) {
+ return entry?.title || entry?.summary || entry?.date || entry?.entryId || 'Journal entry';
+ }
+
+ function itemTimeLabel(item) {
+ const value = timeValue(item);
+ if (!value) return 'No timestamp';
+ return relativeTime(value);
+ }
+
+ function absoluteTime(value) {
+ const ms = timeMs({ occurredAt: value });
+ if (!ms) return '';
+ return new Date(ms).toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ }
+
+ function relativeTime(value) {
+ const ms = timeMs({ occurredAt: value });
+ if (!ms) return 'No timestamp';
+ const diff = Date.now() - ms;
+ if (diff < 0) return absoluteTime(value);
+ const minute = 60 * 1000;
+ const hour = 60 * minute;
+ const day = 24 * hour;
+ if (diff < minute) return 'Just now';
+ if (diff < hour) return `${Math.floor(diff / minute)} min ago`;
+ if (diff < day) return `${Math.floor(diff / hour)}h ago`;
+ if (diff < 2 * day) return 'Yesterday';
+ if (diff < 7 * day) return `${Math.floor(diff / day)} days ago`;
+ const date = new Date(ms);
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: date.getFullYear() === new Date().getFullYear() ? undefined : 'numeric' });
+ }
+
+ function activityCategory(item) {
+ const type = String(item?.type || '').toLowerCase();
+ if (type.startsWith('note.')) return 'notes';
+ if (type.startsWith('file.')) return 'files';
+ if (type.startsWith('browser.capture')) return 'captures';
+ if (type.startsWith('journal.') || type.startsWith('worklog.') || type.startsWith('action.')) return 'journal';
+ return 'activity';
}
function activityTitle(item) {
- return item.title || item.summary || humanActivityType(item.type) || 'Activity event';
+ const type = String(item?.type || '').toLowerCase();
+ const name = entityName(item);
+ if (type === 'note.saved' || type === 'note.edited') return `Edited note ${quoted(name)}`;
+ if (type === 'note.opened') return `Opened note ${quoted(name)}`;
+ if (type === 'note.created') return `Created note ${quoted(name)}`;
+ if (type === 'file.opened') return `Opened file ${quoted(name)}`;
+ if (type === 'file.changed') return `Changed file ${quoted(name)}`;
+ if (type === 'file.created') return `Created file ${quoted(name)}`;
+ if (type === 'file.deleted' || type === 'file.trashed') return `Removed file ${quoted(name)}`;
+ if (type === 'browser.capture.page') return `Captured page ${quoted(name)}`;
+ if (type === 'browser.capture.selection') return `Captured selection ${quoted(name)}`;
+ if (type === 'browser.capture.link') return `Captured link ${quoted(name)}`;
+ if (type === 'browser.capture.file') return `Captured file ${quoted(name)}`;
+ if (type === 'browser.capture.converted') return `Converted capture ${quoted(name)}`;
+ if (type === 'journal.entry.added' || type === 'worklog.entry.added') return `Added journal entry ${quoted(name)}`;
+ if (type === 'action.started') return 'Work session detected';
+ if (type === 'workspace.opened') return 'Workspace opened';
+ return item?.title || item?.summary || 'Workspace activity';
}
- function humanActivityType(type) {
- const labels = {
- 'workspace.selected': 'Workspace selected',
- 'case.selected': 'Workspace selected',
- 'file.opened': 'File opened',
- 'file.changed': 'File changed',
- 'note.saved': 'Note edited',
- 'browser.capture.received': 'Browser capture received',
- 'browser.capture.page': 'Page captured',
- 'browser.capture.selection': 'Selection captured',
- 'browser.capture.link': 'Link captured',
- 'browser.capture.file': 'File captured',
- 'browser.capture.converted': 'Capture converted',
- 'action.started': 'Work session detected'
- };
- return labels[String(type || '').toLowerCase()] || '';
+ function actionForCategory(category) {
+ if (category === 'notes') return { kind: 'notes', label: 'Open Notes' };
+ if (category === 'files') return { kind: 'files', label: 'Open Files' };
+ if (category === 'captures') return { kind: 'browser-inbox', label: 'Review Inbox' };
+ if (category === 'journal') return { kind: 'activity', label: 'View Activity' };
+ return { kind: 'activity', label: 'View Activity' };
}
- function worklogTitle(item) {
- return item.title || item.summary || item.date || item.entryId || 'Worklog item';
+ function buildRecentChanges(events, captureRows, journalRows) {
+ const activityItems = sortByTime(events)
+ .filter(item => !LOW_VALUE_RECENT_TYPES.has(String(item?.type || '').toLowerCase()))
+ .map(item => {
+ const category = activityCategory(item);
+ const action = actionForCategory(category);
+ return {
+ id: item.activityId || `${item.type}:${timeValue(item)}`,
+ category,
+ title: activityTitle(item),
+ meta: `${itemTimeLabel(item)}${item.sourcePluginId ? ' · ' + item.sourcePluginId.replace('verstak.', '') : ''}`,
+ time: timeValue(item),
+ absolute: absoluteTime(timeValue(item)),
+ actionKind: action.kind,
+ actionLabel: action.label,
+ };
+ });
+ const captureItems = captureRows.map(item => ({
+ id: item.captureId || `capture:${timeValue(item)}`,
+ category: 'captures',
+ title: `Captured ${item.kind || 'item'} ${quoted(captureTitle(item))}`,
+ meta: `${itemTimeLabel(item)} · ${item.domain || item.url || 'Browser capture'}`,
+ time: timeValue(item),
+ absolute: absoluteTime(timeValue(item)),
+ actionKind: 'browser-inbox',
+ actionLabel: 'Review Inbox',
+ }));
+ const journalItems = journalRows.map(item => ({
+ id: item.entryId || `journal:${timeValue(item)}`,
+ category: 'journal',
+ title: `Added journal entry ${quoted(journalTitle(item))}`,
+ meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + item.minutes + ' min' : ''}`,
+ time: timeValue(item),
+ absolute: absoluteTime(timeValue(item)),
+ actionKind: 'activity',
+ actionLabel: 'View Activity',
+ }));
+ return sortByTime([...activityItems, ...captureItems, ...journalItems]).slice(0, 12);
}
- function nextResumeItem(captureRows, worklogRows, activityRows, activityAvailable) {
- if (captureRows.length > 0) {
- const capture = captureRows[0];
- return {
- title: captureTitle(capture),
- meta: capture.url || capture.domain || capture.kind || 'Browser capture',
- kind: 'browser-inbox',
- action: 'Open Inbox',
- };
+ function isResumeEvent(item) {
+ const type = String(item?.type || '').toLowerCase();
+ if (LOW_VALUE_RESUME_TYPES.has(type)) return false;
+ return type === 'file.opened' ||
+ type === 'note.opened' ||
+ type === 'note.saved' ||
+ type === 'note.edited' ||
+ type === 'file.changed' ||
+ type === 'file.created' ||
+ type === 'browser.capture.converted';
+ }
+
+ function buildContinueItems(events, captureRows) {
+ const fromEvents = sortByTime(events)
+ .filter(isResumeEvent)
+ .map(item => {
+ const category = activityCategory(item);
+ const action = actionForCategory(category);
+ return {
+ id: item.activityId || `${item.type}:${timeValue(item)}`,
+ title: activityTitle(item),
+ meta: itemTimeLabel(item),
+ time: timeValue(item),
+ absolute: absoluteTime(timeValue(item)),
+ actionKind: action.kind,
+ actionLabel: action.label,
+ };
+ });
+ if (fromEvents.length) return fromEvents.slice(0, 4);
+ return sortByTime(captureRows).slice(0, 3).map(item => ({
+ id: item.captureId || `capture:${timeValue(item)}`,
+ title: `Review capture ${quoted(captureTitle(item))}`,
+ meta: `${itemTimeLabel(item)} · ${item.domain || item.kind || 'Browser capture'}`,
+ time: timeValue(item),
+ absolute: absoluteTime(timeValue(item)),
+ actionKind: 'browser-inbox',
+ actionLabel: 'Review Inbox',
+ }));
+ }
+
+ function buildNeedsAttention(captureRows, suggestions) {
+ const captureItems = sortByTime(captureRows).slice(0, 4).map(item => ({
+ id: item.captureId || `capture:${timeValue(item)}`,
+ title: captureTitle(item),
+ meta: `${item.kind || 'capture'} · ${itemTimeLabel(item)}`,
+ actionKind: 'browser-inbox',
+ actionLabel: 'Review Inbox',
+ }));
+ const suggestionItems = sortByTime(suggestions).slice(0, 4).map(item => ({
+ id: item.suggestionId || item.entryId || `suggestion:${timeValue(item)}`,
+ title: item.title || item.summary || 'Pending worklog suggestion',
+ meta: `${item.minutes ? item.minutes + ' min · ' : ''}${item.date || itemTimeLabel(item)}`,
+ actionKind: 'activity',
+ actionLabel: 'View Activity',
+ }));
+ return [...captureItems, ...suggestionItems].slice(0, 6);
+ }
+
+ function countCategory(items, category) {
+ return items.filter(item => item.category === category).length;
+ }
+
+ function countLabel(count, singular) {
+ return `${count} ${singular}${count === 1 ? '' : 's'}`;
+ }
+
+ function lastActiveDate(items, captureRows, journalRows) {
+ const source = sortByTime([...items, ...captureRows, ...journalRows])[0];
+ return timeValue(source);
+ }
+
+ async function listFiles(relativeDir) {
+ if (!App.ListVaultFiles) return [];
+ try {
+ return decodeTuple(await App.ListVaultFiles('verstak.files', relativeDir), []);
+ } catch (_) {
+ return [];
}
- if (worklogRows.length > 0) {
- const item = worklogRows[0];
- return {
- title: worklogTitle(item),
- meta: item.minutes ? item.minutes + ' min suggested' : item.date || 'Suggested worklog',
- kind: 'activity',
- action: activityAvailable ? 'Review Activity' : 'Refresh',
- };
- }
- if (activityRows.length > 0) {
- const item = activityRows[0];
- return {
- title: activityTitle(item),
- meta: item.occurredAt || item.receivedAt || item.type || 'Activity event',
- kind: 'activity',
- action: 'Open Activity',
- };
- }
- return null;
}
- async function loadToday() {
+ async function loadKeyResources() {
+ const workspace = String(workspaceRootPath || '').trim();
+ if (!workspace) return [];
+ const [rootEntries, notesEntries] = await Promise.all([
+ listFiles(workspace),
+ listFiles(`${workspace}/Notes`),
+ ]);
+ const overview = [...notesEntries, ...rootEntries].find(item => /(^|\/)overview\.md$/i.test(String(item.relativePath || item.name || '')));
+ if (!overview) return [];
+ return [{
+ id: overview.relativePath || overview.name,
+ title: overview.name || fileName(overview.relativePath) || 'Overview.md',
+ meta: overview.relativePath || 'Workspace overview note',
+ actionKind: hasNotes ? 'notes' : 'files',
+ actionLabel: hasNotes ? 'Open Notes' : 'Open Files',
+ }];
+ }
+
+ async function loadOverview() {
+ const workspaceAtStart = String(workspaceRootPath || '').trim();
+ loadedWorkspaceRoot = workspaceAtStart;
loading = true;
- const [browserSettings, activitySettings, journalSettings] = await Promise.all([
+ const [browserSettings, activitySettings, journalSettings, resources] = await Promise.all([
readPluginSettings('verstak.browser-inbox'),
readPluginSettings('verstak.activity'),
readPluginSettings('verstak.journal'),
+ loadKeyResources(),
]);
+ if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
captures = rowsFor(browserSettings, [
workspaceKey('captures:workspace:'),
'captures:global',
'captures',
- ]).slice(0, 4);
-
- activity = rowsFor(activitySettings, [
+ ]);
+ activityEvents = rowsFor(activitySettings, [
workspaceKey('events:workspace:'),
'events:global',
'events',
- ]).slice(0, 4);
-
+ ]);
+ journalEntries = rowsFor(journalSettings, [
+ workspaceKey('worklog:workspace:'),
+ 'worklog',
+ ]);
worklogSuggestions = rowsFor(journalSettings, [
workspaceKey('suggestions:workspace:'),
- workspaceKey('worklog:workspace:'),
'suggestions',
- 'worklog',
- ]).slice(0, 4);
-
+ ]);
+ keyResources = resources;
loading = false;
}
@@ -156,124 +409,154 @@
}
-
-