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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 16 additions & 7 deletions packages/artifact/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();
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<string>();
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() {
Expand Down Expand Up @@ -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<Task['source']>['type'],
};
}
handleUpdate(task.id, patch);
return;
Expand Down Expand Up @@ -388,7 +398,6 @@ export function App() {
<TopBar
boardName={board.name}
taskCount={tasks.length}
onRefresh={refresh}
onSearch={setSearch}
onTriageNow={async () => {
// Safe-mode: copy the slash-command + prompt to clipboard.
Expand Down
5 changes: 3 additions & 2 deletions packages/artifact/src/__tests__/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand Down Expand Up @@ -104,7 +105,7 @@ describe('mergeWithCache (snapshot-tagged)', () => {
locallyCreatedIds: new Set<string>(),
};
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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/artifact/src/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions packages/artifact/src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,7 +40,6 @@ interface TopBarProps {
export function TopBar({
boardName,
taskCount,
onRefresh,
onSearch,
onTriageNow,
onConnectFolder,
Expand Down
4 changes: 3 additions & 1 deletion packages/artifact/src/hooks/useTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
2 changes: 1 addition & 1 deletion packages/artifact/test/e2e/journey.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/connector-meet-fathom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -60,7 +60,7 @@ export function createFathomConnector(opts: FathomConnectorOpts): Connector {
};
push(item);
}
return maxEnd;
return maxEnd || since;
},
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/landing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
}
}
11 changes: 7 additions & 4 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`;
Expand Down
6 changes: 5 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading