Compare commits

...

3 Commits

Author SHA1 Message Date
Augustin
b85ebb8e54 feat(terminal): add Ctrl+Shift+C/V copy/paste shortcuts
All checks were successful
Beta Release / beta (push) Successful in 48s
xterm captures all keyboard input which prevents standard clipboard
operations. Add custom key handler to intercept Ctrl+Shift+C for
copy (selection) and Ctrl+Shift+V for paste, without interfering
with Ctrl+C (SIGINT) or browser devtools shortcut.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 21:10:24 +02:00
Augustin
7cc206dc20 fix(shell): prevent Enter in AI chat from leaking to terminal
All checks were successful
Beta Release / beta (push) Successful in 48s
Stop propagation of Enter keydown in AI input and defer terminal
focus to next event loop tick to prevent xterm from capturing the
same key event.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 21:07:36 +02:00
Augustin
bf8c0fd380 fix(terminal): improve terminal dimensions and fit timing
All checks were successful
Beta Release / beta (push) Successful in 47s
Use min-height:0 on xterm-wrapper (flex child) instead of height:100%
to properly fill available space in flex layout. Add delayed fit()
calls after initialization to let the layout stabilize before
calculating terminal cell dimensions.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 20:35:49 +02:00
5 changed files with 48 additions and 10 deletions

View File

@@ -92,6 +92,8 @@ export default function App() {
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
],
shell: [
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
],

View File

@@ -143,6 +143,32 @@ function createTerminal(container, settings = {}) {
const webLinksAddon = new WebLinksAddon()
term.loadAddon(fitAddon)
term.loadAddon(webLinksAddon)
term.attachCustomKeyEventHandler((e) => {
if (e.type !== 'keydown') return true
const ctrl = e.ctrlKey || e.metaKey
const shift = e.shiftKey
if (ctrl && shift && e.key === 'C') {
e.preventDefault()
e.stopPropagation()
const selection = term.getSelection()
if (selection) navigator.clipboard.writeText(selection)
return false
}
if (ctrl && shift && e.key === 'V') {
e.preventDefault()
e.stopPropagation()
navigator.clipboard.readText().then(text => {
if (text) term.paste(text)
}).catch(() => {})
return false
}
return true
})
term.open(container)
fitAddon.fit()
@@ -439,6 +465,12 @@ export default function Shell({ api }) {
const entry = tabsRef.current[tab.id]
if (entry) entry.fitAddon.fit()
}
setTimeout(() => {
for (const tab of tabsRef.current._tabList || []) {
const entry = tabsRef.current[tab.id]
if (entry) entry.fitAddon.fit()
}
}, 150)
})
}, [initTerminal])
@@ -472,11 +504,11 @@ export default function Shell({ api }) {
requestAnimationFrame(() => {
if (cancelled) return
const entry = tabsRef.current[tab.id]
if (entry) entry.fitAddon.fit()
if (entry) {
entry.fitAddon.fit()
setTimeout(() => { if (!cancelled) entry.fitAddon.fit() }, 100)
}
})
}
for (const tab of tabs) {
if (!tabsRef.current[tab.id]) {
tryInitTab(tab, 0)
}
@@ -687,7 +719,7 @@ export default function Shell({ api }) {
if (!fromEvent) {
setAiInput('')
focusAiTerminal()
setTimeout(() => focusAiTerminal(), 0)
}
if (trimmed === '/clear') {
@@ -931,7 +963,7 @@ export default function Shell({ api }) {
<input
value={aiInput}
onChange={e => setAiInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleAiSend() } }}
placeholder={aiAtLimit ? '/clear pour continuer' : t('shell.askAi')}
disabled={aiAtLimit && aiInput !== '/clear'}
/>

View File

@@ -16,6 +16,8 @@ const en = {
switchWindow: 'Switch window',
sendMessage: 'Send message',
newLine: 'New line',
copy: 'Copy',
paste: 'Paste',
runCommand: 'Run command',
commandHistory: 'Command history',
},

View File

@@ -16,6 +16,8 @@ const fr = {
switchWindow: 'Changer de fen\u00eatre',
sendMessage: 'Envoyer le message',
newLine: 'Nouvelle ligne',
copy: 'Copier',
paste: 'Coller',
runCommand: 'Ex\u00e9cuter',
commandHistory: 'Historique',
},

View File

@@ -276,8 +276,8 @@ input::placeholder { color: var(--text-disabled); }
.sidebar-tab:hover { background: var(--bg-card); color: var(--text-primary); }
.sidebar-tab.active { background: var(--accent); color: #fff; font-weight: 600; }
.shell-layout { display: flex; height: 100%; }
.shell-terminal-col { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
.shell-layout { display: flex; height: 100%; overflow: hidden; }
.shell-terminal-col { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
.shell-tabs-bar {
display: flex; align-items: center; background: var(--bg-surface);
@@ -382,7 +382,7 @@ input::placeholder { color: var(--text-disabled); }
}
.shell-menu-divider { height: 1px; background: var(--border); margin: 4px 6px; }
.shell-xterm-wrapper { flex: 1; height: 100%; background: var(--bg); overflow: hidden; position: relative; }
.shell-xterm-wrapper { flex: 1; min-height: 0; background: var(--bg); overflow: hidden; position: relative; }
.shell-xterm-instance {
position: absolute;
inset: 0;
@@ -402,7 +402,7 @@ input::placeholder { color: var(--text-disabled); }
.shell-tab.ai-tab .shell-tab-name { color: var(--accent); }
.shell-tab.ai-tab { border-bottom-color: var(--accent); }
.shell-ai-col { width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; flex-shrink: 0; }
.shell-ai-col { width: 320px; max-width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden; }
.ai-panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 13px; color: var(--accent); display: flex; align-items: center; justify-content: space-between; }
.shell-analyze-btn {
display: flex; align-items: center; gap: 4px;