Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,29 @@
Create Project
</button>
</div>
<VisualCommandCenter
:system-status="commandCenterSystemLabel"
:system-summary="systemSummaryText"
:provider-label="selectedProvider"
:installed-skills-count="installedSkills.length"
:active-task-status="activeTaskLabel"
:active-task-title="activeTaskTitle"
:active-task-location="activeTaskPathLabel"
:pending-requests="selectedThreadServerRequests.length"
:queued-messages="selectedThreadQueuedMessages.length"
:review-status="reviewStatusLabel"
:review-branch="reviewStatusValue"
:is-review-open="isReviewPaneOpen"
:alfred-status="alfredControlLabel"
:alfred-summary="telegramStatusText"
:send-mode="inProgressSendMode"
:send-with-enter="sendWithEnter"
:dictation-mode="dictationAutoSend ? 'Auto-send' : 'Manual send'"
:can-continue-task="Boolean(selectedThreadId)"
@new-thread="onStartNewThreadFromToolbar"
@continue-task="selectedThreadId && onSelectThread(selectedThreadId)"
@open-skills="router.push({ name: 'skills' })"
/>
<Teleport to="body">
<div v-if="isExistingFolderPickerOpen" class="new-thread-open-folder-overlay" @click.self="onCloseExistingFolderPanel">
<div class="new-thread-open-folder" role="dialog" aria-modal="true" aria-label="Select folder" @keydown.esc.prevent="onCloseExistingFolderPanel">
Expand Down Expand Up @@ -789,6 +812,7 @@ import ThreadComposer from './components/content/ThreadComposer.vue'
import ThreadPendingRequestPanel from './components/content/ThreadPendingRequestPanel.vue'
import QueuedMessages from './components/content/QueuedMessages.vue'
import RateLimitStatus from './components/content/RateLimitStatus.vue'
import VisualCommandCenter from './components/content/VisualCommandCenter.vue'
import ComposerDropdown from './components/content/ComposerDropdown.vue'
import ComposerRuntimeDropdown from './components/content/ComposerRuntimeDropdown.vue'
import SidebarThreadControls from './components/sidebar/SidebarThreadControls.vue'
Expand Down Expand Up @@ -1167,9 +1191,46 @@ const isHomeRoute = computed(() => route.name === 'home')
const isSkillsRoute = computed(() => route.name === 'skills')
const contentTitle = computed(() => {
if (isSkillsRoute.value) return 'Skills'
if (isHomeRoute.value) return 'New thread'
if (isHomeRoute.value) return 'Command Center'
return selectedThread.value?.title ?? 'Choose a thread'
})
const totalThreadCount = computed(() =>
projectGroups.value.reduce((sum, group) => sum + group.threads.length, 0),
)
function isRateLimitHot(windowState: UiRateLimitWindow | null | undefined): boolean {
return Boolean(windowState && typeof windowState.usedPercent === 'number' && windowState.usedPercent >= 85)
}
const commandCenterSystemLabel = computed(() => {
if (isLoadingThreads.value) return 'Syncing'
if (accountRateLimitSnapshots.value.some((snapshot) => isRateLimitHot(snapshot.primary) || isRateLimitHot(snapshot.secondary))) {
return 'Constrained'
}
return 'Ready'
})
const systemSummaryText = computed(() => `${totalThreadCount.value} threads across ${projectGroups.value.length} projects`)
const activeTaskLabel = computed(() => {
if (!selectedThread.value) return 'Idle'
if (selectedThread.value.inProgress) return 'Running'
if (selectedThreadServerRequests.value.length > 0) return 'Awaiting input'
if (selectedThreadQueuedMessages.value.length > 0) return 'Queued'
return 'Ready'
})
const activeTaskTitle = computed(() => selectedThread.value?.title?.trim() || 'No active thread selected')
const activeTaskPathLabel = computed(() => {
const cwd = selectedThread.value?.cwd?.trim() || newThreadCwd.value.trim()
return cwd || 'No workspace selected'
})
const reviewStatusLabel = computed(() => {
if (isReviewPaneOpen.value) return 'Review open'
if (currentThreadBranch.value?.trim()) return 'Branch loaded'
return 'Waiting'
})
const reviewStatusValue = computed(() => currentThreadBranch.value?.trim() || 'No review branch selected')
const alfredControlLabel = computed(() => {
if (telegramStatus.value.active) return 'Online'
if (telegramStatus.value.configured) return 'Configured'
return 'Local'
})
const browserHostName =
typeof window !== 'undefined'
? (window.location.hostname || window.location.host || 'codexui')
Expand Down Expand Up @@ -3406,7 +3467,7 @@ async function loadWorktreeBranches(sourceCwd: string): Promise<void> {
}

.new-thread-empty {
@apply flex-1 min-h-0 flex flex-col items-center justify-center gap-0.5 px-3 sm:px-6;
@apply flex-1 min-h-0 flex flex-col items-center justify-start gap-4 px-3 py-3 sm:px-6 sm:py-4;
}

