| 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,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); |
| 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 | // Auto-scroll on new messages | 243 | // 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> |
| 779 | 886 | ||
| 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"; |
| 4 | import { useStore } from "../store"; | 4 | import { useStore } from "../store"; |
| 5 | import type { Project } from "../types"; | 5 | import type { Project } from "../types"; |
| 6 | 6 | ||
| 7 | |||
| 7 | function formatRelativeTime(iso: string): string { | 8 | function 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); | ||
| 32 | 32 | ||
| 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 | }; |
| 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 | 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> |
| 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 | {/* 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) => { |