Skip to content

Commit f2a29ee

Browse files
authored
Merge pull request #184 from pylon-code/upstream/2026-08-29-file-attachments
feat(web): attach PDFs, ZIPs, and other files to a turn
2 parents abe70a7 + a7d79e8 commit f2a29ee

30 files changed

Lines changed: 3934 additions & 264 deletions

apps/web/src/components/ChatView.logic.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
shouldDockDraftHeroForSubmission,
3838
shouldReleaseTimelineAnchorForToolActivity,
3939
shouldShowBranchMismatchBanner,
40+
shouldShowPlanFollowUpPrompt,
4041
shouldWriteThreadErrorToCurrentServerThread,
4142
} from "./ChatView.logic";
4243

@@ -636,6 +637,31 @@ describe("shouldShowBranchMismatchBanner", () => {
636637
});
637638
});
638639

640+
describe("shouldShowPlanFollowUpPrompt", () => {
641+
const base = {
642+
pendingUserInputCount: 0,
643+
interactionMode: "plan" as const,
644+
latestTurnSettled: true,
645+
hasActionableProposedPlan: true,
646+
hasComposerAttachments: false,
647+
};
648+
649+
it("shows plan actions for a settled actionable plan without attachments", () => {
650+
expect(shouldShowPlanFollowUpPrompt(base)).toBe(true);
651+
});
652+
653+
it("hides plan actions while the composer has staged attachments", () => {
654+
expect(shouldShowPlanFollowUpPrompt({ ...base, hasComposerAttachments: true })).toBe(false);
655+
});
656+
657+
it("preserves the existing plan follow-up gates", () => {
658+
expect(shouldShowPlanFollowUpPrompt({ ...base, pendingUserInputCount: 1 })).toBe(false);
659+
expect(shouldShowPlanFollowUpPrompt({ ...base, interactionMode: "default" })).toBe(false);
660+
expect(shouldShowPlanFollowUpPrompt({ ...base, latestTurnSettled: false })).toBe(false);
661+
expect(shouldShowPlanFollowUpPrompt({ ...base, hasActionableProposedPlan: false })).toBe(false);
662+
});
663+
});
664+
639665
describe("session branch mismatch dismissal", () => {
640666
it("tracks dismissed keys and treats other keys as active", () => {
641667
expect(isBranchMismatchDismissedForSession("t1:a:b")).toBe(false);

apps/web/src/components/ChatView.logic.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ProjectId,
55
type MessageId,
66
type ModelSelection,
7+
type ProviderInteractionMode,
78
type ProviderDriverKind,
89
type ServerProvider,
910
type ScopedProjectRef,
@@ -446,6 +447,22 @@ export function shouldShowBranchMismatchBanner(input: {
446447
return input.composerHasContent || input.wasShownForCurrentMismatch;
447448
}
448449

450+
export function shouldShowPlanFollowUpPrompt(input: {
451+
pendingUserInputCount: number;
452+
interactionMode: ProviderInteractionMode;
453+
latestTurnSettled: boolean;
454+
hasActionableProposedPlan: boolean;
455+
hasComposerAttachments: boolean;
456+
}): boolean {
457+
return (
458+
input.pendingUserInputCount === 0 &&
459+
input.interactionMode === "plan" &&
460+
input.latestTurnSettled &&
461+
input.hasActionableProposedPlan &&
462+
!input.hasComposerAttachments
463+
);
464+
}
465+
449466
// Session-scoped (module-level so it survives ChatView remounts, e.g. route
450467
// changes). Durable cross-device dismissal is planned as a server-side ack.
451468
const sessionDismissedBranchMismatchKeys = new Set<string>();

0 commit comments

Comments
 (0)