authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-09 00:48:59-08:00
committergravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-02-09 00:48:59-08:00
log7a68e570eb821e5e905d5c8cee71a8aceff7be95
tree7df74c96b4462361d79b9bc55c453468b965d060
parent99a906753ec9c0f86eff6fe306062f0c2c3d15df
signature Commit is signed but in an unrecognized format.

better


2 files changed, 108 insertions(+), 75 deletions(-)

apps/renderer/src/components/ChatView.tsx+107
...@@ -113,6 +113,7 @@ export default function ChatView() {...@@ -113,6 +113,7 @@ export default function ChatView() {
113 const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);113 const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
114 const [selectedEffort, setSelectedEffort] =114 const [selectedEffort, setSelectedEffort] =
115 useState<string>(DEFAULT_REASONING);115 useState<string>(DEFAULT_REASONING);
116 const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);
116 const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>(117 const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>(
117 [],118 [],
118 );119 );
...@@ -210,6 +211,35 @@ export default function ChatView() {...@@ -210,6 +211,35 @@ export default function ChatView() {
210 sandboxMode: "workspace-write",211 sandboxMode: "workspace-write",
211 } as const);212 } as const);
212213
214 const handleRuntimeModeChange = async (
215 mode: "approval-required" | "full-access",
216 ) => {
217 if (mode === state.runtimeMode) return;
218 dispatch({ type: "SET_RUNTIME_MODE", mode });
219 if (!api) return;
220
221 const sessionIds = state.threads
222 .map((t) => t.session)
223 .filter(
224 (s): s is NonNullable<typeof s> =>
225 s !== null && s.status !== "closed",
226 )
227 .map((s) => s.sessionId);
228
229 if (sessionIds.length === 0) return;
230
231 setIsSwitchingRuntimeMode(true);
232 try {
233 await Promise.all(
234 sessionIds.map((id) =>
235 api.providers.stopSession({ sessionId: id }).catch(() => undefined),
236 ),
237 );
238 } finally {
239 setIsSwitchingRuntimeMode(false);
240 }
241 };
242
213 // Auto-scroll on new messages243 // Auto-scroll on new messages
214 const messageCount = activeThread?.messages.length ?? 0;244 const messageCount = activeThread?.messages.length ?? 0;
215 const workLogCount = workLogEntries.length;245 const workLogCount = workLogEntries.length;
...@@ -775,6 +805,83 @@ export default function ChatView() {...@@ -775,6 +805,83 @@ export default function ChatView() {
775 />805 />
776 </svg>806 </svg>
777 </label>807 </label>
808
809 {/* Divider */}
810 <div className="mx-0.5 h-4 w-px bg-white/[0.08]" />
811
812 {/* Runtime mode toggle */}
813 <button
814 type="button"
815 className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]"
816 disabled={isSwitchingRuntimeMode}
817 onClick={() =>
818 void handleRuntimeModeChange(
819 state.runtimeMode === "full-access"
820 ? "approval-required"
821 : "full-access",
822 )
823 }
824 title={
825 state.runtimeMode === "full-access"
826 ? "Full access — click to require approvals"
827 : "Approval required — click for full access"
828 }
829 >
830 {state.runtimeMode === "full-access" ? (
831 <svg
832 width="14"
833 height="14"
834 viewBox="0 0 14 14"
835 fill="none"
836 aria-hidden="true"
837 >
838 <rect
839 x="2"
840 y="5.5"
841 width="10"
842 height="7"
843 rx="1.5"
844 stroke="currentColor"
845 strokeWidth="1.3"
846 />
847 <path
848 d="M9.5 5.5V4a2.5 2.5 0 0 0-5 0"
849 stroke="currentColor"
850 strokeWidth="1.3"
851 strokeLinecap="round"
852 />
853 </svg>
854 ) : (
855 <svg
856 width="14"
857 height="14"
858 viewBox="0 0 14 14"
859 fill="none"
860 aria-hidden="true"
861 >
862 <rect
863 x="2"
864 y="5.5"
865 width="10"
866 height="7"
867 rx="1.5"
868 stroke="currentColor"
869 strokeWidth="1.3"
870 />
871 <path
872 d="M4.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5"
873 stroke="currentColor"
874 strokeWidth="1.3"
875 strokeLinecap="round"
876 />
877 </svg>
878 )}
879 <span>
880 {state.runtimeMode === "full-access"
881 ? "Full access"
882 : "Supervised"}
883 </span>
884 </button>
778 </div>885 </div>
779886
780 {/* Right side: send / stop button */}887 {/* Right side: send / stop button */}
apps/renderer/src/components/Sidebar.tsx+1-75
...@@ -4,6 +4,7 @@ import { readNativeApi } from "../session-logic";...@@ -4,6 +4,7 @@ import { readNativeApi } from "../session-logic";
4import { useStore } from "../store";4import { useStore } from "../store";
5import type { Project } from "../types";5import type { Project } from "../types";
66
7
7function formatRelativeTime(iso: string): string {8function formatRelativeTime(iso: string): string {
8 const diff = Date.now() - new Date(iso).getTime();9 const diff = Date.now() - new Date(iso).getTime();
9 const minutes = Math.floor(diff / 60_000);10 const minutes = Math.floor(diff / 60_000);
...@@ -28,7 +29,6 @@ export default function Sidebar() {...@@ -28,7 +29,6 @@ export default function Sidebar() {
28 const [addingProject, setAddingProject] = useState(false);29 const [addingProject, setAddingProject] = useState(false);
29 const [newCwd, setNewCwd] = useState("");30 const [newCwd, setNewCwd] = useState("");
30 const [isPickingFolder, setIsPickingFolder] = useState(false);31 const [isPickingFolder, setIsPickingFolder] = useState(false);
31 const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);
3232
33 const handleAddProject = () => {33 const handleAddProject = () => {
34 const cwd = newCwd.trim();34 const cwd = newCwd.trim();
...@@ -80,43 +80,6 @@ export default function Sidebar() {...@@ -80,43 +80,6 @@ export default function Sidebar() {
80 }80 }
81 };81 };
8282
83 const handleRuntimeModeChange = useCallback(
84 async (mode: "approval-required" | "full-access") => {
85 if (mode === state.runtimeMode) return;
86
87 dispatch({
88 type: "SET_RUNTIME_MODE",
89 mode,
90 });
91
92 if (!api) return;
93
94 const sessionIds = state.threads
95 .map((thread) => thread.session)
96 .filter(
97 (session): session is NonNullable<typeof session> =>
98 session !== null && session.status !== "closed",
99 )
100 .map((session) => session.sessionId);
101
102 if (sessionIds.length === 0) {
103 return;
104 }
105
106 setIsSwitchingRuntimeMode(true);
107 try {
108 await Promise.all(
109 sessionIds.map((sessionId) =>
110 api.providers.stopSession({ sessionId }).catch(() => undefined),
111 ),
112 );
113 } finally {
114 setIsSwitchingRuntimeMode(false);
115 }
116 },
117 [api, dispatch, state.runtimeMode, state.threads],
118 );
119
120 useEffect(() => {83 useEffect(() => {
121 const onWindowKeyDown = (event: KeyboardEvent) => {84 const onWindowKeyDown = (event: KeyboardEvent) => {
122 const isNewThreadShortcut =85 const isNewThreadShortcut =
...@@ -189,43 +152,6 @@ export default function Sidebar() {...@@ -189,43 +152,6 @@ export default function Sidebar() {
189 </button>152 </button>
190 </div>153 </div>
191154
192 <div className="px-3 pb-3">
193 <div className="rounded-lg border border-white/[0.08] bg-white/[0.03] p-2">
194 <p className="px-1 text-[10px] font-medium uppercase tracking-[0.08em] text-[#a0a0a0]/60">
195 Runtime mode
196 </p>
197 <p className="px-1 pt-0.5 pb-2 text-[10px] text-[#a0a0a0]/45">
198 Switching restarts live sessions.
199 </p>
200 <div className="grid grid-cols-2 gap-1">
201 <button
202 type="button"
203 className={`rounded-md px-2 py-1.5 text-[11px] transition-colors duration-150 ${
204 state.runtimeMode === "approval-required"
205 ? "bg-white/12 text-white"
206 : "text-[#a0a0a0]/70 hover:bg-white/[0.06]"
207 }`}
208 disabled={isSwitchingRuntimeMode}
209 onClick={() => void handleRuntimeModeChange("approval-required")}
210 >
211 Approval required
212 </button>
213 <button
214 type="button"
215 className={`rounded-md px-2 py-1.5 text-[11px] transition-colors duration-150 ${
216 state.runtimeMode === "full-access"
217 ? "bg-white/12 text-white"
218 : "text-[#a0a0a0]/70 hover:bg-white/[0.06]"
219 }`}
220 disabled={isSwitchingRuntimeMode}
221 onClick={() => void handleRuntimeModeChange("full-access")}
222 >
223 Full access
224 </button>
225 </div>
226 </div>
227 </div>
228
229 {/* Project list */}155 {/* Project list */}
230 <nav className="flex-1 overflow-y-auto px-2">156 <nav className="flex-1 overflow-y-auto px-2">
231 {state.projects.map((project) => {157 {state.projects.map((project) => {