mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-10 12:05:53 +00:00
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:
parent
689bd1d765
commit
925bc129c9
6 changed files with 44 additions and 12 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export interface Session {
|
|||
id: string;
|
||||
command: string;
|
||||
workingDir: string;
|
||||
name?: string;
|
||||
status: 'running' | 'exited';
|
||||
exitCode?: number;
|
||||
startedAt: string;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Reference in a new issue