add tmux sessions

This commit is contained in:
2026-01-20 14:04:19 -05:00
parent 2b50c91267
commit 66b4acaf42
37 changed files with 2888 additions and 327 deletions
+72 -42
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { browser } from '$app/environment';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount, onDestroy, tick } from 'svelte';
@@ -7,23 +8,41 @@
import InputBar from '$lib/components/InputBar.svelte';
import PermissionRequest from '$lib/components/PermissionRequest.svelte';
import SessionSettings from '$lib/components/SessionSettings.svelte';
import TerminalView from '$lib/components/TerminalView.svelte';
$: sessionId = $page.params.id;
let inputBar: InputBar;
let messageList: MessageList;
let terminalView: TerminalView;
let steerMode = false;
let isEditingTitle = false;
let editedTitle = '';
let titleInput: HTMLInputElement;
let menuOpen = false;
let autoScroll = true;
let tmuxAlive = false;
// Load auto-scroll preference from localStorage
onMount(() => {
if (browser) {
const stored = localStorage.getItem('spiceflow-auto-scroll');
if (stored !== null) {
autoScroll = stored === 'true';
}
}
if (sessionId) {
activeSession.load(sessionId);
}
});
function handleToggleAutoScroll(event: CustomEvent<boolean>) {
autoScroll = event.detail;
if (browser) {
localStorage.setItem('spiceflow-auto-scroll', String(autoScroll));
}
}
onDestroy(() => {
activeSession.clear();
});
@@ -70,7 +89,11 @@
const newTitle = editedTitle.trim();
isEditingTitle = false;
if (newTitle !== (session.title || '')) {
await activeSession.rename(newTitle);
const result = await activeSession.rename(newTitle);
// For tmux sessions, the ID changes on rename - navigate to new URL
if (result?.idChanged && result.updated?.id) {
goto(`/session/${result.updated.id}`, { replaceState: true });
}
}
}
@@ -89,12 +112,17 @@
$: session = $activeSession.session;
$: externalId = session?.['external-id'] || session?.externalId || '';
$: workingDir = session?.['working-dir'] || session?.workingDir || '';
$: shortId = externalId ? externalId.slice(0, 8) : session?.id?.slice(0, 8) || '';
$: projectName = workingDir.split('/').pop() || '';
$: isNewSession = !externalId && $activeSession.messages.length === 0;
$: assistantName = session?.provider === 'opencode' ? 'OpenCode' : 'Claude';
$: autoAcceptEdits = session?.['auto-accept-edits'] || session?.autoAcceptEdits || false;
$: isTmuxSession = session?.provider === 'tmux';
const providerColors: Record<string, string> = {
claude: 'text-spice-400',
opencode: 'text-emerald-400',
tmux: 'text-cyan-400'
};
function handleToggleAutoAccept(event: CustomEvent<boolean>) {
activeSession.setAutoAcceptEdits(event.detail);
@@ -105,6 +133,15 @@
processing: 'bg-green-500 animate-pulse',
'awaiting-permission': 'bg-amber-500 animate-pulse'
};
function handleTmuxAliveChange(event: CustomEvent<boolean>) {
tmuxAlive = event.detail;
}
// For tmux sessions, use tmuxAlive; for others, use session status
$: statusIndicator = isTmuxSession
? (tmuxAlive ? 'bg-green-500' : 'bg-zinc-600')
: (statusColors[session?.status || 'idle'] || statusColors.idle);
</script>
<svelte:head>
@@ -134,7 +171,7 @@
{:else if session}
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {statusColors[session.status] || statusColors.idle}"></span>
<span class="w-2 h-2 rounded-full {statusIndicator}"></span>
{#if isEditingTitle}
<input
bind:this={titleInput}
@@ -154,23 +191,19 @@
</button>
{/if}
</div>
{#if projectName}
<p class="text-xs text-zinc-500 truncate">{projectName}</p>
{/if}
</div>
<SessionSettings
{autoAcceptEdits}
{autoScroll}
provider={session.provider}
on:toggleAutoAccept={handleToggleAutoAccept}
on:toggleAutoScroll={handleToggleAutoScroll}
on:condenseAll={() => messageList?.condenseAll()}
/>
<span
class="text-xs font-medium uppercase {session.provider === 'claude'
? 'text-spice-400'
: 'text-emerald-400'}"
>
{session.provider}
<span class="text-xs font-medium uppercase {providerColors[session.provider] || 'text-zinc-400'}">
{session.provider === 'tmux' ? 'terminal' : session.provider}
</span>
{/if}
</div>
@@ -194,12 +227,9 @@
{#if session}
<div class="px-3 py-2 border-b border-zinc-700">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full {statusColors[session.status] || statusColors.idle}"></span>
<span class="w-2 h-2 rounded-full {statusIndicator}"></span>
<span class="font-semibold truncate">{session.title || `Session ${shortId}`}</span>
</div>
{#if projectName}
<p class="text-xs text-zinc-500 truncate mt-0.5">{projectName}</p>
{/if}
</div>
{/if}
<button
@@ -211,15 +241,26 @@
</svg>
Back to sessions
</button>
<button
on:click={() => { menuOpen = false; messageList?.condenseAll(); }}
class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
Condense all
</button>
<label class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors cursor-pointer">
<input
type="checkbox"
checked={autoScroll}
on:change={() => { autoScroll = !autoScroll; localStorage.setItem('spiceflow-auto-scroll', String(autoScroll)); }}
class="h-4 w-4 rounded border-zinc-600 bg-zinc-700 text-spice-500 focus:ring-spice-500 focus:ring-offset-0"
/>
<span>Auto-scroll</span>
</label>
{#if !isTmuxSession}
<button
on:click={() => { menuOpen = false; messageList?.condenseAll(); }}
class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
Condense all
</button>
{/if}
{#if session?.provider === 'claude'}
<label class="w-full px-3 py-2 text-left text-sm hover:bg-zinc-700 flex items-center gap-2 transition-colors cursor-pointer border-t border-zinc-700">
<input
@@ -255,23 +296,11 @@
></path>
</svg>
</div>
{:else if isTmuxSession}
<!-- Terminal view for tmux sessions -->
<TerminalView bind:this={terminalView} sessionId={sessionId || ''} {autoScroll} on:aliveChange={handleTmuxAliveChange} />
{:else}
{#if workingDir}
<div class="flex-shrink-0 px-4 py-2 bg-zinc-900/50 border-b border-zinc-800 flex items-center gap-2 text-xs text-zinc-500 landscape-mobile:hidden">
<svg class="h-3.5 w-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<span class="truncate font-mono">{workingDir}</span>
<span class="flex-1"></span>
<button
on:click={() => messageList?.condenseAll()}
class="text-zinc-400 hover:text-zinc-200 transition-colors whitespace-nowrap"
>
condense all
</button>
</div>
{/if}
<MessageList bind:this={messageList} messages={$activeSession.messages} streamingContent={$activeSession.streamingContent} isThinking={$activeSession.isThinking} />
<MessageList bind:this={messageList} messages={$activeSession.messages} streamingContent={$activeSession.streamingContent} isThinking={$activeSession.isThinking} provider={session?.provider} {autoScroll} />
{#if $activeSession.pendingPermission}
<PermissionRequest
@@ -287,6 +316,7 @@
bind:this={inputBar}
on:send={handleSend}
disabled={session?.status === 'processing' && $activeSession.streamingContent !== ''}
autoFocus={true}
placeholder={steerMode
? `Tell ${assistantName} what to do instead...`
: session?.status === 'processing'