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; id: string;
command: string; command: string;
workingDir: string; workingDir: string;
name?: string;
status: 'running' | 'exited'; status: 'running' | 'exited';
exitCode?: number; exitCode?: number;
startedAt: string; startedAt: string;
@ -194,7 +195,9 @@ export class SessionCard extends LitElement {
<!-- Compact Header --> <!-- Compact Header -->
<div class="flex justify-between items-center px-3 py-2 border-b border-vs-border"> <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="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> </div>
${this.session.status === 'running' ${this.session.status === 'running'
? html` ? html`

View file

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

View file

@ -8,6 +8,7 @@ export interface Session {
id: string; id: string;
command: string; command: string;
workingDir: string; workingDir: string;
name?: string;
status: 'running' | 'exited'; status: 'running' | 'exited';
exitCode?: number; exitCode?: number;
startedAt: string; 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-text min-w-0 flex-1 overflow-hidden">
<div <div
class="text-vs-accent text-xs sm:text-sm overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap" 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>
<div <div
class="text-vs-muted text-xs overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap" 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 { private measureCharacterWidth(): number {
if (!this.container) return 8;
// Create temporary element with same styles as terminal content, attached to container // Create temporary element with same styles as terminal content, attached to container
const measureEl = document.createElement('div'); const measureEl = document.createElement('div');
measureEl.className = 'terminal-line'; measureEl.className = 'terminal-line';
@ -160,10 +162,10 @@ export class Terminal extends LitElement {
measureEl.textContent = testString.repeat(repeatCount).substring(0, this.cols); measureEl.textContent = testString.repeat(repeatCount).substring(0, this.cols);
// Attach to container so it inherits all the proper CSS context // Attach to container so it inherits all the proper CSS context
this.container!.appendChild(measureEl); this.container.appendChild(measureEl);
const measureRect = measureEl.getBoundingClientRect(); const measureRect = measureEl.getBoundingClientRect();
const actualCharWidth = measureRect.width / this.cols; const actualCharWidth = measureRect.width / this.cols;
this.container!.removeChild(measureEl); this.container.removeChild(measureEl);
return actualCharWidth; return actualCharWidth;
} }
@ -276,12 +278,12 @@ export class Terminal extends LitElement {
this.touchScrollAccumulator = 0; // Reset accumulator on new pointer down this.touchScrollAccumulator = 0; // Reset accumulator on new pointer down
// Capture the pointer so we continue to receive events even if DOM rebuilds // 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) => { const handlePointerMove = (e: PointerEvent) => {
// Only handle touch pointers that we have captured // 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 currentY = e.clientY;
const deltaY = lastY - currentY; // Change since last move, not since start const deltaY = lastY - currentY; // Change since last move, not since start
@ -323,7 +325,7 @@ export class Terminal extends LitElement {
this.isTouchActive = false; this.isTouchActive = false;
// Release pointer capture // Release pointer capture
this.container!.releasePointerCapture(e.pointerId); this.container?.releasePointerCapture(e.pointerId);
// Add momentum scrolling if needed (only after touch scrolling) // Add momentum scrolling if needed (only after touch scrolling)
if (isScrolling && Math.abs(velocity) > 0.5) { if (isScrolling && Math.abs(velocity) > 0.5) {
@ -338,7 +340,7 @@ export class Terminal extends LitElement {
this.isTouchActive = false; this.isTouchActive = false;
// Release pointer capture // Release pointer capture
this.container!.releasePointerCapture(e.pointerId); this.container?.releasePointerCapture(e.pointerId);
}; };
// Attach pointer events to the container (touch only) // Attach pointer events to the container (touch only)

View file

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