Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2111727
feat(chat): add task process folding and recorded file summaries
TheFlareStar-Re Sep 20, 2026
04dbb47
fix(host): share the tool admission wait budget
TheFlareStar-Re Sep 20, 2026
bea0e21
test(chat): align process probes with nested folding
TheFlareStar-Re Sep 21, 2026
48c6964
fix(chat): show timestamps on hover and restore folded failure marks
TheFlareStar-Re Sep 21, 2026
db0e77b
perf: optimize task summaries and shell review capture
TheFlareStar-Re Sep 21, 2026
5e3a090
feat(chat): add task process folding and recorded file summaries
TheFlareStar-Re Sep 20, 2026
bec0506
fix(host): share the tool admission wait budget
TheFlareStar-Re Sep 20, 2026
213f7d0
test(chat): align process probes with nested folding
TheFlareStar-Re Sep 21, 2026
8cd97ca
fix(chat): show timestamps on hover and restore folded failure marks
TheFlareStar-Re Sep 21, 2026
04fe911
perf: optimize task summaries and shell review capture
TheFlareStar-Re Sep 21, 2026
cee9f34
fix(chat): include delegated edits in task file summaries
TheFlareStar-Re Sep 22, 2026
e1e7ef8
chore: preserve published task summary history
TheFlareStar-Re Sep 22, 2026
c6e7cb3
chore: sync task summaries with upstream main
TheFlareStar-Re Sep 22, 2026
62bd5c2
fix(cicd): sync Chinese E2E traceability
vastsa Sep 22, 2026
0e8b801
chore(release): v0.15.4-beta.1
vastsa Sep 22, 2026
58d0fd6
docs(release): add v0.15.3 notes
vastsa Sep 21, 2026
241baa4
docs(release): prepare v0.15.4 preview notes
vastsa Sep 22, 2026
806e6ca
fix(release): launch pnpm shim on Windows
vastsa Sep 22, 2026
7049c35
chore(release): v0.15.4-beta.2
vastsa Sep 22, 2026
9004a62
chore(release): v0.15.4
vastsa Sep 23, 2026
af4ab55
Merge task summary fixes into v0.15.4
TheFlareStar-Re Sep 23, 2026
60184d7
Merge task summary fixes into current main
TheFlareStar-Re Sep 23, 2026
6deabd9
fix(chat): restore top alignment for exhausted short transcripts
TheFlareStar-Re Sep 23, 2026
09d24df
Merge latest main into task summary PR
TheFlareStar-Re Sep 23, 2026
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ Extend the runtime
**Resident Services**
**Plugin Message Bus**

Chat messages show their local date and time. A task's process expands while it
runs and folds after completion, leaving the final answer visible; expand the
elapsed-time row to revisit the work. Supplemental input sent with Alt+Enter
(steering) stays inside the same process. The queue's **Send now** action instead
ends the active turn at its next boundary and starts a new one; those turns stay
separate. Recorded workspace Write/Edit and captured shell changes appear below
the answer with a separate count/totals header and a spaced file list.
Click a file to review its expanded diffs and guarded rollback in the right
sidebar. Counts remain cumulative edits from loaded parent-session evidence,
not a net Git diff.

</td>

