feat: replace Today workspace surface with Overview

This commit is contained in:
mirivlad 2026-07-10 02:36:00 +08:00
parent 5b4c792de5
commit b845e8b5e2
7 changed files with 686 additions and 234 deletions

View File

@ -52,7 +52,7 @@ test.describe('Command Palette', () => {
const palette = page.locator('.command-palette'); const palette = page.locator('.command-palette');
await expect(palette).toBeVisible(); await expect(palette).toBeVisible();
const items = palette.locator('.command-palette-item'); 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(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(2)).toHaveAttribute('data-command-id', 'verstak.shell.open-activity');
await expect(items.nth(3)).toHaveAttribute('data-command-id', 'verstak.shell.open-browser-inbox'); await expect(items.nth(3)).toHaveAttribute('data-command-id', 'verstak.shell.open-browser-inbox');

View File

@ -99,19 +99,19 @@ test.describe('E: Plugin Manager layout', () => {
await expect(selected).toHaveText('Test'); 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(); await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
const tabs = page.locator('.workspace-tabs'); const tabs = page.locator('.workspace-tabs');
await expect(tabs).toBeVisible({ timeout: 10000 }); 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' }); const filesTab = tabs.locator('[role="tab"]').filter({ hasText: 'Files' });
await expect(todayTab).toBeVisible(); await expect(overviewTab).toBeVisible();
await expect(todayTab).toHaveAttribute('aria-selected', 'true'); await expect(overviewTab).toHaveAttribute('aria-selected', 'true');
await expect(filesTab).toBeVisible(); await expect(filesTab).toBeVisible();
await expect(filesTab).toHaveAttribute('aria-selected', 'false'); await expect(filesTab).toHaveAttribute('aria-selected', 'false');
await expect(page.locator('.workspace-tool')).toHaveCount(0); 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 filesTab.click();
await expect(page.locator('.files-root')).toBeVisible(); await expect(page.locator('.files-root')).toBeVisible();

View File

@ -99,7 +99,7 @@ test.describe('UX follow-up fixes', () => {
expect(workspaceBox.width).toBeGreaterThan(340); expect(workspaceBox.width).toBeGreaterThan(340);
expect(workspaceBox.y).toBeGreaterThan(sidebarBox.y + sidebarBox.height - 1); expect(workspaceBox.y).toBeGreaterThan(sidebarBox.y + sidebarBox.height - 1);
await expect(page.locator('.workspace-header [data-global-search-input]')).toBeVisible(); 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); const hasHorizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth);
expect(hasHorizontalOverflow).toBe(false); expect(hasHorizontalOverflow).toBe(false);

View File

@ -1,7 +1,7 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js'; import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
test.describe('UX Today workspace flow', () => { test.describe('UX Overview workspace flow', () => {
let consoleCollector; let consoleCollector;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -15,37 +15,40 @@ test.describe('UX Today workspace flow', () => {
consoleCollector.assertNoErrors(); 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 }); await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
const tabs = page.getByRole('tab'); 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(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'); const overview = page.locator('[data-overview-root]');
await expect(today).toBeVisible(); await expect(overview).toBeVisible();
await expect(today.locator('[data-today-section="captured"]')).toContainText('Captured'); await expect(overview.locator('[data-overview-section="continue"]')).toContainText('Continue working');
await expect(today.locator('[data-today-section="activity"]')).toContainText('Recent Activity'); await expect(overview.locator('[data-overview-section="recent"]')).toContainText('Recent changes');
await expect(today.locator('[data-today-section="worklog"]')).toContainText('Worklog Suggestions'); await expect(overview.locator('[data-overview-section="attention"]')).toContainText('Needs attention');
await expect(today.locator('[data-today-section="quick-actions"]')).toContainText('Quick Actions'); await expect(overview.locator('[data-overview-section="quick-actions"]')).toContainText('Quick actions');
await expect(today).toContainText('No browser captures yet'); await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('recent changes');
await expect(today).toContainText('No activity events yet'); 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 }) => { test('Overview quick action opens Browser Inbox workspace tool', async ({ page }) => {
await page.locator('[data-today-action="browser-inbox"]').click(); await page.locator('[data-overview-action="browser-inbox"]').click();
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true'); 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.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 page.evaluate(async () => {
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', { await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
'captures:workspace:Project': [ 'captures:workspace:Project': [
{ {
captureId: 'today-capture-1', captureId: 'overview-capture-1',
capturedAt: '2026-06-30T08:00:00.000Z', capturedAt: '2026-06-30T08:00:00.000Z',
kind: 'page', kind: 'page',
url: 'https://example.com/research', url: 'https://example.com/research',
@ -54,7 +57,7 @@ test.describe('UX Today workspace flow', () => {
workspaceRootPath: 'Project', workspaceRootPath: 'Project',
}, },
{ {
captureId: 'today-capture-2', captureId: 'overview-capture-2',
capturedAt: '2026-06-30T08:15:00.000Z', capturedAt: '2026-06-30T08:15:00.000Z',
kind: 'selection', kind: 'selection',
title: 'Quote to process', title: 'Quote to process',
@ -66,41 +69,108 @@ test.describe('UX Today workspace flow', () => {
await window.go.api.App.WritePluginSettings('verstak.activity', { await window.go.api.App.WritePluginSettings('verstak.activity', {
'events:workspace:Project': [ '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', occurredAt: '2026-06-30T08:25:00.000Z',
type: 'note.saved', type: 'note.saved',
title: 'Saved research note', title: 'Overview',
summary: 'Project/Notes/Research.md', 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', workspaceRootPath: 'Project',
}, },
], ],
}); });
await window.go.api.App.WritePluginSettings('verstak.journal', { 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', title: 'Write project summary',
summary: 'Turn recent captures into a worklog entry', summary: 'Turn recent captures into a worklog entry',
minutes: 35, minutes: 35,
workspaceRootPath: 'Project', 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'); const overview = page.locator('[data-overview-root]');
await expect(today.locator('[data-today-summary="captures"]')).toContainText('2'); await expect(overview.locator('[data-overview-summary="notes"]')).toContainText('1');
await expect(today.locator('[data-today-summary="activity"]')).toContainText('1'); await expect(overview.locator('[data-overview-summary="files"]')).toContainText('1');
await expect(today.locator('[data-today-summary="worklog"]')).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"]'); const resume = overview.locator('[data-overview-section="continue"]');
await expect(resume).toContainText('Resume next'); await expect(resume).toContainText('Opened file "draft.md"');
await expect(resume).toContainText('Research Report'); await expect(resume).not.toContainText('Research Report');
await resume.locator('[data-today-action="resume-primary"]').click(); await resume.locator('[data-overview-action="continue-primary"]').click();
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true'); await expect(page.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 }); 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"');
}); });
}); });

View File

@ -13,7 +13,7 @@
const inactiveStatuses = new Set(['disabled', 'failed', 'incompatible', 'missing-required-capability']); const inactiveStatuses = new Set(['disabled', 'failed', 'incompatible', 'missing-required-capability']);
const shellCommands = [ 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-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-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' }, { 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; return;
} }
const actionToTab = { const actionToTab = {
today: 'today', overview: 'overview',
files: 'files', files: 'files',
activity: 'activity', activity: 'activity',
'browser-inbox': 'browser inbox', 'browser-inbox': 'browser inbox',

View File

@ -3,36 +3,79 @@
import * as App from '../../../wailsjs/go/api/App'; import * as App from '../../../wailsjs/go/api/App';
export let workspaceRootPath = ''; export let workspaceRootPath = '';
export let workspaceTitle = '';
export let availableTools = []; export let availableTools = [];
const dispatch = createEventDispatcher(); 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 loading = true;
let activeFilter = 'all';
let captures = []; let captures = [];
let activity = []; let activityEvents = [];
let journalEntries = [];
let worklogSuggestions = []; let worklogSuggestions = [];
let keyResources = [];
let loadedWorkspaceRoot = '';
let toolProbe = 0;
$: hasNotes = hasTool('notes');
$: hasBrowserInbox = hasTool('browser inbox') || hasTool('inbox'); $: hasBrowserInbox = hasTool('browser inbox') || hasTool('inbox');
$: hasActivity = hasTool('activity'); $: hasActivity = hasTool('activity');
$: hasFiles = hasTool('files'); $: 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 = [ $: summaryItems = [
{ key: 'captures', label: 'Captured', count: captures.length }, { key: 'notes', label: 'Notes', count: countCategory(recentChanges, 'notes'), detail: countLabel(countCategory(recentChanges, 'notes'), 'recent change') },
{ key: 'activity', label: 'Activity', count: activity.length }, { key: 'files', label: 'Files', count: countCategory(recentChanges, 'files'), detail: countLabel(countCategory(recentChanges, 'files'), 'recent change') },
{ key: 'worklog', label: 'Worklog', count: worklogSuggestions.length }, { 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(() => { onMount(() => {
loadToday(); toolProbe += 1;
}); });
$: if (workspaceRootPath && workspaceRootPath !== loadedWorkspaceRoot) {
loadOverview();
}
function hasTool(name) { function hasTool(name) {
toolProbe;
name = String(name || '').toLowerCase(); name = String(name || '').toLowerCase();
return (availableTools || []).some(tool => { const fromProps = (availableTools || []).some(tool => {
const label = String(tool?.title || tool?.id || tool?.pluginId || '').toLowerCase(); const label = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
return label.includes(name); 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) { function decodeTuple(response, fallback) {
@ -57,97 +100,307 @@
} }
function rowsFor(settings, keys) { 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) { 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) { 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) { function actionForCategory(category) {
const labels = { if (category === 'notes') return { kind: 'notes', label: 'Open Notes' };
'workspace.selected': 'Workspace selected', if (category === 'files') return { kind: 'files', label: 'Open Files' };
'case.selected': 'Workspace selected', if (category === 'captures') return { kind: 'browser-inbox', label: 'Review Inbox' };
'file.opened': 'File opened', if (category === 'journal') return { kind: 'activity', label: 'View Activity' };
'file.changed': 'File changed', return { kind: 'activity', label: 'View Activity' };
'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 worklogTitle(item) { function buildRecentChanges(events, captureRows, journalRows) {
return item.title || item.summary || item.date || item.entryId || 'Worklog item'; 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) { function isResumeEvent(item) {
if (captureRows.length > 0) { const type = String(item?.type || '').toLowerCase();
const capture = captureRows[0]; if (LOW_VALUE_RESUME_TYPES.has(type)) return false;
return { return type === 'file.opened' ||
title: captureTitle(capture), type === 'note.opened' ||
meta: capture.url || capture.domain || capture.kind || 'Browser capture', type === 'note.saved' ||
kind: 'browser-inbox', type === 'note.edited' ||
action: 'Open Inbox', 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; loading = true;
const [browserSettings, activitySettings, journalSettings] = await Promise.all([ const [browserSettings, activitySettings, journalSettings, resources] = await Promise.all([
readPluginSettings('verstak.browser-inbox'), readPluginSettings('verstak.browser-inbox'),
readPluginSettings('verstak.activity'), readPluginSettings('verstak.activity'),
readPluginSettings('verstak.journal'), readPluginSettings('verstak.journal'),
loadKeyResources(),
]); ]);
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
captures = rowsFor(browserSettings, [ captures = rowsFor(browserSettings, [
workspaceKey('captures:workspace:'), workspaceKey('captures:workspace:'),
'captures:global', 'captures:global',
'captures', 'captures',
]).slice(0, 4); ]);
activityEvents = rowsFor(activitySettings, [
activity = rowsFor(activitySettings, [
workspaceKey('events:workspace:'), workspaceKey('events:workspace:'),
'events:global', 'events:global',
'events', 'events',
]).slice(0, 4); ]);
journalEntries = rowsFor(journalSettings, [
workspaceKey('worklog:workspace:'),
'worklog',
]);
worklogSuggestions = rowsFor(journalSettings, [ worklogSuggestions = rowsFor(journalSettings, [
workspaceKey('suggestions:workspace:'), workspaceKey('suggestions:workspace:'),
workspaceKey('worklog:workspace:'),
'suggestions', 'suggestions',
'worklog', ]);
]).slice(0, 4); keyResources = resources;
loading = false; loading = false;
} }
@ -156,124 +409,154 @@
} }
</script> </script>
<div class="today-root" aria-label="Today"> <div class="today-root overview-root" aria-label="Overview" data-overview-root>
<div class="today-header"> <div class="today-header overview-header">
<div> <div>
<h2>Today</h2> <h2>Overview</h2>
<p>{workspaceTitle || workspaceRootPath || 'Workspace'} overview</p> <p title={lastActive ? absoluteTime(lastActive) : ''}>
{#if loading}
Loading workspace context...
{:else if lastActive}
Last active {relativeTime(lastActive)}
{:else}
No recent workspace activity
{/if}
</p>
</div> </div>
<button type="button" on:click={loadToday}>Refresh</button> <button type="button" data-overview-action="refresh" on:click={loadOverview}>Refresh</button>
</div> </div>
<div class="today-summary" aria-label="Today summary"> <div class="today-summary overview-summary" aria-label="Workspace overview summary">
{#each summaryItems as item} {#each summaryItems as item}
<div class="today-summary-item" data-today-summary={item.key}> <div class="today-summary-item overview-summary-item" data-overview-summary={item.key}>
<strong>{loading ? '...' : item.count}</strong> <strong>{loading ? '...' : item.count}</strong>
<span>{item.label}</span> <span>{item.label}</span>
<small>{loading ? 'Loading...' : item.detail}</small>
</div> </div>
{/each} {/each}
</div> </div>
<section class="today-resume" data-today-section="resume"> <div class="overview-layout">
<div class="today-resume-copy"> <main class="overview-main">
<span>Resume next</span> <section class="today-resume overview-continue" data-overview-section="continue">
{#if loading} <div class="today-resume-copy overview-continue-copy">
<strong>Loading workspace signals...</strong> <span>Continue working</span>
<p>Recent captures, activity, and worklog suggestions will appear here.</p> {#if loading}
{:else if resumeItem} <strong>Loading workspace signals...</strong>
<strong>{resumeItem.title}</strong> <p>Recent files, notes, captures, and journal entries will appear here.</p>
<p>{resumeItem.meta}</p> {:else if primaryContinue}
{:else} <strong title={primaryContinue.title}>{primaryContinue.title}</strong>
<strong>No pending workspace signals</strong> <p title={primaryContinue.absolute}>{primaryContinue.meta}</p>
<p>Start with files, capture something from the browser, or review plugin activity.</p> {:else}
{/if} <strong>No clear resume point yet</strong>
</div> <p>Open files or notes to create a useful return point for this workspace.</p>
{#if resumeItem} {/if}
<button type="button" data-today-action="resume-primary" on:click={() => openTool(resumeItem.kind)}>{resumeItem.action}</button>
{:else if hasFiles}
<button type="button" data-today-action="resume-primary" on:click={() => openTool('files')}>Open Files</button>
{/if}
</section>
<div class="today-grid">
<section class="today-panel" data-today-section="captured">
<div class="today-panel-head">
<h3>Captured</h3>
<button type="button" data-today-action="browser-inbox" on:click={() => openTool('browser-inbox')}>Open Inbox</button>
</div>
{#if loading}
<p class="today-empty">Loading captures...</p>
{:else if captures.length}
<div class="today-list">
{#each captures as capture}
<div class="today-row">
<strong>{captureTitle(capture)}</strong>
<span>{capture.url || capture.domain || capture.kind || 'Browser capture'}</span>
</div>
{/each}
</div> </div>
{:else} {#if primaryContinue}
<p class="today-empty">No browser captures yet. Send a page, selection, link, or file from the browser extension.</p> <button type="button" data-overview-action="continue-primary" on:click={() => openTool(primaryContinue.actionKind)}>{primaryContinue.actionLabel}</button>
{/if} {:else}
</section> <button type="button" data-overview-action="continue-primary" on:click={() => openTool('files')}>Open Files</button>
<section class="today-panel" data-today-section="activity">
<div class="today-panel-head">
<h3>Recent Activity</h3>
{#if hasActivity}
<button type="button" data-today-action="activity" on:click={() => openTool('activity')}>Open Activity</button>
{/if} {/if}
</div> </section>
{#if loading}
<p class="today-empty">Loading activity...</p> <section class="today-panel overview-panel overview-recent" data-overview-section="recent">
{:else if activity.length} <div class="today-panel-head overview-panel-head">
<div class="today-list"> <div>
{#each activity as item} <h3>Recent changes</h3>
<div class="today-row"> <p>Latest meaningful activity in this workspace.</p>
<strong>{activityTitle(item)}</strong> </div>
<span>{item.occurredAt || item.receivedAt || item.type || 'Activity event'}</span> <div class="overview-filters" aria-label="Recent changes filter">
</div> {#each FILTERS as filter}
{/each} <button
type="button"
class:is-active={activeFilter === filter.key}
aria-pressed={activeFilter === filter.key}
data-overview-filter={filter.key}
on:click={() => activeFilter = filter.key}
>
{filter.label}
</button>
{/each}
</div>
</div> </div>
{:else} {#if loading}
<p class="today-empty">No activity events yet. File changes, captures, and conversions will appear here.</p> <p class="today-empty">Loading recent changes...</p>
{/if} {:else if filteredRecentChanges.length}
</section> <div class="today-list overview-list">
{#each filteredRecentChanges as item}
<div class="today-row overview-change-row" data-overview-recent-item={item.category}>
<div>
<strong title={item.title}>{item.title}</strong>
<span title={item.absolute}>{item.meta}</span>
</div>
<button type="button" on:click={() => openTool(item.actionKind)}>{item.actionLabel}</button>
</div>
{/each}
</div>
{:else}
<p class="today-empty">No meaningful changes for this filter yet.</p>
{/if}
</section>
</main>
<section class="today-panel" data-today-section="worklog"> <aside class="overview-side">
<div class="today-panel-head"> {#if needsAttention.length || !loading}
<h3>Worklog Suggestions</h3> <section class="today-panel overview-panel" data-overview-section="attention">
</div> <div class="today-panel-head overview-panel-head">
{#if loading} <div>
<p class="today-empty">Loading worklog suggestions...</p> <h3>Needs attention</h3>
{:else if worklogSuggestions.length} <p>Pending captures and worklog suggestions.</p>
<div class="today-list">
{#each worklogSuggestions as item}
<div class="today-row">
<strong>{worklogTitle(item)}</strong>
<span>{item.minutes ? item.minutes + ' min' : item.date || 'Suggested worklog'}</span>
</div> </div>
{/each} </div>
</div> {#if loading}
{:else} <p class="today-empty">Loading pending items...</p>
<p class="today-empty">No worklog suggestions yet. Activity will be grouped into suggestions once work starts.</p> {:else if needsAttention.length}
<div class="today-list overview-list compact">
{#each needsAttention as item}
<div class="today-row overview-attention-row">
<strong title={item.title}>{item.title}</strong>
<span>{item.meta}</span>
<button type="button" on:click={() => openTool(item.actionKind)}>{item.actionLabel}</button>
</div>
{/each}
</div>
{:else}
<p class="today-empty compact">No pending captures or worklog suggestions.</p>
{/if}
</section>
{/if} {/if}
</section>
<section class="today-panel" data-today-section="quick-actions"> <section class="today-panel overview-panel secondary" data-overview-section="quick-actions">
<div class="today-panel-head"> <div class="today-panel-head overview-panel-head">
<h3>Quick Actions</h3> <h3>Quick actions</h3>
</div> </div>
<div class="today-actions"> <div class="today-actions overview-actions">
{#if hasFiles} {#if hasNotes}
<button type="button" data-today-action="files" on:click={() => openTool('files')}>Open Files</button> <button type="button" data-overview-action="notes" on:click={() => openTool('notes')}>Open Notes</button>
{/if} {/if}
<button type="button" on:click={() => openTool('browser-inbox')}>Process Captures</button> <button type="button" data-overview-action="files" on:click={() => openTool('files')}>Open Files</button>
{#if hasActivity} <button type="button" data-overview-action="activity" on:click={() => openTool('activity')}>Review Activity</button>
<button type="button" on:click={() => openTool('activity')}>Review Activity</button> <button type="button" data-overview-action="browser-inbox" on:click={() => openTool('browser-inbox')}>Open Inbox</button>
{/if} </div>
</div> </section>
</section>
{#if keyResources.length}
<section class="today-panel overview-panel secondary" data-overview-section="key-resources">
<div class="today-panel-head overview-panel-head">
<h3>Key resources</h3>
</div>
<div class="today-list overview-list compact">
{#each keyResources as item}
<div class="today-row overview-resource-row">
<strong title={item.title}>{item.title}</strong>
<span title={item.meta}>{item.meta}</span>
<button type="button" on:click={() => openTool(item.actionKind)}>{item.actionLabel}</button>
</div>
{/each}
</div>
</section>
{/if}
</aside>
</div> </div>
</div> </div>
@ -309,16 +592,9 @@
font-size: 0.8rem; font-size: 0.8rem;
} }
.today-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
padding: 0.75rem;
}
.today-summary { .today-summary {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem 0.75rem 0; padding: 0.75rem 0.75rem 0;
} }
@ -326,7 +602,7 @@
.today-summary-item { .today-summary-item {
min-width: 0; min-width: 0;
display: grid; display: grid;
gap: 0.15rem; gap: 0.16rem;
padding: 0.65rem 0.75rem; padding: 0.65rem 0.75rem;
border: 1px solid var(--vt-color-border); border: 1px solid var(--vt-color-border);
border-radius: var(--vt-radius-lg); border-radius: var(--vt-radius-lg);
@ -339,7 +615,8 @@
line-height: 1; line-height: 1;
} }
.today-summary-item span { .today-summary-item span,
.today-summary-item small {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -348,13 +625,33 @@
font-size: 0.74rem; font-size: 0.74rem;
} }
.today-summary-item span {
color: var(--vt-color-text-secondary);
font-weight: 600;
}
.overview-layout {
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(17rem, 23rem);
gap: 0.75rem;
padding: 0.75rem;
}
.overview-main,
.overview-side {
min-width: 0;
display: grid;
align-content: start;
gap: 0.75rem;
}
.today-resume { .today-resume {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
margin: 0.75rem 0.75rem 0; padding: 0.9rem 1rem;
padding: 0.85rem 1rem;
border: 1px solid rgba(78, 204, 163, 0.24); border: 1px solid rgba(78, 204, 163, 0.24);
border-radius: var(--vt-radius-lg); border-radius: var(--vt-radius-lg);
background: linear-gradient(135deg, rgba(78, 204, 163, 0.11), rgba(27, 36, 64, 0.6)); background: linear-gradient(135deg, rgba(78, 204, 163, 0.11), rgba(27, 36, 64, 0.6));
@ -395,7 +692,6 @@
.today-panel { .today-panel {
min-width: 0; min-width: 0;
min-height: 10rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid var(--vt-color-border); border: 1px solid var(--vt-color-border);
@ -403,6 +699,14 @@
background: var(--vt-color-surface); background: var(--vt-color-surface);
} }
.overview-recent {
min-height: 24rem;
}
.overview-panel.secondary {
background: var(--vt-color-surface-muted);
}
.today-panel-head { .today-panel-head {
min-height: 2.8rem; min-height: 2.8rem;
display: flex; display: flex;
@ -419,12 +723,59 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
.today-panel-head p {
margin: 0.2rem 0 0;
color: var(--vt-color-text-muted);
font-size: 0.74rem;
}
.overview-filters {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.2rem;
border: 1px solid var(--vt-color-border);
border-radius: var(--vt-radius-md);
background: var(--vt-color-background);
}
.overview-filters button,
.today-panel-head button, .today-panel-head button,
.today-actions button, .today-actions button,
.today-header button { .today-header button,
.today-resume button,
.overview-list button {
min-height: 1.85rem; min-height: 1.85rem;
padding: 0.3rem 0.65rem; padding: 0.3rem 0.65rem;
border: 1px solid var(--vt-color-border-strong);
border-radius: var(--vt-radius-md);
background: var(--vt-color-surface-hover);
color: var(--vt-color-text-secondary);
font-size: 0.76rem; font-size: 0.76rem;
cursor: pointer;
}
.overview-filters button {
min-height: 1.55rem;
padding: 0.16rem 0.5rem;
border-color: transparent;
background: transparent;
}
.overview-filters button.is-active,
.overview-filters button:hover,
.today-panel-head button:hover,
.today-actions button:hover,
.today-header button:hover,
.today-resume button:hover,
.overview-list button:hover {
border-color: var(--vt-color-accent);
color: var(--vt-color-text-primary);
}
.overview-filters button.is-active {
background: var(--vt-color-accent-muted);
color: var(--vt-color-accent);
} }
.today-empty { .today-empty {
@ -440,12 +791,20 @@
text-align: center; text-align: center;
} }
.today-empty.compact {
min-height: 5rem;
}
.today-list { .today-list {
display: grid; display: grid;
gap: 0.45rem; gap: 0.45rem;
padding: 0.65rem; padding: 0.65rem;
} }
.today-list.compact {
gap: 0.4rem;
}
.today-row { .today-row {
min-width: 0; min-width: 0;
display: grid; display: grid;
@ -456,6 +815,17 @@
background: var(--vt-color-surface-muted); background: var(--vt-color-surface-muted);
} }
.overview-change-row {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
}
.overview-attention-row,
.overview-resource-row {
grid-template-columns: minmax(0, 1fr);
}
.today-row strong { .today-row strong {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@ -481,13 +851,20 @@
padding: 0.75rem; padding: 0.75rem;
} }
@media (max-width: 860px) { @media (max-width: 980px) {
.today-grid { .overview-layout,
.today-summary {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.today-summary { .today-panel-head {
grid-template-columns: 1fr; align-items: stretch;
flex-direction: column;
}
.overview-filters {
overflow-x: auto;
justify-content: flex-start;
} }
.today-resume { .today-resume {
@ -498,5 +875,10 @@
.today-resume button { .today-resume button {
width: 100%; width: 100%;
} }
.overview-change-row {
grid-template-columns: 1fr;
align-items: stretch;
}
} }
</style> </style>

View File

@ -14,7 +14,8 @@
let workspaceTools = []; let workspaceTools = [];
let toolsLoaded = false; let toolsLoaded = false;
let requestedToolKind = ''; let requestedToolKind = '';
const todayTool = { id: '__today', title: 'Today', pluginId: 'verstak.shell', component: 'TodaySurface', shell: true }; // TODO: Rename TodaySurface.svelte to OverviewSurface.svelte in a refactor-only follow-up.
const overviewTool = { id: '__overview', title: 'Overview', pluginId: 'verstak.shell', component: 'TodaySurface', shell: true };
const toolOrder = new Map([ const toolOrder = new Map([
['notes', 10], ['notes', 10],
@ -29,10 +30,10 @@
$: workspaceRootPath = selectedWorkspace?.rootPath || selectedWorkspace?.name || selectedWorkspace?.id || ''; $: workspaceRootPath = selectedWorkspace?.rootPath || selectedWorkspace?.name || selectedWorkspace?.id || '';
$: workspaceTitle = selectedWorkspace?.title || selectedWorkspace?.name || selectedWorkspace?.id || selectedWorkspaceName; $: workspaceTitle = selectedWorkspace?.title || selectedWorkspace?.name || selectedWorkspace?.id || selectedWorkspaceName;
$: workspaceType = selectedWorkspace?.type || 'workspace'; $: workspaceType = selectedWorkspace?.type || 'workspace';
$: displayedTools = selectedWorkspace ? [todayTool, ...workspaceTools] : []; $: displayedTools = selectedWorkspace ? [overviewTool, ...workspaceTools] : [];
$: activeTool = displayedTools.find(tool => toolKey(tool) === activeToolKey) || displayedTools[0] || null; $: activeTool = displayedTools.find(tool => toolKey(tool) === activeToolKey) || displayedTools[0] || null;
$: if (displayedTools.length > 0 && (!activeToolKey || (toolsLoaded && !displayedTools.some(tool => toolKey(tool) === activeToolKey)))) { $: if (displayedTools.length > 0 && (!activeToolKey || (toolsLoaded && !displayedTools.some(tool => toolKey(tool) === activeToolKey)))) {
activeToolKey = toolKey(todayTool); activeToolKey = toolKey(overviewTool);
} }
$: if (requestedToolKind && workspaceTools.length > 0) { $: if (requestedToolKind && workspaceTools.length > 0) {
const match = findWorkspaceTool(requestedToolKind); const match = findWorkspaceTool(requestedToolKind);
@ -161,8 +162,7 @@
{#if activeTool.shell} {#if activeTool.shell}
<TodaySurface <TodaySurface
{workspaceRootPath} {workspaceRootPath}
{workspaceTitle} availableTools={displayedTools}
availableTools={workspaceTools}
on:openTool={openWorkspaceTool} on:openTool={openWorkspaceTool}
/> />
{:else} {:else}