-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: add branching #2763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add branching #2763
Changes from 190 commits
0751df4
161fc61
f8216d2
efc8855
d51ebdb
f5b677b
f4bc424
2bd9daf
b4d6f19
61a01e8
769610c
d394578
4727ebe
5eb19fc
8386d2d
b535762
7b353ff
3164ef6
9e7f4ee
2762f96
1cee486
02a7e13
4d54c8d
31fa8e2
232a149
b5994c9
d081fe6
6f9b760
2afd496
1a9c002
6da4516
8d6ef0b
f496fd3
7f288c2
7016ac1
4c6b93a
d052d11
869ccbe
174710c
cde9c70
2e55546
7ed4fab
d2de223
f097e2e
903edcd
bea665c
021803d
c870c34
4d9345b
4b3341c
6dc41f6
e07e71a
91e3b60
3b1b098
60e0bfb
c6ea50c
4cbb1d9
30639fb
61bdb5e
4513408
272481b
944a626
bb29a58
6d6e8ff
9f40c90
508de48
87997de
9c3f9a8
06423d7
7b57281
ed734c7
79ea8fe
5d2fecf
51aec4b
f1fa4dc
e48acf0
47ce11d
9840dcd
2475984
17dccd7
0510755
9af020b
333e6b4
ff5727c
f89a976
f686f5a
497ca80
f65f62d
7fcb024
a81b5bd
3c34ed4
fe37b4e
78f66e9
827ed20
fe0924f
226c07a
33446d9
6fb399e
74a0d7d
f337bfe
686e87c
c80bcab
fba64ff
c950022
75d904b
13a51c7
136ba15
80ff36f
0c51b3f
4784876
c29e0a8
5125fb3
f453d44
8152a03
6717f58
53aa1be
9687231
4ac6f57
f30abfe
61a60b6
e7088da
310a38e
be0f4e4
315aa3e
7e9c2f5
5e86dcc
afcb94a
ded1ee2
4eff157
3e34066
7e0c33c
0dca2a6
934a1b9
d30219f
374b85e
bac4eb0
9b6a602
53407b4
7f61893
3bb146c
6966a9a
dbf7140
38464dc
43a2ae0
67f4082
2b45cf0
94f2dfd
c6d5fc1
e4e4bf9
dcd7c8b
891c262
ef715b2
b9734f5
9672ff9
fa20e7d
571f14c
fd445ef
910cde2
ee177b8
84d21fd
02cfae5
53a8b66
efcc3e5
6073444
b2b3a69
7430c8d
4fe0c96
f1576ac
2a12ab6
d2c47af
970347f
2d24f19
e5666d0
70c143d
ac9bc3f
7eb5af7
cb43ed0
fa94c0f
a81eccb
a80ed34
ee90da8
480844f
1079873
3144720
0daf7c2
642f489
0c95c25
6d973f5
dffce62
75fb657
b4b337c
a15fbb5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,7 @@ | |
| "emptyState": "編集するウィンドウを選択してください" | ||
| }, | ||
| "brand": "ブランド", | ||
| "branches": "ブランチ", | ||
| "apps": "アプリ" | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -284,6 +284,7 @@ | |
| "emptyState": "설정을 편집할 창(윈도우)을 선택하세요" | ||
| }, | ||
| "brand": "브랜드", | ||
| "branches": "브랜치", | ||
| "apps": "앱" | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -284,6 +284,7 @@ | |
| "emptyState": "选择一个窗口以编辑其设置" | ||
| }, | ||
| "brand": "品牌", | ||
| "branches": "分支", | ||
| "apps": "应用" | ||
| } | ||
| } | ||
|
|
||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| import React, { useState, useRef, useEffect } from 'react'; | ||
| import { Icons } from '@onlook/ui/icons'; | ||
| import { Menu, Laptop } from 'lucide-react'; | ||
| import { Laptop, Menu } from 'lucide-react'; | ||
| import React, { useEffect, useRef, useState } from 'react'; | ||
|
|
||
| export function ResponsiveWebsiteBlock() { | ||
| const [websiteWidth, setWebsiteWidth] = useState(400); // Initial width in pixels | ||
|
|
@@ -52,7 +51,7 @@ export function ResponsiveWebsiteBlock() { | |
| useEffect(() => { | ||
| document.addEventListener('mousemove', handleMouseMove); | ||
| document.addEventListener('mouseup', handleMouseUp); | ||
|
|
||
| return () => { | ||
| document.removeEventListener('mousemove', handleMouseMove); | ||
| document.removeEventListener('mouseup', handleMouseUp); | ||
|
|
@@ -63,7 +62,7 @@ export function ResponsiveWebsiteBlock() { | |
| <div className="flex flex-col gap-4"> | ||
| <div className="w-full h-100 bg-[#2E2C2D] rounded-lg mb-6 relative overflow-hidden" ref={containerRef}> | ||
| {/* Mini Website Container */} | ||
| <div | ||
| <div | ||
| className="h-80 bg-[#E5E3DE] rounded-lg border border-[#D1CFC9] shadow-lg absolute left-1/2 top-12 transform -translate-x-1/2" | ||
| style={{ width: `${websiteWidth}px` }} | ||
| > | ||
|
|
@@ -92,7 +91,7 @@ export function ResponsiveWebsiteBlock() { | |
| <h1 className="text-xl font-serif mb-3">Le Fidgette</h1> | ||
| <p className="text-xs opacity-90 mb-6 text-balance">Creating natural shapes inspired by the natural world.</p> | ||
| </div> | ||
|
|
||
| {/* "View Work" Button */} | ||
| <div className="w-24 bg-[#8E837D] p-2 text-center cursor-pointer hover:bg-opacity-90 transition-opacity mb-12"> | ||
| <p className="text-[10px] text-white font-medium tracking-wider">VIEW WORK</p> | ||
|
|
@@ -117,21 +116,21 @@ export function ResponsiveWebsiteBlock() { | |
| </div> | ||
| </div> | ||
| {/* Responsive Handles */} | ||
| <div | ||
| <div | ||
| className="absolute left-[-16px] top-1/2 transform -translate-y-1/2 p-4 py-20 -m-4 cursor-ew-resize group" | ||
| onMouseDown={(e) => handleMouseDown(e, 'left')} | ||
| > | ||
| <div className="w-1.5 h-20 bg-gray-400 group-hover:bg-gray-500 rounded-full transition-colors duration-200 shadow-lg"></div> | ||
| </div> | ||
| <div | ||
| <div | ||
| className="absolute right-[-16px] top-1/2 transform -translate-y-1/2 p-4 py-20 -m-4 cursor-ew-resize group" | ||
| onMouseDown={(e) => handleMouseDown(e, 'right')} | ||
| > | ||
| <div className="w-1.5 h-20 bg-gray-400 group-hover:bg-gray-500 rounded-full transition-colors duration-200 shadow-lg"></div> | ||
| </div> | ||
|
Comment on lines
+119
to
130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Make resize handles accessible (keyboard) and self-describing. Add slider semantics with keyboard support (Left/Right arrows). Apply this diff: - <div
+ <div
className="absolute left-[-16px] top-1/2 transform -translate-y-1/2 p-4 py-20 -m-4 cursor-ew-resize group"
- onMouseDown={(e) => handleMouseDown(e, 'left')}
+ onMouseDown={(e) => handleMouseDown(e, 'left')}
+ role="slider"
+ tabIndex={0}
+ aria-label="Resize preview (left handle)"
+ aria-valuemin={200}
+ aria-valuemax={600}
+ aria-valuenow={websiteWidth}
+ onKeyDown={(e) => handleKeyDown(e, 'left')}
>- <div
+ <div
className="absolute right-[-16px] top-1/2 transform -translate-y-1/2 p-4 py-20 -m-4 cursor-ew-resize group"
- onMouseDown={(e) => handleMouseDown(e, 'right')}
+ onMouseDown={(e) => handleMouseDown(e, 'right')}
+ role="slider"
+ tabIndex={0}
+ aria-label="Resize preview (right handle)"
+ aria-valuemin={200}
+ aria-valuemax={600}
+ aria-valuenow={websiteWidth}
+ onKeyDown={(e) => handleKeyDown(e, 'right')}
>Add outside-range support code: const handleKeyDown = (e: React.KeyboardEvent, handle: 'left' | 'right') => {
const step = 10;
if (e.key === 'ArrowLeft') setWebsiteWidth(w => clamp(w - step, 200, 600));
if (e.key === 'ArrowRight') setWebsiteWidth(w => clamp(w + step, 200, 600));
};🤖 Prompt for AI Agents |
||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex flex-row items-start gap-8 w-full"> | ||
| {/* Icon + Title */} | ||
| <div className="flex flex-col items-start w-1/2"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,20 +10,44 @@ import { Terminal } from './terminal'; | |||||||||||||||
|
|
||||||||||||||||
| export const TerminalArea = observer(({ children }: { children: React.ReactNode }) => { | ||||||||||||||||
| const editorEngine = useEditorEngine(); | ||||||||||||||||
| const terminalSessions = editorEngine.sandbox.session.terminalSessions; | ||||||||||||||||
| const activeSessionId = editorEngine.sandbox.session.activeTerminalSessionId; | ||||||||||||||||
| const branches = editorEngine.branches; | ||||||||||||||||
|
|
||||||||||||||||
| const [terminalHidden, setTerminalHidden] = useState(true); | ||||||||||||||||
| // Collect terminal sessions from all branches | ||||||||||||||||
| const allTerminalSessions = new Map<string, { name: string; branchName: string; branchId: string; sessionId: string; session: any }>(); | ||||||||||||||||
| let activeSessionId: string | null = null; | ||||||||||||||||
|
|
||||||||||||||||
| for (const branch of branches.allBranches) { | ||||||||||||||||
| try { | ||||||||||||||||
| const branchData = branches.getBranchById(branch.id); | ||||||||||||||||
| if (!branchData) continue; | ||||||||||||||||
|
|
||||||||||||||||
| // Get the sandbox manager for this branch | ||||||||||||||||
| const sandbox = branches.getSandboxById(branch.id); | ||||||||||||||||
| if (!sandbox?.session?.terminalSessions) continue; | ||||||||||||||||
|
|
||||||||||||||||
| for (const [sessionId, session] of sandbox.session.terminalSessions) { | ||||||||||||||||
| const key = `${branch.id}-${sessionId}`; | ||||||||||||||||
| allTerminalSessions.set(key, { | ||||||||||||||||
| name: session.name, | ||||||||||||||||
| branchName: branch.name, | ||||||||||||||||
| branchId: branch.id, | ||||||||||||||||
| sessionId: sessionId, | ||||||||||||||||
| session: session | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| if (!terminalSessions.size) { | ||||||||||||||||
| return ( | ||||||||||||||||
| <div className="flex items-center justify-center h-full p-1 gap-2"> | ||||||||||||||||
| <Icons.LoadingSpinner className="animate-spin" /> | ||||||||||||||||
| <p className="text-foreground-secondary">Initializing Sandbox...</p> | ||||||||||||||||
| </div> | ||||||||||||||||
| ) | ||||||||||||||||
| // Set active session if this is the currently active branch and session | ||||||||||||||||
| if (branch.id === branches.activeBranch.id && sessionId === sandbox.session.activeTerminalSessionId) { | ||||||||||||||||
| activeSessionId = key; | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+39
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against undefined activeBranch to prevent runtime crash
- if (branch.id === branches.activeBranch.id && sessionId === sandbox.session.activeTerminalSessionId) {
+ const activeBranchId = branches.activeBranch?.id;
+ if (activeBranchId && branch.id === activeBranchId && sessionId === sandbox.session.activeTerminalSessionId) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| } | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| // Skip branches that aren't properly initialized | ||||||||||||||||
| continue; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| const [terminalHidden, setTerminalHidden] = useState(true); | ||||||||||||||||
|
|
||||||||||||||||
| return ( | ||||||||||||||||
| <> | ||||||||||||||||
| {terminalHidden ? ( | ||||||||||||||||
|
|
@@ -77,21 +101,43 @@ export const TerminalArea = observer(({ children }: { children: React.ReactNode | |||||||||||||||
| terminalHidden ? 'h-0 w-0 invisible' : 'h-[22rem] w-[37rem]', | ||||||||||||||||
| )} | ||||||||||||||||
| > | ||||||||||||||||
| <Tabs defaultValue={'cli'} value={activeSessionId} onValueChange={(value) => editorEngine.sandbox.session.activeTerminalSessionId = value} | ||||||||||||||||
| className="w-full h-full"> | ||||||||||||||||
| <TabsList className="w-full h-8 rounded-none border-b border-border"> | ||||||||||||||||
| {Array.from(terminalSessions).map(([id, terminal]) => ( | ||||||||||||||||
| <TabsTrigger key={id} value={id} className="flex-1">{terminal.name}</TabsTrigger> | ||||||||||||||||
| ))} | ||||||||||||||||
| </TabsList> | ||||||||||||||||
| <div className="w-full h-full overflow-auto"> | ||||||||||||||||
| {Array.from(terminalSessions).map(([id]) => ( | ||||||||||||||||
| <TabsContent key={id} forceMount value={id} className="h-full" hidden={activeSessionId !== id}> | ||||||||||||||||
| <Terminal hidden={terminalHidden} terminalSessionId={id} /> | ||||||||||||||||
| </TabsContent> | ||||||||||||||||
| ))} | ||||||||||||||||
| {allTerminalSessions.size > 0 ? ( | ||||||||||||||||
| <Tabs defaultValue={'cli'} value={activeSessionId || ''} onValueChange={(value) => { | ||||||||||||||||
| // Extract branch and session from the combined key | ||||||||||||||||
| const terminalData = allTerminalSessions.get(value); | ||||||||||||||||
| if (terminalData) { | ||||||||||||||||
| // Switch to the branch first | ||||||||||||||||
| editorEngine.branches.switchToBranch(terminalData.branchId); | ||||||||||||||||
| // Then set the active terminal session for that branch | ||||||||||||||||
| const sandbox = branches.getSandboxById(terminalData.branchId); | ||||||||||||||||
| if (sandbox) { | ||||||||||||||||
| sandbox.session.activeTerminalSessionId = terminalData.sessionId; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| }} | ||||||||||||||||
| className="w-full h-full"> | ||||||||||||||||
| <TabsList className="w-full h-8 rounded-none border-b border-border overflow-x-auto justify-start"> | ||||||||||||||||
| {Array.from(allTerminalSessions).map(([key, terminalData]) => ( | ||||||||||||||||
| <TabsTrigger key={key} value={key} className="flex-1"> | ||||||||||||||||
| <span className="truncate"> | ||||||||||||||||
| {terminalData.name} • {terminalData.branchName} | ||||||||||||||||
| </span> | ||||||||||||||||
| </TabsTrigger> | ||||||||||||||||
| ))} | ||||||||||||||||
| </TabsList> | ||||||||||||||||
| <div className="w-full h-full overflow-auto"> | ||||||||||||||||
| {Array.from(allTerminalSessions).map(([key, terminalData]) => ( | ||||||||||||||||
| <TabsContent key={key} forceMount value={key} className="h-full" hidden={activeSessionId !== key}> | ||||||||||||||||
| <Terminal hidden={terminalHidden} terminalSessionId={terminalData.sessionId} branchId={terminalData.branchId} /> | ||||||||||||||||
| </TabsContent> | ||||||||||||||||
| ))} | ||||||||||||||||
| </div> | ||||||||||||||||
| </Tabs> | ||||||||||||||||
| ) : ( | ||||||||||||||||
| <div className="flex items-center justify-center h-full text-muted-foreground"> | ||||||||||||||||
| <span className="text-sm">No terminal sessions available</span> | ||||||||||||||||
| </div> | ||||||||||||||||
| </Tabs> | ||||||||||||||||
| )} | ||||||||||||||||
| </div > | ||||||||||||||||
| </> | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify nonstandard Tailwind utility classes exist in your config.
h-100,max-w-120, andcursor-ew-resizearen’t in Tailwind’s default scale. If not extended, use arbitrary values (e.g.,h-[25rem],max-w-[30rem]) orcursor-col-resize.Run:
If absent, replace:
h-100->h-[25rem](or nearest default likeh-96)max-w-120->max-w-[30rem]cursor-ew-resize->cursor-col-resizeAlso applies to: 100-100, 120-120, 126-126
🏁 Script executed:
Length of output: 22824
🏁 Script executed:
Length of output: 279
Replace undefined Tailwind classes with valid utilities
h-100,max-w-120, andcursor-ew-resizearen’t defined in your Tailwind config nor in the default scale. Update them to:h-[25rem](or nearest default likeh-96) instead ofh-100max-w-[30rem]instead ofmax-w-120cursor-col-resizeinstead ofcursor-ew-resizeApply the same replacements for any
h-100,max-w-100/120/126, andcursor-ew-resizeoccurrences across the codebase.🤖 Prompt for AI Agents