taskchef 9.0.4 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/docs/mcp-app.md +6 -0
- package/package.json +1 -1
- package/src/codex-projects.js +4 -2
- package/src/codex-session-scanner.js +4 -3
- package/src/mcp-app/dist/index.html +77 -72
- package/src/mcp-app/react/main.test.tsx +35 -1
- package/src/mcp-app/react/main.tsx +6 -3
- package/src/mcp-app/react/styles.css +6 -2
- package/src/ui/types.ts +1 -1
|
@@ -13,7 +13,7 @@ vi.mock("@mantine/core", async () => {
|
|
|
13
13
|
createTheme: (theme: unknown) => theme,
|
|
14
14
|
ActionIcon: ({ children, onClick, ...props }: Record<string, unknown>) => React.createElement("button", { "aria-label": props["aria-label"], onClick }, children as React.ReactNode),
|
|
15
15
|
Modal: ({ opened, children }: { opened: boolean; children: React.ReactNode }) => opened ? <div role="dialog">{children}</div> : null,
|
|
16
|
-
Alert: wrap("div"), Box: wrap("div"), Button: ({ children, onClick, disabled }: {children: React.ReactNode; onClick?: () => void; disabled?: boolean}) => <button onClick={onClick} disabled={disabled}>{children}</button>, Group: wrap("div"),
|
|
16
|
+
Alert: wrap("div"), Box: wrap("div"), Button: ({ children, onClick, disabled, ...props }: {children: React.ReactNode; onClick?: () => void; disabled?: boolean; "aria-pressed"?: boolean; title?: string}) => <button onClick={onClick} disabled={disabled} aria-pressed={props["aria-pressed"]} title={props.title}>{children}</button>, Group: wrap("div"),
|
|
17
17
|
MantineProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
18
18
|
TextInput: ({ value, onChange, ...props }: { value: string; onChange: React.ChangeEventHandler<HTMLInputElement>; "aria-label": string }) => <input aria-label={props["aria-label"]} value={value} onChange={onChange} />,
|
|
19
19
|
Paper: wrap("div"), Stack: wrap("div"), Text: wrap("p"), Title: wrap("h2"),
|
|
@@ -633,3 +633,37 @@ test("native card usage setting defaults on and changes without hiding detail us
|
|
|
633
633
|
fireEvent.click(screen.getByRole("button", {name:"Task one"}));
|
|
634
634
|
expect(await screen.findByRole("region", {name:"Usage"})).toBeInTheDocument();
|
|
635
635
|
});
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
test("Pinned toggle combines with filters and updates after pin changes on refresh", async () => {
|
|
639
|
+
tasks = [task("one", "working", new Date().toISOString(), "Alpha"), task("two", "needs_input", new Date().toISOString(), "Beta"), task("old", "completed", "2026-01-01T00:00:00Z", "Alpha"), task("unknown")];
|
|
640
|
+
tasks.slice(0, 3).forEach((item, index) => { item.observed = { pinned: index !== 1, archive: false, lastTurnEvent: null, lastTurnEventAt: null, recentFileActivity: false }; });
|
|
641
|
+
mount();
|
|
642
|
+
await screen.findByRole("button", { name: "Task one" });
|
|
643
|
+
const toggle = screen.getByRole("button", { name: "Pinned" });
|
|
644
|
+
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
|
645
|
+
expect(toggle).toHaveAttribute("title", "Show only pinned chats");
|
|
646
|
+
fireEvent.click(toggle);
|
|
647
|
+
expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
648
|
+
expect(screen.queryByRole("button", { name: "Task two" })).not.toBeInTheDocument();
|
|
649
|
+
expect(screen.queryByRole("button", { name: "Task unknown" })).not.toBeInTheDocument();
|
|
650
|
+
expect(screen.getByRole("button", { name: "Task old" })).toBeVisible();
|
|
651
|
+
fireEvent.change(screen.getByRole("combobox", { name: "Updated" }), { target: { value: "24h" } });
|
|
652
|
+
expect(screen.queryByRole("button", { name: "Task old" })).not.toBeInTheDocument();
|
|
653
|
+
fireEvent.change(screen.getByRole("combobox", { name: "Project" }), { target: { value: "/Beta" } });
|
|
654
|
+
expect(screen.queryByRole("button", { name: "Task one" })).not.toBeInTheDocument();
|
|
655
|
+
expect(within(screen.getByRole("region", { name: "Task board" })).queryAllByRole("article")).toHaveLength(0);
|
|
656
|
+
tasks = tasks.map(item => item.id === "two" ? { ...item, observed: { ...item.observed!, pinned: true } } : item);
|
|
657
|
+
await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Refresh" })); });
|
|
658
|
+
expect(toggle).toHaveAttribute("aria-pressed", "true");
|
|
659
|
+
expect(screen.getByRole("button", { name: "Task two" })).toBeVisible();
|
|
660
|
+
fireEvent.change(screen.getByLabelText("Search Chats"), { target: { value: "missing" } });
|
|
661
|
+
expect(screen.queryByRole("button", { name: "Task two" })).not.toBeInTheDocument();
|
|
662
|
+
fireEvent.change(screen.getByLabelText("Search Chats"), { target: { value: "" } });
|
|
663
|
+
tasks = tasks.map(item => item.observed ? { ...item, observed: { ...item.observed, pinned: false } } : item);
|
|
664
|
+
await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Refresh" })); });
|
|
665
|
+
expect(within(screen.getByRole("region", { name: "Task board" })).queryAllByRole("article")).toHaveLength(0);
|
|
666
|
+
fireEvent.click(toggle);
|
|
667
|
+
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
|
668
|
+
expect(screen.getByRole("button", { name: "Task two" })).toBeVisible();
|
|
669
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cachedImageLoader } from "../../ui/image-cache";
|
|
2
2
|
import { App } from "@modelcontextprotocol/ext-apps";
|
|
3
3
|
import { ActionIcon, Alert, Box, Button, Group, MantineProvider, Modal, createTheme, Paper, SegmentedControl, Select, Stack, Text, TextInput, Title } from "@mantine/core";
|
|
4
|
-
import { IconRefresh, IconSearch, IconSettings } from "@tabler/icons-react";
|
|
4
|
+
import { IconPin, IconRefresh, IconSearch, IconSettings } from "@tabler/icons-react";
|
|
5
5
|
import { createRoot } from "react-dom/client";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import type { DashboardSnapshot, Task, Project } from "../../ui/types";
|
|
@@ -113,6 +113,7 @@ export function TaskChefApp() {
|
|
|
113
113
|
const [scan, setScan] = useState<ScanStats | null>(null);
|
|
114
114
|
const [selected, setSelected] = useState<Task | null>(null);
|
|
115
115
|
const [opened, setOpened] = useState(false);
|
|
116
|
+
const [pinnedOnly, setPinnedOnly] = useState(false);
|
|
116
117
|
const [busy, setBusy] = useState(false);
|
|
117
118
|
const [error, setError] = useState<string | null>(null);
|
|
118
119
|
|
|
@@ -330,6 +331,7 @@ export function TaskChefApp() {
|
|
|
330
331
|
const boardTasks: Task[] = useMemo(() => {
|
|
331
332
|
const terms = search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
332
333
|
return filterTasks(projectTasks, { date, now }).filter((task: Task) => {
|
|
334
|
+
if (pinnedOnly && !task.observed?.pinned) return false;
|
|
333
335
|
const content = [
|
|
334
336
|
task.title, task.promptExcerpt ?? "", task.replyExcerpt ?? "", task.project.name,
|
|
335
337
|
...(task.project.githubRepos ?? []),
|
|
@@ -337,7 +339,7 @@ export function TaskChefApp() {
|
|
|
337
339
|
].join("\n").toLocaleLowerCase();
|
|
338
340
|
return terms.every((term) => content.includes(term));
|
|
339
341
|
});
|
|
340
|
-
}, [projectTasks, date, now, search]);
|
|
342
|
+
}, [projectTasks, date, now, search, pinnedOnly]);
|
|
341
343
|
const visible = useMemo(() => boardTasks.filter((task) => !status || (task.status ?? "unverified") === status), [boardTasks, status]);
|
|
342
344
|
const statusOptions = [{ label: `All ${boardTasks.length}`, value: "" }, ...lanes.map(({ label, status: value }) => ({
|
|
343
345
|
label: `${label} ${boardTasks.filter((task) => task.status === value).length}`, value: value ?? "unverified",
|
|
@@ -418,10 +420,11 @@ export function TaskChefApp() {
|
|
|
418
420
|
<Box className={`taskchef-app-shell${displayMode === "inline" ? " taskchef-app-inline" : ""}`}>
|
|
419
421
|
<header className="taskchef-app-header">
|
|
420
422
|
<Group className="taskchef-app-brand" gap="xs" wrap="nowrap"><img alt="" aria-hidden className="taskchef-app-mark" src={brandIcon} /><Title order={1}>TaskChef</Title></Group>
|
|
421
|
-
{displayMode !== "inline" && <Group className="taskchef-app-filters" gap="xs" wrap="
|
|
423
|
+
{displayMode !== "inline" && <Group className="taskchef-app-filters" gap="xs" wrap="wrap">
|
|
422
424
|
<ProjectPicker data={projects} onChange={(value) => { setProject(value); setCompletedLimit(5); }} value={project} />
|
|
423
425
|
<Select className="taskchef-app-date" aria-label="Updated" data={[{ label: "Latest 24 hours", value: "24h" }, { label: "Latest 7 days", value: "7d" }, { label: "All time", value: "all" }]} onChange={(value) => { setDate(value ?? "all"); setCompletedLimit(5); }} value={date} size="xs" />
|
|
424
426
|
<TextInput className="taskchef-app-search" aria-label="Search Chats" placeholder="Search Chats" type="search" leftSection={<IconSearch size={14} aria-hidden />} value={search} onChange={(event) => { setSearch(event.currentTarget.value); setCompletedLimit(5); setArchivedLimit(5); }} size="xs" />
|
|
427
|
+
<Button className="taskchef-app-pinned" aria-pressed={pinnedOnly} title="Show only pinned chats" size="xs" variant={pinnedOnly ? "filled" : "outline"} leftSection={<IconPin size={14} aria-hidden />} onClick={() => { setPinnedOnly(value => !value); setCompletedLimit(5); setArchivedLimit(5); }}>Pinned</Button>
|
|
425
428
|
</Group>}
|
|
426
429
|
<Group className="taskchef-app-actions" gap="xs" wrap="nowrap">
|
|
427
430
|
<GitHubConnection hideTrigger openSignal={githubOpenSignal} auth={githubAuth} request={githubRequest} openLink={githubOpenLink} refresh={githubRefresh} openSettings={githubOpenSettings} />
|
|
@@ -27,8 +27,9 @@ body { background: var(--taskchef-page); }
|
|
|
27
27
|
.taskchef-app-main .taskchef-toolbar { margin-bottom: 8px; padding-bottom: 10px; }
|
|
28
28
|
.taskchef-app-main .mantine-SegmentedControl-root { max-width: 100%; }
|
|
29
29
|
.taskchef-app-view { width: max-content; }
|
|
30
|
-
.taskchef-app-filters { grid-area: filters; min-width: 0; width: min(100%,
|
|
31
|
-
.taskchef-app-filters .mantine-InputWrapper-root { flex: 1 1
|
|
30
|
+
.taskchef-app-filters { grid-area: filters; min-width: 0; width: min(100%, 680px); }
|
|
31
|
+
.taskchef-app-filters .mantine-InputWrapper-root { flex: 1 1 100px; min-width: 100px; }
|
|
32
|
+
.taskchef-app-pinned { flex: 0 0 auto; white-space: nowrap; }
|
|
32
33
|
.taskchef-app-filters .taskchef-app-date { flex: 0 0 132px; }
|
|
33
34
|
@media (max-width: 650px) {
|
|
34
35
|
.taskchef-app-header { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "brand actions" "filters filters"; gap: 8px; }
|
|
@@ -189,6 +190,9 @@ body { background: var(--taskchef-page); }
|
|
|
189
190
|
.next-appearance-panel { background: var(--taskchef-elevated); }
|
|
190
191
|
|
|
191
192
|
:root[data-taskchef-palette] .mantine-Button-root[data-variant="filled"] { color: var(--taskchef-surface); }
|
|
193
|
+
:root[data-taskchef-palette] .taskchef-app-pinned { background: transparent; color: var(--taskchef-text); border-color: var(--taskchef-border); }
|
|
194
|
+
:root[data-taskchef-palette] .taskchef-app-pinned:hover { background: var(--taskchef-hover); }
|
|
195
|
+
:root[data-taskchef-palette] .taskchef-app-pinned[aria-pressed="true"] { background: var(--taskchef-accent); color: var(--taskchef-surface); border-color: var(--taskchef-accent); }
|
|
192
196
|
:root[data-taskchef-palette] .taskchef-board-card:focus-visible { outline: 2px solid var(--taskchef-focus); outline-offset: 2px; }
|
|
193
197
|
|
|
194
198
|
:root[data-taskchef-palette] body { font-family: var(--taskchef-font); }
|
package/src/ui/types.ts
CHANGED
|
@@ -129,7 +129,7 @@ export interface Task {
|
|
|
129
129
|
promptImages?: { url: string; alt: string }[];
|
|
130
130
|
replyImage?: { url: string; alt: string } | null;
|
|
131
131
|
inputSource?: "scheduled" | "ordinary" | "unverified";
|
|
132
|
-
observed?: { unread?: boolean; source?: string | null; archive: boolean; lastTurnEvent: string | null; lastTurnEventAt: string | null; latestTurnDurationMs?: number | null; recordedChatDurationMs?: number | null; missingTurnDurations?: number; historicalTimePartial?: boolean; recentFileActivity: boolean; directChildCount?: number; userMessages?: number; assistantMessages?: number; sampledBytes?: number; fileBytes?: number };
|
|
132
|
+
observed?: { pinned?: boolean; unread?: boolean; source?: string | null; archive: boolean; lastTurnEvent: string | null; lastTurnEventAt: string | null; latestTurnDurationMs?: number | null; recordedChatDurationMs?: number | null; missingTurnDurations?: number; historicalTimePartial?: boolean; recentFileActivity: boolean; directChildCount?: number; userMessages?: number; assistantMessages?: number; sampledBytes?: number; fileBytes?: number };
|
|
133
133
|
pullRequests?: PullRequestStatus[];
|
|
134
134
|
detailPullRequests?: PullRequestStatus[];
|
|
135
135
|
earlierPullRequest?: PullRequestStatus;
|