From 636f015784cc01d8123fcd81efb034d5576e08b4 Mon Sep 17 00:00:00 2001 From: Cowork Tasks Maintainers Date: Mon, 4 May 2026 21:02:32 -0700 Subject: [PATCH 1/4] fix(connector-meet-fathom): cursor should reflect latest meeting end_time, not since Initialising maxEnd to `since` meant the cursor could never move to a meeting whose end_time was older than since (e.g. in tests that supply a fixed past timestamp). Initialize to '' and fall back to since only when no meetings are returned, so the cursor always reflects the latest end_time seen. Co-Authored-By: Claude Sonnet 4.6 --- packages/connector-meet-fathom/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/connector-meet-fathom/src/index.ts b/packages/connector-meet-fathom/src/index.ts index 9b4e231..5a89255 100644 --- a/packages/connector-meet-fathom/src/index.ts +++ b/packages/connector-meet-fathom/src/index.ts @@ -44,7 +44,7 @@ export function createFathomConnector(opts: FathomConnectorOpts): Connector { const res = await api<{ meetings?: FathomMeeting[] }>(`/meetings?${params.toString()}`); const meetings = res.meetings ?? []; - let maxEnd = since; + let maxEnd = ''; for (const m of meetings) { if (m.end_time > maxEnd) maxEnd = m.end_time; const item: SourceItem = { @@ -60,7 +60,7 @@ export function createFathomConnector(opts: FathomConnectorOpts): Connector { }; push(item); } - return maxEnd; + return maxEnd || since; }, }; } From 5e993396d1cf871478f5dbd473209a316d7a240d Mon Sep 17 00:00:00 2001 From: Cowork Tasks Maintainers Date: Mon, 4 May 2026 21:51:04 -0700 Subject: [PATCH 2/4] ci: build workspace deps before typecheck/lint/test --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c04ce04..855b44e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node }} cache: pnpm - run: pnpm install --frozen-lockfile + - run: pnpm build - run: pnpm typecheck - run: pnpm lint - run: pnpm test - - run: pnpm build From 7a7249e0e513b4447f4ac33b19ff85a58fb53a7d Mon Sep 17 00:00:00 2001 From: Cowork Tasks Maintainers Date: Mon, 4 May 2026 21:58:08 -0700 Subject: [PATCH 3/4] fix(ci): declare artifact as landing devDep so pnpm build respects topological order --- packages/landing/package.json | 3 +++ pnpm-lock.yaml | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/landing/package.json b/packages/landing/package.json index 7e40ece..9481f31 100644 --- a/packages/landing/package.json +++ b/packages/landing/package.json @@ -8,5 +8,8 @@ "build": "node build.mjs", "dev": "node build.mjs && python3 -m http.server 4321 --directory dist", "vercel-build": "pnpm --filter @cowork-tasks/artifact build && node build.mjs" + }, + "devDependencies": { + "@cowork-tasks/artifact": "workspace:*" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c135af4..46a372f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,7 +167,11 @@ importers: specifier: ^1.4.0 version: 1.6.1(@types/node@20.19.39) - packages/landing: {} + packages/landing: + devDependencies: + '@cowork-tasks/artifact': + specifier: workspace:* + version: link:../artifact packages/mcp-server: dependencies: From 5868fb260406502978c583ef5f53bc59e7dd1c9e Mon Sep 17 00:00:00 2001 From: Cowork Tasks Maintainers Date: Mon, 4 May 2026 22:13:11 -0700 Subject: [PATCH 4/4] fix: typecheck + lint + fathom test failures so CI passes Pre-existing strict-mode issues surfaced when CI started running build before typecheck. Fixes: - App.tsx: groupBy='source' patch needs the source-type cast against NonNullable (not Task['source'] which is nullable). - App.tsx: deriveColumns now returns Column-shaped objects with an always-present color (defaulted to #6b6a64). - TopBar.tsx: drop the unused _onRefresh prop entirely; explicit comment notes Cowork chrome owns reload. - App.tsx: drop the JSX comment that crept in on the TopBar opening. - Markdown.tsx: remove unused IMAGE_EXT constant. - useTasks.ts: replace eslint-disable for react-hooks/exhaustive-deps with a real comment (the rule isn't installed and was failing as unknown). The closure-over-apply behavior is intentional. - server.ts: replace require('node:fs')/require('node:path') with named imports so eslint's no-var-requires passes. - journey.spec.ts: drop unused setupCoworkEnv import. - storage.test.ts: cast globalThis through unknown for stricter TS. - connector-meet-fathom: cursor advancement was clamped to "now-24h" on first run because maxEnd started at `since`. Now starts at the original cursor (or empty) so any returned meeting wins; falls back to since only when there were no meetings. 10/10 mcp + 15/15 storage + 84/84 e2e + 2/2 fathom + lint + typecheck all green locally. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/artifact/src/App.tsx | 23 +++++++++++++------ .../artifact/src/__tests__/storage.test.ts | 5 ++-- packages/artifact/src/components/Markdown.tsx | 2 +- packages/artifact/src/components/TopBar.tsx | 3 +-- packages/artifact/src/hooks/useTasks.ts | 4 +++- packages/artifact/test/e2e/journey.spec.ts | 2 +- packages/mcp-server/src/server.ts | 11 +++++---- 7 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/artifact/src/App.tsx b/packages/artifact/src/App.tsx index 8dabdc9..f1c9437 100644 --- a/packages/artifact/src/App.tsx +++ b/packages/artifact/src/App.tsx @@ -47,22 +47,29 @@ function deriveColumns( by: GroupBy, tasks: Task[], fallback: ReadonlyArray<{ id: string; name: string; color?: string }>, -): Array<{ id: string; name: string; color?: string }> { - if (by === 'status') return [...fallback]; - if (by === 'priority') return PRIORITY_BUCKETS; +): Array<{ id: string; name: string; color: string }> { + const DEFAULT_COLOR = '#6b6a64'; + const withColor = (c: { id: string; name: string; color?: string }) => ({ + id: c.id, + name: c.name, + color: c.color ?? DEFAULT_COLOR, + }); + + if (by === 'status') return fallback.map(withColor); + if (by === 'priority') return PRIORITY_BUCKETS.map(withColor); if (by === 'source') { const types = new Set(); for (const t of tasks) types.add(t.source?.type ?? 'manual'); return Array.from(types) .sort() - .map((id) => ({ id, name: id })); + .map((id) => withColor({ id, name: id })); } // owner const owners = new Set(); for (const t of tasks) owners.add(t.owner ?? '__no_owner__'); return Array.from(owners) .sort() - .map((id) => ({ id, name: id === '__no_owner__' ? 'No owner' : id })); + .map((id) => withColor({ id, name: id === '__no_owner__' ? 'No owner' : id })); } export function App() { @@ -177,7 +184,10 @@ export function App() { patch.owner = targetBucket === '__no_owner__' ? undefined : targetBucket; } else if (groupBy === 'source') { // Preserve url/author; just change the type label. - patch.source = { ...(task.source ?? {}), type: targetBucket }; + patch.source = { + ...(task.source ?? {}), + type: targetBucket as NonNullable['type'], + }; } handleUpdate(task.id, patch); return; @@ -388,7 +398,6 @@ export function App() { { // Safe-mode: copy the slash-command + prompt to clipboard. diff --git a/packages/artifact/src/__tests__/storage.test.ts b/packages/artifact/src/__tests__/storage.test.ts index 4dfc7fe..c942e6a 100644 --- a/packages/artifact/src/__tests__/storage.test.ts +++ b/packages/artifact/src/__tests__/storage.test.ts @@ -48,7 +48,8 @@ class MemoryLocalStorage { } beforeEach(() => { - (globalThis as { localStorage: MemoryLocalStorage }).localStorage = new MemoryLocalStorage(); + (globalThis as unknown as { localStorage: MemoryLocalStorage }).localStorage = + new MemoryLocalStorage(); }); describe('mergeWithCache (snapshot-tagged)', () => { @@ -104,7 +105,7 @@ describe('mergeWithCache (snapshot-tagged)', () => { locallyCreatedIds: new Set(), }; const result = mergeWithCache(seed, cache, new Set(), 2); - expect(result[0].title).toBe('new'); + expect(result[0]?.title).toBe('new'); }); it('tombstoned ids are dropped from both seed and cache', () => { diff --git a/packages/artifact/src/components/Markdown.tsx b/packages/artifact/src/components/Markdown.tsx index 392f7e4..32e999d 100644 --- a/packages/artifact/src/components/Markdown.tsx +++ b/packages/artifact/src/components/Markdown.tsx @@ -10,7 +10,7 @@ interface MarkdownProps { } const VIDEO_EXT = /\.(mp4|webm|mov|m4v)(\?|$)/i; -const IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg)(\?|$)/i; +// IMAGE_EXT removed (was unused) — react-markdown handles ![]() natively. /** * Lazy-load mermaid from a CDN so the artifact bundle stays small. diff --git a/packages/artifact/src/components/TopBar.tsx b/packages/artifact/src/components/TopBar.tsx index 155a66b..fdc1a0b 100644 --- a/packages/artifact/src/components/TopBar.tsx +++ b/packages/artifact/src/components/TopBar.tsx @@ -13,7 +13,7 @@ import { fs } from '../api'; interface TopBarProps { boardName: string; taskCount: number; - onRefresh: () => void; + // onRefresh intentionally omitted — Cowork's artifact chrome owns reload. onSearch: (q: string) => void; onTriageNow: () => void; onConnectFolder: () => void; @@ -40,7 +40,6 @@ interface TopBarProps { export function TopBar({ boardName, taskCount, - onRefresh, onSearch, onTriageNow, onConnectFolder, diff --git a/packages/artifact/src/hooks/useTasks.ts b/packages/artifact/src/hooks/useTasks.ts index 09c0c06..2682424 100644 --- a/packages/artifact/src/hooks/useTasks.ts +++ b/packages/artifact/src/hooks/useTasks.ts @@ -256,7 +256,9 @@ export function useTasks(intervalMs = 2000): { if (timer !== null) clearTimeout(timer); document.removeEventListener('visibilitychange', onVisibility); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + // The poll loop closes over `apply` and `loading` but we deliberately + // re-create it only when `intervalMs` changes; otherwise the cadence + // would reset on every render. }, [intervalMs]); return { tasks, version, newlyAdded, refresh, loading, setTasksLocal, resetToSnapshot }; diff --git a/packages/artifact/test/e2e/journey.spec.ts b/packages/artifact/test/e2e/journey.spec.ts index 1d57d20..ab9552d 100644 --- a/packages/artifact/test/e2e/journey.spec.ts +++ b/packages/artifact/test/e2e/journey.spec.ts @@ -1,5 +1,5 @@ import { test, expect, type Page, type Locator } from '@playwright/test'; -import { setupCoworkEnv, gotoBoard } from './harness'; +import { gotoBoard } from './harness'; /** * User-journey tests. Each test is one continuous recording of a real-ish diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 3d0c775..be50573 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -301,13 +301,16 @@ const TOOLS: Tool[] = [ * `data:` URI suitable for the MCP `icons[].src` field. Returns null if * the file is unreadable or missing - the server should still start. */ +// Sync (not async) so this can run inside the constructor. Imported via +// the same paths as elsewhere in this file. +import { readFileSync as readIconFileSync } from 'node:fs'; +import { join as joinIconPath, extname as iconExtname } from 'node:path'; + function readIconAsDataUri(pluginRoot: string | undefined, relPath: string): string | null { if (!pluginRoot) return null; try { - const fs = require('node:fs'); - const path = require('node:path'); - const buf = fs.readFileSync(path.join(pluginRoot, relPath)); - const ext = path.extname(relPath).toLowerCase(); + const buf = readIconFileSync(joinIconPath(pluginRoot, relPath)); + const ext = iconExtname(relPath).toLowerCase(); const mimeType = ext === '.png' ? 'image/png' : ext === '.svg' ? 'image/svg+xml' : 'application/octet-stream'; return `data:${mimeType};base64,${buf.toString('base64')}`;