Skip to content

Commit 67fa3cd

Browse files
authored
Merge pull request #31 from yugo-ibuki/fix/git-diff-layout
Update website documentation and improve diff overlay UI
2 parents 93a7283 + f6f0e2c commit 67fa3cd

19 files changed

Lines changed: 479 additions & 76 deletions

src/main/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
gitPush,
1515
listTmuxSessions,
1616
createSession,
17+
createNewTmuxSession,
1718
killPane,
1819
findShellPane,
1920
ensureShellPane,
@@ -262,6 +263,10 @@ app.whenReady().then(() => {
262263
return createSession(sessionName, command, cwd)
263264
})
264265

266+
ipcMain.handle('tmux:create-new-session', async (_event, { sessionName, command, cwd }) => {
267+
return createNewTmuxSession(sessionName, command, cwd)
268+
})
269+
265270
ipcMain.handle('tmux:kill-pane', async (_event, target: string) => {
266271
return killPane(target)
267272
})

src/main/tmux.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,22 @@ export async function createSession(
808808
}
809809
}
810810

811+
export async function createNewTmuxSession(
812+
sessionName: string,
813+
command: 'claude' | 'codex',
814+
cwd?: string
815+
): Promise<{ success: boolean; error?: string }> {
816+
try {
817+
const args = ['new-session', '-d', '-s', sessionName]
818+
if (cwd) args.push('-c', cwd)
819+
args.push(command)
820+
await run(args)
821+
return { success: true }
822+
} catch (e) {
823+
return { success: false, error: String(e) }
824+
}
825+
}
826+
811827
export async function killPane(target: string): Promise<{ success: boolean; error?: string }> {
812828
if (!TARGET_PATTERN.test(target)) {
813829
return { success: false, error: 'Invalid target format' }

src/preload/index.d.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,13 @@ interface TmuxAPI {
4747
listSkills: (cwd: string) => Promise<{ user: SkillEntry[]; project: SkillEntry[] }>
4848
listTmuxSessions: () => Promise<string[]>
4949
createSession: (sessionName: string, command: 'claude' | 'codex', cwd?: string) => Promise<SendResult>
50+
createNewSession: (sessionName: string, command: 'claude' | 'codex', cwd?: string) => Promise<SendResult>
5051
killPane: (target: string) => Promise<SendResult>
5152
findShellPane: (session: string) => Promise<string | null>
5253
ensureShellPane: (session: string, cwd: string) => Promise<{ success: boolean; target?: string; error?: string }>
5354
sendInput: (target: string, text: string, vimMode?: boolean, images?: string[]) => Promise<SendResult>
5455
capturePane: (target: string) => Promise<string>
5556
getPaneDetail: (target: string) => Promise<PaneDetail | null>
56-
listTmuxSessions: () => Promise<string[]>
57-
createSession: (sessionName: string, command: 'claude' | 'codex', cwd?: string) => Promise<SendResult>
58-
killPane: (target: string) => Promise<SendResult>
5957
gitAdd: (cwd: string) => Promise<SendResult>
6058
gitAddFiles: (cwd: string, files: string[]) => Promise<SendResult>
6159
gitCommit: (cwd: string, message: string) => Promise<SendResult>

src/preload/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const api = {
4646
listTmuxSessions: (): Promise<string[]> => ipcRenderer.invoke('tmux:list-tmux-sessions'),
4747
createSession: (sessionName: string, command: 'claude' | 'codex', cwd?: string): Promise<SendResult> =>
4848
ipcRenderer.invoke('tmux:create-session', { sessionName, command, cwd }),
49+
createNewSession: (sessionName: string, command: 'claude' | 'codex', cwd?: string): Promise<SendResult> =>
50+
ipcRenderer.invoke('tmux:create-new-session', { sessionName, command, cwd }),
4951
killPane: (target: string): Promise<SendResult> => ipcRenderer.invoke('tmux:kill-pane', target),
5052
findShellPane: (session: string): Promise<string | null> =>
5153
ipcRenderer.invoke('tmux:find-shell-pane', session),

src/renderer/src/App.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,6 +1727,42 @@
17271727
padding: 6px 20px;
17281728
}
17291729

1730+
.create-mode-tabs {
1731+
display: flex;
1732+
gap: 0;
1733+
border: 1px solid var(--border);
1734+
border-radius: var(--radius);
1735+
overflow: hidden;
1736+
}
1737+
1738+
.create-mode-tab {
1739+
flex: 1;
1740+
padding: 5px 12px;
1741+
font-size: 9px;
1742+
font-weight: 500;
1743+
font-family: var(--font-mono);
1744+
background: transparent;
1745+
color: var(--text-dim);
1746+
border: none;
1747+
cursor: pointer;
1748+
transition: all var(--transition-fast);
1749+
}
1750+
1751+
.create-mode-tab:hover {
1752+
background: var(--accent-muted);
1753+
color: var(--text);
1754+
}
1755+
1756+
.create-mode-tab-active {
1757+
background: var(--accent);
1758+
color: #fff;
1759+
}
1760+
1761+
.create-mode-tab-active:hover {
1762+
background: var(--accent-hover);
1763+
color: #fff;
1764+
}
1765+
17301766
/* Detail panel kill button */
17311767
.detail-actions {
17321768
display: flex;
@@ -1947,6 +1983,12 @@
19471983
background: var(--bg-hover);
19481984
}
19491985

1986+
.diff-file-focused > .diff-file-header {
1987+
background: var(--bg-hover);
1988+
outline: 1px solid var(--accent);
1989+
outline-offset: -1px;
1990+
}
1991+
19501992
.diff-file-chevron {
19511993
font-size: 9px;
19521994
color: var(--text-dim);

src/renderer/src/components/CreateDialog.tsx

Lines changed: 115 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { useEffect, useRef, useState } from 'react'
22
import { useUiStore } from '../stores/uiStore'
33
import { usePaneStore } from '../stores/paneStore'
44

5+
type Mode = 'new' | 'existing'
6+
57
export function CreateDialog(): React.JSX.Element | null {
68
const createDialog = useUiStore((s) => s.createDialog)
79
const setCreateDialog = useUiStore((s) => s.setCreateDialog)
@@ -13,8 +15,11 @@ export function CreateDialog(): React.JSX.Element | null {
1315
const paneDetail = useUiStore((s) => s.paneDetail)
1416
const setPanes = usePaneStore((s) => s.setPanes)
1517

18+
const [mode, setMode] = useState<Mode>('new')
19+
const [sessionName, setSessionName] = useState('')
1620
const [highlightIndex, setHighlightIndex] = useState(0)
1721
const listRef = useRef<HTMLUListElement>(null)
22+
const inputRef = useRef<HTMLInputElement>(null)
1823

1924
// Sync highlightIndex when dialog opens or sessions change
2025
useEffect(() => {
@@ -29,22 +34,47 @@ export function CreateDialog(): React.JSX.Element | null {
2934
item?.scrollIntoView({ block: 'nearest' })
3035
}, [highlightIndex])
3136

37+
// Focus input when mode switches to 'new'
38+
useEffect(() => {
39+
if (mode === 'new') {
40+
requestAnimationFrame(() => inputRef.current?.focus())
41+
}
42+
}, [mode])
43+
3244
const closeDialog = (): void => {
3345
setCreateDialog(false)
46+
setSessionName('')
47+
setMode('new')
48+
requestAnimationFrame(() => {
49+
document.querySelector<HTMLTextAreaElement>('.textarea')?.focus()
50+
})
51+
}
52+
53+
const handleCreateNew = async (): Promise<void> => {
54+
const name = sessionName.trim()
55+
if (!name) return
56+
const r = await window.api.createNewSession(name, newSessionCommand, paneDetail?.cwd)
57+
if (r.success) {
58+
setCreateDialog(false)
59+
setSessionName('')
60+
useUiStore.getState().flashStatus(`Created session "${name}" with ${newSessionCommand}`, true)
61+
const result = await window.api.listSessions()
62+
setPanes(result)
63+
} else {
64+
useUiStore.getState().flashStatus(r.error ?? 'Failed', false)
65+
}
3466
requestAnimationFrame(() => {
3567
document.querySelector<HTMLTextAreaElement>('.textarea')?.focus()
3668
})
3769
}
3870

39-
const handleCreate = async (): Promise<void> => {
71+
const handleAddToExisting = async (): Promise<void> => {
4072
const target = tmuxSessions[highlightIndex] ?? newSessionTarget
4173
if (!target) return
4274
const r = await window.api.createSession(target, newSessionCommand, paneDetail?.cwd)
4375
if (r.success) {
4476
setCreateDialog(false)
45-
useUiStore
46-
.getState()
47-
.flashStatus(`Created ${newSessionCommand} in ${target}`, true)
77+
useUiStore.getState().flashStatus(`Added ${newSessionCommand} to ${target}`, true)
4878
const result = await window.api.listSessions()
4979
setPanes(result)
5080
} else {
@@ -58,24 +88,30 @@ export function CreateDialog(): React.JSX.Element | null {
5888
if (!createDialog) return null
5989

6090
const handleKeyDown = (e: React.KeyboardEvent): void => {
91+
if ((e.target as HTMLElement).tagName === 'INPUT') return
92+
6193
switch (e.key) {
6294
case 'j':
6395
case 'ArrowDown':
6496
e.preventDefault()
65-
setHighlightIndex((i) => {
66-
const next = Math.min(i + 1, tmuxSessions.length - 1)
67-
setNewSessionTarget(tmuxSessions[next])
68-
return next
69-
})
97+
if (mode === 'existing') {
98+
setHighlightIndex((i) => {
99+
const next = Math.min(i + 1, tmuxSessions.length - 1)
100+
setNewSessionTarget(tmuxSessions[next])
101+
return next
102+
})
103+
}
70104
break
71105
case 'k':
72106
case 'ArrowUp':
73107
e.preventDefault()
74-
setHighlightIndex((i) => {
75-
const next = Math.max(i - 1, 0)
76-
setNewSessionTarget(tmuxSessions[next])
77-
return next
78-
})
108+
if (mode === 'existing') {
109+
setHighlightIndex((i) => {
110+
const next = Math.max(i - 1, 0)
111+
setNewSessionTarget(tmuxSessions[next])
112+
return next
113+
})
114+
}
79115
break
80116
case 'h':
81117
e.preventDefault()
@@ -85,9 +121,14 @@ export function CreateDialog(): React.JSX.Element | null {
85121
e.preventDefault()
86122
setNewSessionCommand('codex')
87123
break
124+
case 'Tab':
125+
e.preventDefault()
126+
setMode((m) => (m === 'new' ? 'existing' : 'new'))
127+
break
88128
case 'Enter':
89129
e.preventDefault()
90-
handleCreate()
130+
if (mode === 'new') handleCreateNew()
131+
else handleAddToExisting()
91132
break
92133
case 'Escape':
93134
e.preventDefault()
@@ -112,29 +153,68 @@ export function CreateDialog(): React.JSX.Element | null {
112153
<div className="pane-popup detail-popup" onClick={(e) => e.stopPropagation()}>
113154
<div className="pane-popup-header">
114155
<span className="pane-popup-title">New Session</span>
115-
<span className="create-dialog-hint">j/k: select · h/l: cmd · Enter: create</span>
156+
<span className="create-dialog-hint">Tab: switch · h/l: cmd · Enter: create</span>
116157
<button className="pane-popup-close" onClick={closeDialog}>
117158
Esc
118159
</button>
119160
</div>
120161
<div className="create-session-form">
121-
<div className="setting-row">
122-
<span className="setting-label">Session</span>
123-
<ul className="create-session-list" ref={listRef}>
124-
{tmuxSessions.map((s, i) => (
125-
<li
126-
key={s}
127-
className={`create-session-item ${i === highlightIndex ? 'create-session-item-active' : ''}`}
128-
onClick={() => {
129-
setHighlightIndex(i)
130-
setNewSessionTarget(s)
131-
}}
132-
>
133-
{s}
134-
</li>
135-
))}
136-
</ul>
162+
{/* Mode tabs */}
163+
<div className="create-mode-tabs">
164+
<button
165+
className={`create-mode-tab ${mode === 'new' ? 'create-mode-tab-active' : ''}`}
166+
onClick={() => setMode('new')}
167+
>
168+
New Session
169+
</button>
170+
<button
171+
className={`create-mode-tab ${mode === 'existing' ? 'create-mode-tab-active' : ''}`}
172+
onClick={() => setMode('existing')}
173+
>
174+
Add to Existing
175+
</button>
137176
</div>
177+
178+
{mode === 'new' ? (
179+
<div className="setting-row">
180+
<span className="setting-label">Name</span>
181+
<input
182+
ref={inputRef}
183+
className="git-commit-input"
184+
placeholder="Session name..."
185+
value={sessionName}
186+
onChange={(e) => setSessionName(e.target.value)}
187+
onKeyDown={(e) => {
188+
e.stopPropagation()
189+
if (e.key === 'Enter' && sessionName.trim()) {
190+
handleCreateNew()
191+
}
192+
if (e.key === 'Escape') {
193+
closeDialog()
194+
}
195+
}}
196+
/>
197+
</div>
198+
) : (
199+
<div className="setting-row">
200+
<span className="setting-label">Session</span>
201+
<ul className="create-session-list" ref={listRef}>
202+
{tmuxSessions.map((s, i) => (
203+
<li
204+
key={s}
205+
className={`create-session-item ${i === highlightIndex ? 'create-session-item-active' : ''}`}
206+
onClick={() => {
207+
setHighlightIndex(i)
208+
setNewSessionTarget(s)
209+
}}
210+
>
211+
{s}
212+
</li>
213+
))}
214+
</ul>
215+
</div>
216+
)}
217+
138218
<div className="setting-row">
139219
<span className="setting-label">Command</span>
140220
<div className="theme-segment">
@@ -154,10 +234,10 @@ export function CreateDialog(): React.JSX.Element | null {
154234
</div>
155235
<button
156236
className="git-btn create-session-btn"
157-
disabled={!tmuxSessions[highlightIndex]}
158-
onClick={handleCreate}
237+
disabled={mode === 'new' ? !sessionName.trim() : !tmuxSessions[highlightIndex]}
238+
onClick={mode === 'new' ? handleCreateNew : handleAddToExisting}
159239
>
160-
Create
240+
{mode === 'new' ? 'Create' : 'Add'}
161241
</button>
162242
</div>
163243
</div>

0 commit comments

Comments
 (0)