mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-19 13:35:54 +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 type { Session } from '../../shared/types.js';
|
||||
import { createLogger } from '../utils/logger.js';
|
||||
import { copyToClipboard } from '../utils/path-utils.js';
|
||||
|
||||
const logger = createLogger('session-card');
|
||||
import './vibe-terminal-buffer.js';
|
||||
import './copy-icon.js';
|
||||
import './clickable-path.js';
|
||||
|
||||
@customElement('session-card')
|
||||
export class SessionCard extends LitElement {
|
||||
|
|
@ -192,46 +194,15 @@ export class SessionCard extends LitElement {
|
|||
e.preventDefault();
|
||||
|
||||
if (this.session.pid) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.session.pid.toString());
|
||||
const success = await copyToClipboard(this.session.pid.toString());
|
||||
if (success) {
|
||||
logger.log('PID copied to clipboard', { pid: this.session.pid });
|
||||
} catch (error) {
|
||||
logger.error('Failed to copy PID to clipboard', { error, pid: this.session.pid });
|
||||
// Fallback: select text manually
|
||||
this.fallbackCopyToClipboard(this.session.pid.toString());
|
||||
} else {
|
||||
logger.error('Failed to copy PID to clipboard', { pid: this.session.pid });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
return html`
|
||||
<div
|
||||
|
|
@ -342,14 +313,7 @@ export class SessionCard extends LitElement {
|
|||
: ''}
|
||||
</div>
|
||||
<div class="text-xs opacity-75 min-w-0 mt-1">
|
||||
<div
|
||||
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>
|
||||
<clickable-path .path=${this.session.workingDir} .iconSize=${12}></clickable-path>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||
import type { Session } from './session-list.js';
|
||||
import './terminal.js';
|
||||
import './file-browser.js';
|
||||
import './clickable-path.js';
|
||||
import type { Terminal } from './terminal.js';
|
||||
import { CastConverter } from '../utils/cast-converter.js';
|
||||
import {
|
||||
|
|
@ -1076,6 +1077,9 @@ export class SessionView extends LitElement {
|
|||
>
|
||||
${this.session.name || this.session.command}
|
||||
</div>
|
||||
<div class="text-xs opacity-75 mt-0.5">
|
||||
<clickable-path .path=${this.session.workingDir} .iconSize=${12}></clickable-path>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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