mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +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;
|
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`
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue