| author | |
| committer | |
| log | 7a68e570eb821e5e905d5c8cee71a8aceff7be95 |
| tree | 7df74c96b4462361d79b9bc55c453468b965d060 |
| parent | 99a906753ec9c0f86eff6fe306062f0c2c3d15df |
| signature | Commit is signed but in an unrecognized format. |
2 files changed, 108 insertions(+), 75 deletions(-)
apps/renderer/src/components/ChatView.tsx+107| ... | ... | @@ -113,6 +113,7 @@ export default function ChatView() { |
| 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() { |
| 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() { |
| 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 */} |
apps/renderer/src/components/Sidebar.tsx+1-75| ... | ... | @@ -4,6 +4,7 @@ import { readNativeApi } from "../session-logic"; |
| 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() { |
| 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() { |
| 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() { |
| 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) => { |