taskchef 9.0.3 → 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.
@@ -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="nowrap">
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%, 560px); }
31
- .taskchef-app-filters .mantine-InputWrapper-root { flex: 1 1 0; min-width: 0; }
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;