Unified · Split
| @@ -113,6 +113,7 @@ export default function ChatView() { · 112 unmodified lines | ||
| 113 | 113 | const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); |
| 114 | 114 | const [selectedEffort, setSelectedEffort] = |
| 115 | 115 | useState<string>(DEFAULT_REASONING); |
| 116 | + const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); | |
| 116 | 117 | const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>( |
| 117 | 118 | [], |
| 118 | 119 | ); |
| @@ -210,6 +211,35 @@ export default function ChatView() { · 91 unmodified lines | ||
| 210 | 211 | sandboxMode: "workspace-write", |
| 211 | 212 | } as const); |
| 212 | 213 | |
| 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 | 243 | // Auto-scroll on new messages |
| 214 | 244 | const messageCount = activeThread?.messages.length ?? 0; |
| 215 | 245 | const workLogCount = workLogEntries.length; |
| @@ -775,6 +805,83 @@ export default function ChatView() { · 559 unmodified lines | ||
| 775 | 805 | /> |
| 776 | 806 | </svg> |
| 777 | 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 | 885 | </div> |
| 779 | 886 | |
| 780 | 887 | {/* Right side: send / stop button */} |
| @@ -4,6 +4,7 @@ import { readNativeApi } from "../session-logic"; · 3 unmodified lines | ||
| 4 | 4 | import { useStore } from "../store"; |
| 5 | 5 | import type { Project } from "../types"; |
| 6 | 6 | |
| 7 | + | |
| 7 | 8 | function formatRelativeTime(iso: string): string { |
| 8 | 9 | const diff = Date.now() - new Date(iso).getTime(); |
| 9 | 10 | const minutes = Math.floor(diff / 60_000); |
| @@ -28,7 +29,6 @@ export default function Sidebar() { · 18 unmodified lines | ||
| 28 | 29 | const [addingProject, setAddingProject] = useState(false); |
| 29 | 30 | const [newCwd, setNewCwd] = useState(""); |
| 30 | 31 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 31 | − const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); | |
| 32 | 32 | |
| 33 | 33 | const handleAddProject = () => { |
| 34 | 34 | const cwd = newCwd.trim(); |
| @@ -80,43 +80,6 @@ export default function Sidebar() { · 45 unmodified lines | ||
| 80 | 80 | } |
| 81 | 81 | }; |
| 82 | 82 | |
| 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 | 83 | useEffect(() => { |
| 121 | 84 | const onWindowKeyDown = (event: KeyboardEvent) => { |
| 122 | 85 | const isNewThreadShortcut = |
| @@ -189,43 +152,6 @@ export default function Sidebar() { · 66 unmodified lines | ||
| 189 | 152 | </button> |
| 190 | 153 | </div> |
| 191 | 154 | |
| 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 | 155 | {/* Project list */} |
| 230 | 156 | <nav className="flex-1 overflow-y-auto px-2"> |
| 231 | 157 | {state.projects.map((project) => { |