@vibe80/vibe80 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. package/server/vitest.config.js +23 -0
@@ -0,0 +1,114 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ export default function useGitIdentity({ t, apiFetch, attachmentSessionId }) {
4
+ const [gitIdentityName, setGitIdentityName] = useState("");
5
+ const [gitIdentityEmail, setGitIdentityEmail] = useState("");
6
+ const [gitIdentityGlobal, setGitIdentityGlobal] = useState({
7
+ name: "",
8
+ email: "",
9
+ });
10
+ const [gitIdentityRepo, setGitIdentityRepo] = useState({
11
+ name: "",
12
+ email: "",
13
+ });
14
+ const [gitIdentityLoading, setGitIdentityLoading] = useState(false);
15
+ const [gitIdentitySaving, setGitIdentitySaving] = useState(false);
16
+ const [gitIdentityError, setGitIdentityError] = useState("");
17
+ const [gitIdentityMessage, setGitIdentityMessage] = useState("");
18
+
19
+ const loadGitIdentity = useCallback(async () => {
20
+ if (!attachmentSessionId) {
21
+ return;
22
+ }
23
+ setGitIdentityLoading(true);
24
+ setGitIdentityError("");
25
+ setGitIdentityMessage("");
26
+ try {
27
+ const response = await apiFetch(
28
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/git-identity`
29
+ );
30
+ if (!response.ok) {
31
+ throw new Error(t("Unable to load Git identity."));
32
+ }
33
+ const payload = await response.json();
34
+ const globalName = payload?.global?.name || "";
35
+ const globalEmail = payload?.global?.email || "";
36
+ const repoName = payload?.repo?.name || "";
37
+ const repoEmail = payload?.repo?.email || "";
38
+ setGitIdentityGlobal({ name: globalName, email: globalEmail });
39
+ setGitIdentityRepo({ name: repoName, email: repoEmail });
40
+ setGitIdentityName(repoName || globalName);
41
+ setGitIdentityEmail(repoEmail || globalEmail);
42
+ } catch (error) {
43
+ setGitIdentityError(error?.message || t("Error during loading."));
44
+ } finally {
45
+ setGitIdentityLoading(false);
46
+ }
47
+ }, [attachmentSessionId, apiFetch, t]);
48
+
49
+ const handleSaveGitIdentity = useCallback(async () => {
50
+ if (!attachmentSessionId) {
51
+ return;
52
+ }
53
+ const name = gitIdentityName.trim();
54
+ const email = gitIdentityEmail.trim();
55
+ if (!name || !email) {
56
+ setGitIdentityError(t("Name and email required."));
57
+ return;
58
+ }
59
+ setGitIdentitySaving(true);
60
+ setGitIdentityError("");
61
+ setGitIdentityMessage("");
62
+ try {
63
+ const response = await apiFetch(
64
+ `/api/v1/sessions/${encodeURIComponent(attachmentSessionId)}/git-identity`,
65
+ {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({ name, email }),
69
+ }
70
+ );
71
+ if (!response.ok) {
72
+ const payload = await response.json().catch(() => ({}));
73
+ throw new Error(payload?.error || t("Update failed."));
74
+ }
75
+ const payload = await response.json().catch(() => ({}));
76
+ const repoName = payload?.repo?.name || name;
77
+ const repoEmail = payload?.repo?.email || email;
78
+ setGitIdentityRepo({ name: repoName, email: repoEmail });
79
+ setGitIdentityMessage(t("Repository Git identity updated."));
80
+ } catch (error) {
81
+ setGitIdentityError(error?.message || t("Update failed."));
82
+ } finally {
83
+ setGitIdentitySaving(false);
84
+ }
85
+ }, [
86
+ attachmentSessionId,
87
+ apiFetch,
88
+ gitIdentityEmail,
89
+ gitIdentityName,
90
+ t,
91
+ ]);
92
+
93
+ useEffect(() => {
94
+ if (!attachmentSessionId) {
95
+ return;
96
+ }
97
+ loadGitIdentity();
98
+ }, [attachmentSessionId, loadGitIdentity]);
99
+
100
+ return {
101
+ gitIdentityName,
102
+ gitIdentityEmail,
103
+ gitIdentityGlobal,
104
+ gitIdentityRepo,
105
+ gitIdentityLoading,
106
+ gitIdentitySaving,
107
+ gitIdentityError,
108
+ gitIdentityMessage,
109
+ setGitIdentityName,
110
+ setGitIdentityEmail,
111
+ loadGitIdentity,
112
+ handleSaveGitIdentity,
113
+ };
114
+ }
@@ -0,0 +1,31 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export default function useLayoutMode({ themeMode, setSideOpen }) {
4
+ const [isMobileLayout, setIsMobileLayout] = useState(() =>
5
+ window.matchMedia("(max-width: 1024px)").matches
6
+ );
7
+
8
+ useEffect(() => {
9
+ const query = window.matchMedia("(max-width: 1024px)");
10
+ const update = () => setIsMobileLayout(query.matches);
11
+ update();
12
+ if (query.addEventListener) {
13
+ query.addEventListener("change", update);
14
+ return () => query.removeEventListener("change", update);
15
+ }
16
+ query.addListener(update);
17
+ return () => query.removeListener(update);
18
+ }, []);
19
+
20
+ useEffect(() => {
21
+ document.documentElement.dataset.theme = themeMode;
22
+ }, [themeMode]);
23
+
24
+ useEffect(() => {
25
+ if (isMobileLayout) {
26
+ setSideOpen(false);
27
+ }
28
+ }, [isMobileLayout, setSideOpen]);
29
+
30
+ return { isMobileLayout };
31
+ }
@@ -0,0 +1,131 @@
1
+ import { useEffect } from "react";
2
+
3
+ export default function useLocalPreferences({
4
+ authMode,
5
+ llmProvider,
6
+ selectedProviders,
7
+ openAiAuthMode,
8
+ showChatCommands,
9
+ showToolResults,
10
+ notificationsEnabled,
11
+ themeMode,
12
+ repoHistory,
13
+ debugMode,
14
+ setLlmProvider,
15
+ setOpenAiLoginError,
16
+ setClaudeLoginError,
17
+ AUTH_MODE_KEY,
18
+ LLM_PROVIDER_KEY,
19
+ LLM_PROVIDERS_KEY,
20
+ OPENAI_AUTH_MODE_KEY,
21
+ CHAT_COMMANDS_VISIBLE_KEY,
22
+ TOOL_RESULTS_VISIBLE_KEY,
23
+ NOTIFICATIONS_ENABLED_KEY,
24
+ THEME_MODE_KEY,
25
+ REPO_HISTORY_KEY,
26
+ DEBUG_MODE_KEY,
27
+ }) {
28
+ useEffect(() => {
29
+ try {
30
+ localStorage.setItem(AUTH_MODE_KEY, authMode);
31
+ } catch (error) {
32
+ // Ignore storage errors (private mode, quota).
33
+ }
34
+ }, [AUTH_MODE_KEY, authMode]);
35
+
36
+ useEffect(() => {
37
+ try {
38
+ localStorage.setItem(LLM_PROVIDER_KEY, llmProvider);
39
+ } catch (error) {
40
+ // Ignore storage errors (private mode, quota).
41
+ }
42
+ }, [LLM_PROVIDER_KEY, llmProvider]);
43
+
44
+ useEffect(() => {
45
+ try {
46
+ localStorage.setItem(LLM_PROVIDERS_KEY, JSON.stringify(selectedProviders));
47
+ } catch (error) {
48
+ // Ignore storage errors (private mode, quota).
49
+ }
50
+ }, [LLM_PROVIDERS_KEY, selectedProviders]);
51
+
52
+ useEffect(() => {
53
+ setOpenAiLoginError("");
54
+ setClaudeLoginError("");
55
+ }, [llmProvider, setClaudeLoginError, setOpenAiLoginError]);
56
+
57
+ useEffect(() => {
58
+ if (selectedProviders.includes(llmProvider)) {
59
+ return;
60
+ }
61
+ const fallback = selectedProviders[0] || "codex";
62
+ if (fallback !== llmProvider) {
63
+ setLlmProvider(fallback);
64
+ }
65
+ }, [selectedProviders, llmProvider, setLlmProvider]);
66
+
67
+ useEffect(() => {
68
+ try {
69
+ localStorage.setItem(OPENAI_AUTH_MODE_KEY, openAiAuthMode);
70
+ } catch (error) {
71
+ // Ignore storage errors (private mode, quota).
72
+ }
73
+ }, [OPENAI_AUTH_MODE_KEY, openAiAuthMode]);
74
+
75
+ useEffect(() => {
76
+ try {
77
+ localStorage.setItem(
78
+ CHAT_COMMANDS_VISIBLE_KEY,
79
+ showChatCommands ? "true" : "false"
80
+ );
81
+ } catch (error) {
82
+ // Ignore storage errors (private mode, quota).
83
+ }
84
+ }, [CHAT_COMMANDS_VISIBLE_KEY, showChatCommands]);
85
+
86
+ useEffect(() => {
87
+ try {
88
+ localStorage.setItem(
89
+ TOOL_RESULTS_VISIBLE_KEY,
90
+ showToolResults ? "true" : "false"
91
+ );
92
+ } catch (error) {
93
+ // Ignore storage errors (private mode, quota).
94
+ }
95
+ }, [TOOL_RESULTS_VISIBLE_KEY, showToolResults]);
96
+
97
+ useEffect(() => {
98
+ try {
99
+ localStorage.setItem(
100
+ NOTIFICATIONS_ENABLED_KEY,
101
+ notificationsEnabled ? "true" : "false"
102
+ );
103
+ } catch (error) {
104
+ // Ignore storage errors (private mode, quota).
105
+ }
106
+ }, [NOTIFICATIONS_ENABLED_KEY, notificationsEnabled]);
107
+
108
+ useEffect(() => {
109
+ try {
110
+ localStorage.setItem(THEME_MODE_KEY, themeMode);
111
+ } catch (error) {
112
+ // Ignore storage errors (private mode, quota).
113
+ }
114
+ }, [THEME_MODE_KEY, themeMode]);
115
+
116
+ useEffect(() => {
117
+ try {
118
+ localStorage.setItem(REPO_HISTORY_KEY, JSON.stringify(repoHistory));
119
+ } catch (error) {
120
+ // Ignore storage errors (private mode, quota).
121
+ }
122
+ }, [REPO_HISTORY_KEY, repoHistory]);
123
+
124
+ useEffect(() => {
125
+ try {
126
+ localStorage.setItem(DEBUG_MODE_KEY, debugMode ? "true" : "false");
127
+ } catch (error) {
128
+ // Ignore storage errors (private mode, quota).
129
+ }
130
+ }, [DEBUG_MODE_KEY, debugMode]);
131
+ }
@@ -0,0 +1,30 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useMessageSync({ socketRef, messagesRef }) {
4
+ const requestMessageSync = useCallback(() => {
5
+ const socket = socketRef.current;
6
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
7
+ return;
8
+ }
9
+ const lastSeenMessageId = (() => {
10
+ if (!Array.isArray(messagesRef.current)) {
11
+ return null;
12
+ }
13
+ for (let i = messagesRef.current.length - 1; i >= 0; i -= 1) {
14
+ if (messagesRef.current[i]?.id) {
15
+ return messagesRef.current[i].id;
16
+ }
17
+ }
18
+ return null;
19
+ })();
20
+ socket.send(
21
+ JSON.stringify({
22
+ type: "worktree_messages_sync",
23
+ worktreeId: "main",
24
+ lastSeenMessageId,
25
+ })
26
+ );
27
+ }, [messagesRef, socketRef]);
28
+
29
+ return { requestMessageSync };
30
+ }
@@ -0,0 +1,132 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ export default function useNotifications({ notificationsEnabled, t }) {
4
+ const lastNotifiedIdRef = useRef(null);
5
+ const audioContextRef = useRef(null);
6
+ const soundEnabled = notificationsEnabled;
7
+
8
+ const ensureNotificationPermission = useCallback(async () => {
9
+ if (!("Notification" in window)) {
10
+ return "unsupported";
11
+ }
12
+ if (Notification.permission === "default") {
13
+ try {
14
+ return await Notification.requestPermission();
15
+ } catch (error) {
16
+ return Notification.permission;
17
+ }
18
+ }
19
+ return Notification.permission;
20
+ }, []);
21
+
22
+ const primeAudioContext = useCallback(() => {
23
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
24
+ if (!AudioContext) {
25
+ return;
26
+ }
27
+ let ctx = audioContextRef.current;
28
+ if (!ctx) {
29
+ ctx = new AudioContext();
30
+ audioContextRef.current = ctx;
31
+ }
32
+ if (ctx.state === "suspended") {
33
+ ctx.resume().catch(() => {});
34
+ }
35
+ }, []);
36
+
37
+ const playNotificationSound = useCallback(() => {
38
+ if (!soundEnabled) {
39
+ return;
40
+ }
41
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
42
+ if (!AudioContext) {
43
+ return;
44
+ }
45
+ let ctx = audioContextRef.current;
46
+ if (!ctx) {
47
+ ctx = new AudioContext();
48
+ audioContextRef.current = ctx;
49
+ }
50
+ if (ctx.state === "suspended") {
51
+ ctx.resume().catch(() => {});
52
+ }
53
+ const oscillator = ctx.createOscillator();
54
+ const gain = ctx.createGain();
55
+ oscillator.type = "sine";
56
+ oscillator.frequency.value = 740;
57
+ gain.gain.value = 0.0001;
58
+ gain.gain.exponentialRampToValueAtTime(0.12, ctx.currentTime + 0.01);
59
+ gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.25);
60
+ oscillator.connect(gain).connect(ctx.destination);
61
+ oscillator.start();
62
+ oscillator.stop(ctx.currentTime + 0.26);
63
+ }, [soundEnabled]);
64
+
65
+ const stripMarkdownForNotification = useCallback((value) => {
66
+ if (!value) {
67
+ return "";
68
+ }
69
+ let output = String(value);
70
+ const vibe80Marker = output.match(/<!--\s*vibe80:/i);
71
+ if (typeof vibe80Marker?.index === "number") {
72
+ output = output.slice(0, vibe80Marker.index);
73
+ }
74
+ output = output.replace(/```([\s\S]*?)```/g, "$1");
75
+ output = output.replace(/`([^`]+)`/g, "$1");
76
+ output = output.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1");
77
+ output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1");
78
+ output = output.replace(/^\s{0,3}#{1,6}\s+/gm, "");
79
+ output = output.replace(/^\s{0,3}>\s?/gm, "");
80
+ output = output.replace(/^\s{0,3}[-*+]\s+/gm, "");
81
+ output = output.replace(/^\s{0,3}\d+\.\s+/gm, "");
82
+ output = output.replace(/[*_~]{1,3}/g, "");
83
+ output = output.replace(/\s+/g, " ").trim();
84
+ return output;
85
+ }, []);
86
+
87
+ const maybeNotify = useCallback(
88
+ (message) => {
89
+ if (!notificationsEnabled) {
90
+ return;
91
+ }
92
+ if (!("Notification" in window)) {
93
+ return;
94
+ }
95
+ if (Notification.permission !== "granted") {
96
+ return;
97
+ }
98
+ if (!message?.id || lastNotifiedIdRef.current === message.id) {
99
+ return;
100
+ }
101
+ if (!document.hidden) {
102
+ return;
103
+ }
104
+ lastNotifiedIdRef.current = message.id;
105
+ const body = stripMarkdownForNotification(message.text || "").slice(
106
+ 0,
107
+ 180
108
+ );
109
+ try {
110
+ new Notification(t("New message"), { body });
111
+ } catch (error) {
112
+ // Ignore notification failures (permissions or browser quirks).
113
+ }
114
+ playNotificationSound();
115
+ },
116
+ [notificationsEnabled, playNotificationSound, stripMarkdownForNotification, t]
117
+ );
118
+
119
+ useEffect(() => {
120
+ if (!notificationsEnabled) {
121
+ return;
122
+ }
123
+ void ensureNotificationPermission();
124
+ primeAudioContext();
125
+ }, [ensureNotificationPermission, primeAudioContext, notificationsEnabled]);
126
+
127
+ return {
128
+ ensureNotificationPermission,
129
+ maybeNotify,
130
+ lastNotifiedIdRef,
131
+ };
132
+ }
@@ -0,0 +1,67 @@
1
+ import { useCallback, useEffect } from "react";
2
+
3
+ export default function usePaneNavigation({
4
+ activePane,
5
+ activeWorktreeId,
6
+ debugMode,
7
+ rpcLogsEnabled,
8
+ terminalEnabled,
9
+ setPaneByTab,
10
+ setToolbarExportOpen,
11
+ lastPaneByTabRef,
12
+ }) {
13
+ const handleViewSelect = useCallback(
14
+ (nextPane) => {
15
+ if ((!debugMode || !rpcLogsEnabled) && nextPane === "logs") {
16
+ return;
17
+ }
18
+ if (!terminalEnabled && nextPane === "terminal") {
19
+ return;
20
+ }
21
+ const key = activeWorktreeId || "main";
22
+ setPaneByTab((current) => ({
23
+ ...current,
24
+ [key]: nextPane,
25
+ }));
26
+ setToolbarExportOpen(false);
27
+ },
28
+ [
29
+ activeWorktreeId,
30
+ debugMode,
31
+ rpcLogsEnabled,
32
+ setPaneByTab,
33
+ setToolbarExportOpen,
34
+ terminalEnabled,
35
+ ]
36
+ );
37
+
38
+ const handleOpenSettings = useCallback(() => {
39
+ if (activePane !== "settings") {
40
+ const key = activeWorktreeId || "main";
41
+ lastPaneByTabRef.current.set(key, activePane);
42
+ }
43
+ handleViewSelect("settings");
44
+ }, [activePane, activeWorktreeId, handleViewSelect, lastPaneByTabRef]);
45
+
46
+ const handleSettingsBack = useCallback(() => {
47
+ const key = activeWorktreeId || "main";
48
+ const previousPane = lastPaneByTabRef.current.get(key);
49
+ const fallbackPane =
50
+ previousPane && previousPane !== "settings" ? previousPane : "chat";
51
+ handleViewSelect(fallbackPane);
52
+ }, [activeWorktreeId, handleViewSelect, lastPaneByTabRef]);
53
+
54
+ useEffect(() => {
55
+ if ((!debugMode || !rpcLogsEnabled) && activePane === "logs") {
56
+ handleViewSelect("chat");
57
+ }
58
+ }, [activePane, debugMode, handleViewSelect, rpcLogsEnabled]);
59
+
60
+ useEffect(() => {
61
+ if (!terminalEnabled && activePane === "terminal") {
62
+ handleViewSelect("chat");
63
+ }
64
+ }, [activePane, handleViewSelect, terminalEnabled]);
65
+
66
+ return { handleViewSelect, handleOpenSettings, handleSettingsBack };
67
+ }
@@ -0,0 +1,13 @@
1
+ import { useState } from "react";
2
+
3
+ export default function usePanelState() {
4
+ const [commandPanelOpen, setCommandPanelOpen] = useState({});
5
+ const [toolResultPanelOpen, setToolResultPanelOpen] = useState({});
6
+
7
+ return {
8
+ commandPanelOpen,
9
+ setCommandPanelOpen,
10
+ toolResultPanelOpen,
11
+ setToolResultPanelOpen,
12
+ };
13
+ }
@@ -0,0 +1,70 @@
1
+ import { useCallback } from "react";
2
+
3
+ export default function useProviderSelection({
4
+ attachmentSessionId,
5
+ socketRef,
6
+ availableProviders,
7
+ llmProvider,
8
+ providerSwitching,
9
+ processing,
10
+ setProviderSwitching,
11
+ setStatus,
12
+ setSelectedProviders,
13
+ setLlmProvider,
14
+ t,
15
+ }) {
16
+ const handleProviderSwitch = useCallback(
17
+ (newProvider) => {
18
+ if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) {
19
+ return;
20
+ }
21
+ if (!availableProviders.includes(newProvider)) {
22
+ return;
23
+ }
24
+ if (newProvider === llmProvider || providerSwitching || processing) {
25
+ return;
26
+ }
27
+ setProviderSwitching(true);
28
+ setStatus(t("Switching to {{provider}}...", { provider: newProvider }));
29
+ socketRef.current.send(
30
+ JSON.stringify({ type: "switch_provider", provider: newProvider })
31
+ );
32
+ },
33
+ [
34
+ availableProviders,
35
+ llmProvider,
36
+ processing,
37
+ providerSwitching,
38
+ setProviderSwitching,
39
+ setStatus,
40
+ socketRef,
41
+ t,
42
+ ]
43
+ );
44
+
45
+ const toggleProviderSelection = useCallback(
46
+ (provider) => {
47
+ if (attachmentSessionId) {
48
+ return;
49
+ }
50
+ setSelectedProviders((current) => {
51
+ const exists = current.includes(provider);
52
+ const next = exists
53
+ ? current.filter((item) => item !== provider)
54
+ : [...current, provider];
55
+ if (!exists) {
56
+ setLlmProvider(provider);
57
+ } else if (provider === llmProvider) {
58
+ const fallback = next[0] || provider;
59
+ if (fallback !== llmProvider) {
60
+ setLlmProvider(fallback);
61
+ }
62
+ }
63
+ return next;
64
+ });
65
+ },
66
+ [attachmentSessionId, llmProvider, setLlmProvider, setSelectedProviders]
67
+ );
68
+
69
+ return { handleProviderSwitch, toggleProviderSelection };
70
+ }