Add ContentLayout with auto-direction + Dashboard/Files/Artifacts panes
- ContentLayout: ResizeObserver-based row/col direction (flips at 1.5× aspect ratio), flex weights 40/40/10 for chat/dashboard/thin, files+artifacts share thin slot - DashboardPane: placeholder page (agent artifacts will render here) - FilesPane / ArtifactsPane: dummy panels with vertical/horizontal orientation toggle - usePanels: replace 'workspace' PaneId with 'dashboard' | 'files' | 'artifacts' - AgentsView: wire ContentLayout, remove WorkspacePane, clean up pane-layout styles - AppToolbar: replace workspace toggle with dashboard/files/artifacts buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4607a73d67
commit
60029f1863
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div class="toolbar-spacer" />
|
<div class="toolbar-spacer" />
|
||||||
|
|
||||||
<!-- Pane toggles (chat, workspace) -->
|
<!-- Pane toggles -->
|
||||||
<button
|
<button
|
||||||
class="toolbar-pill toolbar-pill-icon"
|
class="toolbar-pill toolbar-pill-icon"
|
||||||
:class="{ active: isPaneOpen('chat') }"
|
:class="{ active: isPaneOpen('chat') }"
|
||||||
@ -19,11 +19,27 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="toolbar-pill toolbar-pill-icon"
|
class="toolbar-pill toolbar-pill-icon"
|
||||||
:class="{ active: isPaneOpen('workspace') }"
|
:class="{ active: isPaneOpen('dashboard') }"
|
||||||
@click="togglePane('workspace')"
|
@click="togglePane('dashboard')"
|
||||||
title="Workspace"
|
title="Dashboard"
|
||||||
>
|
>
|
||||||
<RectangleStackIcon class="w-4 h-4" />
|
<RectangleGroupIcon class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="toolbar-pill toolbar-pill-icon"
|
||||||
|
:class="{ active: isPaneOpen('files') }"
|
||||||
|
@click="togglePane('files')"
|
||||||
|
title="Files"
|
||||||
|
>
|
||||||
|
<FolderIcon class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="toolbar-pill toolbar-pill-icon"
|
||||||
|
:class="{ active: isPaneOpen('artifacts') }"
|
||||||
|
@click="togglePane('artifacts')"
|
||||||
|
title="Artifacts"
|
||||||
|
>
|
||||||
|
<SparklesIcon class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="toolbar-divider" />
|
<div class="toolbar-divider" />
|
||||||
@ -102,7 +118,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({ name: 'AppToolbar' });
|
defineOptions({ name: 'AppToolbar' });
|
||||||
import { ref, computed, type Component } from 'vue';
|
import { ref, computed, type Component } from 'vue';
|
||||||
import { WifiIcon, SignalIcon, QueueListIcon, CpuChipIcon, EyeIcon, CircleStackIcon, ChatBubbleLeftIcon, RectangleStackIcon } from '@heroicons/vue/20/solid';
|
import { WifiIcon, SignalIcon, QueueListIcon, CpuChipIcon, EyeIcon, CircleStackIcon, ChatBubbleLeftIcon, RectangleGroupIcon, FolderIcon, SparklesIcon } from '@heroicons/vue/20/solid';
|
||||||
import { THEME_ICONS, THEME_NAMES, useTheme, type Theme } from '../composables/useTheme';
|
import { THEME_ICONS, THEME_NAMES, useTheme, type Theme } from '../composables/useTheme';
|
||||||
import { ws, auth, agents, takeover } from '../store';
|
import { ws, auth, agents, takeover } from '../store';
|
||||||
import { useChatStore } from '../store/chat';
|
import { useChatStore } from '../store/chat';
|
||||||
|
|||||||
115
src/components/ArtifactsPane.vue
Normal file
115
src/components/ArtifactsPane.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="artifacts-pane" :class="orientation">
|
||||||
|
<div class="artifacts-bar">
|
||||||
|
<span class="artifacts-label">Artifacts</span>
|
||||||
|
<button class="orient-btn" @click="toggleOrientation" :title="orientation === 'vertical' ? 'Switch to horizontal' : 'Switch to vertical'">
|
||||||
|
<component :is="orientation === 'vertical' ? Bars3Icon : Bars3BottomLeftIcon" class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="artifacts-body">
|
||||||
|
<span class="artifacts-hint">saved artifacts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'ArtifactsPane' });
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Bars3Icon, Bars3BottomLeftIcon } from '@heroicons/vue/20/solid';
|
||||||
|
|
||||||
|
const orientation = ref<'vertical' | 'horizontal'>('vertical');
|
||||||
|
function toggleOrientation() {
|
||||||
|
orientation.value = orientation.value === 'vertical' ? 'horizontal' : 'vertical';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.artifacts-pane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical: bar on top, body below */
|
||||||
|
.artifacts-pane.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.artifacts-pane.vertical .artifacts-bar {
|
||||||
|
flex-direction: row;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.artifacts-pane.vertical .artifacts-body {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal: bar on left, body to the right */
|
||||||
|
.artifacts-pane.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.artifacts-pane.horizontal .artifacts-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 8px 0;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
.artifacts-pane.horizontal .artifacts-body {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifacts-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--bg-dim, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.artifacts-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.5;
|
||||||
|
writing-mode: initial;
|
||||||
|
}
|
||||||
|
.artifacts-pane.horizontal .artifacts-label {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orient-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.orient-btn:hover { opacity: 1; }
|
||||||
|
|
||||||
|
.artifacts-body {
|
||||||
|
display: flex;
|
||||||
|
opacity: 0.25;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
111
src/components/ContentLayout.vue
Normal file
111
src/components/ContentLayout.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<template>
|
||||||
|
<div class="content-layout" :class="direction" ref="containerRef">
|
||||||
|
|
||||||
|
<div v-if="isChatOpen" class="slot slot-chat">
|
||||||
|
<slot name="chat" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isDashboardOpen" class="slot slot-dashboard">
|
||||||
|
<slot name="dashboard" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Files + Artifacts share one "thin" wrapper.
|
||||||
|
In row mode: thin is a vertical strip on the right.
|
||||||
|
In col mode: thin is a horizontal bar at the bottom. -->
|
||||||
|
<div v-if="isThinVisible" class="slot slot-thin">
|
||||||
|
<div v-if="isFilesOpen" class="slot slot-files">
|
||||||
|
<slot name="files" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isArtifactsOpen" class="slot slot-artifacts">
|
||||||
|
<slot name="artifacts" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'ContentLayout' });
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||||
|
import { usePanels } from '../composables/usePanels';
|
||||||
|
|
||||||
|
const { isChatOpen, isDashboardOpen, isFilesOpen, isArtifactsOpen } = usePanels();
|
||||||
|
|
||||||
|
const isThinVisible = computed(() => isFilesOpen.value || isArtifactsOpen.value);
|
||||||
|
|
||||||
|
// --- Direction: row when landscape, col when portrait-ish ---
|
||||||
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
const direction = ref<'row' | 'col'>('row');
|
||||||
|
|
||||||
|
let ro: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
function onResize(entries: ResizeObserverEntry[]) {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (!entry) return;
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
// Flip to col when width is not at least 1.5× height
|
||||||
|
direction.value = width >= height * 1.5 ? 'row' : 'col';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!containerRef.value) return;
|
||||||
|
ro = new ResizeObserver(onResize);
|
||||||
|
ro.observe(containerRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
ro?.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content-layout {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Slots ─────────────────────────────────────────────── */
|
||||||
|
.slot {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Row mode: horizontal distribution ─────────────────── */
|
||||||
|
.content-layout.row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.content-layout.row .slot-chat { flex: 40 1 0; border-right: 1px solid var(--border); }
|
||||||
|
.content-layout.row .slot-dashboard { flex: 40 1 0; border-right: 1px solid var(--border); }
|
||||||
|
.content-layout.row .slot-thin { flex: 10 1 0; display: flex; flex-direction: column; }
|
||||||
|
.content-layout.row .slot-files { flex: 1 1 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.content-layout.row .slot-artifacts { flex: 1 1 0; }
|
||||||
|
|
||||||
|
/* Remove trailing border on last visible slot in row */
|
||||||
|
.content-layout.row .slot-thin:last-child,
|
||||||
|
.content-layout.row .slot-dashboard:last-child,
|
||||||
|
.content-layout.row .slot-chat:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Col mode: vertical distribution ───────────────────── */
|
||||||
|
.content-layout.col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.content-layout.col .slot-chat { flex: 40 1 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.content-layout.col .slot-dashboard { flex: 40 1 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.content-layout.col .slot-thin { flex: 10 1 0; display: flex; flex-direction: row; }
|
||||||
|
.content-layout.col .slot-files { flex: 1 1 0; border-right: 1px solid var(--border); }
|
||||||
|
.content-layout.col .slot-artifacts { flex: 1 1 0; }
|
||||||
|
|
||||||
|
/* Remove trailing border on last visible slot in col */
|
||||||
|
.content-layout.col .slot-thin:last-child,
|
||||||
|
.content-layout.col .slot-dashboard:last-child,
|
||||||
|
.content-layout.col .slot-chat:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
src/components/DashboardPane.vue
Normal file
44
src/components/DashboardPane.vue
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-pane">
|
||||||
|
<div class="dashboard-empty">
|
||||||
|
<span class="dashboard-label">Dashboard</span>
|
||||||
|
<span class="dashboard-hint">Artifacts and controls from the agent appear here</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'DashboardPane' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
.dashboard-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
.dashboard-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.dashboard-hint {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
src/components/FilesPane.vue
Normal file
115
src/components/FilesPane.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="files-pane" :class="orientation">
|
||||||
|
<div class="files-bar">
|
||||||
|
<span class="files-label">Files</span>
|
||||||
|
<button class="orient-btn" @click="toggleOrientation" :title="orientation === 'vertical' ? 'Switch to horizontal' : 'Switch to vertical'">
|
||||||
|
<component :is="orientation === 'vertical' ? Bars3Icon : Bars3BottomLeftIcon" class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="files-body">
|
||||||
|
<span class="files-hint">file tree</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'FilesPane' });
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Bars3Icon, Bars3BottomLeftIcon } from '@heroicons/vue/20/solid';
|
||||||
|
|
||||||
|
const orientation = ref<'vertical' | 'horizontal'>('vertical');
|
||||||
|
function toggleOrientation() {
|
||||||
|
orientation.value = orientation.value === 'vertical' ? 'horizontal' : 'vertical';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.files-pane {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical: bar on top, body below */
|
||||||
|
.files-pane.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.files-pane.vertical .files-bar {
|
||||||
|
flex-direction: row;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.files-pane.vertical .files-body {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal: bar on left, body to the right */
|
||||||
|
.files-pane.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.files-pane.horizontal .files-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 8px 0;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
.files-pane.horizontal .files-body {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--bg-dim, var(--bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
opacity: 0.5;
|
||||||
|
writing-mode: initial;
|
||||||
|
}
|
||||||
|
.files-pane.horizontal .files-label {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orient-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.12s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.orient-btn:hover { opacity: 1; }
|
||||||
|
|
||||||
|
.files-body {
|
||||||
|
display: flex;
|
||||||
|
opacity: 0.25;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -15,7 +15,7 @@ import { ref, computed, type Component } from 'vue';
|
|||||||
import tenant from '../tenant';
|
import tenant from '../tenant';
|
||||||
|
|
||||||
// Top-level panes (columns)
|
// Top-level panes (columns)
|
||||||
export type PaneId = 'chat' | 'workspace';
|
export type PaneId = 'chat' | 'dashboard' | 'files' | 'artifacts';
|
||||||
// Debug panels (stacked inside the debug column)
|
// Debug panels (stacked inside the debug column)
|
||||||
export type PanelId = 'graph' | 'trace' | 'nodes' | 'awareness';
|
export type PanelId = 'graph' | 'trace' | 'nodes' | 'awareness';
|
||||||
export type PanelLocation = 'side' | 'bottom';
|
export type PanelLocation = 'side' | 'bottom';
|
||||||
@ -77,7 +77,7 @@ function saveSet(key: string, ids: Set<string>) {
|
|||||||
|
|
||||||
// Module-level state (survives HMR)
|
// Module-level state (survives HMR)
|
||||||
const openPanelIds = ref<Set<PanelId>>(loadSet<PanelId>(PANELS_KEY, []));
|
const openPanelIds = ref<Set<PanelId>>(loadSet<PanelId>(PANELS_KEY, []));
|
||||||
const openPaneIds = ref<Set<PaneId>>(loadSet<PaneId>(PANES_KEY, ['chat']));
|
const openPaneIds = ref<Set<PaneId>>(loadSet<PaneId>(PANES_KEY, ['chat', 'dashboard']));
|
||||||
|
|
||||||
export function usePanels() {
|
export function usePanels() {
|
||||||
// Current role
|
// Current role
|
||||||
@ -108,8 +108,10 @@ export function usePanels() {
|
|||||||
const hasDebugColumn = hasSidePanels;
|
const hasDebugColumn = hasSidePanels;
|
||||||
|
|
||||||
// Pane state
|
// Pane state
|
||||||
const isChatOpen = computed(() => openPaneIds.value.has('chat'));
|
const isChatOpen = computed(() => openPaneIds.value.has('chat'));
|
||||||
const isWorkspaceOpen = computed(() => openPaneIds.value.has('workspace'));
|
const isDashboardOpen = computed(() => openPaneIds.value.has('dashboard'));
|
||||||
|
const isFilesOpen = computed(() => openPaneIds.value.has('files'));
|
||||||
|
const isArtifactsOpen = computed(() => openPaneIds.value.has('artifacts'));
|
||||||
|
|
||||||
function togglePanel(id: PanelId) {
|
function togglePanel(id: PanelId) {
|
||||||
const next = new Set(openPanelIds.value);
|
const next = new Set(openPanelIds.value);
|
||||||
@ -158,7 +160,9 @@ export function usePanels() {
|
|||||||
openPanelIds,
|
openPanelIds,
|
||||||
// Top-level panes
|
// Top-level panes
|
||||||
isChatOpen,
|
isChatOpen,
|
||||||
isWorkspaceOpen,
|
isDashboardOpen,
|
||||||
|
isFilesOpen,
|
||||||
|
isArtifactsOpen,
|
||||||
togglePane,
|
togglePane,
|
||||||
isPaneOpen,
|
isPaneOpen,
|
||||||
openPaneIds,
|
openPaneIds,
|
||||||
|
|||||||
@ -24,33 +24,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3-column layout: chat | workspace | debug -->
|
<!-- Main layout: content unit + debug column -->
|
||||||
<div v-show="!showPicker" class="pane-layout">
|
<div v-show="!showPicker" class="pane-layout">
|
||||||
<!-- Chat column -->
|
|
||||||
<ChatPane
|
|
||||||
v-if="isChatOpen"
|
|
||||||
ref="chatPaneRef"
|
|
||||||
class="pane-column"
|
|
||||||
:connected="connected"
|
|
||||||
:selectedAgent="selectedAgent"
|
|
||||||
:selectedMode="selectedMode"
|
|
||||||
:allAgents="allAgents"
|
|
||||||
:defaultAgent="defaultAgent || ''"
|
|
||||||
:wsSend="wsSend"
|
|
||||||
:viewActive="viewActive"
|
|
||||||
@hud-update="onHudUpdate"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Workspace column -->
|
<!-- Content unit: Chat · Dashboard · Files · Artifacts -->
|
||||||
<WorkspacePane
|
<ContentLayout class="pane-content">
|
||||||
v-if="isWorkspaceOpen"
|
<template #chat>
|
||||||
class="pane-column"
|
<ChatPane
|
||||||
/>
|
ref="chatPaneRef"
|
||||||
|
:connected="connected"
|
||||||
|
:selectedAgent="selectedAgent"
|
||||||
|
:selectedMode="selectedMode"
|
||||||
|
:allAgents="allAgents"
|
||||||
|
:defaultAgent="defaultAgent || ''"
|
||||||
|
:wsSend="wsSend"
|
||||||
|
:viewActive="viewActive"
|
||||||
|
@hud-update="onHudUpdate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #dashboard>
|
||||||
|
<DashboardPane />
|
||||||
|
</template>
|
||||||
|
<template #files>
|
||||||
|
<FilesPane />
|
||||||
|
</template>
|
||||||
|
<template #artifacts>
|
||||||
|
<ArtifactsPane />
|
||||||
|
</template>
|
||||||
|
</ContentLayout>
|
||||||
|
|
||||||
<!-- Debug column (stacked panels) -->
|
<!-- Debug column (stacked panels, fixed width) -->
|
||||||
<DebugColumn
|
<DebugColumn
|
||||||
v-if="hasDebugColumn"
|
v-if="hasDebugColumn"
|
||||||
class="pane-column pane-debug"
|
class="pane-debug"
|
||||||
:hudTree="hudState.hudTree"
|
:hudTree="hudState.hudTree"
|
||||||
:hudVersion="hudState.hudVersion"
|
:hudVersion="hudState.hudVersion"
|
||||||
:connected="hudState.connected"
|
:connected="hudState.connected"
|
||||||
@ -72,7 +78,10 @@ import { ws, auth, agents } from '../store';
|
|||||||
import { usePanels } from '../composables/usePanels';
|
import { usePanels } from '../composables/usePanels';
|
||||||
import { LockClosedIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
import { LockClosedIcon, UserGroupIcon } from '@heroicons/vue/20/solid';
|
||||||
import ChatPane from '../components/ChatPane.vue';
|
import ChatPane from '../components/ChatPane.vue';
|
||||||
import WorkspacePane from '../components/WorkspacePane.vue';
|
import ContentLayout from '../components/ContentLayout.vue';
|
||||||
|
import DashboardPane from '../components/DashboardPane.vue';
|
||||||
|
import FilesPane from '../components/FilesPane.vue';
|
||||||
|
import ArtifactsPane from '../components/ArtifactsPane.vue';
|
||||||
import DebugColumn from '../components/DebugColumn.vue';
|
import DebugColumn from '../components/DebugColumn.vue';
|
||||||
|
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
@ -80,7 +89,7 @@ import router from '../router';
|
|||||||
const agentsRoute = useRoute();
|
const agentsRoute = useRoute();
|
||||||
|
|
||||||
// Pane visibility
|
// Pane visibility
|
||||||
const { isChatOpen, isWorkspaceOpen, hasDebugColumn } = usePanels();
|
const { hasDebugColumn } = usePanels();
|
||||||
|
|
||||||
// Auth + agents
|
// Auth + agents
|
||||||
const { connected, send: wsSend } = ws;
|
const { connected, send: wsSend } = ws;
|
||||||
@ -146,29 +155,27 @@ watch([selectedAgent, selectedMode], ([agent, mode], [oldAgent, oldMode]) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 3-column pane layout */
|
/* Outer layout: content unit + debug column */
|
||||||
.pane-layout {
|
.pane-layout {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-column {
|
/* Content unit fills all remaining space */
|
||||||
|
.pane-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.pane-column:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Debug column: fixed width */
|
/* Debug column: fixed width, separated by border */
|
||||||
.pane-debug {
|
.pane-debug {
|
||||||
flex: 0 0 320px;
|
flex: 0 0 320px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Agent picker (no agent selected) */
|
/* Agent picker (no agent selected) */
|
||||||
|
|||||||
Reference in New Issue
Block a user