Improve Today workspace resume surface

This commit is contained in:
mirivlad 2026-07-01 02:56:50 +08:00
parent 80e53ff252
commit 67468e92f7
2 changed files with 221 additions and 0 deletions

View File

@ -39,4 +39,68 @@ test.describe('UX Today workspace flow', () => {
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 }) => {
await page.evaluate(async () => {
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
'captures:workspace:Project': [
{
captureId: 'today-capture-1',
capturedAt: '2026-06-30T08:00:00.000Z',
kind: 'page',
url: 'https://example.com/research',
title: 'Research Report',
domain: 'example.com',
workspaceRootPath: 'Project',
},
{
captureId: 'today-capture-2',
capturedAt: '2026-06-30T08:15:00.000Z',
kind: 'selection',
title: 'Quote to process',
domain: 'example.com',
workspaceRootPath: 'Project',
},
],
});
await window.go.api.App.WritePluginSettings('verstak.activity', {
'events:workspace:Project': [
{
activityId: 'today-activity-1',
occurredAt: '2026-06-30T08:25:00.000Z',
type: 'note.saved',
title: 'Saved research note',
summary: 'Project/Notes/Research.md',
workspaceRootPath: 'Project',
},
],
});
await window.go.api.App.WritePluginSettings('verstak.journal', {
'suggestions:workspace:Project': [
{
entryId: 'today-worklog-1',
title: 'Write project summary',
summary: 'Turn recent captures into a worklog entry',
minutes: 35,
workspaceRootPath: 'Project',
},
],
});
});
await page.locator('.today-header').getByRole('button', { name: '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 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();
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
});
}); });

View File

@ -16,6 +16,12 @@
$: hasBrowserInbox = hasTool('browser inbox') || hasTool('inbox'); $: hasBrowserInbox = hasTool('browser inbox') || hasTool('inbox');
$: hasActivity = hasTool('activity'); $: hasActivity = hasTool('activity');
$: hasFiles = hasTool('files'); $: hasFiles = hasTool('files');
$: summaryItems = [
{ key: 'captures', label: 'Captured', count: captures.length },
{ key: 'activity', label: 'Activity', count: activity.length },
{ key: 'worklog', label: 'Worklog', count: worklogSuggestions.length },
];
$: resumeItem = nextResumeItem(captures, worklogSuggestions, activity, hasActivity);
onMount(() => { onMount(() => {
loadToday(); loadToday();
@ -66,6 +72,37 @@
return item.title || item.summary || item.date || item.entryId || 'Worklog item'; return item.title || item.summary || item.date || item.entryId || 'Worklog item';
} }
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',
};
}
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 loadToday() {
loading = true; loading = true;
const [browserSettings, activitySettings, journalSettings] = await Promise.all([ const [browserSettings, activitySettings, journalSettings] = await Promise.all([
@ -110,6 +147,36 @@
<button type="button" on:click={loadToday}>Refresh</button> <button type="button" on:click={loadToday}>Refresh</button>
</div> </div>
<div class="today-summary" aria-label="Today summary">
{#each summaryItems as item}
<div class="today-summary-item" data-today-summary={item.key}>
<strong>{loading ? '...' : item.count}</strong>
<span>{item.label}</span>
</div>
{/each}
</div>
<section class="today-resume" data-today-section="resume">
<div class="today-resume-copy">
<span>Resume next</span>
{#if loading}
<strong>Loading workspace signals...</strong>
<p>Recent captures, activity, and worklog suggestions will appear here.</p>
{:else if resumeItem}
<strong>{resumeItem.title}</strong>
<p>{resumeItem.meta}</p>
{:else}
<strong>No pending workspace signals</strong>
<p>Start with files, capture something from the browser, or review plugin activity.</p>
{/if}
</div>
{#if resumeItem}
<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"> <div class="today-grid">
<section class="today-panel" data-today-section="captured"> <section class="today-panel" data-today-section="captured">
<div class="today-panel-head"> <div class="today-panel-head">
@ -231,6 +298,83 @@
padding: 0.75rem; padding: 0.75rem;
} }
.today-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
padding: 0.75rem 0.75rem 0;
}
.today-summary-item {
min-width: 0;
display: grid;
gap: 0.15rem;
padding: 0.65rem 0.75rem;
border: 1px solid rgba(78, 204, 163, 0.16);
border-radius: 8px;
background: #121a2c;
}
.today-summary-item strong {
color: #f4f7fb;
font-size: 1rem;
line-height: 1;
}
.today-summary-item span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #8b8ba8;
font-size: 0.74rem;
}
.today-resume {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin: 0.75rem 0.75rem 0;
padding: 0.85rem 1rem;
border: 1px solid rgba(78, 204, 163, 0.24);
border-radius: 8px;
background: linear-gradient(135deg, rgba(78, 204, 163, 0.11), rgba(15, 52, 96, 0.14));
}
.today-resume-copy {
min-width: 0;
display: grid;
gap: 0.22rem;
}
.today-resume-copy span {
color: #4ecca3;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.today-resume-copy strong {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #f4f7fb;
font-size: 0.98rem;
}
.today-resume-copy p {
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #9aa4bd;
font-size: 0.8rem;
}
.today-panel { .today-panel {
min-width: 0; min-width: 0;
min-height: 10rem; min-height: 10rem;
@ -323,5 +467,18 @@
.today-grid { .today-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.today-summary {
grid-template-columns: 1fr;
}
.today-resume {
align-items: stretch;
flex-direction: column;
}
.today-resume button {
width: 100%;
}
} }
</style> </style>