mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
Remove the duplicate progress bar and pulsing status dot. Reduce wave geometry to 190 by 72 and inherit existing light/dark accent colors. Record the provider-agnostic native CLI boundary in AGENTS.md. Checks: frontend 375 tests; web and desktop builds; desktop pack; Electron first-run and returning-user checks with animated loader, light/dark colors and a 560px window.
1136 lines
35 KiB
Vue
1136 lines
35 KiB
Vue
<template>
|
|
<main
|
|
class="app-shell"
|
|
:class="{
|
|
'sidebar-collapsed': sidebarCollapsed,
|
|
'trace-panel-open': tracePanelOpen,
|
|
}"
|
|
:data-theme="theme"
|
|
:data-platform="desktopPlatform"
|
|
:aria-busy="agentDiscoveryPending ? 'true' : undefined"
|
|
>
|
|
<section v-if="booting" class="boot-state" aria-live="polite">
|
|
<img class="boot-logo" :src="productAppIcon" alt="Meldwork" />
|
|
<p>{{ t('app.loading') }}</p>
|
|
<WavePhysicsLoader :theme="theme" />
|
|
</section>
|
|
|
|
<section v-else-if="bridgeMissing" class="bridge-state">
|
|
<img class="bridge-logo" :src="productAppIcon" alt="Meldwork" />
|
|
<h1>{{ t('app.desktopRequired') }}</h1>
|
|
<p>{{ t('app.desktopRequiredDetail') }}</p>
|
|
</section>
|
|
|
|
<template v-else>
|
|
<WindowTitlebar ref="windowTitlebar" v-model:shortcut-menu-open="shortcutMenuOpen"
|
|
:t="t" :theme="theme" :active-view="activeView" :blocked="contentInteractionBlocked"
|
|
:open-system-settings="openSystemSettings" :toggle-locale="toggleLocale" :toggle-theme="toggleTheme"
|
|
:shortcut-definitions="shortcutDefinitions" />
|
|
<transition name="agent-discovery" appear>
|
|
<section
|
|
v-if="agentDiscoveryPending"
|
|
class="agent-discovery-overlay"
|
|
role="status"
|
|
aria-live="polite"
|
|
:aria-label="t('agentDiscovery.title')"
|
|
>
|
|
<PixelBlast :theme="theme" />
|
|
<div class="agent-discovery-content">
|
|
<img class="agent-discovery-mark" :src="productMark" alt="" />
|
|
<div class="agent-discovery-copy">
|
|
<p class="agent-discovery-kicker">{{ t('agentDiscovery.kicker') }}</p>
|
|
<h1>{{ t('agentDiscovery.title') }}</h1>
|
|
<p>{{ t('agentDiscovery.body') }}</p>
|
|
<div class="agent-discovery-status">
|
|
<span class="agent-discovery-status-dot" aria-hidden="true" />
|
|
<span>{{ t('agentDiscovery.status') }}</span>
|
|
</div>
|
|
</div>
|
|
<WavePhysicsLoader :theme="theme" />
|
|
</div>
|
|
</section>
|
|
</transition>
|
|
|
|
<WorkspaceSidebar :controller="workspaceSidebarController" />
|
|
|
|
<section
|
|
class="workspace-pane"
|
|
:inert="contentInteractionBlocked ? '' : undefined"
|
|
:aria-hidden="contentInteractionBlocked ? 'true' : undefined"
|
|
>
|
|
<SystemSettingsView
|
|
v-if="activeView === 'settings'"
|
|
:controller="systemSettingsController"
|
|
/>
|
|
|
|
<HomeDashboard
|
|
v-else-if="activeView === 'home'"
|
|
:agent-description="agentDescription"
|
|
:awaiting-decision-gates="awaitingDecisionGates"
|
|
:discovering="agentDiscoveryPending"
|
|
:group-name="groupName"
|
|
:groups="snapshot.groups"
|
|
:installed-count="installedCount"
|
|
:is-group-awaiting-decision="isGroupAwaitingDecision"
|
|
:is-group-running="isGroupRunning"
|
|
:onboarding-completed="onboardingCompleted"
|
|
:ready-agents="readyAgents"
|
|
:recent-group-meta="recentGroupMeta"
|
|
:refreshing="refreshing"
|
|
:theme="theme"
|
|
@open-agent-manager="openAgentManager"
|
|
@open-direct="openDirect"
|
|
@open-new-group="openNewGroup"
|
|
@refresh-agents="refreshAgents"
|
|
@select-group="selectGroup"
|
|
/>
|
|
|
|
<section v-else class="conversation-pane">
|
|
<ConversationHeader ref="conversationHeader" :controller="conversationHeaderController" />
|
|
|
|
<ConversationTimelineView :controller="timelineController" />
|
|
|
|
<ConversationComposer :controller="composerController" />
|
|
</section>
|
|
</section>
|
|
|
|
<transition name="trace-panel">
|
|
<RunTracePanel
|
|
v-if="tracePanelOpen"
|
|
ref="tracePanel"
|
|
:open="tracePanelOpen"
|
|
:drawer="tracePanelDrawer"
|
|
:human-gate-decision-pending-ids="humanGateDecisionPendingIds"
|
|
:human-gates="tracePanelHumanGates"
|
|
:items="tracePanelItems"
|
|
:selected-agent-run-id="selectedTraceAgentRunId"
|
|
:theme="theme"
|
|
:waiting="tracePanelWaiting"
|
|
@close="closeTracePanel"
|
|
@decide-human-gate="decideHumanGate"
|
|
@select="selectTraceAgentRun"
|
|
@jump-source="jumpToTraceSource"
|
|
/>
|
|
</transition>
|
|
|
|
<transition name="onboarding" appear>
|
|
<div v-if="onboardingVisible" class="onboarding-transition-shell">
|
|
<OnboardingDialog
|
|
ref="onboardingDialog"
|
|
:controller="onboardingDialogController"
|
|
/>
|
|
</div>
|
|
</transition>
|
|
|
|
<transition name="modal-backdrop" appear>
|
|
<div v-if="modal" class="modal-backdrop" @mousedown.self="closeModal">
|
|
<transition name="modal-pop" appear>
|
|
<section
|
|
ref="modalDialog"
|
|
class="modal"
|
|
:class="{
|
|
medium: ['new-group', 'settings', 'custom-agent'].includes(modal),
|
|
'agent-detail-modal': modal === 'agent-detail',
|
|
'unlimited-confirm-modal': modal === 'unlimited-confirm',
|
|
}"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title"
|
|
tabindex="-1"
|
|
>
|
|
<header class="modal-header">
|
|
<div>
|
|
<h2 id="modal-title">{{ modalTitle }}</h2>
|
|
<p v-if="modalSubtitle">{{ modalSubtitle }}</p>
|
|
</div>
|
|
<button class="icon-button" type="button" :aria-label="t('common.close')" :disabled="saving" @click="closeModal">
|
|
<CloseOutline />
|
|
</button>
|
|
</header>
|
|
|
|
<WorkspaceModalContent
|
|
ref="workspaceModalContent"
|
|
:controller="workspaceModalController"
|
|
/>
|
|
</section>
|
|
</transition>
|
|
</div>
|
|
</transition>
|
|
|
|
<transition name="toast">
|
|
<div
|
|
v-if="toastMessage"
|
|
class="toast-message"
|
|
:data-tone="toastTone"
|
|
:role="toastTone === 'error' ? 'alert' : 'status'"
|
|
:aria-live="toastTone === 'error' ? 'assertive' : 'polite'"
|
|
@mouseenter="pauseToast"
|
|
@mouseleave="resumeToast"
|
|
@focusin="pauseToast"
|
|
@focusout="resumeToast"
|
|
>
|
|
<AlertCircleOutline v-if="toastTone === 'error'" />
|
|
<WarningOutline v-else />
|
|
<span>{{ toastMessage }}</span>
|
|
<button
|
|
class="toast-dismiss-button"
|
|
type="button"
|
|
:title="t('common.dismiss')"
|
|
:aria-label="t('common.dismiss')"
|
|
@click="dismissToast"
|
|
>
|
|
<CloseOutline />
|
|
</button>
|
|
</div>
|
|
</transition>
|
|
<transition name="copy-toast">
|
|
<div v-if="copyToastMessage" class="copy-toast-message" role="status" aria-live="polite">
|
|
<CheckmarkCircleOutline />
|
|
<span>{{ copyToastMessage }}</span>
|
|
</div>
|
|
</transition>
|
|
</template>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import PixelBlast from './components/PixelBlast.vue'
|
|
import WavePhysicsLoader from './components/WavePhysicsLoader.vue'
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
import {
|
|
AlertCircleOutline,
|
|
CheckmarkCircleOutline,
|
|
CloseOutline,
|
|
WarningOutline,
|
|
} from '@vicons/ionicons5'
|
|
import ConversationComposer from './components/ConversationComposer.vue'
|
|
import WindowTitlebar from './components/WindowTitlebar.vue'
|
|
import ConversationHeader from './components/ConversationHeader.vue'
|
|
import ConversationTimelineView from './components/ConversationTimelineView.vue'
|
|
import HomeDashboard from './components/HomeDashboard.vue'
|
|
import OnboardingDialog from './components/OnboardingDialog.vue'
|
|
import RunTracePanel from './components/RunTracePanel.vue'
|
|
import SystemSettingsView from './components/SystemSettingsView.vue'
|
|
import WorkspaceModalContent from './components/WorkspaceModalContent.vue'
|
|
import WorkspaceSidebar from './components/WorkspaceSidebar.vue'
|
|
import { agentLabel, setCloudAgentProfiles, setCustomAgentProfiles } from './catalog.js'
|
|
import { createConversationControllers } from './conversationControllers.js'
|
|
import { useAgentCatalog } from './composables/useAgentCatalog.js'
|
|
import { useAgentManagement } from './composables/useAgentManagement.js'
|
|
import { useAgentRefresh } from './composables/useAgentRefresh.js'
|
|
import { useAgentSkills } from './composables/useAgentSkills.js'
|
|
import { useAppNotifications } from './composables/useAppNotifications.js'
|
|
import { useAppPreferences } from './composables/useAppPreferences.js'
|
|
import { useComposerAttachments } from './composables/useComposerAttachments.js'
|
|
import {
|
|
normalizeSkill,
|
|
useComposerContext,
|
|
} from './composables/useComposerContext.js'
|
|
import { useCollapsedGroupMenu } from './composables/useCollapsedGroupMenu.js'
|
|
import { useConversationActions } from './composables/useConversationActions.js'
|
|
import { useConversationExecution } from './composables/useConversationExecution.js'
|
|
import { useConversationTimeline } from './composables/useConversationTimeline.js'
|
|
import { useConversationNavigation } from './composables/useConversationNavigation.js'
|
|
import { useConversationViewport } from './composables/useConversationViewport.js'
|
|
import { useDesktopWorkspaceLifecycle } from './composables/useDesktopWorkspaceLifecycle.js'
|
|
import { useEmptyShowcasePlayback } from './composables/useEmptyShowcasePlayback.js'
|
|
import { useAppWindowInteractions } from './composables/useAppWindowInteractions.js'
|
|
import { useCloudAgentBridges } from './composables/useCloudAgentBridges.js'
|
|
import { useKnowledgeBaseSettings } from './composables/useKnowledgeBaseSettings.js'
|
|
import { useMessageActions } from './composables/useMessageActions.js'
|
|
import { useOnboarding } from './composables/useOnboarding.js'
|
|
import { useOverlayLifecycle } from './composables/useOverlayLifecycle.js'
|
|
import { useRunFinishedNotifications } from './composables/useRunFinishedNotifications.js'
|
|
import { useSystemSettingsNavigation } from './composables/useSystemSettingsNavigation.js'
|
|
import { useWorkspaceNavigationState } from './composables/useWorkspaceNavigationState.js'
|
|
import { desktopApi, emptySnapshot, normalizeSnapshot } from './desktop.js'
|
|
import { locale, t, translateError, translateSystemMessage } from './i18n.js'
|
|
import { createWorkspaceControllers } from './workspaceControllers.js'
|
|
|
|
const snapshot = ref(emptySnapshot())
|
|
const shortcutDefinitions = Object.freeze([
|
|
{ labelKey: 'shortcut.toggleSidebar', keys: '⌘ B' },
|
|
{ labelKey: 'shortcut.newGroup', keys: '⌘ G' },
|
|
{ labelKey: 'shortcut.previousConversation', keys: '⌘ [' },
|
|
{ labelKey: 'shortcut.nextConversation', keys: '⌘ ]' },
|
|
{ labelKey: 'shortcut.openAgents', keys: '⌘ ,' },
|
|
])
|
|
const READ_ONLY_ENFORCED_AGENT_KINDS = new Set([
|
|
'codex', 'hermes', 'openclaw', 'workbuddy', 'zcode', 'pi', 'kimi', 'mimo', 'claude', 'qwen', 'gemini',
|
|
'opencode', 'opencodereview',
|
|
])
|
|
const installCatalog = ref({ platform: '', agents: [] })
|
|
const installerState = ref({ taskId: '', kind: '', phase: 'idle', canCancel: false, errorCode: '' })
|
|
const systemSettingsSection = ref('agents')
|
|
const sidebarCollapsed = ref(false)
|
|
const sidebarAgentsCollapsed = ref(false)
|
|
const sidebarGroupsCollapsed = ref(false)
|
|
const refreshing = ref(false)
|
|
const agentDiscoveryPending = ref(false)
|
|
const sendingGroupIds = ref(new Set())
|
|
const saving = ref(false)
|
|
const humanGateDecisionPendingIds = ref([])
|
|
const modal = ref('')
|
|
const composerContextVersion = ref(0)
|
|
const DEFAULT_MAX_ROUNDS = 6
|
|
const maxRounds = ref(DEFAULT_MAX_ROUNDS)
|
|
const unlimitedRounds = ref(false)
|
|
const unlimitedConfirmationGroupId = ref('')
|
|
const roundSettingsByGroupId = new Map()
|
|
const roundSettingsOpen = ref(false)
|
|
const formError = ref('')
|
|
const customAgentDeleteArmed = ref(false)
|
|
const installConfirmKind = ref('')
|
|
const focusedAgentKind = ref('')
|
|
const shortcutMenuOpen = ref(false)
|
|
const conversationHeader = ref(null)
|
|
const windowTitlebar = ref(null)
|
|
const workspaceModalContent = ref(null)
|
|
const onboardingDialog = ref(null)
|
|
const modalDialog = ref(null)
|
|
const roundSettingsControl = ref(null)
|
|
let systemSettingsNavigation = null
|
|
const {
|
|
copyToastMessage,
|
|
dismissToast,
|
|
notify,
|
|
pauseToast,
|
|
resumeToast,
|
|
showCopyToast,
|
|
showError,
|
|
toastMessage,
|
|
toastTone,
|
|
} = useAppNotifications()
|
|
const collapsedGroupMenuController = useCollapsedGroupMenu({ sidebarCollapsed, shortcutMenuOpen })
|
|
const {
|
|
collapsedGroupMenu,
|
|
collapsedGroupMenuButton,
|
|
collapsedGroupMenuOpen,
|
|
closeCollapsedGroupMenu,
|
|
} = collapsedGroupMenuController
|
|
const api = computed(() => desktopApi())
|
|
const conversationTitleBlock = computed(() => ({
|
|
focus: () => conversationHeader.value?.focusTitleBlock(),
|
|
}))
|
|
const desktopPlatform = computed(() => api.value?.platform || installCatalog.value.platform || '')
|
|
const workspace = computed(() => api.value?.localWorkspace || null)
|
|
const installer = computed(() => api.value?.agentInstaller || null)
|
|
const customAgent = computed(() => api.value?.customAgent || null)
|
|
const knowledgeBase = computed(() => api.value?.localKnowledgeBase || null)
|
|
const cloudAgentBridge = computed(() => api.value?.cloudAgentBridge || null)
|
|
const attachmentsApi = computed(() => api.value?.localAttachments || null)
|
|
const {
|
|
clearOnboardingPlayback,
|
|
completeOnboardingState,
|
|
ensureOnboardingPlayback,
|
|
hasPersistedWorkspaceActivity,
|
|
onboardingCompleted,
|
|
onboardingIndex,
|
|
onboardingLoadingLabel,
|
|
onboardingReady,
|
|
onboardingSlide,
|
|
onboardingSlides,
|
|
onboardingVisible,
|
|
openOnboarding,
|
|
selectOnboardingSlide,
|
|
} = useOnboarding({ refreshAgents: (...args) => refreshAgents(...args) })
|
|
const onboardingDialogController = {
|
|
completeOnboarding: (...args) => completeOnboarding(...args),
|
|
onboardingIndex,
|
|
onboardingLoadingLabel,
|
|
onboardingReady,
|
|
onboardingSlide,
|
|
onboardingSlides,
|
|
selectOnboardingSlide,
|
|
t,
|
|
}
|
|
const knowledgeBaseSettings = useKnowledgeBaseSettings({ knowledgeBase, showError })
|
|
const {
|
|
knowledgeBaseName,
|
|
knowledgeBaseReady,
|
|
loadKnowledgeBaseStatuses,
|
|
localKnowledgeBaseEntries,
|
|
} = knowledgeBaseSettings
|
|
const {
|
|
applyTheme,
|
|
productAppIcon,
|
|
productMark,
|
|
productWordmark,
|
|
setThemePreference,
|
|
theme,
|
|
themePreference,
|
|
toggleLocale,
|
|
toggleTheme,
|
|
} = useAppPreferences()
|
|
const conversationNavigation = useConversationNavigation({ agentLabel, locale, snapshot, t })
|
|
const {
|
|
directGroupsFor,
|
|
groupName,
|
|
isDirectCreationPending,
|
|
recentGroupMeta,
|
|
setDirectCreationPending,
|
|
setSidebarAgentExpanded,
|
|
sortByUpdated,
|
|
toggleSidebarAgentExpanded,
|
|
} = conversationNavigation
|
|
const workspaceNavigationState = useWorkspaceNavigationState({
|
|
clearSidebarDeleteState: () => { sidebarDeleteGroupId.value = '' },
|
|
closeCollapsedGroupMenu,
|
|
closeTracePanel: (...args) => closeTracePanel(...args),
|
|
directGroupsFor,
|
|
flushPendingRunFinishedEvents: (...args) => flushPendingRunFinishedEvents(...args),
|
|
hasFinishedDirectRun: (...args) => hasFinishedDirectRun(...args),
|
|
isDirectCreationPending,
|
|
openAgentManager: (...args) => openAgentManager(...args),
|
|
openDirect: (...args) => openDirect(...args),
|
|
preloadAgentSkills: (...args) => preloadAgentSkills(...args),
|
|
setFinishedDirectRun: (...args) => setFinishedDirectRun(...args),
|
|
setSidebarAgentExpanded,
|
|
sidebarCollapsed,
|
|
snapshot,
|
|
toggleSidebarAgentExpanded,
|
|
})
|
|
const {
|
|
activeGroup,
|
|
activeGroupMemberSignature,
|
|
activeView,
|
|
goHome,
|
|
handleAgentPrimary,
|
|
handleOpenGroup,
|
|
handleSidebarAgentMain,
|
|
isGroupRunning,
|
|
selectGroup,
|
|
selectedGroupId,
|
|
} = workspaceNavigationState
|
|
const sending = computed(() => (
|
|
Boolean(selectedGroupId.value && sendingGroupIds.value.has(selectedGroupId.value))
|
|
))
|
|
const sidebarDeleteGroup = computed(() => snapshot.value.groups.find(group => group.id === sidebarDeleteGroupId.value) || null)
|
|
const sidebarDeletePopoverStyle = computed(() => ({
|
|
left: `${sidebarDeletePopoverPoint.value.left}px`,
|
|
top: `${sidebarDeletePopoverPoint.value.top}px`,
|
|
}))
|
|
const blockingOverlayOpen = computed(() => Boolean(modal.value || onboardingVisible.value))
|
|
|
|
const agentCatalog = useAgentCatalog({
|
|
activeGroup,
|
|
directGroupsFor,
|
|
installCatalog,
|
|
snapshot,
|
|
t,
|
|
theme,
|
|
})
|
|
const {
|
|
agentDescription,
|
|
installedCount,
|
|
mergedCatalog,
|
|
readyAgentKinds,
|
|
readyAgentSignature,
|
|
readyAgents,
|
|
} = agentCatalog
|
|
const agentSkills = useAgentSkills({
|
|
installer,
|
|
mergedCatalog,
|
|
normalizeSkill,
|
|
readyAgents,
|
|
t,
|
|
})
|
|
const {
|
|
agentDetailSkillSummary,
|
|
agentSkillsSnapshot,
|
|
disposeAgentSkills,
|
|
invalidateAgentSkillCatalog,
|
|
loadAgentSkills,
|
|
loadAgentSkillStats,
|
|
preloadAgentSkills,
|
|
resetAgentDetailSkills,
|
|
selectAgentDetail,
|
|
selectedAgentDetail,
|
|
} = agentSkills
|
|
const agentRefresh = useAgentRefresh({
|
|
installCatalog,
|
|
installer,
|
|
installerState,
|
|
invalidateAgentSkillCatalog,
|
|
loadAgentSkillStats,
|
|
readyAgentSignature,
|
|
refreshing,
|
|
showError,
|
|
snapshot,
|
|
workspace,
|
|
})
|
|
const { refreshAgents } = agentRefresh
|
|
const cloudAgentBridges = useCloudAgentBridges({
|
|
bridgeApi: cloudAgentBridge,
|
|
refreshAgents,
|
|
showError,
|
|
})
|
|
const agentManagement = useAgentManagement({
|
|
activeView,
|
|
closeModal,
|
|
customAgent,
|
|
customAgentDeleteArmed,
|
|
focusedAgentKind,
|
|
formError,
|
|
installConfirmKind,
|
|
installer,
|
|
installerState,
|
|
modal,
|
|
normalizeSnapshot,
|
|
refreshAgents,
|
|
saving,
|
|
selectAgentDetail,
|
|
selectedAgentDetail,
|
|
showError,
|
|
snapshot,
|
|
systemSettingsSection,
|
|
t,
|
|
translateError,
|
|
})
|
|
const conversationTimeline = useConversationTimeline({
|
|
activeGroup,
|
|
activeView,
|
|
blockingOverlayOpen,
|
|
conversationTitleBlock,
|
|
locale,
|
|
mergedCatalog,
|
|
selectedGroupId,
|
|
snapshot,
|
|
t,
|
|
translateSystemMessage,
|
|
})
|
|
const {
|
|
activeMessages,
|
|
activeRun,
|
|
activeRunTopicSignature,
|
|
closeTracePanel,
|
|
clearActiveTurn,
|
|
clearDeletedTurnState,
|
|
conversationEmptyVisible,
|
|
focusRunTopic,
|
|
hasFinishedDirectRun,
|
|
jumpToTraceSource,
|
|
liveOutputSignature,
|
|
rememberRunFinishedTurnStatus,
|
|
selectTraceAgentRun,
|
|
selectedTraceAgentRunId,
|
|
setFinishedDirectRun,
|
|
traceDrawerBlocking,
|
|
tracePanel,
|
|
tracePanelDrawer,
|
|
tracePanelItems,
|
|
tracePanelOpen,
|
|
tracePanelRunId,
|
|
} = conversationTimeline
|
|
const pendingHumanGates = computed(() => (
|
|
snapshot.value.humanGates.filter(gate => gate.status === 'pending')
|
|
))
|
|
// Conversations whose run is paused on a decision only the user can make.
|
|
const awaitingDecisionGates = computed(() => pendingHumanGates.value.map((gate) => {
|
|
const groupId = snapshot.value.runs.find(run => run.runId === gate.runId)?.groupId || ''
|
|
const group = snapshot.value.groups.find(item => item.id === groupId)
|
|
return group ? { ...gate, groupId, groupName: groupName(group) } : null
|
|
}).filter(Boolean))
|
|
const awaitingDecisionGroupIds = computed(() => new Set(awaitingDecisionGates.value.map(gate => gate.groupId)))
|
|
function isGroupAwaitingDecision(id) {
|
|
return awaitingDecisionGroupIds.value.has(id)
|
|
}
|
|
const activeRunHumanGates = computed(() => (
|
|
activeRun.value
|
|
? pendingHumanGates.value.filter(gate => gate.runId === activeRun.value.runId)
|
|
: []
|
|
))
|
|
const directHumanGates = computed(() => (
|
|
activeGroup.value?.conversationType === 'direct' ? activeRunHumanGates.value : []
|
|
))
|
|
const directRunBudget = computed(() => (
|
|
activeGroup.value?.conversationType === 'direct' ? activeRun.value?.budget || null : null
|
|
))
|
|
const tracePanelRun = computed(() => (
|
|
snapshot.value.runs.find(run => (
|
|
run.runId === tracePanelRunId.value && run.groupId === activeGroup.value?.id
|
|
)) || null
|
|
))
|
|
const tracePanelHumanGates = computed(() => (
|
|
activeGroup.value?.conversationType !== 'direct' && tracePanelRun.value
|
|
? pendingHumanGates.value.filter(gate => gate.runId === tracePanelRun.value.runId)
|
|
: []
|
|
))
|
|
const tracePanelWaiting = computed(() => (
|
|
tracePanelHumanGates.value.length > 0
|
|
&& tracePanelHumanGates.value.every(gate => tracePanelRun.value?.waitingGateIds.includes(gate.gateId))
|
|
))
|
|
const {
|
|
handleMessageScroll,
|
|
messageScroller,
|
|
scrollToLatest,
|
|
showScrollToLatest,
|
|
} = useConversationViewport({
|
|
activeMessages,
|
|
activeRunTopicSignature,
|
|
liveOutputSignature,
|
|
selectedGroupId,
|
|
})
|
|
const { index: emptyShowcaseIndex } = useEmptyShowcasePlayback({
|
|
visible: conversationEmptyVisible,
|
|
})
|
|
const { completeOnboarding, trapOverlayFocus } = useOverlayLifecycle({
|
|
blockingOverlayOpen,
|
|
clearOnboardingPlayback,
|
|
closeCollapsedGroupMenu,
|
|
closeModal,
|
|
closeTracePanel,
|
|
completeOnboardingState,
|
|
ensureOnboardingPlayback,
|
|
modal,
|
|
modalDialog,
|
|
onboardingDialog,
|
|
onboardingVisible,
|
|
saving,
|
|
tracePanelOpen,
|
|
})
|
|
const messageActions = useMessageActions({
|
|
activeGroup,
|
|
clearDeletedTurnState,
|
|
isGroupRunning,
|
|
notify,
|
|
showCopyToast,
|
|
showError,
|
|
showLatestResponseVersion: conversationTimeline.showLatestResponseVersion,
|
|
snapshot,
|
|
t,
|
|
workspace,
|
|
})
|
|
const {
|
|
clearPendingRunFinishedEvents,
|
|
flushPendingRunFinishedEvents,
|
|
handleRunFinished,
|
|
} = useRunFinishedNotifications({
|
|
hasFinishedDirectRun,
|
|
rememberRunFinishedTurnStatus,
|
|
setFinishedDirectRun,
|
|
snapshot,
|
|
})
|
|
const { messageDeleteArmedId } = messageActions
|
|
const contentInteractionBlocked = computed(() => (
|
|
blockingOverlayOpen.value || traceDrawerBlocking.value || agentDiscoveryPending.value
|
|
))
|
|
const composerContext = useComposerContext({
|
|
activeGroup,
|
|
activeRun,
|
|
agentDescription,
|
|
agentSkillsSnapshot,
|
|
knowledgeBaseName,
|
|
knowledgeBaseReady,
|
|
loadAgentSkills,
|
|
localKnowledgeBaseEntries,
|
|
mergedCatalog,
|
|
notify,
|
|
onSubmit: (...args) => sendMessage(...args),
|
|
preloadAgentSkills,
|
|
sending,
|
|
t,
|
|
theme,
|
|
})
|
|
systemSettingsNavigation = useSystemSettingsNavigation({
|
|
activeView,
|
|
closeModal,
|
|
focusedAgentKind,
|
|
formError,
|
|
installConfirmKind,
|
|
loadKnowledgeBaseStatuses,
|
|
modal,
|
|
saving,
|
|
systemSettingsSection,
|
|
})
|
|
const {
|
|
captureComposerContext,
|
|
clearComposerContext,
|
|
automaticTeamFormation,
|
|
composerMode,
|
|
composerSkippedKinds,
|
|
composerTargetKinds,
|
|
discussionMode,
|
|
disposeComposerContext,
|
|
draft,
|
|
pruneComposerDrafts,
|
|
resetComposerContext,
|
|
restoreComposerContext,
|
|
restoreComposerDraft,
|
|
scheduleComposerResize,
|
|
serializeComposerContext,
|
|
skillMenuOpen,
|
|
stashComposerDraft,
|
|
} = composerContext
|
|
const composerTargetsReady = computed(() => (
|
|
composerTargetKinds.value.length > 0
|
|
&& composerTargetKinds.value.every(kind => readyAgentKinds.value.has(kind))
|
|
))
|
|
const composerDisabled = computed(() => Boolean(activeRun.value) || sending.value)
|
|
const attachmentController = useComposerAttachments({
|
|
activeGroup,
|
|
attachmentsApi,
|
|
composerDisabled,
|
|
composerContextVersion,
|
|
composerTargetKinds,
|
|
mergedCatalog,
|
|
notify,
|
|
showError,
|
|
t,
|
|
})
|
|
const {
|
|
attachmentLimitMessage,
|
|
composerAttachmentSupported,
|
|
composerAttachments,
|
|
discardAttachments,
|
|
importingAttachment,
|
|
safeAttachmentPayload,
|
|
} = attachmentController
|
|
const conversationExecution = useConversationExecution({
|
|
activeGroup,
|
|
activeRun,
|
|
automaticTeamFormation,
|
|
attachmentLimitMessage,
|
|
captureComposerContext,
|
|
clearComposerContext,
|
|
composerAttachmentSupported,
|
|
composerAttachments,
|
|
composerContextVersion,
|
|
composerMode,
|
|
composerSkippedKinds,
|
|
composerTargetKinds,
|
|
composerTargetsReady,
|
|
discardAttachments,
|
|
draft,
|
|
importingAttachment,
|
|
maxRounds,
|
|
normalizeSnapshot,
|
|
notify,
|
|
readyAgentKinds,
|
|
restoreComposerContext,
|
|
roundSettingsOpen,
|
|
safeAttachmentPayload,
|
|
sending,
|
|
sendingGroupIds,
|
|
serializeComposerContext,
|
|
showError,
|
|
snapshot,
|
|
t,
|
|
unlimitedRounds,
|
|
workspace,
|
|
})
|
|
const { canSendMessage } = conversationExecution
|
|
|
|
function sendMessage(...args) {
|
|
return conversationExecution.sendMessage(...args)
|
|
}
|
|
|
|
function stopRun(...args) {
|
|
return conversationExecution.stopRun(...args)
|
|
}
|
|
|
|
async function decideHumanGate(payload = {}) {
|
|
const gate = pendingHumanGates.value.find(item => item.gateId === payload.gateId)
|
|
const option = gate?.options.find(item => item.optionId === payload.optionId)
|
|
const response = typeof payload.response === 'string' ? payload.response.trim() : ''
|
|
if (!gate || !option || humanGateDecisionPendingIds.value.includes(gate.gateId)) return false
|
|
if ((gate.type === 'input' && option.kind === 'respond' && !response)
|
|
|| (gate.type !== 'input' && response)) return false
|
|
const bridge = workspace.value
|
|
if (!bridge?.decideHumanGate) return false
|
|
humanGateDecisionPendingIds.value = [...humanGateDecisionPendingIds.value, gate.gateId]
|
|
try {
|
|
const result = await bridge.decideHumanGate(gate.gateId, {
|
|
optionId: option.optionId,
|
|
...(response ? { response } : {}),
|
|
})
|
|
if (!['approved', 'rejected'].includes(result?.status)
|
|
|| result?.decision?.optionId !== option.optionId) {
|
|
throw new Error('HUMAN_GATE_DECISION_FAILED')
|
|
}
|
|
return true
|
|
} catch (error) {
|
|
showError(error)
|
|
return false
|
|
} finally {
|
|
humanGateDecisionPendingIds.value = humanGateDecisionPendingIds.value
|
|
.filter(gateId => gateId !== gate.gateId)
|
|
}
|
|
}
|
|
|
|
const roundProgressPercent = computed(() => {
|
|
const bounded = Math.max(1, Math.min(10, Number(maxRounds.value) || 1))
|
|
return `${((bounded - 1) / 9) * 100}%`
|
|
})
|
|
const conversationActions = useConversationActions({
|
|
activeGroup,
|
|
activeRun,
|
|
activeView,
|
|
closeModal,
|
|
conversationHeader,
|
|
directGroupsFor,
|
|
formError,
|
|
groupName,
|
|
isDirectCreationPending,
|
|
isGroupRunning,
|
|
modal,
|
|
openAgentManager,
|
|
preloadAgentSkills,
|
|
readyAgents,
|
|
saving,
|
|
selectedGroupId,
|
|
selectGroup,
|
|
sending,
|
|
setDirectCreationPending,
|
|
setSidebarAgentExpanded,
|
|
showError,
|
|
snapshot,
|
|
t,
|
|
translateError,
|
|
workspace,
|
|
workspaceModalContent,
|
|
})
|
|
const {
|
|
cancelInlineTitleEdit,
|
|
defaultDirectory,
|
|
deleteArmed,
|
|
openDirect,
|
|
openNewGroup,
|
|
settingsIntent,
|
|
sidebarDeleteGroupId,
|
|
sidebarDeletePopoverPoint,
|
|
} = conversationActions
|
|
const {
|
|
composerController,
|
|
conversationHeaderController,
|
|
timelineController,
|
|
} = createConversationControllers({
|
|
agentCatalog,
|
|
agentSkills,
|
|
app: {
|
|
activeGroup,
|
|
activeRun,
|
|
canSendMessage,
|
|
compactPath,
|
|
conversationPermissionLabel,
|
|
emptyShowcaseIndex,
|
|
groupName,
|
|
handleMessageScroll,
|
|
maxRounds,
|
|
messageScroller,
|
|
productWordmark,
|
|
requestUnlimitedRounds,
|
|
roundProgressPercent,
|
|
roundSettingsControl,
|
|
roundSettingsOpen,
|
|
saving,
|
|
sendMessage,
|
|
sending,
|
|
scrollToLatest,
|
|
showScrollToLatest,
|
|
shortcutDefinitions,
|
|
shortcutMenuOpen,
|
|
stopRun,
|
|
t,
|
|
theme,
|
|
translateSystemMessage,
|
|
unlimitedRounds,
|
|
},
|
|
attachments: attachmentController,
|
|
composerContext,
|
|
conversationActions,
|
|
knowledgeBase: knowledgeBaseSettings,
|
|
messageActions,
|
|
timeline: conversationTimeline,
|
|
})
|
|
Object.assign(timelineController, {
|
|
decideHumanGate,
|
|
directHumanGates,
|
|
directRunBudget,
|
|
humanGateDecisionPendingIds,
|
|
})
|
|
|
|
const modalTitle = computed(() => ({
|
|
'new-group': t('group.newTitle'),
|
|
settings: settingsIntent.value === 'rename'
|
|
? t(activeGroup.value?.conversationType === 'direct' ? 'settings.renameDirectTitle' : 'settings.renameGroupTitle')
|
|
: t('settings.title'),
|
|
'custom-agent': t('customAgent.title'),
|
|
'unlimited-confirm': t('composer.unlimitedConfirmTitle'),
|
|
'agent-detail': selectedAgentDetail.value?.label || t('systemSettings.openAgentDetailDefault'),
|
|
})[modal.value] || '')
|
|
const modalSubtitle = computed(() => ({
|
|
settings: groupName(activeGroup.value),
|
|
'custom-agent': t('customAgent.subtitle'),
|
|
'agent-detail': agentDetailSkillSummary.value,
|
|
})[modal.value] || '')
|
|
|
|
const {
|
|
systemSettingsController,
|
|
workspaceModalController,
|
|
workspaceSidebarController,
|
|
} = createWorkspaceControllers({
|
|
agentCatalog,
|
|
agentManagement,
|
|
agentRefresh,
|
|
agentSkills,
|
|
app: {
|
|
activeGroup,
|
|
activeView,
|
|
closeModal,
|
|
cloudAgentBridges,
|
|
confirmUnlimitedRounds,
|
|
contentInteractionBlocked,
|
|
customAgentDeleteArmed,
|
|
discoveringAgents: agentDiscoveryPending,
|
|
focusedAgentKind,
|
|
formError,
|
|
goHome,
|
|
groupName,
|
|
handleSidebarAgentMain,
|
|
installConfirmKind,
|
|
installerState,
|
|
isGroupAwaitingDecision,
|
|
isGroupRunning,
|
|
modal,
|
|
openAgentManager,
|
|
openSystemSettings,
|
|
productMark,
|
|
refreshing,
|
|
saving,
|
|
selectGroup,
|
|
selectedGroupId,
|
|
selectSystemSettingsSection,
|
|
setThemePreference,
|
|
sidebarAgentsCollapsed,
|
|
sidebarGroupsCollapsed,
|
|
sidebarCollapsed,
|
|
sidebarDeleteGroup,
|
|
sidebarDeletePopoverStyle,
|
|
systemSettingsSection,
|
|
t,
|
|
theme,
|
|
themePreference,
|
|
toggleLocale,
|
|
toggleTheme,
|
|
},
|
|
collapsedGroupMenu: collapsedGroupMenuController,
|
|
conversationActions,
|
|
conversationNavigation,
|
|
knowledgeBase: knowledgeBaseSettings,
|
|
timeline: conversationTimeline,
|
|
})
|
|
|
|
useAppWindowInteractions({
|
|
closeCollapsedGroupMenu,
|
|
closeModal,
|
|
closeTracePanel,
|
|
collapsedGroupMenu,
|
|
collapsedGroupMenuButton,
|
|
collapsedGroupMenuOpen,
|
|
completeOnboarding,
|
|
windowTitlebar,
|
|
customAgentDeleteArmed,
|
|
deleteArmed,
|
|
messageDeleteArmedId,
|
|
modal,
|
|
onboardingVisible,
|
|
openNewGroup,
|
|
openSystemSettings,
|
|
roundSettingsControl,
|
|
roundSettingsOpen,
|
|
selectGroup,
|
|
selectedGroupId,
|
|
shortcutMenuOpen,
|
|
sidebarCollapsed,
|
|
sidebarDeleteGroupId,
|
|
skillMenuOpen,
|
|
snapshot,
|
|
sortByUpdated,
|
|
tracePanelOpen,
|
|
trapOverlayFocus,
|
|
})
|
|
|
|
const { booting, bridgeMissing } = useDesktopWorkspaceLifecycle({
|
|
afterInitialLoad: () => {
|
|
if (!onboardingCompleted.value && hasPersistedWorkspaceActivity(snapshot.value)) {
|
|
completeOnboarding({ fromHistory: true })
|
|
}
|
|
agentDiscoveryPending.value = true
|
|
const discovery = !onboardingCompleted.value ? openOnboarding() : refreshAgents()
|
|
void discovery.finally(() => { agentDiscoveryPending.value = false })
|
|
void loadKnowledgeBaseStatuses()
|
|
},
|
|
beforeBoot: () => {
|
|
applyTheme(theme.value)
|
|
setCustomAgentProfiles([])
|
|
setCloudAgentProfiles([])
|
|
},
|
|
defaultDirectory,
|
|
handleOpenGroup,
|
|
handleRunFinished,
|
|
installer,
|
|
installerState,
|
|
refreshAgents,
|
|
showError,
|
|
snapshot,
|
|
workspace,
|
|
})
|
|
|
|
function conversationPermissionLabel(group) {
|
|
const kinds = Array.isArray(group?.agentKinds) ? group.agentKinds : []
|
|
const enforced = kinds.length && kinds.every(kind => READ_ONLY_ENFORCED_AGENT_KINDS.has(kind))
|
|
if (!enforced) return t('conversation.agentManagedPermissions')
|
|
return group?.allowWrite ? t('conversation.writeEnabled') : t('conversation.readOnly')
|
|
}
|
|
|
|
function compactPath(path) {
|
|
const value = String(path || '')
|
|
if (value.length <= 42) return value
|
|
const parts = value.split(/[\\/]/).filter(Boolean)
|
|
return parts.length >= 2 ? `.../${parts.slice(-2).join('/')}` : `${value.slice(0, 38)}...`
|
|
}
|
|
|
|
function openAgentManager(kind = '') {
|
|
return systemSettingsNavigation.openAgentManager(kind)
|
|
}
|
|
|
|
function openSystemSettings(section = 'agents', kind = '') {
|
|
return systemSettingsNavigation.openSystemSettings(section, kind)
|
|
}
|
|
|
|
function selectSystemSettingsSection(section) {
|
|
return systemSettingsNavigation.selectSystemSettingsSection(section)
|
|
}
|
|
|
|
function requestUnlimitedRounds() {
|
|
if (unlimitedRounds.value) return
|
|
const groupId = roundSettingsGroupId()
|
|
if (!groupId) return
|
|
unlimitedConfirmationGroupId.value = groupId
|
|
modal.value = 'unlimited-confirm'
|
|
}
|
|
|
|
function confirmUnlimitedRounds() {
|
|
const groupId = unlimitedConfirmationGroupId.value
|
|
if (
|
|
!groupId
|
|
|| groupId !== roundSettingsGroupId()
|
|
|| !snapshot.value.groups.some(group => group.id === groupId)
|
|
) {
|
|
closeModal()
|
|
return
|
|
}
|
|
unlimitedRounds.value = true
|
|
closeModal()
|
|
}
|
|
|
|
function roundSettingsGroupId(group = activeGroup.value) {
|
|
if (!group || group.conversationType === 'direct') return ''
|
|
return String(group.id || '')
|
|
}
|
|
|
|
function persistRoundSettings(groupId = roundSettingsGroupId()) {
|
|
if (!groupId) return
|
|
roundSettingsByGroupId.set(groupId, {
|
|
maxRounds: Math.max(1, Math.min(10, Number(maxRounds.value) || DEFAULT_MAX_ROUNDS)),
|
|
unlimitedRounds: unlimitedRounds.value === true,
|
|
})
|
|
}
|
|
|
|
function restoreRoundSettings(group = activeGroup.value) {
|
|
const groupId = roundSettingsGroupId(group)
|
|
const saved = groupId ? roundSettingsByGroupId.get(groupId) : null
|
|
maxRounds.value = Math.max(1, Math.min(10, Number(saved?.maxRounds) || DEFAULT_MAX_ROUNDS))
|
|
unlimitedRounds.value = saved?.unlimitedRounds === true
|
|
}
|
|
|
|
function closeModal(options = {}) {
|
|
if (saving.value && options?.force !== true) return false
|
|
if (!modal.value) return false
|
|
modal.value = ''
|
|
settingsIntent.value = 'settings'
|
|
formError.value = ''
|
|
deleteArmed.value = false
|
|
customAgentDeleteArmed.value = false
|
|
installConfirmKind.value = ''
|
|
focusedAgentKind.value = ''
|
|
unlimitedConfirmationGroupId.value = ''
|
|
resetAgentDetailSkills()
|
|
return true
|
|
}
|
|
|
|
watch(() => snapshot.value.messages.map(message => message.id).join('\u0000'), () => {
|
|
flushPendingRunFinishedEvents()
|
|
if (
|
|
messageDeleteArmedId.value
|
|
&& !snapshot.value.messages.some(message => message.id === messageDeleteArmedId.value)
|
|
) {
|
|
messageDeleteArmedId.value = ''
|
|
}
|
|
})
|
|
watch([maxRounds, unlimitedRounds], () => {
|
|
persistRoundSettings()
|
|
})
|
|
watch(activeGroupMemberSignature, (value, previous) => {
|
|
const currentGroupId = String(value || '').split('\u0000')[0]
|
|
const previousGroupId = String(previous || '').split('\u0000')[0]
|
|
if (
|
|
modal.value === 'unlimited-confirm'
|
|
&& unlimitedConfirmationGroupId.value !== currentGroupId
|
|
) closeModal()
|
|
if (previousGroupId && previousGroupId !== currentGroupId) persistRoundSettings(previousGroupId)
|
|
restoreRoundSettings(activeGroup.value)
|
|
composerContextVersion.value += 1
|
|
const abandonedAttachments = composerAttachments.value
|
|
const group = activeGroup.value
|
|
cancelInlineTitleEdit({ restoreFocus: false })
|
|
messageDeleteArmedId.value = ''
|
|
clearActiveTurn()
|
|
roundSettingsOpen.value = false
|
|
stashComposerDraft(previousGroupId)
|
|
resetComposerContext(group)
|
|
restoreComposerDraft(currentGroupId)
|
|
pruneComposerDrafts(snapshot.value.groups.map(item => item.id))
|
|
composerAttachments.value = []
|
|
void discardAttachments(abandonedAttachments)
|
|
})
|
|
// A deleted conversation takes its unsent text with it.
|
|
watch(() => snapshot.value.groups.map(group => group.id).join('\u0000'), () => {
|
|
pruneComposerDrafts(snapshot.value.groups.map(group => group.id))
|
|
})
|
|
watch(activeRun, (value) => {
|
|
if (!value) return
|
|
messageDeleteArmedId.value = ''
|
|
roundSettingsOpen.value = false
|
|
cancelInlineTitleEdit({ restoreFocus: false })
|
|
})
|
|
watch(discussionMode, (value) => {
|
|
if (value !== 'auto') roundSettingsOpen.value = false
|
|
})
|
|
watch(
|
|
[draft, () => composerAttachments.value.length, activeGroupMemberSignature],
|
|
scheduleComposerResize,
|
|
{ flush: 'post' },
|
|
)
|
|
watch(activeRunTopicSignature, async (value) => {
|
|
if (!value) return
|
|
await focusRunTopic()
|
|
await scrollToLatest({ force: true })
|
|
})
|
|
watch(readyAgentSignature, (value) => { if (value) void loadAgentSkillStats() })
|
|
watch(() => installerState.value.phase, (phase, previous) => {
|
|
if (phase === 'completed' && previous !== 'completed') void refreshAgents()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
composerContextVersion.value += 1
|
|
const abandonedAttachments = composerAttachments.value
|
|
composerAttachments.value = []
|
|
void discardAttachments(abandonedAttachments)
|
|
disposeComposerContext()
|
|
disposeAgentSkills()
|
|
clearPendingRunFinishedEvents()
|
|
})
|
|
</script>
|