authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 11:20:38-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 11:20:38-08:00
log3c939e8e694bbba5b16bd13d617fb0d41f229eb0
tree308237a08f65d036a4a8ae7303a5aee108f8d8c9
parent6fc632f9a162abcfaa8ed1bdc659ecce526f26ed

Use Lucide icons for ChatView git action menu

- Replace custom inline SVGs with Lucide commit, push, and GitHub icons - Keep git action behavior unchanged while applying minor style cleanup

1 files changed, 15 insertions(+), 71 deletions(-)

apps/web/src/components/ChatView.tsx+15-71
...@@ -40,6 +40,7 @@ import BranchToolbar from "./BranchToolbar";...@@ -40,6 +40,7 @@ import BranchToolbar from "./BranchToolbar";
40import { isTerminalToggleShortcut } from "../terminal-shortcuts";40import { isTerminalToggleShortcut } from "../terminal-shortcuts";
41import ChatMarkdown from "./ChatMarkdown";41import ChatMarkdown from "./ChatMarkdown";
42import ThreadTerminalDrawer from "./ThreadTerminalDrawer";42import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
43import { CloudUploadIcon, GitCommitIcon, GithubIcon } from "lucide-react";
4344
44function formatMessageMeta(createdAt: string, duration: string | null): string {45function formatMessageMeta(createdAt: string, duration: string | null): string {
45 if (!duration) return formatTimestamp(createdAt);46 if (!duration) return formatTimestamp(createdAt);
...@@ -70,62 +71,14 @@ function GitActionIcon(props: { icon: GitActionMenuItem["icon"]; disabled: boole...@@ -70,62 +71,14 @@ function GitActionIcon(props: { icon: GitActionMenuItem["icon"]; disabled: boole
70 const toneClass = props.disabled ? "text-muted-foreground/45" : "text-foreground/85";71 const toneClass = props.disabled ? "text-muted-foreground/45" : "text-foreground/85";
7172
72 if (props.icon === "commit") {73 if (props.icon === "commit") {
73 return (74 return <GitCommitIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;
74 <svg
75 viewBox="0 0 20 20"
76 aria-hidden="true"
77 className={`h-5 w-5 shrink-0 ${toneClass}`}
78 fill="none"
79 stroke="currentColor"
80 strokeWidth="1.8"
81 strokeLinecap="round"
82 strokeLinejoin="round"
83 >
84 <circle cx="10" cy="10" r="4.2" />
85 <path d="M1.8 10h4.2" />
86 <path d="M14 10h4.2" />
87 </svg>
88 );
89 }75 }
9076
91 if (props.icon === "push") {77 if (props.icon === "push") {
92 return (78 return <CloudUploadIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;
93 <svg
94 viewBox="0 0 20 20"
95 aria-hidden="true"
96 className={`h-5 w-5 shrink-0 ${toneClass}`}
97 fill="none"
98 stroke="currentColor"
99 strokeWidth="1.8"
100 strokeLinecap="round"
101 strokeLinejoin="round"
102 >
103 <path d="M5.2 16h9.6a3.2 3.2 0 0 0 .2-6.4h-.5A4.8 4.8 0 0 0 5 9 3.7 3.7 0 0 0 5.2 16Z" />
104 <path d="M10 5.8v6.2" />
105 <path d="m7.7 8.1 2.3-2.3 2.3 2.3" />
106 </svg>
107 );
108 }79 }
10980
110 return (81 return <GithubIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;
111 <svg
112 viewBox="0 0 20 20"
113 aria-hidden="true"
114 className={`h-5 w-5 shrink-0 ${toneClass}`}
115 fill="none"
116 stroke="currentColor"
117 strokeWidth="1.8"
118 strokeLinecap="round"
119 strokeLinejoin="round"
120 >
121 <circle cx="6" cy="5" r="1.7" />
122 <circle cx="6" cy="15" r="1.7" />
123 <circle cx="14" cy="10" r="1.7" />
124 <path d="M6 6.8v6.5" />
125 <path d="M7.8 6.1c2.9 0 3.8 1.4 5.1 2.9" />
126 <path d="M7.8 13.9c2.9 0 3.8-1.4 5.1-2.9" />
127 </svg>
128 );
129}82}
13083
131function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {84function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
...@@ -334,8 +287,7 @@ export default function ChatView() {...@@ -334,8 +287,7 @@ export default function ChatView() {
334 sandboxMode: "workspace-write",287 sandboxMode: "workspace-write",
335 } as const);288 } as const);
336 const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null;289 const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null;
337 const gitBaseDisabled =290 const gitBaseDisabled = !api || !gitCwd || !gitStatus || isGitActionRunning;
338 !api || !gitCwd || !gitStatus || isGitActionRunning;
339 const gitActionMenuItems = useMemo<GitActionMenuItem[]>(() => {291 const gitActionMenuItems = useMemo<GitActionMenuItem[]>(() => {
340 if (!gitStatus) return [];292 if (!gitStatus) return [];
341293
...@@ -343,7 +295,9 @@ export default function ChatView() {...@@ -343,7 +295,9 @@ export default function ChatView() {
343 const hasOpenPr = gitStatus.openPr !== null;295 const hasOpenPr = gitStatus.openPr !== null;
344 const canCommit = !gitBaseDisabled && gitStatus.hasWorkingTreeChanges;296 const canCommit = !gitBaseDisabled && gitStatus.hasWorkingTreeChanges;
345 const canPush =297 const canPush =
346 !gitBaseDisabled && hasBranch && (gitStatus.hasWorkingTreeChanges || gitStatus.aheadCount > 0);298 !gitBaseDisabled &&
299 hasBranch &&
300 (gitStatus.hasWorkingTreeChanges || gitStatus.aheadCount > 0);
347 const canViewPr =301 const canViewPr =
348 !gitBaseDisabled &&302 !gitBaseDisabled &&
349 hasBranch &&303 hasBranch &&
...@@ -403,9 +357,7 @@ export default function ChatView() {...@@ -403,9 +357,7 @@ export default function ChatView() {
403 action,357 action,
404 });358 });
405 } catch (error) {359 } catch (error) {
406 setGitActionError(360 setGitActionError(error instanceof Error ? error.message : "Git action failed.");
407 error instanceof Error ? error.message : "Git action failed.",
408 );
409 } finally {361 } finally {
410 setIsGitActionRunning(false);362 setIsGitActionRunning(false);
411 try {363 try {
...@@ -425,9 +377,7 @@ export default function ChatView() {...@@ -425,9 +377,7 @@ export default function ChatView() {
425 (activeThread.messages.length > 0 ||377 (activeThread.messages.length > 0 ||
426 (activeThread.session !== null && activeThread.session.status !== "closed")),378 (activeThread.session !== null && activeThread.session.status !== "closed")),
427 );379 );
428 const terminalShortcutHint = navigator.platform.includes("Mac")380 const terminalShortcutHint = navigator.platform.includes("Mac") ? "\u2318J" : "Ctrl+J";
429 ? "\u2318J"
430 : "Ctrl+J";
431 const focusComposer = useCallback(() => {381 const focusComposer = useCallback(() => {
432 const textarea = textareaRef.current;382 const textarea = textareaRef.current;
433 if (!textarea) return;383 if (!textarea) return;
...@@ -505,11 +455,7 @@ export default function ChatView() {...@@ -505,11 +455,7 @@ export default function ChatView() {
505 } catch (error) {455 } catch (error) {
506 if (!cancelled) {456 if (!cancelled) {
507 setGitStatus(null);457 setGitStatus(null);
508 setGitActionError(458 setGitActionError(error instanceof Error ? error.message : "Failed to read git status.");
509 error instanceof Error
510 ? error.message
511 : "Failed to read git status.",
512 );
513 }459 }
514 }460 }
515 };461 };
...@@ -927,9 +873,7 @@ export default function ChatView() {...@@ -927,9 +873,7 @@ export default function ChatView() {
927 </button>873 </button>
928 {isGitMenuOpen && (874 {isGitMenuOpen && (
929 <div className="absolute right-0 top-full z-50 mt-1 w-[280px] rounded-3xl border border-border bg-popover p-3 shadow-xl">875 <div className="absolute right-0 top-full z-50 mt-1 w-[280px] rounded-3xl border border-border bg-popover p-3 shadow-xl">
930 <p className="px-3 pb-2 text-[13px] text-muted-foreground/75">876 <p className="px-3 pb-2 text-[13px] text-muted-foreground/75">Git actions</p>
931 Git actions
932 </p>
933 {gitActionMenuItems.map((item) => {877 {gitActionMenuItems.map((item) => {
934 return (878 return (
935 <button879 <button
...@@ -1036,7 +980,7 @@ export default function ChatView() {...@@ -1036,7 +980,7 @@ export default function ChatView() {
1036 </button>980 </button>
1037 <button981 <button
1038 type="button"982 type="button"
1039 className="rounded-md border border-sky-300/30 bg-sky-500/[0.15] px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/[0.22] disabled:cursor-not-allowed disabled:opacity-50"983 className="rounded-md border border-sky-300/30 bg-sky-500/15 px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/22 disabled:cursor-not-allowed disabled:opacity-50"
1040 disabled={isResponding}984 disabled={isResponding}
1041 onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")}985 onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")}
1042 >986 >
...@@ -1052,7 +996,7 @@ export default function ChatView() {...@@ -1052,7 +996,7 @@ export default function ChatView() {
1052 </button>996 </button>
1053 <button997 <button
1054 type="button"998 type="button"
1055 className="rounded-md border border-rose-300/30 bg-rose-500/[0.12] px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/[0.2] disabled:cursor-not-allowed disabled:opacity-50"999 className="rounded-md border border-rose-300/30 bg-rose-500/12 px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/20 disabled:cursor-not-allowed disabled:opacity-50"
1056 disabled={isResponding}1000 disabled={isResponding}
1057 onClick={() => void onRespondToApproval(approval.requestId, "cancel")}1001 onClick={() => void onRespondToApproval(approval.requestId, "cancel")}
1058 >1002 >
...@@ -1171,7 +1115,7 @@ export default function ChatView() {...@@ -1171,7 +1115,7 @@ export default function ChatView() {
1171 <Fragment key={timelineEntry.id}>1115 <Fragment key={timelineEntry.id}>
1172 <div className="flex justify-end">1116 <div className="flex justify-end">
1173 <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3">1117 <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3">
1174 <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-foreground">1118 <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground">
1175 {timelineEntry.message.text}1119 {timelineEntry.message.text}
1176 </pre>1120 </pre>
1177 <p className="mt-1.5 text-right text-[10px] text-muted-foreground/30">1121 <p className="mt-1.5 text-right text-[10px] text-muted-foreground/30">