Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "codbash-desktop",
"productName": "codbash",
"version": "7.16.0",
"version": "7.17.0",
"private": true,
"description": "Desktop shell (Electron) for codbash — wraps the codbash server in a native window.",
"main": "main.js",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codbash-app",
"version": "7.16.0",
"version": "7.17.0",
"description": "Dashboard + CLI for AI coding agents — Claude Code, Codex, Cursor, OpenCode, Kiro. View, search, resume, convert, sync sessions.",
"bin": {
"codbash": "./bin/cli.js",
Expand Down
9 changes: 9 additions & 0 deletions src/changelog.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
'use strict';

const CHANGELOG = [
{
version: '7.17.0',
date: '2026-07-30',
title: 'Name your terminals — and dimmed text is readable again',
changes: [
'Terminal panes can be named: click the ✎ in a pane\'s title bar (or double-click the title) to label it, e.g. "API" and "worker" for two shells in the same folder. An empty name restores the automatic label (agent → folder → ~), and the name survives reloads, saved layouts and restored sessions',
'Fixed dimmed text on a filled background rendering as an empty highlighted bar with no text at all — some rows (e.g. the grey "message above" bars a coding agent draws) lost their text entirely in a codbash pane while staying legible in iTerm. Terminal panes now enforce a minimum contrast ratio, so a dimmed foreground is never washed out to invisible',
],
},
{
version: '7.16.0',
date: '2026-07-30',
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -3387,6 +3387,20 @@ body[data-view="overview"] .toolbar { display: none; }
cursor: pointer;
}
.ws-pane-bm:hover { color: var(--accent-orange, #f59e0b); border-color: var(--accent-orange, #f59e0b); }
/* Rename this terminal — same chrome as the bookmark button next to it. */
.ws-pane-ren {
font-size: 12px;
line-height: 1;
width: 22px; height: 22px;
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
}
.ws-pane-ren:hover { color: var(--accent, #3b82f6); border-color: var(--accent, #3b82f6); }
/* The pane title doubles as a rename target (double-click). */
.ws-pane-status { cursor: default; }
.ws-pane-close {
font-size: 15px;
line-height: 1;
Expand Down
48 changes: 41 additions & 7 deletions src/frontend/workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ function _wsSaveSession() {
var snap = _wsCaptureLayout();
// Don't persist a lone empty pane — that's just the default blank state.
var meaningful = snap.tabs.some(function (t) {
return t.panes.some(function (p) { return p.cmd || p.prefill || p.cwd || p.enteredCmd; });
return t.panes.some(function (p) { return p.cmd || p.prefill || p.cwd || p.enteredCmd || p.name; });
}) || snap.tabs.length > 1 || (snap.tabs[0] && snap.tabs[0].panes.length > 1);
var sig = meaningful ? JSON.stringify(snap) : '';
if (sig === _wsLastSessionSig) return;
Expand Down Expand Up @@ -210,7 +210,7 @@ function _wsRestoreTabsFromSession(sess) {
.slice(0, MAX_WS_PANES)
.map(function (p) {
var restoreCmd = (p && (p.cmd || p.enteredCmd || p.detectedCmd || p.prefill)) || '';
return { id: 'p' + (++_wsPaneSeq), cmd: null, prefill: null, restoreCmd: restoreCmd || null, wantCwd: (p && p.cwd) || null };
return { id: 'p' + (++_wsPaneSeq), cmd: null, prefill: null, restoreCmd: restoreCmd || null, wantCwd: (p && p.cwd) || null, name: (p && p.name) || '' };
});
return { id: 't' + (++_wsTabSeq), name: t.name || ('Tab ' + (ti + 1)), panes: panes,
cols: Array.isArray(t.cols) ? t.cols.slice() : null, rows: Array.isArray(t.rows) ? t.rows.slice() : null };
Expand Down Expand Up @@ -529,6 +529,8 @@ function _wsShortCwd(cwd) {
// A short, human label for a pane's title bar: the running agent (if any),
// otherwise the folder name (e.g. "CoWork"), or "~" for the home directory.
function _wsPaneLabel(pane) {
// A name the user typed always wins — it's the whole point of renaming.
if (pane && pane.name) return pane.name;
if (pane && pane.cmd) {
// Strip leading `VAR=value` env assignments (value may be quoted and hold
// secrets, e.g. HTTPS_PROXY='http://user:pass@host') so the label is the
Expand All @@ -552,7 +554,16 @@ function _wsConnectPane(pane) {
var term = new Terminal({
cursorBlink: _tp.cursorBlink, cursorStyle: _tp.cursorStyle,
fontSize: _tp.fontSize, fontFamily: _tp.fontFamily,
theme: _wsTermTheme(), scrollback: 5000, allowProposedApi: true
theme: _wsTermTheme(), scrollback: 5000, allowProposedApi: true,
// Guarantee readable text against whatever background a TUI paints.
// Without this (xterm's default is 1 = no adjustment) *dimmed* text on a
// filled background renders as an apparently EMPTY highlighted bar — the
// WebGL renderer applies dim by cutting the foreground alpha, so e.g. the
// grey "message above" rows Claude Code draws lost their text entirely,
// while the same output stayed legible in iTerm. 4.5 is the WCAG AA ratio;
// xterm only nudges a foreground when it falls below it, so normal colors
// are left alone.
minimumContrastRatio: 4.5
});
var fit = new FitAddon.FitAddon();
term.loadAddon(fit);
Expand Down Expand Up @@ -734,7 +745,11 @@ function _wsPaneMarkup(pane) {
return '' +
'<div class="ws-pane" data-pane-id="' + escHtml(pane.id) + '">' +
'<div class="ws-pane-bar">' +
'<span class="ws-pane-status" id="wsStatus-' + escHtml(pane.id) + '">connecting…</span>' +
'<span class="ws-pane-status" id="wsStatus-' + escHtml(pane.id) + '" ' +
'title="Double-click to rename this terminal" ' +
'ondblclick="renameWorkspacePane(\'' + escHtml(pane.id) + '\')">connecting…</span>' +
'<button class="ws-pane-ren" title="Rename this terminal" aria-label="Rename terminal" ' +
'onclick="renameWorkspacePane(\'' + escHtml(pane.id) + '\')">&#9998;</button>' +
'<select class="ws-pane-launch" title="Launch an agent or saved command in this pane" ' +
'onchange="launchAgentInPane(\'' + escHtml(pane.id) + '\', this.value); this.selectedIndex=0;">' + _wsLaunchOptionsHtml() + '</select>' +
'<button class="ws-pane-bm" title="Bookmark this folder + agent" aria-label="Bookmark" onclick="bookmarkPane(\'' + escHtml(pane.id) + '\')">&#9734;</button>' +
Expand Down Expand Up @@ -1531,7 +1546,7 @@ function _wsBuildPanes(spec) {
: [{ cwd: spec.cwd, cmd: spec.cmd, prefill: spec.prefill }];
list = list.slice(0, MAX_WS_PANES);
return list.map(function (pc) {
return { id: 'p' + (++_wsPaneSeq), cmd: pc.cmd || null, prefill: pc.prefill || null, wantCwd: pc.cwd || null };
return { id: 'p' + (++_wsPaneSeq), cmd: pc.cmd || null, prefill: pc.prefill || null, wantCwd: pc.cwd || null, name: pc.name || '' };
});
}

Expand Down Expand Up @@ -1815,7 +1830,7 @@ function _wsSerializeTab(tab) {
cols: Array.isArray(tab.cols) ? tab.cols.slice() : null,
rows: Array.isArray(tab.rows) ? tab.rows.slice() : null,
panes: tab.panes.map(function (p) {
return { cmd: p.cmd || '', prefill: p.prefill || '', cwd: p.cwd || p.wantCwd || '', detectedCmd: p.detectedCmd || '', enteredCmd: p.enteredCmd || '' };
return { cmd: p.cmd || '', prefill: p.prefill || '', cwd: p.cwd || p.wantCwd || '', name: p.name || '', detectedCmd: p.detectedCmd || '', enteredCmd: p.enteredCmd || '' };
}),
};
}
Expand Down Expand Up @@ -1846,7 +1861,7 @@ function reopenLastClosedTab() {
.slice(0, MAX_WS_PANES)
.map(function (p) {
var cmd = (p && (p.cmd || p.enteredCmd || p.detectedCmd || p.prefill)) || '';
return { id: 'p' + (++_wsPaneSeq), cmd: null, prefill: null, restoreCmd: cmd || null, wantCwd: (p && p.cwd) || null };
return { id: 'p' + (++_wsPaneSeq), cmd: null, prefill: null, restoreCmd: cmd || null, wantCwd: (p && p.cwd) || null, name: (p && p.name) || '' };
});
var tab = { id: 't' + (++_wsTabSeq), name: spec.name || 'Tab', panes: panes,
cols: Array.isArray(spec.cols) ? spec.cols.slice() : null, rows: Array.isArray(spec.rows) ? spec.rows.slice() : null };
Expand Down Expand Up @@ -1920,6 +1935,23 @@ function addWorkspacePane(cmd) {
_wsRenderPanes(); _wsSyncLayoutButtons();
}

// Give a pane its own name, shown in the title bar instead of the folder/agent
// label. Uses codbashPrompt, not window.prompt — the latter is a no-op in the
// Electron shell. An empty answer clears the name and falls back to the
// auto-label; the name round-trips through saved layouts and the session.
function renameWorkspacePane(id) {
var pane = _wsFindPane(id);
if (!pane) return;
codbashPrompt('Terminal name:', pane.name || _wsPaneLabel(pane)).then(function (name) {
if (name === null) return; // cancelled — leave as-is
var next = String(name).trim().slice(0, 120); // matches MAX_NAME server-side
pane.name = next; // '' clears it → auto-label
var st = document.getElementById('wsStatus-' + pane.id);
if (st) st.textContent = _wsPaneLabel(pane);
_wsSaveSession();
});
}

function closeWorkspacePane(id) {
for (var i = 0; i < _wsTabs.length; i++) {
var tab = _wsTabs[i];
Expand Down Expand Up @@ -2073,6 +2105,7 @@ function _wsCaptureLayout() {
cmd: p.cmd || '',
prefill: p.prefill || '',
cwd: p.cwd || p.wantCwd || '',
name: p.name || '', // user-chosen pane label
detectedCmd: p.detectedCmd || '',
enteredCmd: p.enteredCmd || '',
};
Expand Down Expand Up @@ -2145,6 +2178,7 @@ function applyWorkspaceLayout(id) {
cmd: (p && p.cmd) || null,
prefill: (p && p.prefill) || null,
wantCwd: (p && p.cwd) || null,
name: (p && p.name) || '',
};
});
return { id: 't' + (++_wsTabSeq), name: t.name || ('Tab ' + (ti + 1)), panes: panes };
Expand Down
5 changes: 5 additions & 0 deletions src/workspace-layouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,14 @@ function sanitizePane(p) {
const cwdRaw = p && typeof p === 'object' && typeof p.cwd === 'string' ? p.cwd.trim() : '';
if (cwdRaw.length > MAX_COMMAND) return null;
if (cwdRaw && CONTROL_CHARS.test(cwdRaw)) return null;
// A user-chosen pane label. Kept short: it only has to fit the pane title bar.
const nameRaw = p && typeof p === 'object' && typeof p.name === 'string' ? p.name.trim() : '';
if (nameRaw.length > MAX_NAME) return null;
if (nameRaw && CONTROL_CHARS.test(nameRaw)) return null;
const out = { cmd };
if (prefillRaw) out.prefill = prefillRaw;
if (cwdRaw) out.cwd = cwdRaw;
if (nameRaw) out.name = nameRaw;
return out;
}

Expand Down
28 changes: 28 additions & 0 deletions test/workspace-layouts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ test('sanitizeLayout accepts a multi-tab, multi-pane layout', () => {
assert.equal(l.tabs[1].panes[0].cmd, '');
});

test('sanitizeLayout preserves a user-chosen pane name', () => {
const { m } = freshModule();
const l = m.sanitizeLayout({
name: 'named panes',
tabs: [{ name: 'work', panes: [{ cmd: 'claude', name: 'API server', cwd: '/Users/me/proj' }] }],
});
assert.ok(l);
const p = l.tabs[0].panes[0];
assert.equal(p.name, 'API server');
assert.equal(p.cmd, 'claude');
assert.equal(p.cwd, '/Users/me/proj');
});

test('sanitizeLayout drops a pane name with control characters', () => {
const { m } = freshModule();
assert.equal(
m.sanitizeLayout({ name: 'bad', tabs: [{ name: 't', panes: [{ cmd: '', name: 'evil\x07name' }] }] }),
null
);
});

test('sanitizeLayout omits an empty pane name instead of storing it', () => {
const { m } = freshModule();
const l = m.sanitizeLayout({ name: 'blank', tabs: [{ name: 't', panes: [{ cmd: 'claude', name: ' ' }] }] });
assert.ok(l);
assert.equal('name' in l.tabs[0].panes[0], false);
});

test('sanitizeLayout preserves per-pane prefill and cwd', () => {
const { m } = freshModule();
const l = m.sanitizeLayout({
Expand Down
Loading