Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [2.1.3] - Previous Release
- Previous version baseline
## [2.6.1] - 2025-08-29

### Added
- Assistant alias support across CLI, server, and UI.
- New CLI flags: `--claude-alias <name>` and `--codex-alias <name>`.
- New env vars: `CLAUDE_ALIAS`, `CODEX_ALIAS`.
- `/api/config` now returns `aliases` for the frontend.
- UI now displays configured aliases in buttons, prompts, and messages.
- Tests: added `test/server-alias.test.js` to validate server alias configuration.

### Changed
- Startup logs show configured aliases.
- README updated with alias usage examples.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ npx claude-code-web --https --cert /path/to/cert.pem --key /path/to/key.pem
```bash
# Enable additional logging and debugging
npx claude-code-web --dev

### Assistant Aliases

You can customize how the assistants are labeled in the UI (for example, to display "Alice" instead of "Claude" or "R2" instead of "Codex").

- Flags:
- `--claude-alias <name>`: Set the display name for Claude (default: env `CLAUDE_ALIAS` or "Claude").
- `--codex-alias <name>`: Set the display name for Codex (default: env `CODEX_ALIAS` or "Codex").

Examples:

```
npx claude-code-web --claude-alias Alice --codex-alias R2
```

Or via environment variables:

```
export CLAUDE_ALIAS=Alice
export CODEX_ALIAS=R2
npx claude-code-web
```

These aliases are for display purposes only; they do not change which underlying CLI is launched.
```
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code block is not properly closed. There's an extra closing triple backtick that creates malformed markdown structure.

Copilot uses AI. Check for mistakes.

### Running from source
Expand Down
8 changes: 7 additions & 1 deletion bin/cc-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const program = new Command();
program
.name('cc-web')
.description('Web-based interface for Claude Code CLI')
.version('2.0.0')
.version('2.6.1')
.option('-p, --port <number>', 'port to run the server on', '32352')
.option('--no-open', 'do not automatically open browser')
.option('--auth <token>', 'authentication token for secure access')
Expand All @@ -21,6 +21,8 @@ program
.option('--key <path>', 'path to SSL private key file')
.option('--dev', 'development mode with additional logging')
.option('--plan <type>', 'subscription plan (pro, max5, max20)', 'max20')
.option('--claude-alias <name>', 'display alias for Claude (default: env CLAUDE_ALIAS or "Claude")')
.option('--codex-alias <name>', 'display alias for Codex (default: env CODEX_ALIAS or "Codex")')
.option('--ngrok-auth-token <token>', 'ngrok auth token to open a public tunnel')
.option('--ngrok-domain <domain>', 'ngrok reserved domain to use for the tunnel')
.parse();
Expand Down Expand Up @@ -68,13 +70,17 @@ async function main() {
key: options.key,
dev: options.dev,
plan: options.plan,
// UI aliases for assistants
claudeAlias: options.claudeAlias || process.env.CLAUDE_ALIAS || 'Claude',
codexAlias: options.codexAlias || process.env.CODEX_ALIAS || 'Codex',
folderMode: true // Always use folder mode
};

console.log('Starting Claude Code Web Interface...');
console.log(`Port: ${port}`);
console.log('Mode: Folder selection mode');
console.log(`Plan: ${options.plan}`);
console.log(`Aliases: Claude → "${serverOptions.claudeAlias}", Codex → "${serverOptions.codexAlias}"`);