.new-thread-hero {
Expand Down
182 changes: 182 additions & 0 deletions src/components/content/VisualCommandCenter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<template>
<section class="command-center" aria-label="Visual Command Center">
<div class="command-center-header">
<div>
<p class="command-center-eyebrow">Visual Command Center</p>
<h2 class="command-center-title">System state at a glance</h2>
</div>
<p class="command-center-meta">{{ systemStatus }}</p>
</div>

<div class="command-center-grid">
<article class="command-card command-card-system">
<div class="command-card-header">
<p class="command-card-label">System Status</p>
<span class="command-card-status">{{ systemStatus }}</span>
</div>
<p class="command-card-value">{{ systemSummary }}</p>
<ul class="command-card-list">
<li>Provider: {{ providerLabel }}</li>
<li>Skills loaded: {{ installedSkillsCount }}</li>
</ul>
</article>

<article class="command-card command-card-task">
<div class="command-card-header">
<p class="command-card-label">Active Task</p>
<span class="command-card-status">{{ activeTaskStatus }}</span>
</div>
<p class="command-card-value">{{ activeTaskTitle }}</p>
<ul class="command-card-list">
<li>{{ activeTaskLocation }}</li>
<li>Pending requests: {{ pendingRequests }}</li>
<li>Queued follow-ups: {{ queuedMessages }}</li>
</ul>
</article>

<article class="command-card command-card-review">
<div class="command-card-header">
<p class="command-card-label">Review Status</p>
<span class="command-card-status">{{ reviewStatus }}</span>
</div>
<p class="command-card-value">{{ reviewBranch }}</p>
<ul class="command-card-list">
<li>Review pane: {{ isReviewOpen ? 'Open' : 'Closed' }}</li>
</ul>
</article>

<article class="command-card command-card-control">
<div class="command-card-header">
<p class="command-card-label">Alfred Control</p>
<span class="command-card-status">{{ alfredStatus }}</span>
</div>
<p class="command-card-value">{{ alfredSummary }}</p>
<ul class="command-card-list">
<li>Send mode: {{ sendMode }}</li>
<li>Enter sends: {{ sendWithEnter ? 'On' : 'Cmd+Enter required' }}</li>
<li>Dictation: {{ dictationMode }}</li>
</ul>
<div class="command-card-actions">
<button class="command-card-action command-card-action-primary" type="button" @click="$emit('new-thread')">
New thread
</button>
<button class="command-card-action" type="button" :disabled="!canContinueTask" @click="$emit('continue-task')">
Continue task
</button>
<button class="command-card-action" type="button" @click="$emit('open-skills')">
Skills hub
</button>
</div>
</article>
</div>
</section>
</template>

<script setup lang="ts">
defineProps<{
systemStatus: string
systemSummary: string
providerLabel: string
installedSkillsCount: number
activeTaskStatus: string
activeTaskTitle: string
activeTaskLocation: string
pendingRequests: number
queuedMessages: number
reviewStatus: string
reviewBranch: string
isReviewOpen: boolean
alfredStatus: string
alfredSummary: string
sendMode: string
sendWithEnter: boolean
dictationMode: string
canContinueTask: boolean
}>()

defineEmits<{
'new-thread': []
'continue-task': []
'open-skills': []
}>()
</script>

<style scoped>
@reference "tailwindcss";

.command-center {
@apply w-full max-w-7xl rounded-[1.5rem] border border-zinc-200 bg-zinc-50/90 px-3 py-3 sm:px-4 sm:py-4 xl:px-6 xl:py-5;
}

.command-center-header {
@apply flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between xl:gap-6;
}

.command-center-eyebrow {
@apply m-0 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-500;
}

.command-center-title {
@apply mt-1 text-base font-medium tracking-tight text-zinc-950 sm:text-lg xl:text-[1.35rem];
}

.command-center-meta {
@apply m-0 inline-flex items-center self-start rounded-full bg-white px-3 py-1 text-xs font-medium text-zinc-600 xl:px-4 xl:py-1.5;
}

.command-center-grid {
@apply mt-3 grid gap-2.5 sm:grid-cols-2 xl:mt-5 xl:grid-cols-12 xl:gap-4;
}

.command-card {
@apply flex min-h-32 flex-col gap-2 rounded-[1.25rem] border border-zinc-200 bg-white p-3 shadow-sm xl:min-h-[16rem] xl:gap-3 xl:rounded-[1.5rem] xl:p-5;
}

.command-card-system {
@apply xl:col-span-3;
}

.command-card-task {
@apply xl:col-span-4;
}

.command-card-review {
@apply xl:col-span-2;
}

.command-card-control {
@apply sm:col-span-2 xl:col-span-3;
}

.command-card-header {
@apply flex items-center justify-between gap-2 xl:gap-3;
}

.command-card-label {
@apply m-0 text-[11px] font-semibold uppercase tracking-[0.14em] text-zinc-500;
}

.command-card-status {
@apply inline-flex items-center rounded-full bg-zinc-100 px-2 py-1 text-[11px] font-medium text-zinc-700 xl:px-2.5;
}

.command-card-value {
@apply m-0 text-sm font-medium leading-5 text-zinc-950 xl:text-base xl:leading-6;
}

.command-card-list {
@apply m-0 flex list-none flex-col gap-1.5 p-0 text-sm leading-5 text-zinc-600 xl:gap-2 xl:text-[0.95rem] xl:leading-6;
}

.command-card-actions {
@apply mt-auto flex flex-wrap gap-2 pt-1 xl:gap-2.5 xl:pt-3;
}

.command-card-action {
@apply inline-flex h-8.5 items-center justify-center rounded-full border border-zinc-200 bg-white px-3.5 text-sm font-medium text-zinc-700 transition hover:bg-zinc-50 disabled:cursor-default disabled:opacity-60 xl:h-10 xl:px-4;
}

.command-card-action-primary {
@apply border-zinc-900 bg-zinc-900 text-white hover:bg-zinc-800;
}
</style>