mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Don't expand home dir for grid view
This commit is contained in:
parent
a51ecb174f
commit
d1b0c43a09
4 changed files with 139 additions and 43 deletions
81
web/src/client/components/clickable-path.ts
Normal file
81
web/src/client/components/clickable-path.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* Clickable Path Component
|
||||||
|
*
|
||||||
|
* Displays a formatted path (with ~/ for home directory) that can be clicked to copy the full path.
|
||||||
|
* Provides visual feedback with hover effects and a copy icon.
|
||||||
|
*
|
||||||
|
* @fires path-copied - When path is successfully copied (detail: { path: string })
|
||||||
|
* @fires path-copy-failed - When path copy fails (detail: { path: string, error: string })
|
||||||
|
*/
|
||||||
|
import { LitElement, html } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { formatPathForDisplay, copyToClipboard } from '../utils/path-utils.js';
|
||||||
|
import { createLogger } from '../utils/logger.js';
|
||||||
|
import './copy-icon.js';
|
||||||
|
|
||||||
|
const logger = createLogger('clickable-path');
|
||||||
|
|
||||||
|
@customElement('clickable-path')
|
||||||
|
export class ClickablePath extends LitElement {
|
||||||
|
// Disable shadow DOM to use Tailwind
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ type: String }) path = '';
|
||||||
|
@property({ type: String }) class = '';
|
||||||
|
@property({ type: Number }) iconSize = 12;
|
||||||
|
|
||||||
|
private async handleClick(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!this.path) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await copyToClipboard(this.path);
|
||||||
|
if (success) {
|
||||||
|
logger.log('Path copied to clipboard', { path: this.path });
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('path-copied', {
|
||||||
|
detail: { path: this.path },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Copy command failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to copy path to clipboard', { error, path: this.path });
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('path-copy-failed', {
|
||||||
|
detail: {
|
||||||
|
path: this.path,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.path) return html``;
|
||||||
|
|
||||||
|
const displayPath = formatPathForDisplay(this.path);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="truncate cursor-pointer hover:text-accent-green transition-colors inline-flex items-center gap-1 max-w-full ${this
|
||||||
|
.class}"
|
||||||
|
title="Click to copy path"
|
||||||
|
@click=${this.handleClick}
|
||||||
|
>
|
||||||
|
<span class="truncate">${displayPath}</span>
|
||||||
|
<copy-icon size="${this.iconSize}" class="flex-shrink-0"></copy-icon>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,10 +14,12 @@ import { LitElement, html } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import type { Session } from '../../shared/types.js';
|
import type { Session } from '../../shared/types.js';
|
||||||
import { createLogger } from '../utils/logger.js';
|
import { createLogger } from '../utils/logger.js';
|
||||||
|
import { copyToClipboard } from '../utils/path-utils.js';
|
||||||
|
|
||||||
const logger = createLogger('session-card');
|
const logger = createLogger('session-card');
|
||||||
import './vibe-terminal-buffer.js';
|
import './vibe-terminal-buffer.js';
|
||||||
import './copy-icon.js';
|
import './copy-icon.js';
|
||||||
|
import './clickable-path.js';
|
||||||
|
|
||||||
@customElement('session-card')
|
@customElement('session-card')
|
||||||
export class SessionCard extends LitElement {
|
export class SessionCard extends LitElement {
|
||||||
|
|
@ -192,46 +194,15 @@ export class SessionCard extends LitElement {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.session.pid) {
|
if (this.session.pid) {
|
||||||
try {
|
const success = await copyToClipboard(this.session.pid.toString());
|
||||||
await navigator.clipboard.writeText(this.session.pid.toString());
|
if (success) {
|
||||||
logger.log('PID copied to clipboard', { pid: this.session.pid });
|
logger.log('PID copied to clipboard', { pid: this.session.pid });
|
||||||
} catch (error) {
|
} else {
|
||||||
logger.error('Failed to copy PID to clipboard', { error, pid: this.session.pid });
|
logger.error('Failed to copy PID to clipboard', { pid: this.session.pid });
|
||||||
// Fallback: select text manually
|
|
||||||
this.fallbackCopyToClipboard(this.session.pid.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fallbackCopyToClipboard(text: string) {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
logger.log('Text copied to clipboard (fallback)', { text });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Fallback copy failed', { error });
|
|
||||||
}
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handlePathClick(e: Event) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(this.session.workingDir);
|
|
||||||
logger.log('Path copied to clipboard', { path: this.session.workingDir });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to copy path to clipboard', { error, path: this.session.workingDir });
|
|
||||||
// Fallback: select text manually
|
|
||||||
this.fallbackCopyToClipboard(this.session.workingDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
|
|
@ -342,14 +313,7 @@ export class SessionCard extends LitElement {
|
||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs opacity-75 min-w-0 mt-1">
|
<div class="text-xs opacity-75 min-w-0 mt-1">
|
||||||
<div
|
<clickable-path .path=${this.session.workingDir} .iconSize=${12}></clickable-path>
|
||||||
class="truncate cursor-pointer hover:text-accent-green transition-colors inline-flex items-center gap-1 max-w-full"
|
|
||||||
title="Click to copy path"
|
|
||||||
@click=${this.handlePathClick}
|
|
||||||
>
|
|
||||||
<span class="truncate">${this.session.workingDir}</span>
|
|
||||||
<copy-icon size="12" class="flex-shrink-0"></copy-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import type { Session } from './session-list.js';
|
import type { Session } from './session-list.js';
|
||||||
import './terminal.js';
|
import './terminal.js';
|
||||||
import './file-browser.js';
|
import './file-browser.js';
|
||||||
|
import './clickable-path.js';
|
||||||
import type { Terminal } from './terminal.js';
|
import type { Terminal } from './terminal.js';
|
||||||
import { CastConverter } from '../utils/cast-converter.js';
|
import { CastConverter } from '../utils/cast-converter.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -1076,6 +1077,9 @@ export class SessionView extends LitElement {
|
||||||
>
|
>
|
||||||
${this.session.name || this.session.command}
|
${this.session.name || this.session.command}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-xs opacity-75 mt-0.5">
|
||||||
|
<clickable-path .path=${this.session.workingDir} .iconSize=${12}></clickable-path>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-xs flex-shrink-0 ml-2 relative">
|
<div class="flex items-center gap-2 text-xs flex-shrink-0 ml-2 relative">
|
||||||
|
|
|
||||||
47
web/src/client/utils/path-utils.ts
Normal file
47
web/src/client/utils/path-utils.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Path utilities for formatting and displaying paths
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a file path for display by replacing the home directory with ~
|
||||||
|
* @param path The absolute path to format
|
||||||
|
* @returns The formatted path with ~ replacing the home directory
|
||||||
|
*/
|
||||||
|
export function formatPathForDisplay(path: string): string {
|
||||||
|
const homeDir = '/Users/steipete';
|
||||||
|
if (path.startsWith(homeDir)) {
|
||||||
|
return '~' + path.slice(homeDir.length);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard with fallback for older browsers
|
||||||
|
* @param text The text to copy
|
||||||
|
* @returns Promise<boolean> indicating success
|
||||||
|
*/
|
||||||
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (_error) {
|
||||||
|
// Fallback for older browsers or permission issues
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
textArea.style.top = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
return result;
|
||||||
|
} catch (_err) {
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue