taskchef 9.0.1 → 9.0.3
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 +1 -1
- package/package.json +1 -1
- package/src/codex-session-scanner.js +11 -3
- package/src/mcp-app/dist/index.html +56 -51
- package/src/mcp-app/react/NextTaskDetail.test.tsx +14 -0
- package/src/mcp-app/react/styles.css +7 -0
- package/src/ui/components/TaskBoard.test.tsx +11 -0
- package/src/ui/components/TurnContent.test.tsx +13 -0
- package/src/ui/components/TurnContent.tsx +5 -2
- package/src/ui/types.ts +1 -0
|
@@ -122,3 +122,17 @@ test('Needs answer adds an open-chat action without replacing the saved prompt o
|
|
|
122
122
|
expect(screen.getByText('The connection failed. Approval requested.')).toBeInTheDocument();
|
|
123
123
|
fireEvent.click(screen.getByRole('button',{name:'Open chat to answer'}));expect(open).toHaveBeenCalledTimes(1);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
test("interrupted details show the recorded error after the response and clear it on recovery", () => {
|
|
127
|
+
const task = { ...fixtureTask(), status: "interrupted" as const, turnError: "Selected model is at capacity. Please try a different model.", replyText: "Progress before the error." };
|
|
128
|
+
const props = { opened: true, busy: false, error: null, onClose: () => {}, onOpenCodex: () => {} };
|
|
129
|
+
const { rerender } = render(<MantineProvider><NextTaskDetail {...props} task={task} /></MantineProvider>);
|
|
130
|
+
const notice = screen.getByLabelText("Latest turn error");
|
|
131
|
+
expect(notice).toHaveTextContent(task.turnError);
|
|
132
|
+
expect(notice).toHaveClass("taskchef-turn-error");
|
|
133
|
+
expect(screen.getByLabelText("Latest turn response").compareDocumentPosition(notice) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
134
|
+
rerender(<MantineProvider><NextTaskDetail {...props} task={{ ...task, replyText: null, replyExcerpt: null }} /></MantineProvider>);
|
|
135
|
+
expect(screen.getByLabelText("Latest turn error")).toBeVisible();
|
|
136
|
+
rerender(<MantineProvider><NextTaskDetail {...props} task={{ ...task, status: "working" }} /></MantineProvider>);
|
|
137
|
+
expect(screen.queryByLabelText("Latest turn error")).toBeNull();
|
|
138
|
+
});
|
|
@@ -210,3 +210,10 @@ body { background: var(--taskchef-page); }
|
|
|
210
210
|
.taskchef-question-footer button.mantine-Anchor-root { display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border: 1px solid var(--taskchef-border-heavy); border-radius: 16px; font-size: 12px; text-decoration: none; }
|
|
211
211
|
.taskchef-question-footer button.mantine-Anchor-root:hover { background: var(--taskchef-hover); text-decoration: none; }
|
|
212
212
|
.taskchef-question-footer button.mantine-Anchor-root:focus-visible { outline: 2px solid var(--taskchef-focus); outline-offset: 2px; }
|
|
213
|
+
|
|
214
|
+
.taskchef-turn-error { display: flex; align-items: flex-start; gap: 12px; margin-top: 16px; padding: 18px 20px; border: 1px solid var(--taskchef-border); border-radius: 20px; background: var(--taskchef-surface); color: var(--taskchef-text); font-size: var(--mantine-font-size-sm); line-height: 1.5; overflow-wrap: anywhere; }
|
|
215
|
+
.taskchef-turn-error > svg { flex: none; margin-top: 2px; }
|
|
216
|
+
.taskchef-turn-error > span { min-width: 0; white-space: pre-wrap; }
|
|
217
|
+
.taskchef-turn-error-compact { gap: 6px; margin-top: 8px; padding: 0; border: 0; border-radius: 0; background: transparent; font-size: var(--mantine-font-size-xs); }
|
|
218
|
+
|
|
219
|
+
.taskchef-turn-error-compact > span { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
|
|
@@ -375,3 +375,14 @@ test("covers prefer response images and fall back to prompt images for every sta
|
|
|
375
375
|
expect(await screen.findByAltText("First prompt")).toBeVisible();
|
|
376
376
|
expect(screen.queryByRole("img",{name:"Reply"})).toBeNull();
|
|
377
377
|
});
|
|
378
|
+
|
|
379
|
+
test("interrupted cards show the recorded capacity reason even without a reply", () => {
|
|
380
|
+
const interrupted = { ...task(1, "interrupted"), replyExcerpt: null, turnError: "Selected model is at capacity. Please try a different model.", observed: { archive: false, lastTurnEvent: "failed", lastTurnEventAt: null, recentFileActivity: false } };
|
|
381
|
+
const props = { completedLimit: 5, onMoreCompleted: vi.fn(), onOpenCodex: vi.fn(), onOpenDetail: vi.fn() };
|
|
382
|
+
const { rerender } = render(<MantineProvider><TaskBoard {...props} tasks={[interrupted]} groupInterruptedWithWaiting /></MantineProvider>);
|
|
383
|
+
expect(screen.getByLabelText("Latest turn error")).toHaveTextContent(interrupted.turnError);
|
|
384
|
+
fireEvent.click(screen.getByRole("button", { name: interrupted.title }));
|
|
385
|
+
expect(props.onOpenDetail).toHaveBeenCalledWith(interrupted);
|
|
386
|
+
rerender(<MantineProvider><TaskBoard {...props} tasks={[{ ...interrupted, status: "working" }]} /></MantineProvider>);
|
|
387
|
+
expect(screen.queryByLabelText("Latest turn error")).toBeNull();
|
|
388
|
+
});
|
|
@@ -42,3 +42,16 @@ test("card prompt cleanup does not change response headings", () => {
|
|
|
42
42
|
rerender(<TurnContent task={{...task,status:"needs_input"}} compact />);
|
|
43
43
|
expect(screen.getByLabelText("Latest turn response")).toHaveTextContent("My requests:");
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
test("compact errors keep the full message accessible and details show it without a clamp", () => {
|
|
48
|
+
const turnError = Array.from({ length: 20 }, (_, i) => `Recorded failure detail ${i}.`).join("\n");
|
|
49
|
+
const task = { ...fixtureTask(), status: "interrupted" as const, turnError, replyExcerpt: null };
|
|
50
|
+
const { rerender } = render(<TurnContent task={task} compact />);
|
|
51
|
+
const compact = screen.getByLabelText("Latest turn error");
|
|
52
|
+
expect(compact).toHaveClass("taskchef-turn-error-compact");
|
|
53
|
+
expect(compact.querySelector("span")).toHaveAttribute("title", turnError);
|
|
54
|
+
rerender(<TurnContent task={task} />);
|
|
55
|
+
expect(screen.getByLabelText("Latest turn error")).not.toHaveClass("taskchef-turn-error-compact");
|
|
56
|
+
expect(screen.getByLabelText("Latest turn error").textContent).toBe(turnError);
|
|
57
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IconClock } from "@tabler/icons-react";
|
|
1
|
+
import { IconAlertCircle, IconClock } from "@tabler/icons-react";
|
|
2
2
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import type { Task } from "../types";
|
|
4
4
|
import { ImageGallery } from "./ImageGallery";
|
|
@@ -33,9 +33,11 @@ export function TurnContent({ task, compact = false, loadImage, workedDurationMs
|
|
|
33
33
|
const renderImage = useCallback((url: string, alt: string) => loadImage ? <ReplyImagePreview key={`${task.id}:${task.turnId}:${url}`} task={currentTask.current} url={url} alt={alt} loadImage={loadImage} onLightboxChange={onLightboxChange} /> : null, [task.id, task.turnId, loadImage, onLightboxChange]);
|
|
34
34
|
const prompt = compact ? task.promptExcerpt : task.promptText ?? task.promptExcerpt;
|
|
35
35
|
const reply = compact ? task.replyExcerpt : task.replyText ?? task.replyExcerpt;
|
|
36
|
+
const errorNotice = task.status === "interrupted" && task.turnError
|
|
37
|
+
? <div role="status" aria-label="Latest turn error" className={compact ? "taskchef-turn-error taskchef-turn-error-compact" : "taskchef-turn-error"}><IconAlertCircle size={16} stroke={1.7} aria-hidden /><span title={compact ? task.turnError : undefined}>{task.turnError}</span></div> : null;
|
|
36
38
|
if (compact) {
|
|
37
39
|
const text = task.status === "working" ? prompt : reply;
|
|
38
|
-
return text ? <div className="taskchef-turn-card" aria-label={task.status === "working" ? "Latest turn request" : "Latest turn response"}><ReplyMarkdown key={`${task.id}:${task.turnId}`} compact annotations={task.annotations} hideRequestLabels={task.status === "working"} text={text} renderImage={task.status === "working" ? hidePromptImage : undefined} /></div> : null
|
|
40
|
+
return <>{errorNotice}{text ? <div className="taskchef-turn-card" aria-label={task.status === "working" ? "Latest turn request" : "Latest turn response"}><ReplyMarkdown key={`${task.id}:${task.turnId}`} compact annotations={task.annotations} hideRequestLabels={task.status === "working"} text={text} renderImage={task.status === "working" ? hidePromptImage : undefined} /></div> : null}</>;
|
|
39
41
|
}
|
|
40
42
|
const promptImages = task.promptImages ?? (task.promptImage ? [task.promptImage] : []);
|
|
41
43
|
const duration = workedDurationMs ?? task.observed?.latestTurnDurationMs;
|
|
@@ -49,5 +51,6 @@ export function TurnContent({ task, compact = false, loadImage, workedDurationMs
|
|
|
49
51
|
<div className="taskchef-turn-divider" role="separator">{duration != null && Number.isFinite(duration) && duration >= 0 ? `Worked for ${formatWorkedDuration(duration)}` : null}</div>
|
|
50
52
|
<div className="taskchef-turn-response" aria-label="Latest turn response"><ReplyMarkdown key={`${task.id}:${task.turnId}`} onAnnotationChange={onAnnotationChange} annotations={task.annotations} text={reply} renderImage={loadImage ? renderImage : undefined} /></div>
|
|
51
53
|
</>}
|
|
54
|
+
{errorNotice}
|
|
52
55
|
</div>;
|
|
53
56
|
}
|