authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-14 22:43:27-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-14 23:35:48-08:00
log78fabcfde9576fbcca2133d0ec173485ba879bc0
tree6b7be11788a67a183a154b8c858d458f76d7bde6
parent7690837f38c56b79d26f61b16fc60ad671dcc1ee

Refine git action success toasts with contextual summaries

- replace generic success text with action-specific toast titles/descriptions - include short SHA/branch details for push and commit outcomes - add tests for PR/commit/push summaries and description truncation

3 files changed, 131 insertions(+), 16 deletions(-)

apps/web/src/components/GitActionsControl.logic.test.ts+91
...@@ -4,6 +4,7 @@ import {...@@ -4,6 +4,7 @@ import {
4 buildMenuItems,4 buildMenuItems,
5 requiresDefaultBranchConfirmation,5 requiresDefaultBranchConfirmation,
6 resolveQuickAction,6 resolveQuickAction,
7 summarizeGitResult,
7} from "./GitActionsControl.logic";8} from "./GitActionsControl.logic";
89
9function status(overrides: Partial<GitStatusResult> = {}): GitStatusResult {10function status(overrides: Partial<GitStatusResult> = {}): GitStatusResult {
...@@ -577,3 +578,93 @@ describe("requiresDefaultBranchConfirmation", () => {...@@ -577,3 +578,93 @@ describe("requiresDefaultBranchConfirmation", () => {
577 assert.isFalse(requiresDefaultBranchConfirmation("commit_push", false));578 assert.isFalse(requiresDefaultBranchConfirmation("commit_push", false));
578 });579 });
579});580});
581
582describe("summarizeGitResult", () => {
583 it("returns commit-focused toast for commit action", () => {
584 const result = summarizeGitResult({
585 action: "commit",
586 commit: {
587 status: "created",
588 commitSha: "0123456789abcdef",
589 subject: "feat: add optimistic UI for git action button",
590 },
591 push: { status: "skipped_not_requested" },
592 pr: { status: "skipped_not_requested" },
593 });
594
595 assert.deepEqual(result, {
596 title: "Committed 0123456",
597 description: "feat: add optimistic UI for git action button",
598 });
599 });
600
601 it("returns push-focused toast for push action", () => {
602 const result = summarizeGitResult({
603 action: "commit_push",
604 commit: {
605 status: "created",
606 commitSha: "abcdef0123456789",
607 subject: "fix: tighten quick action tooltip hover handling",
608 },
609 push: {
610 status: "pushed",
611 branch: "foo",
612 upstreamBranch: "origin/foo",
613 },
614 pr: { status: "skipped_not_requested" },
615 });
616
617 assert.deepEqual(result, {
618 title: "Pushed abcdef0 to foo",
619 description: "fix: tighten quick action tooltip hover handling",
620 });
621 });
622
623 it("returns PR-focused toast for created PR action", () => {
624 const result = summarizeGitResult({
625 action: "commit_push_pr",
626 commit: {
627 status: "created",
628 commitSha: "89abcdef01234567",
629 subject: "feat: ship github shortcuts",
630 },
631 push: {
632 status: "pushed",
633 branch: "foo",
634 },
635 pr: {
636 status: "created",
637 number: 42,
638 title: "feat: ship github shortcuts and improve PR CTA in success toast",
639 },
640 });
641
642 assert.deepEqual(result, {
643 title: "Created PR #42",
644 description: "feat: ship github shortcuts and improve PR CTA in success toast",
645 });
646 });
647
648 it("truncates long description text", () => {
649 const result = summarizeGitResult({
650 action: "commit_push_pr",
651 commit: {
652 status: "created",
653 commitSha: "89abcdef01234567",
654 subject: "short subject",
655 },
656 push: { status: "pushed", branch: "foo" },
657 pr: {
658 status: "created",
659 number: 99,
660 title:
661 "feat: this title is intentionally extremely long so we can validate that toast descriptions are truncated with an ellipsis suffix",
662 },
663 });
664
665 assert.deepEqual(result, {
666 title: "Created PR #99",
667 description: "feat: this title is intentionally extremely long so we can validate t...",
668 });
669 });
670});
apps/web/src/components/GitActionsControl.logic.ts+36-13
...@@ -21,28 +21,51 @@ export interface GitQuickAction {...@@ -21,28 +21,51 @@ export interface GitQuickAction {
21 hint?: string;21 hint?: string;
22}22}
2323
24export function describeGitResult(result: GitRunStackedActionResult): string {24const SHORT_SHA_LENGTH = 7;
25 const parts: string[] = [];25const TOAST_DESCRIPTION_MAX = 72;
2626
27 if (result.commit.status === "created") {27function shortenSha(sha: string | undefined): string | null {
28 parts.push(result.commit.subject ?? "Committed changes");28 if (!sha) return null;
29 return sha.slice(0, SHORT_SHA_LENGTH);
30}
31
32function truncateText(value: string | undefined, maxLength = TOAST_DESCRIPTION_MAX): string | undefined {
33 if (!value) return undefined;
34 if (value.length <= maxLength) return value;
35 if (maxLength <= 3) return "...".slice(0, maxLength);
36 return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
37}
38
39export function summarizeGitResult(
40 result: GitRunStackedActionResult,
41): { title: string; description?: string } {
42 const withDescription = (title: string, description: string | undefined) =>
43 description ? { title, description } : { title };
44
45 if (result.pr.status === "created" || result.pr.status === "opened_existing") {
46 const prNumber = result.pr.number ? ` #${result.pr.number}` : "";
47 const title = `${result.pr.status === "created" ? "Created PR" : "Opened PR"}${prNumber}`;
48 return withDescription(title, truncateText(result.pr.title));
29 }49 }
3050
31 if (result.push.status === "pushed") {51 if (result.push.status === "pushed") {
32 parts.push(52 const shortSha = shortenSha(result.commit.commitSha);
33 result.push.upstreamBranch53 const branch = result.push.branch ?? result.push.upstreamBranch;
34 ? `Pushed to ${result.push.upstreamBranch}`54 const pushedCommitPart = shortSha ? ` ${shortSha}` : "";
35 : "Pushed",55 const branchPart = branch ? ` to ${branch}` : "";
56 return withDescription(
57 `Pushed${pushedCommitPart}${branchPart}`,
58 truncateText(result.commit.subject),
36 );59 );
37 }60 }
3861
39 if (result.pr.status === "created") {62 if (result.commit.status === "created") {
40 parts.push(result.pr.number ? `Created PR #${result.pr.number}` : "Created PR");63 const shortSha = shortenSha(result.commit.commitSha);
41 } else if (result.pr.status === "opened_existing") {64 const title = shortSha ? `Committed ${shortSha}` : "Committed changes";
42 parts.push(result.pr.number ? `PR #${result.pr.number}` : "Opened existing PR");65 return withDescription(title, truncateText(result.commit.subject));
43 }66 }
4467
45 return parts.join(" · ") || "No changes needed.";68 return { title: "Done" };
46}69}
4770
48export function buildMenuItems(71export function buildMenuItems(
apps/web/src/components/GitActionsControl.tsx+4-3
...@@ -13,9 +13,9 @@ import {...@@ -13,9 +13,9 @@ import {
13 type GitActionMenuItem,13 type GitActionMenuItem,
14 type GitDialogAction,14 type GitDialogAction,
15 type GitQuickAction,15 type GitQuickAction,
16 describeGitResult,
17 requiresDefaultBranchConfirmation,16 requiresDefaultBranchConfirmation,
18 resolveQuickAction,17 resolveQuickAction,
18 summarizeGitResult,
19} from "./GitActionsControl.logic";19} from "./GitActionsControl.logic";
20import { Badge } from "~/components/ui/badge";20import { Badge } from "~/components/ui/badge";
21import { Button } from "~/components/ui/button";21import { Button } from "~/components/ui/button";
...@@ -282,6 +282,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -282,6 +282,7 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
282 try {282 try {
283 const result = await promise;283 const result = await promise;
284 stopProgressUpdates();284 stopProgressUpdates();
285 const resultToast = summarizeGitResult(result);
285286
286 const prUrl = result.pr.url ?? gitStatus?.openPr?.url;287 const prUrl = result.pr.url ?? gitStatus?.openPr?.url;
287 const shouldOfferPushCta = action === "commit" && result.commit.status === "created";288 const shouldOfferPushCta = action === "commit" && result.commit.status === "created";
...@@ -292,8 +293,8 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -292,8 +293,8 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
292293
293 toastManager.update(progressToastId, {294 toastManager.update(progressToastId, {
294 type: "success",295 type: "success",
295 title: "Done",296 title: resultToast.title,
296 description: describeGitResult(result),297 description: resultToast.description,
297 timeout: 10_000,298 timeout: 10_000,
298 ...(shouldOfferPushCta299 ...(shouldOfferPushCta
299 ? {300 ? {