Add optional session name support

- Add optional session name field to create form
- Update Session interface to include name property
- Backend now accepts and stores custom session names
- Session cards and views display name when available, fallback to command
- Session names are passed to tty-fwd for better identification

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-17 23:45:55 +02:00
parent 689bd1d765
commit 925bc129c9
6 changed files with 44 additions and 12 deletions

View file

@ -6,6 +6,7 @@ export interface Session {
id: string;
command: string;
workingDir: string;
name?: string;
status: 'running' | 'exited';
exitCode?: number;
startedAt: string;
@ -194,7 +195,9 @@ export class SessionCard extends LitElement {
<!-- Compact Header -->
<div class="flex justify-between items-center px-3 py-2 border-b border-vs-border">
<div class="text-vs-text text-xs font-mono pr-2 flex-1 min-w-0">
<div class="truncate" title="${this.session.command}">${this.session.command}</div>
<div class="truncate" title="${this.session.name || this.session.command}">
${this.session.name || this.session.command}
</div>
</div>
${this.session.status === 'running'
? html`

View file

@ -5,6 +5,7 @@ import './file-browser.js';
export interface SessionCreateData {
command: string[];
workingDir: string;
name?: string;
}
@customElement('session-create-form')
@ -16,6 +17,7 @@ export class SessionCreateForm extends LitElement {
@property({ type: String }) workingDir = '~/';
@property({ type: String }) command = 'zsh';
@property({ type: String }) sessionName = '';
@property({ type: Boolean }) disabled = false;
@property({ type: Boolean }) visible = false;
@ -95,6 +97,11 @@ export class SessionCreateForm extends LitElement {
this.command = input.value;
}
private handleSessionNameChange(e: Event) {
const input = e.target as HTMLInputElement;
this.sessionName = input.value;
}
private handleBrowse() {
this.showFileBrowser = true;
}
@ -125,6 +132,11 @@ export class SessionCreateForm extends LitElement {
workingDir: this.workingDir.trim(),
};
// Add session name if provided
if (this.sessionName.trim()) {
sessionData.name = this.sessionName.trim();
}
try {
const response = await fetch('/api/sessions', {
method: 'POST',
@ -135,10 +147,11 @@ export class SessionCreateForm extends LitElement {
if (response.ok) {
const result = await response.json();
// Save to localStorage before clearing the command
// Save to localStorage before clearing the fields
this.saveToLocalStorage();
this.command = ''; // Clear command on success
this.sessionName = ''; // Clear session name on success
this.dispatchEvent(
new CustomEvent('session-created', {
detail: result,
@ -225,6 +238,18 @@ export class SessionCreateForm extends LitElement {
</div>
<div class="p-4">
<div class="mb-4">
<div class="text-vs-text mb-2">Session Name (optional):</div>
<input
type="text"
class="w-full bg-vs-bg text-vs-text border border-vs-border outline-none font-mono px-4 py-2"
.value=${this.sessionName}
@input=${this.handleSessionNameChange}
placeholder="My Session"
?disabled=${this.disabled || this.isCreating}
/>
</div>
<div class="mb-4">
<div class="text-vs-text mb-2">Working Directory:</div>
<div class="flex gap-4">

View file

@ -8,6 +8,7 @@ export interface Session {
id: string;
command: string;
workingDir: string;
name?: string;
status: 'running' | 'exited';
exitCode?: number;
startedAt: string;

View file

@ -709,9 +709,9 @@ export class SessionView extends LitElement {
<div class="text-vs-text min-w-0 flex-1 overflow-hidden">
<div
class="text-vs-accent text-xs sm:text-sm overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap"
title="${this.session.command}"
title="${this.session.name || this.session.command}"
>
${this.session.command}
${this.session.name || this.session.command}
</div>
<div
class="text-vs-muted text-xs overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap"

View file

@ -145,6 +145,8 @@ export class Terminal extends LitElement {
}
private measureCharacterWidth(): number {
if (!this.container) return 8;
// Create temporary element with same styles as terminal content, attached to container
const measureEl = document.createElement('div');
measureEl.className = 'terminal-line';
@ -160,10 +162,10 @@ export class Terminal extends LitElement {
measureEl.textContent = testString.repeat(repeatCount).substring(0, this.cols);
// Attach to container so it inherits all the proper CSS context
this.container!.appendChild(measureEl);
this.container.appendChild(measureEl);
const measureRect = measureEl.getBoundingClientRect();
const actualCharWidth = measureRect.width / this.cols;
this.container!.removeChild(measureEl);
this.container.removeChild(measureEl);
return actualCharWidth;
}
@ -276,12 +278,12 @@ export class Terminal extends LitElement {
this.touchScrollAccumulator = 0; // Reset accumulator on new pointer down
// Capture the pointer so we continue to receive events even if DOM rebuilds
this.container!.setPointerCapture(e.pointerId);
this.container?.setPointerCapture(e.pointerId);
};
const handlePointerMove = (e: PointerEvent) => {
// Only handle touch pointers that we have captured
if (e.pointerType !== 'touch' || !this.container!.hasPointerCapture(e.pointerId)) return;
if (e.pointerType !== 'touch' || !this.container?.hasPointerCapture(e.pointerId)) return;
const currentY = e.clientY;
const deltaY = lastY - currentY; // Change since last move, not since start
@ -323,7 +325,7 @@ export class Terminal extends LitElement {
this.isTouchActive = false;
// Release pointer capture
this.container!.releasePointerCapture(e.pointerId);
this.container?.releasePointerCapture(e.pointerId);
// Add momentum scrolling if needed (only after touch scrolling)
if (isScrolling && Math.abs(velocity) > 0.5) {
@ -338,7 +340,7 @@ export class Terminal extends LitElement {
this.isTouchActive = false;
// Release pointer capture
this.container!.releasePointerCapture(e.pointerId);
this.container?.releasePointerCapture(e.pointerId);
};
// Attach pointer events to the container (touch only)

View file

@ -165,6 +165,7 @@ app.get('/api/sessions', async (req, res) => {
id: sessionId,
command: sessionInfo.cmdline.join(' '),
workingDir: sessionInfo.cwd,
name: sessionInfo.name,
status: sessionInfo.status,
exitCode: sessionInfo.exit_code,
startedAt: sessionInfo.started_at,
@ -188,13 +189,13 @@ app.get('/api/sessions', async (req, res) => {
// Create new session
app.post('/api/sessions', async (req, res) => {
try {
const { command, workingDir } = req.body;
const { command, workingDir, name } = req.body;
if (!command || !Array.isArray(command) || command.length === 0) {
return res.status(400).json({ error: 'Command array is required and cannot be empty' });
}
const sessionName = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const sessionName = name || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const cwd = resolvePath(workingDir, process.cwd());
const args = [