From a929c166a3efe349b4e36d229bfd85e4571cd35a Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Mon, 15 Jun 2026 11:47:21 +0700 Subject: [PATCH 1/2] feat(app): rename a workspace by double-clicking its name Double-click a sidebar workspace name to edit it inline; Enter/blur commits via setWorkspaceMeta({name}) (empty/unchanged is a no-op), Esc cancels. The input stops pointer/key propagation so it doesn't trigger select or drag. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src/Sidebar.tsx | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/app/src/Sidebar.tsx b/app/src/Sidebar.tsx index 3c7553c..5ab4603 100644 --- a/app/src/Sidebar.tsx +++ b/app/src/Sidebar.tsx @@ -41,6 +41,7 @@ export function Sidebar({ const [hovered, setHovered] = useState(null); const [drag, setDrag] = useState<{ id: string; section: string } | null>(null); const [dropAt, setDropAt] = useState(null); + const [editing, setEditing] = useState<{ id: string; draft: string } | null>(null); const dragRef = useRef<{ id: string; section: string } | null>(null); const dropRef = useRef(null); const [, setTick] = useState(0); @@ -59,6 +60,17 @@ export function Sidebar({ const togglePin = (w: WorkspaceView) => { void setWorkspaceMeta(w.id, { pinned: !w.pinned }); }; + const commitRename = () => { + setEditing((cur) => { + if (cur) { + const name = cur.draft.trim(); + const w = workspaces.find((x) => x.id === cur.id); + if (name && w && name !== w.name) void setWorkspaceMeta(cur.id, { name }); + } + return null; + }); + }; + // Persist a new ordering for one section by reassigning sequential `order` // values (per-section; values never compared across sections). const commitReorder = (items: WorkspaceView[], fromId: string, toIndex: number) => { @@ -123,7 +135,30 @@ export function Sidebar({ color: isActive ? COLORS.textPrimary : COLORS.textSecondary, }}> - {w.name} + {editing?.id === w.id ? ( + e.target.select()} + onMouseDown={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + onChange={(e) => setEditing({ id: w.id, draft: e.target.value })} + onBlur={commitRename} + onKeyDown={(e) => { + e.stopPropagation(); + if (e.key === "Enter") { e.preventDefault(); commitRename(); } + else if (e.key === "Escape") { e.preventDefault(); setEditing(null); } + }} + style={{ flex: 1, minWidth: 0, background: COLORS.bgApp, color: COLORS.textPrimary, border: `1px solid ${COLORS.accent}`, borderRadius: 4, padding: "2px 6px", fontFamily: FONT.ui, fontSize: 13, outline: "none" }} + /> + ) : ( + { e.stopPropagation(); setEditing({ id: w.id, draft: w.name }); }} + title="Двойной клик — переименовать" + style={{ flex: 1, fontWeight: isActive ? 600 : 400, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}> + {w.name} + + )} {(hovered === w.id || w.pinned) && ( Date: Mon, 15 Jun 2026 11:47:21 +0700 Subject: [PATCH 2/2] fix(pty): always set TERM/COLORTERM for spawned shells A GUI/launchd-spawned daemon has no TERM in its environment, so child shells inherited none and tput/zsh/ncurses failed ('tput: No value for $TERM'). The PTY now defaults TERM=xterm-256color and COLORTERM=truecolor (matching xterm.js) unless the caller already provides them. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/spacesh-pty/src/lib.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/spacesh-pty/src/lib.rs b/crates/spacesh-pty/src/lib.rs index b2946a1..e2addce 100644 --- a/crates/spacesh-pty/src/lib.rs +++ b/crates/spacesh-pty/src/lib.rs @@ -38,6 +38,16 @@ impl PtyHandle { cmd.arg(a); } cmd.cwd(&spec.cwd); + // Guarantee a terminal environment even when the daemon was launched + // without one (GUI/launchd have no TERM, which breaks tput/zsh/ncurses). + // xterm.js renders an xterm-256color/truecolor terminal. Caller-provided + // values in spec.env win. + if !spec.env.iter().any(|(k, _)| k == "TERM") { + cmd.env("TERM", "xterm-256color"); + } + if !spec.env.iter().any(|(k, _)| k == "COLORTERM") { + cmd.env("COLORTERM", "truecolor"); + } for (k, v) in &spec.env { cmd.env(k, v); } @@ -125,6 +135,19 @@ mod tests { assert!(text.contains("SPACESH_OK"), "got: {text:?}"); } + #[tokio::test] + async fn term_is_set_even_without_inherited_env() { + // Clear TERM in the parent to emulate a GUI/launchd-spawned daemon. + std::env::remove_var("TERM"); + let mut handle = PtyHandle::spawn(shell_spec("printf %s \"$TERM\"")).unwrap(); + let mut collected = Vec::new(); + while let Some(chunk) = handle.output.recv().await { + collected.extend_from_slice(&chunk); + } + let text = String::from_utf8_lossy(&collected); + assert!(text.contains("xterm-256color"), "got: {text:?}"); + } + #[tokio::test] async fn resize_does_not_error() { let handle = PtyHandle::spawn(shell_spec("sleep 0.2")).unwrap();