// Display authentication status prominently
if (noAuth) {
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": "claude-code-web",
"version": "2.5.3",
"version": "2.6.1",
"description": "Web-based interface for Claude Code CLI accessible via browser",
"main": "src/server.js",
"bin": {
Expand Down
60 changes: 51 additions & 9 deletions src/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class ClaudeCodeWebInterface {
this.currentMode = 'chat';
this.planDetector = null;
this.planModal = null;
// Aliases for assistants (populated from /api/config)
this.aliases = { claude: 'Claude', codex: 'Codex' };


// Initialize the session tab manager
Expand Down Expand Up @@ -66,10 +68,12 @@ class ClaudeCodeWebInterface {
return;
}

await this.loadConfig();
this.setupTerminal();
this.setupUI();
this.setupPlanDetector();
this.loadSettings();
this.applyAliasesToUI();
this.disablePullToRefresh();

// Show loading while we initialize
Expand Down Expand Up @@ -111,6 +115,44 @@ class ClaudeCodeWebInterface {
this.disconnect();
});
}

async loadConfig() {
try {
const res = await this.authFetch('/api/config');
if (res.ok) {
const cfg = await res.json();
if (cfg?.aliases) {
this.aliases = {
claude: cfg.aliases.claude || 'Claude',
codex: cfg.aliases.codex || 'Codex'
};
}
if (typeof cfg.folderMode === 'boolean') {
this.folderMode = cfg.folderMode;
}
}
} catch (_) { /* best-effort */ }
}

getAlias(kind) {
return (this.aliases && this.aliases[kind]) ? this.aliases[kind] : (kind === 'codex' ? 'Codex' : 'Claude');
}

applyAliasesToUI() {
// Start prompt buttons
const startBtn = document.getElementById('startBtn');
const dangerousSkipBtn = document.getElementById('dangerousSkipBtn');
const startCodexBtn = document.getElementById('startCodexBtn');
const dangerousCodexBtn = document.getElementById('dangerousCodexBtn');
if (startBtn) startBtn.textContent = `Start ${this.getAlias('claude')}`;
if (dangerousSkipBtn) dangerousSkipBtn.textContent = `Dangerous ${this.getAlias('claude')}`;
if (startCodexBtn) startCodexBtn.textContent = `Start ${this.getAlias('codex')}`;
if (dangerousCodexBtn) dangerousCodexBtn.textContent = `Dangerous ${this.getAlias('codex')}`;

// Plan modal title
const planTitle = document.querySelector('#planModal .modal-header h2');
if (planTitle) planTitle.textContent = `📋 ${this.getAlias('claude')}'s Plan`;
}

detectMobile() {
// Check for touch capability and common mobile user agents
Expand Down Expand Up @@ -528,7 +570,7 @@ class ClaudeCodeWebInterface {

async runCommandFromPath(relPath) {
if (!this.currentClaudeSessionId) {
this.showError('Start Claude/Codex in a session first');
this.showError(`Start ${this.getAlias('claude')}/${this.getAlias('codex')} in a session first`);
return;
}
try {
Expand Down Expand Up @@ -574,7 +616,7 @@ class ClaudeCodeWebInterface {
return;
}
if (!this.currentClaudeSessionId) {
this.showError('Start Claude/Codex in a session first');
this.showError(`Start ${this.getAlias('claude')}/${this.getAlias('codex')} in a session first`);
return;
}
// Send and close
Expand Down Expand Up @@ -782,7 +824,7 @@ class ClaudeCodeWebInterface {
console.log('[session_joined] Existing session with stopped Claude, showing restart prompt');
// For existing sessions where Claude has stopped, show start prompt
// This allows the user to restart Claude in the same session
this.terminal.writeln('\r\n\x1b[33mClaude Code has stopped in this session. Click "Start Claude Code" to restart.\x1b[0m');
this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('claude')} has stopped in this session. Click "Start ${this.getAlias('claude')}" to restart.\x1b[0m`);
this.showOverlay('startPrompt');
}
}
Expand Down Expand Up @@ -829,7 +871,7 @@ class ClaudeCodeWebInterface {
break;

case 'claude_stopped':
this.terminal.writeln(`\r\n\x1b[33mClaude Code stopped\x1b[0m`);
this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('claude')} stopped\x1b[0m`);
// Show start prompt to allow restarting Claude in this session
this.showOverlay('startPrompt');
this.loadSessions(); // Refresh session list
Expand Down Expand Up @@ -857,7 +899,7 @@ class ClaudeCodeWebInterface {
break;

case 'exit':
this.terminal.writeln(`\r\n\x1b[33mClaude Code exited with code ${message.code}\x1b[0m`);
this.terminal.writeln(`\r\n\x1b[33m${this.getAlias('claude')} exited with code ${message.code}\x1b[0m`);

// Mark session as error if non-zero exit code
if (this.sessionTabManager && this.currentClaudeSessionId && message.code !== 0) {
Expand Down Expand Up @@ -931,8 +973,8 @@ class ClaudeCodeWebInterface {

this.showOverlay('loadingSpinner');
const loadingText = options.dangerouslySkipPermissions ?
'Starting Claude Code (⚠️ Skipping permissions)...' :
'Starting Claude Code...';
`Starting ${this.getAlias('claude')} (⚠️ Skipping permissions)...` :
`Starting ${this.getAlias('claude')}...`;
document.getElementById('loadingSpinner').querySelector('p').textContent = loadingText;
}

Expand All @@ -955,8 +997,8 @@ class ClaudeCodeWebInterface {

this.showOverlay('loadingSpinner');
const loadingText = options.dangerouslySkipPermissions ?
'Starting Codex (⚠️ Bypassing approvals and sandbox)...' :
'Starting Codex...';
`Starting ${this.getAlias('codex')} (⚠️ Bypassing approvals and sandbox)...` :
`Starting ${this.getAlias('codex')}...`;
document.getElementById('loadingSpinner').querySelector('p').textContent = loadingText;
}

Expand Down
13 changes: 10 additions & 3 deletions src/public/session-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ class SessionTabManager {
this.notificationsEnabled = false;
this.requestNotificationPermission();
}

getAlias(kind) {
if (this.claudeInterface && typeof this.claudeInterface.getAlias === 'function') {
return this.claudeInterface.getAlias(kind);
}
return kind === 'codex' ? 'Codex' : 'Claude';
}

requestNotificationPermission() {
if ('Notification' in window) {
Expand Down Expand Up @@ -216,7 +223,7 @@ class SessionTabManager {
promptDiv.innerHTML = `
<div style="margin-bottom: 10px;">
<strong>Enable Desktop Notifications?</strong><br>
Get notified when Claude completes tasks in background tabs.
Get notified when ${this.getAlias('claude')} completes tasks in background tabs.
</div>
<div style="display: flex; gap: 10px;">
<button id="enableNotifications" style="
Expand Down Expand Up @@ -853,7 +860,7 @@ class SessionTabManager {

// Send notification that Claude appears to have finished
this.sendNotification(
`✅ ${sessionName} - Claude appears finished`,
`✅ ${sessionName} - ${this.getAlias('claude')} appears finished`,
`No output for 90 seconds (worked for ${Math.round(duration / 1000)}s)`,
sessionId
);
Expand Down Expand Up @@ -981,4 +988,4 @@ class SessionTabManager {
}

// Export for use in app.js
window.SessionTabManager = SessionTabManager;
window.SessionTabManager = SessionTabManager;
8 changes: 7 additions & 1 deletion src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class ClaudeCodeWebServer {
this.startTime = Date.now(); // Track server start time
// Commands directory (in user's home)
this.commandsDir = path.join(os.homedir(), '.claude-code-web', 'commands');
// Assistant aliases (for UI display only)
this.aliases = {
claude: options.claudeAlias || process.env.CLAUDE_ALIAS || 'Claude',
codex: options.codexAlias || process.env.CODEX_ALIAS || 'Codex'
};

this.setupExpress();
this.loadPersistedSessions();
Expand Down Expand Up @@ -382,7 +387,8 @@ class ClaudeCodeWebServer {
res.json({
folderMode: this.folderMode,
selectedWorkingDir: this.selectedWorkingDir,
baseFolder: this.baseFolder
baseFolder: this.baseFolder,
aliases: this.aliases
});
});

Expand Down
22 changes: 22 additions & 0 deletions test/server-alias.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const assert = require('assert');
const { ClaudeCodeWebServer } = require('../src/server');

describe('Server Aliases', function() {
it('should set aliases from options', function() {
const server = new ClaudeCodeWebServer({
claudeAlias: 'Buddy',
codexAlias: 'Robo',
noAuth: true // avoid auth middleware complexity
Comment on lines +5 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Tear down ClaudeCodeWebServer in tests to prevent hanging suite

Both alias tests instantiate ClaudeCodeWebServer and immediately assert on server.aliases, but the constructor schedules a 30 s setInterval in setupAutoSave() and registers process listeners. Because the test never calls handleShutdown()/close() or clears the interval, npm test never exits—the event loop stays alive until the process is killed (confirmed by the suite hanging when run locally). Add an afterEach that invokes the server’s shutdown path to clean up timers.

Useful? React with 👍 / 👎.

});

assert.strictEqual(server.aliases.claude, 'Buddy');
assert.strictEqual(server.aliases.codex, 'Robo');
});

it('should default aliases when not provided', function() {
const server = new ClaudeCodeWebServer({ noAuth: true });
assert.ok(server.aliases.claude && server.aliases.claude.length > 0);
assert.ok(server.aliases.codex && server.aliases.codex.length > 0);
});
});