</tr>
Expand Down
27 changes: 2 additions & 25 deletions apps/desktop/src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
} from "../features/chat/composer/editor";
import { useComposerAttachments } from "../features/chat/composer/hooks/useComposerAttachments";
import { useComposerDraft } from "../features/chat/composer/hooks/useComposerDraft";
import { useComposerDockHeight } from "../features/chat/composer/hooks/useComposerDockHeight";
import { useComposerSubmit } from "../features/chat/composer/hooks/useComposerSubmit";
import { ComposerImageAttachments } from "../features/chat/composer/ComposerImageAttachments";
import { ComposerInput } from "../features/chat/composer/ComposerInput";
Expand Down Expand Up @@ -146,8 +147,6 @@ export function Composer({
const [permissionOpen, setPermissionOpen] = useState(false);
const enhancementInvalidateRef = useRef<() => void>(() => {});
const composerShellRef = useRef<HTMLDivElement>(null);
const dockRef = useRef<HTMLDivElement>(null);
const publishedDockHeightRef = useRef(-1);

const invalidatePromptEnhancement = () => {
enhancementInvalidateRef.current();
Expand Down Expand Up @@ -485,29 +484,7 @@ export function Composer({
);
};

// Keep the transcript's bottom reserve in sync with the composer's real
// height (it grows with multi-line input) so the last message sits just
// above the box instead of far below it.
useEffect(() => {
const el = dockRef.current;
if (!el) return;
// Setting a custom property on documentElement invalidates style for the
// whole document, so an unchanged dock height must not be republished.
const publish = () => {
const h = Math.round(el.getBoundingClientRect().height);
if (h === publishedDockHeightRef.current) return;
publishedDockHeightRef.current = h;
document.documentElement.style.setProperty(
"--composer-dock-height",
`${h}px`,
);
};
publish();
if (typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(publish);
ro.observe(el);
return () => ro.disconnect();
}, [variant]);
const dockRef = useComposerDockHeight(variant);

return (
<div
Expand Down
58 changes: 49 additions & 9 deletions apps/desktop/src/components/ReviewChangeCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { memo, useId, useState } from "react";
import { memo, useEffect, useId, useState } from "react";
import { useTranslation } from "react-i18next";
import type {
ReviewChange,
ReviewChangeStatus,
ReviewRollbackStatus,
UiMessage,
} from "@pi-desktop/shared";
import { reviewChangeFromMessage } from "../lib/workspace-review";
import { reviewChangesFromMessage } from "../lib/workspace-review";
import { useAppStore } from "../stores/app-store";
import { cx } from "./ui";
import { IconCheck, IconChevronRight, IconSnapshot } from "./icons";
Expand All @@ -18,17 +19,32 @@ const STATUS_MARKS: Record<ReviewChangeStatus, string> = {
deleted: "D",
};

function DiffBody({ message, compact }: { message: UiMessage; compact: boolean }) {
function changeForSnapshot(
message: UiMessage,
snapshotId?: string,
): ReviewChange | null {
const changes = reviewChangesFromMessage(message);
return snapshotId
? changes.find((change) => change.snapshotId === snapshotId) ?? null
: changes[0] ?? null;
}

function DiffBody({
message,
change,
compact,
}: {
message: UiMessage;
change: ReviewChange;
compact: boolean;
}) {
const { t } = useTranslation();
const change = reviewChangeFromMessage(message);
const rollback = useAppStore((state) => state.rollbackWorkspaceChange);
const [rollingBack, setRollingBack] = useState(false);
const [rollbackStatus, setRollbackStatus] = useState<ReviewRollbackStatus | null>(
null,
);

if (!change) return null;

const runRollback = async () => {
if (!change.reversible || change.state === "rolledBack" || rollingBack) return;
setRollingBack(true);
Expand Down Expand Up @@ -105,15 +121,22 @@ function DiffBody({ message, compact }: { message: UiMessage; compact: boolean }

export const ReviewChangeCard = memo(function ReviewChangeCard({
message,
snapshotId,
compact = false,
revealToken,
}: {
message: UiMessage;
snapshotId?: string;
compact?: boolean;
revealToken?: number;
}) {
const { t } = useTranslation();
const change = reviewChangeFromMessage(message);
const change = changeForSnapshot(message, snapshotId);
const detailsId = useId();
const [open, setOpen] = useState(false);
const [open, setOpen] = useState(revealToken !== undefined);
useEffect(() => {
if (revealToken !== undefined) setOpen(true);
}, [revealToken]);

if (!change) return null;

Expand Down Expand Up @@ -172,9 +195,26 @@ export const ReviewChangeCard = memo(function ReviewChangeCard({
</button>
{open ? (
<div className="review-change-card-body" id={detailsId}>
<DiffBody message={message} compact={compact} />
<DiffBody message={message} change={change} compact={compact} />
</div>
) : null}
</section>
);
});

export const ReviewChangeCards = memo(function ReviewChangeCards({
message,
compact = false,
}: {
message: UiMessage;
compact?: boolean;
}) {
return reviewChangesFromMessage(message).map((change) => (
<ReviewChangeCard
key={change.snapshotId}
message={message}
snapshotId={change.snapshotId}
compact={compact}
/>
));
});
32 changes: 30 additions & 2 deletions apps/desktop/src/components/workpanel/ReviewTab.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { reviewChangesFromMessages, summarizeReviewChanges } from "../../lib/workspace-review";
import { transcriptViewMessages } from "../../lib/transcript-reading";
import { useAppStore } from "../../stores/app-store";
import { IconDiff } from "../icons";
import { ReviewChangeCard } from "../ReviewChangeCard";
import { WorkTabEmpty } from "./WorkTabEmpty";

export function ReviewTab() {
const { t } = useTranslation();
const messages = useAppStore((state) => state.messages);
const entries = useMemo(() => reviewChangesFromMessages(messages), [messages]);
const liveMessages = useAppStore((state) => state.messages);
const activeSessionId = useAppStore((state) => state.activeSessionId);
const selection = useAppStore((state) =>
state.activeSessionId
? state.workPanelContexts[state.activeSessionId]?.reviewSelection
: undefined,
);
const view = useAppStore((state) =>
state.activeSessionId ? state.transcriptViews[state.activeSessionId] : undefined,
);
const messages = useMemo(
() => selection ? transcriptViewMessages(liveMessages, view) : liveMessages,
[liveMessages, selection, view],
);
const allEntries = useMemo(
() => reviewChangesFromMessages(messages),
[messages],
);
const entries = useMemo(() => {
if (!selection || selection.sessionId !== activeSessionId) return allEntries;
const snapshotIds = new Set(selection.snapshotIds);
return allEntries.filter(
({ change }) =>
change.path === selection.selectedPath &&
snapshotIds.has(change.snapshotId),
);
}, [activeSessionId, allEntries, selection]);
const summary = useMemo(() => summarizeReviewChanges(entries), [entries]);

if (entries.length === 0) {
Expand Down Expand Up @@ -37,7 +63,9 @@ export function ReviewTab() {
<ReviewChangeCard
key={entry.change.snapshotId}
message={entry.message}
snapshotId={entry.change.snapshotId}
compact
revealToken={selection?.revision}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useCallback, useLayoutEffect, useRef } from "react";

/** Publish after the editor's layout effects so the reserve is ready for paint. */
export function useComposerDockHeight(variant: "home" | "docked") {
const dockRef = useRef<HTMLDivElement>(null);
const publishedHeightRef = useRef(-1);
const publish = useCallback(() => {
const element = dockRef.current;
if (!element) return;
const height = Math.round(element.getBoundingClientRect().height);
// Settings retains this component beneath a hidden ancestor. Keep the last
// real reserve until it is visible again; zero is not a measured dock size.
if (height <= 0) return;
// Root custom properties invalidate document-wide styles; skip equal pixels.
if (height === publishedHeightRef.current) return;
publishedHeightRef.current = height;
document.documentElement.style.setProperty("--composer-dock-height", `${height}px`);
}, []);

// Drafts, attachments and status rows can all resize the dock in a commit.
// Measure once per layout phase, without waiting for ResizeObserver delivery.
useLayoutEffect(publish);

useLayoutEffect(() => {
const element = dockRef.current;
if (!element || typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(publish);
observer.observe(element);
return () => observer.disconnect();
}, [publish, variant]);

return dockRef;
}
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ export function useComposerDraft({
commitEditorDom();
};

useEffect(() => {
// Restore the destination before paint so editor sizing and the dock reserve
// settle in the same layout phase instead of correcting the transcript later.
useLayoutEffect(() => {
const previousKey = draftKeyRef.current;
if (previousKey !== draftKey) {
invalidatePromptEnhancement();
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/features/chat/transcript/ActivityGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
getToolAction,
getToolSummary,
} from "../../../lib/tool-display";
import { ReviewChangeCard } from "../../../components/ReviewChangeCard";
import { ReviewChangeCards } from "../../../components/ReviewChangeCard";
import { IconChevronRight, IconCircleAlert, IconSparkles, IconWorkflow } from "../../../components/icons";
import {
DisclosureCollapseRail,
Expand Down Expand Up @@ -365,7 +365,7 @@ export const ActivityGroup = memo(function ActivityGroup({
onUserInteraction={claimDisclosure}
{...(item.delegate ? { delegate: item.delegate } : {})}
/>
<ReviewChangeCard message={item.message} />
<ReviewChangeCards message={item.message} />
</Fragment>
);
}
Expand Down
Loading