Skip to content

Commit fbdc45a

Browse files
authored
feat: improve workspace focus UX (#731)
1 parent e50e87a commit fbdc45a

File tree

25 files changed

+728
-536
lines changed

25 files changed

+728
-536
lines changed

apps/twig/src/main/menu.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import os from "node:os";
66
import path from "node:path";
77
import {
88
app,
9+
BrowserWindow,
910
clipboard,
1011
dialog,
1112
Menu,
@@ -206,8 +207,17 @@ function buildViewMenu(): MenuItemConstructorOptions {
206207
return {
207208
label: "View",
208209
submenu: [
209-
{ role: "reload" },
210-
{ role: "forceReload" },
210+
{
211+
label: "Reload",
212+
accelerator: "CmdOrCtrl+Shift+R",
213+
click: () => BrowserWindow.getFocusedWindow()?.webContents.reload(),
214+
},
215+
{
216+
label: "Force Reload",
217+
accelerator: "CmdOrCtrl+Shift+Alt+R",
218+
click: () =>
219+
BrowserWindow.getFocusedWindow()?.webContents.reloadIgnoringCache(),
220+
},
211221
{ role: "toggleDevTools" },
212222
{ type: "separator" },
213223
{ role: "resetZoom" },

apps/twig/src/renderer/components/GlobalEventHandlers.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { usePinnedTasksStore } from "@features/sidebar/stores/pinnedTasksStore";
55
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
66
import { useTaskViewedStore } from "@features/sidebar/stores/taskViewedStore";
77
import { useTasks } from "@features/tasks/hooks/useTasks";
8+
import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace";
89
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
910
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
1011
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
@@ -43,6 +44,12 @@ export function GlobalEventHandlers({
4344
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
4445
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
4546

47+
const currentTaskId = view.type === "task-detail" ? view.data?.id : undefined;
48+
const { workspace: currentWorkspace, handleToggleFocus } = useFocusWorkspace(
49+
currentTaskId ?? "",
50+
);
51+
const isWorktreeTask = currentWorkspace?.mode === "worktree";
52+
4653
const { data: allTasks = [] } = useTasks();
4754
const pinnedTaskIds = usePinnedTasksStore((state) => state.pinnedTaskIds);
4855
const sessions = useSessions();
@@ -165,6 +172,16 @@ export function GlobalEventHandlers({
165172
useHotkeys(SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, toggleRightSidebar, globalOptions);
166173
useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions);
167174

175+
useHotkeys(
176+
SHORTCUTS.TASK_REFRESH,
177+
handleToggleFocus,
178+
{
179+
...globalOptions,
180+
enabled: !!currentTaskId && isWorktreeTask,
181+
},
182+
[handleToggleFocus],
183+
);
184+
168185
// Task switching with mod+0-9
169186
useHotkeys(
170187
SHORTCUTS.SWITCH_TASK,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type React from "react";
2+
3+
interface KeyHintProps {
4+
children: React.ReactNode;
5+
style?: React.CSSProperties;
6+
}
7+
8+
export function KeyHint({ children, style }: KeyHintProps) {
9+
return (
10+
<kbd
11+
style={{
12+
display: "inline-flex",
13+
alignItems: "center",
14+
fontSize: "11px",
15+
fontFamily: "inherit",
16+
color: "var(--gray-11)",
17+
...style,
18+
}}
19+
>
20+
{children}
21+
</kbd>
22+
);
23+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2+
import type React from "react";
3+
import { KeyHint } from "./KeyHint";
4+
5+
interface TooltipProps {
6+
children: React.ReactNode;
7+
content: React.ReactNode;
8+
shortcut?: string;
9+
side?: "top" | "right" | "bottom" | "left";
10+
align?: "start" | "center" | "end";
11+
sideOffset?: number;
12+
delayDuration?: number;
13+
open?: boolean;
14+
defaultOpen?: boolean;
15+
onOpenChange?: (open: boolean) => void;
16+
}
17+
18+
export function Tooltip({
19+
children,
20+
content,
21+
shortcut,
22+
side = "top",
23+
align = "center",
24+
sideOffset = 6,
25+
delayDuration = 200,
26+
open,
27+
defaultOpen,
28+
onOpenChange,
29+
}: TooltipProps) {
30+
return (
31+
<TooltipPrimitive.Provider delayDuration={delayDuration}>
32+
<TooltipPrimitive.Root
33+
open={open}
34+
defaultOpen={defaultOpen}
35+
onOpenChange={onOpenChange}
36+
>
37+
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
38+
<TooltipPrimitive.Portal>
39+
<TooltipPrimitive.Content
40+
side={side}
41+
align={align}
42+
sideOffset={sideOffset}
43+
className="dark"
44+
style={{
45+
display: "flex",
46+
alignItems: "center",
47+
gap: "8px",
48+
backgroundColor: "var(--gray-2)",
49+
color: "var(--gray-12)",
50+
padding: "6px 10px",
51+
borderRadius: "6px",
52+
fontSize: "12px",
53+
lineHeight: "1.4",
54+
whiteSpace: "nowrap",
55+
border: "1px solid var(--gray-4)",
56+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.25)",
57+
zIndex: 9999,
58+
animationDuration: "150ms",
59+
animationTimingFunction: "ease-out",
60+
willChange: "transform, opacity",
61+
}}
62+
>
63+
<span>{content}</span>
64+
{shortcut && (
65+
<KeyHint style={{ fontSize: "12px" }}>{shortcut}</KeyHint>
66+
)}
67+
</TooltipPrimitive.Content>
68+
</TooltipPrimitive.Portal>
69+
</TooltipPrimitive.Root>
70+
</TooltipPrimitive.Provider>
71+
);
72+
}
73+
74+
export { TooltipPrimitive };

apps/twig/src/renderer/constants/keyboard-shortcuts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
4343
{
4444
id: "task-refresh",
4545
keys: SHORTCUTS.TASK_REFRESH,
46-
description: "Refresh app",
46+
description: "Toggle focus mode",
4747
category: "general",
48+
context: "Worktree task",
4849
},
4950
{
5051
id: "settings",

apps/twig/src/renderer/features/panels/components/DraggableTab.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface DraggableTabProps {
1515
tabData: TabData;
1616
isActive: boolean;
1717
index: number;
18+
draggable?: boolean;
1819
closeable?: boolean;
1920
isPreview?: boolean;
2021
onSelect: () => void;
@@ -34,6 +35,7 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
3435
tabData,
3536
isActive,
3637
index,
38+
draggable = true,
3739
closeable = true,
3840
isPreview,
3941
onSelect,
@@ -49,6 +51,7 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
4951
id: tabId,
5052
index,
5153
group: panelId,
54+
disabled: !draggable,
5255
transition: {
5356
duration: 200,
5457
easing: "ease",
@@ -131,7 +134,7 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
131134
gap="1"
132135
pl="3"
133136
pr={onClose ? "1" : "3"}
134-
className="group relative flex-shrink-0 cursor-grab select-none border-r border-b-2 transition-colors"
137+
className={`group relative flex-shrink-0 select-none border-r border-b-2 transition-colors ${draggable ? "cursor-grab" : "cursor-pointer"}`}
135138
style={{
136139
borderRightColor: "var(--gray-6)",
137140
borderBottomColor: isActive ? "var(--accent-10)" : "transparent",

apps/twig/src/renderer/features/panels/components/PanelLayout.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { DragDropProvider } from "@dnd-kit/react";
22
import type { Task } from "@shared/types";
3-
import { useSettingsStore } from "@stores/settingsStore";
43
import type React from "react";
54
import { useCallback, useEffect } from "react";
65
import { useDragDropHandlers } from "../hooks/useDragDropHandlers";
@@ -170,25 +169,14 @@ export const PanelLayout: React.FC<PanelLayoutProps> = ({ taskId, task }) => {
170169
const layout = usePanelLayoutStore((state) => state.getLayout(taskId));
171170
const initializeTask = usePanelLayoutStore((state) => state.initializeTask);
172171
const dragDropHandlers = useDragDropHandlers(taskId);
173-
const terminalLayoutMode = useSettingsStore(
174-
(state) => state.terminalLayoutMode,
175-
);
176-
const loadTerminalLayout = useSettingsStore(
177-
(state) => state.loadTerminalLayout,
178-
);
179-
const isLoading = useSettingsStore((state) => state.isLoading);
180172

181173
usePanelKeyboardShortcuts(taskId);
182174

183175
useEffect(() => {
184-
loadTerminalLayout();
185-
}, [loadTerminalLayout]);
186-
187-
useEffect(() => {
188-
if (!layout && !isLoading) {
189-
initializeTask(taskId, terminalLayoutMode);
176+
if (!layout) {
177+
initializeTask(taskId);
190178
}
191-
}, [taskId, layout, initializeTask, terminalLayoutMode, isLoading]);
179+
}, [taskId, layout, initializeTask]);
192180

193181
if (!layout) {
194182
return null;

apps/twig/src/renderer/features/panels/components/PanelTab.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { TabData } from "@features/panels/store/panelTypes";
22
import type React from "react";
33
import { DraggableTab } from "./DraggableTab";
4-
import { StaticTab } from "./StaticTab";
54

65
interface PanelTabProps {
76
tabId: string;
@@ -42,19 +41,6 @@ export const PanelTab: React.FC<PanelTabProps> = ({
4241
badge,
4342
hasUnsavedChanges,
4443
}) => {
45-
if (!draggable) {
46-
return (
47-
<StaticTab
48-
label={label}
49-
isActive={isActive}
50-
onSelect={onSelect}
51-
icon={icon}
52-
badge={badge}
53-
hasUnsavedChanges={hasUnsavedChanges}
54-
/>
55-
);
56-
}
57-
5844
return (
5945
<DraggableTab
6046
tabId={tabId}
@@ -63,6 +49,7 @@ export const PanelTab: React.FC<PanelTabProps> = ({
6349
tabData={tabData}
6450
isActive={isActive}
6551
index={index}
52+
draggable={draggable}
6653
closeable={closeable}
6754
isPreview={isPreview}
6855
onSelect={onSelect}

apps/twig/src/renderer/features/panels/components/StaticTab.tsx

Lines changed: 0 additions & 52 deletions
This file was deleted.

apps/twig/src/renderer/features/panels/components/TabbedPanel.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ interface TabbedPanelProps {
5555
draggingTabPanelId?: string | null;
5656
onAddTerminal?: () => void;
5757
onSplitPanel?: (direction: SplitDirection) => void;
58+
rightContent?: React.ReactNode;
5859
}
5960

6061
export const TabbedPanel: React.FC<TabbedPanelProps> = ({
@@ -69,6 +70,7 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
6970
draggingTabPanelId = null,
7071
onAddTerminal,
7172
onSplitPanel,
73+
rightContent,
7274
}) => {
7375
const activeTab = content.tabs.find((tab) => tab.id === content.activeTabId);
7476

@@ -191,26 +193,30 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
191193
/>
192194
)}
193195
</Flex>
194-
{content.droppable && (onSplitPanel || onAddTerminal) && (
196+
{(rightContent ||
197+
(content.droppable && (onSplitPanel || onAddTerminal))) && (
195198
<Flex
199+
align="center"
196200
style={{
197201
position: "absolute",
198202
right: 0,
199203
top: 0,
200204
height: "32px",
201205
borderLeft: "1px solid var(--gray-6)",
206+
borderBottom: "1px solid var(--gray-6)",
202207
background: "var(--color-background)",
203208
}}
204209
>
205-
{onSplitPanel && (
210+
{rightContent}
211+
{content.droppable && onSplitPanel && (
206212
<TabBarButton
207213
ariaLabel="Split panel"
208214
onClick={handleSplitClick}
209215
>
210216
<SquareSplitHorizontalIcon width={12} height={12} />
211217
</TabBarButton>
212218
)}
213-
{onAddTerminal && (
219+
{content.droppable && onAddTerminal && (
214220
<TabBarButton ariaLabel="Add terminal" onClick={onAddTerminal}>
215221
<Terminal size={14} />
216222
</TabBarButton>

0 commit comments

Comments
 (0)